Effective C++学习第八天

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

          当你定义了一个变量,如果在使用变量之前出现异常,那么你得承受一次构造成本和析构成本,而且你没有使用该变量;本条款给出的建议是延迟变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义知道能够给它初值实参为止;这样不仅可以避免构造(析构)非必要的对象,还可以避免无意义的default构造行为;

条款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)各转型动作的目标比较窄,编译器容易诊断;

          转型并不是告诉编译器把某种类型视为另一种类型,任何一个类型转换(不论是通过转型操作而进行的显式转换或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的代码;

          对于一个base class指针指向一个derived class对象,有时候上述的两个指针值并不相同,它们之间会有一个偏移量在运行期间施加在derived*指针上来得到正确的base*指针值;转型代码错误分析:

class window{                                       class specialwindow:public window{

public:                                               public:

           virtual void onresize( ){...}                              virtual void onresize( ){

}                                                                                 static_cast<window>(*this).onresize( );//解决方法

                                                                                                       //window::onresize();

                                                                                 }

原本想着采用static_cast将派生类对象转化为基类对象执行onresize函数,然后再执行派生类的onresize函数;但实际上是在当前对象的副本上执行了window::onresize,在当前对象上执行是specialwindow专属动作(个人理解:相当于转型可以当做为创建一个副本执行了一次函数调用的过程);

对于dynamic_cast:用于你相对一个derived class执行derived class操作函数,但是你的手上只有一个指向base的pointer或者reference,你只能通过dynamic_cast向下寻找你的derived class对象,然而使用dynamic_cast会用到strcmp比较class名称,使得运行效率极低;改善这个问题的方法有两种:

          1)使用容器并在其中存储直接指向derived class对象指针(通常是智能指针),如此便消除了通过base class接口处理对象的需要(但这种情况下,你需要多个容器分别储存不同派生类的指针,而且每个容器都具备类型安全性),具体代码如下:

class window {...};

class specialwindow::public window{

public:

               void blink( );

};

typedef std::vector<std::trl:shared_ptr<specialwindow>vpsw;

vpsw winptrs;

for(vpsw::iterator iter=winptrs.begin( );iter!=winptrs.end( );iter++)

                  (*it)->blink( );

            2)通过base class接口处理所有可能之各种window派生类,也就是提供virtual函数;

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

           对于常见的pimpl设计方法(即数据和实现分离),有两个需要注意的地方:1)成员变量的封装性最多等于返回你reference的函数的访问级别;2)const成员函数传出来一个reference,后者(reference)所指数据与对象自身有关联,而它又被储存于对象之外,那么函数调用者可以修改那笔数据;

           绝对不要令成员函数返回一个指针指向访问级别较低的成员函数,如果你那么做,后者的访问级别就会被提高到较高者,因为客户可以去的一个指向访问级别更低的函数,然后通过那个指针去调用它;

           如果有一个handle(成员函数返回reference,指针或者迭代器)被传出去,那么就可以用这个handle访问对象的数据,对象的封装性也就下降了;

         避免返回handles(reference,指针,迭代器)指向对象内部,遵守这个规定,可以增加封装性,帮助const成员函数更像const,并将发生“虚吊号码牌(指针或引用指向不存在的对象)”的可能性降到最低;

条款29:为“异常安全”而努力是值得的

         当异常抛出时,带有异常安全性的函数会:1)不泄露资源;2)不允许数据败坏;

         异常安全函数提供一下三个保证中的一个:1)基本承诺;异常被抛出,程序内的任何事情仍然保持在有效的状态下,没有任何对象或者数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态;2)强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知,如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前的状态”,调用一个提供强烈保证的函数后,程序状态只有两种可能,如预期般到达函数成功执行后的状态或者回到函数被调用前的状态。3)不抛掷保证,承诺不抛掷异常,因为它们总能完成它们原先承诺的功能;

         强烈保证往往能够以copy-and-swap实现出来,但是强烈保证并非对所有的函数都有实现或者具备现实意义,一般基本承诺就可以了;

       函数提供的“异常安全性”通常最高只等于其所调用之各个函数的“异常安全性保证”中的最弱者。

条款31:透彻了解inlining的里里外外

           inlining函数的好处:产生较小的目标码,调用它们不需要承受函数调用所招致的额外开销;缺点是会导致程序体积过大(虚内存现象)inline造成代码膨胀导致额外的换页行为,降低高速缓存器装置的击中率; 

         inline只是对编译器的一个申请,不是强制命令,编译器可以忽略;这项申请可以隐喻提出,也可以明确指出(加inline关键字)。隐喻的方式是将函数定义在class定义式内;

        inline函数通常被放置在头文件中,在编译阶段实现对函数本体的替换;编译器拒绝将复杂(循环或者递归)、virtual函数函数指针调用的情况下将函数变成inline;

        inline函数在随着程序库的升级时无法升级,同时不支持设置端点调试(本身不是函数);

       结论:将大多数inline函数限制在小型、被频繁调用的函数身上,这可以使得日后调试过程和二进制升级更容易,也可以使得潜在的代码膨胀最小化,使程序的速度提升最大化;

      不要只因为function templates出现在头文件,将它们声明为inline;

条款31:将文件间的编译依存关系降至最低

        在设计对象的过程中,我们可以将对象分割为两个class,一个只提供接口(数据),一个负责实现该接口(函数实现),这就是所谓的pimpl方式;这种设计方式的好处是:1)在修改数据代码时,我们只需要修改接口数据就行,而不要修改实现;2)在函数声明时,当其中有class时,我们只需要声明class而不需要定义class;具体设计策略如下:

              如果使用object reference或者object pointers可以完成任务,就不要使用objects;

              如果能够,尽量以class声明式替换class定义式,这样可以省去调用构造函数的开销;

             为声明式和定义式提供不同的头文件;

   上述的方法称为handle class 的构造方法,常见的handle class的构造方法除了有pimpl方式,还有一种特殊的抽象基类(interface class),这种class的目的是调试derived class 接口,因此通常不带成员变量,没有构造函数,只有一个虚析构函数和一组pure virtual函数,如:

class person{

public:

           virtual ~person( );

           virtual std::string name( ) const=0;

           virtual std::string birthdata( ) const=0;

          virtual std::string address( )const=0;

};

interface class自己创建单个对象,这样的函数称为factory函数或virtual构造函数,它们返回指针(智能指针),指向动态分配所得对象,而该对象支持interface class接口,通常函数被声明为static;代码如下:

class person{

public:

         static std::trl::shared_ptr<person>(create(const std::string&name,const Date&birthday,const address&addr);

};

           Handle class和interface class解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性;对于handle class,成员函数必须通过impl pointer取得对象数据,每一次访问就会增加一层间接性,同时每一个对象消耗的内存数量必须增加impl pointer的大小,最后impl pointer必须初始化,指向一个动态分配的impl object,因此可能会带来bad alloc异常;interface class每次调用只付出一个间接跳跃成本,此外派生的interface class中必须含有一个vptr,这个指针会增加存放对象所需的内存数量。

        在程序开发中,如果使用handle class和interface class的实现码在速度或大小差异大于类之间的耦合时,可以用具体类代替handle class和interface class;

猜你喜欢

转载自blog.csdn.net/xx18030637774/article/details/80834145