【读书笔记】Effective C++

2018年12月17日 2357点热度 0人点赞 0条评论

条款01:视C++为一个语言联邦

  • C++是一种多重范型编程语言,同时支持过程形式,面向对象形式,函数形式,泛型形式,元编程形式的编程方式
  • C++主要有四种编程风格
    • C: 以区块、语句、预处理器、内置类型、数组、指针等为主
    • C with class: 围绕构造函数、析构函数、封装、继承、多态、动态绑定等概念展开
    • Template C++: 模板以及模板元编程范型
    • STL: 协调容器、迭代器、算法及函数对象的模板库
  • C++高效编程规则取决于你使用C++哪一部分

条款02:尽量以const,enum,inline替换 #define

  • 宏记号名称可能没有进入符号表,导致编译出错时难以追踪,最好使用常量替换
  • #define无法定义类的专有常量,也不能提供任何封装性,因为#define不重视作用域
  • 当类内的需要常量声明式,编译器又无法识别时,可以使用“enum hack”的方式来实现,即类内枚举,这同样适用于不占用空间
  • “enum hack”是模板元编程的基础技术
  • 使用内联的模板函数来替换宏函数,避免宏带来的麻烦,同时不损失效率
  • 宏在现在并不能完全被替代,#include,#ifdef/#endif仍具有重要作用

条款03:尽可能用const

  • 对于确定的不会改变的值,加上const,告知编译器和其他程序员该值保持不变
  • 指针常量和常量指针也是老生常谈了
  • const成员函数使得类更加容易被理解,同时pass by reference-to-const能够提高程序效率
  • 通常const成员函数意味着该函数不会对成员对象进行修改,但无法保证其返回指针类型的成员函数,然后在外部修改
  • 为避免重复代码,可以使用non-const成员函数调用const成员函数,需要使用两次转型,不能用const成员函数调用non-const成员函数,因为无法保证non-const成员函数修改成员对象内容

条款04:确定对象被使用前已被初始化

  • C++不会保证初始化内置类型,需要手动初始化
  • 构造函数内只会为成员对象赋值,使用“成员初始值列”替换赋值动作
  • 成员对象初始化顺序为其类内声明顺序,于成员初始化列的顺序无关
  • 不同编译单元内的全局变量初始化顺序无法明确,使用函数内部静态变量的方式控制初始化顺序

条款05:了解C++默默编写并调用哪些函数

  • 如果你没写,在程序用到时,编译器会默认替你声明需要的默认构造函数、析构函数、拷贝构造函数、拷贝赋值符
  • 如果编译为你创建的默认函数与函数本身的功能存在冲突时,编译器会为你取消创建对应的函数

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

  • 一般而言可以将不想被调用的函数设为私有成员函数,但无法保证友元和其他成员函数的访问
  • 也可以只声明,这样在编译时就知道了
  • 可以继承类似Boost库中nocopyable型的类
  • 补充:C++11中将函数 "= delete"表示函数已被删除

条款07:为多态基类声明virtual析构函数

  • 基类指针指向子类时,delete基类指针时,若基类析构函数不是virtual的,则无法调到子类的析构函数
  • 虚函数存在虚表中,有虚函数的类中还会多余一个虚表的指针,因此不要随便将类析构函数设为虚函数,会多一个指针的空间
  • 大部分的STL容器,如string,vector都没有虚析构函数,因此继承其时要考虑到析构时的风险
  • 将父类析构函数声明为一个纯虚函数,并给上一份定义,是一份不错的选择
  • 用到多态的类应该声明虚析构函数,用不到多态的类不应该声明虚析构函数

条款08:别让异常逃离析构函数

  • C++没有明确禁止在析构函数中抛出异常,但不要这么做
  • 析构函数中抛出异常会导致不明确的行为,因为同时析构多个类时,比如vector和数组等,会抛出多个异常,导致不明确的行为
  • 可以在发生异常时直接abrot,也可以不处理异常,两者都不是最优解
  • 可以提供一个供用户提前调用的接口函数,将可能发生异常的代码包在里面,让用户在析构前检测异常

