Chinaunix首页 | 论坛 | 博客
  • 博客访问: 184312
  • 博文数量: 33
  • 博客积分: 2047
  • 博客等级: 大尉
  • 技术积分: 333
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-21 19:35
文章分类

全部博文(33)

文章存档

2011年(1)

2010年(21)

2009年(11)

分类: C/C++

2010-08-07 14:46:13

第一章

Item 03Use const whenever possible

constnon-const成员函数中避免重复 P23

       如果成员函数的non-const版本和const版本所做事情完全一样,仅仅是返回类型不同,此时我们用non-const版本调用const版本是避免代码重复的安全做法。在其中castconst属性是安全的。因为调用non-const版本的对象肯定是non-const

       例如下面代码:

class TextBlock

{

public:

   

const char& operator[](std::size_t position) const  // as before

{

   

    ….

    …. // implementation

    return text[position];

}

 

char& operator[](std::size_t position)

{

    // op[]返回值的const移除,为*this加上const调用const op[]

    return const_cast(

        static_cast(*this)[position] );

}

};

       这份代码中,有两个转型,我们要non-const operator[]调用const兄弟,但non-const operator[]内部若只是单纯调用operator[],会递归调用自己。那么会大概进行一百万次,为了避免无穷递归,我们必须明确调用的是const operator[]。所以需要将*this转型为const TextBlock&这样这里就有了两次转型。

       这里需要注意并不能利用non-const版本来实现其const版本。这样反向调用可能会带来风险。

       请记住:

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

       b. 编译器强制实施bitwise constness,,就是说声明为const对象,该对象在物理上是不能修改它的一个bit的。但你编写程序时应该使用概念上的常量性 conceptual constness

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

 

Item04 Make sure that objects are initialized before they’re used.

确定对象在使用之前已被初始化

       首先请区别assignmentinitialization。其次C++规定,对象的成员变量初始化动作发生在进入构造函数体之前。因此在构造函数中,都是对成员变量的赋值或读取操作,而不是初始化。成员变量初始化发生时间比较早,在这些成员的default构造函数被自动调用之时。而对于内置类型,不保证一定在构造函数体赋值语句之前已经获得初始值。

       一般构造函数较好的写法是利用member initialization list替换函数体内的赋值操作。例如:

       ABEntry::ABEntry(const std::string& name, const std::string& address,

                    const std::list& phones)

         : theName(name),

          theAddress(address),

          thePhones(phones),

          numTimesConsulted(0)      // 这些都是initializations

       {  }

       为了一致性,对成员的初始化一律采用member initialization list这种方法,因为const成员和引用成员一定要初始化,不能被赋值。

       成员初始化的顺序很固定。base classes早于其derived classes被初始化,class的成员变量总是以其声明顺序依次初始化。

 

需要注意:不同编译单元内定义的non-local static对象的初始化次序 P30

       C++对于“定义在不同编译单元内的non-local static对象”的初始化次序没有明确定义。

       可以用local static对象来替代,然后经由某个函数来返回这些local static对象的reference,这就是Singleton模式的一个常见实现手法。

       这样用的基础在于C++保证,函数内的local static对象会在该函数调用期间 首次遇到对象定义式时被初始化。所以如果你以函数来替代直接访问non-local static对象,就保证你获得的reference肯定指向一个已初始化对象。更棒的是,如果你从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构对象成本。真正的non-local static对象可没这等便宜。

       例如:

       class FileSystem { … };

       FileSystem& tfs()

       {

              static FileSystem fs;

              return fs;

       }     // Singleton pattern

       class Directory { … };

       Directory::Directory( params )

       {

             

              std::size_t diskNum = tfs().numDisks();

             

       }

       Directory& tempDir()

       {

              static Directory tempDir;

              renturn tempDir;

       } // non-local static对象变换成函数替代

       这种结构,客户程序只是用tfs()代替了tfs对象,他们得到的是local static对象的引用。这些函数可以成为inline函数,尤其是频繁被调用时。

       这些函数内含static对象的事实使得他们在多线程系统中带有不确定性。任何一种non-const static对象,不论是local还是non-local在多线程环境下等待某事发生都有麻烦。处理办法一种为:在程序的单线程启动阶段(single-threaded  startup potrion)手动调用所有reference-returning函数,这可消除与初始化有关的竞速形势( race conditions )”

       当然运用二次检测加锁也可解决多线程调用这个初始化函数形成竞速的问题。这就是thread-safety的问题。可参考《深入浅出Design Pattern》的单例模式一节。

       请记住:

       a. 为内置类型对象进行手工初始化,因为C++不保证初始化他们。

       b. 构造函数最好用成员初始化列表,初始值列表的排列次序和他们在class中的声明次序相同。

       c. 为避免跨编译单元 初始化次序问题,请以local static对象替换non-local static对象。

