C++的const关键字

C++中的const关键字多才多艺且无处不在,它的语法意义并不高深,用法却变化多端。

定义常量

关键字const对变量的类型加以限定,将变量定义为一个常量,任何对变量赋值的行为在编译阶段即被编译器阻止。因为const对象一旦创建后,其值不能再改变,所以const对象必须被初始化,初始值可以是任意表达式。还可以利用一个对象初始化另一个对象,它们是不是const都无关紧要。

1
2
3
4
5
const int bufsize = 512;     // 编译时初始化 
const int i = get_size(); // 运行时初始化
int j = 42;
const int ci = j; // 用非const对象初始化const对象
int k = ci; // 用const对象初始非cosnt对象

默认状态下,const对象仅在文件内有效,当多个文件中出现类同名的const变量时,等同于在不同文件中分别定义了独立的变量。如果想在文件间共享const变量,需要在一个文件中定义const,而在其他多个文件中声明并使用它,而且,不论是声明还是定义,都添加extern关键字。

引用与const

把引用绑定到const对象上,称之为对常量的引用(reference to const),简称为“常量引用”。对const的引用不能用于修改它所绑定的对象:

1
2
3
4
const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象

必须注意,对const的引用仅仅限制不可以通过该引用改变对象的值,引用的对象本身是否是常量并无限制。对象有可能是一个非常量,允许通过其他途径改变它的值:

1
2
3
4
5
int i = 42;
int &r1 = i; // 引用ri绑定对象i
const int &r2 = i; // r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; // r1并非常量,i的值修改为0
r2 = 0; // 错误:r2是一个常量引用

常量引用可以绑定一个非常量对象,但非常量引用不能绑定到一个常量对象上。

指针与const

指针常量

和引用类似,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:

1
2
3
4
const double pi = 3.14       // pi是个常量,它的值不能改变
double *ptr = π // 错误:ptr是一个普通指针
const double *cptr = π // 正确:cptr可以指向一个常量
*cptr = 42; // 错误:不可以给*cptr赋值

和引用类似,指向常量的指针也没有规定其所指对象必须是一个常量。可以这么想:指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。

常量指针

和引用不同的是,指针本身也是对象,允许把指针本身定义为常量。把*放在const关键字之前用以说明指针是一个常量,即,不变的是指针本身(地址值),而非它指向的那个值:

1
2
3
4
int errNumb = 0;
int *const curErr = &errNumb; // curErr将一直指向errNumb
const double pi = 3.14;
const double *const pip = π // pip是一个指向常量对象的常量指针

pip是一个指向常量的常量指针,则不论是pip所指的对象值还是pip自己存储的那个地址都不能改变。相反地,curErr指向的是非常量整数,可以通过curErr修改errNumb的值:

1
2
3
*pip = 3.1416                     // 错误:pip指向常量
*curErr = 1; // 正确:可以通过curErr改变errNumb的值
curErr = nullptr; // 错误:curErr本身的值不能改变

顶层const

名词顶层const表示指针本身是个常量,而名词底层const表示指针所指的对象是一个常量。

函数与const

修饰形参

首先介绍参数传递的机制。每次调用函数时,会用传入的实参对形参进行初始化,形参初始化的机理和变量初始化一样。所以,形参只是实参的一个拷贝,修改形参对实参的值毫无影响。

形参为顶层const

形参为顶层const主要是以下两种形式:

1
2
void func(const type var);     // 形参本身不能修改
void func(type *const var); // 形参是一个常量指针,指针本身的值不能修改

此时,const关键字表示形参本身是常量,不能修改形参的值。需要特别注意的是,和其它初始化过程一样,用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略了。当形参有顶层const时,传给它常量对象或非常量对象都可以,因此下面两个函数声明,尽管形式上有差异,但其实完全一样:

1
2
void fcn(const int i);    // fcn能读取i,但不能向i写值
void fcn(int i); // 错误:重复定义类fcn(int)

如果这两种形式的函数同时存在,编译器会报错,提示重复定义。

形参为底层const

当形参为底层const时,函数原型如下:

1
2
void func(const type *var);    // 不能通过指针修改它指向的对象
void func(const type &var); // 不能通过引用修改它绑定的对象

此时,const关键字表示不能通过形参修改它所引用的对象。将形参声明为常量指针或引用非常普遍,因为既保证了不修改所指对象,也避免了对象的拷贝。初始化过程不能忽略形参的底层const,因此,下面两个函数声明是不一样的:

1
2
void fcn(const int &ref);    // 常量版的函数
void fcn(int &ref); // 非常量版的函数

常量版的函数能够接受一个非常量的实参,反之则不行。所以,当确认不会改变实参时,尽量使用常量引用作为函数的形参,这不仅能提醒调用者,不能通过形参修改它所引用的对象,还能使函数能够操作一个常量对象。

修饰返回值

首先介绍值是如何被返回的。返回值的方式和初始化变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

类似的,有如下几种用法:

1
2
3
4
const type func();     // 返回值本身不能修改
type *const func(); // 返回值是一个常量指针,指针本身的值不能修改
const type *func(); // 不能通过返回的指针修改它指向的对象
const type &func(); // 不能通过返回的引用修改它绑定的对象

前两种用法中,函数返回值本身是常量,不能被修改。

后面两种用法,分别返回对象的const指针和const引用,表示不能通过它们修改对象。返回对象的引用常常用于运算符重载,如若返回const引用,则运算后的对象不能再修改。

类和const

修饰类的成员变量

用const修饰类的成员函数,表示成员变量是常量,不能被修改,只能在初始化列表中赋值。

修饰类的成员函数

将const关键字放在成员函数的参数列表之后,表示成员函数是常量成员函数

1
2
3
4
class ClassType {
/* ... */
void func() const;
};

const成员函数有如下两个作用:

  1. 使得接口容易理解,提醒调用者,该const成员函数无法修改类对象的内容
  2. 使得操作“常量对象”成为可能,因为常量对象只能调用const成员函数

本博客的另一篇文章详细解释了const成员函数。

参考

  1. C++ Primer
  2. Effecitve C++
  3. 关于C++ const 的全面总结