Программирование PC Ключевое слово const в языке C++ Thu, March 28 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Ключевое слово const в языке C++ Печать
Добавил(а) microsin   

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

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

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

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

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

const int Constant1=96;

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

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

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

const int * Constant2

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

int const * Constant2

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

int * const Constant3

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

int const * const Constant4

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

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

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

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

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

char *Function1()
{ return "Какой-то текст";}

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

Function1()[1]='a';

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

const char *Function1()
{ return "Какой-то текст";}

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

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

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

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

void Subroutine1(int Parameter1)
{ printf("%d", Parameter1);}

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

void Subroutine2(int Parameter1)
{ Parameter1=96;}

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

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

void Subroutine3(int &Parameter1)
{ Parameter1=96;}

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

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

void Subroutine4(int *Parameter1)
{ *Parameter1=96;}

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

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

void Subroutine4(big_structure_type &Parameter1);

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

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

void Subroutine4(big_structure_type const &Parameter1);

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

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

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

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

class Class1
{ void Method1();
  int MemberVariable1;}

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

void Class1::Method1()
{ MemberVariable1=MemberVariable1+1;}

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

class Class2
{ void Method1() const;
  int MemberVariable1;}

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

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

const int*const Method3(const int*const&)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' виртуально бесполезным.

[Ссылки]

1. The C++ 'const' Declaration: Why & How site:duramecho.com.
2. Определения "char const *" и "const char *" - в чем разница?
3. Использование описателя const в C.

 

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


Защитный код
Обновить

Top of Page