static对象声明在函数内就是local static对象,声明在namespaceclassfileglobal作用域中的都是non-local static对象。

 

第二章 Constructors, Destructors and Assignment Operators

1. 只有当class内至少有一个virtual函数时,才为他声明virtual析构函数。

2. 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class

3. operator=返回一个reference to *this

       赋值可以写成连锁形式:a = b = c = 5;

       为了实现连锁赋值,赋值操作符必须返回一个reference指向操作符左侧实参。

       这个规则适合于所有赋值相关运算,例如+=-=等等。这种规则只是个协议,并无强制性,如果不遵守,编译也会通过。然而标准库和内置类型都遵守这个协议。所以没有特别的理由,还是遵守吧。

      

       operator=中处理“自我赋值”P55

       自我赋值的安全性问题,在有资源涉及的情况下,要考虑copy构造和operator=所发生的行为是否合理,保证资源不会在使用时被释放或资源泄漏。

       例如一份不安全的operator=实现版本,其实也是异常不安全的。

       Widget& Widget::operator=(const Widget& rhs)

       {

           delete pb;                                       // 停止使用当前的bitmap

               pb = new Bitmap(*rhs.pb);           // 使用rhs bitmap的副本

               return *this;                                   // 返回当前对象作为返回值

       }

       这里就会有自我赋值问题,this&rhs可能指向同一对象。若是这样就会删除rhsbitmap,这样rhs持有一个指针指向已删除的对象。

       下面一组精心安排的代码就考虑到了new Bitmap(*rhs.pb)这句可能会带来异常(可能是内存不足或copy构造函数抛出的异常),同时解决了自我赋值的问题。一举两得。

       Widget& Widget::operator=(const Widget& rhs)

       {

              Bitmap pOrig = pb;                         // 记住原来的pb

              pb = new Bitmap(*rhs.pb);             // pb指向*rhs.pb的一个副本

              delete pOrig;                                          // 删除原来的pb

              return *this;

       }

       如果new Bitmap抛出异常,pb以及他所在 Widget保持原状。同时处理了自我赋值问题。

 

Item 12 Copy all parts of an object

       复制对象的每个成分:当你编写copying函数时,请确保(1) 复制所有local成员变量,(2) 调用所有base classes内的适当copying函数,完成base部分的复制。

       如果你发现你的copy构造函数和copy assignment操作符有相似代码,消除重复代码做法是,建立一个private的辅助成员函数给两者调用。这样可以消除两者间的代码重复。

       请记住:

a.      copying函数应该确保复制对象内的所有成员以及所有base class成分。

b.      不要尝试以某个copying函数实现另一个copying函数。应将共同部分放入一个辅助函数内供两者调用。

 

第三章 Resource Management

Item 13 Use objects to manage resources P61

       一个直接易懂的方法是基于对象的构造、析构和copying函数基础之上。智能指针就是这种方法的代表。标准库的auto_ptr也是。利用auto_ptr避免潜在的资源泄漏,示例如下:

       void fun()

       {

              std::auto_ptr pInv(createInvestment());      // 调用factory函数

             

                               // 函数调用结束后,由auto_ptr的析构函数自动删除pInv

       }

以对象管理资源两个关键点:

1.      获得资源后立即放进管理对象( managing object )内。

2.      管理对象运用析构函数确保资源被释放。

