Перейти к содержанию

Последнее обновление:2026-10-13

C++ const: константы и два вида const-указателей

Об авторе

Публичный аккаунт в WeChat:

C++ const подробно: константы, указатель на const и const-указатель | C++那些事

1. Смысл const

Тип const — это тип, описанный с помощью квалификатора типа const; значение переменной или объекта типа const не может обновляться.

2. Роль const

  • Позволяет определять константы
const int a=100;
  • Проверка типов

    • Разница между const-константами и константами, определёнными макросом #define:

      const-константа имеет тип, поэтому компилятор может выполнять проверки безопасности; у макроопределения #define нет типа данных — это лишь простая замена строк, поэтому проверка безопасности невозможна.Благодарим двух знатоков, указавших на проблему; см.: issue

    • const-константы поддерживают все типы.

    • В остальных случаях это просто const-квалифицированная переменная; не путайте её с константой.
  • Защита от изменения, охранный эффект и повышение надёжности программы

void f(const int i){
    i++; // error!
}
  • Экономия памяти и избежание ненужных выделений

    • const целочисленного/перечисляемого типа, инициализированный константным выражением (т.е. константное выражение), по характеру использования гораздо ближе к константе времени компиляции: если не брать его адрес и использовать только значение, он обычно оптимизируется в непосредственный операнд, как #define; в память он попадает только при взятии адреса или odr-use между единицами трансляции, и во всей программе существует лишь одна его копия.
    • После разворачивания #define в литерал он в виде непосредственного операнда вшивается в инструкции; сами инструкции тоже занимают память, поэтому несколько использований означают несколько вхождений (см. обсуждение в issue #5).
    • Замечание: не всякая const-переменная является константным выражением — таковы только переменные целочисленного/перечисляемого типа, инициализированные константным выражением; остальные const-переменные — лишь const-квалифицированные переменные (например, с плавающей точкой, пользовательских типов); правильным средством выражения констант времени компиляции является constexpr.

3. Объекты const по умолчанию локальны в файле

Обратите внимание: переменные не const по умолчанию являются extern. Чтобы const-переменная была доступна в других файлах, её нужно явно указать как extern в этом файле.

Доступ к переменной без const из разных файлов

// file1.cpp
int ext;
// file2.cpp
#include<iostream>

extern int ext;
int main(){
    std::cout<<(ext+10)<<std::endl;
}

Доступ к const-константе из разных файлов

// extern_file1.cpp
extern const int ext=12;
// extern_file2.cpp
#include<iostream>
extern const int ext;
int main(){
    std::cout<<ext<<std::endl;
}

Резюме:
Видно, что переменным без модификатора const не нужно явное объявление extern! А const-константе нужно явное объявление extern, причём её нужно инициализировать! Поскольку константа после определения не может изменяться, при определении она обязана быть инициализирована.

4. Определение констант

const int b = 10;
b = 0; // error: assignment of read-only variable ‘b’
const string s = "helloworld";
const int i,j=0 // error: uninitialized const ‘i’

Выше есть две ошибки: + b — константа, её нельзя менять! + i — константа, её нужно инициализировать! (Поскольку константа после определения не может изменяться, при определении она обязана быть инициализирована.)

5. Указатели и const

Существует четыре вида const, связанных с указателями:

const char * a; // 指向const对象的指针或者说指向常量的指针。
char const * a; // 同上
char * const a; // 指向类型对象的const指针。或者说常指针、const指针。
const char * const a; // 指向const对象的const指针。

Резюме:
Если const стоит слева от *, то const квалифицирует переменную, на которую указывает указатель, то есть указуемое является константой;
если const стоит справа от *, то const квалифицирует сам указатель, то есть сам указатель является константой.

Другой способ прочтения
Читайте английское справа налево, взяв «to» за разделитель: то, что до «to», описывает свойство указателя, а то, что после «to», — свойство цели

const char * p; //p is a pointer to const char
char const * p; //同上
char * const p; //p is a const pointer to char
const char * const p; //p is a const pointer to const char
Если сам указатель получил свойство const, он не может менять адрес, на который указывает
Если тип цели — char, содержимое можно изменять через указатель, например: *char='y';
Если тип цели — const char, содержимое нельзя изменять через указатель

Конкретное применение выглядит так:

(1) Указатель на константу

const int *ptr;
*ptr = 10; // error

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

Кроме того, нельзя сохранять адрес const-объекта в указателе void*; для сохранения адреса const-объекта нужно использовать указатель типа const void*.

