Effective C++ 知识点

1.让自己习惯C++

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

  1. C:C++的基础,包含内置数据类型、语句、数组、指针等,没有模板、异常、重载;
  2. Object-Oriented C++:面向对象提醒,包含封装、继承、多态、virtual函数等;
  3. Template C++:泛型编程,模板元编程;
  4. STL:一个template模板库,包含容器、迭代器、算法、函数对象等

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

  • 让编译器替换预处理器,将相关符号编译到符号表中;
  • 对于单纯常量,以const对象或enums替换#define;
  • 对于形式函数的宏,改用inline函数替换#define

03:尽可能使用const

  • const与指针:如果const出现在星号()左边,表示被指数据是常量,不能修改数据;如果const出现在星号()右边,表示指针自身是常量,不能修改为指向其他变量。
  • 在类的成员函数中,为了避免代码冗余,可通过const成员函数实现出non-const成员函数,即完整实现const成员函数,且在non-const成员函数中直接调用const成员函数(将non-const对象强制转换位const对象后再调用const函数,返回值再强制去除const属性)。

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

  • C++规定,对象的成员变量的初始化动作,发生在进入构造函数之前。
  • 为内置类型对象进行手工初始化,因为C++不保证初始化它们(全局初始化为0,局部不初始化)。
  • 构造函数使用成员列表初始化,而不是在构造函数中赋值。列表初始化成员顺序与声明顺序一致。
  • 为避免“跨编译单元之初始化次序”问题,以local static对象替换non-local static对象。

2.构造/析构/赋值运算

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

  • 默认构造函数
  • 析构函数
  • 复制构造函数
  • 移动构造函数
  • 赋值操作符

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

对于编译器自动合成的函数(构造、析构、复制构造、移动构造、赋值操作符),明确拒绝的方式是使用delete明确删除该函数。

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

  • 如果一个类作为基类将被其它类继承,或该类存在virtual函数,则其析构函数需要定义为virtual函数。
  • 如果一个类设计时明确不应该被继承,则应该使用关键字final进行限制。

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

  • 析构函数抛出异常,将会导致过早结束程序或发生不明确行为。如果析构函数中调用的函数可能抛出异常,析构函数应该捕获异常,然后处理他们或结束程序。
  • 如果调用方需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非析构函数中)执行该操作(比如关闭数据库连接)。

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

先构造基类,再构造子类。先析构子类,再析构基类。所以在构造和析构函数中调用虚函数,无法调用到子类的虚函数,而是调用到自身的函数。

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

通过返回reference to this,可以实现链式赋值,例如x=y=z=1。该方式同样适用于+=、-=、=。

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

确保任何函数如果操作一个以上的对象,而其中多个对象可能是同一个对象时,其行为仍然正确。

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

在复制构造函数和赋值操作符函数中,应该先调用基类对应的复制构造函数和赋值操作符函数,再为本身的每个成员初始化和赋值。

3.资源管理

13:以对象管理资源

  • 构造一个类来管理资源,再故障函数中保存获得的资源,在析构函数中释放该资源。
  • 使用智能指针保存获得的资源,由智能指针自动释放资源。

14:在资源管理类中小心复制行为

复制行为包括复制构造函数和赋值操作符函数。在资源管理类中应该明确禁止复制,或通过引用计算等方式严格控制复制流程。

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

在资源管理类中提供接口,让调用者能够取得原始资源的进行使用。

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

使用new与delete时,[]符号应该成对出现。

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

以独立语句将新对象置入智能指针。否则,中途发生异常将导致资源泄露。

4.设计与声明

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

  • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
  • “阻止误用”的办法包括参数类型限制,参数值限制,以及消除客户的资源管理责任。
  • 通过智能指针管理资源,避免要求客户释放资源。

19:设计class犹如设计type

设计class,应该考虑一下因素:

  • 新type的对象应该如何被创建和销毁?(构造函数与析构函数)
  • 对象的初始化和对象的赋值该有什么样的差别?(构造函数与赋值操作符函数)
  • 新type的对象如果被passed by value,意味着什么?(将调用复制构造函数)
  • 什么是新type的“合法值”?(构造函数参数,赋值操作要求)
  • 新type需要配合某个继承类吗?(遵守继承类的相关规则)
  • 新type需要实现类型转换吗?(与其他类型相互转换)
  • 新type需要支持哪些操作符?(比如*、+、[])
  • 新type的哪些标准接口应该被禁用?(比如复制构造函数)
  • 新type的接口权限控制?(public、protected、private)
  • 新type的未声明接口?(默认的复制构造函数对资源的管理)
  • 新type有多么一般化?(是否应该定义新的class template)
  • 新type有必要存在吗?(是否在已有类中扩展函数等方式就能完成替代)

20:函数参数中尽量以pass by reference to const 替换pass by value

  • 尽量以pass by reference to const 替换pass by value,前者更加高效(避免调用构造函数与析构函数),且可避免切割问题(子类被切割为基类)
  • 以上规则不适用于内置类型与STL的迭代器和函数对象。

21:函数必须返回对象时,禁止返回对象的reference(引用)

