const是c++的关键字之一,被const修饰的都受到强制保护,可以预防意外的变动,提高程序的健壮性。
我把const的使用情况分为四种:const对象(变量) ,const引用,const和指针,const和函数,下边依次介绍。
1. const对象
我们经常遇见的使用const修饰变量的目的是为了防止变量的值被修改:
例如,定义一个变量bufsize代表一个常数512的时候会有一个严重的问题:即bufsize是可以被修改的,它可能被有意无意的修改。const限定符提供了一个解决方法:它把一个对象转换成一个常量。
定义bufsize为常量并初始化为512(因为常量在定义后就不能被修改,所以定义时必须初始化)。
变量bufsize仍然是一个左值,但现在这个左值是不可修改的。任何修改bufsize的尝试都会导致编译错误:
注意:用常量表达式初始化的const变量,大部分编译器在编译是会用相应的常量表达式替换这些const变量的任何使用,所以在实践中不会有任何存储空间用于存储用常量表达式初始化的const变量,当使用const_cast去掉变量的const属性后,再对变量赋值,会发生如下情况:
对同一块内存地址有两个不同的值,这是因为const变量t是用常量1来初始化的,在访问t时,直接用内存中的1替换了。如果不用常量表达式来对t进行初始化,就正常了:
另外一种使用const对象的方法跟变量的作用域有关:
在全局作用域里定义非const变量时,它在整个程序中都可以访问。我们可以把一个非const变量定义在一个文件中,假设已经做了合适的声明,就可以在另外的文件中使用这个变量:
//file_1.cpp
int counter; //definition
//file_2.cpp
extern int counter; //uses counter from file_1
++counter; //increments counter defined in file_1
与其他变量不同,除非特别说明,在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。
通过指定const变量为extern,就可以在整个程序中访问const对象:
//file_1.cpp
extern const int counter = 0; //defines and initializes
//file_2.cpp
extern const int counter; //uses counter from file_1
cout<<counter; //counter = 0
有时会声明一个类对象为const对象。
如果将某个类对象声明为const,则编译器不允许该对象调用任何可能修改它的成员函数。
所以在使用这个const对象的接口的时候,要保证不会修改const对象的值,因此需要把调用的接口函数修饰为const,在函数头后面加const修饰即可,这就是const函数的用法,仅当一个函数是类成员时,修饰为const才有意义。
如果想要修改成员变量,需要在变量前加mutable 修饰。
2. const引用
引用是对象的另一个名字,const引用是指向const对象的引用:
可以读取但是不能修改refbuf,因此,任何对refbuf的赋值都是不合法的,这个限制的意义是:因为不能直接对bufsize赋值,所以不能通过使用refbuf来修改bufsize。
非const引用可以修改其所指向的对象的值,正如ref2,所以将普通的非const引用绑定到const对象上是不合法的。
const引用可以初始化为不同类型的对象 或者 初始化为右值,如字面常量:
同样的初始化对于非const引用却是不合法的,而且会导致编译时错误。其原因非常微妙啊~
观察将引用绑定到不同的类型时所发生的事情:
double dval = 3.14;
const int &ri = dval;
编译器会把这些代码转换成如下形式的编码:
int tmp = dval;
const int &ri = dval;
如果ri不是const,那么可以给ri赋一个新的值,这样做不会修改dval,而是修改了tmp。期望对ri的赋值会修改dval的程序员会发现dval并没有被修改。仅允许const引用绑定到需要临时使用的值完全避免了这个问题,因为const引用是只读的。
非const引用只能绑定到与该引用同类型的对象。
const引用则可以绑定到不同但是相关的类型的对象或者绑定到右值。
3. const和指针
指针和const限定符之间有两种交互:即指向const对象的指针和const指针。另外还有指向const对象的const指针。
1. 指向const对象的指针
可以使用指针来修改它所指向的对象的值,如果指针指向const对象,则不允许用指针来修改其所指的const值。为了保持这个特性,c++强制要求指向const对象的指针也必须具有const特性:
const double *cptr; //cptr may point to a double that is const
这里的cptr是一个指向double类型const对象的指针,const限定了cptr指针所指向的对象的类型,而并非cptr本身,即cptr本身不是const,在定义时不需要对它进行初始化。如果需要的话,允许给cptr重新赋值,使其指向另一个const对象,但是不能通过cptr修改其所指对象的值:
把一个const对象的地址赋给一个非const对象的指针也会导致编译时错误:
另外,不能使用void*指针保存const对象的地址,而必须使用const void* 类型的指针保存const对象的地址:
允许把非const对象的地址赋给指向const对象的指针,但不能通过指针修改其值,因为指向const对象的指针一经定义就不允许修改其所指向的对象的值:
可以看到,在使用指向const对象的指针时,如果它指向一个非const对象,这个const对象可以通过其他方法改变它的值,导致const指针指向的对象的值也变化了,所以,不能保证指向const的指针所指对象的值一定不可修改。
2. const指针
const指针本身的值不能修改:
int err = 0;
int *const p = &err;
const指针p的值不能修改,即不能使p指向其他对象,p需要在定义时初始化。
const指针所指的对象的值是否能修改取决于该对象的类型,即可以使用const指针修改其所指向的对象的值。
3. 指向const对象的const指针
既不允许修改pp所指向对象的值,也不允许修改pp的指向。
4. typedef和指针
typedef string *pstring;
const pstring cstr;
cstr的实际类型是: string *const cstr;
4. const和函数
当一个函数是类的成员函数时,修饰为const才有意义。
对于函数的形参,如果是指针形参:
指针形参的类型将影响函数调用所使用的实参:可以将指向const型和非const型对象的指针传递给指向const对象的指针形参,而不能将指向const对象的指针传递给指向非const的指针形参。
const形参:
如果函数的形参是非const形参,则既可以传递const实参,也可以传递非const实参:这是因为初始化是复制了初始化式的值,所以可以用const对象初始化非const对象,反之亦然。
如果函数的形参是const形参,则既可以传递const实参,也可以传递非const实参。
令人吃惊的是:尽管函数的形参是const,但是编译器却将函数的定义视为其形参被声明为普通的非const类型:
反正编译器对形参是否是const类型不加区别:
引用形参:
我们知道:当需要在函数中修改实参的值,或者需要传递大的对象,或者没办法实现对象的复制时,要用到引用形参。
在传递大对象时,如果不希望修改对象的值,使用const引用可以避免修改实参。如果引用形参的唯一目的是为了避免复制实参,则应将形参定义为const引用。
当需要修改引用的对象时,函数的形参只是非const引用类型,则非const引用只能与完全相同的非const对象相关联。这样就限制了函数的使用,即只能传递非const类型的对象,所以应该将不修改相应实参的形参定义为const引用。