首页 > 代码库 > effective c++条款26-31“class and function的实现”整理

effective c++条款26-31“class and function的实现”整理

一、类的实现面临的问题:

太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handls)”可能会破坏封装并留给客户虚吊号码牌;为考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合则可能导致让人不满意的冗长建置时间。

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

有些对象,你可能过早的定义它,而在代码执行的过程中发生了导常,造成了开始定义的对象并没有被使用,而付出了构造成本来析构成本。
所以我们应该在定义对象时,尽可能的延后,甚至直到非得使用该变量前一刻为止,应该尝试延后这份定义直到能够给它初值实参为止。
这样做的好处是:不仅可以避免构造(析构)非必要对象,还可以避免无意义的default构造行为。
遇到循环怎么办?此时往往我们会有两个选择:
做法A:1个构造函数+1个析构函数+n个赋值操作 // 在循环外面定变量,在循环内赋值
做法B:n个构造函数+n个析构函数   // 在循环内定义并初始化变量
这时候要估计赋值的成本低还是构造+析构的成本低,另外值得考虑的是对象作用域的问题。

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

转型语法通常有三种不同的形式:
1,C风格的转型动作:(T)expression
2,函数风格的转型动作:T(expression)
3,上面的第二种被称为“旧式转型”,C++提供了四种新式转型:

A:const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++ style转型操作符。
B:dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某些对象是否归属于集成体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
C:reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它并不可移植。
D:static_cast用来强迫隐式转换,例如将non-const对象转换成cosnt对象,将int转换成double,将void*指针转为typed 指针,将pointer-to-base转为pointer-to-derived等等。
C++提供对旧式转型的支持,但是更推荐使用新式转型,原因是:第一,它们很容易在代码中被辨识出来。第二,各转型动作的目标越明确,编译器越可能诊断出错误的运用。要点:

    如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
    如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
    宁可使用C++-styles(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

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

函数如果“返回一个handle代码对象内部成分”总是危险的,不论这所谓的handle是个指针或迭代器或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里唯一关键的是,有个handle被传出去了,一旦如此你就是暴露在“handle比其所指对象更长寿的”风险下。
要点:
    避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。

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

异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
强烈保证往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可以实现或具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中最弱者。

六、条款30:透彻了解inlining的里里外外

关于inline:
1. inline函数的调用,是对函数本体的调用,是函数的展开,使用不当会造成代码膨胀。
2. 大多数C++程序的inline函数都放在头文件,inlining发生在编译期。
3. inline函数只代表“函数本体”,并没有“函数实质”,是没有函数地址的。

值得注意的是:
1. 构造函数与析构函数往往不适合inline。因为这两个函数都包含了很多隐式的调用,而这些调用付出的代价是值得考虑的。可能会有代码膨胀的情况。
2. inline函数无法随着程序库升级而升级。因为大多数都发生在编译期,升级意味着重新编译。
3. 大部分调试器是不能在inline函数设断点的。因为inline函数没有地址。

请记住:
1. 大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可以使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2. 另外,对function templates的inline也要慎重,保证其所有实现的函数都应该inlined后再加inline。

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


这个问题产生是源于希望编译时影响的范围尽量小,编译效率更高,维护成本更低,这一需求。
实现这个目标首先第一个想到的就是,声明与定义的分离,用户的使用只依赖声明,而不依赖定义(也就是具体实现)。
但C++的Class的定义式却不仅仅只有接口,还有实现细目(这里指实现接口需要的私有成员)。而有时候我们需要修改的通常是接口的实现方法,而这一修改可能需要添加私有变量,但这个私有变量对用户是不应该可见的。但这一修改却放在了定义式的头文件中,从而造成了,使用这一头文件的所有代码的重新编译。
于是就有了pimpl(pointer to implementation)的方法。用pimpl把实现细节隐藏起来,在头文件中只需要一个声明就可以,而这个poniter则作为private成员变量供调用。
这里会有个有意思的地方,为什么用的是指针,而不是具体对象呢?这就要问编译器了,因为编译器在定义变量时是需要预先知道变量的空间大小的,而如果只给一个声明而没有定义的话是不知道大小的,而指针的大小是固定的,所以可以定义指针(即使只提供了一个声明)。
这样把实现细节隐藏了,那么实现方法的改变就不会引起别的部分代码的重新编译了。而且头文件中只提供了impl类的声明,而基本的实现都不会让用户看见,也增加了封装性。

结构应该如下:
class AImpl;
class A {
public:
    ...
private:
    std::tr1::shared_ptr<AImpl> pImpl;
};
这一种类也叫handle class

另一种实现方法就是用带factory函数的interface class。就是把接口都写成纯虚的,实现都在子类中,通过factory函数或者是virtual构造函数来产生实例。

声明文件时这么写:
class Person
{
public:
    static shared_ptr<Person> create(const string&,
        const Data&,
        const Adress&);
};

定义实现的文件这么写
class RealPerson :public Person
{
public:
    RealPerson(...);
    virtual ~RealPerson(){}
    //...
private:
    // ...
};
以上说的为了去耦合而使用的方法不可避免地会带上一些性能上的牺牲,但作者建议是发展过程中使用以上方法,当以上方法在速度与/或大小上的影响比耦合更大时,再写成具体对象来替换以上方法。

请记住:
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相信于定义式。基于此构想的两个手段是Handles classes和Interface classes。
程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用。


effective c++条款26-31“class and function的实现”整理