Fork me on GitHub

读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

1 编译器会默认生成哪些函数 

什么时候空类不再是一个空类?答案是用c++处理的空类。如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数拷贝赋值运算符和析构函数,如果你一个构造函数都没有声明,编译器同样会为你声明一个默认拷贝构造函数。这些所有的函数会是public和inline的(Item30)。因此,如果你写了下面的类:

1 class Empty{};

本质上来说和下面的类是一样的:

 1 class Empty {
 2 
 3 public:
 4 
 5 Empty() { ... } // default constructor
 6 
 7 Empty(const Empty& rhs) { ... } // copy constructor
 8 
 9 ~Empty() { ... } // destructor — see below
10 
11 // for whether it’s virtual
12 
13 Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
14 
15 };

 这些函数只有在被需要的时候才会生成。程序中需要这些函数是很平常的事。下面的代码会导致相应的函数被生成:

1 Empty e1; // default constructor; destructor
2 
3 Empty e2(e1); // copy constructor
4 
5 e2 = e1; // copy assignment operator

 

2 默认生成的函数会做些什么?

考虑到编译器会为你生成函数,这些函数会做些什么?默认拷贝构造函数和析构函数会给编译器腾出一个地方用来放“藏在幕后的代码”,像基类和非静态数据成员的构造函数和析构函数的调用。注意生成的默认析构函数不是虚函数(Item7),除非生成默认析构函数的类继承自一个声明了虚析构函数的基类(这样默认析构函数的虚或者非虚继承自基类)。

2.1 默认拷贝构造函数

对于拷贝构造函数和拷贝赋值运算符来说,编译器生成的版本只是将源对象的非静态数据成员简单的拷贝到目标对象上。例如,考虑一个NamedObject 模板类允许你同类型T的对象进行关联。

 1 template<typename T>
 2 
 3 class NamedObject {
 4 
 5 public:
 6 
 7 NamedObject(const char *name, const T& value);
 8 
 9 NamedObject(const std::string& name, const T& value);
10 
11 ...
12 
13 private:
14 
15 std::string nameValue;
16 
17 T objectValue;
18 
19 };

因为在NamedObject中声明了一个构造函数,编译器不再为其生成默认构造函数。这很重要,如果你仔细的设计了一个需要带参构造函数的类,你不必担心编译器会添加一个不带参数的构造函数来覆盖你的版本。

NamedObject既没有声明拷贝构造函数也没有声明拷贝赋值运算符,所以编译器会产生这些函数(如果它们被需要)。看下面的例子,拷贝构造函数这么使用:

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

编译器生成的拷贝构造函数必须分别用no1.nameValue和no1.objectValue初始化no2.nameValue和no2.objectValue。nameValue的类型是string,标准的string类型有个拷贝构造函数,于是no2.nameValue会通过调用string的拷贝构造函数来进行初始化,构造函数用no1.nameValue作为参数。另外,NamedObject<int>::objectValue的类型是int,于是no2.objectValue会通过拷贝no1.objectValue的bits来进行初始化。

2.2 默认拷贝赋值运算符

编译器为NamedObject<int>生成的拷贝赋值运算符的行为同拷贝构造函数基本上是一样的。但是一般来说,编译器生成的拷贝赋值运算符只有在生成的代码合法,并且有合理的机会证明其有意义,行为同拷贝构造函数才是一样的。如果不满上述任何一个条件,编译器都会拒绝为你的类生成operator=。

举个例子:考虑NamedObject像下面这样定义,nameValue是指向string的引用,objectValue是const T.

 1 template<typename T>
 2 
 3 class NamedObject {
 4 
 5 public:
 6 
 7 // this ctor no longer takes a const name, because nameValue
 8 
 9 // is now a reference-to-non-const string. The char* constructor
10 
11 // is gone, because we must have a string to refer to.
12 
13 NamedObject(std::string& name, const T& value);
14 
15 ... // as above, assume no
16 
17 // operator= is declared
18 
19 private:
20 
21 std::string& nameValue; // this is now a reference
22 
23 const T objectValue; // this is now const
24 
25 };

 考虑下面会发生什么:

 1 std::string newDog("Persephone");
 2 
 3 std::string oldDog("Satch");
 4 
 5 NamedObject<int> p(newDog, 2); // when I originally wrote this, our
 6 
 7                                  // dog Persephone was about to
 8 
 9                                  // have her second birthday
10 
11 NamedObject<int> s(oldDog, 36); // the family dog Satch (from my
12 
13                                 // childhood) would be 36 if she
14 
15                                 // were still alive
16 
17 p = s; // what should happen to the data members in p?

在赋值之前,p.nameValue和s.nameValue都会指向string对象,虽然不是同一个。赋值如何影响p.nameValue?在赋值之后,p.nameValue应该指向s.nameValue所指向的那个string么?也就是,引用本身会改变么?如果是这样,就开辟了一块新天地,因为c++没有提供使引用指向另一个对象的方法。或者,p.nameValue指向的string对象应该被修改?这样就会影响其他对象,这些对象持有指向这个被修改的string的指针或者引用。这是编译器生成的拷贝赋值运算符应该做得?

面对这个难题,c++拒绝编译代码。如果你想在一个类中支持含有引用成员的拷贝赋值运算符,你必须自己定义一个拷贝赋值运算符。对于包含const成员的类,编译器的行为也是类似的。修改const成员是不合法的,因此当一个默认拷贝赋值运算符生成时,编译器对如何处理它们是不确定的。最后,如果在基类中将拷贝赋值运算符声明成private,编译器拒绝在派生类中生成拷贝赋值运算符。毕竟编译器为派生类生成的拷贝赋值运算符需要能够处理基类的部分,在这种情况下,它们当然不能够触发派生类没有权限调用的函数。

posted @ 2017-02-12 12:25  HarlanC  阅读(1365)  评论(0编辑  收藏  举报