一、复制构造函数的介绍
在一般的数据类型中, 我们经常会用一个变量来初始化另一个变量, 例如:
int a = 10; int b = a;
使用a变量来初始化b变量, 同样, 对于类创建的对象也可以用这种方式使用一个对象去初始化另一个对象。例如还在上篇中介绍的 Point 类中, 使用一个对象去初始化另一个对象:
//Point.h #include <iostream> class Point { public: Point(int x = 0, int y = 0):xPos(x), yPos(y) {} void printPoint() { std::cout<<"xPos = "<< xPos <<std::endl; std::cout<<"yPOs = "<< yPos <<std::endl; } private: int xPos; int yPos; };
#include "Point.h" int main() { Point M(10, 20); Point N = M; //使用对象 M 初始化对象 N N.printPoint(); return 0; }
编译运行的结果:
xPos = 10 yPOs = 20 Process returned 0 (0x0) execution time : 0.462 s Press any key to continue.
上面代码使用Point类创建了一个对象 M,初始化xPos, yPos为 10和20, 在 Point N = M; 这行中创建了一个对象 N 并且已经初始化好的 M 来初始化它, 所以当对象 N 在调用 printPoint 时输出的结果与 M 的xPos, yPos值相同。
语句 Point N = M; 也可以写成 Point N(M);
的形式。在执行该句时就相当于将 M 中每个数据成员的值赋值到对象 N 中相对应的成员数据中。当然, 这只是表面现象,
实际上系统调用了一个复制构造函数来完成这部分的动作, 当类中没有显式定义该复制构造函数时, 编译器会默认为其生成一个默认复制构造函数,
也称拷贝构造函数, 该函数的原型如下:
Point::Point( const Point & );
也可以将该复制构造函数看做是一个普通的构造函数, 只不过是函数的形参不同罢了, 其复制构造函数的形参为本类对象的引用类型。
二、默认复制构造函数的不足
尽管有默认的复制构造函数来解决一般对象与对象之间的初始化问题, 但是在有些情况下我们必须手动显式的去定义复制构造函数, 例如:
#include <iostream> #include <cstring> using namespace std; class Book { public: Book( const char *name ) { bookName = new char [strlen(name)+1]; //使用 new 申请 strlen(name)+1 大小的空间 strcpy(bookName, name); } ~Book() { delete []bookName; } //释放申请的空间 void showName(){ cout<<"Book name: " << bookName << endl; } private: char *bookName; }; int main() { Book CPP("C++ Primer"); Book T(CPP); //使用 CPP 初始化对象 T CPP.showName(); CPP.~Book(); //手动释放对象CPP所申请的空间 T.showName(); return 0; }
编译运行的结果:
Book name: C++ Primer Book name: Process returned 0 (0x0) execution time : 0.281 s Press any key to continue.
按照前面的思 路, 使用 CPP 对象对 T 对象进行初始化后, 那么 T 对象的 bookName 属性理论上来说也是 "C++ Primer", 但是从输出结果来看在输出 CPP 对象的 bookName 属性时是正常的, 而 T 对象的 bookName 输出有问题, 正确的情况下应该也是 "C++ Primer", 不过此时输出的却是空白。
这正是构造函数的一点不足之处, 造成这种现象的原因在于 CPP.~Book(); 这行, 还原下该默认复制构造函数的实现:
Book( const Book &obj ) { bookName = obj.bookName; }
可以看到, 实际上当用 CPP 对象来初始化 T 对象时, 默认复制构造函数只是简单的将 CPP 对象的 bookName 赋值给 T 对象的 bookName, 换句话说, 也就是只是将 CPP 对象的 bookName 指向的空间地址赋值给 T 的 bookName, 这样一来, T 对象的 bookName 和 CPP 对象的 bookName 就是指向同一处内存单元, 当 CPP 对象调用析构函数后, CPP 的 bookName 所指向的内存单元就会被释放, 由于 T 对象 bookName 与 CPP 对象的 bookName 指向的是同一处内存, 所以此时 T 对象的 bookName 指向的内存就变成了一处不可用的非法内容(因为已经释放), 所以在指向的内存被释放的情况下进行输出势必会造成了输出的错误。
一般来说, 当类中含有指针型的数据成员、需要使用动态内存的, 最好手动显式定义复制构造函数来避免该问题。
三、显式定义复制构造函数
显式定义复制构造函数的步骤非常简单, 只要记得函数的参数是 本类成员的引用 就行, 虽然也可以通过指针来实现, 但是不推荐这样做, 指针在某种程度上来说要比引用危险。显式定义复制构造函数解决上例中的问题:
#include <iostream> #include <cstring> using namespace std; class Book { public: Book( const char *name ) { bookName = new char [strlen(name)+1]; strcpy(bookName, name); } Book( const Book &obj ) //显式定义复制构造函数 { bookName = new char [strlen(obj.bookName)+1]; //调用复制构造函数时再次申请一处新的空间 strcpy(bookName, obj.bookName); } ~Book() { delete []bookName; } //释放申请的空间 void showName(){ cout<<"Book name: " << bookName << endl; } private: char *bookName; }; int main() { Book CPP("C++ Primer"); Book T = CPP; //使用 CPP 初始化对象 T CPP.showName(); CPP.~Book(); //手动释放对象CPP所申请的空间 T.showName(); return 0; }
编译运行的结果:
Book name: C++ Primer Book name: C++ Primer Process returned 0 (0x0) execution time : 0.281 s Press any key to continue.
在该示例中我们显式定义了复制构造函数来代替默认复制构造函数, 在该复制构造函数的函数体内, 不是再直接将源对象所申请空间的地址赋值给被初始化的对象, 而是自己独立申请一处内存后再将源对象的属性复制过来, 此时 CPP 对象的 bookName 与 T 对象的 bookName 就是指向两处不同的内存单元, 这样即便是源对象 CPP 被销毁后被初始化的对象 T 也不会再受到影响。