《Effective C++》 rule 03: Use const whenever possible
const 可以修饰哪些东西?
-
const 可以修饰全局 (global) 或命名空间 (namespace) 或类外部的常量.
-
const 也可以修饰文件、函数或作用域中(block scope)被声明为 static 的对象.
-
const 还可以修饰类内部的静态 (static) 和非静态 (non-static) 成员变量.
-
const 与指针结合时,const 既可以修饰指针本身,也可以修饰指针所指对象.
- 当 const 在
*
左边时,表示指针指向的对象是常量. - 当 const 在
*
右边时,表示指针本身是常量(即指针始终指向这个对象). - 当 const 在
*
两边时,表示指针本身和指针指向的对象都是常量.
例如:
char greeting[] = Hello; char* p = greeting; // non-const pointer, non-const data (指针本身和指针所指对象都不是常量) const char* p = greeting; // non-const pointer, const data(指针所指对象是常量,指针不是常量) char* const p = greeting; // const pointer, non-const data(指针是常量,而指针所指对象不是) const char* const p = greeting; // const pointer, const data(指针本身和指针所指对象都是常量)
PS:有时 const 不一定写在对象类型之前,而是在对象类型和
*
之间,这也能用来表示指针所指对象是常量.例如:
void f1(const Widget* pw); void f2(Widget const* pw);
这两种写法等价,都表示 pw 指向的是一个
const Widget*
对象. - 当 const 在
STL 迭代器与指针
迭代器的作用就像是一个
T*
指针!
有一个需要注意的地方:声明一个迭代器为 const 时,实际上是相当于 T* const
指针.
- 即,指针自身是常量,而指针所指对象实际上是可以改变的!
而如果我们想表示指针所指对象为常量,则需要使用 const _iterator
.
例:
std::vector<int> vec; ... // 相当于 T* const const std::vector<int>::iterator iter = vec.begin(); // 即 iter 本身是常量 *iter = 10; // 没问题,修改迭代器指向的对象 ++iter; // 错误:迭代器是个常量 // 相当于 const T* std::vector<int>::const_iterator cIter = vec.begin(); // 即 iter 所指对象是常量 *cIter = 10; // 错误:iter 所指对象是常量 ++cIter; // 没问题,修改迭代器指向的对象
尽量多使用 const
对于函数返回值,如果没有修改参数或局部变量的需要,就应该将它的返回值声明为
const
这样的目的主要是为了安全,防止程序员因为失误对不该修改的函数返回值、参数或对象进行修改.
const 成员函数
Q:为什么要有 const 成员函数?
A:原因有二.
- 这样可以知道函数可以改变对象内容.
- 这样可以 操作 const 对象.
一个重要的 C++ 特性
两个成员函数,如果只是常量性(constness)不同,是可以被重载的!
也就是说,对于同样的一种“操作”,可以有处理 const 对象和 non-const 对象的两套逻辑.
看一个例子,有个类 TextBlock
,用来表示一段文字:
class TextBlock { public: ... const char& operator[](std::size_t position) const { // 处理 const 对象的 operator[] return text[position]; } char& operator[](std::size_t position) { // 处理 non-const 对象的 operator[] return text[position]; } private: std::string text; };
对于 operator[]
,可以这么调用:
TextBlock tb(Hello); std::cout << tb[0]; // 调用 non-const 对象版本的 operator[] const TextBlock ctb(World); std::cout << ctb[0]; // 调用 const 对象版本的 operator[]
还有一种更常见的调用 const 对象版本函数/运算符的例子:
void print(const TextBlock& ctb) { std::cout << ctb[0]; // 调用 const 对象版本的 operator[], 即 const TextBlock::operator[] }
重载的两个版本的函数,主要的区别就是,处理 const 对象的版本的函数不可以修改对象(很好理解,因为对象是 const 的嘛):
std::cout << tb[0]; // 没问题,读一个 non-const 对象 tb[0] = 'x'; // 没问题,修改一个 non-const 对象 std::cout << ctb[0]; // 没问题,读一个 const 对象 ctb[0] = 'x'; // 有问题!不能修改一个 const 对象
这里还有一个要注意的点:non-const operator[]
的返回值类型是 char&
而不是 char
.
- 如果返回值类型是
char
,则语句tb[0] = 'x';
无法通过编译.- 因为 char 是内置类型,如果函数的返回值是个内置类型,那么修改函数返回值是不合法的!
- 因为我们要修改对象,所以得传递引用(pass by reference),否则只是修改一个副本,没法起到修改对象的作用.
bitwise constness (physical constness) 和 logical constness
bitwise constness: 成员函数只有不修改对象的任何成员变量(当然 static 成员变量除外)时才可以声明为 const.
也就是说,bitwise constness 没有修改对象中的任何一个 bit (所以叫 bitwise constness).
这其实也是 const 本身的定义. 即没有对修饰的对象内的任何 non-static 成员变量进行修改.
What's wrong with bitwise constness?
bitwise constness
有个问题.
考虑这样一种情况:
- 类中有个指针对象,一个 bitwise constness 的成员函数没有修改这个指针对象.
- 不过,这个指针指向的对象不属于这个类.
- 因此,在这个 bitwise constness 的成员函数中有可能会对这个指针所指对象进行了修改.
- 不过这也是完全合法的(编译通过),因为理论上这个指针所指对象并不属于这个类(只有这个指针属于中这个类),而这里只是对指针所指对象进行修改(而没有修改指针本身),因此是完全 ok 的.
例子:
class CTextBook { public: ... char& operator[] (std::size_t position) const { // bitwise const 声明 return pText[position]; } private: char* pText; }
注意,这里的返回值是 char&,是个引用,所以有可能会修改指针所指对象的值.
- 然而,这里并没有修改
pText
这个指针,因此编译器认为这个成员函数声明为 const 没有问题.
而正如刚才所说,这里是可以修改指针所指对象的值的:
const CTextBook cctb(Hello); // 声明一个常量对象 char* pc = &cctb[0]; // 调用 const operator[] 取得一个指针指向 cctb 的数据 *pc = 'J'; // cctb 现在有了 Jello 这样的内容.
语法上,上面的代码并没有问题. 指针 pc 并没有修改,只修改了 pc 指向的对象 cctb
, 而 cctb
并不属于 CTextBook
,所以 operator[]
是符合一个 const 成员函数的要求滴~
但是逻辑上,这里还是产生了一些 意外的修改. 于是乎便有了 logical constness
logical constness
logical constness
与 bitwise constness
的不同在于: logical constness
不在乎是否(物理上)修改了类对象的 bits, 它的关注点在逻辑上是否有 产生修改 (不管修改的是什么).
- 换句话说,
logical constness
允许(物理上)修改类对象,而主要保证逻辑上不要产生错误的修改.
考虑刚才的 CTextBook
例子,我们可能有这样一个需求:缓存文本的长度,并根据缓存的长度去读这段文本.
那么有如下代码:
class CTextBook { public: ... std::size_t length() const; private: char* pText; std::size_t textLength; // 缓存的已读的文本的长度 bool lengthIsValid; // 判断当前长度是否有效(文本是否还可以继续读) }; std::size_t CTextBook::length() const { if (!lengthIsValid) { textLength = std::strlen(pText); lengthIsValid = true; } }
当然,这段代码会报错,因为尝试在 一个 const 成员函数 CTextBook::length()
内修改类 CTextBook
的两个成员变量 textLength
和 lengthIsValid
.
解决办法是:使用 mutable
关键字释放掉 non-static 成员变量的 bitwise constness 约束:
class CTextBook { public: ... std::size_t length() const; private: char* pText; // 声明:即使在 const 成员函数内,这两个成员变量也有可能会被修改 mutable std::size_t textLength; mutable bool lengthIsValid; }; std::size_t CTextBook::length() const { if (!lengthIsValid) { // 在 const 成员函数内修改 mutable 成员变量,没问题 textLength = std::strlen(pText); lengthIsValid = true; } }
这里,(物理上)修改了类对象的 non-static
对象,而保证了逻辑上没有错误的修改,这就是 logical constness
.
在 const 和 non-const 成员函数中避免重复
前面提到了:两个成员函数,如果只是常量性(constness)不同,是可以被重载的!
那么对于重载的这两个函数,里面可能有大量的重复代码:
class TextBook { public: ... cosnt char& operator[] (std::size_t position) const { ... // 边界检查(bounds checkint) ... // 访问日志数据(log access data) ... // 校验数据完整性(verify data integrity) return text[position]; } char& operator[] (std::size_t position) { ... // 边界检查(bounds checkint) ... // 访问日志数据(log access data) ... // 校验数据完整性(verify data integrity) return text[position]; } private: std::string text; };
显然,这里对于 constness 不同的函数的重载导致了很多代码重复,这会带来很多问题——比如:
- 不好维护
- 代码量膨胀
- 编译时间增长
解决办法:casting away constness
一句话概括:在处理 非const 对象版本的函数中调用处理 const 对象版本的函数。
- 这样,同样的代码只写了一份.
上面的例子可以改成这样:
class TextBook { public: ... // 处理 const 对象版本的函数还是原来的样子 cosnt char& operator[] (std::size_t position) const { ... // 边界检查(bounds checkint) ... // 访问日志数据(log access data) ... // 校验数据完整性(verify data integrity) return text[position]; } // 处理 非const 对象版本的函数调用 const char& operatorp[] char& operator[] (std::size_t position) { /* 这里的 const_cast 的作用是为了去调用 const op[] 而 static_cast 的作用则是消除了 调用的 op[] 的 constness */ return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]); } private: std::string text; };
这样,通过将 const_cast 和 static_cast 结合,就实现了用 const 成员函数实现 非const 成员函数,避免了代码重复!
总结
- 尽量多使用 const,const 可以帮助编译器检查一些错误用法(比如不该被修改的数据等).
- const 可以施加在任何作用域内的对象、函数参数、函数返回类型、成员函数.
- 编译器在语法上做的检查时 bitwise constness,而实际编写程序时我们应该用 logical constness(必要时使用 mutable).
- 当 const 成员函数和 non-const 成员函数有着实质上等价的实现时,为了避免代码重复,可以使用 const_cast 结合 static_const. 从而在 非const 成员函数中调用 const 成员函数.