C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝的核心辨析

C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝的核心辨析

C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝的核心辨析

在这里插入图片描述

💡 学习目标:掌握拷贝构造函数与赋值运算符的定义及调用场景,理解深拷贝与浅拷贝的本质区别,能够在实际开发中避免内存泄漏与野指针问题。
💡 学习重点:拷贝构造函数的触发条件、浅拷贝的缺陷、深拷贝的实现方法、赋值运算符的重载原则。

一、拷贝构造函数的概念与触发场景

结论:拷贝构造函数是一种特殊的构造函数,用于通过一个已存在的对象创建一个新对象,其参数必须是本类对象的常量引用(const 类名&)。

1.1 拷贝构造函数的语法格式

class 类名 {public:// 普通构造函数 类名(参数列表);// 拷贝构造函数 类名(const 类名& other);};

⚠️ 注意事项

  1. 拷贝构造函数的参数必须是常量引用,使用 const 防止实参被修改,使用引用避免无限递归调用拷贝构造函数。
  2. 如果没有手动定义拷贝构造函数,编译器会自动生成一个默认拷贝构造函数,实现简单的成员变量值拷贝。

1.2 拷贝构造函数的触发条件

拷贝构造函数在以下三种场景下会被自动调用:

  1. 使用一个对象初始化另一个新对象
  2. 函数参数为类对象(值传递)
  3. 函数返回值为类对象(值传递)
1.2.1 代码演示:触发场景验证
#include<iostream>#include<string>usingnamespace std;classPerson{public: string name;int age;// 普通构造函数Person(string n,int a):name(n),age(a){ cout <<"普通构造函数被调用"<< endl;}// 拷贝构造函数Person(const Person& other){this->name = other.name;this->age = other.age; cout <<"拷贝构造函数被调用"<< endl;}};// 场景2:函数参数为类对象(值传递)voidfunc(Person p){ cout <<"函数内对象姓名:"<< p.name << endl;}// 场景3:函数返回值为类对象(值传递) Person getPerson(){ Person p("王五",30);return p;}intmain(){// 普通构造创建对象 Person p1("张三",20);// 场景1:使用 p1 初始化 p2 Person p2 = p1;// 场景2:值传递传递对象func(p1);// 场景3:值传递返回对象 Person p3 =getPerson();return0;}
1.2.2 运行结果
普通构造函数被调用 拷贝构造函数被调用 拷贝构造函数被调用 函数内对象姓名:张三 普通构造函数被调用 拷贝构造函数被调用 

二、浅拷贝与深拷贝的核心区别

💡 浅拷贝是指仅拷贝对象的成员变量值,深拷贝是指不仅拷贝成员变量值,还为指针成员重新分配内存并拷贝数据,二者的核心差异体现在指针成员的处理上。

2.1 浅拷贝的实现与缺陷

默认拷贝构造函数和默认赋值运算符实现的是浅拷贝,当类中包含指针成员时,浅拷贝会导致多个对象的指针指向同一块内存,引发严重问题。

2.1.1 浅拷贝的问题代码演示
#include<iostream>#include<cstring>usingnamespace std;classString{private:char* str;// 指针成员public:// 普通构造函数:分配堆内存String(constchar* s =""){ str =newchar[strlen(s)+1];strcpy(str, s); cout <<"普通构造函数:分配内存"<< endl;}// 析构函数:释放堆内存~String(){delete[] str; cout <<"析构函数:释放内存"<< endl;}// 打印字符串voidshow(){ cout << str << endl;}};intmain(){ String s1("Hello C++");// 浅拷贝:s2.str 与 s1.str 指向同一块内存 String s2 = s1; s1.show(); s2.show();return0;}
2.1.2 运行结果与问题分析
普通构造函数:分配内存 Hello C++ Hello C++ 析构函数:释放内存 析构函数:释放内存 

问题1:重复释放内存

  • s1s2 的指针指向同一块堆内存。
  • 程序结束时,两个对象的析构函数会先后释放同一块内存,导致内存崩溃

问题2:修改一个对象影响另一个

