《Effective C++》学习笔记(七)
2014-08-30 22:25
190 查看
原创文章,转载请注明出处:/article/8316774.html
只要你定义了一个变量,而且它具有一个构造、析构函数,那么它将会要有构造和析构的成本。即使变量最终并未被使用,这个成本也仍会耗费,我们应该也要避免这种情况。
或许你会认为你不可能你不可能定义一个不使用的变量,但是想想有没有把异常的情况考虑进去。比如:
所以延后它的定义式,放到if判断之后,确保没有异常抛出再定义。
但是这个代码还是有问题,因为目前它调用的是默认构造函数,但是一般定义出来之后都会给它赋初值。所以可以将构造后赋值改成,直接在构造时指定初值。
我们不只应该延后变量的定义,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造非必要对象,还可以避免无意义的默认构造函数。
可能你也会和我一样,考虑过变量是定义在循环外还是循环内,如下:
这两个方法,做法A是1个构造函数 + 1个析构函数 + n个复制操作
做法B是n个构造函数 + n个析构函数
如果一个赋值的成本低于一组构造+析构的成本,那么A大体而言比较高效,尤其是n比较大的时候。
否则,B或许更好,因为A发明合法中w的作用域比B大,这对程序的可理解性和易维护性造成冲击,因此除非1)你知道赋值成本比“构造+析构”成本低,2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法B。
总结:
尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。理论上如果你的程序通过了编译,就表示它并不企图在任何对象上执行不安全、无意义、荒谬的动作。
不幸的是转型破坏了类型系统。它可能导致任何种类的问题,有些容易发现,有些却很难。
转型分为旧式转型和新式转型,旧式转型指的是C风格的转型,如:
(T) expression
T (expression)
这两种形式没太大差别,只是括号位置不同,C++还提供了四种新式转型:
1)它们在代码中容易被辨识出来,以便查错。
2)各种动作目标转型动作的目标愈窄化,编译器愈可能诊断出错误,如去掉常量性,除非用const_cast,否则无法通过编译。
许多程序员认为转型其实什么都没做,只是告诉编译器把某种类型视作另一种类型,这是一个错误的观念。任何一个类型转换,往往真的令编译器编译出运行期间执行的代码。
上述例子表名一个对象可能拥有一个以上的地址(如”以Base*指向它“时的地址和”以Derived*指向它“时的地址)。
看完这个,我们来看一下dynamic_cast,dynamic_cast的很多实现版本的执行速度比较慢。
之所以需要dynamic_cast通常是因为你想在一个你认定为Derived class 的对象身上执行Derived class操作函数,但是你手上只有一个指向base的pointer或者reference,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题:
1)使用容器,并在其中存储直接执行派生类对象的指针(通常是智能指针),如此便消除额”通过基类接口处理对象“的需要。
2)在基类中提供virtual函数做你想对各个派生类做的事。举个例子,虽然只有SpecialWindow可以闪烁,但或许将闪烁函数声明于Base class内并提供一份什么也没做的默认实现码是有意义的。
优良的C++代码很少使用转型,但要完全摆脱它们又不太切实际。
总结:
1)如果可以,尽量避免转型,贴别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
2)如果转型是必要的,试着将它隐藏于某个函数背后,客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
3)宁可使用C++Style(新式)转型,不要使用旧型转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
书上给了个矩形的例子,我们来看看:
我们可以看到Rectangle有upperLeft、lowerRight两个方法,编译可以通过,但是实际上是有问题的,为什么呢,其实之前提到过,因为它们的返回值可以用来更改pData内部数据。
这给我带来了两个教训:
1)成员变量的封装性最多只等于“返回其reference”的函数的访问级别。
2)如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。(之前有提过bitwise constness)
上面我们返回的是reference,但事实上对于指针和迭代器也是适用的,他们都是handles(号码牌,用来取得某个对象)。
其实我们在返回值Point&之前加上const就可以解决这两个问题。这样用户可以读取Points,但是无法修改。
但即使如此,upperLeft和lowerRight还是返回呢“代表对象内部”的handles,有可能在其他场合带来问题。更明确的说,它可能导致dangling handles(空悬的号码牌)。举个例子:
这并不意味着你一定不能让成员函数返回handle。有时候你必须那么做,比如operator[]就允许你“采摘”string和vector的个别元素,而这些operator[]s就是返回reference指向“容器内的数据”,那些数据会伴随着容器的销毁而销毁。尽管如此,这样的函数只是例外不是常态。
总结:
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条件可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
前言
接下来的两篇笔记将围绕《Effective C++》第五章《实现》来学习。
条款26:尽可能延后变量定义式的出现时间
只要你定义了一个变量,而且它具有一个构造、析构函数,那么它将会要有构造和析构的成本。即使变量最终并未被使用,这个成本也仍会耗费,我们应该也要避免这种情况。或许你会认为你不可能你不可能定义一个不使用的变量,但是想想有没有把异常的情况考虑进去。比如:
string encryptPassword(const string &password) { using namespace std; string encryted; if (password.length() < MinimumPasswordLength) { ... } return encryted; }对象encryted并没完全使用,但如果有个异常被丢出,它就真的被使用了,但是你必须要付出它的构造析构成本。
所以延后它的定义式,放到if判断之后,确保没有异常抛出再定义。
但是这个代码还是有问题,因为目前它调用的是默认构造函数,但是一般定义出来之后都会给它赋初值。所以可以将构造后赋值改成,直接在构造时指定初值。
我们不只应该延后变量的定义,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造非必要对象,还可以避免无意义的默认构造函数。
可能你也会和我一样,考虑过变量是定义在循环外还是循环内,如下:
// 方法A:定义于循环外 Widget w; for (int i = 0; i < n; ++i) { w = 取决于i的某个值; ... } | // 方法B:定义于循环内 for (int i = 0; i < n; ++i) { Widget w(取决于i的某个值); ... } |
做法B是n个构造函数 + n个析构函数
如果一个赋值的成本低于一组构造+析构的成本,那么A大体而言比较高效,尤其是n比较大的时候。
否则,B或许更好,因为A发明合法中w的作用域比B大,这对程序的可理解性和易维护性造成冲击,因此除非1)你知道赋值成本比“构造+析构”成本低,2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法B。
总结:
尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
条款27:尽量少做转型动作
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。理论上如果你的程序通过了编译,就表示它并不企图在任何对象上执行不安全、无意义、荒谬的动作。不幸的是转型破坏了类型系统。它可能导致任何种类的问题,有些容易发现,有些却很难。
转型分为旧式转型和新式转型,旧式转型指的是C风格的转型,如:
(T) expression
T (expression)
这两种形式没太大差别,只是括号位置不同,C++还提供了四种新式转型:
const_cast< T >(expression) // 将对象的常量性质移除 dynamic_cast< T >(expression) // 执行“安全向下转型”,如pointer-to-base转换成pointer-to-derived reinterpret_cast< T >(expression) // 执行低级转型,如将point int转换成int,在低级代码以外很少见 static_cast< T >(expression) // 强迫隐式转换,例如将non-const变为const,将pointer-to-base转换成pointer-to-derived,等等多种转换,但是他不能将const转换成non-const,因为那是const_cast才办得到旧式转型依旧合法,但是新式转型比较受欢迎,因为:
1)它们在代码中容易被辨识出来,以便查错。
2)各种动作目标转型动作的目标愈窄化,编译器愈可能诊断出错误,如去掉常量性,除非用const_cast,否则无法通过编译。
许多程序员认为转型其实什么都没做,只是告诉编译器把某种类型视作另一种类型,这是一个错误的观念。任何一个类型转换,往往真的令编译器编译出运行期间执行的代码。
class Base { ... } class Derived:public Base { ... } Derived d; Base *pb = &d;这里我们不过是简历一个base class指针指向一个derived class对象,但是有时候上述两个指针值并不相同。这种情况下会有个偏移量(offset)在运行期间被施行于Derived*指针上,用以取得正确的Base*指针值。
上述例子表名一个对象可能拥有一个以上的地址(如”以Base*指向它“时的地址和”以Derived*指向它“时的地址)。
看完这个,我们来看一下dynamic_cast,dynamic_cast的很多实现版本的执行速度比较慢。
之所以需要dynamic_cast通常是因为你想在一个你认定为Derived class 的对象身上执行Derived class操作函数,但是你手上只有一个指向base的pointer或者reference,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题:
1)使用容器,并在其中存储直接执行派生类对象的指针(通常是智能指针),如此便消除额”通过基类接口处理对象“的需要。
class Window { ... } class SpecialWindow: public Window { public: void blink(); ... }; typedef vector<shared_ptr<Window>> VPM; VPM winPtrs; for (auto iter : winPtrs) { if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get())) psw->blink(); }应该改成:
class Window { ... } class SpecialWindow: public Window { public: void blink(); ... }; typedef vector<shared_ptr<SpecialWindow>> VPSM; VPSM winPtrs; for (auto iter : winPtrs) { (*iter)->blink(); }
2)在基类中提供virtual函数做你想对各个派生类做的事。举个例子,虽然只有SpecialWindow可以闪烁,但或许将闪烁函数声明于Base class内并提供一份什么也没做的默认实现码是有意义的。
class Window { virtual void blink() { } ... } class SpecialWindow: public Window { public: void blink() { ... }; ... }; typedef vector<shared_ptr<Window>> VPM; VPM winPtrs; for (auto iter : winPtrs) { (*iter)->blink(); }不论哪一种写法——“使用类型安全容器”或“将virtual函数往继承体系上方移动”,都并非放之四海而皆准,但许多情况下它们提供了一个可行的dynamic_cast替代方案。当它们行得通时,你应该欣然拥抱他们。
优良的C++代码很少使用转型,但要完全摆脱它们又不太切实际。
总结:
1)如果可以,尽量避免转型,贴别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
2)如果转型是必要的,试着将它隐藏于某个函数背后,客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
3)宁可使用C++Style(新式)转型,不要使用旧型转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
条款28:避免返回handles指向对象内部成分
书上给了个矩形的例子,我们来看看:class Point // 用来表示坐标点 { public: Point(int x, int y); ... void setX(int newVal); void setY(int newVal); } struct RectData // 用点的数据来存储矩形 { Point ulhc; point lrhc; }; class Rectangle { public: Point& upperLeft() const { return pData->ulhc; } Point& lowerRight() const { return pData->lrhc; } ... private: std::shared_ptr< RectData ><rectdata> pData; ... }</rectdata>
我们可以看到Rectangle有upperLeft、lowerRight两个方法,编译可以通过,但是实际上是有问题的,为什么呢,其实之前提到过,因为它们的返回值可以用来更改pData内部数据。
这给我带来了两个教训:
1)成员变量的封装性最多只等于“返回其reference”的函数的访问级别。
2)如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。(之前有提过bitwise constness)
上面我们返回的是reference,但事实上对于指针和迭代器也是适用的,他们都是handles(号码牌,用来取得某个对象)。
其实我们在返回值Point&之前加上const就可以解决这两个问题。这样用户可以读取Points,但是无法修改。
但即使如此,upperLeft和lowerRight还是返回呢“代表对象内部”的handles,有可能在其他场合带来问题。更明确的说,它可能导致dangling handles(空悬的号码牌)。举个例子:
class GUIobject { ... }; const Rectangle boundingBox(const GUIObject &obj); GUIObject *pgo; ... const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());upperLeft返回的是一个临时变量,在取地址的表达式结束后,这个变量就会析构了,所以pUpperLeft保存的指针也就变成了空悬、虚吊的了。
这并不意味着你一定不能让成员函数返回handle。有时候你必须那么做,比如operator[]就允许你“采摘”string和vector的个别元素,而这些operator[]s就是返回reference指向“容器内的数据”,那些数据会伴随着容器的销毁而销毁。尽管如此,这样的函数只是例外不是常态。
总结:
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条件可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
相关文章推荐
- 《Effective C++》学习笔记条款27 尽量少做转型动作
- 《Effective C++》学习笔记(一)
- 《Effective C++》学习笔记(六)
- 《effective c++》学习笔记(一)
- 《effective c++》学习笔记(四)
- 《effective c++》学习笔记(五)
- 《Effective C++》学习笔记——条款26
- 《effective c++》学习笔记(七)
- 《Effective C++》学习笔记(1)
- 《Effective C++》学习笔记条款02 尽量以const,enum,inline替换#define
- 《effective c++》学习笔记(一)
- 《effective c++》学习笔记(四)
- 《effective c++》学习笔记(四)
- 《effective c++》学习笔记(五)
- 《effective c++》学习笔记(七)
- 《Effective C++》学习笔记——条款30
- 《Effective C++》学习笔记——条款39
- 《Effective C++》学习笔记——条款40
- 《Effective C++》学习笔记条款03 尽可能使用const
- 《Effective C++》学习笔记条款19 设计class犹如设计type