如果你自己没有声明,编译器就会为它声明编译器版本的拷贝构造函数,一个拷贝赋值运算符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个默认构造函数。所有这些函数都是public且inline。因此,如果你写下:
class Empty {};
就好像你写下了这样的代码:
class Empty {
public:
Empty() {...} // default constructor
Empty(const Empty& rhs) {...} // copy constructor (default)
~Empty() {...} // destructor (default)
Empty& operator=(const Empty& rhs) {...} // copy assignment (default)
};
只有当这些函数被调用时,它们才会被编译器创建出来。下面的代码造成上述每个函数被编译器产生:
Empty e1; // default constructor
// destructor
Empty e2(e2); // copy constructor
e2 = e2; // operator=()
默认构造函数和析构函数主要是给编译器一个地方用来放置幕后的代码,像是调用基类和非静态成员变量的构造函数和析构函数。注意,编译器产生的析构函数是个non-virtual,除非这个基类自身声明有virtual析构函数。
至于拷贝构造函数和拷贝赋值运算符,编译器创建的版本只是单纯地将源对象地每个non-static成员变量拷贝到目标对象。考虑一个NamedObject模板,它允许你将一个个名称和类型为T的对象产生关联:
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没有声明拷贝构造函数和拷贝赋值运算符,所以编译器会为它创建那些函数。仙子啊,看看拷贝构造函数的用法:
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1); // copy constructor
编译器生成的拷贝构造函数必须以 no1.nameValue
和 no1.objectValue
作为初值设定 no2.nameValue
和 no2.objectValue
。nameValue的类型是string,而标准string有个拷贝构造函数,所以 no2.objectValue
的初始化方式是调用string的拷贝构造函数并以 no1.nameValue
为参数。另一个成员的类型是int,是个内置类型,所以会以拷贝 no1.objectValue
的每一个bits来完成初始化。
编译器为 NamedObject<int>
生成的拷贝赋值运算符,其行为基本上与拷贝构造函数一致。但一般而言只有当生成的代码合法且具有适当机会证明它有意义,其表现才会如前所述。万一有一个条件不符合,编译器会拒绝为class生成 operator=
。
假设NamedObject定义如下,其中nameValue是个 std::string&
,objectValue是个 const T
:
template <typename T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value);
...
private:
std::string& nameValue;
const T objectValue;
};
现在考虑下面会发生什么事:
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s; // 现在p的成员变量会发生什么事?
赋值之后 p.nameValue
应该指向 s.nameValue
所指向的那个string吗?也就是说引用自身可被改动吗?这当然不允许。编译器生成的拷贝赋值运算符究竟该怎么做呢?
面对这个难题,C++的响应是拒绝编译那一行赋值操作。如果你打算在一个内含引用成员的class内支持赋值操作,你必须自定义拷贝赋值运算符。面对内含const成员的class,编译器的反应也一样。还有最后一种情况,如果某个基类将拷贝赋值运算符声明为private,编译器将拒绝为其派生类生成拷贝赋值运算符。
请记住
- 编译器可以暗自为class创建默认构造函数、拷贝构造函数、拷贝赋值运算符、以及析构函数。