Effective C++学习笔记之第五章(4)

来源:互联网 发布:苏州网络李刚评话视频 编辑:程序博客网 时间:2024/04/29 15:35

chapter 5 实现

item31:将文件的编译依存关系降至最低
1)首先考虑一个关于person的类,一般的定义如下:

#include <string>#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;  // implementation detail    Date theBirthDate;   // implementation detail    Address theAddress;  // implementation detail};

这里我们发现包含了date和address定义的头文件,OK,问题来了,如果说这两个头文件中的东西改变了,或者是这两个头文件包含的头文件里面的东西改变了,那么,所有用到这些定义的代码都要重新编译一遍。my god!想想就觉得很烦~
2)OK,那现在将date和address的声明和定义分开,这样的话,只有在person类的接口改变的时候才会重新编译。

namespace std {//为什么这种申明是错的呢?因为string是一个typedef而不是类     class string;  // forward declaration (an incorrect one — see below)}  class Date;         // forward declarationclass Address;      // forward declarationclass 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这种build-in type,不需要手工声明,仅仅通过#include适当的头文件即可。一般头文件不太可能成为编译瓶颈。
3)但是这样还是有问题,my god!假设用户需要定义一个person对象,就需要预先知道它的大小吧,要不怎么分配内存。OK,获取大小的时候需要找到它里面的成员变量以及其实现的细节吧,这里其实就相当于已经跟那些定义好的date和address变量产生了耦合。但是如果我们是定义一个build-in type,OK,那就没那么麻烦啦~因为编译器都知道build-in type的大小啦~所以用一个具体的PersonImpl的类去实现数据的耦合,person里面只包含这个类的一个指针就OK了,这样不管PersonImpl里面的数据的定义怎么变,与person无关。在定义其对象时,它就包含一个指针而已。这就是所谓的桥梁设计模式?如果我没有记错的话,关于设计模式真心知道得很少。

#include <string>  // standard library components shouldn't be forward-declared#include <memory>            // for tr1::shared_ptr; see belowclass PersonImpl;            // forward decl of Person impl. classclass Date;                  // forward decls of classes used inclass Address;              // Person interfaceclass 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:   // ptr to implementation;  std::tr1::shared_ptr<PersonImpl> pImpl;};//需要定义person对象时,就定义一个指针。int main(){  int x;               // define an int  Person *p;           // define a pointer to a Person  ...}

4)几个准则:
如果对象的引用或指针可以实现,就不要用对象。因为定义一个类型的指针仅仅需要关于它的声明就可以了,管它怎么定义呢。那要是定义对象的话,就必须要知道它的定义了。
尽量依赖于变量的声明而不是定义。注意,你不需要在声明一个函数(用到某一个类)的时候知道这个类的定义,就算是以值传参或者返回。当然以值传参并不是一个好主意,但不得不这么做的时候,也不影响你降低编译依存关系。

class Date;                        // class declaration#include "datefwd.h"               //或者直接包含头文件Date today();                      // fine — no definitionvoid clearAppointments(Date d);    // of Date is needed

用不同的文件将声明和定义分开来。稍微有点经验的程序员都经常干这事。
5)关于export关键字。它的作用是允许模板的声明和定义分开来,不过目前很多编译器都不支持这个关键字,并且关于这个关键字的实际应用也比较少。
6)关于指针实现的类(Handle Class)。这两种实现方式下,做改变是不需要重新编译的,除非改变的是接口:
一是将它们的所有函数转交给相应的实现类,由它去完成实际工作。二是将它们定义成抽象的类(Interface Class),不带成员变量,也没有构造函数,只有一个虚析构函数以及一组纯虚函数来叙述整个接口。有必要明确C++在这一点上和Java和.Net的区别。后者不允许在Interface内实现成员变量和函数,而前者可以实现,如果确定这种实现是有意义的话。(第二种方式我需要好好领悟领悟)

//方案一person的定义#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();}//方案二person的定义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的客户必须有办法为这种class创建新的对象,通常调用一个特殊函数来扮演真正将被实例化的派生类的构造函数角色。这样的函数通常称为factory函数或者虚构造函数,返回指向动态分配所得对象的指针,且该对象支持Interface Class接口。

class Person {public: ...// return a tr1::shared_ptr to a new Person initialized with the given params; static std::tr1::shared_ptr<Person> create(const std::string& name,const Date& birthday,const Address& addr); ...//注意这里是一个static函数,可以根据使用它的具体类来进行定义};//用户的调用方式std::string name;Date dateOfBirth;Address address;...// create an object supporting the Person interfacestd::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));...std::cout << pp->name() << " was born on " << pp->birthDate()          << " and now lives at " << pp->address();//这里不需要人工使用delete,why?

7)当然没有完美的方法,只有知己知彼,方能百战百胜,缺点:
Handle Class,增加了每一个对象的内存消耗,以及动态内存分配及其释放动作带来的额外开销,甚至有可能出现bad-alloc。
Interface Class,每次函数调用有一个间接跳跃成本,并且每个派生类对象里面含有一个vptr,会增加内存消耗,如多重继承,并且有多个基类有虚函数。
8)当然这些Handle Class和Interface Class如果离开了inline函数,也不可能有太大的作为。why?inline函数是干嘛用的?你忘了,好吧,你可以再回去看看,体会体会。透彻了解inline函数的里里外外。还不知道答案?我走了~

原创粉丝点击