转自:http://www.cs.nyu.edu/~xiaojian/bookmark/local/copyCplus.pdf
我们知道如果程序员不写构造函数,编译器会自动提供一个无参构造函数。这个默认的构造函数在会调用成员变量的默认构造函数,从而完成对象的初始化。若要覆盖默认构造函数,我们只需提供一个自定义的构造函数即可。
除默认构造函数外,编译器还会提供另一种构造函数:拷贝构造函数。当需要根据一个已有的对象来构造另一个对象时,拷贝构造函数就会被调用。例如我们有一个包含字符串的类,mystring:
class mystring { public: mystring(const char* s = ""); ~mystring(); ... private: int length; char *str; }; 在mystring这个例子中,我们假定构造函数会为字符串分配空间,析构函数则会释放这些空间。在像下面这样创建一个新的mystring对象时,拷贝构造函数会被调用:
mystring me("Geraldo"); mystring clone = me; // copy constructor gets called.拷贝构造函数在以传值的方式进行参数传递时或以传值方式返回一个值时会被调用。现在有下面这个openfile方法: static void openFile(mystring filename) { // Convert the object to a character string, open a stream... }
我们可以这样调用这个方法: mystring name(“flights.txt”); openFile(name);
在将name对象作为参数进行传递时,拷贝构造函数会被调用,来根据name来生成一个openFile中的mystring类型参数对象。因为我们没有在mystring中提供一个拷贝构造函数,因此默认拷贝构造函数会被调用。
默认拷贝构造函数
默认的拷贝构造函数是通过对每个字段进行拷贝实现的。在mystring中,默认拷贝构造函数会拷贝int型的length字段和char*型的str指针。这种方式只会实现指针的复制,指针指向的字符串内存并不会被复制,只是多了一个指向这块内存的指针而已。这种方式叫做shallow copy,内存的实际布局如下图所示:这种方式存在共享内存的问题。如果其中的任一个对象修改了这段内存的内容,那么另一个对象将在不知情的情况下受到影响。更糟糕的是,如果一个对象调用析构函数,那么这段内存将被回收,另一个对象的指针就成了“脏指针”。
真正所需:The Deep Copy
避免shallow copy的负面影响的一个有效方法是使用Deep Copy。使用这种方式,我们拷贝的不仅仅是指向字符串的指针,字符串本身也会被拷贝,其效果如下图所示:
拷贝构造函数的声明
为了给mystring提供一个deep copy版本的拷贝构造函数,我们需要自己去声明自己的拷贝构造函数。在拷贝构造函数中,参数类型为指向同类对象的引用,并且没有返回值类型。例如在mystring中,我们将这样声明拷贝构造函数:
class mystring { public: mystring(const char* s = ““); mystring(const mystring& s); ... };
mystring::mystring(const mystring& s) { length = s.length; str = new char[length + 1]; strcpy(str, s.str); }
拷贝构造函数的局限
在进行对象间的赋值操作时,拷贝构造函数不会被调用。例如在下面的例子中,尽管我们提供了Deep Copy的拷贝构造函数,但在赋值操作中这个拷贝构造函数不会被调用,使用的仍是shallow copy。mystring betty(“Betty Rubble”); // Initializes string to “Betty Rubble” mystring bettyClone; // Initializes string to empty string bettyClone = betty; 这是因为在这里,调用的是“=”运算符而不是拷贝构造函数。“=”运算符默认情况下使用的是shallow copy,不过C++中有运算符重载机制,我们可以将“=”重载为使用deep copy的赋值运算符。
这里顺便提一下,如果你不想其他人通过拷贝构造函数的方式来拷贝你的对象,那么你可以将拷贝构造函数声明为private的(从而使默认拷贝构造函数失效),而且不去实现这个函数,那么编译器将阻止对这个对象的使用传值的方式进行传参等需要使用拷贝构造函数的地方。=
赋值
我们可以对“=”进行运算符重载。赋值运算符必须被定义为一个成员函数,从而保证运算符左边为一个对象。每个类中都定义了“=”运算符,默认的“=”运算符使用的是shallow copy的方式。你可以将“=”声明为private并不提供实现,从而阻止对象到对象间的赋值操作。“=”运算符与拷贝构造函数的一个重要区别在于“=”运算符会释放已有的一些资源,如下面的代码所示:
const mystring& operator=(const mystring& rhs) { if (this != &rhs) { delete[] this->str; // donate back useless memory this->str = new char[strlen(rhs.str) + 1]; // allocate new memory strcpy(this->str, rhs.str); // copy characters this->length = rhs.length; // copy length } return *this; // return self-reference so cascaded assignment works }
总结
1. 当新对象还不存在,需要调用构造函数时,将使用拷贝构造函数,包括:- 传值传参
- 传值返回值
- 使用对象初始化
- 对象 -- 对象间的赋值
- 我认为这是由拷贝构造函数的性质决定的。拷贝构造函数本质上是一个构造函数,只有在可以使用构造函数的时候才能被调用。