Effective C++ 条款 05 - 12(注:08 还没看呢)
条款 05:了解 C++ 默默编写并调用哪些函数
- 如果并且只有
当你自己没声明的时候
,编译器会为类自动声明
一个 构造函数、一个 拷贝构造函数、一个 拷贝赋值运算符、一个 析构函数; - 这些函数只有
真正需要被调用的时候才会被自动 定义
; - 这些函数都是
public
和inline
的; - 只有基类的析构是非 private 虚析构,子类的合成析构才会被自动声明为虚析构;
- 编译器拒绝合成拷贝赋值运算符的几种情况:1) 类内有引用类型的成员(如果赋值给引用本身,违背了引用初始化后不能更改的标准,如果赋值给引用的对象,编译器又不知道是不是真的应该改变这个对象);2) 类内有 const 类型的成员;3) 基类的拷贝赋值运算符是 private 的,则子类不会自动合成拷贝赋值运算符。
条款 06:若不想使用编译器自动生成的函数,就该明确拒绝
- 对于拷贝构造和拷贝赋值,有时我们希望一些类拒绝这两种操作,即,希望编译器不要自动合成它们。可以用两种方式:
- 第一种:1. 手动给这个类声明拷贝构造和拷贝赋值 —— 编译器不会合成它们;2. 将手动声明的拷贝构造和拷贝赋值设为
private
的 ——用户
无法调用它们,否则会出现编译错误
;3.只声明而不实现
它们 ——类本身的成员函数及类的友元
也无法调用它们,否则会出现链接
错误。 - 第二种:让这个类
继承自一个空基类,将这个空基类的拷贝构造和拷贝赋值都设为 private
,这样,无论如何这个类的对象都不能调用拷贝构造和拷贝赋值了,否则会出现编译错误
。这个基类里无任何实际成员,private 的拷贝构造和拷贝赋值也无需实现,不占空间。
class Uncopyable {
protected:
Uncopyable();
~Uncopyable();
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
}
class ClzCannotBeCopied : public Uncopyable { // 实际上也可以不用 public 继承
...
}
条款 07:为多态基类声明虚析构函数
- 如果一个类中含有至少一个 virtual 成员函数 —> 说明它想要被当做一个多态基类来使用 —> 需要将析构函数设为虚函数 —>
否则,当用这个基类的指针 delete 一个它的派生类的对象时,属于派生类的那部分就无法被释放掉,造成部分销毁、资源泄漏
。 - 如果一个类的设计不是为了成为一个多态基类,那么它的析构函数就不应该设计成 virtual ,因为只要类中有一个 virtual 函数,就会为类生成一个
虚函数表
(一个包含虚函数的函数指针的数组),在类的对象内,会有一个指针的大小(4 字节或 8 字节)用来存放对象的虚表指针
来指向这个虚函数表 —> 对象的体积变大
,而这是完全无必要的。 - C++ 中
string 和 STL 的实现都是不想被做为多态基类
的,即它们的析构函数都不是 virtual 的,不要继承它们。
条款 08:别让异常逃离析构函数
条款 09:绝不在构造和析构过程中调用 virtual 函数
- 构造时先构造基类成分,再构造派生类成分 ——>
对象的基类成分构造期间,不会下降到派生类
,此时的 virtual 函数是基类的虚函数,不会动态绑定到派生类上(就好像这个 virtual 函数此时不是 virtual 函数一样),而且此时用运行时类型识别如 dynamic_cast 或 typeid 得到的都是基类类型。 - 析构时先析构派生类成分,再析构基类成分 ——> 同理,对象的派生类成分析构之后,编译器也会像看待基类一样看待这个对象,包括 virtual 函数不再绑定到派生类的函数上、运行时类型识别也不再是派生类。
- 要小心地注意不要在构造和析构过程中调用 virtual 函数,尤其是要小心它们中调用的一些非 virtual 函数内是否让它们间接地调用了 virtual 函数。
- 举个例子:
// 一个错误的示例:程序的本意是在构造期间调用 log 方法,在派生类中重写 log 方法,根据不同的派生类型打印不同的信息。
// 然而在构造期间调用的 virtual 函数并不会绑定到派生类上。
// 在这个例子中,由于 log 是个纯虚函数,会报错。
// 如果 log 不是纯虚函数,就会执行,并且在所有派生类中都会执行基类本身的 log() 方法。
class Fruit {
public:
Fruit() { log(); } // 错误行为
private:
virtual void log() = 0;
}
class Apple : public Fruit {
public:
Apple() {}
private:
virtual void log() override {
std::cout << "I am an apple." << std::endl;
}
}
class Banana : public Fruit {
public:
Banana() {}
private:
virtual void log() override {
std::cout << "I am a banana." << std::endl;
}
}
- 如果我们就是想实现上述的目的该怎么办呢?——
简单工厂方法
,在构造时给基类一个参数作为标志,让基类的 non-virtual 函数根据这个标志来实现不同的行为。
class Fruit {
public:
Fruit(const std::string& str) { log(str); } // 此时 log 非虚,可以调用。
private:
void log(const std::string& str);
}
void Fruit::log(const std::string& str) {
if (str == "apple") {
std::cout << "I am an apple." << std::endl;
}
else if (str == "banana") {
std::cout << "I am a banana." << std::endl;
}
}
class Apple : public Fruit {
public:
Apple("apple") {}
}
class Banana : public Fruit {
public:
Banana("banana") {}
}
条款 10:令 operator= 返回一个 reference to *this
- 这个条款一句话完事儿:
拷贝赋值运算符应该返回一个引用(而非一个值),这样便于实现连锁赋值。同样的,复合赋值也应该返回引用
,如 +=、-= …… 另外,就算返回了一个值也不会编译出错,但是我们应该遵循这种 “与内置类型保持一致” 的约定。
条款 11:在 operator= 中处理 “自我赋值”
- 也许我们认为 “自我赋值” 这种蠢蠢的情况永远不会在用户端发生,但是实际上它发生得还挺频繁的,这是由于
“别名”
。例如两个指向同一对象的指针
、或者arr[i] 和 arr[j] 中 i == j
的情况。甚至可能类型不同的两个对象实际上也是同一个对象,如基类指针类型和派生类指针类型
就可能指向同一个派生类对象。 - 三种处理自我赋值的方式:
if (this == &rhs) return *this;
——> 简单高效的判断方式,可以处理自我赋值,但不能解决异常安全;合理安排语句顺序
——> 实际上就是将一些成员先交给一个 tmp 值,等赋值成功之后再 delete 这些 tmp 值,如果赋值失败了,还能把原来的值还原回来,从而解决异常安全问题;copy and swap
——> 将参数 rhs 进行一次拷贝,然后将 this 与这份拷贝进行交换,离开函数作用域之后,这份拷贝会自动析构。也有一些实现方法将 operator= 的参数直接写成 pass-by-value 而非 pass-by-reference 来实现 copy and swap 中的 copy,但是感觉没啥必要。
条款 12:复制对象时勿忘其每一个成分
不要忘记复制每一个成分
,指的是:当你已经写完了拷贝构造和拷贝赋值之后。如果类新增了成员
,别忘记修改拷贝构造和拷贝赋值,把这个新增的成员也加上(因为编译器不会提醒你)。不要忘记复制基类的成分
,指的是:就是字面意思…… 如果你不去手动调用基类的对应函数 ——> 对于拷贝构造函数来说,它会调用基类的默认构造函数(因为你相当于没有给基类部分提供实参),于是拷贝得到的派生类对象的基类部分就成了缺省初始化的值(值初始化或者默认初始化,取决于类型);对于拷贝赋值来说,就是没有改变原有的对象的属于基类部分的那些成分的值。
class Derived {
public:
Derived(const Derived& rhs) :
Base(rhs), // 注意这里
i(rhs.i) { ... }
Derived& operator=(const Derived& rhs) {
if (*rhs == this) return *this;
Base::operator=(rhs); // 注意这里
i = rhs.i;
return *this;
}
private:
int i;
}
- 另外,即使拷贝赋值和拷贝构造中有很多重复,也不要让它们两个相互调用,因为那样没有意义。最好的办法是写
第三个函数
,让他们都调用这个 “第三个函数”。
转载:https://blog.csdn.net/qq_29505369/article/details/101525191
查看评论