const
的一个奇妙的特性是,他允许你指定一个语义约束,即被const
修饰的对象都是不可修改的,而编译器会帮助你在编译时维护这个性质,这能让你更早地发现程序设计过程中的漏洞和缺陷。因此,只要允许,就应该明确地使用const
对变量进行限定。
const对象
只要是对象,那么不管它在哪,都可以添加const
修饰;对于指针,还可以分别指定指针本身以及指针所指向的对象是否可以修改:
char greeting[] = "hello";
char *p1 = greeting;
const char *p2 = greeting;
const char *const p3 = greeting;
const
最具威力的功能是针对函数声明的应用。在一个函数声明中,const
可以和函数返回值、函数的各个参数以及函数自身(如果该函数是成员函数的话)产生关联。
令函数返回一个常量值,可以降低因为函数调用者的操作错误而造成程序崩溃的可能性。举个例子,考虑有理数乘法的实现函数:
class Rational { /* ... */ };
const Rational &operator*(const Rational &r1, const Rational &r2) { /*...*/ }
这里为什么要返回const
对象呢?因为如果不加这个限定,那么客户端可能会写出这样的代码:
(a * b) = c;
这样做是没有意义的,但是很多时候可能出现类似这样的手误,例如:
if((a * b) = c) {
//...
}
为返回值添加一个const
,就能轻易地避免这样的问题。
const成员函数
将const
施加于成员函数的目的是表明该函数可以作用于const对象
之上。这一类成员函数很重要,这是因为:
- 不论是对于类的使用者还是编译器而言,知道那些函数不会改变对象是非常重要的。
- 只能在
const对象
上调用const成员函数
。
需要注意的是,两个成员函数如果只是常量属性不同,那么是可以构成重载的。这是一个C++当中非常重要的特性。例如下面的这个类,用于封装一大段文字:
class TextBlock
{
//...
const char &operator[](std::size_t index) const { return text[index]; }
char &operator[](std::size_t index) { return text[index]; }
private:
std::string text;
};
这个类可能会通过以下方式调用:
void printText(const TextBlock &block)
{
std::cout << block[0];
}
只要重载operator[]
运算符并给出不同的返回结果,就可以令const
与非const
的TextBlock
对象获得不同的处理。
成员函数如果是const
修饰的,那么意味着什么?有两种回答:
- 二进制常量
- 逻辑常量
二进制常量指的是成员函数不会修改对象在内存中的任何一个二进制位。然而很多成员函数的做法是:在对象内部维护一个指针或者引用,并在const成员函数
中通过这个指针或者引用修改外部对象,这种情况就属于逻辑常量。
例如,CTextBlock
类中可能会缓存文本的长度:
class CTextBlock
{
char *text;
std::size_t length;
bool isLengthValid;
//...
public:
std::siz_t length() const
{
if(!isLengthValid) {
length = std::strlen(text);
isLengthValid = true;
}
return length;
}
};
此时CTextBlock
的length()
成员函数从直觉上看很显然应该被修饰为const
,但是其中却需要修改缓存的长度,这就违反了二进制常量性。解决的方法是使用mutable关键字,它解除了const
在某个成员对象上所施加的二进制常量性:
class CTextBlock
{
char *text;
mutable std::size_t length;
mutable bool isLengthValid;
//...
public:
std::size_t length() const
{
if(!isLengthValid) {
length = std::strlen(text);
isLengthValid = true;
}
return length;
}
};
在const
和非const
成员函数中复用代码
mutable
很好用,但它同样不是一个万能灵药,至少它无法避免代码重复。 例如,在上面的operator[]
函数中,可能不是简单地检查某个字符,还会进行边界检查、日志打印、数据完整性校验等工作。此时,很容易写出以下代码:
class TextBlock
{
//...
const char &operator[](std::size_t index) const {
//边界检查...
//打印日志...
//数据完整性校验...
return text[index];
}
char &operator[](std::size_t index) {
//边界检查...
//打印日志...
//数据完整性校验...
return text[index];
}
private:
std::string text;
};
原文中,作者不认为将这些代码抽取到一个私有成员函数中是一个好办法。但我认为尚可。
真正需要做的事情是令其中一个方法直接调用另外一个方法:
class TextBlock
{
//...
const char &operator[](std::size_t index) const {
//边界检查...
//打印日志...
//数据完整性校验...
return text[index];
}
char &operator[](std::size_t index) {
return const_cast<char&>(
static_cast<const TextBlock &>(*this)[position];
);
}
private:
std::string text;
};
反向做法:令const成员函数
调用非const
成员函数是不可取的,因为这可能会违背编译器要求的二进制常量属性。
【注意】:
- 将某些东西声明为
const
可以让编译器帮我们更快地检查出问题。const
可以作用于对象、函数签名的任何位置、成员函数本体中。 - 编译器对
const
对象将强制检查其二进制常量性;但是从编码的角度来看,我们也要注重逻辑常量性。 - 当
const
函数实现和非const
函数实现几乎相同时,可以让非const
函数调用const
函数以复用实现代码。