1. 1. 基础议题
    1. 1.1. 条款一:区分Pointers && References
    2. 1.2. 条款2:尽量使用c++提供的类型转换符号
    3. 1.3. 条款3:绝对不要以多态的方式处理数组
    4. 1.4. 条款4:非必要不提供默认无参构造函数
  2. 2. 操作符
    1. 2.1. 条款5:对定制的类型转换函数保持警觉
    2. 2.2. 条款6:区别++–的前置/后置形式
    3. 2.3. 条款7:不要重载&& ||和,操作符
    4. 2.4. 条款8:了解不同意义的new和delete
      1. 2.4.1. new operator 和 operator new
      2. 2.4.2. operator delete和delete operator
      3. 2.4.3. operator new[]
  3. 3. 异常
    1. 3.1. 条款9:利用析构函数释放资源

本文章为阅读该书过程中的总结和笔记,并不保证完全正确

对书内内容的摘抄纯粹是因为买不起书,毕竟现在手里的书都是从图书馆借的:-(

基础议题

条款一:区分Pointers && References

首先明确,指针指向的对象可变且可为空,引用指向的对象不可变,且不可为空
因此:引用无需判空,因此效率更高(可能)
其次,由于指针可变,引用不可变,因此,如果需要考虑不指向任何对象或指向不同的对象时,使用指针,其余情况则使用引用
再次,如果需要实现某些特定操作符时,考虑使用引用,否则语法可能会十分奇怪

1
2
3
4
5
vector<int> v(10);
v[5] = 10;
// 如果返回指针
vector<int*> vp(10);
*vp[5] = 10; // 语法很奇怪

条款2:尽量使用c++提供的类型转换符号

C风格的类型转换(type)expression没有任何的安全性检查,而且在程序编写后难以检查在哪里发生了类型转换,且C++类型转换提供了更多样化的转型手段
关于具体转换方式与其细节,可以参考另一篇文章

条款3:绝对不要以多态的方式处理数组

假如有:

1
2
3
4
5
6
public A{

}
public B:public A{

}

那么此时,如果我们有一个函数

1
2
3
void printA(A[]) {
//遍历A数组并打印
}

如果A和B的成员变量数目不同,且我们传入了B,此时程序还是会正常调用printA函数,(如果没有奇怪的对[]的重载),则会按照A函数的size进行定位,结果可能会很糟糕
另外,如果在该函数中错误的调用了构造函数或析构函数,可能也会导致出现意料之外的情况

条款4:非必要不提供默认无参构造函数

构造函数应该包含可以区分该对象的信息(有点像是主键),一个无参且没有任何初始化操作的构造函数对于程序来说是意义不明的,同样,如果因为逻辑意外导致了一个空对象在程序内被到处传递,可能会导致严重的后果
即便在无参构造函数中将关键信息写为了例如UNSPECIFIED的参数,这样在之后的每次操作中都要耗费更多的时间判断其是否为空,会造成效率的浪费

操作符

条款5:对定制的类型转换函数保持警觉

现代编译器是十分智能的,例如有一个类

1
2
3
4
5
6
7
8
9
10
11
12
Class Rat{
public:
Rat(int a = 1,int b = 1) {
this.a = a;
this.b = b;
}
operator double() {
return static_cast<double>(a)/b;
}
private:
int a,b;
}

如果我们调用

1
std::cout<<Rat(1,2);

假设我们预期想让他输出1/2,但是我们并没有对<<运算符进行重载,按照预期,程序应该报错
但是此时编译器可能会自动寻找你是否有重载其他运算符,在这个例子中编译器找到了double(),而且<<支持double,此时编译器就有可能会自动将其转换为double,然后输出一个0.5,这显然与预期不符
如果这个数字被显式的输出了,那么问题还相对好定位,如果他只是复杂程序里的一个中间量,那么定位的难度可能会大幅增长:-(
那么我们再来看看另一种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define mian main
template<class T>
class Array{
public:
Array(int maxm,int minn);
Array(int size);
T& operator[](int index);
bool operator==(const Array<int>& lhs,const Array<int>& rhs); // 这是一个用来比较数组是否相等的函数
}

int mian(){
Array<int> a(10);
Array<int> b(10);
for(int i=0;i<10;i++) {
if(a == b[i]) { // 本意是想让两个int进行比较
// do something
}
}
}

在上文的代码编写中,本意是a[i] == b[i],但编写时遗漏了[],按预期程序不应该运行,但由于聪明的编译器为我们扫除障碍,他找到了一个构造函数 __Array(int size);__,因此程序很可能会变成这样
(注意多变量构造函数不会被作为用户定制转换行为存在)

1
2
3
...
if(a == static_cast< Array<int> >(b[i]))
...

程序发现了单参构造方法的存在,让他们都成为了Array,然后调用了重载后的==
虽然在这个例子中看起来结果并没有什么大的不同,但消耗了更多的时间和空间
况且,如果是更为复杂的情况呢?
如何避免
使用关键字 __explicit__,禁止隐式类型转换
关于哪些隐式类型转换是合法的,标准库中有一句话:没有任何一个转换程序可以内含一个以上的用户定制转换行为(亦即单自变量constructor或隐式类型转换操作符),如果你没看懂的话,可以看下面的例子
如何改写上面的程序
首先明确一个事实,编译器只会考虑一次用户定制转换行为,因此我们可以定义一个ArraySize类(可以由int构造),即Array(ArraySize a);,当写出Array a(10)时,编译器会找到int->ArraySize,构造成功,但是对于==,编译器不会去找int->ArraySize->Array,因为这涉及到两个用户定制转换行为,这样也可以达到同样的目的。

条款6:区别++–的前置/后置形式

由于c++多态是根据参数类型来区分的,因此在底层实现时,约定后置操作符需要给一个默认值,前置操作符不需要
++i:累加后返回当前值
i++:累加后返回原始值
这样就带来一个问题:++++i非常的正常且舒畅,且两次操作都可以正常,因为在整个过程中都没有新变量的出现
但i++++就出现问题了,i++++实质上可以看错i.operator++(0).operator++(0),可以看到第二次操作操作在了第一次操作返回的结果上,假如i本来是1,i++++后返回1,但是这个时候i的值为2,同样,对于现在的不同编译器来说,i++++被看作是UB,那么如何从根源上解决问题呢?
答案:

1
2
3
4
5
const Int Int::operator++(int) {
Int oldValue = *this;
++(*this);
return oldValue;
}

这样,在进行第二次操作时,会由于返回值的类型为const而报错,因此可以规避问题
另外,这个int是为了区分两个自增所存在的(c++的多态依靠参数类型和个数来区分重载哪一个函数),没有名称是因为有的编译器会发出”某物未被使用”的警告,但如果“某物”没有名字,那么编译器就不会警告

条款7:不要重载&& ||和,操作符

首先我们重温一下相关名词
&&:都真为真
||:一真则真
,:求一连串值,以最后一个表达式的值作为整个表达式的值
短路效应:表达式结果已不可撼动时,就会立刻停止,比如a||b–||c++,如果a为真,那么后面的b–和c++都不会发生

但如果在程序中重载了这三个运算符,那么原本期望的短路效应则不复存在,取而代之的是函数调用
假设程序现在有表达式A和B(都带有一些操作)
对于 A&&B
在原本的程序逻辑中,如果A不成立,那么B也不会被执行,如果A不成立,那么也是A先执行,然后B再执行
但如果发生了&&的重载,原表达式则会被看作: __A.operator&&(B)__,不论A是否成立,B都一定会执行
更可怕的是,C++标准并没有规定对于 A.operator&&(B) 来说,A和B哪一个操作会被率先执行,手动重载的运算符永远无法突破这个限制,因此你无法做到重载后,让运算符保持其原有的含义和行为
这样会破坏掉原有的规则,也会违背C++程序员的代码习惯

条款8:了解不同意义的new和delete

new operator 和 operator new

1
string *ps = new string("qwq");

这个行为中涉及到的new是上文所说的new operator,由语言内建,不可更改或重写,包含分配内存->调用构造函数两步,永远不能被改变
如果我们拆分new __operator__,可以抽象为下列几个语句

1
2
3
void* memory = operator new(sizeof string);
// call constructor on memory
return static_cast<string*>(memory);

可以看出new __operator__包含operator new(下称on)
on所做的动作仅仅是分配size_t大小的内存,不会做任何其他的操作(其实就是封装了malloc),我们可以重载的new其实是on,但要遵守以下规定:

  • 返回类型为void *
  • 第一个参数为size_t size

那么如果我们有一片分配好的原始内存,我们需要在它上面构造对象,可以采用placement new(一个特殊版本的operator new)

1
2
3
SelfDefineClass* construct(void* buffer, int size) {
return new(buffer)SelfDefinedClass(size);
}

这样可以在给定的地址buffer上调用这个类的构造函数,即placement new

1
2
3
void* operator new(size_t, void *location) {
return location;
}

在new operator中,包含on和实例化两个步骤,即找到一片可用的内存,在这块内存上初始化,那么在placement new时,程序不再需要自己寻找可用内存,直接在接受的void*上构建即可,这就是placement new,但注意,如果想要调用placement new,需要包含new库(或new.h)
至于那个size没有名称的原因,是为了防止编译器发出“某物未被使用”的警告,和条款6是类似的

operator delete和delete operator

和new一样,operator delete负责析构->释放内存,而delete operator只负责释放内存,同理,如果对于一片内存,我们使用的是placement new初始化,而且我们摧毁掉之后并不像交还内存使用权(或者暂时无法交还等情况),那么我们应该直接调用析构函数而绕过内存释放,反之同理

operator new[]

operator new[]是较晚加入的c++新特性,在c++11之前,operator会调用全局operator new来进行初始化,在c++11及以后支持了原生操作的new[]
点击这里查看文档
因此,如果我们用c++11+的版本,可以对new[]进行重写,以达到一些特殊的效果,delete[]同理

异常

条款9:利用析构函数释放资源

1
2
3
4
5
void Process() {
A* a= doSth();
a->doSth();
delete a;
}

上述程序看起来不会发生内存泄漏,运行正常的情况下a会被删除,如果a->doSth()出问题程序只会崩掉,但是如果,我们catch了Exception,导致a->doSth()出现了问题,但问题被我们捕获住了,程序继续运行了,那么这个时候就会发生内存泄漏,崩溃一次,泄露一次。
解决方法也很简单

1
2
3
4
5
6
7
8
9
10
11
void Process() {
A* a= doSth();
try{
a->doSth();
} catch( exception e){
delete a;
e.what();
throw;
}
delete a;
}

这样子无论如何a就都会被删除掉,但后果可能是:你的程序被奇怪的try-catch层层包围,不久就变成了一座*山
那么为了避免这个问题,我们可以使用智能指针

1
2
3
4
void Process() {
auto_ptr<A> pa = doSth();
pa->doSth();
}

程序就变成了这个样子
关于智能指针和他们之间的区别可以看
智能指针详解