C++波澜壮阔40年|类和对象篇:拷贝构造与赋值重载的演进与实现

C++波澜壮阔40年|类和对象篇:拷贝构造与赋值重载的演进与实现
在这里插入图片描述


🔥@雾忱星: 个人主页
👀专栏:《数据结构与算法入门指南》《C++学习之旅》
💪学习阶段:C/C++、数据结构与算法
⏳“人理解迭代,神理解递归。”


文章目录


引言

在C++面向对象编程中,对象的复制操作无处不在。无论是函数传参、返回值传递,还是对象间的赋值,都需要精确控制数据的复制行为。

C++通过拷贝构造函数和赋值运算符重载两套机制,为开发者提供了对象复制的完整解决方案。
本文将从基础概念出发,深入解析这两种复制机制的实现细节与应用技巧。

一、拷贝构造函数

如果一个构造函数第一个参数是**自身类型的引用**,且 其他所有参数都有默认值(如果有) ,就叫做 拷贝构造,是特殊的构造函数。

  • 基本形式:
#include<iostream>usingnamespace std;//基本形式classExample{public:Example(Example& d){//...}};

1.1 解析:拷贝构造特点

(部分规则与构造函数相同)

  1. 拷贝构造函数是构造函数的一个重载;
  2. 拷贝构造函数的第一个参数必须是自身类类型的引用:类名& 或 const 类名&(最好加 const)。 如果使用传值的方式,在逻辑上会引发无穷递归调用;
  3. 拷贝构造函数可以有多个参数,第一个为引用,其他必须有缺省值;
  4. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成;
  5. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。默认生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造
  6. 类似前面说的Date类,成员变量全是内置类型且不指向资源,编译器默认生成的拷贝构造就够了。类似Stack类,虽然也都是内置类型,但是指针指向资源,那么编译器默认生成的浅拷贝/值拷贝就不太够,需要显式定义深拷贝。再对于MyQueue类,自定义类型Stack变量成员就直接调用它的拷贝构造。

【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。

  1. 传值返回会产生一个临时对象来调用拷贝构造;而传引用返回,返回的是对象的别名,不会产生拷贝,但是返回的对象为一个当前函数局部域的局部对象,函数结束就会销毁,这时传引用返回是有问题的,类似于野指针。
    (传引用返回会减少拷贝,但是要确保返回对象在函数结束时不会被销毁)

解释特点第2条:

在这里插入图片描述

看图也可以明白,当==拷贝构造函数传值传参==时,函数的形参是实参拷贝出来的新对象,要调用拷贝构造,但是拷贝构造函数也是传值传参就又要调用拷贝构造,这样无限循环下去……

其次,在引用传参最好加上const,因为将对象传过来,也不会将对象进行改变操作,那么const就方便了传参(权限缩小)。当然,这时候传const对象也是可以的(权限平移)。

    • 特点第2条拓展:既然要引用传参,那么指针可以吗?
      先说,传指针是可以的,但是函数就变成普通的构造函数,不是拷贝构造函数。
#include<iostream>usingnamespace std;//传指针classDate{public://构造函数:全缺省Date(int day =8,int month =1,int year =2026){ _day = day; _month = month; _year = year;}//指针传参Date(Date* d){ _day = d->_day; _month = d->_month; _year = d->_year;}voidPrint(){ cout << _year <<'/'<< _month <<'/'<< _day << endl;}private:int _day;int _month;int _year;};intmain(){//调用构造函数初始化d1 Date d1; d1.Print();//传地址 Date d2(&d1); d2.Print();return0;}
在这里插入图片描述
  • 解释特点第5条:

拷贝构造函数就和构造、析构有点不同。它会对内置类型的成员变量进行处理。
类似Date类这样全是内置类型的变量,编译器默认生成的就够用;对于复杂结构的类Stack,就要自定义深拷贝;对于MyQueue这样的类,不显式定义拷贝构造,编译器就会调用成员变量对应类的拷贝构造。

  • 解释特点第6条: 通过实现栈来观察
    • 有指向的资源,浅拷贝的后果:
      • 一个对象改变会影响另一个对象;
      • 析构时,同一块空间会释放两次空间;
