• [转]文件间的编译依赖性


    我们打开自己的C++程序代码,对某个类的实现做了细小的改动。改动的不是接口,而是类的实现,只是细节部分。然后准备重新生成程序,此情况下编译和链接应该只会花几秒种。因为只是改动了一个类。于是Rebuild或make(或其它类似命令),然而发现所有文件都在被重新编译、重新链接。

       问题的原因在于,将接口从实现中分离这方面,C++做得不是很出色。尤其是,C++的类定义中不仅包含接口规范,还有不少实现细节。例如:

    class Person {
    public:
      Person(const string& name, const Date& birthday,
             const Address& addr, const Country& country);
      virtual ~Person();
      ...                  // 简化起见,省略了拷贝构造函数和赋值运算符函数
      string name() const;
      string birthDate() const;
      string address() const;
      string nationality() const;
    private:
      string name_;            // 实现细节
      Date birthDate_;         // 实现细节
      Address address_;        // 实现细节
      Country citizenship_;    // 实现细节
    };

       Person的实现用到了一些类,即string, Date,Address和Country;Person要想被编译,就得让编译器能够访问得到这些类的定义。这样的定义一般是通过#include指令来提供的,所以在定义Person类的文件头部,可以看到象下面这样的语句:

    #include <string>           // 用于string类型
    #include "date.h"
    #include "address.h"
    #include "country.h"

        这样定义Person的文件和这些头文件之间就建立了编译依赖关系。所以若任一个辅助类(即string, Date,Address和Country)改变了它的实现,或任一个辅助类所依赖的类改变了实现,包含Person类的文件以及任何使用了Person类的文件就必须重新编译。对于Person类的用户来说,实在是令人讨厌,因为这种情况用户绝对是束手无策。为什么C++一定要将一个类的实现细节放在类的定义中。例如,为什么不能象下面这样定义Person,使得类的实现细节与之分开。

    class string;         // "概念上" 提前声明string 类型
    class Date;           // 提前声明
    class Address;        // 提前声明
    class Country;        // 提前声明
    class Person {
    public:
      Person(const string& name, const Date& birthday,
             const Address& addr, const Country& country);
      virtual ~Person();
      ...                      // 拷贝构造函数, operator=
      string name() const;
      string birthDate() const;
      string address() const;
      string nationality() const;
    };

       若这种方法可行,除非类的接口改变,Person 的用户不需要重新编译。大系统的开发过程中,在开始类的具体实现之前,接口往往基本趋于固定,所以这种接口和实现的分离将大大节省重新编译和链接所花的时间。

     可惜的是,现实总是和理想相抵触,看看下面就会认同这一点:
    int main()
    {
      int x;                      // 定义一个int
      Person p(...);              // 定义一个Person
                                  // (为简化省略参数)
      ...
    }

        看到x的定义,编译器必须知道为它分配一个int大小的内存。这没问题,每个编译器都知道一个int有多大。然而,当看到p的定义时,编译器虽然知道必须为它分配一个Person大小的内存,但怎么知道一个Person对象有多大呢?唯一的途径是借助类的定义,但如果类的定义可以合法地省略实现细节,编译器怎么知道该分配多大的内存呢?

      原则上说,这个问题不难解决。有些语言如Smalltalk,Eiffel和Java每天都在处理这个问题。它们的做法是,当定义一个对象时,只分配足够容纳这个对象的一个指针的空间。也就是说,对应于上面的代码,它们这样做:

    int main()
    {
      int x;                     // 定义一个int
      Person *p;                 // 定义一个Person指针
        
      ...
    }

       可能以前碰到过这样的代码,因为它实际上是合法的C++语句。程序员完全可以自己来做到 "将一个对象的实现隐藏在指针身后"。

       采用这一技术来实现Person接口和实现的分离。首先,在声明Person类的头文件中只放下面的东西:
    // 编译器要知道这些类型名,因为Person的构造函数要用到
    class string;      // 对标准string来说这样做不对,
    class Date;
    class Address;
    class Country;
    // 类PersonImpl将包含Person对象的实现细节,此处只是类名的提前声明
    class PersonImpl;
    class Person {
    public:
      Person(const string& name, const Date& birthday,
             const Address& addr, const Country& country);
      virtual ~Person();
      ...                               // 拷贝构造函数, operator=
      string name() const;
      string birthDate() const;
      string address() const;
      string nationality() const;
    private:
      PersonImpl *impl;                 // 指向具体的实现类
    };

       这时Person的用户程序完全和string,date,address,country以及person的实现细节分开了。那些类可以随意修改,而它们可以不需要重新编译。另外,因为看不到Person的实现细节,用户不可能写出依赖这些细节的代码。这是真正的接口和实现的分离。

       分离的关键在于,"对类定义的依赖" 被 "对类声明的依赖" 取代了。为了降低编译依赖性,只要知道这一条就足够了:只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义。其它一切方法都源于这一简单的设计思想。

    · 若可使用对象的引用和指针,就避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明。定义此类型的对象则需要类型定义的参与。

    · 尽可能使用类声明,而不使用类定义。因为声明一个函数时,若用到某个类,绝对不需要这个类的定义,即使函数是通过传值来传递和返回这个类:
      class Date;                    // 类的声明
      Date returnADate();            // 正确 ---- 不需要Date的定义
      void takeADate(Date d);    
      传值通常不是个好主意,但出于什么原因不得不这样做时,千万不要还引起不必要的编译依赖性。

    · 不要在头文件中再包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己去包含其它的头文件,以使用户代码最终得以通过编译。事实上,这种技术很受推崇,并被运用到C++标准库中;头文件<iosfwd>就包含了iostream库中的类型声明(而且仅仅是类型声明)。

     Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句柄类(Handle class)或信封类(Envelope class)。对它们所指向的类,前一种情况对应的叫法是主体类(Body class);后一种情况则叫信件类(Letter class)。)

      句柄类实际上只是把所有的函数调用都转移到了对应的主体类中,主体类真正完成工作。例如,下面是Person的两个成员函数的实现:
    #include "Person.h"      // 因为是在实现Person类所以必须包含类的定义
    #include "PersonImpl.h"  // 也须包含PersonImpl类的定义,否则不能调用其成员函数。
    // 注意PersonImpl和Person含有一样的成员函数,它们的接口完全相同
    Person::Person(const string& name, const Date& birthday,
                   const Address& addr, const Country& country)
    {
      impl = new PersonImpl(name, birthday, addr, country);
    }
    string Person::name() const
    {
      return impl->name();
    }

        注意Person的构造函数怎样调用PersonImpl的构造函数(隐式地以new来调用)以及Person::name怎么调用PersonImpl::name。使Person成为一个句柄类并不改变Person类的行为,改变的只是行为执行的地点。

     除了句柄类,另一选择是使Person成为一种特殊类型的抽象基类,称为协议类(Protocol class)。根据定义,协议类没有实现;它存在的目的是为派生类确定一个接口。它一般没有数据成员,没有构造函数;有一个虚析构函数,还有一套纯虚函数,用于制定接口。Person的协议类象下面这样:

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

     Person类的用户必须通过Person的指针和引用来使用它,因为实例化一个包含纯虚函数的类不可能的。和句柄类的用户一样,协议类的用户只是在类的接口被修改的情况下才需要重新编译。

       协议类的用户必然有什么方法来创建新对象。常通过调用一个函数来实现,此函数扮演构造函数的角色,而这个构造函数所在的类即那个真正被实例化的隐藏在后的派生类。这种函数叫法挺多(如工厂函数(factory function),虚构造函数(virtual constructor)),但行为却一样:返回一个指针,此指针指向支持协议类接口的动态分配对象。这样的函数象下面这样声明:

    // makePerson是支持Person接口的对象的"虚构造函数" ( "工厂函数")
    Person*  makePerson(const string& name,   // 用给定的参数初始化一个新Person对象
                 const Date& birthday,       //然后返回对象指针
                 const Address& addr,       
                 const Country& country);  

    用户这样使用它:
    string name;
    Date dateOfBirth;
    Address address;
    Country nation;
    ...
    // 创建一个支持Person接口的对象
    Person *pp = makePerson(name, dateOfBirth, address, nation);
    ...
    cout  << pp->name()              // 通过Person接口使用对象
          << " was born on "        
          << pp->birthDate()
          << " and now lives at "
          << pp->address();
    ...
    delete pp;                       // 删除对象

     makePerson这类函数和它创建的对象所对应的协议类(对象支持这个协议类的接口)是紧密联系的,所以将它声明为协议类的静态成员是很好的习惯:
    class Person {
    public:
      ...        
    // makePerson现在是类的成员
      static Person * makePerson(const string& name,
                                 const Date& birthday,
                                 const Address& addr,
                                 const Country& country);

       这样就不会给全局名字空间(或任何其他名字空间)带来混乱,因为这种性质的函数会很多。

       某个地方,支持协议类接口的某个具体类(concrete class)必然要被定义,真的构造函数也必然要被调用。它们都背后发生在实现文件中。例如,协议类可能会有一个派生的具体类RealPerson,它具体实现继承而来的虚函数:

    class RealPerson: public Person {
    public:
      RealPerson(const string& name, const Date& birthday,
                 const Address& addr, const Country& country)
      :  name_(name), birthday_(birthday),
         address_(addr), country_(country)
      {}
      virtual ~RealPerson() {}
      string name() const;          // 函数的具体实现没有在这里给出
      string birthDate() const;     //但它们都很容易实现
      string address() const;      
      string nationality() const;   
    private:
      string name_;
      Date birthday_;
      Address address_;
      Country country_;

    有了RealPerson,写Person::makePerson就非常简单:
    Person * Person::makePerson(const string& name,
                                const Date& birthday,
                                const Address& addr,
                                const Country& country)
    {
      return new RealPerson(name, birthday, addr, country);
    }

     实现协议类有两个最通用的机制,RealPerson展示了其中之一:先从协议类(Person)继承接口规范,然后实现接口中的函数。另一种实现协议类的机制涉及到多继承。

       句柄类和协议类分离了接口和实现,从而降低了文件间编译的依赖性。但它在运行时会多耗点时间,也会多耗点内存。

     句柄类的情况下,成员函数必须通过(指向实现的)指针来获得对象数据。这样,每次访问的间接性就多一层。此外,计算每个对象所占用的内存大小时,还应算上这个指针。还有,指针本身还要被初始化(在句柄类的构造函数内),以使之指向被动态分配的实现对象,所以,还要承担动态内存分配(以及后续的内存释放)所带来的开销。

     对于协议类,每个函数都是虚函数,所以每次调用函数时必须承担间接跳转的开销。而且,每个从协议类派生而来的对象必然包含一个虚指针。这个指针可能会增加对象存储所需要的内存数量(具体取决于:对对象的虚函数来说,此协议类是不是它们的唯一来源)。

     最后一点,句柄类和协议类都不大会使用内联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。

       在开发阶段要尽量用句柄类和协议类来减少 "实现" 的改变对用户的负面影响。如果带来的速度和/或体积的增加程度远远大于类之间依赖性的减少程度,那么,当程序转化成产品时就用具体类来取代句柄类和协议类。

     有些人还喜欢混用句柄类、协议类和具体类,并且用得很熟练。这固然使得开发出来的软件系统运行高效、易于改进,但有一个很大的缺点:还是必须得想办法减少程序重新编译时消耗的时间。

    原文地址:http://blog.csdn.net/armman/archive/2007/03/01/1518642.aspx
  • 相关阅读:
    Security+考试通过心得
    Splunk Power User认证
    Splunk笔记
    关于工作
    智能合约安全-parity多重签名钱包安全漏洞
    kickstart构建Live CD 添加文件问题
    Local Authentication Using Challenge Response with Yubikey for CentOS 7
    计算Linux权限掩码umask值
    IntelliJ IDEA 常用快捷键
    关于常量池-----小例子
  • 原文地址:https://www.cnblogs.com/taoxu0903/p/726790.html
Copyright © 2020-2023  润新知