1. 编译依赖问题引入
在 C++ 中,类定义不仅包含接口,还包含大量实现细节,这导致文件间存在编译依赖。例如Person
类:
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
类的文件需#include
string
、Date
和Address
相关头文件,若这些头文件或其依赖文件改变,包含Person
类及使用Person
的文件都需重新编译。
2. 前向声明的问题
尝试通过前向声明减少依赖存在两个问题:
- 标准库类型声明问题:
string
是typedef
,其前向声明复杂且不应手动声明,应直接使用#include
。 - 对象大小确定问题:C++ 编译器需通过类定义确定对象大小,若类定义省略实现细节,编译器无法得知对象大小。例如:
int main() {
int x;
Person p(params);
}
编译器可根据已知信息为int
型变量x
分配空间,但无法仅通过省略实现细节的Person
类定义为p
分配空间。
3. pimpl 惯用法(Handle 类)
- 设计思路:将类拆分为仅提供接口的主类和实现接口的类,主类仅包含指向实现类的指针,如
Person
类可设计为:
#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
的客户无需关注Date
、Address
等细节,实现改变时客户无需重新编译,且减少客户对实现细节的依赖。 - 实现示例:
#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();
}
4. Interface 类
- 设计思路:作为抽象基类为派生类指定接口,通常无数据成员、有虚析构函数和纯虚函数。例如:
class Person {
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
...
};
- 对象创建:客户通过
factory
函数(如create
)创建对象,该函数返回指向动态分配对象的智能指针。例如:
class Person {
public:
...
static std::tr1::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr);
...
};
std::string name;
Date dateOfBirth;
Address address;
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
- 派生类实现:如
RealPerson
从Person
派生并实现其虚函数:
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;
};
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));
}
5. 两种方法的代价与使用建议
- 代价:
- Handle 类:成员函数访问数据增加间接层,需额外存储指针,存在动态内存分配成本及异常风险。
- Interface 类:函数调用为虚拟,存在间接跳转成本,对象可能需额外存储虚表指针。两者都不利于大量使用
inline
函数。
- 建议:开发过程中使用
Handle
类和Interface
类减少实现变化对客户的影响,产品阶段若速度和大小影响显著,可考虑用具体类替代。
6. 总结
最小化编译依赖的核心是用对声明的依赖替代对定义的依赖,Handle
类和Interface
类是基于此的有效方法。库头文件应保持完整且仅包含声明,无论是否涉及模板均适用。