设为首页 加入收藏

TOP

《Effective C++》学习笔记(八)(二)
2015-07-20 17:46:57 来源: 作者: 【 】 浏览:16
Tags:Effective 学习 笔记
+程序中是编译期行为。

大部分编译器拒绝将太过复杂(含有循环、递归等)的函数inlining,而所有的virtual函数都不能inlining,因为virtual意味着“等待,知道运行期才确定调用哪个函数”,而inline意味“执行前先将动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,你就很难责备它们拒绝将函数本体inlining。

有时候编译器inline某个函数的同时,还可能为其生成一个函数本体(比如程序要取某个line函数地址),值得一提的是,编译器通常不对“通过函数指针而进行的调用”实施inling,这就是说line函数的调用可能是被inlined,也可能不被inlined,取决于调用的实施方式。

“将构造函数和析构函数进行inling”是一个很糟糕的想法。看下面这段代码:

?

Derived::Derived()
{ // 空白Derived构造函数的观念性实现
    Base::Base(); // 初始化Base成分
    
    try{
        dm1.std::string::string();
    }catch(...){
        Base::~Base();
        throw;
    }
    
    try{
        dm2.std::string::string();
    }catch(...){
        dm1.std::string::~string();
        Base::~Base();
        throw;
    }
    
    try{
        dm3.std::string::string();
    }catch(...){
        dm2.std::string::~string();
        dm1.std::string::~string();
        Base::~Base();
        throw;
    }
}
这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精致复杂的做法来处理异常。尽管如此,这已能准确反映Derived的空白构造函数必须提供的行为。

程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。

?

总结:

1)将大多数inlining限制在小型,被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2)不要因为function templates出现在头文件,就将它们声明为inline。

?

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

假设你对c++程序的某个class实现文件做了些轻微改变,修改的不是接口,而是实现,而且只改private成分。

然后重新建置这个程序,并预计只花数秒就好,当按下“Build”或键入make,会大吃一惊,因为你意识到整个世界都被重新编译和链接了!

问题是在c++并没有把“将接口从实现中分离”做得很好。class 的定义式不只详细叙述了class接口,还包括十足的实现细目:

?

class Person{ 
public: 
    Person(const std::string& name, const Date& birthday, const Address& addr); 
    std::string name() const; 
    std::string birthDate() const; 
    std::string address() const; 
    ... 
private: 
    std::string theName;        //实现细目 
    Date theBirthDate;          //实现细目 
    Address theAddress;         //实现细目 
};

?

这个class Person无法通过编译,Person定义文件的最上方可能存在这样的东西:

#include 
  
    
#include date.h 
#include address.h
  
这样写显然在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。可能就会导致开头我们提到的使你陷入窘境的情形出现。所以这里我们采取了另外一种实现方式,即将对象实现细则隐藏与一个指针背后。具体这样做:把Person类分割为两个类,一个只提供接口,另一个负责实现该接口。

?

?

#include 
  
   
#include 
   
     class PersonImpl; class Date; class Address; class Person{ public: Person(const std::string& name,const Date& birthday,const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... private: std::tr1::shared_ptr
    
      pImpl; }
    
   
  

?

这里,Person只内含一个指针成员,指向其实现类(PersonImpl)。这个设计常被称为pimpl idiom(pimpl是“pointer to implementation”的缩写)。

这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每件事都源自于这个简单的涉及策略。

1)如果用object reference 或 object pointer可以完成任务,就不要用objects。

2)如果能够,尽量以class声明式替换class定义式。

3)为声明式和定义式提供不同的头文件。

先Person这样使用pimpl idiom的classes,往往被称为Handle classes。

另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class称之为Interface class。这种class只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。一个针对Person而写的Interface class或许看起来像这样:

?

//Person.h
...
using std::string;
class Date;
class Address;
class Person
{
public:
    virtual ~Person();
    virtual string name()const = 0;
    virtual string birthDate()const = 0;
    virtual string address()const = 0;
    ...
    static std::tr1::shared_ptr
  
   
    create(const string& name,const Date& birthday,const Address& addr);
};
...

// person.cpp
...
class RealPerson:public Person
{
public:
    RealPerson(const string& name,const Date& birthday,const Address& addr);
    virtual ~RealPerson(){}
    string name()const;
    ...
private:
    string name_
首页 上一页 1 2 3 下一页 尾页 2/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇有意思的进程创建函数fork()的问题 下一篇Leetcode 细节实现题 Spiral Matr..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

·新书介绍《Python数 (2025-12-25 04:49:47)
·怎么利用 Python 进 (2025-12-25 04:49:45)
·金融界大佬力荐,Pyt (2025-12-25 04:49:42)
·你必须要弄懂的多线 (2025-12-25 04:22:35)
·如何在 Java 中实现 (2025-12-25 04:22:32)