条款09:绝不在构造和析构过程中调用virtual函数

  • 在C++继承关系中,在子类构造函数运行前,会首先调用父类构造函数
  • 在构造过程中调用析构函数时,子类创建未完成,此时会造成不明确的行为
  • 析构函数同理,父类析构函数调用在子类析构函数之后
  • 可以将函数写为非虚函数,并将需要在构造函数中的调用的函数所需要的参数,借由构造函数参数传递进去

条款10:令operator=返回一个reference to *this

  • C++中标准类型赋值可写成连锁形式,且赋值为右结合律形式
  • 通常自己定义的类,赋值操作符的返回值也应返回一个自身的引用
  • 这虽然是一个协议,但除特殊原因外应当遵守

条款11:在operator=中处理 “自我赋值”

  • 在使用赋值操作符时要考虑到自我赋值的情况
  • 主要有两种情况,自我赋值安全性和异常安全性
  • 要避免在自我赋值时可能会提前删除本身对象的情况
  • 正确的操作是先拷贝一份赋值数据,再将自身与拷贝到的数据交换,最后自动释放拷贝的对象
  • 也可以采用传值的方式将传值和拷贝数据结合在一起,不过这样思路可能会不清晰

条款12:复制对象时勿忘其每一个成分

  • 如果你想自己写拷贝构造函数和拷贝操作符时,不要忘记拷贝每一个成员,如果你忘记了,编译器并不会报错,甚至没有任何警告
  • 当发生继承时,不要忘记拷贝父类中的对象
  • 不要尝试拷贝构造函数调用拷贝操作符,或拷贝操作符调用拷贝构造函数,若两者代码重复,可以考虑新建一个函数,共同调用

条款13:以对象管理资源

  • 使用std::auto_ptr替代传统的new delete实现对象的自动删除
  • RAII:资源取得时机便是初始化时机
  • 运用析构函数确保资源被释放
  • std::auto_ptr通过拷贝构造函数和拷贝赋值符复制,之前会变为null
  • std::shared_ptr通过引用计数器来实现对象的管理,以及资源的释放
  • std::auto_ptr和std::shared_ptr都不能delete[],不要在动态分配的数组上使用它们,用std::vector或std::string代替

条款14:在资源管理类中小心copying行为

  • 一个RALL(resource acquisition is initialization)类面临复制时的抉择:
  • 禁止复制、深拷贝、转移所有权、使用类似shared_ptr的引用计数规则

条款15:在资源管理类中提供对原始资源的访问

  • APIs往往要求访问原始资源,每一个RAII class应该提供取得其管理资源对象的方法
  • 使用隐式转换来对原始资源访问比较方便,但要考虑到潜在的安全问题

条款16:成对使用new和delete时要采用相同形式

  • new表达式中使用[],必须在相应的delete表达式中使用[],同理new不使用时,也不要在delete中使用
  • 使用标准库中的string和vector

条款17:以独立语句将newd对象置入智能指针

  • 以独立语句将newd对象存入智能指针内,以防编译器重排,抛出异常时导致的内存泄露
  • 例:processWidget(std::trl::shared_ptr<Widget>(new Widget), prioriy());
    • 执行new,然后调用prioriy,创造智能指针
    • prioriy抛出异常时内存泄漏

条款18:让接口容易被正确使用,不易被误用

  • 理想上,用户使用某个接口却没有达到预期,代码不应通过编译
  • 促进正确使用方法有接口一致性,与内置类型兼容
  • 阻止误用办法有建立新类型、限制类型上操作、束缚对象值、消除客户的资源管理责任
  • 首先考虑用户可能出现的错误,其次可以限制类型,如:加const
  • 可以使用智能指针防止new/delete带来的问题,防范跨DLL的new/delete问题

条款19:设计class犹如设计type

  • 新的type的对象应当如何被创建和销毁?构造和析构及内存分配和释放
  • 对象的初始化和对象的赋值该有什么样的差别?构造函数和赋值操作符
  • 新type的对象如果被passed by value,意味着什么?拷贝构造函数
  • 什么是新type的合法值?错误检查和抛异常
  • 新type需要配合某个继承图系?子类受父类的约束
  • 新的type需要什么样的转换?隐式转换和显式转换
  • 什么样的操作符和函数对新type而言是否合理?
  • 什么样的标准函数应当驳回?权限
  • 谁该取用新的type的成员?权限
  • 什么是新type的未声明接口?对效率、安全、资源运用提供保证
  • 新type有多么一般化?模板
  • 你真的需要一个新的type?non-member函数或模板ss

条款20:宁以pass-by-reference-to-const替换pass-by-value

  • 函数调用时,使用值传递自定义对象,会造成没有必要的拷贝
  • 引用传递可以防止对象切割,因为引用也是体现多态的一种方式
  • 内置类型使用值传递效率更高
  • 小型types并不意味着和内置类型,并不一定值传递效率高
  • STL的迭代器和函数对象适合于值传递

条款21:必须返回对象时,别妄想返回reference

  • 该返回对象就返回对象,该返回引用就返回引用,不要矫枉过正
  • 无论是栈区对象,函数堆区对象,亦或是静态对象都不适用于为替换返回对象而返回引用

条款22:将成员变量声明为private

  • 通过成员函数控制成员变量可以轻松的实现读写控制
  • 在不同的形式中转换,选择时间优先还是空间优先
  • 提高封装性,避免在修改类时造成大量代码的修改

条款23:宁以non-member、non-friend替换member函数

  • member函数比non-member提供更好的封装性
  • 对象内的数据,越多的函数可访问它,那数据的封装性越低
  • 使用namespace将系列便利函数封装在一起,其可以跨越多个文件,而类却不能
  • 使用namespace使系列函数形成依赖,又很方便扩展

条款24:若所有参数皆需类型转换,请为此采用non-member函数

  • C++类成员函数形式的运算符重载只能出现在运算符左侧
  • 当需要混合运算时,使用non-member形式的重载,可避免

条款25:考虑写出一个不抛异常的swap函数

  • 类支持拷贝构造函数和拷贝操作符就能使用swap函数
  • 通过特化swap来实现对具体类的交换操作
  • 若函数内部成员无法访问,尝试使用友元
  • 为具体类写一个公有swap成员函数来实现具体的操作,使用特化版调用该成员函数
  • C++不允许偏特化模板函数
  • 重载swap也是个方法,但C++标准规定:可以全特化std空间内的模板,但不可以添加新的东西,比如重载
  • 通常类模板的swap函数,可以将其写在自己的命名空间内,再写一个公有成员函数供调用。使用时,声明using namespace std,即可先找类作用域内的swap,再找全局,最后找std内的swap函数
  • 非类模板的swap可以写成全特化的形式,当然也可以象上面那样

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

  • 直到非得使用该变量时再创建变量,甚至直到给其赋予初值
  • 变量应该在循环内还是循环外的问题,取决于赋值操作与构造、析构的消耗成本

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

  • const_cast通常被用来将对象的常量性转除
  • dynamic_cast用来执行向下转型,会有一定的性能消耗,毕竟是运行期转型
  • reinterpret_cast执行低级转型,比如int*转为int较少用
  • static_cast是强迫隐式转换
  • 尽量使用新式的类型转换,容易分辨和统计

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

  • 成员变量的封装性最多只等于“返回其reference”的函数的访问级别
  • 即使const成员函数传出的reference,若数据与自身有关联,又被存储到对象外,函数的调用者有可能修改这个数据
  • 指针、reference、迭代器都是所谓的handles
  • 可以通过返回const修饰的reference来避免数据被修改

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

  • 当异常被抛出时,异常函数会:不泄露任何资源。不允许数据破坏
  • 异常安全函数保证:异常被抛出时,程序内任何事物保持有效状态。如果异常被抛出,程序状态不改变。承诺不抛出异常
  • copy & swap
  • 异常安全保证取决于各函数异常安全保证最弱者

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

  • inline函数通常位于头文件中,编译期完成
  • inline是个申请,编译器可以选择是否忽略
  • virtual和inline是冲突的,函数指针进行调用的函数通常不被inline,构造和析构函数不能是inline
  • inline无法随函数库升级而升级,调试模式不应支持inline
  • inline应限制在小型,频繁调用的函数上,可减少代码膨胀,提升程序速度

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

  • pimpl: pointer to implementation 类成员内只含一个指针成员指向其实现类,将对象的实现细节隐藏于指针背后
  • 如果使用引用或指针能完成任务就不要用类型。如果能够,尽量用class声明替代定义式。为声明和定义提供不同的头文件。
  • 使用pimpl的类往往被称为Handle Classes,一般将其所有函数交给其实现类
  • 另一个制作Handle class的办法是让其成为抽象基类,通常被称为Interface class
  • 相依于声明式,不要相依于定义式
  • 库文件头应以,完全且仅有声明式的形式存在,无论是否涉及templates

