Ключевое слово const с указателями и переменными

Практический курс по C/C++: https://stepik.org/course/193691

На предыдущем занятии мы с вами увидели, что указатель на массив и обычный указатель – это практически одно и тоже. Через любой дополнительный указатель на массив мы также можем обращаться к любому его элементу, используя операцию квадратные скобки [], записывать или считывать оттуда данные. Мало того, при необходимости, мы даже можем объявить обычный указатель так, чтобы его адрес нельзя было менять в процессе работы программы. Ровно так, как и указатель на массив. Сделать это возможно с помощью ключевого слова const, которое пришло в язык Си из языка С++ и поддерживается стандартном С99.

Давайте посмотрим на возможности ключевого слова const. Довольно часто в программах можно увидеть объявления указателей в виде:

const short *ptr_ar;

Что оно означает? Первое, что приходит на ум, это объявление константного указателя, то есть, указателя, который не может менять значение своего адреса. Но это не так. В данной конструкции ключевое слово const означает, что через указатель ptr_ar нельзя менять значения в ячейках памяти. А вот адрес, как раз, мы вполне можем менять. Например:

ptr_ar = ar;
ptr_ar[0] = 10;   // ошибка, т.к. ptr_ar определен с const

На чтение значений никаких ограничений нет:

short x = ptr_ar[0];

В дальнейшем мы можем изменить адрес указателя ptr_ar:

ptr_ar++;

и прочитать следующее значение из массива:

short x2 = *ptr_ar;

То есть, ключевое слово const в представленной записи задает лишь ограничение на запись значений, но не на считывание.

Но, что если нам нужно объявить именно константный указатель, то есть, с неизменным начальным адресом? Для этого ключевое слово const следует прописывать после указания типа следующим образом:

short * const ptr_ar = ar;

Тогда ему можно присвоить начальный адрес только в момент инициализации. Обычная операция присваивания уже недопустима:

ptr_ar = ar;

Также недопустимы все остальные операции изменения адреса, например:

ptr_ar++;

А вот изменение значений и считывание вполне возможно:

ptr_ar[0] = 10;
short x2 = ptr_ar[1];

Надо сказать, что ключевое слово const в таком качестве в реальной практике программирования почти никогда не встречается. Я привел эту запись исключительно в образовательных целях, чтобы вы знали, как ее понимать, если где-нибудь встретите. Чаще всего const прописывают в самом начале, чтобы запретить изменение данных через указатель.

Давайте немного глубже посмотрим на работу ключевого слова const, чтобы правильно понимать, что же оно в действительности делает. Объявим в программе массив и два указателя на него:

#include <stdio.h>
 
int main(void) 
{
         short ar[] = {4, 3, 2, 1, 5, 6, 7};
         const short * ptr_1 = ar;
         short * ptr_2 = ar;
 
         return 0;
}

Как видите, первый указатель объявлен с ключевым словом const, а второй без него. Соответственно, с помощью первого указателя можно только читать данные из массива, а с помощью второго и читать и менять. Например:

ptr_2[0] = 10;
int a = ptr_1[0];

Смотрите, мы изменили значение первого элемента массива и это изменение было прочитано с помощью первого указателя ptr_1. То есть, ключевое слово const накладывает ограничения не на уровне ячеек памяти, а на уровне указателя, у которого оно прописано. В действительности, этот const учитывается только компилятором языка Си. На уровне машинных кодов и первый и второй указатели идентичны. Именно компилятор при трансляции программы контролирует корректность использования указателя с ключевым словом const. В данном случае, компилятор контролирует, чтобы не происходило записи в ячейки памяти с помощью указателя ptr_1. Если такое где-либо встречается, то компиляция прерывается и выдается ошибка. Если же указатель ptr_1 в программе используется только для чтения, то программа успешно переводится в машинные коды.

Соответственно, чтобы нельзя было «схитрить» и обойти константное объявление указателя, язык Си накладывает некоторые ограничения на его использование. В частности, нельзя обычному указателю присвоить адрес константного указателя:

short * ptr_2 = ptr_1;

А вот наоборот можно:

short * ptr_2 = ar;
const short * ptr_1 = ptr_2;

Часто ключевое слово const можно встретить при объявлении глобальных массивов из констант, например:

#include <stdio.h>
 
const int marks[] = {1, 2, 3, 4, 5};
 
int main(void) 
{
         // программа, использующая массива marks
         return 0;
}

Либо при определении параметров функции, как, например, это сделано в функции printf():

int printf(const char *format, ...);

Ключевое слово const с переменными

В заключение этого занятия пару слов о возможности использования ключевого слова const при объявлении обычных переменных. В целом, здесь все то же самое. Если записать прописать const в самом начале:

#include <stdio.h>
 
int main(void) 
{
         const int code = 13;
 
         return 0;
}

то переменную можно только инициализировать, но нельзя присваивать ей какие-либо значения:

code = 15;

То есть, операция присваивания становится недопустимой для таких переменных. Только операция инициализации.

И вот здесь обратите внимание, что переменная code – это, по-прежнему, обычная переменная с той лишь разницей, что мы не можем изменить ее значение в процессе работы программы. Неправильно воспринимать ее как константу. На уровне машинных кодов – это такая же переменная, как и любая другая. А ключевое слово const прописано исключительно для компилятора. В процессе трансляции он будет контролировать, чтобы переменная не меняла свое значение. То есть, const – это просто указание для компилятора, в машинные коды оно никак не переводится.

В связи с этим, константные переменные следует все же рассматривать как переменные. Например, их по-прежнему, нельзя использовать в метках case условного оператора switch:

#include <stdio.h>
 
int main(void) 
{
         const int code = 13;
         int item = 1;
 
         switch(item) {
         case code:
                   printf("error");
         }
 
         return 0;
}

Так как на момент компиляции программы значение переменной code не определено. Число 13 ей будет присвоено только при выполнении программы и размещения переменной code в памяти устройства. Правда, такие переменные допускается использовать при объявлении массивов:

const int code = 13;
char str[code];

Здесь включается механизм создания массивов переменной длины на основе переменных. Делать так крайне не рекомендуется. Машинный код программ, использующие такие объявления, может получаться крайне неэффективным, а значит, сводит на нет главное достоинство языка Си – скорость исполнения программы и полный контроль за использованием ресурсов. Поэтому такую конструкцию лучше заменить на классическую:

#define SIZE        13
char str[SIZE];

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме