您的位置:首页 > 编程语言 > C语言/C++

Effective C++总结

2020-04-02 07:30 471 查看

Effective C++阅读笔记

条款1 C++语言联邦

C++的“次语言”,用哪一个模块遵守其特定的规则:
(1)C
(2)OO C++
(3)模板
(4)STL

条款2 尽量不用#define

宏有时不会进入符号表

对于单纯常量,最好以const对象或enums替换#define
对于形似函数的宏,最好用template inline函数代替#define

条款3 关于const

如果const出现在 * 左边,表示被指物是常量;如果出现在 * 右边,表示指针自身是常量;如果出现在 * 两边,则被指物和指针都是常量。

T* const,表示这个指针不能指向其它的东西,但是其所指的东西可以改动;
const T*表示所指的东西不可改动,但指针所指可以改动。

条款4 关于初始化

对于内置类型(比如int),对其进行手动初始化。

对于非内置类型,需要使用构造函数进行初始化。尽量使用构造函数初始化列表来初始化,而不是先=default再在构造函数函数体内执行赋值操作。

函数体内的static对象叫local static对象。为免除“跨编译单元初始化的顺序问题”,尽量以local static对象替换non-local static对象。

条款5 C++默默编写并调用了哪些函数

C++会为空类暗自创建default构造函数、拷贝构造函数、拷贝赋值操作符以及析构函数,所有这些都是public且inline的。

如果主动在类中声明构造函数,编译器不再创建default构造函数。

条款6 若不想编译器自动生成,则显式拒绝

如果不想编译器生成条款5中的函数,可以通过将相应的成员函数声明成private并且不予实现。

或者定义一个基类,将这些成员函数放在基类的private中,然后private继承它,这样可以避免直接声明成private时,类的成员函数和友元还能访问private。

条款7 为多态基类声明虚析构函数

带多态性质的基类应该定义一个虚析构函数,这是因为如果一个派生类对象经由基类指针删除,而基类含有非虚析构函数,会出现未定义的结果。 一个类只要有虚函数就应该定义虚析构函数。

如果一个类的设计目的不是作为基类使用,或不带多态性质,就不应该为其定义虚析构函数,比如string、vector等标准库容器。

纯虚析构函数导致一个类变成抽象类,即不能被实例化。 ~virtual F() = 0;

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

避免在析构函数里raise异常,如果可能有也要用“双保险”的调用,try~catch。

可以把产生异常的代码放到一个普通函数中,由用户调用。

条款9 不在构造和析构的过程中调用虚函数

在构造和析构器近,这类调用不会下降至派生类,即派生类对象内的基类部分会先于派生类部分被构造妥当,在基类构造期间,“虚函数不是虚函数”。

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

为了实现连续赋值,赋值操作符必须返回一个reference指向操作符的左侧实参,即赋值操作符return *this。

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

确保当对象自我赋值时operator= 行为正确。可以使用“identity test”、copy-and-swap等方式。

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

如果为一个类添加一个成员变量,必须同时修改copying函数(拷贝构造、拷贝赋值)。

(派生类的)copying函数应确保复制对象内的所有成员变量及所有基类成分。
基类成分往往是private,应该让派生类的拷贝函数调用相应的基类函数。

条款13 以对象管理资源

把资源放进对象内,可以依赖C++“析构函数自动调用机制”确保资源被释放。

为防止资源泄露,使用RAII对象,它们在构造函数中获得资源,在析构函数中释放资源。
RAII:Resource Acquisition Is Initialization,“资源取得的时机便是初始化时机”,也就是以对象管理资源。

常用的两个RAII类分别是shared_ptr和auto_ptr(智能指针)。
auto_ptr复制动作会使它(被复制物)指向null,即“受auto_ptr管理的资源没有一个以上的auto_ptr同时指向它”。
shared_ptr(引用计数型智能指针)可以多对一,在无人指向的时候自动删除,但不能打破环状引用。

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

复制RAII对象必须一并复制它所管理的资源。

普通的RAII类拷贝行为是:禁止复制(将copying操作声明为private)、对底层资源用“引用计数法”(shared_ptr),但其它行为也能被实现。

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

每一个RAII类都应该提供一个“取得其所管理资源”的办法。

对原始资源的访问可以是显示转换(get)或隐式转换。一般显示转换比较安全,隐式转换对客户比较方便。

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

如果new中使用[],则需要使用delete [](数组的情况);
如果new中不含[],则只用delete。

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

如果一个语句中包含多个操作,比如调用函数,执行new,将new的对象置入智能指针,那么C++完成这三件事的次序可能有多种,如果调用夹在两者之间,一旦调用raise依次,就会导致内存泄漏。

