「笔记」《Effective C++》 读书笔记(二)

注意!

这只是我的读书笔记,帮助我记录。其中可能有一些不严谨的解释或没有做很详细的解释,大家学习还是建议直接看书(虽然书中也有不严谨的解释)。

构造 / 析构 / 赋值运算

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

一个 class 如果没有声明任何构造函数和析构函数,编译器会帮你声明以下的函数

  • default 构造函数
  • copy 构造函数
  • copy assignment 操作符
  • 析构函数
    所有这些编译器为你生成的函数都是 public 的、大多数都是 inline 的( Effective C++ 写的是都是 inline ),且只在需要时生成。

当你声明了任何一个构造函数,编译器将不再为你声明 default 构造函数。

对于两个要做拷贝的函数,它们可能是 bitwise 或者 memberwise。具体的 Effective C++ 只做了简略的解释。想深入了解的可以看《深度探索C++对象模型》,之后我也会写读书笔记或书摘。

如果类中存在不可重新赋值的成员将报错。

比如下面的类

1
2
3
4
5
6
7
8
template<class T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value):nameValue(name),objectValue(value){}
private:
std::string& nameValue;
const T objectValue;
};

引用类型和 const 修饰的成员不可被重新赋值,如果存在两个 NamedObject<int> 类的对象 a 和 b ,做 a = b 将直接报错。编译器无法为它声明拷贝函数。
如果基类的 copy assignment 操作符被声明为 private 也是会导致编译失败。

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

可以将构造函数声明为 private 禁止调用。比如有些类不希望被拷贝,可以将 copy 构造函数和 copy assignment 操作符声明为 private 。

1
2
3
4
5
6
7
8
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};

也可以让不希望被拷贝的类直接继承 Uncopyable 类。Boost 库中也提供了名为 noncopyable 的class 保证被继承的类不被拷贝。

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

基类无虚析构函数,会导致向上造型后被部分销毁导致内存泄露。

谨慎继承不带虚析构函数的类,如:string、STL 库中的所有容器…

不是所有的类都需要虚析构函数,vptr 和 vtbl 会占有大量空间,且降低了可移植性(不可传给其他语言)。

许多人的心得时:只有当 class 内含至少一个 virtual 函数 才为它声明 virtual 析构函数。

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

C++ 不能同时处理多个异常,主要原因是被抛出的元素的内存空间是分配在栈区的,抛出异常后会跳出那一层括号,栈区应该被清理。如果你在存在一个异常的情况下去处理另一个异常很可能会覆盖掉之前异常抛出的元素,导致程序过早结束或出现不明确行为
如果你的类中析构会抛出异常,那这个类的容器或数组在析构时很可能遇到多个异常,造成严重的后果。

有时我们类的析构函数必须执行一个可能抛出异常的行为时怎么办?比如关闭各种连接。
书中提供了三种方案

1
2
3
4
try{ A.close(); }
catch(...){
std::abort();
} //调用 abort 结束程序

1
2
3
4
try{ A.close(); }
catch(...){
//记下运转记录,记下对 close 的调用失败
} //吞下异常
1
2
3
4
5
6
7
8
9
10
11
12
void close()
{
db.close();
closed = true;
}
//析构
if(!closed){
try{ db.close(); }
catch(...){
//记下运转记录,记下对 close 的调用失败
}
}

第三种就是直接将 close 函数开放给使用者,让使用者在析构前调用。

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

在构造函数和析构函数中调用虚成员函数,可能得不到你想要的结果,它实际上会调用基类的那个函数。

解释

构造过程

构造过程进入基类的构造函数时,派生类的成员还没被初始化,如果调用派生类的虚函数可能会用到派生类部分的成员,所以编译器将构造过程中的对象当做 当前进入的构造函数所属的类的一个对象,当然只能调用与当前构造函数同属一个类的函数了。

析构过程

进入析构函数,先销毁掉派生类的成员,在刚进入析构函数时这个对象已经不是完整的一个派生类的对象了,编译器只能把它当做它的基类的一个对象来看待。

在构造和析构期间不要调用 virtual 函数,因为这个类调用从不下降至 drived class (比起当前执行构造函数和析构函数的那层)。

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

返回 *this 的引用,可以让你的类的对象实现连锁赋值。
如:x = y = z = 15;

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

如果类中存在一个指向堆中元素的成员,那就要注意赋值时的自我赋值。

1
2
3
4
5
6
7
8
9
10
11
12
class A{
public:
string *s;
A():s(new string()){}
A& operator= (const A rhs)
{
delete s;
s = new string(*rhs.s);

return *this;
}
};

如果 this&rhs 相等就尴尬了,它会先 delete 自己的 s ,实际上 *thisrhs 里的 s 指向的 string 对象都被销毁了。数据丢失而去之后也没办法使用成员 s 了。

相对安全的版本是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
public:
string *s;
A():s(new string()){}
A& operator= (const A& rhs)
{
if(this == &rhs) return *this;

delete s;
s = new string(*rhs.s);

return *this;
}
};

加入认同测试,保证了 “自我赋值” 的安全性,但还不具备 “异常安全性”。
如果在 s = new string(*rhs.s); 这一步导致异常,那 s 将指向一块已被删除的 string 。

1
2
3
4
5
6
7
8
A& operator= (const A& rhs)
{
string* pOrig = pb;
s = new string(*rhs.s);
delete pOrig;

return *this;
}

这样即使抛出异常,赋值失败也不会造成其他副作用,可以将 s 保持原状。

copy and swap 技术

1
2
3
4
5
6
7
8
9
void swap(A& rhs); //交换*this 和 rhs 的数据 见条款29

A& operator= (const A& rhs)
{
A temp(rhs);
swap(temp);

return *this;
}

by value 传值方式的 copy and swap 技术

1
2
3
4
5
6
7
8
void swap(A& rhs); //交换*this 和 rhs 的数据 见条款29

A& operator= (const A rhs)
{
swap(rhs);

return *this;
}

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

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
class A{
private:
int a;
public:
A(){}
A(A &rhs):a(rhs.a){}

A& operator= (A &rhs){
a = rhs.a;
return *this;
}
};

class B : public A{
private:
int b;
public:
B(){}
B(B &rhs):b(rhs.b){}

B& operator= (B &rhs){
b = rhs.b;
return *this;
}
};

小心这样的代码,B 的两个 Copying 函数,都没有拷贝基类的数据( int a )。

Copying 函数应该确保赋值“对象内的所有成员变量” 及 “所有 base class 成分”。

1
2
3
4
5
6
7
8
9
10
11
12
13
class B : public A{
private:
int b;
public:
B(){}
B(B &rhs):b(rhs.b),A(rhs){} // + !

B& operator= (B &rhs){
A::operator=(rhs); // + !
b = rhs.b;
return *this;
}
};

两个 Copying 函数一般有相近的代码,但不要在一个 Copying 函数内调用另一个 Copying 函数。

应该将共同机能放进第三个函数中,并由两个 Copying 函数共同调用。

本文标题:「笔记」《Effective C++》 读书笔记(二)

文章作者:赵砚潇

发布时间:2018年07月03日 - 17:07

最后更新:2018年12月21日 - 20:12

原始链接:https://blog.zyx.sh/2018/07/03/effective-cpp02/

许可协议: 署名-非商业性使用-相同方式分享 4.0 国际 转载请保留原文链接及作者。

Donate comment here