const int p = 10;
const void * vp = &p;
void *vp = &p; // error

Ещё один важный момент: разрешается присваивать адрес не-const-объекта указателю на const-объект.

Присваивание адреса не-const-объекта указателю на const-объект:

const int *ptr;
int val = 3;
ptr = &val; // ok

Мы не можем изменить значение val через указатель ptr, даже если он указывает на не-const-объект!

Мы не можем изменить базовый объект через указатель на const-объект, однако если указатель указывает на не-const-объект, сам указуемый объект можно изменить другими способами. Значение, на которое указывает const-указатель, изменить можно — просто не через указатель на const-объект! Изменим так:

int *ptr1 = &val;
*ptr1=4;
cout<<*ptr<<endl;

Резюме:
1. Для указателя на константу нельзя изменять значение объекта через указатель.
2. Нельзя сохранять адрес const-объекта в указателе void*; для этого нужно использовать указатель типа const void*.
3. Разрешается присваивать адрес не-const-объекта указателю на const-объект; чтобы изменить значение указуемого объекта, нужно делать это другими способами, а не напрямую через текущий указатель.

(2) Константный указатель (const-указатель)

const-указатель должен быть инициализирован; значение, на которое он указывает, менять можно, а само указание — нельзя.

#include<iostream>
using namespace std;
int main(){
    int num=0, num1=1;
    int * const ptr=&num; // const指针必须初始化!且const指针的指向不能修改
    ptr = &num1; // error! const指针不能修改指向!
    cout<<*ptr<<endl;
}

Код даёт ошибку компиляции: const-указатель не может изменить то, на что указывает.

#include<iostream>
using namespace std;
int main(){
    int num=0, num1=1;
    int * const ptr=&num; // const指针必须初始化!且const指针的指向不能修改
    *ptr = 1;
    cout<<*ptr<<endl;
}

С этим кодом ничего не происходит, нормально выводится 1.

Наконец, когда адресу const-константы присваивается ptr, поскольку ptr указывает на переменную, а не на const-константу, возникает ошибка: ошибка const int* -> int *!

#include<iostream>
using namespace std;
int main(){
    const int num=0;
    int * const ptr=&num; // error! const int* -> int*
    cout<<*ptr<<endl;
}

Если выше изменить на const int *ptr или на const int *const ptr, оба варианта будут работать нормально!

(3) Константный указатель на константу

Поняв два предыдущих случая, этот понять довольно легко:

const int p = 3;
const int * const ptr = &p; 

ptr — это const-указатель, указывающий на const-объект типа int.

6. Использование const в функциях

const квалифицирует возвращаемое значение функции

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

(1) const int

const int func1();

Само по себе это бессмысленно, потому что возвращаемое значение и так присваивается другой переменной!

(2) const int*

const int* func2();

Содержимое, на которое указывает указатель, неизменяемо.

(3) int *const

int *const func2();

Сам указатель неизменяем.

const квалифицирует параметры функции

(1) Переданный параметр и сам указатель неизменяемы внутри функции — бессмысленно!

void func(const int var); // 传递过来的参数不可变
void func(int *const var); // 指针本身不可变

Это показывает, что параметр нельзя изменять в теле функции, но здесь в этом нет никакого смысла: var — формальный параметр, и внутри функции он не меняется. То же самое, когда передаваемый параметр — указатель.

Когда входной параметр передаётся «по значению», функция автоматически создаёт временную переменную для его копирования, такому входному параметру защита изначально не нужна — не добавляйте const.

(2) Содержимое, на которое указывает параметр-указатель, — константа и неизменяемо

void StringCopy(char *dst, const char *src);

Здесь src — входной параметр, dst — выходной. Добавив const к src, если какой-либо оператор в теле функции попытается изменить содержимое src, компилятор укажет на ошибку. Это одно из действий добавления const.

(3) Параметр — ссылка: чтобы повысить эффективность и одновременно предотвратить изменение.

void func(const A &a)

Для параметров не внутренних типов данных функция, объявленная как void func(A a), обречена на низкую эффективность: в теле функции будет создан временный объект типа A для копирования параметра a, а конструирование, копирование и разрушение временного объекта требуют времени.

Чтобы повысить эффективность, объявление функции можно изменить на void func(A &a), ведь «передача по ссылке» лишь одалживает псевдоним параметра и не требует созда вать временный объект.

Но у функции void func(A &a) есть недостаток:

при «передаче по ссылке» параметр a может быть изменён, чего мы не хотим. Решается это просто — добавляем const, и функция в итоге принимает вид void func(const A &a).

