Какое значение имеет служебное слово const
Перейти к содержимому

Какое значение имеет служебное слово const

Многоликий const

Ключевое слово const — одно из самых многозначных в C++. Правильно использование const позволяет организовать множество проверок ещё на этапе компиляции и избежать многих ошибок из числа тех, которые бывает трудно найти при помощи отладчиков и/или анализа кода.

Первая половина заметки рассчитана скорее на начинающих (надеюсь мнемоническое правило поможет вам запомнить, где и для чего используется const), но, возможно, и опытные программисты смогут почерпнуть интересную информацию о перегрузке методов по const.

Константы и данные

Самый простой случай — константные данные. Возможно несколько вариантов записи:

Все они правильные и делают одно и тоже — создают переменную, значение которой изменить нельзя.

Константы и указатели

При использовании const с указателями, действие модификатора может распространяться либо на значение указателя, либо на данные на которые указывает указатель.

Работает (const относится к данным):

Тоже самое и тоже работает:

А вот это уже не работает:

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

то ситуация была бы диаметрально противоположной.

Существует мнемоническое правило, позволяющее легко запомнить, к чему относится const. Надо провести черту через «*», если const слева, то оно относится к значению данных; если справа — к значению указателя.

Ну и конечно, const можно написать дважды:

Константы и аргументы/результаты функций

C функциями слово const используется по тем же правилам, что при описании обычных данных.

Константы и методы (перегрузка)

А вот с методами есть одна тонкость.

Во-первых, для методов допустимо использование const, применительно к this. Синтаксис таков:

Кроме того, этот const позволяет перегружать методы. Таким образом, вы можете писать оптимизированные варианты методов для константных объектов.

И ещё… я собрался в отпуск… возможно, не смогу ответить на комментарии до понедельника-вторника. Не сочтите за невнимание 🙂

Какое значение имеет служебное слово const

Подсистема const одна из самых грязных фич языка C++.

Вроде бы концепция должна быть простой: переменные, декларированные с модификатором const становятся константами, т. е. объектами, которые не может изменить программа. Однако этот термин используется как костыль для починки недостающих возможностей C++, и здесь смысл получается неприятно сложным и иногда разочаровывающе строгим. Далее будут сделаны попытки объяснить, как используется ключевое слово ‘const’, и почему оно вообще существует (перевод статьи [1]).

[Простое использование const]

Простейшее использование const — для декларирования именованной константы. Эта фича C++ была унаследована от языка C.

Чтобы создать декларацию константы, слева от её имени добавляется ключевое слово const. Конечно же, необходимо сразу же инициализировать значение в конструкторе (в момент декларации константы), потому позже по правилам языка мы не имеем права изменить константу. Например,

Создаст целочисленную константу, из-за отсутствия воображения названную как ‘Constant1’, со значением 96.

Такие константы полезны для параметров, которые используются в программе, если их не надо изменять после того, как программа скомпилирована. Достоинство const по сравнению с директивой препроцессора языка C ‘#define’ в том, что компилятор знает о типе константы, и не просто подставляет в нужное место программы нужный текст. Таким образом, при компиляции может быть сделана дополнительная проверка типа, и если что-то не так, то сообщения об ошибке несоответствия типа могут пригодиться.

Это также работает для указателей, но нужно быть осторожным в определении места расположения ключевого слова const, поскольку это повлияет на то, что именно будет константой — или сам указатель будет константой, или то место будет константой, на которое указатель указывает. Это вводит некоторую путаницу, но разобраться просто, если помнить, что const применяется к тому элементу выражения, слева от которого стоит. Например, выражение

декларирует указатель именем Constant2, который указывает на какое-то постоянное значение типа int. Т. е. сам указатель изменять можно, но значение, на которое указывает указатель, изменять нельзя. Вот альтернативная запись того же самого (подробнее см. [2]):

А это выражение:

декларирует, что Constant3 является постоянным указателем на некоторую переменную int. Т. е. значение указателя будет всегда неизменным, а значение переменной, на которую указывает указатель, менять можно. И следующее выражение:

декларирует, что Constant4 является постоянным указателем, указывающим на постоянный int. Просто запомните, что ‘const’ прикладывается непосредственно к объекту справа от const (кроме случая, когда справа от const нет ничего; тогда const прикладывается непосредственно к тому, что слева от него).

[Использование const в возвращаемых значениях функции]

