C++的拷贝构造函数以及深浅拷贝问题
引言
在C++中我们可能会使用一个已初始化的类来初始化另一个未初始化的类,这时编译器会怎样操作呢?
#include <iostream> #include <string.h> using namespace std; class Student { public: Student(int num, const char *name, int age) { cout << "Student int char * int " << endl; m_num = num; int len = strlen(name); m_name = new char[len + 1]; strcpy(m_name, name); m_age = age; } ~Student() { cout << "~Student" << endl; delete[] m_name; } private: int m_num; char *m_name; int m_age; }; int main() { Student s(1, "zhnagsan", 18); //使用带参构造函数初始化对象 Student s1 = s; //使用已经初始化的对象初始化新对象 return 0; }
运行结果:
根据运行结果可以推知在程序运行过程中调用了一次构造函数和两次析构函数,但我们在主函数内创建了两个对象,此时却只构造了一个对象。原因是当我们用已存在的对象初始化新对象时编译器会调用拷贝构造函数
this指针
在介绍拷贝构造函数之前先介绍一下前置知识,以免大家对后续内容产生疑问。this指针是C++类成员函数中隐含的特殊指针,指向当前调用该函数的对象实例
#include <iostream> #include <string.h> using namespace std; //创建类 class Test { public: int getNum() { return m_num; } void setNum(int num) { m_num = num; } int m_num; }; int main() { Test A; //输出类的大小 cout << sizeof(A) << endl; return 0; }
输出结果
由输出结果不难看出定义的类对象的大小为4字节,此时不免产生疑问,难道类内的方法(函数)不占用内存吗?,当然不是,在类中的成员函数(包括构造函数,析构函数等)不存储在对象的内存空间中,而是被编译后存放在公共代码区。因此同一类的所有对象共享同一份成员函数代码,无需为每个对象重复分配内存。
大致结构如下:
此时又出现一个问题,既然不同类对象共享类内的方法,那当我们使用不同类对象调用同一方法时编译器如何区分呢?此时就用到了this指针,当对象调用成员函数时,编译器会自动传递一个隐藏的this指针作为函数参数,其值为当前调用对象的地址。
-
作用:告诉公共代码区的成员函数“当前操作的是哪个对象的数据”,确保函数能准确访问对象的成员变量。
class Student { private: string name; public: void setName(string name) { //参数与成员变量重名时编译器会隐式调用this指针 this->name = name; //this->name指代成员变量,name 指代参数 } };
公共代码区 | this指针 |
---|---|
存储成员函数的二进制代码,实现代码复用 | 传递对象地址,标识当前操作的对象实例 |
所有对象共享同一份函数代码 | 确保函数访问正确的对象数据 |
总结
this指针时对象“自我认识”的核心,解决了成员命名冲突,对象自引用,链式调用等问题,是C++面向对象编程中连接对象与成员函数的桥梁。
运算符重载
运算符重载是C++的核心特性之一,允许自定义类型(如类,结构体)重载已有的运算符(如+,-,*,/等),使其支持类似内置的运算语法,增强代码可读性和直观性。
-
作用:可以直观理解为为一个符号绑定操作,可以实现简化操作的功能,原本复杂的功能,现在只要调用一个符号就能轻松实现
-
语法:返回类型
operator
运算符(参数列表){..........}
class String { private: char* str; public: //重载 = String & operator=(const String & other) { if (this == &other) { return *this; //防止自赋值 } delete[] str; //释放原有内存 str = new char[strlen(other.str) + 1]; strcpy(str, other.str); return *this; //支持链式赋值(a = b = c) } };
上述代码就重定义了=运算符,将深拷贝与=运算符绑定在一起,当我们想要实现深拷贝操作时只需调用=运算符即可。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,用于用已存在的对象初始化新对象,是对象“值传递”的基础
定义
-
形式:类名(
const
类名& 源对象)必须使用引用传递,通常加
const
确保源对象不被修改 -
默认行为:若未显示定义,编译器会生成默认拷贝构造函数,执行浅拷贝(成员逐一复制)
#include <iostream> // 引入标准输入输出流库 #include <string.h> // 引入字符串处理库 using namespace std; // 使用标准命名空间 // 定义学生类 class Student { public: // 构造函数:用学号、姓名、年龄初始化对象 Student(int num, const char *name, int age) { cout << "Student int char * int " << endl; m_num = num; int len = strlen(name); // 计算姓名长度 m_name = new char[len + 1]; // 动态分配姓名空间 strcpy(m_name, name); // 拷贝姓名内容 m_age = age; } Student() { cout << "Student init" << endl; } // 赋值运算符重载:用已有对象赋值给当前对象(浅拷贝) Student & operator=(const Student &other) { cout << "operator = Student" << endl; this->m_num = other.m_num; this->m_name = other.m_name; // 浅拷贝,两个对象指向同一块内存 this->m_age = other.m_age; return *this; } // 拷贝构造函数:用已有对象初始化新对象(浅拷贝,指针直接赋值) Student(const Student &other) { cout << "Student copy Student" << endl; this->m_num = other.m_num; this->m_name = other.m_name; this->m_age = other.m_age; } // 析构函数:释放动态分配的姓名空间 ~Student() { cout << "~Student" << endl; delete[] m_name; } private: int m_num; // 学号 char *m_name; // 姓名(动态分配) int m_age; // 年龄 }; int main() { Student stu1(1, "zhnagsan", 18); // 使用带参构造函数初始化对象 Student stu2; stu2 = stu1; // 使用赋值运算符重载,将stu1赋值给stu2 stu2.operator=(stu1); // 效果与直接使用=一致 return 0; // 程序结束,自动调用析构函数 }
运行结果:
上述代码便可以实现对类类型对象的浅拷贝,即将类内成员逐一复制,但此时又会引发另一个问题。
浅拷贝问题及解决方案
问题成因
拷贝构造函数的代码虽然在Visual Stdio Code中能够正常运行,但是当我们在Visual Stdio中再次运行相同代码时便会引发报错。
系统默认生成的拷贝构造函数和我们在拷贝构造函数中的代码都是浅拷贝,这种拷贝方式会引发报错的原因是拷贝构造函数和赋值运算符重载都只是简单的把m_name指针赋值给新对象,这样会导致多个对象的m_name指针指向同一块内存:
当程序结束时析构函数会重复释放同一块内存,这样便会引发报错。
解决方案
既然如此就只能用深拷贝来代替浅拷贝
// 深拷贝构造函数:用已有对象初始化新对象,分配新空间并拷贝内容 Student(const Student &other) { cout << "Student copy Student (deep copy)" << endl; this->m_num = other.m_num; if (other.m_name) { int len = strlen(other.m_name); this->m_name = new char[len + 1]; strcpy(this->m_name, other.m_name); } else { this->m_name = nullptr; } this->m_age = other.m_age; }
深拷贝就是为新的对象分配新的空间这样在析构时就不会报错了
总结
拷贝构造函数是C++对象复刻的核心机制,默认浅拷贝适用于简单类,而含动态资源的类必须自定义深拷贝以避免内存问题