由于auto_ptr被销毁时会自动删除它所指的对象,所以一定要注意别让多个auto_ptr指向同一对象。如果那样,对象会被多次删除。为预防这个问题,auto_ptr有一个特殊性质:若通过copy构造函数或copy assignment操作符复制他们,他们会变成null,而复制所得的指针将取得资源的唯一拥有权。是资源的唯一entry

       std::auto_ptr pInv2(pInv1);           // pInv2指向对象,pInv1null

       pInv1 = pInv2;                                                     // pInv1指向对象,pInv2null

       auto_ptr的复制行为诡异,所以对于容器,是不能存放auto_ptr对象的。

另一方法是引用计数型智能指针RCSPreference-counting smart pointer,无法打破环状引用问题,两个不被使用的对象互相引用对方。

       TR1库的tr1::shared_ptr就是RCSP,所以上面的fun函数可以这样写:

       void fun()

       {

             

              std::tr1::shared_ptr pInv(createInvestment());

             

                        // 经过shared_ptr析构函数自动删除pInv

       }

shared_ptr的复制行为正常多了。

       void fun()

       {

             

              std::tr1:;shared_ptr pInv1(createInvestment());

              std::tr1::shared_ptr pInv2(pInv1);        // 两者指向同一对象

              pInv1 = pInv2;                                                            // 无任何改变

             

              // pInv1pInv2被销毁,他们所指对象也被自动销毁

       }

由于tr1::shared_ptr的复制行为正常,所以他们可被用于STL容器。

资源管理很重要

Item 14 在资源管理类中小心copying行为,确保copying行为如你所愿

       P68 有常见的RAII类的copying行为。Resource Acquisition Is initialization,资源取得时机就是初始化时机。

Item15 Provide access to raw resources in resource-managing classes. 要资源管理类提供原始资源的访问

       APIs经常需要访问原始资源,所以每个RAII class设计时应该提供一个取得其所管理资源的办法。

       对原始资源的访问可能经由显式转换或隐式转换达成。一般显式转换比较安全,隐式转换对客户比较方便。

      

Item 16 Use the same form in corresponding uses of new and delete

 

第四章 Designs and Declarations

Item 18 Make interfaces easy to use correctly and hard to use incorrectly

Item 19 Treat class design as type design

       class design的工作责任很重大,如何设计高效的class?设计class需要面对以下问题:

       新的type对象应该如何被创建和销毁?这影响到class构造函数和析构函数、内存分配函数和释放函数(operator new, operator new[], operator delete, operator delete[])的设计。

       对象的初始化和对象的赋值有什么差别?这影响你的构造函数和赋值操作符的行为和之间的差异,别混淆初始化赋值的差别,他们对应不同的函数调用。

       见书P85

Item 20 Prefer pass-by-reference-to-const to pass-by-value

       尽量以pass-by-reference-to-const替代pass-by-value。前者通常比较高效,并可避免切割问题。

       以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往更适合。

Item 21 Don’t try to return a reference when you must return an object.

必须返回对象时,别妄想返回其reference P90

       任何函数都不能返回local对象的reference,如果那样将会坠入无定义行为的深渊。对于返回local对象的指针也是一样。

       这一条款示例了很多平时出现的烂代码,所以对我用处很大。只有了解什么是缺点什么是优点,并且你知道为什么这样?你就到达了另一个层次。

       一个必须返回新对象的函数正确写法是:直接让那函数返回新对象。关键如何辨别必须返回新对象的函数。

       inline const Rational operator*(const Rational& lhs, const Rational& rhs)

       {

              return Rational(lhs.n * rhs.n, lhs.d * ths.d);

       }

请记住:

       绝不要返回pointerreference指向一个local stack对象,或返回指向一个heap-allocated对象,或返回pointerreference指向一个local static对象而有时可能同时需要多个这样的对象。Item 4已说明在单线程环境中合理返回reference指向一个local static对象提供了一份设计实例。

       著名设计原则:封装变化,将变化封装在一个较小的范围内,这样修改时不会大动干戈。

       还有要依赖于抽象,即依赖接口而不是具体类型。

       这些都是深入浅出设计模式中基本的设计原则。

Item 22 将成员变量声明为private

       从封装的角度看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。

请记住:

       一定要将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,提供class作者充分的实现弹性。

       protected并不比public更具封装性。

Item 23 一个non-member non-friend函数比member函数更受欢迎