条款32:确定你的public继承塑模出is-a关系

  • public继承意味着是is-a关系,每一个基类所适用的地方,派生类都适用

条款33:避免遮掩继承而来的名称

  • 遮掩产生的源头是“作用域”
  • 派生类内的名称会遮掩基类内的名称
  • 可使用using声明式或转交函数(private继承)来让被遮掩的名称再现

条款34:区分接口继承和实现继承

  • 接口继承和实现继承不同,public继承下,派生类总是继承基类接口
  • 纯虚函数只 指定接口继承,纯虚函数可实现,并由类名强制调用
  • 普通的虚函数有 指定接口继承 和 缺省实现继承
  • 非虚函数有 指定接口继承 和 强制实现继承

条款35:考虑virtual函数以外的其他选择

  • 使用non-virtual interface(NVI)手法,是Template Method设计模式的一种,保留成员函数为non-virtual,并调用一个private virtual函数进行实际工作,优点是可以控制调用虚函数的前后文时机,虚函数只负责事情如何完成
  • 将虚函数替换为函数指针成员变量,是Strategy设计模式的一种表现形式,优点是可以变更函数的计算方式,甚至运行期都可以,非常有弹性,缺点是如果需要类内的private变量计算,则需要弱化封装结构,如将函数指针声明为friends
  • std::function成员变量替换虚函数可允许任何可调用物搭配一个兼容于需求的签名式
  • 将继承体系内的virtual函数替换为另一个体系内的virtual函数,这是Strategy设计模式的传统实现手法

条款36:绝不重新定义继承而来的 non-virtual函数

  • non-virtual函数是静态绑定的
  • 父类所适用的地方,子类也应当适用,避免隐藏父类的函数

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

  • 虚函数是动态绑定,但缺省参数值是静态绑定的
  • 若基类虚函数有缺省参数值,可通过NVI手法避免

条款38:通过复合塑模出has-a或“根据某物实现出”

  • 复合是类型间的关系,某种类型的对象内含它种类型对象
  • 应用域,程序对象其实相当于你所塑造的世界中的某些事物,复合意味着has-a
  • 实现域,对象纯粹是实现细节上的人工制品,复合意味着is-implemenyed-in-terms-of

条款39:明智而审慎地使用private继承

  • private继承的类间关系不是is-a,派生类所有继承来的成员全部为private
  • private继承意味着(implemented-in-terms-of)根据某物实现出,通常比复合级别低
  • 当派生类需要访问基类的protected成员时,或者重新定义继承而来的virtual函数时,private继承是合理的
  • 相比复合,private继承可以造成empty base最优化,对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要的

条款40:明智而审慎地使用多重继承

  • 多重继承容易出现歧义,比如两个基类中有相同的函数名称
  • 菱形继承的两种方案:复制每一条路线的数据(默认)。采用virtual继承
  • 虚继承会带来额外的开销,非必要不使用,避免放置数据
  • 多重继承有自己的应用场景,比如public继承interface,private继承协助实现的类

