0%

二刷Effective C++


针对《Effective C++》各个条款的核心要点。

注:部分笔记记录于侯捷C++视频.md


让自己习惯C++

1、 多用explicit

可避免编译器执行非预期的类型转换。

2、A a=b;

调用的是拷贝构造函数,而不是拷贝赋值。

3、C++四部分:面向过程,面向对象,模板,STL

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

前者是编译器处理,后者是预处理器处理。
缺点1:例如 #define A 10, 假如A的运用出现错误,编译时出现的问题显示的是10,不容易被发现。
缺点2:#define 的东西不分作用域
缺点3:加上括号,写出来很复杂。
取enum的地址不合法。

5、 mutable的作用是使变量在const成员函数中也能被修改

6、const和non-const成员函数中避免重复——在non-const中调用const

需要做个转型动作。将非const成员先转换成const,然后调用const函数,然后再去除const(只能用const_cast),然后返回。

7、考虑跨编译单元初始化顺序时,最好用local static对象。

local static对象是在函数内的static对象,其他的为non-local static对象。

不同的编译单元内的non-local static对象的初始化相对次序并无确切定义。

单例模式


构造/析构/赋值运算

8、虚析构函数

用基类指针指向派生类对象时(动态绑定)进行析构,则派生类多余部分没销毁干净。

析构函数改成虚函数后,则析构函数调用的是派生类的。

但是,无端设置,会增加存储体积。往往是至少有一个虚函数的类才声明虚析构函数。

纯虚函数写虚析构函数时也要加 =0,且要加一份 空定义

9、不要在构造中或者析构中调用虚函数

在base构造期间,虚函数不是虚函数。

**10、派生类的拷贝构造(拷贝赋值)函数中,一定要将参数传给父类的拷贝构造(拷贝赋值)函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A
{
public:
A(const A& a):f(a.f)
{
}
A& operator=(const A& a)
{
f=a.f;
return *this;
}

private:
int f;
}

class B:public A
{
public:
B(const B& b):A(b),q(b.q)//将参数传给父类的拷贝构造函数
{
}

B& operator=(const B& b)
{
A::operator=(b);//调用父类的拷贝赋值函数
q=b.q;
return *this;
}

private:
int q;
}

资源管理

11、用对象管理资源

避免过早的return,导致在末尾的delete语句没有执行而发生资源泄漏,利用对象的析构函数可解决这个问题。

两个关键:

1、在获得资源后立即放进管理对象内。2、运用析构函数确保资源被释放。

12、auto_ptr和share_ptr

auto_ptrs的拷贝构造、拷贝赋值会将本身变回null,而复制所得的指针将会获得资源的唯一拥有权,有点不正常,因此不能用于容器

shared_ptr引入了计数器,不会有以上性质,可用于容器。

并且,auto_ptr和share_ptr不能指向动态分配的数组,因为他们的析构函数是delete,而不是delete [].

13、资源管理类小心copying行为

通常两种行为:

1、禁止复制,私有继承Uncopyable(它的拷贝操作是私有的)。

2、对底层资源祭出“引用计数法”——shared_ptr,若想在shared_ptr计数为0时不被删除,则需要定义它的删除器。

shared_ptr构造函数接受两个参数,第一个参数类型必须为指针,第二个参数为删除器。

14、资源管理类一般要提供一个方法来访问底层资源

通常两个手段:

1、显式get函数 (一般比较安全)

2、隐式转换

1
2
3
4
5
6
7
8
9
10
11
class Font
{
public:
operator FontHandle () const //隐式转换函数
{
return f;
}

private
FontHandle f;
}

15、delete数组时应该要加中括号

1
2
new A[100];
delete [] A;

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

避免资源泄漏

1
2
shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());

设计与声明

17、尽量让接口和系统自带的接口行为相似

18、函数传参尽量传引用

优点:

1、速度快

2、防止对象切割(子类以by value形式传给父类)

19、避免返回不存在的对象(返回引用)

这里的引用是函数声明式的返回类型是 引用

1
2
3
4
5
6
//下列是容易出现的错误代码
const Rational& operator*(const Rational& ls,const Rational& rs)
{
Rational result(ls.n * rs.n , ls.d * rs.d);
return result;//等下返回的对象会被删除
}

20、成员变量设置为private,提高封装程度

protected 并不比 public 更具有封装性。

21、外部函数比成员函数更好,可提高封装程度

增加类的可延伸性;降低编译相依度。

外部函数不能访问私有成员,因此提高了封装程度。

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

类的加减乘除重载,且该函数不一定需要成为类的友元。

23、写一个好的swap函数的考虑过程

1、当std::swap效率不高时,提供一个 swap成员函数

2、提供一个no-member的swap来调用成员版本 来访问私有成员

3、若想在swap成员函数中使用stl的swap,使用**using std::swap **而不是 std::swap(a,b) ,后者是强制调用,前者是使stl中的得以暴露,然后根据命名空间的搜索过程来寻找合适的。


实现

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

如果在程序抛出异常了,太早定义这个变量,它就有可能没被使用,但消耗了成本。

并且最好延后到能够给它初值为止,因为这样可以直接定义,而不是赋值。(赋值通常比较慢)

25、尽量少转型

1、要用用新式的。

2、(假如手上只有一个基类指针,但是想要调用派生类函数)

​ 避免dynamic_casts的两种方式:

​ a.基类虚函数设置为空,派生类虚函数设置内容。(区分函数职能)

​ b.将基类指针存储在派生类类型的容器中,然后用迭代器解引用调用。

26、尽量避免返回handles(指针、引用、迭代器)指向对象内部