Item 25 Consider support for a non-throwing swap

       这条很重要,涉及到很多情况下swap的使用,以及std::swap不高效时该怎么做。

Item 29 为异常安全而努力是值得的

       非异常安全代码可能会带来程序处于不安全状态,就是当有异常出现时,程序的数据会处于败坏状态,资源泄漏。

       我们可通过以对象来管理资源保证资源合理使用和释放。追求异常安全代码,使得异常出现时数据不会处于败坏状态。 这种有点像数据库的事务操作,要么成功改变数据库到新状态,要么不改变数据库的数据。这种原子性操作为异常安全性的强烈保证。

       异常安全性保证有三种见书P158

       函数的异常安全性保证是其接口的一部分,所以应该慎重考虑选择,就像设计函数接口其他部分一样。

请记住:

a.      异常安全函数(Exception safe function)即使发生异常也不会出现资源泄漏或任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

b.      强烈保证往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备实现意义,还要考虑copy的效率。

c.       函数提供的异常安全保证通常最高只等于其所调用的各个函数的异常安全保证中的最弱者。[木桶原理]

Item 30 透彻了解inline

       一个表面看似inline的函数是否真的是inline,取决于你的build环境,主要是编译器。大多数编译器提供一个诊断级别:如果他们无法将你要求的函数inlining,会给你一个警告。

       平均而言一个程序80%的执行时间花费在20%的代码上。所以要找出这20%的代码,对它进行瘦身。除非你选对目标,否则一切都是虚功。

请记住:

a.      将大多数inlining限制在小型、被频繁使用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易。也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

b.      不要只因为function templates出现在头文件,就将他们声明为inline。函数模板声明在头文件中,是为了编译时编译器对模板具体化,需要知道模板的实现。inline函数在头文件实现也是为了编译器inlining时需要知道函数的实现。

Item 31 解耦合,降低程序代码对实现的依赖,使之依赖于抽象

       Handle classesInterface classes解除了接口和实现之间的耦合关系,从而降低了文件间的编译依赖性(compilation dependencies)。所有这种依赖于抽象的戏法得付出多少代价:它使得你在运行期丧失若干速度,又让你为每个对象超额付出若干内存。

       在程序发展过程中使用Handle classesInterface classes以求实现有所变化时对其客户端程序带来最小冲击。而当他们导致速度或大小差异过大以至于classes直接的耦合相比之下不重要时,就以具体类Concrete class替换Handle classesInterface classes

请记住:

a.      支持编译依赖最小化的一般构想是:依赖于声明式,不要依赖于实现式。基于此构想的两个手段是Handle classesInterface classes

b.      程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。

第六章 Inheritance and Object-Oriented Design

Item 34 区分接口继承和实现继承

请记住:

a.      接口继承和实现继承不同。在public继承下,derived class总是继承base class的接口。

b.      声明pure virtual函数的目的是为了让derived class只继承函数接口。

c.       声明impure virtual非纯virtual函数的目的是让derived class继承该函数的接口和缺省实现。

d.      声明non-virtual函数的目的是为了令derived class继承函数的接口以及一份强制的实现。

Item 35 考虑virtual函数以外的选择

Item 36 绝对不要重新定义继承而来的non-virtual函数 P208

Item 37 绝不重新定义继承而来的缺省参数值

       virtual函数是动态绑定,而virtual函数的缺省参数值却是静态绑定,这就是问题所在。本条例就要针对这个问题。例如:

class Shape

{

public:

       enum ShapeColor { Red, Green, Blue, Black};

       // 所以形状都必须提供draw的实现,用来绘出自己

       virtual void draw(ShapeColor color = Red) const = 0;

      

};

class Rectangle : public Shape

{

public:

       // 注意,赋予不同的缺省参数值。真糟糕!

       virtual void draw(ShapeColor color = Green) const;

      

};

class Circle : public Shape

{

public:

       virtual void draw(ShapeColor color) const;

       // 请注意:当用户以对象调用此函数时,一定要指定实参。

       //          因为静态绑定下这函数并不从其base类继承缺省参数值。

       //         但若以指针或reference调用此函数,可以不指定实参,

       //         因为动态绑定下这函数从base得到了缺省参数值。

};

后面还有很多item请读书。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

阅读(946) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~