Item 5: 了解 C++ 默认添加和调用的函数

编译器默认为一个类生成的函数

如果你自己不声明一个拷贝构造函数,一个拷贝赋值运算符和一个析构函数,编译器就会为这些东西声明一个它自己的版本。此外,如果你自己根本没有声明构造函数,编译器就会为你声明一个缺省构造函数。所有这些函数都被声明为 public 和 inline。作为结果,如果你写:

class Empty{};

在本质上和你这样写是一样的:

class Empty {
public:
  Empty() { ... }                  //@ default constructor
  Empty(const Empty& rhs) { ... }  //@ copy constructor
  ~Empty() { ... }   //@ destructor — see below,for whether it's virtual
  Empty& operator=(const Empty& rhs) { ... } //@ copy assignment operator
};

这些函数只有在它们被需要的时候才会生成,但是并不需要做太多的事情,就会用到它们。下面的代码会促使每一个函数生成:

Empty e1;    //@ default constructor;destructor
Empty e2(e1);  //@ copy constructor
e2 = e1;    //@ copy assignment operator

生成的析构函数是非虚拟的,除非它所在的类是从一个基类继承而来,而基类自己声明了一个虚拟析构函数,这种情况下,函数的虚拟性来自基类。

对于拷贝构造函数和拷贝赋值运算符,编译器生成版本只是简单地从源对象拷贝每一个非静态数据成员到目标对象。

template<typename T>
class NamedObject {
public:
  NamedObject(const char *name, const T& value);
  NamedObject(const std::string& name, const T& value);
  ...
private:
  std::string nameValue;
  T objectValue;
};

NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1);                 //@ calls copy constructor

nameValue 的类型是 string,标准 string 类型有一个拷贝构造函数,所以将通过以 no1.nameValue 作为参数调用 string 的拷贝构造函数初始化 no2.nameValue。而另一方面,NamedObject::objectValue 的类型是 int,而 int 是内建类型,所以将通过拷贝 no1.objectValue 的每一个二进制位初始化 no2.objectValue。

编译器生成拷贝赋值运算符的相关约束

拷贝赋值运算符只有在结果代码合法而且有一个合理的可理解的巧合时,编译器才会默认生成。

template<class T>
class NamedObject {
public:
  NamedObject(std::string& name, const T& value);
  //@ assume no operator= is declared
  
private:
  std::string& nameValue;           //@ this is now a reference
  const T objectValue;              //@ this is now const
};

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);                                
NamedObject<int> s(oldDog, 36);                                                
p = s;                                                                   

赋值之前,p.nameValue 和 s.nameValue 都引向不同的 string 对象。如果赋值成功,则会修改引用绑定的对象,但是 C++ 并没有提供使一个引用引向另一个对象的方法。面对这个难题,C++ 拒绝编译代码。如果你希望一个包含引用成员的类支持赋值,你必须自己定义拷贝赋值运算符。

对于含有 const 成员的类,编译器会有类似的行为。改变 const 成员是不合法的,所以编译器隐式生成的赋值函数无法确定该如何对待它们。

如果基类将拷贝赋值运算符声明为 private,编译器拒绝为从它继承的派生类生成隐式拷贝赋值运算符。毕竟,编译器为派生类生成的拷贝赋值运算符也要处理其基类构件。

但是需要注意的是,含有引用成员和 const 成员对拷贝赋值运算符的约束对于拷贝构造函数是不适用的:

std::string newDog("Persephone");
NamedObject<int> p(newDog, 2);
NamedObject<int> p2(p);	//@ 调用编译器默认生成的拷贝构造函数
std::cout << p.nameValue << std::endl;
std::cout << p.objectValue << std::endl;
std::cout << p2.nameValue << std::endl;
std::cout << p2.objectValue << std::endl;

总结

  • 编译器可以隐式生成一个类的缺省构造函数,拷贝构造函数,拷贝赋值运算符和析构函数。
  • 默认生成的析构函数是非虚析构函数,除非该类继承自一个基类,基类中含有虚析构函数。
  • 拷贝构造函数与拷贝赋值运算符,都是简单地从源对象拷贝每一个非静态数据成员到目标对象。
  • 含有引用成员,const 成员的类,编译器不会默认生成拷贝赋值运算符,需要自己定义,但是这种约束对于拷贝构造函数则无限制。
  • 如果基类将拷贝赋值运算符声明为 private,编译器将不会为从它继承的派生类生成隐式拷贝赋值运算符。
posted @ 2020-01-02 09:28  刘-皇叔  阅读(238)  评论(0编辑  收藏  举报