Effective C++手册Item 5: 了解 C++ 为你偷偷地加上和调用了什么函数

Item 5: 了解 C++ 为你偷偷地加上和调用了什么函数

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

一个 empty class(空类)什么时候将不再是 empty class(空类)?答案是当 C++ 搞定了它。如果你自己不声明一个 copy constructor(拷贝构造函数),一个 copy assignment operator(拷贝赋值运算符)和一个 destructor(析构函数),编译器就会为这些东西声明一个它自己的版本。此外,如果你自己根本没有声明 constructor(构造函数),编译器就会为你声明一个 default constructor(缺省构造函数)。所有这些函数都被声明为 public 和 inline(参见 Item 30)。作为结果,如果你写

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

假设编译器为你写了这些函数,那么它们做些什么呢?default constructor(缺省构造函数)和 destructor(析构函数)主要是给编译器一个地方放置 "behind the scenes" code(“幕后”代码)的,诸如 base classes(基类)和 non-static data members(非静态数据成员)的 constructors(构造函数)和 destructor(析构函数)的调用。注意,生成的 destructor(析构函数)是 non-virtual(非虚拟)的(参见 Item 7),除非它所在的 class(类)是从一个 base class(基类)继承而来,而 base class(基类)自己声明了一个 virtual destructor(虚拟析构函数)(这种情况下,函数的 virtualness(虚拟性)来自 base class(基类))。

对于 copy constructor(拷贝构造函数)和 copy assignment operator(拷贝赋值运算符),compiler-generated versions(编译器生成版本)只是简单地从 source object(源对象)拷贝每一个 non-static data member(非静态数据成员)到 target object(目标对象)。例如,考虑一个 NamedObject template(模板),它允许你将名字和类型为 T 的 objects(对象)联系起来的:

template
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 中声明了一个 constructors(构造函数),编译器就不会再生成一个 default constructor(缺省构造函数)。这一点很重要,它意味着如果你小心地设计一个 class(类),使它需要 constructor arguments(构造函数参数),你就不必顾虑编译器会不顾你的决定,轻率地增加一个不需要参数的 constructors(构造函数)。

NamedObject 既没有声明 copy constructor(拷贝构造函数)也没有声明 copy assignment operator(拷贝赋值运算符),所以编译器将生成这些函数(如果需要它们的话)。看,这就是 copy constructor(拷贝构造函数)的用法:

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

编译器生成的 copy constructor(拷贝构造函数)一定会用 no1.nameValue 和 no1.objectValue 分别初始化 no2.nameValue 和 no2.objectValue。nameValue 的类型是 string,标准 string 类型有一个 copy constructor(拷贝构造函数),所以将通过以 no1.nameValue 作为参数调用 string 的 copy constructor(拷贝构造函数)初始化 no2.nameValue。而另一方面,NamedObject::objectValue 的类型是 int(因为在这个 template instantiation(模板实例化)中 T 是 int),而 int 是 built-in type(内建类型),所以将通过拷贝 no1.objectValue 的每一个二进制位初始化 no2.objectValue。

编译器为 NamedObject 生成的 copy assignment operator(拷贝赋值运算符)本质上也会有同样的行为,但是,通常情况下,只有在结果代码合法而且有一个合理的可理解的巧合时,compiler-generated(编译器生成)的 copy assignment operator(拷贝赋值运算符)才会有我所描述的行为方式。如果这两项检测中的任一项失败了,编译器将拒绝为你的 class(类)生成一个 operator=。

例如,假设 NamedObject 如下定义,nameValue 是一个 reference to a string(引向一个字符串的引用),而 objectValue 是一个 const T:

template
class NamedObject {
public:
  // this ctor no longer takes a const name, because nameValue
  // is now a reference-to-non-const string. The char* constructor
  // is gone, because we must have a string to refer to.
  NamedObject(std::string& name, const T& value);
  ...                               // as above, 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 p(newDog, 2);               // when I originally wrote this, our
                                             // dog Persephone was about to
                                             // have her second birthday
NamedObject s(oldDog, 36);              // the family dog Satch (from my
                                             // childhood) would be 36 if she
                                             // were still alive
p = s;                                       // what should happen to
                                             // the data members in p?

assignment(赋值)之前,p.nameValue 和 s.nameValue 都引向 string objects(对象),虽然并非同一个。那个 assignment(赋值)对 p.nameValue 产生了什么影响呢?assignment(赋值)之后,p.nameValue 所引向的 string 是否就是 s.nameValue 所引向的那一个呢,也就是说,reference(引用)本身被改变了?如果是这样,就违反了常规,因为 C++ 并没有提供使一个 reference(引用)引向另一个 objects(对象)的方法。换一种思路,是不是 p.nameValue 所引向的那个 string objects(对象)被改变了,从而影响了其他 objects(对象)—— pointers(指针)或 references(引用)持续指向的那个 string,也就是,赋值中并没有直接涉及到的对象?这是 compiler-generated(编译器生成)的 copy assignment operator(拷贝赋值运算符)应该做的事情吗?

面对这个难题,C++ 拒绝编译代码。如果你希望一个包含 reference member(引用成员)的 class(类)支持 assignment(赋值),你必须自己定义 copy assignment operator(拷贝赋值运算符)。对于含有 const members(const 成员)的 classes(类),编译器会有类似的行为(就像上面那个改变后的 class(类)中的 objectValue)。改变 const members(const 成员)是不合法的,所以编译器隐式生成的 assignment function(赋值函数)无法确定该如何对待它们。最后,如果 base classes(基类)将 copy assignment operator(拷贝赋值运算符)声明为 private,编译器拒绝为从它继承的 derived classes(派生类)生成 implicit copy assignment operators(隐式拷贝赋值运算符)。毕竟,编译器为派生类生成的 copy assignment operator(拷贝赋值运算符)也要处理其 base class parts(基类构件)(参见 Item 12),但如果这样做,它们当然无法调用那些 derived classes(派生类)无权调用的 member functions(成员函数)。

Things to Remember

  • 编译器可以隐式生成一个 class(类)的 default constructor(缺省构造函数),copy constructor(拷贝构造函数),copy assignment operator(拷贝赋值运算符)和 destructor(析构函数)。