zoukankan      html  css  js  c++  java
  • 条款31:将文件间的编译依存关系降至最低

    1、文件间依存度高的话带来的影响?

    假如你修改了C++ class实现文件,修改的仅仅是实现,而没有修改接口,而且只修改private部分。此时,重新构建这个程序时,会发现整个文件、以及用到该class 的文件都被会被重新编译和连接,这不是我们想要看到的。

    2、出现上述问题的原因

    问题出在C++没有把关于接口与实现相分离这件事做好。C++ 的class 的定义式中不仅定义了接口,还定义了实现细目(成员变量)。
    例如:

    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;         //实现细目 
    };
    

    当编译器没有取得实现代码所需要的class string,Date和Address的定义式时,它无法通过编译它所需要的这样的定义式往往由#include <>提供(里面有class string,Date和Address的实现代码)。例如本例中需要:。

    #include <string> 
    #include "date.h" 
    #include "address.h"
    

    如果这些头文件中(或头文件所依赖的头文件)的一个的实现被改变了,那么每一个用到class 类的文件都得重新编译。这就是所谓的文件间的依存度比较高。

    3、解决文件间依存性的一个不成熟方案

    C++ 为什么不如下述这样做,以实现接口与实现分离呢?

    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; 
        ... 
    };
    

    上述设想不成立,原因有两条:

    • 第一:string并不是一个class,他是一个typedef(定义为basic_string)。因此上述针对string做的声明并不正确;正确的声明比较复杂,因为涉及额外的tempalte。退一步讲,你本来就不应该尝试手工声明标准库程序的一部分,你应该仅仅使用适当的#include完成目的。其实标准头文件这也不是编译的瓶颈,也有解决的方法。例如:你可以值改变你的接口涉及,避免使用
      标准头文件的非法的#include。
    • 第二:编译器必须在编译期间知道对象的大小。这才是问题的关键。例如:下述程序中,当编译器看到x时,由于知道它是int类型的,也就知道需要为它分配多大的空间。但是当编译器看到自定义的类Person对象p时,编译器必须看到Person的类定义才能知道为p对象分配多大的内存。如果class中没有实现细目,那么编译器就无法确定为其分配多大内存。

    4、解决文件间依存关系的正确手法一:handle class

    (1)基本思想

    将对象的实现细目隐藏到一个指针(通常是一个智能指针)背后。
    例如:

    #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 类写成两个部分,接口那部分是主要的部分,其中含了一个智能指针,指向实现细目。而实现细目另外定义了一个类:PersonImpl。这种设计手法被称为:pimpl idiom。
    注意:pimpl 指的是 pointer to implementation。这种class内的指针往往被称为:pImpl指针。上述class的写法 往往被称为handle class。

    (2)上述手法的好处

    实现了接口与实现的分离。即:Person的客户与 Date、Address、以及Person的实现细目就分离了。
    实现接口与实现的分离所带来的的好处:

    • 这些class的修改,都不需要Person客户进行重新编译。
    • 而且由于客户无法看到实现细目,也就不能写出由这些实现细目所决定的代码。
    (3)上述实现的关键
    (4)该手法下的其他情况细分
    • 如果用 object reference 或 object pointer 可以完成任务,就不要用 objects。
      可以只靠声明式定义出指向该类型的 pointer 和 reference;但如果定义某类型的 objects,就需要用到该类型的定义式。
    • 如果能够,尽量以 class 声明式替换 class 定义式。
      当你声明一个函数而它用到某个 class 时,你并不需要该 class 的定义式,纵使函数以 by value 方式传递该类型的参数(或返回值)亦然。
    • 为声明式和定义式提供不同的头文件。
      两个头文件应该包吃一致性,其中一个头文件发生改变,另一个就得也改变。一个内含了class 接口的定义,另一个仅仅内含声明。

    例如:只含声明式的Date class 的头文件应该命名为datefwd.h(有一定的命名规则)。
    注意:本条款适用于template 也适用于 non-template。

    5、解决文件间依存关系的正确手法二:另外一种实现handle class的手法,interface class手法

    (1)基本思想

    令Person class 成为一种特殊的abstract base class (抽象基类),称为interface class。这样的类通常:没有成员变量,也没有构造函数,只有一个virtual 的析构函数以及一组pure virtual 用来描述接口。

    (2)C++ 接口类与其他语言接口类的不同

    像.net和java 的接口,他们不允许在接口类中定义成员函数和成员变量。但是C++的接口类并不禁止,这样的规则使得C++语言具有更大的弹性。

    (3)interface class 类创建对象的方式

    由于这样的类往往没有构造函数,因此通过工厂函数或者virtual构造函数创建,他们返回指针,指向动态分配对象所得的对象,这样的对象支持interface class的 接口,这样的函数在interface class往往被声明为 static,例如:

    class Person{ 
    public: 
        ... 
        static std::tr1::shared_ptr<Person> 
        create(const std::string& name, const Date& birthday, const Address& addr); 
    };
    

    客户使用他们像这样:

    std::string name; 
    Date dateBirth; 
    Address address; 
    std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address)); 
    ... 
    std::cout << pp->name() 
                << "was born on " 
                << PP->birthDate() 
                << " and now lives at " 
                << pp->address(); 
    ...
    

    当然支持 interface class 接口的那个具象类(concrete classes)必须被定义出来,而真正的构造函数必须被调用。
    假设有个 derived class RealPerson,提供继承而来的 virtual 函数的实现:

    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; 
    };
    
    

    有了 RealPerson 之后,写出 Person::create 就真的一点也不稀奇了:

    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)); 
    }
    

    一个更现实的 Person::create 实现代码会创建不同类型的 derived class 对象,取决于诸如额外参数值、独自文件或数据库的数据、环境变量等等。

    RealPerson 示范实现了 Interface class 的两个最常见机制之一:从 interface class 继承接口规格,然后实现出接口所覆盖的函数。

    6、两种手法带来的开销

    handle classes 和 interface classes 解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。

    • handle classe

      成员函数必须通过 implementation pointer 取得对象数据。那会为每一次访问增加一层间接性。每个对象消耗的内存必须增加一个 implementation pointer 的大小。 implementation pointer 必须初始化指向一个动态分配的 implementation object,所以还得蒙受因动态内存分配儿带来的额外开销。

    • Interface classe

      由于每个函数都是 virtual,必须为每次函数调用付出一个间接跳跃。此外 Interface class 派生的对象必须内含一个 vptr(virtual table pointer)。

    7、两种手法的适用场景

    在程序开发过程中使用 handle class 和 interface class 以求实现码有所改变时对其客户带来最小冲击。

    而当他们导致速度和/或大小差异过于重大以至于 class 之间的耦合相形之下不成为关键时,就以具象类(concrete class)替换 handle class 和 interface class。

  • 相关阅读:
    偷窃转基因玉米种子引发中美打农业官司
    关于PreferenceActivity的使用和一些问题的解决(自己定义Title和取值)
    大写中文数字-財务
    【leetcode】LRU
    【AC大牛陈鸿的ACM总结贴】【ID AekdyCoin】人家当初也一样是菜鸟
    android面试题 不单单为了面试也是一次非常好的学习
    存储系统的实现-探析存储的机制和原理
    unity3d脚本编程
    ubuntu12.04 安装配置jdk1.7
    Android中一个类实现的接口数不能超过七个
  • 原文地址:https://www.cnblogs.com/lasnitch/p/12764163.html
Copyright © 2011-2022 走看看