绝不要返回一个指针或引用指向一个函数内的局部对象,因为函数结束时该对象已经析构。

22:将成员变量声明为private

对于成员变量,尽量声明为private,除非是基类成员而明确需要在子类中进行修改,则为protected。

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

该方式可以减少类的成员函数数据,即减少能够访问到类内部数据的函数数据,则提高了类的封装性。该方式同时增加了类的机能扩充性(例如扩展更多的方面函数,包装类的接口)。

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

如果你需要为某个函数的所有参数(包括被this指针所指的哪个隐喻参数)进行类型转换,那么这个 函数必须是个non-member(比如为类实现的两个对象的+、-、*操作符)。

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

如果swap的缺省实现对你的class或class template提供可接受的效率,则无需做任何事情。否则应该:

  1. 提供一个public swap成员函数,让它高效且正确的置换你的类型的两个对象值。在你的成员函数swap中,使用了using声明,以便让std::swap在你的swap函数内可见。该函数不允许抛出异常。
  2. 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
  3. 如果编写的是一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数。

5.实现

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

不止应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后定义直到能够给它初始值实参为止。这样可避免无意义的构造、析构,以及默认构造函数的调用。

27:尽量少做转型动作

  • const_cast:用来将对象的常量属性去除。它也是唯一有此能力的C++ style转型操作符。
  • static_cast:静态转换用来进行强迫隐式转换,在编译期间完成。可将int转为double,或将non-const转为const(但无法实现const到non-const的转换),或类指针的向上转换(子类指针转换为基类指针)。
  • dynamic_cast:用来执行“安全向下转型”(基类指针转换为子类指针,失败返回0。子类指针转为为基类指针可以用static_cast,或直接赋值由编译器完成隐式转换),只能用于含有虚函数的类。它是唯一无法由C风格转换所实现的动作,也是唯一可能耗费重大运行成本的转型动作。
  • reinterpret_cast:用来执行低级转型,实际动作(及结果)可能取决于编译器,这也表示它不可移植。要求编译器按照指定类型解析数据,比如将int类型数据解析为一个指针地址。

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

避免返回handles(包括引用、指针、迭代器)指向对象内部,可提高类的封装性,增强安全性。

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

异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏。分为三种保证:基本型、强烈型、不抛异常型。

  • 基本型:如果抛出异常,程序内的任何事物仍然保持在有效状态下。
  • 强烈型:如果抛出异常,程序状态不改变,即处于调用该函数之前的状态。否则是函数执行成功。该方式往往通过copy-and-swap方式实现。
  • 不抛异常型:保证完成函数的功能,绝不抛出异常(例如作用于内置类型的操作都提供不抛异常型的安全保证)。

30:透彻了解inline

将inline限制在小型、被频繁调用的函数身上。

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

  • 通过前置声明的方式,代替头文件包含。
  • 通过PIMPL的方式,隐藏类的数据和实现细节。

6.继承与面向对象设计

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

子类是父类的一种(比如学生与人的关系),任何使用父类的地方,均可以用子类进行替换。反之不成立。

33:避免遮掩继承而言的名称

子类内的名称会遮掩父类内的同名名称,如果继承的基类中存在重载函数,而子类只希望重新定义或覆写其中一部分,则应该使用using声明那些不希望被遮掩的名称。

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

  • 声明一个纯虚函数的目的是为了让子类只继承函数接口。
  • 声明一个普通虚函数的目的是为了让子类继承函数接口和默认实现。
  • 声明非虚函数的目的是为了让子类继承函数接口和强制实现。

35:考虑虚函数以外的其他选择

  • 虚函数的替代方案包括NVI(non-virutal interface)手法以及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将虚函数从成员函数移到类外,带来的一个缺点时,非成员函数无法访问类的内部数据。
  • 通过function可调用对象,来替换虚函数的方案,可以为相同类型的不同对象指定不同的处理函数。

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

非虚函数属于静态绑定关系,重新定义了非虚函数将导致基类的同名函数被遮掩。在通过指针或引用调用该函数接口时,将根据指针或引用本身的类型(编译时确定)来决定调用的实际接口。

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

虚函数是动态绑定的,而缺省参数值是静态绑定的。如果虚函数存在缺省参数,强烈建议修改为非虚函数来设置缺省参数,并重新添加无缺省参数的虚函数,由修改后的非虚函数进行调用。

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

  • 应用域:has-a,在类中包含某个对象,使用其基本接口。
  • 实现域:is-implemented-in-terms-of(根据某物实现出),在类中包含某个对象,用其完成本类的主要功能实现(例如通过list实现set)。

39:明智而谨慎地使用private继承

  • 私有继承时,编译器在函数调用中不会自动将子类转换为父类,可能导致调用失败。
  • 私有继承意味着is-implemented-in-terms of(根据某物实现出)。它通常比复合的级别低。但是当子类需要访问父类的保护成员时,或需要重新定义继承而来的虚函数时,私有继承是合理的。
  • 私有继承可以造成空类的最优化,这对致力于“对象尺寸最小化”的程序库开发者而言,是很重要的。

