Effective C++

Effective C++

改善程序与设计的55个具体做法

让自己习惯C++

视C++为一个语言联邦

  • C++是多重范式编程语言:
    • 过程式编程;
    • 面向对象编程;
    • 函数式编程;
    • 泛型编程;
    • 元编程(metaprogramming). --- 利用模板实现.
  • STL是一个template程序库:
    • 容器(containers).
    • 迭代器(iterators).
    • 算法(algorithms).
    • 函数对象(function objects).

尽量以const, enum, inline替换#define

  • 宏的预处理方式不太安全.

尽可能地使用const

  • const指定一个语义约束, 并通过编译器进行强制实施.

确定对象被使用前已被初始化

  • 读取未初始化的值会导致不明确的行为.
  • 初始化列表应该与变量声明次序保持一致.
  • 所谓static对象, 其寿命从被构造出来直到程序结束为止.
  • 为了避免'跨编译单元的初始化序列', 应该以局部static对象替换掉非局部的static对象.

构造, 析构, 赋值运算

了解C++默默编写并调用那些函数

  • copy构造函数;
  • copy 赋值('=')操作符;
  • 默认的析构函数;
    • 编译器产出的虚构函数是non-virtual.
  • 默认的构造函数, 这些函数都被设置为inline和public.
  • 如果某个基类将copy assignment操作符声明为private, 编译器将不会为派生类生成一个赋值操作符.

若不想使用编译器自动生成的函数, 就应该明确拒绝

  • 将复制构造函数和copy assignment操作符声明为private. 而且故意不实现它.
  • 为多态基类声明virtual析构函数, 设计工厂模式的实现经常用到.
  • virtual函数的目的是让派生类的实现得以客制化.
  • 任何类(class)只要有virtual函数都应该有一个virtual的析构函数, 确保多态的正常运行.
    • 主要用来确定运行期该执行那个virtual函数.
    • 当对象调用某个virtual函数时, 实际上被调用的函数取决于该对象的vptr(徐标指针)所指向的vptl(虚函数列表);
      • 编译器会在vptl(虚函数列表)中寻找适当的函数指针.

不要让异常逃离析构函数

  • 析构函数应该捕获任何异常, 并不再让其进行传播.
  • class应该提供处理异常的普通函数.

绝不在构造和析构函数中调用virtual函数

  • 在派生类的基类构造期间, 对象的类型是基类而不是派生类.
  • 这类函数调用并不能调用派生类的虚函数的多态.

令 operator=返回一个指向this指针的引用

  • 赋值操作符必须返回一个reference指向操作符的左侧实参.

在 operator=中处理自我赋值

  • copy and swap技术. 值传递会造成复制副本.

复制对象时, 不要忘记其每一个成分

  • 进行深拷贝.
  • copying所有的局部成员变量;
  • 调用适当的基类中的copy函数.
  • 不应该在copy assignment操作符中调用copy构造函数.

资源管理

  • C++ 程序中最常见的资源是动态分配的内存.
    • 文件描述符(file descriptors).
    • 互斥锁(mutex locks).
    • 数据库连接.
    • 网络socket.

以对象管理资源

  • 获得资源后了立即放进管理对象内(managing object).
  • 资源取得时机便是初始化时机(Resource Acquisition Is Initialization, RAII).
  • 管理对象(managing object) 运用析构函数确保资源被释放.

在资源管理中心小小copying行为

  • 禁止复制, 许多时候允许RAII对象被复制是不合理的.
  • 对底层资源使用引用计数法(reference count).
  • 复制底部资源使用深度拷贝.
  • 转移底部资源的拥有权(shared_ptr, weak_ptr, unique_ptr).

在资源内部中提供对原始资源的访问

  • shared_ptr中有一个get的成员函数, 用来执行显示转换, 返回智能指针内部的原始指针.
  • shared_ptr将它所有的引用计数机构都封装起来了.

成对使用new和delete时要采取相同形式

  • new不仅要分配动态内存, 而且还要调用一个(或多个)构造函数.
  • 所以delete中也要求要执行对应的析构函数.
  • 尽量最好不要对数组形式做typedefs动作.

