• effective c++ 笔记 条款26-31


    条款 26:尽可能延后变量定义式出现的时间

    应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止,以此避免构造(和析构)非必要对象,还可以避免无意义的default构造行为
    对于循环操作,在循环前还是中进行构造,取决于赋值操作与构造+析构操作的成本对比。循环前构造会将变量的作用域扩大,除非知道该变量的赋值成本比“构造+析构”成本低,或者对这段程序的效率要求非常高,否则建议使用循环中构造

    条款27: 尽量少做转型动作

    const_cast:通常被用来将对象的常量性转除
    dynamic_cast:主要用来执行“安全向下转型”,要用于基类和派生类之间的类型转换,具有类型检查的功能,比static_cast安全。运行时转型,但开销大,会执行对继承体系的检查
    reinterpret_cast:意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植
    static_cast:用来强迫隐式转换,编译时进行类型转换,主要用于基本数据类型的转换、隐式转换的显式化和向上转型
    尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。
    如果有个设计需要转型动作,通常两种做法,一:使用容器,并在其中存储直接指向derived class对象的指针(通常是智能指针),这样就避免了上述需求。二:在base class内提供virtual函数做你想对各个派生类想做的事情。这样可以使得你通过base class 接口处理“所有可能之各种派生类”。
    如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内
    宁可使用C++新式转型,也不用用C的旧式,因为新式的更容易被注意到,而且各自用途专一

    条款28:避免返回handles 指向对象内部成分

    reference、指针、迭代器系统都是所谓的handles。函数返回一个handle,随之而来的便是“降低对象封装性”的风险。它也可能导致:虽调用const成员函数却造成对象状态被更改的风险。

    Point& UpperLeft() const { return pData->ulhc; }
    Point& LowerRight() const { return pData->lrhc; }
    
    • 1
    • 2

    成员变量可以在外部被修改,违反 logical constness 的原则的。绝对不应该令成员函数返回一个指针指向“访问级别较低”的成员函数。
    改成返回常引用可以避免对成员变量的修改

    const Point& UpperLeft() const { return pData->ulhc; }
    const Point& LowerRight() const { return pData->lrhc; }
    
    • 1
    • 2

    但是依然会带来一个称作 dangling handles(空悬句柄) 的问题,当对象不复存在时,你将无法通过引用获取到返回的数据
    最保守的做法,返回一个成员变量的副本

    Point UpperLeft() const { return pData->ulhc; }
    Point LowerRight() const { return pData->lrhc; }
    
    • 1
    • 2

    条款29: 为“异常安全”而努力是值得的

    异常安全函数(Exception-safe functions)提供以下三个保证之一:

    1. 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下,没有任何对象或数据结构会因此败坏,所有对象都处于一种内部前后一致的状态,然而程序的真实状态是不可知的,也就是说客户需要额外检查程序处于哪种状态并作出对应的处理。
    2. 强烈保证: 如果异常被抛出,程序状态完全不改变,换句话说,程序会回复到“调用函数之前”的状态。
    3. 不抛掷(nothrow)保证: 承诺绝不抛出异常,因为程序总是能完成原先承诺的功能。作用于内置类型身上的所有操作都提供 nothrow 保证。
      throw() C++11 中已经被弃用,取而代之的是noexcept关键字:
    int DoSomething() noexcept;
    
    • 1

    当异常抛出时,1.不泄漏任何资源 2.不允许数据败坏;

    void PrettyMenu::ChangeBackground(std::vector<uint8_t>& imgSrc) {
        lock(&mutex);
        delete bgImage;
        ++imageChanges;
        bgImage = new Image(imgSrc);
        unlock(&mutex);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    若在new Image函数中抛出异常,mutex会发生资源泄漏,未执行unlockbgImageimageChanges也会发生数据败坏,bgImage指向已删除对象

    为做到强烈保证:

    1. 使用“资源管理类”条款13
    2. 调换代码顺序
    void PrettyMenu::ChangeBackground(std::vector<uint8_t>& imgSrc) {
        Lock m1(&mutex);
        bgImage.reset(std::make_shared<Image>(imgSrc));
        ++imageChanges;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    删除动作只发生在新图像创建成功后,只有make_shared成功,reset才调用,delete也只在reset函数内使用

    另一个常用于提供强烈保证的方法是copy and swap,为打算修改的对象做出一份副本,对副本执行修改,并在所有修改都成功执行后,用一个不会抛出异常的swap方法将原件和副本交换

    void PrettyMenu::ChangeBackground(std::vector<uint8_t>& imgSrc) {
        Lock m1(&mutex);
        auto pNew = std::make_shared<PMImpl>(*pImpl);    // 获取副本
        pNew->bgImage.reset(std::make_shared<Image>(imgSrc));
        ++pNew->imageChanges;
        std::swap(pImpl, pNew);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    强烈保证并非对所有函数都可实现或具备现实意义

    异常安全保证具有木桶效应

    条款30:透彻了解inlining的里里外外

    inlining函数可以免除函数调用成本的开销
    inlining两种实现方式:一种是为其显式指定inline关键字,另一种是直接将成员函数的定义式写在类中
    inline函数通常一定被置于头文件内,因为inlining大部分情况下都是编译期行为;template通常也被置于头文件内,因为大部分建置环境都是在编译期完成具现化动作;如果一个template具现出来的函数都应该inlined,则将此template声明为inline,否则应避免此声明
    inline只是对编译器的一个申请,不是强制命令。大多数编译器如无法将要求的函数inline化,会给出一个警告信息
    将大多数inlining限制在小型、被频繁调用的函数身上,以便于日后的调试和二进制升级
    编译器通常不对“通过函数指针而进行的调用”实施inlining
    构造函数和析构函数并不适合用于inlining,往往会引起代码的膨胀(即不要随便将构造函数和析构函数的定义体放在类声明中)
    inline函数代码如发生改变,所有用到该inline函数的程序都必须重新编译
    inline修饰符用于解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题
    在现在的 C++ 标准中,inline作为优化建议的含义已经被完全抛弃,取而代之的是“允许函数在不同编译单元中多重定义”,使得可以在头文件中直接给出函数的实现(每个.cpp的编译都是独立的,对于每个cpp来说,都是包含了Func的声明和实现,所以在链接时不清楚到底是链接哪一个同名函数)
    头文件里面定义全局变量或实现了具体的函数会报错“fatal error LNK1169: 找到一个或多个多重定义的符号”,类、结构体、函数声明等抽象的东西不会

    条款 31:将文件间的编译依存关系降至最低

    为了增加编译速度,应该减少类文件之间的相互依存性(include),但是类内又常常使用到其他类,不得不相互依存
    解决方案是:将类的声明和定义分开(不同的头文件),声明相互依存,而定义不相依存,这样当定义需要变更时,编译时不需要再因为依赖而全部编译。即尽可能让头文件自我满足
    两种方法:
    Handle classes/句柄类:一个声明类,一个imp实现类,声明类中不涉及具体的定义,只有接口声明,在定义类中include声明类,而不是继承。handle类的成员函数必须通过imp指针取得对象数据,每次访问会增加一层间接性会增加开销。且imp指针必须初始化在handle类的构造函数内,指向一个动态分配得来的imp对象,指针大小和动态分配内存也会带来开销
    Interface classes/接口类:在接口类中提供纯虚函数,作为一个抽象基类,定义类作为其子类来实现具体的定义。增加存储虚表指针和实现虚函数跳转带来的开销

  • 相关阅读:
    Pr:多机位编辑
    MySQL优化01-索引
    透明多级分流系统(架构扫盲贴)
    iOS13以后 获取KeyWindow
    20_多线程01
    Java面试题整理(带答案)
    基于ssm框架的毕业设计管理系统毕业设计源码211633
    15年架构师:再有面试官问你Kafka,就拿这篇学习笔记怼他
    DOX-HSA/HGB/FITC/Glu人血清蛋白/血红蛋白/荧光素/葡萄糖修饰阿霉素
    关于Android 日历事件的实现
  • 原文地址:https://blog.csdn.net/Mhypnos/article/details/136199100