C++:3.构造、拷贝构造、赋值重载与资源管理
2026/9/6 0:19:23 网站建设 项目流程

一、复习

  1. 析构函数
    不同对象的析构时机:
  • 栈对象:出函数作用域 } 时自动析构
  • 全局对象:程序运行结束时析构
  • 堆对象:手动调用delete 时析构
  1. this指针
  • 类的普通成员方法,编译器编译后,都会增加一个形参变量 类类型 *this
  • 对象调用方法的时候,实际上调用的是全局方法,把对象的地址作为实参
    传入,this指针接收
  • 成员方法里面,访问的成员变量或者其它的成员方法,前面默认都会加上this->
  • 一个类定义的很多对象,每个对象都有自己的成员变量,但是共享一套成员方法,
    在成员方法里面,有个this指针形参变量,专门指向调用该方法的对象,读写该对象的成员变量

二、 C++类深度剖析:构造、拷贝构造、赋值重载与资源管理

一、C++类四大默认函数概览

当你一个都不写,编译器会隐式生成:

  1. 默认构造
  2. 拷贝构造函数
  3. 赋值运算符重载 operator=
  4. 析构函数

默认版本全部为浅拷贝!只要类管理堆资源,必须自己实现深拷贝。

基础类框架

#include<iostream>#include<cstring>usingnamespacestd;classPerson{public:// 默认构造Person(){cout<<"call Person() | this = "<<this<<endl;name=nullptr;}// 有参构造Person(constchar*name,intage,charsex){cout<<"call Person(const char*, int, char) | this = "<<this<<endl;this->name=newchar[strlen(name)+1];strcpy(this->name,name);this->age=age;this->sex=sex;}// 拷贝构造函数:深拷贝实现Person(constPerson&rhs){cout<<"call Person(const Person& rhs) 源:"<<&rhs<<" -> 新对象:"<<this<<endl;// 重新开辟堆内存,复制内容(深拷贝核心)this->name=newchar[strlen(rhs.name)+1];strcpy(this->name,rhs.name);this->age=rhs.age;this->sex=rhs.sex;}// 赋值运算符重载,深拷贝Person&operator=(constPerson&rhs){cout<<"call operator=(const Person& rhs) 源:"<<&rhs<<" => 当前对象:"<<this<<endl;// 1. 防止自赋值 per1 = per1;if(this==&rhs){return*this;}// 2. 释放当前对象旧堆资源,避免内存泄漏delete[]this->name;// 3. 分配新内存,拷贝数据this->name=newchar[strlen(rhs.name)+1];strcpy(this->name,rhs.name);this->age=rhs.age;this->sex=rhs.sex;// 返回*this,支持链式赋值 a=b=creturn*this;}// 析构函数~Person(){cout<<"call ~Person() | this = "<<this<<endl;delete[]name;name=nullptr;}constchar*getName(){returnname;}intgetAge(){returnage;}chargetSex(){returnsex;}voidshow(){cout<<"姓名:"<<name<<" 年龄:"<<age<<" 性别:"<<(sex=='M'?"男":"女")<<endl;}private:char*name;intage;charsex;};

二、拷贝构造函数

1. 触发时机(只有新建对象初始化时调用)