По аналогии, следует ли переписать void func(int x) как void func(const int &x) ради эффективности? Совсем не обязательно: у параметров внутренних типов данных нет конструирования и разрушения, а копирование происходит очень быстро — эффективность «передачи по значению» и «передачи по ссылке» практически одинакова.

Резюме:
1. Для входных параметров не внутренних типов данных следует заменить «передачу по значению» на «передачу по const-ссылке» с целью повышения эффективности. Например, заменить void func(A a) на void func(const A &a).

2. Для входных параметров внутренних типов данных не следует заменять «передачу по значению» на «передачу по const-ссылке». Иначе ни цели повышения эффективности достичь не удастся, ни понятность функции не сохранится. Например, void func(int x) не следует менять на void func(const int &x).

Выше решены два интервьюерских вопроса:

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

7. Использование const в классах

В классе любую функцию, не изменяющую поля данных, следует объявлять const. Если при написании const-функции-члена вы нечаянно измените поле данных или вызовете другую не-const-функцию-член, компилятор укажет на ошибку, что, несомненно, повышает надёжность программы.

Функция-член, описанная ключевым словом const, называется постоянной функцией-членом. Только постоянные функции-члены имеют право работать с константами и константными объектами; функции-члены, не описанные ключевым словом const, нельзя использовать для работы с константными объектами.

Const-поля класса должны инициализироваться через список инициализации, как показано ниже:

class Apple{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
};

Apple::Apple(int i):apple_number(i)
{

}

Const-объекты могут обращаться только к const-функциям-членам, тогда как не-const-объекты могут обращаться к любым функциям-членам, включая const-функции-члены.

Например:

// apple.cpp
class Apple
{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
    void take(int num) const;
    int add();
    int add(int num) const;
    int getCount() const;

};
// apple.cpp
Apple::Apple(int i) : apple_number(i)
{
}
int Apple::add(int num)
{
    take(num);
    return 0;
}
int Apple::add(int num) const
{
    take(num);
    return 0;
}
void Apple::take(int num) const
{
    std::cout << "take func " << num << std::endl;
}
int Apple::getCount() const
{
    take(1);
    //    add(); // error
    return apple_number;
}
int main()
{
    Apple a(2);
    cout << a.getCount() << endl;
    a.add(10);
    const Apple b(3);
    b.add(100);
    return 0;
}
// main.cpp

Компиляция: bazel run basic_content/const/class_const/first_example:main

В этот момент возникает ошибка: getCount() — const-функция-член, и разрешение перегрузки add() внутри неё совпадёт только с const-версией (int add(int num) const); лишь закомментировав эту const-версию, получим ошибку из-за вызова не-const add(). То есть: const-объекты могут обращаться только к const-функциям-членам, а не-const-объекты — к любым функциям-членам (см. issue #338).

Когда вызов меняется на:

const Apple b(3);
b.add(); // error

Это доказывает, что const-объекты могут обращаться только к const-функциям-членам.

Помимо инициализации const-констант списком инициализации, как показано выше, можно использовать и следующие способы:

Первый: объединить определение константы со static, то есть:

static const int apple_number

Второй: инициализировать снаружи:

const int Apple::apple_number=10;

Конечно, если вы компилируете в режиме C++11, можно инициализировать прямо при определении и просто написать:

static const int apple_number=10;
// 或者
const int apple_number=10;

Оба варианта поддерживаются в C++11!

Достаточно добавить -std=c++11 при компиляции!

Здесь упомянули static — скажем о нём кратко:

В C++ статические переменные-члены static нельзя инициализировать внутри класса. Внутри класса — только объявление; определение должно находиться вне тела определения класса, обычно инициализацию делают в файле реализации класса.

Объявление в классе:

static int ap;

Использование в файле реализации класса:

int Apple::ap=666

Что касается этого пункта, C++11 не позволяет объявить и сразу инициализировать — то есть используется способ, описанный выше.

Частые вопросы

Чем указатель на const отличается от const-указателя?

const int* p — указатель на константу: значение менять нельзя. int* const p — константный указатель: нельзя переприсвоить сам указатель. Правило: const слева от * защищает значение, справа — указатель.

Может ли const-объект вызвать не-const метод?

Нет, const-объект вызывает только const-методы. Не-const объект вызывает любые.

const или #define для констант?

Лучше const: есть проверка типов, соблюдение областей видимости и отлаживаемость. #define — текстовая подстановка без типа.

Похожие темы

Комментарии