Из возможных комбинаций указателей и const, когда постоянный указатель указывает на переменную. Это полезно для ячеек хранения, которые можно изменить, но их положение в памяти (адрес) не меняется.

Еще полезнее указатель (постоянный или нет), указывающий на ‘const’ значение. Это полезно для возврата постоянных строк и массивов из функции, потому что эти строки и массивы реализованы как указатели, и иначе программа могла бы их случайно попытаться изменить и разрушить. Вместо трудной задачи найти место сбоя в программе, попытка изменить неизменяемое значение будет обнаружена еще во время компиляции.

Например, если функция, возвращающая постоянную строку ‘Some text’ написана так:

то программа потерпит сбой, если случайно поменяет значение примерно так:

В то время как компилятор сразу нашел бы ошибку, если бы функция была написана с ключевым словом const:

потому что компилятор знает о том, что возвращенное функцией значение поменять нельзя.

[Когда это становится «грязным» — в передаче параметра]

Когда подпрограмма или функция вызывается с параметрами, то переменные, переданные как параметры, могут быть использованы не только для передачи данных в функцию, но еще и для целей возврата обработанных данных из функции. Некоторые языки разрешают такую фичу специальным синтаксисом, как например добавлением типов параметров ‘in:’, ‘out:’ и ‘inout:’, в то время как на языке C нужно работать на более низком уровне, и указывать метод передачи переменных, выбрав такой, который разрешает нужное направление перемещения данных.

Например, подпрограмма наподобие такой:

принимает целочисленный параметр Parameter1 способом по умолчанию для языков C и C++ — в подпрограмму передается копия передаваемого объекта. Таким образом, подпрограмма может прочитать значение переданной переменной, но подпрограмма никак не может повлиять на этоу переменную, потому что все изменения переданного параметра внутри функции будут производится на копии, и эти изменения потеряются, когда функция завершит свою работу. Например, если подпрограмма написана так:

то по завершению работы функции переданная в параметре переменная останется неизмененной, она не будет установлена в значение 96.

Добавление символа ‘&’ к имени параметра в C++ (очень запутывающий синтаксис, потому что & перед именем переменной в коде программы генерируют в языке C указатели!) приведет к тому, что в подпрограмму (или функцию) будет передана не копия переменной, а ссылка на неё. Т. е. внутри функции будет использоваться сама переменная, так что функция может её поменять. Таким образом, если подпрограмма написана так:

то она установит на выходе переданную переменную в значение 96. Этот метод передачи переменной не как копии называется на языке ‘ссылкой’ (reference).

Передача переменных по ссылке является дополнением языка C++, которого не было в C. Чтобы передать изменяемую переменную на оригинальном C, использовалась передача указателя на переменную. Тогда внутри тела функции появляется возможность поменять внешнюю по отношению к функции переменную, которая передана в параметре через указатель. Пример:

Это будет работать (в том числе и на C++), но синтаксис получается довольно громоздким.

Но как здесь может быть использовано ‘const’? Ну в общем есть второй способ передать данные по ссылке или указателю вместо копии. Достоинство этого второго метода — экономия памяти, когда копирование переменной заняло бы слишком много памяти. Особенно это полезно использовать вместе с большими типами переменных (например, с определенными пользователем структурами на C или классами на C++). Чтобы экономить память и не копировать передаваемые переменные, подпрограмма может быть декларирована так:

Но здесь возникает проблема, потому что использование ‘&’ может привести к нежелательному изменению переданной переменной — если она не должна быть изменена. Причем исправить ничего нельзя, если функция была скомпилирована в двоичную библиотеку. Возникает риск в необходимости доверять подпрограмме, что она случайно не поменяет внутри себя переданную переменную.

Чтобы устранить проблему, в списке параметров может быть использовано ключевое слово ‘const’. Например, если написать

то это заставит компилятор передать переменную как есть, не копируя, но кроме этого запретит её изменение в теле функции. Это выглядит грязным, потому что по сути искусственно превращает доступ к переменной на только на чтение, хотя цель этого всего состояла в трюке заставить компилятор выполнить некоторую оптимизацию по экономии памяти (обычно в стеке).

В идеале программист не должен был бы точно детализировать, как переменная передается, ему нужно было бы просто указать направление передачи данных, и оставить для самого компилятора задачу оптимизировать код. Но язык C был специально разработан в расчете на сырое, низкоуровневое программирование (C еще иногда называют «интеллектуальным ассемблером») — чтобы программы могли эффективно работать даже на намного менее мощных компьютерах, чем те, что распространены сейчас. Таким образом, программист все еще должен явно задавать такие моменты в своем коде.