  • 如果修改 s1.str 指向的内容,s2.str 的内容也会被改变,违背对象的独立性。

2.2 深拷贝的实现与优势

深拷贝的核心是为指针成员重新分配内存,并将原对象指针指向的数据拷贝到新内存中,从而保证每个对象的指针成员都有独立的内存空间。

2.2.1 深拷贝的实现:重写拷贝构造函数
#include<iostream>#include<cstring>usingnamespace std;classString{private:char* str;public:String(constchar* s =""){ str =newchar[strlen(s)+1];strcpy(str, s); cout <<"普通构造函数:分配内存"<< endl;}// 手动实现深拷贝构造函数String(const String& other){// 为新对象分配独立内存this->str =newchar[strlen(other.str)+1];// 拷贝数据strcpy(this->str, other.str); cout <<"深拷贝构造函数:分配独立内存"<< endl;}~String(){delete[] str; cout <<"析构函数:释放内存"<< endl;}voidshow(){ cout << str << endl;}// 提供修改字符串的方法,验证独立性voidsetStr(constchar* s){delete[] str; str =newchar[strlen(s)+1];strcpy(str, s);}};intmain(){ String s1("Hello C++"); String s2 = s1;// 调用深拷贝构造函数 cout <<"修改前:"<< endl; s1.show(); s2.show();// 修改 s1 的内容 s1.setStr("Hello Deep Copy"); cout <<"修改后:"<< endl; s1.show(); s2.show();return0;}
2.2.2 运行结果与优势分析
普通构造函数:分配内存 深拷贝构造函数:分配独立内存 修改前: Hello C++ Hello C++ 修改后: Hello Deep Copy Hello C++ 析构函数:释放内存 析构函数:释放内存 

核心优势

