C++中的 const 关键字的用法非常灵活,而使用const将大大改善程序的健壮性,采用符号常量写出的代码更容易维护。
Const 是C++中常用的类型修饰符,常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。
〇、用法分类
- 常变量: const 类型说明符 变量名
- 常引用: const 类型说明符 &引用名
- 常数组: 类型说明符 const 数组名[大小]
- 常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名
- 常对象: 类名 const 对象名
- 常数据成员: const 类型说明符 数据成员名
- 常成员函数: 类名::fun(形参) const
在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小])中, const 与“类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。如:
const int a=5; 与 int const a=5; 等同
类名 const 对象名 与 const 类名 对象名 等同
一、常量与指针
1. 指针常量和常量指针
使用指针时涉及到两个对象:该指针本身和被它所指的对象。
将一个指针的声明用const在 * 前修饰将使那个对象而不是使这个指针成为常量;要将指针本身而不是被指对象声明为常量,必须使用声明运算符 *const。
所以出现在 * 之前的const是作为基础类型的一部分:
char *const cp; //指向char的const指针
char const *pc1; //指向const char的指针
const char *pc2; //指向const char的指针(后两个声明是等同的)
从右向左读的记忆方式:
cp is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容,pc称作“指针常量”即指针本身是常量。
pc2 is a pointer to const char. 故*pc2的内容不可以改变,但pc2可以指向别的字符串,pc1和pc2称作“常量指针”即指向常量的指针。
const只对它左边的东西起作用,唯一的例外就是const本身就是最左边的修饰符,那么它才会对右边的东西起作用。
注意:允许把非 const 对象的地址赋给指向 const 对象的指针(此时不允许通过指针修改对象的内容),不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。
2. 在函数参数中的指针常量和常量指针
在函数参数中指针常量时表示不允许将该指针指向其他内容。
在函数参数中使用常量指针时表示在函数中不能改变指针所指向的内容。
二、常量与引用
常量与引用的关系稍微简单一点。因为引用就是另一个变量的别名,它本身就是一个常量。也就是说不能再让一个引用成为另外一个变量的别名,那么他们只剩下代表的内存区域是否可变。
const int& ri = i; // 正确:表示不能通过该引用去修改对应的内存的内容,即常量引用。
int& const rci = i; // 错误!不能这样写。
由此可见,如果我们不希望函数的调用者改变参数的值。最可靠的方法应该是使用引用。
三、常量类成员函数和常量类数据成员
常量函数是C++对常量的一个扩展,它很好的确保了C++中类的封装性。一般放在函数体后,形如:void fun() const; 。
我们需要明白常量函数是为了最大程度的保证对象的安全。通过使用常量函数,我们可以只允许必要的操作去改变对象的状态,从而防止误操作对对象状态的破坏。但是,就像上面看见的一样,这样的保护其实是有限的。关键还是在于我们开发人员要严格的遵守使用规则。另外需要注意的是常量对象不允许调用非常量的函数。
1. 常量函数与修改类内数据成员
在C++中,为了防止类的数据成员被非法访问,将类的成员函数分成了两类,一类是常量成员函数(也被称为观察者);另一类是非常量成员函数(也被称为变异者)。在一个函数的签名后面加上关键字const后该函数就成了常量函数。对于常量函数,最关键的不同是编译器不允许其修改类的数据成员。
常量函数内试图去改变数据成员的值,在编译的时候引发异常。
对于非常量的成员函数,我们可以根据需要读取或修改数据成员的值。但是,这要依赖调用函数的对象是否是常量。通常,如果我们把一个类定义为常量,我们的本意是希望他的状态(数据成员)不会被改变。常量对象的状态不允许被修改,因此,通过常量对象调用非常量函数时将会产生语法错误。
C++也允许我们在数据成员的定义前面加上mutable,以允许该成员可以在常量函数中被修改。
2. 常量函数重载
当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。
3. 常量数据成员
const数据成员的初始化只能在类的构造函数的初始化表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。
const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。
枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
四、用于函数声明
在一个函数声明中,const可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。
1. 修饰参数的const
void fun0(const A* a ); void fun1(const A& a);
调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化。
如形参为const A*a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;
如形参为const A&a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
- 参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。
- 对于非内部数据类型的输入参数,因该将“值传递”的方式改为“const引用传递”,目的是为了提高效率。例如,将void Func(A a)改为void Func(const A &a)
- 对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x)不应该改为void Func(const int &x)
2. 修饰返回值的const
一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
八、其他问题
1. const常量与宏常量
const常量有数据类型,而宏常量没有数据类型。
编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误(边际效应)。