拷贝构造函数是一种特殊的构造函数,它用于用同一类的另一个对象来初始化一个对象。简单地说,一个构造函数,它通过用之前创建的同一类的对象来初始化一个对象,就是拷贝构造函数。例如:
// 声明一个类 class Wall { private: double length; double height; public: // 用两个参数初始化变量的构造函数 Wall(double len, double hgt) { length = len; height = hgt; } // 用一个Wall对象作为参数的拷贝构造函数 // 复制obj参数的数据 Wall(Wall &obj) { length = obj.length; height = obj.height; } double calculateArea() { return length * height; } }; int main() { // 创建一个Wall类的对象,并用两个参数初始化数据成员 Wall wall1(10.5, 8.6); // 用wall1的内容复制到wall2 Wall wall2 = wall1; // 打印wall1和wall2的面积 cout << "Area of Wall 1: " << wall1.calculateArea() << endl; cout << "Area of Wall 2: " << wall2.calculateArea(); return 0; }
这里,我们创建了一个Wall类的对象wall1,并用两个参数(10.5和8.6)初始化了它的数据成员length和height。然后,我们用wall1的内容复制到了另一个Wall类的对象wall2。这就调用了拷贝构造函数Wall (Wall &obj),它把wall1的length和height复制到了wall2。最后,我们打印了wall1和wall2的面积,它们是相同的。
拷贝构造函数在以下情况下会被调用:
• 当一个对象以直接初始化或拷贝初始化的方式从同一类型的另一个对象初始化时(除非重载解析选择了更好的匹配或者调用被省略),包括:• 初始化:T a = b; 或 T a(b);,其中b是类型T;
• 函数参数传递:当一个对象以值传递方式传递给一个函数时,会调用拷贝构造函数来创建形参;
• 函数返回值:当一个函数以值返回方式返回一个对象时,会调用拷贝构造函数来创建返回值;
• 当编译器生成临时对象时,如果我们没有在类中定义拷贝构造函数,那么C++编译器会自动创建一个默认的拷贝构造函数,它会按照成员逐一复制(memberwise copy)或浅复制(shallow copy)的方式复制数据成员 。但是,在有些情况下,我们需要自己定义拷贝构造函数,以实现深复制(deep copy)或其他特殊功能。例如:
// 声明一个类 class String { private: char *s; int size; public: // 用字符串长度和字符指针初始化变量的构造函数 String(int n, char *str) { size = n; s = new char[size + 1]; strcpy(s, str); } // 显示字符串内容 void display() { cout << s << endl; } // 自定义拷贝构造函数 String(String &obj) { size = obj.size; s = new char[size + 1]; strcpy(s, obj.s); } }; int main() { // 创建一个String类的对象,并用字符串长度和字符指针初始化数据成员 String str1(6, "Hello"); // 显示str1的内容 str1.display(); // 用str1的内容复制到str2 String str2 = str1; // 显示str2的内容 str2.display(); }
这里,我们自定义了一个拷贝构造函数String (String &obj),它为新创建的对象分配了新的内存空间,并把obj的数据成员复制到了新对象中。这样,我们就实现了深复制,避免了两个对象共享同一块内存空间的问题。如果我们没有自定义拷贝构造函数,那么编译器生成的默认拷贝构造函数会导致两个对象的s指针指向同一个地址,这可能会引起内存泄漏或其他错误。
也就是说,如果我们不自己定义拷贝构造函数,那么编译器会为我们生成一个默认的拷贝构造函数,它的作用是将原对象的所有数据成员逐个复制到新对象中。但是,这样做有一个问题,就是如果数据成员中有指针类型的变量,那么复制的只是指针的值,也就是内存地址,而不是指针所指向的内容。这样一来,两个对象的指针变量就会指向同一块内存空间,这可能会导致以下问题:
- 如果我们修改了其中一个对象的指针所指向的内容,那么另一个对象的指针所指向的内容也会跟着改变,这可能会破坏对象的封装性和一致性。
- 如果我们删除了其中一个对象,那么它的指针所指向的内存空间也会被释放,但是另一个对象的指针还是指向那个已经被释放的地址,这就会造成悬空指针(dangling pointer),如果我们再试图访问或操作那个地址,就会发生内存错误或程序崩溃。
举个例子,假设我们有一个名为Student的类,它有两个数据成员,一个是int类型的变量id,用来存储学生的编号,另一个是char类型的指针name,用来存储学生的姓名。我们没有自己定义拷贝构造函数,而是使用编译器生成的默认拷贝构造函数。我们可以用以下代码来演示上述问题:
// 声明一个Student类 class Student { private: int id; char *name; public: // 用编号和姓名初始化变量的构造函数 Student(int n, char *str) { id = n; name = new char[strlen(str) + 1]; strcpy(name, str); } // 显示学生信息 void display() { cout << "ID: " << id << ", Name: " << name << endl; } // 析构函数 ~Student() { delete[] name; } }; int main() { // 创建一个Student类的对象,并用编号为1和姓名为"Tom"初始化数据成员 Student stu1(1, "Tom"); // 显示stu1的信息 stu1.display(); // 用stu1的内容复制到stu2 Student stu2 = stu1; // 显示stu2的信息 stu2.display(); // 修改stu2的姓名为"Jerry" strcpy(stu2.name, "Jerry"); // 显示stu1和stu2的信息 stu1.display(); stu2.display(); // 删除stu1 delete stu1; // 显示stu2的信息 stu2.display(); }
运行这段代码,我们可以看到以下输出:
ID: 1, Name: Tom ID: 1, Name: Tom ID: 1, Name: Jerry ID: 1, Name: Jerry Segmentation fault (core dumped)
从输出中可以看出:
- 当我们用stu1复制到stu2时,它们的id和name都被复制了过来。但是,复制的只是name指针的值,也就是内存地址,并没有为stu2分配一块新的内存空间来存储姓名。
- 当我们修改了stu2的姓名为"Jerry"时,由于stu1和stu2的name指针都指向同一块内存空间,所以stu1的姓名也被改成了"Jerry"。
- 当我们删除了stu1时,它的name指针所指向的内存空间也被释放了。但是stu2的name指针还是指向那个已经被释放的地址。当我们再试图显示stu2的信息时,就会发生内存错误或程序崩溃。
因此,为了避免这些问题,我们需要自己定义拷贝构造函数,并在其中实现深拷贝。深拷贝的意思是,不仅复制指针的值,还要为新对象分配一块新的内存空间,并将原对象的指针所指向的内容复制过来。这样,两个对象的指针就会指向不同的内存空间,互不影响。例如,我们可以在Student类中添加以下拷贝构造函数:
// 自定义拷贝构造函数 Student(Student &obj) { id = obj.id; name = new char[strlen(obj.name) + 1]; strcpy(name, obj.name); }
这样,当我们用stu1复制到stu2时,就会调用这个拷贝构造函数,为stu2分配一块新的内存空间,并将stu1的姓名复制过来。当我们修改或删除其中一个对象时,另一个对象不会受到影响。运行修改后的代码,我们可以看到以下输出:
ID: 1, Name: Tom ID: 1, Name: Tom ID: 1, Name: Tom ID: 1, Name: Jerry ID: 1, Name: Tom ID: 1, Name: Jerry
从输出中可以看出:
- 当我们用stu1复制到stu2时,它们的id和name都被复制了过来。但是,复制的不仅是name指针的值,还有name指针所指向的内容。因此,stu2有了自己的一块内存空间来存储姓名。
- 当我们修改了stu2的姓名为"Jerry"时,由于stu1和stu2的name指针指向不同的内存空间,所以stu1的姓名没有被改变。
- 当我们删除了stu1时,它的name指针所指向的内存空间也被释放了。但是stu2的name指针还是指向自己的内存空间。当我们再显示stu2的信息时,就不会发生内存错误或程序崩溃。