Effective C++读书笔记(2)
条款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的专栏