以独立语句将newed对象置入智能指针

  • 如果不这么做, 一旦异常被抛出, 有可能导致难以察觉的资源泄漏.

设计与声明

让接口容易被正确使用, 不易被误用

设计class犹如设计type

宁愿以pass-by-reference-to-const替换pass-by-value(传引用的方式替换值传递)

必须要返回对象时, 不要返回其reference

  • 这可能导致coredump, 因为内部对象离开作用域就会自动析构掉了.

将成员变量声明为private

宁以非成员, 非友函数替换掉成员函数

若所有参数皆需要类型转换, 应该采用非成员函数

考虑写一个不抛异常的swap函数

  • 调用swap时应针对std::swap使用using声明式, 然后调用swap并且不带任何命名空间资格修饰.

实现

  • 太快定义变量可能造成效率上的拖延;
  • 过度使用转型(casts)可能导致代码变慢又难维护, 还会招来微妙难解的错误;
  • 返回对象"内部数据的句柄(handles)";
  • 未考虑异常带来的冲击则可能导致资源泄漏和数据败坏;
  • 过度热心地inlining可能引起代码膨胀;
  • 过度耦合(coupling)则可能导致让人不满意的冗长编译时间(buid times).

尽可能延后变量定义式的出现时间

尽量少做类型转换动作

  • C++四种新式转换:
    • const_cast(expression);
    • dynamic_cast(expression);
    • reinterpret_cast(expression);
    • static_cast(expression);

避免返回handles()指向对象内部成分

为异常安全而努力是值得的

  • 不泄漏任何资源, 不允许数据败坏, 基本承诺, 强烈保证.

彻底了解inlining的里里外外

  • 使用inline会增加目标码的大小 ---> 进而导致换页行为 ---> 降低高速缓存的命中率 ---> 效率受损.
  • Template的具现化与inlining无关.
  • 80-20经验法则, 80%的执行时间花费在20%的代码上.
  • 不要因为函数模板(function template)出现在头文件, 就将他们声明为inline.

将文件间的编译依存关系降至最低

  • 如果使用对象引用或对象指针可以完成的任务, 就不要使用对象(objects)了.
  • 如果能够, 尽量以class声明式替换class定义式.
  • 为声明式和定义式提供不同的头文件.

继承与面向对象设计

  • 每个继承链接(link)可以是public, protected, 或private.

确定public继承是is-a关系.

  • 世界上不存在一个适用于所有软件的完美设计.
  • 好的接口可以防止无效的代码通过编译.
  • public 继承主张, 能够实施于基类身上的每件事情, 也可以施行于派生类对象身上.
  • 类之间(classes)的关系还有:
    • has-a(有一个)的关系;
    • is-implemented-in-terms-of(根据某物实现出).

避免遮掩继承而来的名称

  • 内层作用域的名称会遮掩外层作用域的名称.
  • 派生类继承了声明于基类中的所有东西. --- 派生类的作用域被嵌套在基类的作用域内.
  • 派生类仅希望重新定义或覆写基类中的一部分, 必须为原本会被遮掩的每个名称引入一个using声明式, 否则某些希望继承的名称会被遮掩.
  • private继承中, 可能需要转交函数(forwarding function).

区分接口继承和实现继承

  • 函数接口继承(function interfaces); --- 纯虚函数进行实现的.
  • 函数实现继承(function implementations);
  • 纯虚函数(pure virtual)函数的目的是为了让派生类只继承函数接口.
  • pure virtual有两个突出的特点:
    • 它们必须被继承了的具象类重新声明;
    • 而且它们在抽象类中没有具体的实现.
  • 声明一个简朴的非纯虚函数, 是让派生类继承该函数的接口和缺省实现.
  • 声明非虚函数的目的是为了令派生类继承函数的接口及一份强制性实现.

考虑virtual函数以外的其他选择

  • 由非虚接口手法实现Template模式;
  • 由Function Pointers实现Strategy模式.
  • 用函数对象实现Strategy模式.

