const在C++中使用十分广泛,不同位置使用的意义也不尽相同,所以想写篇文章对其做一个总结。
首先,明确const是“不变”这个基本意义,但是不变不意味着什么都不变,下面将会看到。
1. const与变量
基本原则:const变量(对象)不能被修改
const在变量中的引入和魔数有关,所谓“魔数”指的是突然出现的一个常量值(也叫字面值常量)。
1
2
3
4
|
for ( int i = 0; i < 512; i++) { // todo } |
上例中,512即为魔数,512突然出现在循环中,令人不能得知其意义,所以引入const。
1
2
3
4
5
|
const int length = 512; for ( int i = 0; i < length; i++) { // todo } |
这样就知道循环是在长度范围内。
1.1 const修饰一个变量(或者说对象),使其变成一个常量,表示该变量的值无法再被修改,正因为如此,所以定义一个常量的时候,必须初始化。
1.2 const常量的作用域:
我们知道,在全局作用域内声明一个变量(此处特指非const修饰的变量),其作用于整个程序,在其他文件中也能被引用,原因是在全局作用域声明一个变量,默认是extern修饰的。
在全局作用域内声明一个const变量,默认不是extern修饰,所以其只能作用于本文件内,若要在其他文件中访问,需要显式声明为extern
2. const与引用
基本原则:const引用是指向const变量(对象)的引用
1
2
|
const int ival = 1024; const int &refVal = ival; |
2.1 const引用可以指向一个相关类型(不是本类型)的const变量
1
2
|
double dval = 3.14; const int &refVal = dval; |
编译器将double转换成一个临时的int对象,然后让const引用绑定到这个临时对象,所以改变dval的值不会改变refVal,也就是说dval仍然是非const变量,refVal仍然是常量引用。
primer第四版是上面的说法,但我在VS2012中,const也可以指向一个本类型的非const变量,查找资料的原因大概是满足reference-campatible条件。
理论上,我们应该严格遵守,常量引用指向常量对象,非常量引用指向非常量对象,避免出错。
3. const与指针
const与指针的关系分为两种:const修饰的指针和指向const对象的指针,二者const的位置不相同
3.1 指向const对象的指针(const位于指针符号*前面)
对于一个const对象,必须用一个指向const的指针来指向它。原因在于,const修饰使得对象无法被改变,而指针如果不是指向const的指针,则可以通过指针来修改对象,这是不被允许的。
1
2
|
const int ival = 1; const int *ptrVal = &ival; |
反过来,对于一个指向const对象的指针,可以指向任意一个对象,这个该怎么理解呢?我们首先看看指针赋值的过程:
1
|
int *ptr = &val; |
将val的地址赋值给ptr,因为赋值的只是地址,所以不知道ptr所指向的对象是否为const。
如果我们把一个地址赋值给一个指向const的指针,那么指针认为这是一个const的对象,也就是说,ptr指针指向了一个“自认为”是const的对象。
1
2
|
int ival = 1; const int *ptrVal = &ival; |
上面的程序是正确的,我们需要明确,ival是非const变量,所以我们可以通过给ival赋值更改ival的值。ptrVal指向了一个自认为是const的对象,所以我们无法通过*ptrVal来更改ival的值。
3.2 const修饰的指针(const位于指针符号*后面)
1
|
int * const ptr; |
上式声明了一个const类型的指针,表示的意思是指针本身是一个常量,不能被修改。
如何理解?指针本身的值是一个地址,如果指针本身是一个常量,则这个地址值不能被修改,也就是说指针只能指向这个地址,不能指向其他地方。但指针所指向的地址的内容不属于指针本身的值,所以其所指向的内容可以改变。
1
2
3
4
5
|
int ival = 1; int * const ptr = &ival; *ptr = 2; // ok int ivalTwo = 11; ptr = &ivalTwo // error |
综上,可以定义一个指向const对象的const指针
1
|
const int * const ptr = &ival; |
3.3 typedef中易出错的const指针
1
2
|
typedef string *ptr; const ptr s_ptr; |
上式不能直接替换理解为const string *s_prt; 从而认为s_ptr是一个指向const string的指针。
首先,ptr是一个指针,const修饰的是一个指针,所以应该是string *const s_ptr; s_ptr是一个指向string的const指针。
4. const与数组
const与数组的点在于const在定义时必须初始化这个原则,所以使用动态分配数组时,如果数组存储的是const类型的对象,必须进行初始化(使用初始化符号())。
5. const与函数返回值
修饰函数的返回值,用于返回一个常量。
1
|
const int foo(); |
5.1 返回通过值传递
如果函数返回时采用值传递,比如返回一个int类型,那么函数会把返回的值(比如47)复制到外部临时存储单元中(产生临时副本),所以加const修饰毫无意义
1
2
|
int foo(); const int foo(); |
二者完全相同。需要注意的是,值传递产生临时副本,效率低(下面const与函数参数有讲),所以通常采用引用传递来返回。
5.2 返回通过引用传递(并不多见)
如果返回值不是内部类型,通常使用引用传递来返回结果,因为引用传递的是本身,不需要产生临时副本。但需要注意的是,此时仅仅返回一个别名。
1
2
|
ClassType &foo(); const ClassType &foo(); |
const修饰的返回引用值,表示函数调用的结果只能赋值给一个同类型的const引用。
5.3 返回通过指针传递
1
|
const ClassType *foo(); |
表示函数返回一个ClassType类型的指针,这个指针指向一个const对象,指针所指的内容不能被修改,所以函数的返回值只能赋值给指向一个const的同类型的指针。
1
2
|
const ClassType *ptr = foo(); //ok ClassType *ptr = foo(); //error |
6. const与函数参数
首先需要明确,const修饰的目的就在于保护所修饰的内容不被改变。
在C++中,函数参数分为值传递,指针传递和引用传递。
6.1 值传递
值传递在函数调用时产生一个临时副本,函数中对传入参数的修改和操作是对副本的操作,不改变实参本身的值,所以无需const来保护。
值传递的保护很好,但值传递存在缺点,需要产生临时副本,如果传入的是对象,那么需要进行构造、复制、和析构等操作,效率不高。这时候可以考虑引用传递
1
2
|
void foo1( int x); void foo2(ClassType instance); //开销较大 |
下面这种保护无意义:
1
2
|
void foo1( const int x); void foo2( const ClassType instance); |
6.2 引用传递
通过传入实参的引用,降低开销。因为引用即本身,不需要去产生一个临时副本。
1
2
|
void foo1( int &x); void foo2(ClassType &ref); |
对于上述两个函数,函数调用和值传递的形式完全一样,不同的是函数内部得到的x和ref是调用传入实参的引用。也正因为如此,引用可以通过函数改变传入的参数来改变实参。这对与实参来说,比较危险,这时候需要通过const修饰来保护传入的引用不被修改。
1
2
|
void foo1( const int &x); void foo2( const ClassType &ref); |
通常来说,对于基本内部类型,不存在对象的构造等操作,所以下面两种保护参数不被修改的方式效率基本一样。
1
2
|
void foo1( int x); void foo1( const int &x); |
6.3 指针传递
指针传递在保护参数不被修改上和引用传递是一样的,指针传递还有一个功能是可以扩大接收参数的范围:
1
|
void foo1( const ClassType *ptr); |
结合上面const与指针,我们知道,ptr指向一个自认为是const类型的对象,所以!传入的对象不一定是const修饰的对象,可以是const对象,也可以非const对象。反过来,如果没有const修饰函数的形参,则只能传入非const对象。
需要明确,无论传入的是否是const对象,都无法通过指针来修改这个对象,这和上面指针与const的关系是一致的。
最后需要知道:const只能修饰一个输入参数,如果是输出参数,无论是引用传递还是指针传递,都不能使用const来修饰
7. const与类的数据成员
const修饰的数据成员不能在构造函数中进行初始化,只能使用成员初始化列表进行初始化。
我的理解是,因为在构造函数执行之前,使用成员初始化列表对数据成员进行初始化,如果在构造函数中对数据成员进行初始化,相当于对const进行二次赋值,这是不被允许的。
之所以这么理解,可以参照类中引用类型的初始化,也是必须在初始化列表中进行初始化,因为引用类型也是在定义的时候必须初始化,要求和const一样,所以二者都只能使用成员初始化列表来进行初始化。
8. const与类的成员函数
const成员函数中,const位于函数的参数列表后面(函数声明前面表示函数的返回值是一个常量)
const修饰的成员函数表示成员函数是一个只读的作用,不改变成员变量。
const成员函数真正的含义在于,const其实修饰的成员函数的是隐含参数this指针,也就是说传入的是const ClassType *this,因为this指向一个const对象,所以不能修改。
因为this是指向对象的指针,所以我们需要再次结合const与指针的知识:
(1)const修饰了this,得到const ClassType *this,this指向一个“自认为”是const的对象(也就是本身),所以任何对象(const或者非const)都可以调用一个const成员函数,因为传入的指针都把自身这个对象看作是const对象,所以不能被修改。
(2)对于一个const对象,当其调用成员函数的时候,默认都传入this指针参数,因为this此时指向一个const对象(本身),所以相当于成员函数被const修饰,成员函数是一个const成员函数,所以反过来说,const对象只能调用const成员函数,因为非const修饰的成员函数,this指针不是指向const对象。
(3)在8.2的基本上,进一步,每个成员函数都可以调用其他成员函数,每个成员函数都传入this指针,所以成员函数相互调用必须保持this指针的一致性,所以const成员函数只能调用const成员函数,因为二者传入的this指针都是const修饰的。对于非const成员函数,其传入非const修饰的this指针,所以不能被调用。
搞清楚const真正的含义就明白了,一定要保持const成员函数传入的是const指针这个意识,对象调用就需要看对象(本身,指针,引用)是否是const。