[Грязнее грязного — в Объектно-Ориентированном Программировании]

В ООП вызов ‘метода’ (это теперь так называется функция в стиле ООП) объекта имеет дополнительное усложнение. Кроме доступа к переменным в списке параметров метода, метод имеет доступ к полям (или членам, т. е. переменным) самого объекта класса, которые всегда передаются непосредственно, не как копии. Например, простейший класс Class1, определенный так:

не имеет вообще никаких явных параметров для метода Method1, но вызов Method1 в этом классе мог бы поменять значение члена класса MemberVariable1. Например, так:

Решение состоит в том, чтобы добавить const после списка параметров:

Это запретит методу Method1 в классе Class2 изменять какие бы ни было члены в объекте этого класса.

И конечно, иногда нужно комбинировать некоторые из этих разных использований ключевого слова const, что может ввести в конфуз:

В этом примере целых 5 раз было использовано ключевое слово const, что означает следующее: переменная, на которую будет указывать возвращенный из метода Method3 указатель, менять нельзя. Нельзя также менять и сам возвращенный указатель. Причем переданный в метод указатель не будет изменен, как и значение, на который указывает, не может быть изменено, и Method3 не имеет права изменять поля своего класса!

[Неудобства от const]

Помимо запутывающего синтаксиса const, есть другие полезные фичи, которыми система может ограничивать действия выполняемой программы.

Кое-что в программировании на C++ раздражает, когда нужно оптимизировать программу по скорости. Например, метод, объявленный как const, не может даже внести изменения в скрытые части своего объекта, которые все равно были бы не видны снаружи. Это включает сохранение промежуточных результатов долгих вычислений, которые сохранили бы время обработки в последующих вызовах методов класса. Вместо этого нужно либо передавать эти промежуточные результаты обратно в вызывающий код, чтобы потом их передать снова в подпрограмму (что грязно) или все вычислять заново в каждом вызове (что не эффективно). В поздних версиях C++ было добавлено ключевое слово mutable, которое разрешает отменить ограничение ‘const’ для этой цели. Это возлагает ответственность на программиста — как он действительно будет использовать константы. Так что если Вы пишете программу, где есть какой-то класс, использующий mutable, то у Вас уже нет гарантии, что константы действительно останутся постоянными во всех случаях, так что ключевое слово виртуально становится бесполезным.

Нельзя просто избегать использования const в методах класса, потому что const заразная. Объект, который сделан как const, например будучи переданным как параметр способом ‘const &’, может вызывать только те из его методов, которые явно были декларированы как ‘const’ (потому что система вызовов C++ слишком проста, чтобы работать с такими методами, которые не объявлены явно как const, и они ничего фактически не изменили бы). Поэтому методы класса, которые не изменяют объект, лучше всего декларировать как ‘const’, чтобы не было препятствий для их вызова, когда объект класса так или иначе получил состояние ‘const’. В более поздних версиях C++ объект или переменная, которая была объявлена как ‘const’, может быть преобразована к изменяемой путем использования ‘const_cast’ которая является костылем, похожим на ‘mutable’, и снова делает ‘const’ виртуально бесполезным.

Переменные const , let и var

Переменные в JavaScript хранят значения, которыми оперирует код. Для создания переменных используются ключевые слова var , let и const .

Использование var оправданно, если нужно писать код для старых браузеров времён IE 11 или Opera mini. Во всех остальных случаях лучше использовать let и const , так как они не позволят допустить ошибки, приводящие к неправильным значениям в переменных или изменениям глобальных переменных.

Что такое переменные

Переменные — это именованные контейнеры для хранения данных.

Для создания переменной используется ключевое слово let , const или var . Сразу за ключевым словом идёт название переменной либо перечень переменных через запятую. Создание переменной также называют объявлением переменной. Например:

Чаще всего, при объявлении переменной ей устанавливают стартовое значение при помощи оператора присваивания = . Тип значения может быть абсолютно любым — строка, число, объект, массив и так далее.

Затем переменную можно использовать как заменитель значения в коде:

Правила именования переменных

Для имени переменной можно использовать следующие символы:

  • буквы латинского алфавита;
  • цифры;
  • символы $ и _ .

Первый символ не должен быть цифрой:

В качестве названий переменных нельзя использовать зарезервированные языком слова. Например: class , super , throw , yield , var , let , const и так далее. С полным списком таких слов можно ознакомиться здесь.

Создание переменных

Перед выполнением скрипта JavaScript находит код создания переменных и заранее создаёт их. Получается, что в начале выполнения скрипта все переменные, описанные в коде, уже объявлены. В зависимости от браузера, они могут быть равны undefined (в Chrome и Safari), либо, в случае с let и const в браузере Firefox, не равны ничему и иметь специальное состояние uninitialized :

Дебаггер Firefox, выполнение только началось, и все переменные уже объявлены, хотя код ещё не дошёл до выполнения нужных строк.

Код ещё не начал выполняться, а переменные уже объявлены.

В конце скрипта, после того как произошло присвоение стартовых значений, переменные равны 5 , 10 и 15 :

Дебаггер Firefox, выполнение дошло до строк с объявлением переменных и значения установлены.

JavaScript выполнил код объявления переменных и установил значения.

Получается, что некоторое время переменная может содержать значение undefined и быть доступной для чтения. Этим нужно пользоваться с осторожностью.

Переменные let и const

Переменные let и const появились в версии EcmaScript 2015 года (ES6), пришли на смену устаревшему var .

Объявление

Используя ключевое слово let , можно объявить переменную без присвоения ей начального значения. В таком случае она будет равна undefined :

При помощи const нельзя объявлять переменные без значения:

К переменным let и const нельзя обращаться до их объявления в коде:

У let и const есть так называемая temporal dead zone (TDZ) — момент выполнения скрипта до объявления переменной. Переменная может находиться и выше объявления, при условии, что содержащая её часть кода будет выполнена после инициализации:

TDZ есть также и у es6 -классов, несмотря на то, что они являются «синтаксическим сахаром» над обычными функциями.

А функции (объявленные как Function Declaration) TDZ не имеют.

Оба типа переменных имеют блочную область видимости и не становятся частью глобального объекта ( window в браузере, global в Node.js). Блочная область видимости не даёт получить значение переменной вне блока, где она была объявлена.

Если объявить переменные внутри блока if , то обращение к ним вне блока будет выбрасывать ошибку:

Одинаковые имена переменных

Объявление переменной с именем, которое уже используется в текущей области видимости, приведёт к ошибке:

То же правило работает и при использовании const , и при использовании смешанного подхода:

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

Смена значения в let и const

Значение в переменной, созданной через let , можно изменять:

Стартовое значение const изменить нельзя, будь то примитивное значение:

Или ссылка на объект:

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

Переменные var

Объявление переменных при помощи ключевого слова var было в JavaScript с первых версий.

Объявление

Переменные var можно объявлять без присвоения им значения, в таком случае они будут равны undefined :

Переменные, объявленные через var , имеют функциональную область видимости. Они доступны только в пределах текущей функции или глобального объекта, если функции нет:

Объявление переменной вне функций делает их глобальными переменными. Они доступны как свойства глобального объекта:

К переменным, объявленным при помощи ключевого слова var , можно обращаться до момента объявления. В отличие от let и const , ошибки это не вызовет. Такое поведение называется hoisting — «всплытие»:

Разберём, как работает функциональная область видимости:

Перед выполнением функции в глобальной области видимости присутствует переменная a , равная 5 :

Старт выполнения кода в дебаггере. Уже объявлена переменная

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

Во время выполнения функции формируется новая функциональная область видимости, в которой тоже присутствует переменная a . Эта переменная была объявлена с помощью var внутри функции, в момент выполнения которой она «всплыла» со значением равным undefined . В строке 4 происходит обращение именно к ней (до её объявления), а не к той, что находится вне функции.

Дебаггер, JavaScript начал выполнять функцию и не дошёл до объявления переменной, но по факту она уже объявлена.

В начале выполнения функции локальная переменная всплыла — JavaScript заранее объявил её, но ещё не установил значение.

В строке 8 значение переменной a уже равно 10.

Дебаггер, JavaScript выполнил строку с инициализацией локальной переменной.

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

После выполнения функции локальная область видимости была удалена. В консоли выводится глобальная переменная a .

Дебаггер, JavaScript выполнил весь код. Локальная переменная уничтожена, доступна только глобальная.

На последнем шаге локальная переменная уже недоступна, так как выполняется код из глобальной области видимости.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *