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

文章目录

  • 第一种方法
  • 第二种方法

 C++在分离接口和实现方面做得不好。类定义不仅指定了类接口,还指定了相当多的实现细节。

#include 
#include "date.h"
#include "address.h"

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类的文件。
为什么不能这样定义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;
    ...
};

int main()
{
    int x; 		// 定义一个int(内置类型大小是明确的)
    Person p(params); // 定义一个Person(需要通过数据成员的大小,计算所需内存)
    Person *p2; 	// 定义一个指向Person对象的指针(指针也是内置类型)
    ...
}

主要原因有下:

  • string不是一个类,它是一个typedef(basic_string的别名)。
  • 如果这样可行的话,Person的客户端只有在类的接口改变时才必须重新编译。
  • 更重要的:前置声明的困难在于编译器在编译期间需要知道对象的大小。

第一种方法

 可以采用pimpl (pointer to implemention)方法,“将对象实现细节隐藏在指针背后”

#include  // 标准库组件不应该前置声明
#include  // std::shared_ptr
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; // 指向实现的对象
};
  • 这种分离的关键是:从依赖定义变为依赖声明。
  • 当可以使用对象的引用和指针时,应避免使用对象。
    当声明一个函数需要用到某个类时,并不需要该类的定义,即使函数通过值传递或返回该类型:
#include "datefwd.h" // 声明(但不定义)Date类的头文件
//class Date; // 类声明;已放入"datefwd.h" 
Date today(); 		// 没问题,这里不需要Date的定义
void clearAppointments(Date d); // 同上

回到我们的Person类,下面是Person的两个成员函数的实现方式:

#include "Person.h" 	// 正在实现Person类,所以必须包含它的类定义
#include “PersonImpl.h” 	// 还必须包含PersonImpl的类定义
 			// 否则无法调用它的成员函数;
			// 注意,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();
}

第二种方法

Handle类方法的另一种选择是让Person成为一种特殊的抽象基类,称为接口类:

class Person {
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    ...
};

这个Person类作为基类提供接口接口,使用该类型的引用或指针,指向采用工厂设计模式创造的派生类对象。

class Person {
public:
    ...
  virtual ~Person();
  virtual std::string name() const = 0;
  virtual std::string birthDate() const = 0;
  virtual std::string address() const = 0;

    static std::tr1::shared_ptr<Person> // 返回类型为shared_ptr 
    create(const std::string& name,  const Date& birthday,  const Address& addr);  
    ...
};

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::shared_ptr<Person>(new RealPerson(name,birthday,addr));
}
std::string name;
Date dateOfBirth;
Address address;
...
// 创建一个支持Person接口的对象
std::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout << pp->name() // 通过Person接口使用对象
        << " was born on "  << pp->birthDate()
        << " and now lives at "<< pp->address();
... 
  • 最小化编译依赖关系背后的一般思想是依赖声明而不是定义。基于这种思想的两种方法是句柄类和接口类。
  • 库头文件应该以完整且仅包含声明的形式存在。无论是否涉及模板,这都适用。

你可能感兴趣的:(Effective,C++,c++,c++)