С помощью ключевого слова const
в C++ можно объявлять не только константы. Оно имеет много значений. И не лишним будем понимать каждое из них. Это позволяет писать более надежный код, указывая компилятору на ограничения, которые он должен проверять. Таким образом, любая попытка неправильного использования переменной или функции, объявленной с использованием const
, будет выявлена на самых ранних этапах.
const
Наиболее простой и интуитивно понятный вариант использования const
заключается в объявлении константных значений:
const int CONST_VAL = 0;
// CONST_VAL = 1; приводит к ошибке на этапе компиляции
Похожего результата можно добиться с помощью макросов:
#define CONST_VAL 0
Но вариант с макросом не столь хорош, поскольку не дает возможности указать тип переменной. Из-за возможных проблем с типизацией лучше пользоваться константами, а не макросами. Более того, макросов лучше и вовсе избегать. В C++ реализована прекрасная поддержка шаблонных функций и классов, которые в большинстве случаев представляют более надежную альтернативу. Однако стоит признать, что бывают случаи, когда макросы оказываются полезными. Но сейчас речь не о них.
Таким образом, если мы объявили переменную с const
, то значение должно быть присвоено сразу. Потом уже ничего не сделать. А что, если нам надо объявить константный вектор? В C++11 для этого предусмотрена специальная конструкция:
const std::vector< int > CONST_VECTOR = { 1, 2, 3, 4 };
А что, если мы по какой-то причине не может пользоваться C++11? И в этом случае можно легко объявить константный вектор:
std::vector< int > makeVector() {
std::vector< int > v;
v.push_back( 1 );
v.push_back( 2 );
v.push_back( 3 );
v.push_back( 4 );
return v;
}
const std::vector< int > CONST_VECTOR = makeVector();
То есть для сложных типов, которые не получается заполнить в одну строку, достаточно вынести компоновку в отдельную функцию для инициализации. С другой стороны, мы могли бы объявить вектор в функции makeVector()
с ключевым словом static
и вместо константы использовать саму функцию. Но это уже дело вкуса:
std::vector< int > makeVector() {
static std::vector< int > v;
if( v.empty() ) {
v.push_back( 1 );
v.push_back( 2 );
v.push_back( 3 );
v.push_back( 4 );
}
return v;
}
Константная ссылка объявляется схожим образом:
int x = 0;
const int& xRef = x; // то же самое: int const& xRef = x;
// xRef = 1; приводит к ошибке на этапе компиляции
Суть константных ссылок заключается в том, что мы получаем доступ к значению переменной, на которую сделана ссылка, но изменить ее не имеем права. Основное назначение константных ссылок заключается в передаче входных параметров функциям. Но об этом немного позже.
Для указателей существует три варианта использования const
:
int x = 0;
// Вариант 1
const int* xPtr1 = &x; // то же самое: int const* xPtr1 = &x;
xPtr1 = NULL; // имеем право изменить то, на что будет указывать указатель
// *xPtr1 = 1; но нельзя изменить значение переменной, на которую указываем
// Вариант 2
int* const xPtr2 = &x;
// xPtr2 = NULL; нельзя изменять то, на что будет указывать указатель
*xPtr2 = 1; // но можем менять значение переменной, на которую он указывает
// Вариант 3
const int* const xPtr3 = &x;
// xPtr3 = NULL; ничего нельзя менять
// *xPtr3 = 1;
В варианте 1 мы получили указатель, который можно использовать как более гибкую константную ссылку. Он работает почти так же, но мы можем в любой момент сослаться на другую переменную. Вариант 2 работает так же, как обычная ссылка. Значение менять можно, а указать на другую переменную не выйдет. И наконец вариант 3. Он равносилен случаю константной ссылки. То есть один раз объявили указатель и ничего больше менять не можем.
В первом и втором варианте вполне можно обойтись использованием ссылок. Особой разницы нет. Для третьего ограничиться ссылкой получится не всегда. Например:
const char* const CONST_STR = "Hello, world!";
Строку в стиле C с помощью ссылки мы объявить не сможем. Нужен указатель. И он должен быть константным, поскольку изменение содержания строки запрещено и приведет к неопределенному поведению. А второй const
здесь не помешает, чтобы получить жесткую привязку указателя к заданному значению и запретить случайные присвоения:
// Ничего нельзя:
// CONST_STR[ 0 ] = 'h';
// CONST_STR = "Good bye!";
const
Общее правило для применений const
в функциях достаточно простое. Входные параметры лучше объявлять с ключевым словом const
. Однако примитивные типы int
, double
, char
и т.д. являются исключениями из этого правила. Это объясняется тем, что примитивные типы эффективнее передавать по значению, а не по ссылке или указателю. Причиной этому служит то, что большинство примитивных типов меньше по размеру, чем адрес в памяти, который нужно было бы передать в противном случае. Кроме того, передача по значению упрощает работу оптимизатору, поскольку выполняется прямое копирование, а не косвенные операции в адресном пространстве.
Если же вы хотите передать в функцию объект структуры или класс, то используйте константную ссылку:
struct MyStruct {
int x;
};
void myFunction( const MyStruct& myStruct ) {
int x = myStruct.x; // можем читать
// myStruct.x = 1; но не можем менять значение
}
Каноничным примером на этот случай является конструктор копирования:
class MyClass {
public:
MyClass( const MyClass& other );
// MyClass( MyClass other ); нельзя
};
Если бы мы попытались передать в конструктор копирования не ссылку, а значение, то для инициализации этого значения нам пришлось бы вызвать конструктор копирования, который мы и хотим реализовать.
Еще const
можно использовать для объявления константных функций-членов классов:
class MyClass {
public:
void set( int x );
int get() const;
};
У класса в примере две функции: set()
и get()
. Первая предназначена для установки значения, а вторая для его получения. Ключевое слово const
в этом случае позволяет нам явно об этом сообщить. Причем, эта информация будет полезна и компилятору, и тем, кто будет работать с нашим классом. Ведь они будут знать, что константные функции-члены не меняют состояние класса. Можно сравнить это с флагом read-only. Вот что будет, если передать константную ссылку на объект класса MyClass
в функцию:
void myFunction( const MyClass& myClass ) {
// myClass.set( 1 ); нельзя ничего менять
int x = myClass.get(); // а вот читать пожалуйста
}
То есть объявив функцию-член get()
, как константную, мы пояснили компилятору, что она не меняет состояние объекта и предназначена только для чтения. Если бы мы забыли про const
, то в функции myFunction()
мы бы ничего не смогли сделать с экземплярами класса MyClass
, а компилятор бы выдавал ошибки при попытке вызова его функций-членов. Но если бы оказалось, что нам и правда нужно менять состояние объекта, то ключевое слово const
из сигнатуры функции пришлось бы убрать. А по принятым соглашениям ссылку имело бы смысл заменить на указатель:
void myFunction( MyClass* myClass ) {
myClass->set( 1 );
int x = myClass->get();
}
Но тут есть один тонкий момент. Иногда бывает полезно инкапсулировать информацию о том, что на самом деле внутреннее состояние класса меняется, но все равно объявить функцию-член константной. Например, в многопоточной среде мы можем использовать мьютексы:
class MyClass {
public:
MyClass() : m_x( 0 ) {
}
void set( int x ) {
std::lock_guard< std::mutex > lock( m_mutex );
m_x = x;
}
int get() const {
// Не будет работать, потому что меняется одно из полей класса:
// std::lock_guard< std::mutex > lock( m_mutex );
return m_x;
}
private:
int m_x;
std::mutex m_mutex;
};
И что же делать? - Для этого в C++ предусмотрено ключевое слово mutable
. Если мы объявим поле мьютекса, как mutable
, то укажем компилятору, что состояние объекта может меняться даже в константных функциях-членах:
class MyClass {
public:
MyClass() : m_x( 0 ) {
}
void set( int x ) {
std::lock_guard< std::mutex > lock( m_mutex );
m_x = x;
}
int get() const {
std::lock_guard< std::mutex > lock( m_mutex ); // теперь все работает
return m_x;
}
private:
int m_x;
mutable std::mutex m_mutex;
};
Вот мы и разобрались с ключевым словом const
в С++. Оно имеет довольно много значений и позволяет улучшить надежность и читаемость кода, если правильно им пользоваться. Поэтому я нахожу довольно странным, что в большинстве других языков программирования, которые мне известны, хоть и есть некие аналоги const
, но имеют в них ограниченную область применения. Более того, я считаю, что в C++ реализована одна из лучших возможных схем управления константами.
Legotckoi:
Добрый день.
Статья замечательная, НО…
Почему последний пример сделан с использованием Qt?
Речь же идёт о C++ и в течение всей статьи не было и намёка на Qt, также как и в заголовке статьи и в тегах к статье.
Смысл передан, но огорошивает применение в коде фреймворка Qt в качестве примера к статье, которая построена вокруг STL.
Было бы лучше переделать последний пример.
Здравствуйте. Спасибо за совет. Пример в ближайшее время поправлю.
Legotckoi
Добрый день.
Статья замечательная, НО…
Почему последний пример сделан с использованием Qt?
Речь же идёт о C++ и в течение всей статьи не было и намёка на Qt, также как и в заголовке статьи и в тегах к статье.
Смысл передан, но огорошивает применение в коде фреймворка Qt в качестве примера к статье, которая построена вокруг STL.
Было бы лучше переделать последний пример.