1. 首页
    2. C++

    【读书笔记】Effective C++

    条款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:尽可能延后变量定义式的出现时间

    • 直到非得使用该变量时再创建变量,甚至直到给其赋予初值
    • 变量应该在循环内还是循环外的问题,取决于赋值操作与构造、析构的消耗成本
    评分 0, 满分 5 星
    0
    0
    看完收藏一下,下次也能找得到
    • 版权声明:本文基于《知识共享署名-相同方式共享 3.0 中国大陆许可协议》发布,转载请遵循本协议
    • 文章链接:https://icebmji.com/blog/?p=818 [复制] (转载时请注明本文出处及文章链接)
    上一篇:
    :下一篇

    发表评论

    此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

    沙发空缺中,还不快抢~