1、文件间依存度高的话带来的影响?
假如你修改了C++ class实现文件,修改的仅仅是实现,而没有修改接口,而且只修改private部分。此时,重新构建这个程序时,会发现整个文件、以及用到该class 的文件都被会被重新编译和连接,这不是我们想要看到的。
2、出现上述问题的原因
问题出在C++没有把关于接口与实现相分离这件事做好。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 string,Date和Address的定义式时,它无法通过编译它所需要的这样的定义式往往由#include <>提供(里面有class string,Date和Address的实现代码)。例如本例中需要:。
#include <string>
#include "date.h"
#include "address.h"
如果这些头文件中(或头文件所依赖的头文件)的一个的实现被改变了,那么每一个用到class 类的文件都得重新编译。这就是所谓的文件间的依存度比较高。
3、解决文件间依存性的一个不成熟方案
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;
...
};
上述设想不成立,原因有两条:
- 第一:string并不是一个class,他是一个typedef(定义为basic_string)。因此上述针对string做的声明并不正确;正确的声明比较复杂,因为涉及额外的tempalte。退一步讲,你本来就不应该尝试手工声明标准库程序的一部分,你应该仅仅使用适当的#include完成目的。其实标准头文件这也不是编译的瓶颈,也有解决的方法。例如:你可以值改变你的接口涉及,避免使用
标准头文件的非法的#include。 - 第二:编译器必须在编译期间知道对象的大小。这才是问题的关键。例如:下述程序中,当编译器看到x时,由于知道它是int类型的,也就知道需要为它分配多大的空间。但是当编译器看到自定义的类Person对象p时,编译器必须看到Person的类定义才能知道为p对象分配多大的内存。如果class中没有实现细目,那么编译器就无法确定为其分配多大内存。
4、解决文件间依存关系的正确手法一:handle class
(1)基本思想
将对象的实现细目隐藏到一个指针(通常是一个智能指针)背后。
例如:
#include <string>
#include <memory>
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<PersonImpl> pImpl; // 指向实现物的指针
};
上述程序中,将原本的Person 类写成两个部分,接口那部分是主要的部分,其中含了一个智能指针,指向实现细目。而实现细目另外定义了一个类:PersonImpl。这种设计手法被称为:pimpl idiom。
注意:pimpl 指的是 pointer to implementation。这种class内的指针往往被称为:pImpl指针。上述class的写法 往往被称为handle class。
(2)上述手法的好处
实现了接口与实现的分离。即:Person的客户与 Date、Address、以及Person的实现细目就分离了。
实现接口与实现的分离所带来的的好处:
- 这些class的修改,都不需要Person客户进行重新编译。
- 而且由于客户无法看到实现细目,也就不能写出由这些实现细目所决定的代码。
(3)上述实现的关键
(4)该手法下的其他情况细分
- 如果用 object reference 或 object pointer 可以完成任务,就不要用 objects。
可以只靠声明式定义出指向该类型的 pointer 和 reference;但如果定义某类型的 objects,就需要用到该类型的定义式。 - 如果能够,尽量以 class 声明式替换 class 定义式。
当你声明一个函数而它用到某个 class 时,你并不需要该 class 的定义式,纵使函数以 by value 方式传递该类型的参数(或返回值)亦然。 - 为声明式和定义式提供不同的头文件。
两个头文件应该包吃一致性,其中一个头文件发生改变,另一个就得也改变。一个内含了class 接口的定义,另一个仅仅内含声明。
例如:只含声明式的Date class 的头文件应该命名为datefwd.h(有一定的命名规则)。
注意:本条款适用于template 也适用于 non-template。
5、解决文件间依存关系的正确手法二:另外一种实现handle class的手法,interface class手法
(1)基本思想
令Person class 成为一种特殊的abstract base class (抽象基类),称为interface class。这样的类通常:没有成员变量,也没有构造函数,只有一个virtual 的析构函数以及一组pure virtual 用来描述接口。
(2)C++ 接口类与其他语言接口类的不同
像.net和java 的接口,他们不允许在接口类中定义成员函数和成员变量。但是C++的接口类并不禁止,这样的规则使得C++语言具有更大的弹性。
(3)interface class 类创建对象的方式
由于这样的类往往没有构造函数,因此通过工厂函数或者virtual构造函数创建,他们返回指针,指向动态分配对象所得的对象,这样的对象支持interface class的 接口,这样的函数在interface class往往被声明为 static,例如:
class Person{
public:
...
static std::tr1::shared_ptr<Person>
create(const std::string& name, const Date& birthday, const Address& addr);
};
客户使用他们像这样:
std::string name;
Date dateBirth;
Address address;
std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address));
...
std::cout << pp->name()
<< "was born on "
<< PP->birthDate()
<< " and now lives at "
<< pp->address();
...
当然支持 interface class 接口的那个具象类(concrete classes)必须被定义出来,而真正的构造函数必须被调用。
假设有个 derived class RealPerson,提供继承而来的 virtual 函数的实现:
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;
};
有了 RealPerson 之后,写出 Person::create 就真的一点也不稀奇了:
std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
一个更现实的 Person::create 实现代码会创建不同类型的 derived class 对象,取决于诸如额外参数值、独自文件或数据库的数据、环境变量等等。
RealPerson 示范实现了 Interface class 的两个最常见机制之一:从 interface class 继承接口规格,然后实现出接口所覆盖的函数。
6、两种手法带来的开销
handle classes 和 interface classes 解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。
-
handle classe
成员函数必须通过 implementation pointer 取得对象数据。那会为每一次访问增加一层间接性。每个对象消耗的内存必须增加一个 implementation pointer 的大小。 implementation pointer 必须初始化指向一个动态分配的 implementation object,所以还得蒙受因动态内存分配儿带来的额外开销。
-
Interface classe
由于每个函数都是 virtual,必须为每次函数调用付出一个间接跳跃。此外 Interface class 派生的对象必须内含一个 vptr(virtual table pointer)。
7、两种手法的适用场景
在程序开发过程中使用 handle class 和 interface class 以求实现码有所改变时对其客户带来最小冲击。
而当他们导致速度和/或大小差异过于重大以至于 class 之间的耦合相形之下不成为关键时,就以具象类(concrete class)替换 handle class 和 interface class。