绝不重新定义继承而来的非虚函数

  • 非虚函数是静态绑定的, 而virtual函数却是动态绑定的, 只用在运行的时候才知道该调用那个函数.(通过指针类型进行推断).
  • 多态性(polymorphic)基类内的析构函数也应该是virtual(虚)的.

绝不重新定义继承而来的缺省参数值

  • virtual函数系动态绑定(dynamically bound), 而缺省参数值却是静态绑定(statically bound).
  • 静态绑定也叫前期绑定(early binding), 而动态绑定又叫后期绑定(late binding).
  • 函数调用的缺省参数值指调用前已将进行赋值(draw(ShapeColor color = Red)😉.

通过聚合/复合实现has-a关系或者'根据某物实现出'

  • 复合意味has-a(有一个)或is-implemented-in-terms-of(根据某物实现出).
  • is-implemented-in-terms-of(根据某物实现出)表现更多的是复用一个类.
  • 复用(composition)与public继承完全不同的意义.
  • 在应用域(application domain), 复合意味has-a(有一个).
  • 在实现域(implementation domain), 复合意味is-implemented-in-terms-of(根据某物实现出).

明智而审慎地使用private继承

  • 如果两个类之间的继承关系是private, 编译器不会自动将一个派生类对象转换为一个基类对象.
  • 由private基类继承而来的所有成员, 在派生类中都会变成private属性, 即使他们在基类中原本是public属性和protected属性.
  • private继承意味着只有实现部分被继承, 接口部分应省略掉.

明智而审慎地使用多重继承

  • 单一继承是好的, 但多重继承不值得拥有(或使用).
  • 多重继承的意思是继承一个以上的基类, 但这些基类并不常在继承体系中又有更高级的基类, 可能会导致钻石型多重继承.

模板与泛型编程

  • C++ template机制完全是一部完整的图灵机(turing-complete): 可以被用来计算任何可计算的值.

了解隐式接口和编译期多态

  • 面向对象编程以显示接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题.
  • 对类而言, 接口是显式的(explicit), 以函数签名为中心; 多态通过virtual(虚)函数发生于运行期.
  • 对template参数而言, 接口是隐式的(implicit), 基于有效表达式; 多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期.

了解typename的双重意义

  • typename暗示参数并非一定得是个class类型.
  • template内出现的名称如果相依于某个template参数, 称之为从属名称(dependent names).
  • 任何时候当想要在template中涉及一个嵌套从属类型名称, 就必须在前一个位置放上关键字typename.
  • 但不能在基类列表或成员初值列表内作为基类的修饰符.

学习处理模板化基类内的名称

  • 对编译器承若基类模板的任何特化版本都将支持其一般(泛化)版本所提供的接口.
  • 可在派生类模板内通过this指针, 指涉基类模板内的成员名称.或写基类资格修饰符完成.

将与参数无关的代码抽离templates

  • 任何template代码都不应该与狗哥造成膨胀的template参数相依关系.

运行成员函数模板接受所有兼容类型

  • 使用成员函数模板(member function templates)生成可接受所有兼容类型的函数.
  • 声明member templates用于泛化copy构造或泛化assignment操作, 不如声明正常的构造函数和copy assignment操作符.

需要类型转换时请为模板定义非成员函数

使用trait classes表现类型信息

认识template元编程

定制new和delete

  • heap是一个可被改动的全局资源.

了解new-handler的行为

  • 让更多内存可被使用.
  • 安装另一个new-handler.
  • 卸除new-handler.
  • 抛出bad_alloc(或派生自bad_alloc)的异常.
  • 不返回(通常调用abort或exit).

了解new和delete的合理替换时机

  • 用来检测运用上的错误.
  • 为了强化效果.
  • 为了手机使用上的统计数据.

编写new和delete是需要固守常规

写了placement new也要写placement delete

杂项讨论

不要轻视编译器的警告

熟悉包括TR1在内的标准程序库

熟悉Boost

posted @ 2019-04-14 14:36  coding-for-self  阅读(780)  评论(0编辑  收藏  举报