На предыдущем занятии мы с вами увидели, что указатель на массив и обычный
указатель – это практически одно и тоже. Через любой дополнительный указатель
на массив мы также можем обращаться к любому его элементу, используя операцию
квадратные скобки [], записывать или считывать оттуда данные. Мало того, при
необходимости, мы даже можем объявить обычный указатель так, чтобы его адрес
нельзя было менять в процессе работы программы. Ровно так, как и указатель на
массив. Сделать это возможно с помощью ключевого слова const, которое пришло
в язык Си из языка С++ и поддерживается стандартном С99.
Давайте
посмотрим на возможности ключевого слова const. Довольно часто
в программах можно увидеть объявления указателей в виде:
Что оно означает?
Первое, что приходит на ум, это объявление константного указателя, то есть,
указателя, который не может менять значение своего адреса. Но это не так. В
данной конструкции ключевое слово const означает, что
через указатель ptr_ar нельзя менять значения в ячейках
памяти. А вот адрес, как раз, мы вполне можем менять. Например:
ptr_ar = ar;
ptr_ar[0] = 10; // ошибка, т.к. ptr_ar определен с const
На чтение
значений никаких ограничений нет:
В дальнейшем мы
можем изменить адрес указателя ptr_ar:
и прочитать
следующее значение из массива:
То есть,
ключевое слово const в представленной записи задает лишь
ограничение на запись значений, но не на считывание.
Но, что если нам
нужно объявить именно константный указатель, то есть, с неизменным начальным
адресом? Для этого ключевое слово const следует
прописывать после указания типа следующим образом:
short * const ptr_ar = 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 = 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 – это,
по-прежнему, обычная переменная с той лишь разницей, что мы не можем изменить
ее значение в процессе работы программы. Неправильно воспринимать ее как
константу. На уровне машинных кодов – это такая же переменная, как и любая
другая. А ключевое слово 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];