Effective C++读书笔记(2)

来源:岁月联盟 编辑:exp 时间:2012-02-04

条款03:尽可能使用const

Use const whenever possible.

         const允许你告诉编译器和其他程序员某值应该保持不变。

         如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

1. char greeting[] = "Hello";

2. char* p = greeting;         //non-const pointer, non-const data

3. const char* p = greeting;       //non-const pointer, const data

4. char* const p = greeting;       //const pointer, non-const data

5. const char* const p = greeting; //const pointer, const data

         如果被指物是常量,既可以关键字const写在类型之前,又可以把它写在类型之后、星号之前。两种写法的意义等价:

1.  void f1(const Widget* pw);    //f1获得一个指针,指向一个常量Widget对象.. 

2.  void f2(Widget const * pw);   //f2也是

         STL迭代器系以指针为底层塑模出来,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const 指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果希望迭代器所指的东西(数据)不可被改动(即希望STL模拟一个const T* 指针),则用const_iterator:

1.  std::vector<int> vec; 
2.  ... 
3.  const std::vector<int>::iterator iter = vec.begin( ); //T* const
4.  *iter = 10;                     //没问题,改变iter所指物 
5.  ++iter;                         //错误!iter是const 
6.  std::vector<int>::const_iterator cIter = vec.begin( );// const T* 
7.  *cIter = 10;                        //错误! *cIter是const 
8.  ++cIter;                        //没问题,改变cIter。
         令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。

1.  class Rational { ... }; 
2.  const Rational operator* (const Rational& lhs,const Rational& rhs);
    为什么返回一个const对象?原因是如果不这样客户就能实现这样的行为:

1.  Rational a, b, c; 
2.  ... 
3.  (a * b) = c;        //在a * b的成果上调用operator=
         如果a和b都是内置类型,这样的代码直截了当就是不合法。而一个"良好的用户自定义类型"的特征是它们避免无端地与内置类型不兼容。将operator* 的回传值声明为const可以预防那个荒唐的赋值动作。

 

const成员函数

         将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这类成员函数可以得知哪个函数可以改动对象内容而哪个函数不行,很是重要。

         两个成员函数如果只是常量性不同,可以被重载。这实在是一个重要的C++特性(前几天的面试刚碰到过):

1.  class TextBlock { 
2.  public: 
3.    ... 
4.    const char& operator[](std::size_t position) const
5.    { return text[position]; }        // operator[] for const对象. 
6.    char& operator[](std::size_t position)      
7.    { return text[position]; }        // operator[] for non-const对象. 
8.  private: 
9.    std::string text; 
10. }; 
11. 
12. TextBlock tb("Hello"); 
13. std::cout << tb[0];     //调用non-const TextBlock::operator[] 
14. const TextBlock ctb("World"); 
15. std::cout << ctb[0];        //调用const TextBlock::operator[]
         只要重载operator[]并对不同的版本给予不同的返回类型,就可以令const和non-const TextBlocks获得不同的处理:

1.  std::cout << tb[0];//没问题 - 读一个non-const TextBlock 
2.  tb[0] = 'x';        //没问题 - 写一个non-const TextBlock 
3.  std::cout << ctb[0];//没问题 - 读一个const TextBlock 
4.  ctb[0] = 'x';   //错误! - 写一个const TextBlock
         上述错误只因operator[] 的返回类型以致,至于operator[] 调用动作自身没问题。

         请注意,non-const operator[] 的返回类型是个reference tochar,不是char。如果operator[]只是返回一个char,下面这样的句子就无法通过编译:

1.  tb[0] = 'x';
         返回类型是内置类型的函数,改动函数返回值不合法。纵使合法,C++以值传递意味被改动的其实是tb.text[0]的一个副本,不是tb.text[0]自身。

l  将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

 

         成员函数如果是const意味什么?

         bitwise const:成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const,也就是说它不更改对象内的任何一个bit。bitwise constness正是C++ 对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。

         不幸的是许多成员函数虽然不十足具备const性质却能通过bitwise测试:

1. class CTextBlock { 

2. public: 

3.    ...

4.    char& operator[](std::size_t position) const // bitwise const声明, 

5.    { return pText[position]; }      // 但其实不适当.跟之前相比少了一个const!

6. private: 

7.    char* pText;

8. };

         operator[]实现代码并不更改私有变量pText,于是编译器为operator[]产出目标码,并认定它是bitwiseconst。

1.  const CTextBlock cctb("Hello");//声明一个常量对象。 
2.  char* pc = &cctb[0];//调用const operator[]取得一个指针, 指向cctb的数据。 
3.  *pc = 'J';              //cctb现在有了 "Jello" 这样的内容。
         以上代码没有任何错误:创建一个常量对象并设以某值,而且只对它调用const成员函数。但终究还是改变了它的值。

         这种情况导出所谓的logical constness:一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此,也就是自己可以改用户不能改。

1.  class CTextBlock { 
2.  public: 
3.     ... 
4.     std::size_t length() const; 
5.  private: 
6.     char* pText; 
7.     std::size_t textLength; //最近一次计算的文本区块长度。 
8.     bool lengthIsValid; //目前的长度是否有效。 
9.  }; 
10. std::size_t CTextBlock::length() const 
11. { 
12.    if (!lengthIsValid) { 
13.       textLength = std::strlen(pText);
14.       //错误!在const成员函数内不能对私有变量textLength进行修改 
15.       lengthIsValid = true;      
16.       //错误!在const成员函数内不能对私有变量lengthIsValid进行修改
17.    }                          
18.    return textLength;  }
         解决办法很简单:利用C++ 中的mutable(可变的)修饰符,mutable释放掉non-static成员变量的bitwiseconstness约束:

1.  class CTextBlock { 
2.  public: 
3.     ... 
4.     std::size_t length() const; 
5.  private: 
6.     char* pText; 
7.     mutable std::size_t textLength; //这些成员变量可能总是会被更改,                
8.     mutable bool lengthIsValid;     //即使在const成员函数内。
9.  };                              //现在,刚才的length函数就可以了~
l  编译器强制实施bitwise constness,但你编写程序时应该使用"概念上的常量性"(conceptual constness)。

 

在const和non-const成员函数中避免重复

         对于"bitwise-constness非我所欲"的问题,mutable是个解决办法,但它不能解决所有的const相关难题。举个例子,假设TextBlock(和 CTextBlock)内的operator[] 不单只是返回一个reference指向某字符,也执行边界检验(boundschecking)、志记访问信息(loggedaccess info.)、甚至可能进行数据完善性检验。把所有这些同时放进const和non-const operator[] 中,导致两个版本的operator[]及大量的代码重复。

         真正该做的是实现operator[]的机能一次并使用它两次,也就是说,令其中一个调用另一个。本例中constoperator[]完全做掉了non-const版本该做的一切,唯一的不同是其返回类型多了一个const资格修饰。

1.  class TextBlock { 
2.  public: 
3.    ... 
4.    const char& operator[](std::size_t position) const //一如既往 
5.    { 
6.      ... 
7.      return text[position]; 
8.    } 
9.    char& operator[](std::size_t position)    //现在只调用const op[] 
10.   { 
11.      return 
12.         const_cast<char&>(          //将op[]返回值的const转除 
13.           static_cast<const TextBlock&>(*this)//为*this加上const 
14.               [position]                    //调用const op[] 
15.         ); 
16.   } 
17. ... 
18. };
         这里共有两次转型:第一次用来为 *this添加const(这使接下来调用operator[]时得以调用const版本),第二次则是从constoperator[]的返回值中移除const。

         添加const的那一次转型强迫进行了一次安全转型(将non-const对象转为const对象),所以我们使用static_cast。移除const的那个动作只可以藉由const_cast完成,没有其他选择。

         简单来说就是non-const版本为了调用const版本先转换常量性,应用const版本功能完毕后,为了符合non-const的返回值,再去除常量性。

         const成员函数承诺绝不改变其对象的逻辑状态(logicalstate),non-const成员函数却没有这般承诺。如果在const函数内调用non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象被改动了。这就是为什么"const成员函数调用non-const成员函数"是一种错误行为:因为对象有可能因此被改动。

l  当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

 摘自 pandawuwyj的专栏