降低了封装性。——可采用增加const

但是增加了const还是不好,有可能出现指针指向虚调号码牌(临时对象,但是在调用后被删除了)。

27、减少抛出异常

一个一般化的设计策略——copy and swap,为你打算修改的对象做一份副本,然后在副本上修改。若有任何修改错误,那么原对象保持不变,只有在所有修改成功后,才做替换。

实际上,一般是将所有数据放置一个对象中,然后原对象通过一个指针指向那个数据对象(副本)(pimpl idiom手法)。

同时,一个函数操作得越多,那这个函数越容易抛出异常。

28、 了解inline

过度会造成程序体积太大(占用内存变大——额外换页,降低缓存效率等)

  • 函数体在class内的,都是inline。(包括友元函数)

  • inline函数一般放在头文件中;

  • virtual函数、通过函数指针调用的函数、构造析构函数拒绝inline

  • debug版本一般也拒绝inline,在不存在的函数中打断点很困难。

**29、实现与接口分离

第一种手法:

A对象中存一个指针pa,pa指针(接口)指向具体实现的对象(实现类)。(pimpl idiom手法)(代理模式

  • 如果能够,声明式比定义式好。

  • 为声明式和定义式提供不同的头文件。(比如一个文件只需要声明,就只用包含.h文件)

第二种手法

把接口定义为纯虚类,但该纯虚类中有一个create函数(工厂函数),create函数返回一个类型为自身,但指向派生类的指针(使用接口类的构造函数,构造函数参数为派生类的构造函数的返回值),这样就可以利用多态来进行运用真正的函数(派生类中的)。

实现类继承上述纯虚类,并覆盖所有的纯虚函数

详细见P145

30、public继承=is a

31、避免遮掩而来的名称

如果派生类与父类的函数同名但是参数类型不同,那么父类的函数不会被继承,就算调用时采用的是父类的参数格式,也调用不到。

若想解决,可以使用using声明,此时两者都可以用。若只想继承一种(父类有同名的两种),可以用转交函数,因为using会全部继承下来。

32、区分接口继承和实现继承

  • 纯虚函数的目的是让派生类只继承接口。

​ 基函数的纯虚函数也可以有定义,但是调用它的方式只有通过指明 类名称。

  • 普通虚函数的目的是让派生类继承接口和缺省版本。
  • 非虚函数的目的是让派生类继承接口和一份强制性实现。

一个灵活的手法:父类为纯虚函数,里面有定义(缺省版本),派生类中想用缺省版本的,在虚函数中指定父类名称调用缺省版本,不想要缺省版本的自己定义。(一种比较次的方法是父类定义纯虚函数和一个缺省的非虚函数,派生类想要用缺省版本的,在纯虚函数调用继承下来的非虚函数,不想要的自己定义。有一点不好就是,假如该非虚函数是protected的,不想暴露,但是纯虚函数是公开的,这样就给暴露了,降低了封装性)

注意:纯虚函数不是不可以定义,而是一般没有作用,对于含有纯虚函数的抽象类,不可以实例化对象。

**33、虚函数的替代方案

模板模式使用Non-Virtual Interface手法实现——将具有多态性质的函数设置为非虚函数,在该函数中调用真正的虚函数,这样真正的虚函数就可以设置为private。

策略模式使得同一类,可以有不同的健康计算函数。

虚函数的替代方案:

  • NVI手法,模板模式的一种特殊情况,用public非虚函数包含低访问性的虚函数
  • 把虚函数替换为函数指针变量——策略模式的一种特例。
  • 用std:function 替换虚函数
  • 传统策略模式

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

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

当继承一个带有缺省参数值的虚函数,虚函数是动态绑定,缺省参数值却是静态绑定

36、慎用Private继承,它意味着——根据某物实现出

由于private继承将基类的所有public都改为private,因此,可以将private继承视为继承子类的实现而略去子类的接口(因为子类的接口由于private的原因不能再被调用者调用,相当于接口被取消)

用public继承和复合也许更好,虽然会复杂一点。

37、菱形继承得采用——虚继承

菱形的左右 虚继承于 菱形的上顶点

理论上看,public继承应该总是virtual的,但是虚继承有成本。


模板与泛型编程

38、typename的另一种用法

编译器可能不知道下面是个迭代器指针类型,因为可能是C类型中有个成员变量叫做const_iterator,又或者是x是个全局变量。

1
2
3
4
5
template<typename C>
void printf2nd(const C& container)
{
C::const_iterator x;//C::const_iterator是个嵌套从属类型,这是个错误代码,正确写法应该在前面加一个typename
}

但是,不得在 基类列成员初值列 内以它作为基类的修饰符。

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

以带有Base-Derived关系的B、D两类型分别具现化某个模板,他们产生的两个具现体并不具有继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class Smartptr
{
public:
template<typename U>
Smartptr(const Smartptr<U>& other):holder(other.get())//叫做泛化构造函数
{
...
}

T* get {return holder;}

private:
T* holder;
}

代码中使用 U* 指针去初始化 T* 指针,筛选了函数群(如果不这样,那么任何一个类型U都可以初始化T),使得只有使用子类才能初始化(向上兼容)。

当你写了泛化构造函数,还是要去写正常的copy构造函数和拷贝赋值函数。

成员函数模板还有一个功能就是赋值。(比如shared_ptr中可以兼容内置指针,weak_ptr,auto_ptr的赋值)

40、一个模板类的乘法,其他类型想隐式转换为模板类型,需要定义非成员函数

在模板中写函数,且该函数是friend的,且函数体在模板类中,而不是成员函数。

详细看条款46


还不是很明白的

条款8

条款25

条款46