而以独立语句将new的对象放进智能指针,编译器对于“跨越语句的各项操作”没有重排顺序的自由,所以调用new–>智能指针是分开的。

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

促进接口正确使用的办法包括保持接口的一致性、与内置类型的行为兼容。

阻止误用的办法包括建立新类型、限制类型上的操作、束缚对象值以及消除客户的资源管理责任。

tri::shared_ptr支持定制型删除器(计数为0时不释放内存空间)这一点可以防止DLL问题,可以被用来自动解除互斥锁mutexes。

条款19 设计class犹如设计type

class的设计就是type的设计。定义一个新type之前考虑本条讨论的主题(见原书)。

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

尽量以pass-by-reference-to-const替换pass-by-value,前者通常比较高效,并且可以避免对象切割问题。

对象切割问题: 当一个派生类对象以按值传递的方式传递,并被视为一个基类对象时,基类的拷贝构造函数会被调用,且切割掉派生类的部分。

当遇到内置类型(int)以及STL的迭代器和函数对象时,还是按值传递比较恰当。

条款21 必须返回对象时,别返回其reference

绝不要返回以下三种:
1、指针或引用指向一个局部对象(存放在栈中的对象);
2、返回引用指向堆中分配的对象;
3、指向一个同时需要多个local static对象时的指针或引用。、

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

将成员变量声明为private,客户唯一能够访问对象的办法就是通过成员函数。
这可以赋予客户访问数据的一致性、可细微划分的访问控制、允许约束条件获得保证,并提供类作者实现弹性。

从封装的角度,只有两种访问权限:private(提供封装)和其它(不提供封装)。
public和protected都意味着不封装:前者改变时破坏使用它的客户码,后者改变时破坏所有使用它的派生类。

条款23 宁可拿非成员函数和非友元函数替换成员函数

用非成员函数和非友元函数替换成员函数,可以增加封装性、包裹弹性和机能扩充性。

封装使我们能够改变事物而只影响有限客户:越多的东西被封装,越少的人可以看到它;而越少的人看到它,设计者就有越大的弹性去改变它。

C++标准程序库的组织方式:由很多头文件组成(vector,algorithm,memory等等),每个头文件声明std的某些机能,头文件中可以namespace声明某一类函数。

条款24 若所有参数都需要类型转换,采用非成员函数

如果需要为某个函数的所有参数(包括被this指针所指的那个隐式参数)进行类型转换,那么这个函数必须是个非成员函数。

只有当参数被列于参数列表(parameter list)内,这个参数才是隐式类型转换的合格参与者,换句话说:
this对象所指的隐式参数(被调用的成员函数所隶属的那个对象)不能执行隐式转换

条款25 写出不抛异常的swap函数

当std::swap对某个class效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。

当swap缺省实现版本效率不足(类或模板使用了某种pimpl手法:pointer to implementation)时:
(1)提供一个成员版本的swap,这个swap不会抛出异常。
(2)也应该提供一个非成员版本的swap来调用前者。
(3)如果是对于classes(而非模板),应该特化std::swap,并令其调用成员版本的swap。(全特化:total template specialication)

调用swap时应针对std::swap使用using声明式(using std::swap),接下来调用时不带任何“命名空间资格修饰”。

不要在std内加入某些新东西,也不要在global命名空间内塞满各种名称、class、template。

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

不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
这样不仅能够避免构造和析构非必要的对象,还可以避免无意义的default构造行为。

变量如果只在循环内使用,最好是定义在循环内。
定义在循环外:1个构造函数+1个析构函数+n个赋值操作(或其它操作);
定义在循环内:n个构造函数+n个析构函数。

条款27 尽量少做转型动作

C++提供四种新式转型:
(1)const_cast
(2)dynamic_cast
(3)reinterpret_cast
(4)static_cast

  • const_cast通常被用来将对象的常量性转除,它也是唯一有此能力的C+±style转型操作符。
  • dynamic_cast主要用来执行“安全向下转型”,可能耗费很大的运行成本
  • reinterpret_cast意图执行低级转型,例如将指向int的指针转型为int。
  • static_cast用来强迫隐式转换,例如将non-const对象转换成const对象,将int转换成double等等。
  • 之所以需要dynamic_cast,通常因为想在派生类对象身上执行派生类的操作函数,但手上只有一个指向基类的指针或引用,只能靠它们处理对象。此时有两种方法可以避免使用dynamic_cast:
    (1)使用类型安全容器
    (2)将virtual函数往继承体系上方移动

如果转型是必要的,试着将它隐藏于某个函数背后,客户可以调用该函数而不用把转型放在他们自己的代码中。

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

