IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> EffectiveC++-条款31:将文件间的编译依存关系降至最低 -> 正文阅读

[C++知识库]EffectiveC++-条款31:将文件间的编译依存关系降至最低

一. 内容

  1. 假如你对C++程序的某个class实现做了轻微修改,仅仅只是修改了private接口,然后重新构建这个程序。当你以为只要数秒就能完成的事:毕竟只有一个class被修改。但当你按下 build 类似按钮后,你意识到整个世界都被重新编译和连接了!发生这样的事,难道你不气恼吗?

  2. 问题出在:C++并没有把接口从实现中分离这件事做的很好。比如你在头文件定义了一些类成员变量,非指针或者引用,编译器就需要其定义式。而这样的定义式通常由 #include 指示符提供。所以你可能会 #include一大串头文件,不幸的是,这样一来,这些引入的头文件和该头文件便形成了编译依存的关系,当引入的头文件中任何一个被修改,或者这些引入的头文件自身依赖的其他头文件被修改,那么每一个包含该头文件的文件都需要重新编译。这样的连串编译关系会对许多项目造成难以估计的灾难。

  3. 你或许奇怪,为什么C++坚持将 class 的实现放在其定义式中。比如你可以使用前置声明,但是注意两个问题:

    • 并不是所有的成员都是class,有些可能是 typedefs,templates。正确的前置比较复杂。
    • 编译器必须在编译器间知道对象的大小,前置声明仅仅只能使用 references 或者 pointers。如果是基础类型或者指针,引用没问题,每个编译器都知道有多大,但是对于用户自定义类型的对象,是不存在约定的,它不知道要分配多少空间用以放置一个 custom class,编译器获得这项信息的唯一方式就是询问 class 的定义式。如果 class 不在定义式列出其实现,编译器该如何知道分配多少空间呢?
  4. 采用 pimpl 手法,可以使得 class 的数据成员的实现与定义分离,这是真正的接口与实现分离。其关键在于以声明的依存性替换了定义的依存性。这揭示了一个本质:尽可能让头文件自我满足,万一做不到,则让它与其他文件内的声明式相依。其他每一件事都源自这个简单的策略:

    • 如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects。你可以只靠一个类型声明式 class XXX 就可定义出该类型的 references 和 pointers;但如果定义某类型的 objects,就需要用到该类型的定义式 XXX xxx

    • 如果可以,尽量以 class 声明式替换 class 定义式。注意,当你声明一个函数而它用到某一个 class 时,你并不需要该 class 的定义式,纵使函数以 by value方式传递该参数,返回值亦然。

      虽然这种无需定义式的声明函数令人惊奇,但是一旦任何人调用那些函数,还是需要对应的定义式。或许你会奇怪,何必费心声明一个没人调用的函数呢?假设你有一个函数库内含数百个函数声明,客户不太可能调用到每一个,这样你便可将提供 class定义式的义务,从函数声明的头文件,转移到函数被调用的客户头文件,这样便大大降低了编译的依存性。

    • 为声明式和定义式提供不同的头文件。一个用于声明式,一个用于定义式,当然这两个文件必须保持一致性,如果某个声明式被改变了,两个文件都得改变。因此程序库客户应该总是 #include 一个声明文件,而非前置声明若干函数。比如在C++标准库中,存在 <iosfwd> 头文件内含 iostream 各组件的声明式,其对应定义分布在若干不同的头文件,包括 <sstream>,<streambuf>,<fstream> 和 <iostream>。对于 template,C++也提供 export 关键字可以让 template 声明式和 template 定义式分割于不同的头文件。不幸的是,支持该关键字的编译器目前非常少。

  5. 我们来看一个 pimpl 手法的例子:

    student.h

    #pragma once
    #include <memory>
    
    
    class StudentImpl;
    
    class Student {
    public:
        Student(int Id);
        
        /** 因为此处智能指针使用的是 unique_ptr ,它为了保证高效性,
         * 其删除器是自身的一部分,它必须保证 raw pointer 为 complete 对象。
         * 由于编译器默认生成的析构函数是 inline ,
         * 此时 Impl 所指之物仅仅是前置声明,是一个 non-complete 对象,所以会报错。
         * 因此如果使用 unique_ptr 而不是 shared_ptr 实现 Impl时,
         * 不要使用默认的析构行为,请自行额外实现。
         * 因为shared_ptr不使用自身的 deleter,无需这种保证。
         */
        ~Student();
        
        Student(const Student&) = delete;
        Student& operator=(const Student&) = delete;
        Student(Student&&) = delete;
        Student& operator=(Student&&) = delete;
        void SetId(int Id) const;
        int GetId() const;
    private:
        std::unique_ptr<StudentImpl> Impl;
    };
    

    student.cpp

    #include "Student.h"
    
    
    class StudentImpl {
    public:
        StudentImpl(int mId) {
            Id = mId;
        }
    
        ~StudentImpl() = default;
        StudentImpl(const StudentImpl&) = default;
        StudentImpl& operator=(const StudentImpl&) = default;
        StudentImpl(StudentImpl&&) = default;
        StudentImpl& operator=(StudentImpl&&) = default;
    
        void SetId(int mId) {
            Id = mId;
        }
    
        int GetId() const {
            return Id;
        }
    
    private:
        int Id;
    };
    
    Student::Student(int Id): Impl(std::make_unique<StudentImpl>(Id)) {}
    Student::~Student() = default;
    
    void Student::SetId(int Id) const {
        Impl->SetId(Id);
    }
    
    int Student::GetId() const {
        return Impl->GetId();
    }
    

    main.cpp

    const Student Moota(233);
    std::cout<<Moota.GetId()<<"\n";
        
    Moota.SetId(666);
    std::cout<<Moota.GetId()<<"\n";
    

    像 Student 这样使用 Impl 的 classes,往往称为 Handle classes。它们会将所有函数转交给 Impl 实现所有的实际工作。

  6. 另一个制作 Handle classes 的办法是:令 Student 成为一种特殊的 abstract base class(抽象基类),成为 Interface class。这种 class 的目的是详细描述 derived class 的接口,因此它们通常不带成员变量,也没有构造函数,只有一个 virtual 析构函数,以及一组 pure virtual 成员函数,用以描述整个接口。

  7. Interface class 类似 Java 和 .NET 的 Interfaces。但 C++ 的 Interface classes 并不需要负担 Java,.NET 的 Interfaces 所需负担的责任。举个例子,Java 和 NET 都禁止接口内实现其成员变量或者成员函数,但 C++ 并没有禁止这样的行为,这使得我们可以有更大的编程弹性。

  8. 对于 Interface class 的客户,必须以接口的指针或者引用来编写应用程序。因为不可能针对内含 pure-virtual 的函数的 abstract class 具现出实例。就像 Handle class的客户那样,除非 Interface class 的接口被修改,否则客户不需要重新编译。

  9. 对于 Interface class,客户通常使用 factory (工厂)函数或者 virtual 构造函数去创建其对象,它们会根据不同的参数值,读自文件或者数据库的数据,环境变量等创建不同类型的 derived class对象。

  10. 实现 Interface class 的两个常见机制:从 Interface class 继承接口,然后实现出接口所覆盖的函数。另一个实现方法涉及多重继承,我们将在条款40进行介绍。

  11. Handle class 和 Interface class 解除了接口和实现的耦合关系,从而降低了文件间的编译依存性。但正如你所想的,这需要付出代价,通常也就是计算机运行中通常要付出的那些:丧失在运行期的速度和增加额外的内存消耗。

    在 Handle class 身上,成员函数必须通常 Impl 取得对象数据,这意味着多一层访问的消耗,并且 Impl 的存在需要额外的动态内存分配,这意味着内存分配消耗和释放内存消耗,以及潜在的 bad_alloc 内存不足异常。

    而对于 Interface class,由于每一个函数都是 virtual,所以你必须 付出 virtual 实现的代价:额外的虚指针和虚表消耗。

    最后,无论Handle classes或 Interface classes,都不能充分利用inline函数,因为它们大多数情况需要隐藏类的实现细节,如函数本体。

    当然,如果只因为若干额外的成本就不使用这些技术,将是严重的错误。最好的方式是:当它们影响运行速度,内存大小过于重大,而类的接口和实现耦合已经显得不再重要时,才考虑使用具体类替换这个行为。

二. 总结

  1. 支持编译依存性最小化的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
  2. 程序库头文件应该以完全且仅声明式(full and declaration-only forms)的形式存在。这种做法不论是否涉及 templates 都适用。
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-07-17 16:02:42  更:2022-07-17 16:05:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/13 0:08:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码