C++自学笔记_复制构造函数_《C++ Primer》
在内置数据类型中,一般可以用一个变量初始化另一个变量。同样,对于类类型的对象,也可以用一个对象初始化另一个对象,编译器会合成一个复制构造函数。
#include <iostream> using namespace std; class Point{ public: Point(int x=0,int y=0):xPos(x),yPos(y){} void printPoint(){ cout<<"xPos:"<<xPos<<endl; cout<<"yPos:"<<yPos<<endl; } private: int xPos; int yPos; }; int main() { Point M(10,20); Point N=M; N.printPoint(); //使用对象M初始化对象N return 0; }
编译运行的结果:
xPos:10 yPos:20 Process returned 0 (0x0) execution time : 0.191 s Press any key to continue.
语句 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<<"bookName="<<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; }
编译运行后的结果:
bookName=C++ Primer bookName=(U Process returned 0 (0x0) execution time : 0.317 s Press any key to continue.
按照前面的思路, 使用 CPP 对象对 T 对象进行初始化后, 那么 T 对象的 bookName 属性理论上来说也是 "C++ Primer", 但是从输出结果来看在输出 CPP 对象的 bookName 属性时是正常的, 而 T 对象的 bookName 输出有问题, 正确的情况下应该也是 "C++ Primer", 不过此时输出的却是乱码。
这正是默认复制构造函数的不足之处:还原下编译器生成的默认复制构造函数的实现:
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]; //使用new申请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<<"bookName="<<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; }
编译运行结果:
bookName=C++ Primer bookName=C++ Primer Process returned 0 (0x0) execution time : 0.315 s Press any key to continue.
在该示例中我们显式定义了复制构造函数来代替默认复制构造函数, 在该复制构造函数的函数体内, 不是再直接将源对象所申请空间的地址赋值给被初始化的对象, 而是自己独立申请一处内存后再将源对象的属性复制过来, 此时 CPP 对象的 bookName 与 T 对象的 bookName 就是指向两处不同的内存单元, 这样即便是源对象 CPP 被销毁后被初始化的对象 T 也不会再受到影响。