小言_互联网的博客

Effective C++ 条款 05 - 12【构造/析构/赋值运算】

374人阅读  评论(0)

Effective C++ 条款 05 - 12(注:08 还没看呢)


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

  • 如果并且只有当你自己没声明的时候,编译器会为类自动 声明 一个 构造函数、一个 拷贝构造函数、一个 拷贝赋值运算符、一个 析构函数
  • 这些函数只有真正需要被调用的时候才会被自动 定义
  • 这些函数都是 publicinline 的;
  • 只有基类的析构是非 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 的情况。甚至可能类型不同的两个对象实际上也是同一个对象,如基类指针类型和派生类指针类型就可能指向同一个派生类对象。
  • 三种处理自我赋值的方式:
  1. if (this == &rhs) return *this; ——> 简单高效的判断方式,可以处理自我赋值,但不能解决异常安全;
  2. 合理安排语句顺序 ——> 实际上就是将一些成员先交给一个 tmp 值,等赋值成功之后再 delete 这些 tmp 值,如果赋值失败了,还能把原来的值还原回来,从而解决异常安全问题;
  3. copy and swap ——> 将参数 rhs 进行一次拷贝,然后将 this 与这份拷贝进行交换,离开函数作用域之后,这份拷贝会自动析构。也有一些实现方法将 operator= 的参数直接写成 pass-by-value 而非 pass-by-reference 来实现 copy and swap 中的 copy,但是感觉没啥必要。

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

  1. 不要忘记复制每一个成分,指的是:当你已经写完了拷贝构造和拷贝赋值之后。如果类新增了成员,别忘记修改拷贝构造和拷贝赋值,把这个新增的成员也加上(因为编译器不会提醒你)。
  2. 不要忘记复制基类的成分,指的是:就是字面意思…… 如果你不去手动调用基类的对应函数 ——> 对于拷贝构造函数来说,它会调用基类的默认构造函数(因为你相当于没有给基类部分提供实参),于是拷贝得到的派生类对象的基类部分就成了缺省初始化的值(值初始化或者默认初始化,取决于类型);对于拷贝赋值来说,就是没有改变原有的对象的属于基类部分的那些成分的值。
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;
}
  1. 另外,即使拷贝赋值和拷贝构造中有很多重复,也不要让它们两个相互调用,因为那样没有意义。最好的办法是写第三个函数,让他们都调用这个 “第三个函数”。


转载:https://blog.csdn.net/qq_29505369/article/details/101525191
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场