C++之将文件间的编译依存关系降至最低(31)---《Effective C++》

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

问题提出:如果我们将某个class的实现文件做了某些修改,修改并不是class的接口,而是其实现部分,而且只改了其private部分,然后重建整个文件,然鹅当你进行编译的时候,可以发现该类都被重新编译和链接了,什么鬼???

问题的原因是C++并没有“将接口从实现中分离”该部分做的很好,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无法通过编译,因为这个文件中包含了类Date,address和string的定义是,这样的定义式通常通过#include指示符提供。Person的上面通常包括如下代码:

#include <string>
#include "date.h"
#include "address.h"

这样Person定义文件和导入文件之间形成了一种“编译依存关系”,如果头文件中有任何一个被改变,或者头文件中依赖的其他头文件有任何改变,那么每一个含入的Person class的文件就需要重新编译,任何使用Person class的文件同样也需要被重新编译,这样连串编译依存关系将使得整个项目超级复杂,同时容易崩溃!

既然这样问题很大的话,为什么C++坚持将class的实现细目至于class的定义式中呢?为什么不这样定义Person,将实现细目分开叙述???代码如下:

namespace std{
    class string;
}
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;
    ...
}; 

这样实现的话将存在两个问题:
1)string并不是class,只是basic_string,因此针对上述string而做的前置声明并不明确,正确的前置声明比较复杂,因为设计额外的templates,然而这并不重要;
2)编译器需要在编译期间内知道对象的大小。
看看C++中是如何解决这个问题的吧!

#include <string>
#include <memory>//此乃为了trl::shared_ptr而含入;
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::trl::shared_ptr<PersonImpl> pImpl;//指针,指向实现物

可以看到这里main class中只包含一个指针成员,指向其实现类,这般设计通常被称为pimpl idiom,这种class内的指针往往就是pImpl,就像上面代码这样。这种设计,Person的客户就完全与Date,Address以及Person的实现细目完全分离了。
**

这个分离的关键在于“声明的依存性”替换为“定义的依存性”,那正是编译器依存性最小化的本质,

**现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依,其他每一件事都源自于这个简单的设计策略:
1)如果使用object references或者object pointers可以完成任务,就不要使用object,你可以只靠一个类型声明式就可以定义出指向该类型的references和pointers,但如果定义某类型的objects,就需要用到该类型的定义式;
2)如果能够,尽量用class声明式替换为class定义式,注意,当你声明一个函数而它用到某个class时候,你并不需要class的定义;
3)为声明式和定义式提供不同的头文件。

像Person这样使用pimpl idiom的class,往往被称为Handle classes,那么如何使用实现Person中的成员函数呢?答案有两种:

1)当然是利用PersonImpl实现类中的实现函数啦!

#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name,const Date& birthday,const Address& addr):pImpl(new PersonImpl(name,birthday,addr))
{}
...
std::string Person::name() const{
    return pImpl->name();
}

这里需要注意Person构造函数伊new调用PersonImpl的构造函数,已经Person::name函数中调用PersonImpl::name,让Person变成一个Handle class并不会改变它做的事,只会改变它做事的方法。
2)令Person称为一种特殊的abstract base class(抽象基类),称为Interface class,这种class的目的是详细一一描述derived classes的接口,因此它通常不带成员变量,也没有构造函数,只有一系列virtual函数和一个virtual析构函数,通常我们可以设计一个factory工厂函数或者virtual构造函数进行实现,如:

class Person{
public:
    virtual ~Person();
    virtual std::string name()const=0;
    virtual std::string birthDate() const=0;
    virtual std::string address() cosnt=0;
    ...
};
class RealPerson:public Person{
public: 
    RealPerson(const std::string& name,const Date& birthday,const Address& addr):theName(name),theBirthDate(birthday),theAddress(addr)
    {}
    virtual ~RealPerson(){}
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
};
class Person{
public:
    ...
    static std::trl::shared_ptr<Person> create(const std::string& name,const Date& birthday,const Address& addr);
    ...
};

对于Handle classes来说,成员函数必须通过implementation pointer取得对象那个数据,那回味每一次访问添加一层间接性。每一个对象小号的内存数量必须增加implementation pointer大小,最后impementation pointer必须初始化,指向一个动态分配得来的implementation object,所以你需要体会因动态内存分配(及其后的释放动作)而来的而外开销,以及遭遇bad_alloc有慈航的可能性;
对于Interface classes来说,每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃成本,同时Interface class派生出来的向必须内含一个vptr,这个只恨可能会增加存放对象所需要的内存数量。

总结:
1)支持“编译依存性最小化”的一般方法是:相依赖于声明书式,不要依赖于定义式,基于次构想的两个手段是Handle classes和Interface classes。
2)程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否设计templates都适用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值