读到Effective C++条款31时有些困惑,下面是自己的理解,如有错误谢谢指出。
考虑这样一个文件Person.h
#include <string>
class Person {
public:
Person(unsigned int _age, const std::string& _name);
void setAge(unsigned int _age);
void setName(const std::string& _name);
unsigned int getAge() const;
std::string getName() const;
private:
unsigned int age;
std::string name;
};
当对此文件做出任何修改时,所有直接或者间接包含此文件的源文件都将重新编译(vs2021下的自动编译生成),我是认为预处理时头文件的展开使得引入Person.h的源文件发生变动从而必须重新编译。这可能就是所谓的编译依赖吧,如果在一个大工程中,众多的源文件重新编译将是一个很耗时间的过程,那么该如何避免或者减少此问题的发生呢?首先考虑我们在修改Person.h时,往往修改的是具体实现,而接口较少变化,那么如果可以将类的实现细节与类的定义式(其中包含接口的声明)分开,即将Person.h与Person类的具体实现分开,更专业一些叫做接口与实现分离,问题就解决了,下面都将遵循这一思路,比如这样做。
// 修改后的Person.h
#include <string>
class Person {
public:
Person(unsigned int _age, const std::string& _name);
void setAge(unsigned int _age);
void setName(const std::string& _name);
unsigned int getAge() const;
std::string getName() const;
};
在其他文件中去定义Person类的实现细节。
// Person.cpp
#include “Person.h"
unsigned int Person::age;
std::string Person::name;
// ...
很遗憾,这只是我一厢情愿写的,C++并不支持这样做,原因是当其他文件中涉及Person对象的定义时,编译器必须知道Person类的大小才能知道该分配多少空间给它,方法就是查询Person的定义式,然而上述定义式中是查不到的,但这引出了一个可能的解决方法,名为Handle Class,即在Person类中只包含指向PersonImp类对象的指针,具体实现细节在PersonImpl类中,如下做法。
// 新添加的PersonImpl.h
#include <string>
class PersonImpl {
public:
PersonImpl(unsigned int _age, const std::string& _name);
void setAge(unsigned int _age);
void setName(const std::string& _name);
unsigned int getAge() const;
std::string getName() const;
private:
unsigned int age;
std::string name;
};
这里补充一下声明与定义的知识.。
// 声明是将一个符号引入程序,只是告诉编译器这是个什么东西,以下为声明:
class Date;
struct Address;
extern float rate;
extern void print();
typedef unsigned int uint;
void show(const Date& date);
struct Number {
static int number;
};
template<typename T>
class Vector;
template<typename T>
void type(const T& t);
// 定义则提供了一个实体在程序中的唯一描述,以下为定义或者声明并定义:
int number;
extern double pi = 3.14;
void hello() {}
int Number::number = 1;
extern void happy() {}
class Person {};
template<typename T>
struct People {};
template<typename T>
void eat(const T& t) {}
当你使用类引用或者类指针能够完成任务时,就不要用实体,因为你只靠一个类声明就能定义出类引用和类指针,声明一个函数用到某个类时,并不需要类的定义,只需要声明,可以到定义该函数时再引入类的定义式,好了接着往下想。
// 修改后的Person.h
#include <string>
#include <memory>
class PersonImpl;
// 我们使用类前置声明而不是引入PersonImpl.h,如果那样做,所有包含Person.h的文件也会包含
// PersonImpl.h,修改PersonImpl.h时,还是得大面积重新编译。
// 书上还讲到应该将声明和定义式置于两个不同的头文件,需要类的声明时include那个头文件而不是
// 显式的自己声明,不是很懂原因。
class Person {
public:
Person(unsigned int _age, const std::string& _name);
void setAge(unsigned int _age);
void setName(const std::string& _name);
unsigned int getAge() const;
std::string getName() const;
private:
std::shared_ptr<PersonImpl> pImpl;
};
// 修改后的Person.cpp
#include "Person.h"
#include "PersonImpl.h"
// ...
这样一来,Person.h和包含具体实现的PersonImpl.h就隔离了,再对PersonImpl.h修改时,只要Person.h不动,那些引入Person.h的源文件就不需要重新编译了,这其实是在接口与实现之间加入了一个中间层从而实现了接口与实现的分离,那么为何不直接将Person定义为一个抽象接口类呢?下面是Interface Class解决方法。
// 修改后的Person.h
#include <string>
#include <memory>
class Person {
public:
virtual ~Person() = default;
virtual void setAge(unsigned int _age) = 0;
virtual void setName(const std::string& _name) = 0;
virtual unsigned int getAge() const = 0;
virtual std::string getName() const = 0;
static std::shared_ptr<Person> create(unsigned int _age, const std::string& _name);
};
// 新增的RealPerson.h
#include "Person.h"
class RealPerson : public Person {
public:
RealPerson(unsigned int _age, const std::string& _name);
virtual ~RealPerson() = default;
virtual void setAge(unsigned int _age);
virtual void setName(const std::string& _name);
virtual unsigned int getAge() const;
virtual std::string getName() const;
private:
unsigned int age;
std::string name;
};
// 新增的RealPerson.cpp
#include "RealPerson.h"
std::shared_ptr<Person> Person::create(unsigned int _age, const std::string& _name) {
return std::shared_ptr<Person>(new RealPerson(_age, _name));
}
// ...
到这里书上所讲的内容理解的差不多了(勉强能够自圆其说),但还是有许多不懂得地方,以上的两种方法都假设接口不变,但当成员变量被修改后,接口真的能保持不变吗,可能是没经历过实际的项目,还需要慢慢消化。