intmain(){Personper1("赵云",23,'M');// 方式1Personper2(per1);// 方式2Person per3=per1;per2.show();per3.show();return0;}

✅ 关键点
Person per = obj创建对象阶段 → 拷贝构造

❌ 不要混淆:

Person a;a=per1;// a早已创建,这里不会调用拷贝构造!调用赋值重载

2. 浅拷贝问题说明

编译器默认版本等价于:

Person(constPerson&rhs){this->name=rhs.name;// 只复制指针地址!浅拷贝this->age=rhs.age;this->sex=rhs.sex;}

两个对象name指向同一块堆内存;析构时delete[]两次 → 程序崩溃。


三、赋值运算符重载 operator=

1. 使用示例代码

intmain(){Personper1("赵云",23,'M');Person per4;// per4已经构造完成per4=per1;// 调用 operator=per4.show();per1=per1;// 测试自赋值防护Person per5;per5=Person("张飞",25,'M');// 临时对象赋值return0;}

2. 必须遵守三条规范

  1. 判断自赋值this == &rhs
  2. 先释放自身旧资源
  3. 返回*this引用

✔ 一句话区分拷贝构造 / 赋值重载

  • 拷贝构造:对象还不存在,正在创建初始化
  • 赋值重载:对象已经存在,覆盖原有内容

四、函数返回局部对象:RVO返回值优化案例(重点)

完整待测代码

PersonGetPerson(Person&per){constchar*name=per.getName();intage=per.getAge();charsex=per.getSex();PersontmpPer(name,age,sex);// 栈上局部对象returntmpPer;}intmain(){Personper1("赵云",23,'M');Person per2=GetPerson(per1);per2.show();return0;}

4.1 不开启编译器优化(理论标准流程)

  1. Person per1("赵云",23,'M');:调用三参数有参构造函数,创建per1。
  2. 进入GetPerson函数内部:
    • Person tmpPer(name, age, sex);:调用三参数有参构造,在函数栈上创建局部对象tmpPer
  3. return tmpPer;
    • 使用tmpPer拷贝构造临时对象(函数返回值临时对象)。
    • 函数作用域结束:局部对象tmpPer销毁,调用析构函数
  4. Person per2 = GetPerson(per1);
    • 用返回的临时对象,拷贝构造per2对象
    • 临时对象生命周期结束,调用析构函数销毁临时对象。
  5. main函数结束:per2析构、per1析构,各调用一次析构。

无优化完整调用顺序:
三参构造(per1) → 三参构造(tmpPer) → 拷贝构造(临时返回对象) → 析构(tmpPer) → 拷贝构造(per2) → 析构(临时对象) → 析构(per2) → 析构(per1)

4.2 开启RVO返回值优化(编译器默认开启,实际运行现象)

RVO:返回局部对象时,编译器把局部对象的内存直接构造到外部接收对象per2的内存空间,省去两次拷贝构造、省去一次局部tmpPer析构

实际调用顺序:

  1. Person per1("赵云", 23, 'M');三参数有参构造 per1
  2. GetPerson内部,直接在per2的内存上执行构造逻辑 →三参数有参构造 per2
  3. main结束,依次析构 per2、per1。

实际运行只会看到两次三参构造,两次析构,看不到拷贝构造执行

4.3 两种写法对比

//写法1:创建局部对象再returnPersontmpPer(name,age,sex);returntmpPer;//写法2:return 匿名临时对象returnPerson(name,age,sex);

第二种属于NRVO,同样会被编译器优化,不会产生多余拷贝。

4.4 区分两种接收返回值写法

//写法A:对象初始化,Person per2 = GetPerson(per1);// 接收返回值,对象正在创建,走拷贝构造(会被RVO优化掉)Person per2=GetPerson(per1);//写法B:先定义对象,再赋值Person per2;per2=GetPerson(per1);
  • 写法A:per2是新建对象,理论上触发拷贝构造,RVO优化后直接构造。
  • 写法B:per2已经通过默认构造函数创建完成;GetPerson返回临时对象,调用赋值运算符重载把临时对象赋值给per2;函数结束临时对象调用析构销毁。

写法B调用顺序:
三参构造(per1) → 默认构造(per2) → 三参构造(tmpPer) →拷贝构造(返回临时) →析构(tmpPer) → operator=(per2 = 临时对象) →析构(返回临时) →main结束析构per2、per1。

重点面试:
Person per2 = GetPerson(per1);不是赋值!是对象初始化;
per2 = GetPerson(per1);才是赋值重载调用。


五、explicit 关键字详解

5.1 隐式转换问题

给类增加单参数构造:

classTest{public:Test(intage){cout<<"构造 age = "<<age<<endl;}};intmain(){Test t=20;// 不写explicit时合法!隐式转换:Test temp(20); Test t = temp;return0;}

很多时候这种自动转换是意外bug。

5.2 explicit 禁止隐式转换

classTest{public:explicitTest(intage){cout<<"构造 age = "<<age<<endl;}};intmain(){// Test t = 20; // 编译报错,禁止隐式转换Testt(20);// ✅ 显式调用,允许return0;}

使用小结

  1. 只修饰构造函数,多用于单参数构造
  2. 阻止编译器自动发生隐式类型转换
  3. C++开发建议:单参数构造默认加上explicit

六、核心总结

  1. 管理堆内存的类,必须手写拷贝构造 + operator= + 析构(三法则);
  2. 浅拷贝 = 复制指针;深拷贝 = 重新开辟内存复制数据;
  3. Person obj = otherobj = other,前者拷贝构造,后者赋值重载;
  4. 赋值函数务必处理自赋值
  5. explicit杜绝构造函数意外隐式转换;
  6. 函数返回局部对象:标准流程会触发拷贝构造;现代编译器RVO/NRVO优化会消除多余拷贝;
  7. 接收函数返回值:
    • Person p = func();对象初始化,理论拷贝构造;
    • p = func();对象已存在,调用赋值运算符重载。

拓展:关闭RVO优化gcc参数:-fno-elide-constructors,可以看到没有优化的完整调用流程,方便学习调试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询