ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему
Причин для этого несколько.
Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме
Любая статья является изложением знаний в общем случае. У многих людей мышление устроено так, что прочтя на форуме конкретный вопрос и конкретный ответ на этот вопрос, у них появится бОльшее понимание, чем после прочтения теоретических выкладок (даже если они подкреплены конкретными примерами). Ссылки на такие обсуждения я, как правило, включаю в последний раздел статьи.
Начинающие, как правило, поиск ответов на свои вопросы ведут именно в форуме, а не в блогах. А потому конкретный вопрос и конкретный ответ для них будет более удобным и полезным именно на форуме. Многие люди умеют работать методом тыка, лишь бы был конкретный пример в качестве образца. А потому такое обсуждение будет им полезным даже без прочтения статьи
Исторически сложилось, что раньше (когда ещё не было блога) статьи располагались на форуме и представлены были в виде двух тем. Первая тема создавалась в специально отведённой свалке и представляла собой черновик, который со временем дорабатывался до законченной статьи. После этого статья переезжала во вторую тему в тематическом разделе. А первая тема оставалась дополнительной свалкой для замечаний и мелких вопросов по теме. Ссылку на старое местоположение данной свалки я помещаю в начале статьи. Вопросы, по возможности, прошу создавать в отдельных темах, но если вопрос действительно мелкий, то можно его задать и в указанной свалке.
1. Общие сведения
Неформальное объяснение действия квалификатора const состоит в том, что квалификатор действует на тип-указатель (а точнее, на звёздочку) или переменную, которые написаны справа от квалификатора.
2. Переменная, которую нельзя модифицировать
Справа от const находится переменная. Означает, что константной является переменная, т.е. в неё записывать нельзя. Однако формально с точки зрения языка мы имеем переменную "a" с типом "const int" (а не константную переменную "a" с типом "int"). Это означает, что для конструкции
компилятор выругается на неявное преобразование, поскольку переменная b имеет тип "указатель на int", а выражение "&a" имеет тип "указатель на const int". По умолчанию такое преобразование считается опасным, т.к. по языку через "b" можно будет сделать запись в "a". Поэтому компилятор пропустит такой код только с явным преобразованием
а после любого явного преобразования указателей компилятор снимает с себя всякую ответственность за неправильный код типа
| C | 1
2
3
4
5
6
7
8
| const int a = 5;
int *b = (int*) &a;
...
/* Здесь мы изменили значение переменной a, которую описали с квалификатором
* const. В случае работы с оптимизациями компилятор может нижеидущий
* код "x=a" заменить на "x=5", поскольку "a" описано как немодифицируемая переменная */
*b = 6;
int x = a; |
|
Ну и следует упомянуть, что зачастую пишут
что является эквивалентной записью, поскольку, как я уже говорил, неформально квалификатор const действует ТОЛЬКО на типы-указатели и переменные. Т.е. const по сути действует на "a", поскольку "int" не является типом-указателем
3. Указатель, по которому нельзя модифицировать
или, как чаще пишут
В данном случае const действует на тип-указатель "int*" (т.е. const действует на звёздочку, которая относится к int). В итоге мы имеем переменную "c", которая имеет тип "const int*", что есть "указатель на const int". Конструкция означает, что мы имеем указатель, по которому нельзя ничего модифицировать и который указывает на немодифицируемую память
При этом с указателями const есть некий тонкий момент. Программист обязан сам следить за тем, чтобы во время жизни переменной с типом "указатель на const" он всегда смотрел на немодифицируемые участки памяти. С точки зрения синтаксиса следующий код является коректным
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const int *c;
int d;
/* На такую конструкцию компилятор НЕ должен ругаться, т.к.
* с точки зрения поинтерных записей конструкция опасной не является,
* т.е. через указатель "c" мы не можем модифицировать
* значение переменной "d" */
c = &d;
...
int x = *c;
...
/* В этом месте код формально становится некорректным, т.к. компилятор
* в режиме с оптимизациями имеет право нижеидущий код "y=*c"
* заменить на "y=x", поскольку "c" должно указывать на неизменяемую
* память */
d++;
...
int y = *c; |
|
но на исполнении в теории может повести себя не так, как ожидалось (зависит от компилятора)
4. Будьте внимательны при описании указателя на const
Как уже говорилось выше, неформально квалификатор const влияет на звёздочку или на имя переменной, стоящей справа от квалификатора. Очередная расхлябанность языка Си приводит к тому, что записи
и
являются эквивалентными. Во втором случае справа от const находится тип "int", на который const НЕ влияет (поскольку он влияет только на звёздочки и имена переменных), а потому const как бы относится только к имени переменной "a". Однако я предпочитаю первую форму записи. И вот почему. При работе с const нужно учитывать, что квалификатор влияет только на явные звёздочки. В записи
| C | 1
2
| typedef int* ptr_t;
const ptr_t a; |
|
квалификатор const относится к переменной "a", поскольку справа от const'а нет явных звёздочек. Однако если мы раскроем typedef текстовой заменой "ptr_t" на "int*", то мы получим неэквивалентную запись
поскольку const уже относится к указателю, а не переменной. Именно по этой причине я пишу "int const a;", а не "const int a;"
5. Краткие итоги
Чтобы итог всего этого дела был перед глазами, напишу всё в одном месте:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| /* Переменную нельзя модифицировать */
int const a;
/* Эквивалентно "int const a;" */
const int a;
/* Указатель модифицировать (a = ...) можно, но записывать
* по указателю (*a = ...) нельзя */
const int *a;
/* Эквивалентно "const int *a;" */
int const *a;
/* Указатель модифицировать (a = ...) нельзя, но записывать
* по указателю (*a = ...) можно */
int * const a;
/* Указатель модифицировать (a = ...) нельзя, записывать
* по указателю (*a = ...) нельзя */
const int * const a; |
|
Для полноты картины следует написать, что для ссылок в Си++ (не путать с указателями) квалификатор const работает по тому же принципу: действует на "&", находящийся справа от квалификатора. В случае ссылки прилеплять квалификатор к имени переменной смысла не имеет, потому как по языку ссылки и так нельзя модифицировать. Опять-таки для полноты картины: для квалификатора volatile работают все те же правила, что и для квалификатора const.
Вкратце можно сказать про три основных назначения квалификатора const:
- Уберечь от собственных ошибок на этапе компиляции. Понятно, что на этапе текстового разбора могут выявиться только ошибки, где идёт запись напрямую через переменную с квалификатором const (или типа которой есть указатель на const). Если же имеют место быть случаи типа того, что приведён в разделе 4.3, то тут уже ничто от ошибки не спасёт
- При передаче параметров в функции. Очень часто требуется в функцию передать аггрегатный (структура или массив, коим в том числе являются и строки Си) параметр. В случае, когда параметр в функции только читается, то можно поступить двумя способами. Передать по значению или передать по косвенности через const-указатель (или const-ссылку в Си++). В первом случае имеется неоптимальность работы кода, т.к. будет копироваться объект большого размера. Во втором случае код будет более быстрый, потому что будет передаваться только адрес объекта, но квалификатор const возле параметра-указателя подскажет программисту о том, что внутри функции записи в объект не произойдёт. Здесь надо сделать оговорку, что внутри функции программист кривыми руками может написать код типа того, что приведено в конце раздела 4.3. В случае работы с массивом в языках Си\Си++ вообще нет никакой возможности передавать их по значению, а потому там работа идёт только по косвенности
- Оптимизация кода компилятором. Особенно это касается случаев, когда указатель на объект передаётся в функцию, но в случае параметра в виде const-указателя компилятор понимает, что внутри функции объект не меняется. Однако в реальной жизни оказалось так, что программисты слишком часто пишут неправильные коды (по типу того, что приведён в разделе 4.3), поэтому многие современные компиляторы по умолчанию НЕ делают никаких оптимизаций, связанных с указателями на const, а делают только по специальным опциям типа "доверять квалификаторам const". Либо делают оптимизации, но имеют опцию "НЕ доверять квалификаторам const". Как правило это требуется для сборки старого софта, который писался во времена, когда машины были слабыми, памяти мало, а потому в компиляторах агрессивных оптимизаций не проводилось и такие ошибки не вскрывались
6. Ссылки на темы, где обсуждался данный вопрос
|