typedefint STDataType;classStack{public:Stack(int n =4){ _a =(STDataType*)malloc(sizeof(STDataType)* n);if(nullptr== _a){perror("malloc申请空间失败");return;} _capacity = n; _top =0;}// Stack st2(st1);Stack(const Stack& s){ _a = s._a; _capacity = s._capacity; _top = s._top;}voidPush(STDataType x){if(_top == _capacity){int newcapacity = _capacity *2; STDataType* tmp =(STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if(tmp ==NULL){perror("realloc fail");return;} _a = tmp; _capacity = newcapacity;} _a[_top++]= x;}~Stack(){ cout <<"~Stack()"<< endl;free(_a); _a =nullptr; _top = _capacity =0;}private: STDataType* _a; size_t _capacity; size_t _top;};intmain(){ Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1);return0;}
在这里插入图片描述

(会产生错误,通常是内存相关的问题。)

在这里插入图片描述


这样浅拷贝会使两个对象的指针变量都指向同一块空间,最后的两次析构就导致第二次析构对已经释放完的空间再次释放,发生错误。

    • 有指向的资源,自定义深拷贝:(先简单了解)
      • 不仅仅对成员拷贝,还对指向的资源空间数据进行处理。(开空间)
Stack(const Stack& s){ _a =(STDataType*)malloc(sizeof(STDataType)* s._capacity);if(_a ==NULL){perror("realloc fail");return;}memcpy(_a, s._a, s._top *sizeof(STDataType)); _capacity = s._capacity; _top = s._top;}
在这里插入图片描述


调试程序发现:不指向同一空间。

  • 解释特点第7条:
    在上面栈的基础上
int&func1(){int ret =1;return ret;//返回的是ret的别名} Stack&func2(){ Stack st;return st;}intmain(){int ret1 =func1();//ret在函数结束时就销毁了,所以这里存在错误 cout << ret1 <<'\n';//可能是1或者随机值 Stack ret2 =func2();//调拷贝构造,但是st不存在return0;}
在这里插入图片描述

根据特点7,函数传值返回是会调用拷贝构造的,但是传引用返回不会。对于st这里,函数就是进行析构(成员函数),那么在通过返回的别名来访问st肯定是错的。

【所以,在传引用返回是一定要注意返回对象是否还存在!】

1.2 关键:拷贝构造的调用

  • 用一个对象初始化另一个同类的对象(在创建的同时初始化)

基本形式:

Example a; Example b(a);// 调用拷贝构造函数 Example c = a;// 调用拷贝构造函数
#include<iostream>usingnamespace std;classDate{public://构造函数:全缺省Date(int day =8,int month =1,int year =2026){ _day = day; _month = month; _year = year;}//拷贝构造函数Date(Date& d){ _day = d._day; _month = d._month; _year = d._year;}voidPrint(){ cout << _year <<'/'<< _month <<'/'<< _day << endl;}private:int _day;int _month;int _year;};intmain(){//调用构造函数初始化d1 Date d1;//不要写成Date d1(); d1.Print();//创建对象的同时,调用拷贝构造进行初始化 Date d2(d1); d2.Print();return0;}
在这里插入图片描述
  • 函数参数按值传递该类的对象(传值传参)

基本形式:

voidfunc(Example obj){...} Example a;func(a);// 调用拷贝构造函数
注意: 调用函数,形参是用实参拷贝构造出来的新对象,将实参传递就符合调用拷贝构造的规则。(函数形参也是一个需要被创建的对象。)
#include<iostream>usingnamespace std;classDate{public://构造函数:全缺省Date(int day =8,int month =1,int year =2026){ _day = day; _month = month; _year = year;}//拷贝构造函数Date(Date& d){ _day = d._day; _month = d._month; _year = d._year;}voidPrint(){ cout << _year <<'/'<< _month <<'/'<< _day << endl;}private:int _day;int _month;int _year;};voidfunc(Date d){ d.Print();}intmain(){//调用构造函数初始化d1 Date d1; d1.Print();func(d1);//调用拷贝函数return0;}
在这里插入图片描述
在VS2022上调试程序发现,调用func()函数时,将对象传参,会先进行拷贝构造,然后进入func()函数内。(函数进行传值传参,会先完成传参)

二、赋值运算符重载

2.1 铺垫:运算符重载特点

  1. 运算符被用于类类型的对象时,C++允许通过运算符重载的形式指定新的含义。C++规定,类类型对象使用运算符时,需要转换成调用相应的运算符重载,没有会报错;
  2. 运算符重载式具有特殊名字的函数,名称由operator和后面的运算符构成,与普通函数一样,具有返回值、返回类型、参数、函数体等;
  3. 重载运算符函数的参数个数和运算符的操作对象数量相同:一元运算符一个参数,二元运算符两个参数。对于二元:左侧运算对象传给第一个参数,右侧传给第二个参数;
  4. 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个;
  5. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致;
  6. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@;
  7. .*::sizeof? :. 以上五个运算符不能重载;
  8. 重载运算符至少有一个类类型的参数,不能通过运算符重载改变内置类型对象的含义,如:operator+(int x, int y);
  9. 一个类需要哪些运算符重载。是看那些有实际意义,比如:Date类的operatoe-有意义,operatoe*没有意义;
  10. 重载 ++ 运算符时,有 前置 ++ 和 后置 ++ ,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置 ++ 重载时,增加一个int形参,跟前置 ++ 构成函数重载,方便区分;
  11. 重载 <<>> 时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第二个形参位置当类类型对象。

2.1.1 核心:理解运算符重载

【搭配举例】:

#include<iostream>usingnamespace std;classDate{public://构造函数Date(int day =8,int month =1,int year =2026){ _day = day; _month = month; _year = year;}//如果运算符重载载在类外实现,解决变量私有化一个方法:intGetyear(){return _year;}//或者在类内定义//比较Date类对象是否相等booloperator==(const Date& d)//this占第一个,显式第一个参数为左侧运算对象//第4条:默认第一个参数为this指针{return _day == d._day && _month == d._month && _year == d._year;}private:int _day;int _month;int _year;};//类外定义//bool operator==(const Date& x1, const Date& x2) //第2条//{// return x1.day == x2._day && x1._month == x2._month &&// x1._year == x2._year;//}intmain(){ Date d1(1,1,1); Date d2; cout <<(d1 == d2)<< endl;//第3条}

额外注意】:

  • 当在类外定义运算符重载时,因为成员变量私有无法访问,可采取:
    • 将成员变量访问改为公有,但是太极端危险;
    • 在类内定义int Getyear();之类函数,获取成员变量;
    • 直接在类内定义运算符重载成为成员函数(推荐),但是要注意参数的改变(第4条)。
    • 友元函数(后面会有)。
  • 在最后输出结构,注意优先级。<< / >> 优先级较高,所以... == ... 要加括号。

介绍.*运算符】:C++不常用,了解

#include<iostream>usingnamespace std;voidfunc1(){ cout <<"void func()"<< endl;}classA{public:voidfunc2(){ cout <<"A::func()"<< endl;}};intmain(){// 普通函数指针void(*pf1)()= func1;(*pf1)();// A类型成员函数的指针void(A::* pf2)()=&A::func2; A aa;(aa.*pf2)();//这里就是使用的.*//(aa.*pf2)(&aa);错误,this指针不能显式出现参数。return0;}

2.2 进阶:赋值运算符重载特点

赋值运算符重载是一个默认成员函数,用于完成两个已存在的对象直接的拷贝复制,要和拷贝构造区分开。

  1. 赋值运算复是一个运算符重载,C++规定必须为成员函数。参数建议写成const当前类类型引用传参,当然传值传参会调用拷贝构造;
  2. 有返回值,建议写成当前类类型引用,传引用返回可以提高效率,有返回值就可以连续赋值;
  3. 当没有显式实现,编译器会默认生成,其行为和默认生成的拷贝构造类似,对内置类型成员变量会先完成值拷贝,对自定义类型会调用相应的赋值重载函数;
  4. 类似Date类,为内置类型成员且不指向任何资源,编译器默认生成的浅拷贝就够了。但是类似Stack类,有指向的资源,就需要自定义深拷贝。

(这里和拷贝构造类似)

【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。

2.2 核心:理解赋值运算符重载

#include<iostream>usingnamespace std;classDate{public://构造函数Date(int day =8,int month =1,int year =2026){ _day = day; _month = month; _year = year;}//拷贝构造Date(const Date& d){ _day = d._day; _month = d._month; _year = d._year;}//赋值重载//d1 = d2 Date&operator=(const Date& d){if(this!=&d){ _day = d._day; _month = d._month; _year = d._year;}return*this;//返回d1别名,不拷贝}voidPrint(){ cout << _year <<'/'<< _month <<'/'<< _day << endl;}private:int _day;int _month;int _year;};intmain(){ Date d1(1,1,1); Date d2(d1);//拷贝构造 Date d3 = d1;//拷贝构造 d2.Print(); d3.Print(); Date d4; d5 = d4 = d1;//赋值重载 d4.Print();return0;}
  • 【说明】 Date& operator=(const Date& d);为什么可以传引用返回?
    在拷贝构造部分,有过说明“传值返回会发生拷贝”,但是this不是这个函数的局部对象,不会销毁,额外的拷贝就很麻烦,没必要。

总结

🍓 我是晨非辰Tong!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:

拷贝构造函数与赋值运算符重载构成了C++对象复制机制的核心支柱。它们分别负责对象初始化和对象赋值两种不同场景的复制需求。

掌握这些复制控制机制,不仅能写出更安全的代码,更能深入理解C++对象生命周期的管理哲学。这是从C++使用者迈向C++设计者的重要一步。

Read more

mysql-9.6.0-winx64 安装踩雷教程

mysql-9.6.0-winx64 安装踩雷教程

今天安装了mysql-9.6.0-winx64,有部分踩雷事项。 下载地址:mysql 1、D盘新建文件夹mysql,把文件压缩到这个文件夹底下 2、在安装包的根目录底下建一个my.ini文件。文件里面写的内容可以直接复制。 * 注意:很多旧教程里面的配置信息是错误和新的mysql不匹配。 会面临错误:MySQL 9.6.0 启动失败。根源是 配置项: default_authentication_plugin=mysql_native_password 在 9.6 版本中已被移除,同时因配置错误导致系统表 mysql.component 缺失。 * basedir具体的地址填写你自己的。 * datadir的data现在是没有的,要等后面初始化的时候才生成。 [mysqld]port=3307basedir=D:\\mysql\\mysql-9.6.0-winx64 datadir=D:

By Ne0inhk
Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构

Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构 前言 在鸿蒙(OpenHarmony)生态迈向工业数字化、涉及海量历史报表同步、离线数据采集及跨系统异构数据对齐的背景下,如何实现一种既能处理超大规模文本、又能保障转换极速且具备“非阻塞”特性的数据清洗方案,已成为决定应用数据吞吐能力与内存稳健性的核心因素。在鸿蒙设备这类强调 AOT 极致性能与受限内存足迹的环境下,如果应用依然采用原始的循环分割或同步全量加载 CSV,由于由于数据规模的膨胀,极易由于由于“内存瞬时爆表”导致鸿蒙应用的任务栈卡死。 我们需要一种能够流式处理(Streaming)、支持自动化字段映射(Auto-mapping)且具备零样板代码特性的转换方案。 csv2json 为 Flutter 开发者引入了“数据流变幻”范式。它将结构松散的 CSV 文本精确轰击为高维度的 JSON

By Ne0inhk
Flutter 组件 angel3_auth 适配鸿蒙 HarmonyOS 实战:多策略身份验证,构建全栈式安全鉴权与身份防腐架构

Flutter 组件 angel3_auth 适配鸿蒙 HarmonyOS 实战:多策略身份验证,构建全栈式安全鉴权与身份防腐架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 angel3_auth 适配鸿蒙 HarmonyOS 实战:多策略身份验证,构建全栈式安全鉴权与身份防腐架构 前言 在鸿蒙(OpenHarmony)生态迈向全栈式开发、涉及跨端统一登录、多因子安全验证(MFA)及高性能服务端 API 保护的背景下,如何构建一套坚固、可扩展且具备“多策略适配”能力的身份验证架构,已成为决定全栈系统安全等级与用户信任度的基石。在鸿蒙设备这类强调分布式安全域与跨端信任链的环境下,如果应用依然依赖硬编码的简单鉴权逻辑,由于由于身份上下文的复杂性,极易由于由于“鉴权粒度过粗”导致越权访问或遭受 CSRF/XSS 等复合型攻击。 我们需要一种能够解耦认证逻辑、支持多种插拔式策略(如 JWT、Local、OAuth2)且具备高度可定制性的鉴权中间件。 angel3_auth 为 Dart 全栈开发者引入了“

By Ne0inhk
黑马点评完整代码(RabbitMQ优化)+简历编写+面试重点 ⭐

黑马点评完整代码(RabbitMQ优化)+简历编写+面试重点 ⭐

简历上展示黑马点评 完整代码地址 微服务学成在线项目 前言 当初就是当作一个学习笔记和个人面试记录发的,没想到这么多人收藏浏览,还是感慨学Java的人确实多啊。 适合什么人看呢,我仅仅说说我个人的理解,因为我现在也是个经历秋招的双非学生。 1.初学者学习完Redis基础,想来个实战,黑马点评还是特别好的一个项目,基本包含了所有数据类型的运用和redis其他功能的扩展,这篇文章可以带你提炼重点,很好的走下流程。 2.但大部分人是冲着找实习和秋招去的,像我这种学历不高的秋招就不要写黑马点评了,即使包装,也会很容易看出来,我找实习的时候就被面试官问到这是不是黑马点评过,我们可以把其中的闪光点迁移到你找的其他项目中,比如缓存穿透雪崩击穿的解决方法,redisson分布式锁解决一人一单,这种在大多项目中都可以添加,自圆其说就行。 3.对于找实习的像大二,大三上的,想找个小厂试试手垂直向上升的,可以吃透它,面试官问你遇到的困难或者是你觉得难点,就可以重点讲一人一单这个解决方法和流程,越详细越好。 4.前提是大家不用直接用这套模板,太多人用了,这也是我从网上找的别人的,巧用AI让它改改项

By Ne0inhk