条款21: 尽可能使用const
使用const的好处在于它允许指定一种语意上的约束——某种对象不能被修改——编译器具体来实施这种约束。通过const,你可以通知编译器和其他程序员某个值要保持不变。只要是这种情况,你就要明确地使用const ,因为这样做就可以借助编译器的帮助确保这种约束不被破坏。
const关键字实在是神通广大。在类的外面,它可以用于全局或名字空间常量(见条款1和47),以及静态对象(某一文件或程序块范围内的局部对象)。在类的内部,它可以用于静态和非静态成员(见条款12)。
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const,还有,两者都不指定为const:
char *p = "Hello"; // 非const指针,
// 非const数据
const char *p = "Hello"; // 非const指针,
// const数据
char * const p = "Hello"; // const指针,
// 非const数据
const char * const p = "Hello"; // const指针,
// const数据
语法并非看起来那么变化多端。一般来说,你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。
在指针所指为常量的情况下,有些程序员喜欢把const放在类型名之前,有些程序员则喜欢把const放在类型名之后、星号之前。所以,下面的函数取的是同种参数类型:
class Widget { ... };
void f1(const Widget *pw); // f1取的是指向
// Widget常量对象的指针
void f2(Widget const *pw); // 同f2
因为两种表示形式在实际代码中都存在,所以要使自己对这两种形式都习惯。
const的一些强大的功能基于它在函数声明中的应用。在一个函数声明中,const可以指的是函数的返回值,或某个参数;对于成员函数,还可以指的是整个函数。
让函数返回一个常量值经常可以在不降低安全性和效率的情况下减少用户出错的几率。实际上正如条款29所说明的,对返回值使用const有可能提高一个函数的安全性和效率,否则还会出问题。
例如,看这个在条款19中介绍的有理数的operator*函数的声明:
const Rational operator*(const Rational& lhs,
const Rational& rhs);
很多程序员第一眼看到它会纳闷:为什么operator*的返回结果是一个const对象?因为如果不是这样,用户就可以做下面这样的坏事:
Rational a, b, c;
...
(a * b) = c; // 对a*b的结果赋值
我不知道为什么有些程序员会想到对两个数的运算结果直接赋值,但我却知道:如果a,b和c是固定类型,这样做显然是不合法的。一个好的用户自定义类型的特征是,它会避免那种没道理的与固定类型不兼容的行为。对我来说,对两个数的运算结果赋值是非常没道理的。声明operator*的返回值为const可以防止这种情况,所以这样做才是正确的。
关于const参数没什么特别之处要强调——它们的运作和局部const对象一样。(但,见条款M19,const参数会导致一个临时对象的产生)然而,如果成员函数为const,那就是另一回事了。
const成员函数的目的当然是为了指明哪个成员函数可以在const对象上被调用。但很多人忽视了这样一个事实:仅在const方面有不同的成员函数可以重载。这是C++的一个重要特性。再次看这个String类:
class String {
public:
...
// 用于非const对象的operator[]
char& operator[](int position)
{ return data[position]; }
// 用于const对象的operator[]
const char& operator[](int position) const
{ return data[position]; }
private:
char *data;
};
String s1 = "Hello";
cout << s1[0]; // 调用非const
// String::operator[]
const String s2 = "World";
cout << s2[0]; // 调用const
// String::operator[]
通过重载operator[]并给不同版本不同的返回值,就可以对const和非const String进行不同的处理:
String s = "Hello"; // 非const String对象
cout << s[0]; // 正确——读一个
// 非const String
s[0] = 'x'; // 正确——写一个
// 非const String
const String cs = "World"; // const String 对象
cout << cs[0]; // 正确——读一个
// const String
cs[0] = 'x'; // 错误!——写一个
// const String
另外注意,这里的错误只和调用operator[]的返回值有关;operator[]调用本身没问题。 错误产生的原因在于企图对一个const char&赋值,因为被赋值的对象是const版本的operator[]函数的返回值。
还要注意,非const operator[]的返回类型必须是一个char的引用——char本身则不行。如果operator[]真的返回了一个简单的char,如下所示的语句就不会通过编译:
s[0] = 'x';
因为,修改一个“返回值为固定类型”的函数的返回值绝对是不合法的。即使合法,由于C++“通过值(而不是引用)来返回对象”(见条款22)的内部机制的原因,s.data[0]的一个拷贝会被修改,而不是s.data[0]自己,这就不是你所想要的结果了。
让我们停下来看一个基本原理。一个成员函数为const的确切含义是什么?有两种主要的看法:数据意义上的const(bitwise constness)和概念意义上的const(conceptual constness)。
bitwise constness的坚持者认为,当且仅当成员函数不修改对象的任何数据成员(静态数据成员除外)时,即不修改对象中任何一个比特(bit)时,这个成员函数才是const的。bitwise constness最大的好处是可以很容易地检测到违反bitwise constness规定的事件:编译器只用去寻找有无对数据成员的赋值就可以了。实际上,bitwise constness正是C++对const问题的定义,const成员函数不被允许修改它所在对象的任何一个数据成员。
不幸的是,很多不遵守bitwise constness定义的成员函数也可以通过bitwise测试。特别是,一个“修改了指针所指向的数据”的成员函数,其行为显然违反了bitwise constness定义,但如果对象中仅包含这个指针,这个函数也是bitwise const的,编译时会通过。这就和我们的直觉有差异:
class String {
public:
// 构造函数,使data指向一个
// value所指向的数据的拷贝
String(const char *value);
...
operator char *() const { return data;}
private:
char *data;
};
const String s = "Hello"; // 声明常量对象
char *nasty = s; // 调用 operator char*() const
*nasty = 'M'; // 修改s.data[0]
cout << s; // 输出"Mello"
显然,在用一个值创建一个常量对象并调用对象的const成员函数时一定有什么错误,对象的值竟然可以修改!(关于这个例子更详细的讨论参见条款29)
这就导致conceptual constness观点的引入。此观点的坚持者认为,一个const成员函数可以修改它所在对象的一些数据(bits) ,但只有在用户不会发觉的情况下。例如,假设String类想保存对象每次被请求时数据的长度:
class String {
public:
// 构造函数,使data指向一个
// value所指向的数据的拷贝
String(const char *value): lengthIsValid(false) { ... }
...
size_t length() const;
private:
char *data;
size_t dataLength; // 最后计算出的
// string的长度
bool lengthIsValid; // 长度当前
// 是否合法
};
size_t String::length() const
{
if (!lengthIsValid) {
dataLength = strlen(data); // 错误!
lengthIsValid = true; // 错误!
}
return dataLength;
}
这个length的实现显然不符合“bitwise const”的定义——dataLength 和lengthIsValid都可以修改——但对const String对象来说,似乎它一定要是合法的才行。但编译器也不同意, 它们坚持“bitwise constness”,怎么办?
解决方案很简单:利用C++标准组织针对这类情况专门提供的有关const问题的另一个可选方案。此方案使用了关键字mutable,当对非静态数据成员运用mutable时,这些成员的“bitwise constness”限制就被解除:
class String {
public:
... // same as above
private:
char *data;
mutable size_t dataLength; // 这些数据成员现在
// 为mutable;他们可以在
mutable bool lengthIsValid; // 任何地方被修改,即使
// 在const成员函数里
};
size_t String::length() const
{
if (!lengthIsValid) {
dataLength = strlen(data); // 现在合法
lengthIsValid = true; // 同样合法
}
return dataLength;
}
mutable在处理“bitwise-constness限制”问题时是一个很好的方案,但它被加入到C++标准中的时间不长,所以有的编译器可能还不支持它。如果是这样,就不得不倒退到C++黑暗的旧时代去,在那儿,生活很简陋,const有时可能会被抛弃。
类C的一个成员函数中,this指针就好象经过如下的声明:
C * const this; // 非const成员函数中
const C * const this; // const成员函数中
这种情况下(即编译器不支持mutable的情况下),如果想使那个有问题的String::length版本对const和非const对象都合法,就只有把this的类型从const C * const改成C * const。不能直接这么做,但可以通过初始化一个局部变量指针,使之指向this所指的同一个对象来间接实现。然后,就可以通过这个局部指针来访问你想修改的成员:
size_t String::length() const
{
// 定义一个不指向const对象的
// 局部版本的this指针
String * const localThis =
const_cast<String * const>(this);
if (!lengthIsValid) {
localThis->dataLength = strlen(data);
localThis->lengthIsValid = true;
}
return dataLength;
}
做的不是很漂亮。但为了完成想要的功能也就只有这么做。
当然,如果不能保证这个方法一定可行,就不要这么做:比如,一些老的“消除const”的方法就不行。特别是,如果this所指的对象真的是const,即,在定义时被声明为const,那么,“消除const”就会导致不可确定的后果。所以,如果想在成员函数中通过转换消除const,就最好先确信你要转换的对象最初没有被定义为const。
还有一种情况下,通过类型转换消除const会既有用又安全。这就是:将一个const对象传递到一个取非const参数的函数中,同时你又知道参数不会在函数内部被修改的情况时。第二个条件很重要,因为对一个只会被读的对象(不会被写)消除const永远是安全的,即使那个对象最初曾被定义为const。
例如,已经知道有些库不正确地声明了象下面这样的strlen函数:
size_t strlen(char *s);
strlen当然不会去修改s所指的数据——至少我一辈子没看见过。但因为有了这个声明,对一个const char *类型的指针调用这个函数时就会不合法。为解决这个问题,可以在给strlen传参数时安全地把这个指针的const强制转换掉:
const char *klingonGreeting = "nuqneH"; // "nuqneH"即"Hello"
//
size_t length =
strlen(const_cast<char*>(klingonGreeting));
但不要滥用这个方法。只有在被调用的函数(比如本例中的strlen)不会修改它的参数所指的数据时,才能保证它可以正常工作。