概述
原因
请将文件中的编译依存关系降到最低。如果你没有做到的话,可能你只修改了一个小数据,但是修改重新编译连接整个程序。
问题出在C++并没有把”将接口从实现中分离“这事做的很好。类的定义时不止详细描述了类接口,还包括实现细节。比如:
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;
};
这里Person类无法通过编译----如果编译器没有取得其实现代码所用到的类string、Date、Address 的定义式。这样的定义式通常由#include提供:
#include <string>
#include "date.h"
#include "address.h"
不幸的是,这样一来Person定义文件和其含入文件之间形成了一种编译依存关系。这些头文件中有任何一个改变,或者这些头文件依赖的其他文件由任何改变,那么每一个含入Person类的文件就得重新编译,任何使用Person类的文件也必须重新编译。
那,C++为什么捕将实现细目分开描述?
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;
};
这样做的话Person的客户只需要在Person接口被修改过时才重新编译了。因为这个想法存在两个问题
- string不是个类,而是个typedef(定义为basic_string< char>)。因此上面针对string的前置声明不正确,而且你本来就不应该尝试手动声明一部分标准程序库,你应该仅仅使用适当的#include完成目的
- C++前置声明时,编译器必须在编译期间知道对象的大小。举个例子:
int main(){
int x;
Person p(parmas);
}
当编译器看到x的定义式,它知道必须分配多少内存(通常位于stack内)才能够有一个int。没问题,每个编译器都知道一个int有多大。当编译器看到p的定义式,它也必须知道分配足够空间以放置一个Person,但它如何知道一个Person对象有多大呢?编译器获得这项信息的唯一版本就是询问class定义式。然而如果class定义式可以合法的不列出实现细目,编译器就不知道应该分配多少空间了。
此问题在java等语言不存在,因为当我们以那种语言定义对象时,编译器只分配足够空间给一个指针(用以指向该对象)使用。也就是说相当于:
int main(){
int x;
Person *p;
}
实现
当然,C++也可以做到”将对象实现细目隐藏在一个指针背后“的游戏。针对Person:将Person分割为两个类,一个只提供接口,另一个负责实现该接口:
#include<string> //标准库组件不应该被前置声明
#include<memory>
class PersonImpl; //Person实现类的前置声明
class Date; //Person接口用到的类的前置声明
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::shared_ptr<PersonImpl> pImpl; // 指向实现类
};
// 这种设计常被称为pimpl idiom
这样,Person的客户就完全与Dates、Address以及Person的实现细目完全分离了。那些类的任何修改也不需要Person客户重新编译。由于客户无法看到Person的实现细目,也就不可能写出什么”取决于那些细目“的代码,实现了真正的”接口与实现分离“
这个分离的关键在于以”声明的依存性“替换”定义的依存性“,那正是编译依存性最小化的本质:现实中让头文件尽可能的自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每一件事都源于这个简单的设计策略:
- 如果使用object reference或者object pointer可以完成任务,就不要使用object。你可以只靠一个类型声明式就定义出指向该类型的reference和pointer;但如果定义某类型的object,就需要用到该类型的定义式
- 如果能够,尽量以类声明式调换类定义式。 (这个地方我不认同,请看C/C++编程:前置声明:倾向于使用#include,而不是前置声明)
- 为声明式和定义式提供不同的文件 (这个地方我不认同,建议将声明放进头文件,定义放在源文件)
像Person这样使用pimpl idiom的类,往往被称为Handle classes。那么这样的类应该怎么做点真正的事情呢?
- 方法一:将它们的所有函数转交给相应的实现类并由后者完成实际工作。比如:
#include "Person.h"
#include "PersonImpl.h" // 注意PersonImpl和Person有着完全相同的成员函数,两者接口完全相同
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成为一种特殊的abstract base class(抽象基类),称为Interface class。这种类的作用是描述派生类的接口,因此它通常不带成员变量,没有构造函数,只有一个虚析构函数以及一组纯虚函数。
比如Person的interface class应该如下:
class Person{
public:
virtual ~Person():
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
};
interface class的客户必须有办法为这种类创建新对象。它们通常调用一个特殊函数,此函数扮演”真正将被具现化“的那个派生类的构造角色。这样的函数通常叫做factory(工厂)函数或者virtual构造函数。它们返回指针(最好是智能指针),指向动态分配所得对象,而该对象支持interface class接口。这样的函数又往往在interface class内为static:
class Person{
public:
static std::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address & addr);
客户会这样使用它们:
std::string name;
Data dateOfBirth;
Address address;
// 创建一个对象,支持Person接口
std::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
// 通过Person的接口使用这个对象
pp->name();
当然,支持interface class接口的那个具现类必须被定义处理,而且真正的构造函数必须被调用:
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 = 0;
std::string address() const = 0;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
std::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address & addr){
return std::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
RealPerson示例实现了interface class的两个最常见的机制之一:从interface class继承接口规格,然后实现接口所覆盖的函数(第二个机制的实现设计多重继承)
代价
上面两种实现有什么代价呢?
- 在handler class上,成员函数必须通过implementation pointer取得对象数据,这会为每一次访问增加一层间接性,每一个对象消耗的内存数据必须增加implementation pointer的大小。最后,implementation pointer必须初始化(在handle class构造函数内),指向一个动态分配的implementation object,所以将承受动态内存分配(和释放)的开销
- 置于interface class,由于每个函数但是virtual,所以你必须为每次函数调用付出一个间接跳跃成本。此外interface class派生的对象必须包含一个vptr,这个指针可能会增加存放对象所需的内存数量-----实际取决于这个对象除了interface class之外是否还有其他虚函数来源
最后
以上就是鳗鱼宝贝为你收集整理的C/C++编程:将文件中的编译依存关系降到最低原因实现代价的全部内容,希望文章能够帮你解决C/C++编程:将文件中的编译依存关系降到最低原因实现代价所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复