40:明智而谨慎地使用多重继承

  • 多重继承比单一继承复杂。
  • 虚继承会增加大小、速度、初始化、等成本。如果虚基类不带任何数据,将是最具实用价值的情况。
  • 多重继续的确又正当用途。其中一种情况是“public继承某个Interface class”和“private继承某个协助实现的class”的组合场景。

7.模板与泛型编程

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

  • class和template都支持接口和多态。
  • 对class而言,接口是显示的,以函数签名为中心。多态是通过虚函数实现,发生于运行期。
  • 对template而言,接口是隐式的,基于有效表达式。多态是通过模板具现化和函数重载解析,发生于编译期。

42:了解typename的双重意义

  • 声明template参数时,前缀关键字class和typename可互换。
  • 在模板内标识嵌套从属类型名称(类型与模板参数有关)时,只能使用typename关键字。但不能出现在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。

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

在类模板的继承中,编译器不会在子类中主动查找基类的函数(因为模板基类可能被特例化,导致某些接口不存在)。为了让编译器在子类中能够识别基类的函数(包括虚函数),可以:

  • 在子类中调用基类的函数之前加上“this->”。
  • 在子类中通过using关键字明确告诉编译器使用基类的某个函数。
  • 在子类中调用基类的函数之前加上基类的名称前缀,即指定该函数来源于基类(将导致虚函数调用无效)。

44:将与参数无关的代码抽离template

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

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

  • 请使用member function template(成员函数模板)生成“可接受所有兼容类型”的函数。
  • 如果声明member template用于“泛化copy构造函数”或“泛化赋值操作符函数”,还需要声明正常的复制构造函数和赋值操作符函数。

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

当编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,将那些函数定义为“class template 内部的friend函数”。

47:请使用traits class表现类型信息

traits并不是C++关键字或一个预先定义的构件,它是一种技术,也是一个C++程序员共同遵守的协议。其要求之一是,它对内置类型和用户自定义类型的表现必须一样好。
习惯上,traits总是被实现为structs,但他们却又往往被称为traits class(特征类)。
设计并实现traits class步骤:
1.确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将来可取得其分类。
2.为该信息选择一个名称。
3.提供一个template和一组特化版本,内含你希望支持的类型相关信息。
使用traits class步骤:
1.建立一组重载函数(身份像劳工)或函数模板,彼此间的差异只在于各自的traits参数。另每个函数实现码与其接受之traits信息相应和。
2.建立一个控制函数(身份像工头)或函数模板,它调用上述那些“劳工函数”并传递traits class所提供的信息。
traits class使得“类型相关信息”在编译期可用。它们以template和“template 特化”完成实现。整合重载技术后,traits class有可能在编译期对类型执行条件判断测试。

48:认识template元编程

  • template metaprogramming(TMP,模板元编程)可将工作由运行期移到编译期,可实现早期错误侦测和更高效的代码执行效率。
  • TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

8.定制new和delete

49:了解new-handler的行为

当operator new无法满足内存申请时,会不断调用new-handler函数,直到找到足够的内存。设计new-handler应该考虑:

  • 让更多内存可被使用。
  • 安装另一个new-handler。
  • 卸除new-handler,以便于抛出异常。
  • 抛出bad_alloc的异常,以传播到内存索求处。
  • 不返回,调用abort或exit结束程序。
    set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。nothrow new是一个颇为局限的工具,因为它只适用于内存分配,后续的构造函数调用还是可能抛出异常。

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

自定义new与delete的原因:

  • 用来检测运行上的错误。
  • 为了收集动态分配内存之使用统计信息。
  • 为了增加分配和归还的速度。
  • 为了降低缺省内存管理器带来的控件额外开销。
  • 为了弥补缺省分配器中的非最佳齐位。
  • 为了将香港对象成簇集中。
  • 为了获得非传统的行为。

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

  • operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。class专属半辈子则还应该处理“比正确大小更大的(错误)申请”。
  • operator delete应该在收到null指针时不做任何事情。class专属版本则还应该处理“比正确大小更大的(错误)申请”。

52:写了placement new时也要写placement delete

placement new(定位new)的参数除了一个size_t之外,还会有一个指针,该指针指向对象该被构造之处(比如某个数组的固定位置)。

  • 当编写了一个placement operator new,请确保编写对应的placement operator delete。否则,程序将可能发生内存泄漏。
  • 当编写placement new 和placement delete时,请确保不要无意思地遮掩了它们的默认正常版本。

9.其它

53:不要轻易忽略编译器的警告

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一种编译器,你原本依赖的警告信息有可能消失。

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

  • C++标准程序库主要机能由STL、iostreams、locals组成。并包含C99标准程序库。
  • TR1添加了智能指针、一般指针、hash-based容器、正则表达式以及另外10个组件的支持。
  • TR1本身只是一份规范。为获得TR1提供的功能,你需要一份实现。一个好的实现来源是Boost库。

55:熟悉Boost

  • Boost是一个社群,也是一个网站。致力于免费、源码开源、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。
  • Boost提供许多TR1组件实现,以及其他许多程序库。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值