Effective C++

Effective C++

参考:

  1. Effective C++, 3rd edition

trivial tips

技术

  1. C++ 是多范式语言

    1. 视 C++ 为一个多范式编程语言,C++ 支持过程、OO、OB、泛型、函数式编程与元编程等范式(1)
  2. 尽量避免宏 #define 的使用(2)

  3. 尽可能使用 const 关键字(3)

    1. 注意容器中 iteratorconst_iterator 之间的区别,这其实是语法上的无奈

    2. 注意 bitwise const 和 logical const 的区别,前者是编译器对 const 的实现方法,后者是对 const 的用法

    3. 虽然不应该使用类型转换但为了避免重复可以在 non-const 函数种调用 const 函数不要在 const 函数中调用 non-const 函数,这样违反了语义

      return const_cast<char&>( // 非安全转型,去除返回值中的 const 修饰
          static_cast<const T&>(*this).ret_char_ref(); // 安全转型,给当前对象加上 const 修饰
      );
      
  4. 不要混淆赋值与初始化(4)

    1. 注意跨编译单元的初始化无序问题;常用的解决办法是使用 local static 替换 no-local static
  5. 编译器自动生成 Big5 的原则

    1. 如果没有定义且可行则编译器自动为对象生成拷贝构造、拷贝赋值和析构函数(默认非 virtual);如果没有自定义构造函数则编译器会生成一个默认构造函数 (5)
  6. class A: private Uncopyable {...}; (6)

  7. 多态基类的析构函数加上 virtual,注意这里的两个关键词:多态&基类 (7)

    1. 多态场景下,没有 virtual 析构的类不应该被继承,不然会因局部析构的问题造成内存泄漏等异常。C++ 中 的 string 和一些容器,比如 vector/map/set 等都没有 virtual 析构,所以要避免继承这些类,尽量使用 final 关键字避免这些类的继承。非多态用途的类,比如 Uncopyable,可以被继承
    2. 不要给不需要的类加上 virtual,避免不必要的性能损失
  8. 不要让析构函数抛异常,一个合适的解决方法是做两手准备,将在析构中可能抛异常的动作独立出去,作为一个可执行函数,然后在析构中调用并尝试捕获异常,如此用户就可以自行选择调用方式。析构中捕获异常要么直接 abort,要么吞掉异常(8)

  9. 不要在构造与析构函数中调用 virtual 函数(9)

  10. inline (30)

    1. 类声明(头文件)中定义的函数默认 inline
    2. 有被取地址操作的 inline 函数会被剥夺 inline 属性;很多调试场景,inline 默认是禁止的;构造与析构函数的 inline 是非常差的设计

设计

  1. 令 operator = 返回 reference to *this,a = b = c (10)

  2. 使用证同测试在 operator = 中处理自我赋值;更好的办法是使用异常安全代码:使用 swap。没有处理自我赋值的拷贝赋值可能造成资源意外释放等问题(11)

    A& operator=(const A& rhs) {
    	A tmp(rhs);
    	swap(*this, tmp);
    	return *this;
    }
    
  3. 使用独立的语句将资源置入自能指针中,避免调用异常时造成资源泄漏(17)

    //A的创建/fun的执行/智能指针的创建,随编译器实现而不同,一旦出现异常很可能出现资源泄漏
    process(std::shared_ptr<A>(new A()), fun()); // 错误用法
    // 下面的写法比较合适
    auto sptr = std::shared_ptr<A>(new A());
    process(sptr, fun());
    
  4. 适当的引入新类型可以减少不必要的错误,比如引入 Day/Month/Year 类,可以减少参数填些的错误(18)

  5. 封装意味着减少信息的泄漏,非 private 的成员变量与更多的成员函数都会泄漏更多的信息(22、23)

    1. 将所有成员变量设置为 private 虽然在某些场景下增加了代码量但提高了代码的维护提供了弹性(22)
    2. 可能的话,使用 non-member/non-friend 函数代替 member 函数(23)
      1. 面向对象提倡封装,non-member/non-friend 可以提供比 member 更高的封装性能。封装程度越高意味着对象向外提供的信息越少,从实现上看就是对成员变量的访问(成员函数)更少
      2. 非必要的 helper 函数可以放到其他编译单元中以减少代码之间的依赖
  6. 使用 non-member 函数实现所有参数的自动类型转换,例如 class A 与常量之间的乘法(24)

  7. 尽量少做转型(27)

    1. 常见转型工具
      1. const_cast唯一能去除变量const属性的 C++-style 转型工具
      2. dynamic_cast,性能差,常见的实现方法是字符串比较,继承链越长比较次数越多
      3. interpret_cast
      4. static_cast一般会生成一个原对象的副本 ,强制隐式转换
    2. 默认转型会造成编译器额外的工作,比如继承关系中同一个对象可能有不同指针指向不同位置表示不同对象
  8. 保证异常安全:不泄漏资源 & 不破坏数据,常常可以使用 copy-and-swap 方式实现异常安全(29)

    1. 不泄漏资源:RAII,例如 lock_guard/智能指针;特别注意new/delete之间的代码
    2. 不破坏数据:比如异常安全的 swap 会先构造一个临时变量而不是直接赋值,避免赋值失败对原始数据的破坏。这里的原则是:all or nothing,修改原始数据前准备好一切,直接 swap 就可以修改原始变量
  9. 减少编译依赖,最好实现头文件中“只有声明式”(31)

    1. C++ 编译依赖的一个原因:编译时编译器需要知道对象的大小以分配内存。Java 等语言不存在此类问题,因为 Java 中所有对象都在堆中,栈中分配一个指针大小的内存空间即可
    2. 手段(依赖声明式而非定义式)
      1. 尽量使用前置声明,会因为 string 是 typedef 所以 string 前置声明比较复杂,不过标准库依赖对编译性能的影响不大
      2. 优先使用指针与引用:类内能使用饮用或者指针,就不要使用对象
      3. Handle class:如果可以,尽量使用声明式头取代定义式头。比如 Person & PersonImpl,前者为声明头(fwd),后者为定义,前者在内部使用后者的指针即可实现很多的依赖去除;或者直接使用接口与继承

面向对象

  1. 面向行为继承。public 继承意味着 is-a,这里的 is 是面向行为(函数)的。企鹅是鸟单不能飞,所以企鹅不应该继承自鸟(32)
  2. 区分接口继承与实现即成,impure virtual fun(34)
  3. 绝不重写继承而来的非虚函数(36)
  4. 不修改继承而来的虚函数默认参数(37)
  5. 虚函数的替代与优化方法(35)
    1. NVI(non-virtual interface),类似装饰模式。使用非虚函数作为接口,在非虚函数中调用虚函数。NVI 的好处是可以在虚函数前后执行一些准备与清理任务而又有虚函数的灵活性
    2. 使用 functor/bind 实现策略模式,比继承实现的策略模式更灵活

模板(略)

new&delete

  1. 可以给 new 提供一个函数,申请内存失败时调用
posted @ 2021-01-10 17:12  jiahu  阅读(222)  评论(0编辑  收藏  举报