《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* 对象.

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:原因有二.

  1. 这样可以知道函数可以改变对象内容.
  2. 这样可以 操作 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 constnessbitwise 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 的两个成员变量 textLengthlengthIsValid.

解决办法是:使用 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_caststatic_cast 结合,就实现了用 const 成员函数实现 非const 成员函数,避免了代码重复!

总结

  • 尽量多使用 const,const 可以帮助编译器检查一些错误用法(比如不该被修改的数据等).
  • const 可以施加在任何作用域内的对象、函数参数、函数返回类型、成员函数.
  • 编译器在语法上做的检查时 bitwise constness,而实际编写程序时我们应该用 logical constness(必要时使用 mutable).
  • 当 const 成员函数和 non-const 成员函数有着实质上等价的实现时,为了避免代码重复,可以使用 const_cast 结合 static_const. 从而在 非const 成员函数中调用 const 成员函数.