  1. 内存独立s1s2 的指针指向不同的内存空间,修改一个对象不会影响另一个。
  2. 避免重复释放:析构函数释放的是各自独立的内存,不会导致内存崩溃。

三、赋值运算符重载与深拷贝

💡 赋值运算符(=)的默认行为也是浅拷贝,当类中包含指针成员时,必须手动重载赋值运算符并实现深拷贝,其实现逻辑与深拷贝构造函数类似,但需要处理自赋值问题。

3.1 赋值运算符重载的语法与原则

3.1.1 核心语法
类名&operator=(const 类名& other){// 1. 处理自赋值if(this==&other){return*this;}// 2. 释放当前对象的原有内存delete[]this->指针成员;// 3. 分配新内存并拷贝数据this->指针成员 =new 类型[大小]; 拷贝数据逻辑;// 4. 返回当前对象的引用,支持链式赋值return*this;}
3.1.2 核心原则
  1. 处理自赋值:防止 a = a 这种情况导致内存提前释放。
  2. 释放原有内存:避免内存泄漏。
  3. 返回对象引用:支持链式赋值(如 a = b = c)。

3.2 代码演示:赋值运算符的深拷贝实现

#include<iostream>#include<cstring>usingnamespace std;classString{private:char* str;public:String(constchar* s =""){ str =newchar[strlen(s)+1];strcpy(str, s); cout <<"普通构造函数:分配内存"<< endl;}// 深拷贝构造函数String(const String& other){this->str =newchar[strlen(other.str)+1];strcpy(this->str, other.str); cout <<"深拷贝构造函数:分配独立内存"<< endl;}// 重载赋值运算符,实现深拷贝 String&operator=(const String& other){// 1. 处理自赋值if(this==&other){return*this;}// 2. 释放当前对象的原有内存delete[]this->str;// 3. 分配新内存并拷贝数据this->str =newchar[strlen(other.str)+1];strcpy(this->str, other.str); cout <<"赋值运算符重载:深拷贝"<< endl;// 4. 返回当前对象引用return*this;}~String(){delete[] str; cout <<"析构函数:释放内存"<< endl;}voidshow(){ cout << str << endl;}};intmain(){ String s1("Hello C++"); String s2;// 调用赋值运算符重载 s2 = s1; s1.show(); s2.show();// 测试链式赋值 String s3; s3 = s2 = s1; cout <<"链式赋值后 s3:"; s3.show();return0;}
3.2.1 运行结果
普通构造函数:分配内存 普通构造函数:分配内存 赋值运算符重载:深拷贝 Hello C++ Hello C++ 赋值运算符重载:深拷贝 链式赋值后 s3:Hello C++ 析构函数:释放内存 析构函数:释放内存 析构函数:释放内存 

四、拷贝构造函数与赋值运算符的区别

核心区别总结:拷贝构造函数用于创建新对象,赋值运算符用于给已存在的对象赋值,二者的调用时机和执行逻辑完全不同。

特性拷贝构造函数赋值运算符
调用时机用已有对象创建新对象时调用已存在的对象赋值时调用
参数要求必须是 const 类名&通常是 const 类名&
内存操作分配新内存,无原有内存需要释放需先释放当前对象原有内存
返回值无返回值(构造函数特性)必须返回 类名&,支持链式赋值
默认实现默认浅拷贝默认浅拷贝

五、深拷贝的实战案例:自定义数组类

💡 需求:设计一个自定义数组类 MyArray,支持动态扩容,要求实现深拷贝构造函数和赋值运算符重载,避免浅拷贝导致的内存问题。

5.1 需求分析

  1. 成员变量:int* arr(存储数组数据)、int size(数组大小)。
  2. 核心功能:构造函数分配内存、深拷贝构造、赋值运算符重载、打印数组、析构函数释放内存。
  3. 要求:保证多个对象的数组数据独立,修改一个对象的数组不影响其他对象。

5.2 完整代码实现

#include<iostream>#include<cstring>usingnamespace std;classMyArray{private:int* arr;// 动态数组指针int size;// 数组大小public:// 构造函数:创建指定大小的数组MyArray(int s =0):size(s){if(size >0){ arr =newint[size];// 初始化数组元素为 0memset(arr,0,sizeof(int)* size);}else{ arr =nullptr;} cout <<"构造函数:创建大小为 "<< size <<" 的数组"<< endl;}// 深拷贝构造函数MyArray(const MyArray& other){this->size = other.size;if(this->size >0){// 分配独立内存this->arr =newint[this->size];// 拷贝数组数据for(int i =0; i <this->size; i++){this->arr[i]= other.arr[i];}}else{this->arr =nullptr;} cout <<"深拷贝构造函数:拷贝大小为 "<< size <<" 的数组"<< endl;}// 赋值运算符重载:深拷贝 MyArray&operator=(const MyArray& other){if(this==&other){return*this;}// 释放当前对象原有内存if(this->arr !=nullptr){delete[]this->arr;}// 拷贝大小并分配新内存this->size = other.size;if(this->size >0){this->arr =newint[this->size];for(int i =0; i <this->size; i++){this->arr[i]= other.arr[i];}}else{this->arr =nullptr;} cout <<"赋值运算符重载:深拷贝数组"<< endl;return*this;}// 设置数组指定位置的值voidsetValue(int index,int value){if(index >=0&& index < size){ arr[index]= value;}else{ cout <<"⚠️ 索引越界"<< endl;}}// 打印数组voidprintArray(){if(arr ==nullptr){ cout <<"数组为空"<< endl;return;} cout <<"数组元素:";for(int i =0; i < size; i++){ cout << arr[i]<<" ";} cout << endl;}// 析构函数:释放内存~MyArray(){if(arr !=nullptr){delete[] arr; arr =nullptr;} cout <<"析构函数:释放数组内存"<< endl;}};intmain(){// 创建数组对象 MyArray arr1(5);// 设置数组值 arr1.setValue(0,10); arr1.setValue(1,20); arr1.setValue(2,30); arr1.printArray();// 深拷贝构造新对象 MyArray arr2 = arr1; arr2.setValue(0,100); cout <<"修改 arr2 后:"<< endl; arr1.printArray(); arr2.printArray();// 赋值运算符重载 MyArray arr3(3); arr3 = arr1; arr3.setValue(1,200); cout <<"修改 arr3 后:"<< endl; arr1.printArray(); arr3.printArray();return0;}

5.3 运行结果

构造函数:创建大小为 5 的数组 数组元素:10 20 30 0 0 深拷贝构造函数:拷贝大小为 5 的数组 修改 arr2 后: 数组元素:10 20 30 0 0 数组元素:100 20 30 0 0 构造函数:创建大小为 3 的数组 赋值运算符重载:深拷贝数组 修改 arr3 后: 数组元素:10 20 30 0 0 数组元素:10 200 30 0 0 析构函数:释放数组内存 析构函数:释放数组内存 析构函数:释放数组内存 

六、开发规范与常见问题

6.1 深拷贝的开发规范

  1. 三法则原则:当类中包含指针成员时,必须同时实现拷贝构造函数、赋值运算符重载、析构函数,三者缺一不可。
  2. 优先使用智能指针:C++11 及以上版本中,可以使用 unique_ptrshared_ptr 等智能指针替代裸指针,自动管理内存,避免手动实现深拷贝。
  3. 避免不必要的深拷贝:如果类中没有指针成员,直接使用默认的浅拷贝即可,无需手动实现深拷贝。

6.2 常见问题与解决方案

6.2.1 问题1:忘记处理自赋值

解决方案:在赋值运算符重载函数开头,添加 if (this == &other) 判断,直接返回 *this

6.2.2 问题2:释放内存后未置空指针

解决方案:析构函数或赋值运算符中释放内存后,将指针置为 nullptr,避免野指针。

6.2.3 问题3:深拷贝时内存分配失败

解决方案:可以添加异常处理逻辑,捕获内存分配失败的异常,增强程序健壮性。

七、本章总结

✅ 拷贝构造函数用于用已有对象创建新对象,赋值运算符用于给已存在对象赋值,二者的调用时机不同。
✅ 浅拷贝仅拷贝成员变量值,适用于无指针成员的类;深拷贝为指针成员分配独立内存,避免内存崩溃。
✅ 当类中包含指针成员时,必须遵循三法则原则,同时实现深拷贝构造函数、赋值运算符重载和析构函数。
✅ 合理使用深拷贝可以保证对象的独立性和内存安全,是 C++ 高级编程的核心技能之一。

Read more

从0到1搞懂Linux动静态库制作与底层原理|开发者必备指南

从0到1搞懂Linux动静态库制作与底层原理|开发者必备指南

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 《Git深度解析》:版本管理实战全解 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 一、先搞懂:Linux下的库是什么?二进制的“代码积木” 1.1 库的本质 1.2 库的分类与系统位置 1.3 预备工作:自定义库源码 二. 静态库:编译时链接,独立运行 2.1 整体图示:理清思路 2.2 静态库制作流程(Makefile 自动化 ,更简便) 2.3 静态库使用场景与命令

By Ne0inhk
MySQL迁金仓:高兼容+自动化,国产化迁移低成本落地实战

MySQL迁金仓:高兼容+自动化,国产化迁移低成本落地实战

目录 一、MySQL兼容性:决定迁移成本与难度的核心 1.1 协议层兼容:应用端几乎零改动,省出大量工时 1.2 语法与函数兼容:核心SQL直接复用,无需改写 1.3 数据类型与字符集兼容:贴合MySQL习惯,数据零损耗 1.4 数据库对象兼容:视图、触发器、函数直接复用,省掉隐性工作量 1.5 兼容差异注意事项:少量微调,工具自动处理 二、金仓迁移工具链(KDTS+KFS):全流程自动化,严控成本与停机时间 2.1 工具链核心:针对性解决手工迁移的核心痛点 2.2 KDTS:全量迁移自动化引擎,替代低效手工导入 2.3 KFS:增量同步+

By Ne0inhk
Flutter 组件 r_flutter 的适配 鸿蒙Harmony 实战 - 驾驭资源映射自动化、实现鸿蒙端资产强类型引用与资产冲突静态校验方案

Flutter 组件 r_flutter 的适配 鸿蒙Harmony 实战 - 驾驭资源映射自动化、实现鸿蒙端资产强类型引用与资产冲突静态校验方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 r_flutter 的适配 鸿蒙Harmony 实战 - 驾驭资源映射自动化、实现鸿蒙端资产强类型引用与资产冲突静态校验方案 前言 在鸿蒙(OpenHarmony)的大型 UI 工程开发中,“资源管理”是一个极易产生低级错误的重灾区。面对动辄几百个图标(PNG/SVG)、各种自定义字体文件以及多层级的资源目录。如果我们依然使用硬编码字符串(如 Image.asset('assets/images/home_icon_v2_final.png')),那么不仅毫无代码提示可言,由于文件名拼写错误引发的运行期资源丢失(Missing Asset)更是家常便饭。 我们需要一种“代码即资产”的强类型保护。 r_flutter

By Ne0inhk

Linux:初始网络(下)

或许你有一个疑问,“发请求、收响应”,却不清楚数据在网线里到底是怎么从一台主机走到另一台主机的。这篇博客在上一篇博客基础上,将最基础的局域网通信原理出发,拆解数据封装与解包的核心逻辑,再延伸到跨网段的网络传输,帮你建立起网络传输的完整宏观认知,所以大家要认真阅读啦~~ 一、同局域网通信:以太网内的主机如何直接对话 局域网是我们最常接触的网络场景,比如家里的路由器连接的电脑、手机,公司内网的办公设备,都属于同一个局域网。我们先从最核心的问题切入,理解局域网通信的底层逻辑 1. 核心问题:同一局域网的两台主机,能直接通信吗? 答案是:完全可以!局域网内的主机通信,本质是基于以太网协议、通过 MAC 地址完成的二层直连通信,原理就像我们在同一个教室里上课:老师喊出同学的名字,全班同学都能听到这个声音,但只有名字对应的同学会做出回应,其他同学会自动忽略这个信息 2. 局域网通信的唯一身份标识:MAC 地址 在以太网的局域网里,每一台主机的唯一性,靠的就是 MAC 地址来保证。 * 核心定义:MAC 地址用来识别数据链路层中相连的节点,是网卡的 “物理身份证”

By Ne0inhk