引用、指针和迭代器统统都是handles(号码牌,用来取得某个对象),而返回一个”代表对象内部数据“的handle,随之而来的是”降低对象封装性“的风险。
——这里的对象内部指的是:成员变量、被声明成private和protected的成员函数。

避免返回handles指向对象内部可以增加封装性、帮助const成员函数的行为像个const,并避免发生”号码牌虚吊(dangling handles)“,即handles所指的东西不复存在。

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

当异常被抛出时,带有异常安全性的函数会:
(1)不泄露任何资源
(2)不允许数据破坏

异常安全函数提供以下三个保证之一:
(1)基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
(2)强烈保证:如果异常被抛出,程序状态不改变。
(3)不抛掷(nothrow):承诺绝不抛出异常。

“强烈保证”往往以copy-and-swap实现,但“强烈保证”可能耗费太大的代价(需要copy)。

异常安全性具有“短板效应”。

条款30 关于inlining函数

inline只是对编译器的一个申请,不是强制命令,且inline和function template没有必然联系。

慎用inline,一般将大多数inlining限制在小型、被频繁调用的函数身上,因为inlining会导致代码膨胀问题:对此函数的每一个调用都会以函数的本体替换之

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

支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。

程序库头文件应该以“完全且仅有声明式”的形式存在,不论是否设计template。

为达到编译依存性最小化,可以遵循以下几个设计策略:
(1)如果使用object references或object pointers可以完成任务,就不使用objects。
(2)如果可以,尽量以class声明式替换class定义式。
(3)为声明式和定义式提供不同的头文件。

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

public继承意味着“is-a”(是一种)的关系。适用于基类身上的每一件事情一定也适用于派生类身上,因为每一个派生类对象都是一个基类对象。

宁可采取“在编译期拒绝企鹅飞行”的设计,而不是“只在运行期才能发现错误”的设计。

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

继承其实是派生类的作用域被嵌套在基类的作用域内。

派生类内的名称会遮掩基类内的名称。 为了使被遮掩的名称重见天日,可以使用:
(1)using声明式——using 基类::func()
(2)inline转交函数(forwarding functions)——用转交函数调用基类被遮掩的函数。

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

接口继承和实现继承不同,在public继承下,派生类总会继承基类的接口。

(1)声明一个纯虚函数的目的是为了让派生类只继承函数接口。(派生类必须提供自己的实现)

(2)声明非纯虚函数的目的是让派生类继承该函数的接口和缺省实现。(派生类必须提供该函数,但是可以自己实现也可以使用基类缺省版本。)

(3)声明非虚函数的目的是为了让派生类继承函数的接口及一份强制性的实现。

条款35 考虑虚函数以外的其它选择(关于设计模式)

当寻找某个设计方案时,可以考虑虚函数的替代方案,有以下几个:

  • 使用non-virtual interface(NVI)手法(Template Method设计模式的一种特殊形式,它以公有的非虚成员函数包裹较低访问性的虚函数。)
  • 将虚函数替换为“函数指针成员变量”。(Stratrgy设计模式的某种形式。)
  • 以tr1::function成员变量替换虚函数
  • 将继承体系内的虚函数替换成另一个继承体系内的虚函数。(Stratrgy设计模式的传统实现手法。)

虚函数的替代方案包括NVI手法以及Stratrgy设计模式的多种形式,前者自身是一个特殊形式的Templated Method设计模式。

将机能从成员函数移到类外部函数,带来一个缺点是非成员函数无法访问类的非共有成员。

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

非虚函数是静态绑定,虚函数是动态绑定的

非虚函数被调用取决于指向该对象的指针或引用类型,任何情况下都不该重新定义一个继承而来的非虚函数。

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

本条款仅限于“继承一个带有缺省参数值的虚函数”

虚函数是动态绑定的,而缺省参数值是静态绑定的。

  • 调用一个定义于派生类内的虚函数的同时,可能会使用基类为它指定的缺省参数值。

解决方法是考虑替代设计(条款35),比如使用NVI:令基类中的一个public非虚函数调用虚函数,并指定缺省参数。

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

复合是类型之间的一种关系,当某类型对象含它种类型对象时就是这种关系。

可以将程序中的对象分为两种:

  • 应用域:在应用域中复合意味着“has-a(有一个)”
  • 实现域:在实现域中复合意味着“is-implemented-in-terms-of(根据某物实现出)”

复合的意义和public继承完全不同。

  • 点赞
  • 收藏
  • 分享
  • 文章举报
青山遇绝壁 发布了35 篇原创文章 · 获赞 0 · 访问量 648 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: