const的意义
const与指针
首先复习一下const修饰指针的含义:
如果const出现在*左边,被指物是常量
const出现在*右边,指针自身是常量
两边都有 指向常量的常量指针
至于他们相对类型名的次序则无关紧要
1
2 void f(const Widget* pw);
void f(Widget const* pw);
两种写法的意义相同
const与迭代器
STL迭代器以指针为根据塑造出来,因此iterator就类似于一个T*指针。那么同理,把迭代器声明为const就像把指针声明为const一样(T *const);这意味着该迭代器不得指向不同的东西。
如果你希望迭代器的指向物不可修改,那你应该使用STL中的const_iterator,用它来模拟const T*。
const与返回值
我们应该尽量令函数返回一个常量值,从而在不放弃安全和高效的前提下降低因客户失误而造成的意外。(诸如运算符的返回值应该是一个const对象)
const 成员函数
用const修饰成员的目的是为了确认该成员函数可以作用于const对象。它们有两大重要特性:
- 它们使用class接口时我们可以清楚看到哪些函数可以改变函数的内容。
- 它们可以操作const对象,这是编写高效代码的关键。因为改善c++程序效率的一个根本办法就是使用pass by reference-to-const方式传递对象。
const成员函数的另一个易被忽视之处在于常量性的不同可以构成重载。
最常见的例子莫过于下标运算符的定义,const的下标运算符返回const引用,普通则返回普通引用。这样则避免了试图修改一个const对象。
成员函数是const意味着什么?
这一问题直接引发了2个流派之争:bitwise constness与logical constness
bitwise constness阵营的人认为成员函数只有在不改变对象内任何一个成员变量的情况下才可以称为const。
这听起来很有道理,但实际上很多不具备const性质的成员函数却能够通过bitwise测试。举例而言,假如一个class内有一个指针,我们通过指针修改指针指向的对象,那么它不会引发编译器异议。最终,我们创建了一个常量对象并对它调用了const成员函数,可是最终它的值还是被改变了。
这种情况导出了logical constness。这一派拥护者指出,一个const成员可以修改所处理对象的某些数据,但只有在客户端检测不出的情况才能如此。这种情况是很直观的,但是编译器只能辨别bitwise constness.为了避免这种无谓的情况,我们可以用mutable来修饰non-static成员变量,在样就算在const成员里我们也可以修改它们。
然而mutable并不能解决所有的问题。至少在以下情况不能:
假设下标运算符不仅需要返回,还需要进行边界检测,扩展边界等操作。显然,这样的下标运算符的const性质十分繁琐,而且伴随着大量的代码重复。
我们真正需要做的是实现const下标运算符的功能,并在non-const函数中调用它,然后再去除它返回对象的const属性。在这里去除const是安全的,因为不管谁调用non-const operator[]都必须要有一个non-const对象。其实现如下:1
2
3
4char &operator[](std::size_t position){
return const_cast<char&>
(static_cast<const TextBlocks>(*this)[])
}
很显然,我们先把this转为了const对象,这样就可以调用const版本的下标运算符。再把const函数返回的对象去除了const属性,我们只有const_cast一种方法来实现这种操作。
之所以我们不在const成员函数里调用普通成员函数,是因为这一点及其危险:const成员承诺不修改对象,而non-const成员却可能修改对象。而且要调用non-const函数的话你必须先通过const_cast去掉this的const性质,这简直就是雪崩的前兆。通过non-const函数调用const函数自然很安全。
总结
- 将某些东西声明为const可帮助编译器侦测出错误用法。尤其是返回值可以用const修饰。
- 编译器强制实施bitwise constness,但编写程序时应该活用mutable,做到logical constness.
- 当const函数与non-const函数实现几乎等价时,我们应该在non-const函数中调用const版本以避免代码重复。