0. const限定符
有时我们希望定义这样一种变量,它的值不能被改变,比如缓冲区的大小、π(3.1415926)等,既可以防止其他协作者编写代码时一不小心改变了这个重要的值,而且该数值是有类型的,又可以限定其参与的运算。
在C++中,可以使用const
修饰变量来实现。
const int buff_size = 32;
当然,const不仅仅可以修饰变量,它还有其他很多用法:
- 修饰基本内置类型
- 修饰自定义对象
- const与数组
- const与引用
- const与指针(顶层const与底层const)
- 修饰函数参数
- 修饰函数返回值
- 修饰成员函数
1. const修饰基本内置类型
const int buff_size = 32;
这是const最基本的用法,我们在以后的代码不能如下一样修改buff_size
const int buff_size = 32;
buff_size = 64; // 错误,编译不通过
在定义const变量的时候,必须进行初始化。编译器在编译过程中,会把用到该变量的地方全部替换成相应的值。
默认情况下,const仅在当前文件中有效。若希望多个文件使用同一个const变量,可以使用extern来修改,在任何声明和定义处使用(当然只需要定义一次)。
2. const修饰自定义对象
const MyClass obj;
这样的声明意味着obj中的成员变量不可以被修改,且只能调用常成员函数(见本文第8节)。
但是C++还提供了一个关键字mutable
(参考【深度C++】之“mutable”),它可以允许我们修改常量对象中的成员变量。
3. const与数组
const与数组只有一种形式,即:
const int arr[] = {2, 3};
同样,必须初始化;不可以执行arr[0] = 1
类似的代码。
参考【深度C++】之“数组”,我们了解编译器通常把数组处理为指向首元素的响应类型的指针,这里也是,类似于const int *
类型。
const与指针相关内容,请看本文第5节。
4. const与引用
4.1 定义方法
可以把引用绑定到const对象上,我们称之为对常量的引用。
const int ci = 422;
const int &r_ci = ci;
上述代码第二行将一个引用绑定到了一个常量。为了正确的绑定,必须将引用声明为const。
我们通常称对常量的引用为常量引用。
这样的叫法,非常非常非常容易和常量指针搞混。这样我们就不可以通过引用r_ci
修改ci
r_ci = 500; // 错误
4.2 常量引用的初始化
C++的引用必须与其所引用的对象的类型严格一致。
但是在初始化常量引用的时候,可以用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
例如非常量对象、字面值、表达式。
int i = 42;
const int &r1 = i; // 正确,常量引用绑定在一个非const变量上
const int &r2 = 42; // 正确,常量引用绑定在字面值上
const int &r3 = r1 * 2; // 正确,常量引用绑定在表达式结果上
当一个常量引用被绑定到另外一种数据类型上的时候,编译器执行了如下的过程:
- 生成另一个临时常量,用目标对象的值初始化临时常量;
- 将常量引用绑定到上述的临时常量上。
看如下代码解释:
double d = 3.14;
const int &ri = d;
// 会被编译器解释成如下代码
double d = 3.14;
const int temp = d; // 执行double -> int的隐式转换
const int &ri = temp; // 将常量引用ri绑定到临时量上
若变量类型和常量引用不需要执行转换,则没有生成临时量这一步。
double d = 3.14;
const double &rd = d; // 不需要生成临时量
C++规定,将左值引用绑定到临时量是非法的;但是却可以将常量引用绑定在临时量上。因此,我们可以将函数的返回值声明成常量引用:
const int &get_a() {
int a = 3;
return a;
}
调用get_a
将得到数值3。这一点也在【深度C++】之“引用”中初始化引用的例外情况1提及:可以用临时量初始化初始化常量引用。
这里其实可以用C++11的右值引用来直观感受一下。我们可以写成:
double d = 3.14;
const int &&ri = d;
// 编译会通过,会产生一个临时量,可以绑定右值引用
但是却不能写成:
double d = 3.14;
const double &&rd = d;
// 此时编译不通过了,因为没有临时量产生
// 初始化右值引用的必须是一个右值
5. const与指针
5.1 指向常量的指针
和对常量的引用(常量引用)一样,我们可以定义一个指向常量的指针:
const double pi = 3.14;
const double *ptr_pi = π
// 错误调用,不可以通过ptr_pi修改pi
*ptr_pi = 3.1415926;
指针的类型必须与其所指向的类型严格一致。
但是允许一个指向常量的指针,指向一个非常量对象。
例如:
double pi = 3.14;
const double *ptr_pi = π
可以理解为是指针的“自以为是”的常量。
这种用法,和常量引用类似,但是没有临时量的概念加入,基本数据类型必须是严格匹配的。
其实不论是常量引用还是指向常量的指针,都是引用或指针的“自以为是”。它们觉得自己引用或指向了一个常量,所以自觉不去改变目标的值。
5.2 常量指针
因为指针是C++中的对象,引用仅仅是别名,因此不可以将引用设置为const,但是可以将一个指针声明为const。
(╯‵□′)╯︵┻━┻
实现的效果就是,该指针一旦初始化完成之后,就不能更改指针内容,即不允许将该指针再指向别的对象。
int buff_size = 512;
// pti将始终指向buff_size
int *const pti = &buff_size;
// 允许通过常量指针修改原对象的值
// 因为原对象也是非常量
*pti = 1024;
const double pi = 3.14;
// pt_pi是一个常量指针,指向常量的常量指针
const double *const pt_pi = π
5.3 顶层const与底层const
指针本身是不是常量及所指的是不是一个常量,是两个毫无关联的问题。
- 使用术语顶层const,表示指针本身是个常量;
- 使用术语底层const,表示指向的对象是个常量。
更一般的,任意对象的const,都是顶层const
int i = 0;
// 顶层const,修饰普通对象
// 不允许改变ci的值
// 其实可以写成 int const ci = 5;
const int ci = 5;
// 顶层const,修饰指针对象
// 不允许改变cp_i的值
int *const cp_i = &i;
// 底层const,修饰指针指向的对象
// 不允许通过p_ci改变ci的值,即*p_ci = 10
const int *p_ci = &ci;
// 左侧的是底层const,右侧的是顶层const
// 既不允许改变cp_ci的值,也不能使用*cp_ci的改变ci的值
const int *const cp_ci = &ci;
// 底层const,引用都是底层const
// 不允许通过r_ci改变ci的值
const int &r_ci = ci;
注意:只有两处有底层const,一个是指针,一个是引用。
当执行对象的拷贝操作时,常量是顶层const还是底层const区别很大。
顶层const不受什么影响,对拷贝源不做过多要求,因为拷贝只关心拷贝源的值。
// ci是顶层const的常量
// 可以赋值给变量i
i = ci;
// cp_ci有顶层const
// 可以赋值给没有顶层const的p_ci
p_ci = cp_ci;
但是,如果拷贝目标拥有底层const,那么要求拷贝源也必须是一个常量,拷贝目标和拷贝源必须具有相同的底层const:
- 可以把非底层const的变量赋值给一个底层const
int a = 20;
int *p_a = &a;
// 非底层const赋值给底层const
const int *p_ca = p_a;
- 但是不可以使用底层const赋值给非底层const
int a = 20;
const int *p_ca = &a;
// 编译出错
// 底层const赋值给非底层const
int *p_a = p_ca;
这么严格的匹配关系,在函数调用时传递形参时会用到,尤其是当含有函数重载的时候,会决定调用哪个重载函数,见下一节。
6. const修饰函数参数
我们可以将函数参数声明为const,且在函数调用处,利用传递给函数的实参对函数形参初始化。
形参的初始化方式和变量的初始化方式是一样的,因此调用函数传递const实参的规则,和上述的顶层const和底层const的规则一模一样。
因为C++支持函数的重载,又因为顶层const的要求很松,若如下声明了两个函数:
void func(const int i);
void func(int i); // 错误
当我们调用的时候并区分不开,因此上述声明重复定义了func,是不允许的,这一点在【深度C++】之“函数重载”也提及,在【深度C++】之“函数匹配”中详细介绍了上述示例错误的原因。
C++标准建议,当我们不需要修改某个函数参数的值的时候,尽量将其声明为常量引用,即:
void func(const int &i);
这样就可以避免出现二义性问题,还可以提高程序的运行效率(因为使用引用)。
7. const修饰函数返回值
函数返回一个值的方式和初始化一个变量或者形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
通常情况下,返回基本内置类型,不需要添加const,因为函数返回的是一个右值。
返回指针和引用的时候,如果不添加const,我们可能会写出如下代码:
string &shorter_string(string &s1, string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
shorter_string(s1, s2) = "new string";
虽然正确,但是很迷惑,通常添加const。
const string &shorter_string(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
在第4节我们也提及了返回常量引用的函数,可以返回函数体内的临时量:
// 调用get_a会得到数值3
const int &get_a() {
int a = 3;
return a;
}
要想做到这种效果,必须添加const。
8. const修饰成员函数
可以如以下方式定义一个const成员函数(常量成员函数):
class MyClass {
private:
int i = 0;
public:
int func() const;
};
紧随参数列表之后的const关键字,其作用是修改隐式常量指针this的类型。
默认情况下,this的类型是指向类类型非常量版本的常量指针:
// 示例,C++很可能就是这么声明this的
// 这是一个顶层const,this指针的值是不允许被修改的
// 即只能指向本类的一个实例自己
MyClass *const this
加入我们将MyClass
对象声明为了常量,指针变成了:
// 既包含顶层const,又包含底层const
const MyClass *const this
导致不能在一个常量对象上调用普通的成员函数,包括常量对象的引用和常量对象的指针:
const MyClass mc;
// 无法调用,底层const约束不能改变任何成员变量
mc.non_const_func();
当我们把const添加在成员函数之后,就可以使用常量对象、及其引用和指针调用了。
此时,我们不能再在const成员函数中修改任何的成员变量。
const MyClass my_class;
my_class.func();
关于函数的调用,有以下规则:
- 若my_class不是常量,且类只有func的常量版本,则会调用func的常量版本;
- 若my_class不是常量,同时声明了func的非常量版本和常量版本,则会调用func的非常量版本。
- 若my_class是常量,那么只会调用常量版本
见如下两个示例:
class MyClass {
public:
int func() const { return 1; }
};
int main() {
// 不是常量
MyClass mc;
// 输出1
std::cout << mc.func() << std::endl;
}
class MyClass {
public:
int func() { return 0; }
int func() const { return 1; }
};
int main() {
// 不是常量
MyClass mc;
// 调用普通版本,输出0
std::cout << mc.func() << std::endl;
// 是常量
const MyClass c_mc;
// 输出1
std::cout << c_mc.func() << std::endl;
}
9. 总结
C++中的const的是将对象和函数声明为只读状态的一个关键字。
const可以与基本内置类型使用,此时定义的变量初始化之后就不会改变,编译器会将用到该变量的地方都替换成变量的值。
const可以修饰自定义对象,此时只能调用const成员函数。
const可以与引用使用,表明这是一个对常量的引用,通常称作常量引用。
const可以与指针使用,要区分指向常量的指针和常量指针,以及顶层const和底层const的定义。
const可以修饰函数的参数,通常传递常量引用时使用。
const可以修饰函数的返回值,通常在返回指针和引用时使用。
const可以修饰成员函数,修改隐式常量指针this的类型,使常量对象可以调用成员函数。