【Effective C++读书笔记】篇十一(条款26~条款28)

来源:互联网 发布:3d游戏编程大师技巧 编辑:程序博客网 时间:2024/05/22 05:31

条款26:尽可能延后变量定义式的出现时间                                                                      


原因如下:

1、有的变量在定义之后并不一定会被使用到,尽可能延后其定义时间以确保变量一定会被使用,避免无意义的变量构建与析构费用;

2、有的时候我们会使用默认值构造对象,然后在真正要使用该变量的地方再调用赋值函数赋初值,相比之下,“尽可能延后”则可以使用初值直接初始化对象,如此构造对象不经能够避免构造(及析构)非必要对象,还可以避免无意义的 default 构造行为,让初始过程附带说明变量的目的。

举个例子:

fun(string &myname){  string name;  ...  name = myname;  ...}

fun(string &myname){  string name(myname);  ...}
相比,后者就会好很多。

“但循环怎么办呢?”

如下两种情况,那个好呢?

//方案A:定义在循环外                                    //方案B:定义在循环内Widget w;                                            for(int i = 0; i < n; ++i)           for(int i = 0; i < n; ++i)                           {{                                                      Widget w(取决于i的某个值);  w = 取决于i的某个值;                                    ...  ...                                                 }}                          
做法A:1次构造+1次析构+n次赋值

做法B:n次构造+n次析构


做法A大体而言比较高效些,特别是在n很大的时候。此外,做法A造成名称w的作用域(覆盖整个循环)比做法B更大,有时那对程序的可理解性和易维护性造成冲突。

因此我们的选择是:除非(1)你知道赋值成本比"构造+析构"成本低;(2)你正在处理代码中效率高度敏感的部分。否则你应该使用做法B。



请记住:尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。





条款27:尽量少做转型动作                                                                                               


C风格的转型:(T) expression函数风格的转型:T (expression)
以上两种均为“旧式转型”,除此之外,C++ 还提供四种新式转型:

const_cast<T> (expression)dynamic_cast<T> (expression)reinterpret_cast<T> (expression)static_cast<T> (expression)
使用新式转型较受欢迎。

1)它们很容易在代码中被辨识出来,因此得以简化“找出类型系统在哪个地点被破坏”;

2)各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。比如去常量性,只能使用 const_cast 。


此外还需小心,在C++里,单一对象可能拥有一个以上的地址(例如“以Base* 指向它”时的地址和"以 Derived* 指向它"时的地址)。它们之间有一个偏移量。对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味“由于知道对象如何布局”而设计的转型,在某一平台行得通,在其他平台并不一定行得通。这个世界有许多悲惨的程序员,他们历经千辛万苦才学到这堂课。


另一件关于转型有趣的事情是:我们很容易写出某些似是而非的代码(在其他语言中也许真是对的)。

如下面的代码:

class base{  public:    virtual void fun() {...}    ...};class derived: public base{  public:    virtual void fun()     {      static_cast<base>(*this).fun();    //将 *this 转型为base,然后调用其 fun()      ...                                //这里进行 derived 专属行为    }}
需要格外小心的是,这里的转型导致执行base类fun()的并不是当前derived对象,而是一个使用 derived 对象的 base 成分拷贝初始化而得 base 副本对象身上的 fun() 函数。

那么如果base::fun()修改了对象内容,改动的则是副本,derived::fun()专属行为也修改对象内容,则是真的修改了对象。这使当前对象进入一种“伤残”状态:其base class 成分的更改没有落实,而 derived class 成分的更改倒是落实了。


解决之道是:

class derived: public base{  public:    virtual void fun()     {      base::fun();    //调用base::fun()      ...             //这里进行 derived 专属行为    }}


请记住:

1、如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计;

2、如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内;

3、宁可使用C++新式转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有这分门别类的职掌。






条款28:避免返回handles指向对象内部成分                                                                  



有一个如下的设计:

class Point{  public:    Point(int x, int y);    ...    void setX(int newVal);    void setY(int newVal);};class Rectangle{  public:    Point& upperLeft() const {return ulhc;}    Point& lowerRight() const {return lrhc;}  private:    Point ulhc;      //左上角    Point lrhc;       //右下角}
这样的设计可以通过编译,但却是错误的,实际上它是自我矛盾的。一方面,upperLeft()和lowerRight()被声明为const成员函数,保证不修改对象;另一方面,这两个函数却都返回 references 指向 private 内部数据,调用者于是可以通过这些 references 更改内部数据!例如:

Point p1(0, 0);Point p2(100, 100);const Rectangle rec(p1, p2);(rec.upperLeft()).setX(50);


解决方案:

对 const 函数的返回类型加上 const,保证其不会被修改。



即便如此, upperLeft和lowerRight还是返回了“代表对象内部”的handles,有可能在其他场合带来问题。更确切地说,它可能导致空悬的handle,即“传出去的 handle 比其所在对象更长寿”这样的风险。最常见的就是函数返回值构成的临时变量。如下:

class GUIObject{...};const Rectangle boundingBox(const GUIObject& obj);
现在,客户有可能这么使用这个函数:

GUIObject *pgo;...const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
我们知道 boundingBox(*pgo) 返回的 Rectangle 对象是个临时变量,会在这句话结束后被销毁,但是这里却调用 upperLeft() 函数将该临时对象的内部变量的 handle 给传了出来,这就是一个致命的错误。



请记住:避免返回 handles (括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const ,并将发生虚吊 handles 的可能性将至最低。


0 0
原创粉丝点击