目录
前言
const 就字面意思而言,最重要的一点就是 不可变,这也是它的主要用途。本文将用通俗的语言讲解 const 的用法以及一些注意事项:
1. const 修饰变量
1.1 引入
假设在一个程序中,我们用到了 12 这个数字,它表示一年有 12 个月。
你可以直接在程序中大量使用 12,但是当 “别人阅读你的程序” 又或者 “一段时间后你再阅读此程序”,可能会纳闷:
12 表示什么?时钟的一圈为 12 时?还是 12 生肖?又或者是一年有 12 个月?
显然这样的代码让人费解。
此外,如果有一天一年有 13 个月,那么所有用到 12 的地方(并且表示的是 一年有 12 个月的 “12”)都要被修改。
这不仅工作量较大,而且可能导致你修改了错误的地方。
我们可以将这个特殊的数字用一个变量 per_year_months 来保存。
int PER_YEAR_MONTHS = 12;
这时当别人看到 per_year_months 时,马上就反应过来:这是每年的月份数,是不是可读性更强一些?
但仍然还有瑕疵:如果你在程序的某处不小心修改了它的值,这会导致未定义行为,使得程序可能出现逻辑错误。
对此我们 期望它是一个常量,并且编译器能阻止任何试图改变常量的行为。
下面有两套方案:
- 宏定义 #define
- 语法:
#define var_name var_value
- 作用:
在预处理阶段,编译器将 var_name 替换为 var_value。
- 语法:
需要注意的是 #define 仅仅是 简单的文本替换。
#define PER_YEAR_MONTHS 12
在预处理阶段,'PER_YEAR_MONTHS' 被文本替换为 '12'
并不是将 '12' 赋值给 'PER_YEAR_MONTHS'
即 'PER_YEAR_MONTHS' 并不是变量 (左值)
虽然 var_name 不占用内存,但是由于仅仅是文本替换,编译器并不会对其进行类型检查。
c++ 是强类型的语言,编译器通过类型检查,能帮助我们规避代码中潜在的一些问题,因此 此方案不建议使用。
- const 限定符
- 语法:
const type var_name;
- 作用:定义一个 type 类型的常量 var_name,var_name 值不可变(编译时,运行时都不可变)。
- 语法:
const int PER_YEAR_MONTHS = 12;
// 以下均错误:试图修改常量的值
PER_YEAR_MONTHS = 100;
PER_YEAR_MONTHS += 2;
虽然 const 变量占用内存,但是编译器会进行类型检查。
此外,使用此方案还有一个好处:倘若有一天 PER_YEAR_MONTHS = 13,那么我们只需要修改
const int PER_YEAR_MONTHS = 13;
即可。
1.2 必须初始化
const 修饰的变量必须初始化:编译时初始化 与 运行时初始化。
c++11 引入的 constexpr 关键字为 编译时初始化。
const int a; // 错误:a 未初始化
int size() { //... }
const int b = size(); // 正确:运行时初始化
const int c = 1; // 正确:编译时初始化
int d = 1;
const int e = d; // 正确:能用 非const 初始化 const
int f = e; //正确:能用 const 初始化 非const
1.3 默认限定文件内有效
在一个源文件中定义的 const 常量,该常量只能在该文件中使用,其他文件无法访问(也就相当于它的作用域为当前文件)。
如果在不同文件中定义了同名的 const 常量,等同于在不同文件中定义了独立的常量。这就好比:
int fic1()
{
const int a;
}
int fic2()
{
const int a;
}
尽管 fic1、fic2 中都定义了 a,但是两个 a 由于作用域不同,因此它们相互独立,互不相干。
1.4 extern 关键字
有时我们需要 const 常量能够在不同文件之间使用,那么 extern 就派上用场了。
假如我们需要多个文件共享常量 A,可以在头文件中声明
// const.h
extern const int A; // 声明
在对应的源文件中定义
// const.cpp
#include "const.h"
const int A = 1;
// 或者
extern const int A = 1;
// 合法,但是 extern 多余
那么其他需要使用此常量的源文件,只需要将它所在的头文件包含即可使用。
2. 对常量的引用
可以将引用绑定在 const 对象上,该引用需要加上 const 修饰,表示 不能通过此引用修改所绑定对象的值,也就是所指的对象不可变。
引用本身就不能改变,不能将 const int& p = a; 理解为 因为 const ,所以 p 不可变。
同时由于引用不是对象,所以“常量引用” 这一概念严格意义上说并不存在,但是有的地方仍然使用这一概念,实际上代指 “对常量的引用”。
const int a = 1;
const int& a1 = a; // 正确:不能通过 a1 修改 a 的值
int& a2 = a; // 错误:试图将非const引用指向const对象
int b = 1;
const int& b1 = b; //正确:但是不能通过 b1 修改 b 的值
int& b2 = b; // 正确:可以通过 b2 修改 b 的值
对于 int& a2 = a,我们也可以从语义上来理解为什么它是错误的:
首先 a 为 const 对象,也就表示 a 不可修改
然而 a2 却是非 const 引用,也就表示我们可以通过 a2 修改 a 的值
出现了矛盾,故错误。
3. 指针 与 const
3.1 指向常量的指针
const 指针类型 var_name;
表示 指针指向的对象不可变,但是指针可以改变。
const int a = 1;
int b = 1;
const int* ptr = &a; // 正确
*ptr = 2; // 错误:试图改变所指对象的值
ptr = &b; // 正确
在看下面的常量指针,可能读者会容易混淆二者,这里说说作者的一种方法:
类型解析从右到左
类型解析:
'const int*' ptr = &a;
1. '*':所以 ptr 为指针
2. const int:所以 *ptr = const int,
即对指针解引用为 const int
3. 综上:ptr是指向 const int 的指针
3.2 常量指针
指针类型 const var_name;
表示 指针不可变,但是可通过指针修改所指的对象。
int a = 1;
int* const ptr1 = &a; // 正确
*ptr1 = 2; // 正确:a = 2
const int b = 1;
int* const ptr2 = &b; // 错误
类型解析:
'int* const' ptr2 = &a;
1. const:说明 ptr2 不可变 (常量)
2. *:ptr2 是指针 (常量指针)
3. int:*ptr2 = int
4. 综上:ptr2 为指向 int 的常量指针
对于 int* const ptr2 = &b; 可以从语义上理解:
b 是 const int,然而 ptr2 仅仅是常量指针,也就是说能够通过 ptr2 修改 b 的值
这与 b 不可修改相矛盾,故错误
那么怎么可以使得其正确呢?这就是下面介绍的:
3.3 指向常量的常量指针
前两中的混合,所以表示 指针不可变,指向的对象也不可变。
const int a = 1;
const int b = 1;
const int* const ptr2 = &b; // 正确
*ptr2 = 2; // 错误
ptr2 = &a; // 错误
看完上面的部分代码,你可能会注意到:对于 const 修饰的对象,当发生拷贝(赋值)时,有时候会报错,这是为什么呢:
4. 顶层 const 与 底层 const
这两个概念,更多的是出现在指针上:
- 顶层 const:表示指针本身是个常量
- 底层 const:表示指针指向的对象是个常量
更一般的:
- 顶层 const 也适用于其他数据类型,表示 该对象本身是个常量
- 由于引用也类似指针,因此引用存在 底层 const
int a = 0;
int *const p1 = &a; // 常量指针:顶层 const
const int* p2 = &a; // 指向常量的指针:底层 const
const int b = a; // 自身不可变:顶层 const
const int& a1 = a; // 所指的对象不可变:底层 const
对常量的引用都是底层 const,因为它表示该引用所指的对象不可变
对于执行拷贝操作,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般地可以理解为:对于指针与引用,非const 能转为 const,反之不能),顶层 const无影响。
// 1. 变量
int i = 0;
const int ci = i; // 正确:都无底层 const,顶层 const 没有影响
int a = ci; // 正确:都无底层 const,没有影响
// 2. 引用
int& r1 = i; // 正确:r1 与 i 都不是底层 const
int& r = ci; // 错误:r 不是底层 const,但是 ci 是,并且 const int& (正确情况下 r 的类型) 不能隐式转为 int& (r 的实际类型)
const int& r = ci; // 正确:r 与 ci 都是底层 const
// 3. 指针
int* p1 = &i; // 正确:p1 与 i 都不是底层 const
int* p2 = &ci; // 错误:p2 不是,ci 是, const int* 不能转为 int*
int *const p3 = &i; // 正确:都不是
const int* p4 = &i; // 正确:虽然 p4 是,i 不是,但是 int* 可以转为 const int*
5. const 成员函数
类的成员函数可以用 const 修饰,表示 这个函数内部不能改变该类的所有成员,可变数据成员 (mutable data member) 除外。
class A {
public:
void fic() const // const 成员函数
{
var++; // 正确:可以修改可变数据成员
sta--; // 错误
}
private:
mutable int var; // 可变数据成员
int sta;
};
6. const 形参与实参
6.1 顶层 const 的影响
在函数传递参数时,如果形参有 const 修饰,就需要注意 顶层const 带来的影响。
在形参中,顶层const会被忽略
都被忽略了,还能有什么影响?
void fic(const int x) { }
void fic(int x) { }
在 C++ 中,当函数同名不同参时,这样的函数是被允许存在的(重载函数),但是对于上面的两个函数会报错:重复定义 fic(int)。
这是因为 const int 为顶层 const,被忽略掉,因此 fic(const int) 等价与 fic(int)。
此外对于 fic(const int),当实参去初始化形参时,顶层 const 被忽略,所以以下语句均正确
int i = 0;
const int ci = 0;
fic(i);
fic(ci);
6.2 底层 const 的影响
前面我们知道:一般来说,const 不能隐式转为 非const。
const_cast 可以显式地将 const 转为非const
因此对于函数
void fic(int& arg) { }
void fic(int* arg) { }
非 const 引用 称为 普通引用
下面的语句都是错误的
const int i = 0;
fic(i); // 不能将普通引用绑定在常量i上
fic(1); // 不能将引用绑定在字面值上
const int* p;
fic(p); // 不能用指向 const int 的指针初始化 int*
【建议】尽量不使用 对非 const 的引用
7. 其他
当然有的教程也会写到 const 可以修饰函数返回值,这跟前面修饰变量,引用,指针等没什么区别,就只是表明返回值不可修改,拷贝时仍然会受到底层 const 的影响。
最后
可以看出,const 的主要作用为 告诉编译器其值不能修改。
本文参考 《C++ Primer》 以及 一些网上资料,同时添加自己的一些理解,如有错误,欢迎指正。