条款41:了解隐式接口和编译期多态

  • C++ template机制是一部完整的图灵机
  • 面向对象编程世界,总是以显式接口和运行期多态来解决问题,templates和泛型编程中显示接口和运行期多态重要性被降低,隐式接口和编译期多态被提前
  • 显式接口通常由函数签名决定,隐式接口则是由表达式组成。多态通过template具现化和函数重载解析

条款42:了解typename的双重意义

  • template声明式中的typename和class完全一样
  • template内出现的名称依存于某个template参数则为从属名称,如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称,如果是类型还可称为嵌套从属类型名称
  • 一般想要在template中指涉一个嵌套从属类型名称时,要在紧邻的位置前放置一个typename字段,表明其是一个类型名
  • 例外:base classes list(基类继承列表)内和成员初始值列表中的嵌套从属类型名称前不能加typename字段

条款43:学习处理模板化基类的名称

  • C++编译器知道base class templaes 可能被特化,所以会拒绝在模板化基类中寻找继承而来的名称
  • 办法1:在base class调用函数动作前加上“this->”。办法2:使用using声明式,告诉编译器base class中的名称。办法3:使用父类类型名调用,这种方法对虚函数不适用

条款44:将与参数无关的代码抽离templates

  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
  • 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数
  • 因类型参数而造成的代码膨胀,往往可降低,做法是带有完全相同二进制表述的具现类型共享代码

条款45:运用成员函数模板接受所有兼容类型

  • 使用泛化的成员函数模板,生成可接受所有兼容类型的函数
  • 声明泛化的拷贝构造函数或泛化赋值操作符时,还需要声明正常的函数,以防止泛化函数处理模板本身

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

  • template实参推导过程中从不将隐式类型转换函数纳入考虑
  • 当需要支持时可将那些函数定义为友元函数

条款47:请使用traits classes表现类型信息

  • traits classes 使得“类型相关信息”在编译期可用,使用templates和templates特化完成实现
  • 整合重载技术后,traits class 有可能在编译期对类型执行if...else测试

条款48:认识template元编程

  • TMP 模板元编程可将工作由运行期移往编译期,可更早地实现早期错误侦测和更高地执行效率
  • TMP可确保度量正确,在早期侦测错误,优化矩阵运算,生成客户定制地设计模式,比如智能指针

条款49:了解new-handler的行为

  • 当operator new抛出异常之前会调用客户指定的错误处理函数
  • operator new无法满足内存申请时,会不断调用new-handler函数,直到找到足够内存
  • 一个良好的new-handler函数必须做以下事情
    • 更多的内存可被使用
    • 安装另一个new-handler
    • 卸载new-handler
    • 抛出bad_alloc
    • 不返回:通常abort或者exit
  • 由于历史原因如今的C++ operator new在内存分配失败时不再返回null而是抛出bad_alloc的异常,可以使用std::nothrow来阻止operator new抛出异常,并返回null,但无法阻止对象的构造函数抛出异常,因此如今Nothrow new如今的局限性非常大

条款50:了解new和delete的合理替换时机

  • 人们想要替换编译器提供的默认new或delete操作符,往往基于以下的理由
    • 用来检测运用上的错误。当delete失败,导致内存泄漏时,可以通过自定义的new operator超额分配内存,并在额外区间定义一些标识值,这样,可以通过检测这些标识位来判断指针的有效性
    • 为了强化效能。编译器自带的new和delete会处理不同粒度的内存申请,因此其对特定的内存申请并不能做到最佳,因为还要照顾其他粒度的内存申请,而自定义的new和delete可以只针对特定的场景做优化
    • 为了收集使用上的统计数据。自定义的new和delete使得我们可以自己掌握诸如分配区块大小,寿命分布,次序等行为数据
    • 增加分配和归还速度。例如你所开发的程序是个单线程程序,就可以不用考虑线程安全,大大提高速度
    • 为了降低缺省内存管理器带来的空间额外消耗。泛用型内存管理器常常在每个分配区块上有额外开销。
    • 弥补缺省分配器中的非最佳齐位。例如在x86架构下,doubles如果是8-byte齐位时是最快的,然而默认的内存管理器并不能保证此项
    • 为了将相关对象成簇集中。特定数据结构经常一起访问时,避免缺页中断
    • 为了获得非传统的行为。比如为C API穿上一件C++外套,在delete归还内存时,将其内存覆写为0

条款51:编写new和delete时需固守常规

  • operator new 必须返回正确的值;内存不足时需要调用new-handling函数;对零内存需求有所准备;避免掩盖正常形式的operator new
  • 注意针对基类所设计的operator new会被子类所继承,如果子类没有重写父类的operator new,则会调用父类的operator new,如果父类的new只针对自身做了一下优化和处理,这将导致子类出现一些问题
  • 类的自定义new不需要考虑size为0的情况,因为即使是没有成员变量的类,在申请内存时size也是1
  • operator delete第一件保证的事就是删除空指针时必须是安全的

条款52:写了placement new也要写placement delete

  • 当人们提起placement new时通常是指唯一额外实参是void*的自定义operator new,少数时候才是任意额外实参
  • 只有在类的构造函数中抛异常时,才会引发程序调用placement new 对应的delete,而正常的delete对象只会调用默认的operator delete
  • 类的专属operator new和delete会遮掩默认的new,因为成员函数名会遮掩外围作用域的相同名称,因此确保避免遮掩缺省的new,需要实现所有版本的operator new delete函数。缺省的global如下,不要忘记了对应的delete
    • void* operator new(std::size_t) throw(std::bad_alloc);
    • void* operator new(std::size_t, void*) throw();
    • void* operator new(std::size_t, const std::nothrow_t&) throw();

条款53:不要轻视编译器的警告

  • 严肃对待编译器的警告,努力在编译器最高警告级别下争取无任何警告。至少你要知道编译器给出警告的意思,并判断是否需要修改
  • 不同的编译器警告能力不同,因此不要依赖编译器警告

条款54:让自己熟悉包括TR1在内的标准程序

  • 作者成书时间很早,tr1中许多特性已经并入到了C++标准库中,或者删除了
  • C98标准库成分
    • STL(标准模板库):容器,迭代器,算法,函数对象,容器适配器和函数适配器
    • Iostreams:cin,cout,cerr,clog
    • 国际化支持:诸如wchar_t和wstring
    • 数值处理,包括复数模板和纯数值数组
    • 异常阶层体系
    • C89标准库
  • TR1新组件14个
    • 智能指针:shader_ptr和weak_ptr;function表示任何函数或函数对象;bind;hash tables;正则表达式;变量组;array;mem_fn;reference_wrapper;随机数;数学特殊函数;C99兼容扩充;type traits(类型萃取);result_of

条款55:让自己熟悉Boost

  • Boost委员会和C++标准委员会之间关系很深,Tr1中就有三分之二奠基于Boost的成果
  • Boost是一个社群,也是一个网站。致力于免费、开源、同僚复审的C++程序库开发。
  • Boost程序的主题非常繁多,比如
    • 字符串与文本处理:类型安全的prinf-like格式化动作,正则和语汇单元切割和解析
    • 容器:接口和stl相似且大小固定的数组、大小可变的bitsets和多维数组
    • 函数对象和高级编程:Lambda表达式
    • 泛型编程:覆盖一大组的traits classes
    • 模板元编程
    • 数学和数值:有理数、八元数和四元数、常见公约数等
    • 正确性和测试:将隐式模板接口形式化的程序库
    • 数据结构:类型安全的unions和tuple程序库
    • 语言间支持:C++和python互操作
    • 内存:Pool程序库,高效率而区块大小固定的分配器
    • 杂项:CRC校验、日期时间处理、文件

icebmji

这个人很懒,什么都没留下