【C++从练气到飞升】04---拷贝构造函数

 

 🎈个人主页:库库的里昂
收录专栏:C++从练气到飞升
🎉鸟欲高飞先振翅,人求上进先读书

目录

⛳️推荐

一、拷贝构造函数的引入

1. 以日期类为例:进行的值拷贝是不会发生错误的

2. 以栈类为例:进行的值拷贝会发现发生错误

二、拷贝构造函数

1. 拷贝构造函数的概念

2. 拷贝构造函数的特征

3. 拷贝构造函数针对自定义类型,自定义类型的对象在拷贝的时候,C++规定必须要调用拷贝构造函数

4. 拷贝构造函数的典型调用场景


⛳️推荐

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站

一、拷贝构造函数的引入

对于上章节的学习我们认识并了解了两大默认成员函数:构造函数和析构函数。构造函数主要用来进行对象的成员变量初始化操作,而析构函数主要用来对战斗后的战场做清理工作。当我们不写这些函数时,编译器会自动生成默认的构造与析构函数,但有时候,编译器生成的并不能满足我们对代码的需求,这就需要我们自己去写了(比如Stack类),所以要根据情况的不同而去选择性的写。
此外就引入一个问题,假设我们需要创建一个对象和已经存在的对象一模一样那应该怎么办呢?显然易见的答案就是拷贝,但真的只是简简单单的拷贝吗?通过对比如下两种类的拷贝:

1. 以日期类为例:进行的值拷贝是不会发生错误的

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream> 
#include<assert.h>
using namespace std;
class Date
{
public:Date(int year = 1,int month = 1,int day = 1){_year = year;_month = month;_day = day;}void Printf(){cout << _year<<"/" << _month << "/" << _day << endl;}
private:int _year = 1;int _month;int _day;
};void func1(Date d)
{d.Printf();
}int main()
{Date d1(2023, 9, 12);func1(d1);return 0;
}

2. 以栈类为例:进行的值拷贝会发现发生错误

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream> 
#include<assert.h>
using namespace std;class Stack
{
public:Stack(size_t n = 4){cout << "Stack(size_t n=4)" << endl;if (n == 0){a = nullptr;top = capacity = 0;}else{a = (int*)malloc(sizeof(int) * n);if (a == nullptr){perror("realloc fail");exit(-1);}top = 0;capacity = n;}}void Init(){a = nullptr;top = capacity = 0;}void Push(int x){if (top == capacity){size_t newcapacity = capacity == 0 ? 4 : capacity * 2;int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);if (tmp == nullptr){perror("realloc fail");exit(-1);}if (tmp == a){cout << capacity << "原地扩容" << endl;}else{cout << capacity << "异地扩容" << endl;}a = tmp;capacity = newcapacity;}a[top++] = x;}~Stack(){cout << "~Stack()" << endl;free(a);a = nullptr;top = capacity = 0;}int Top(){return a[top - 1];}void Pop(){assert(top > 0);--top;}void Destroy(){free(a);a = nullptr;top = capacity = 0;}bool Empty(){return top == 0;}
private:int* a;int top;int capacity;
};void func2(Stack s)
{}int main()
{Stack s1;func2(s1);return 0;
}

报错原因

🌟同样的拷贝方式为什么对于日期类不会报错,而对于栈类就会报错呢?

解决方式:

  • 采用引用
void func2(Stack& s)
{引用没有值拷贝的问题,s就是s1别名,没有两个对象指向同一块空间的这种说法(这是一个对象)
}

🌟这里有一个误解:采用引用它不是也会析构吗? —> 同一个对象不会析构两次,s是s1的别名,s不析构,不调用析构函数

但是采用引用,s的修改也会影响s1,那如何让s改变且不影响s1?这就需要引入拷贝构造函数

二、拷贝构造函数

1. 拷贝构造函数的概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

2. 拷贝构造函数的特征

拷贝构造函数也是特殊的成员函数,其特征如下:

  • 🌏拷贝构造函数是构造函数的一个重载形式。
  • 🌏拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
#include<iostream> 
using namespace std;
class Date
{
public:Date(int year = 1,int month = 1,int day = 1){_year = year;_month = month;_day = day;}Date(Date& d){cout << "Date(Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}void Printf(){cout << _year<<"/" << _month << "/" << _day << endl;}
private:int _year = 1;int _month;int _day;
};int main()
{Date d1;以下两种写法是等价的Date d2(d1);  调用拷贝构造Date d3 = d1;   调用拷贝构造定义了一个日期类对象d1,然后想再创建一个和d1一模一样的日期类对象d2,也就是用d1去拷贝d2return 0;
}

😽 注意—>拷贝构造的错误写法:引发无穷递归

对于下述代码按常规理解就是创建d2对象的时候,把d1传过去,然后用形参d接收,再把d的值赋值给this指针(this指针指向的是d2,也就是赋值给了d2)并不会发现有任何错误

//Date d2(d1);
Date(Date d){_year = d._year;_month = d._month;_day = d._day;}

其实编译器是会报错因为底层发生了错误

😽根据下述图解来探索一下引发无穷递归的原因

如上图所示,执行date d2(d1);调用拷贝构造函数, d1传参给拷贝构造的形参d, 形参d在接收实参d1的时候,又要去调用拷贝构造来创建d,所以会出现 date d(d1),而拷贝的过程中又会调用自身的拷贝构造函数,会无休止的递归下去。


😽 对于上述的错误可以采用下述方法进行规避:
采用引用的方法:Date d2(d1);调用拷贝构造,d1传给了d,且d是d1的别名,this指针就是d2,这样d1就拷贝给了d2,此时就不会再去无穷无尽的调用拷贝构造

//Date d2(d1);
Date(Date& d){_year = d._year;_month = d._month;_day = d._day;}

3. 拷贝构造函数针对自定义类型,自定义类型的对象在拷贝的时候,C++规定必须要调用拷贝构造函数

  • 🌏若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
}

😽注意:建议写拷贝构造函数时加上const

Date(const Date& d)//d是d1的别名,权限缩小{cout << "Date(Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}

表明拷贝构造函数中没有对传递进来的对象做任何修改,也是防止拷贝构造函数对对象进行修改,实际上不加const也是可以照常运行的。不过还是建议:不需要改变对象时,传引用时加上const

  • 🌏编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类(浅拷贝)这样的类是没必要的。但是对于栈类(深拷贝)的对象,是必须要显示写的,不然会出现析构两次的问题
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}
 

  • s1对象调用构造函数创建,在构造函数中,默认申请了10个元素的空间然后里面存了4个元素12 3 4
  • s2对象使用s1拷贝构造,而Stack类没有显式定义拷贝构造函数,则编译器会给Stack类生成一份默认的拷贝构造函数,默认拷贝构造函数是按照值拷贝的,即将s1中内容原封不动的拷贝到s2中。因此s1和s2指向了同一块内存空间。
  • 当程序退出时,s2和s1要销毁。s2先销毁,s2销毁时调用析构函数,已经将0x11223344的空间释放了,但是s1并不知道,到s1销毁时,会将0x11223344的空间再释放一次,一块内存空间多次释放,肯定会造成程序崩溃。

😽注意:
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
😽综上所述栈类是要自己写拷贝构造的具体如下:

深拷贝就是去堆上重新申请一块空间,把s1中_array指向的空间中的内容,拷贝到新申请的空间,再让s2中的_array指向该空间

	Stack(const Stack& s){cout << "Stack(Stack& s)" << endl;//深拷贝_array = (DataType*)malloc(sizeof(DataType) * s._capacity);if (NULL == _array){perror("malloc申请空间失败");return;}memcpy(_array, s._array, sizeof(DataType)*s._size );_size = s._size;_capacity = s._capacity;}

😽总结:
我们不写,编译默认生成的拷贝构造,跟之前的构造函数特性不一样:

  • 内置类型,值拷贝(浅拷贝)
  • 自定义的类型,调用他的拷贝
  • Date不需要我们实现拷贝构造,默认生成就可以用

  • Stack需要我们自己实现深拷贝的拷贝构造,默认生成会出问题(析构两次)

4. 拷贝构造函数的典型调用场景

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d)
{Date temp(d);return temp;
}
int main()
{Date d1(2022, 1, 13);Test(d1);return 0;
}

   

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位读者三连支持。文章有问题可以在评论区留言,博主一定认真认真修改,以后写出更好的文章。你们的支持就是博主最大的动力。


  •  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/760452.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

oracle实现批量插入

一、Dao层(增加Parm参数) void insert(Param("list") List<TicketInfo> ticketInfos); 二、Mapper层(加入条件判断值是否为空) insert all<foreach collection"list" item"item" index"index">into 表名<trim prefix…

批量重启某个节点的pod

在我们日常运维工作中&#xff0c;经常有下线某个节点的需求&#xff0c;如果采用直接驱逐&#xff0c;会导致服务不可用&#xff0c;故采用重启的方式&#xff0c;可以实现新pod 漂移到新节点上。以下是采用shell 命令的方式重启某个节点的pod。 前置条件&#xff1a;已把下线…

【一竞技CS2】pasha将骑行前往哥本哈根Major

1、传奇老将pasha最近发文表示&#xff1a;自己将在3月23日&#xff0c;从华沙的西吉斯蒙德圆柱出发&#xff0c;开始了前往哥本哈根Major的旅程。 去年pasha于当地时间5月12日清晨出发&#xff0c;并一路开启直播记录自己骑行前往巴黎的旅程。最终他于5月19日抵达埃菲尔铁塔。…

05-shell编程-比较判断

一、判断文件/目录是否存在 1&#xff0c;判断文件是否存储在 [rootgong ~]# [ -f /etc/hosts ] && echo "它存在" || echo "它不存在" 它存在 [rootgong ~]# [ -f /etc/hostssssss ] && echo "它存在" || echo "它不存在…

基于python+vue智慧农业小程序flask-django-php-nodejs

传统智慧农业采取了人工的管理方法&#xff0c;但这种管理方法存在着许多弊端&#xff0c;比如效率低下、安全性低以及信息传输的不准确等&#xff0c;同时由于智慧农业中会形成众多的个人文档和信息系统数据&#xff0c;通过人工方法对知识科普、土壤信息、水质信息、购物商城…

AVL树的实现

概念 1.为了解决二叉搜索树有序插入&#xff0c;会退化成链表&#xff0c;导致效率低下。 AVL树的左右子树高度差不超过1&#xff0c;所以AVL树的查找效率为logn。 2.在左子树高度增加&#xff0c;平衡因子减一&#xff0c;在右子树高度增加&#xff0c;平衡因子加一。 节点…

C# winform修改背景图 控件双向绑定 拖拽打开图片

修改背景图 说明 这里我准备基于百度飞桨PaddleSeg项目的人像分割模块做一个人像抠图&#xff0c;这里顺便用上了双向绑定和图片拖拽打开。 下面就是示例&#xff1a; 用颜色替换 用背景图替换 保存成功后的图片 一、使用百度飞桨PaddleSeg //初始化 引擎engine new Padd…

如何配置VS Code环境

一、下载 Visual Studio Code - Code Editing. Redefined 二、傻瓜式安装 如果出现没有安装路径选择&#xff0c;则看下面图片 经过上面操作后&#xff0c;可以修改路径 三、按照下面步骤配置环境变量即可 Visual Studio Code 中的 C 和 MinGW-w64 入门

数据库学习之数据库基本知识

什么是数据库 数据&#xff1a;描述事物地符号记录&#xff0c;包括单不限于数字、文字、图形、图像、声音、语言等。数据有多种形式&#xff0c;这个内容都可以经过式子化地处理后存入计算机。 数据库&#xff1a;数据仓库。是长期存放再计算机内、有组织、可共享地大量数据…

UE5 C++增强输入

一.创建charactor&#xff0c;并且包含增强输入相关的头文件 1.项目名.build.cs。添加模块“EnhancedInput”&#xff0c;方便找到头文件和映射的一些文件。 PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine&q…

在基于Android相机预览的CV应用程序中使用 OpenCL

查看&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV4.9.0在Android 开发简介 下一篇&#xff1a;在 MacOS 中安装 本指南旨在帮助您在基于 Android 相机预览的 CV 应用程序中使用 OpenCL ™。教程是为 Android Studio 20…

用图解说明mysql 行锁加锁规则

加锁原则 原则 1&#xff1a;加锁的基本单位是 next-key lock。希望你还记得&#xff0c;next-key lock 是前开后闭区间。原则 2&#xff1a;查找过程中访问到的对象才会加锁。优化 1&#xff1a;索引上的等值查询&#xff0c;给唯一索引加锁的时候&#xff0c;next-key lock …

【Java】Oracle发布Java22最新版本

甲骨文&#xff08;ORACLE&#xff09;已经于2023年3月19日正式发布了最新版本的JDK&#xff0c;版本号&#xff1a;22 根据官方声明&#xff0c;Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进&#xff0c;包括对Java 语言、其API 和性能&#xff0c;以…

python vtk读取vtk文件

参考&#xff1a; https://cloud.tencent.com/developer/ask/sof/101993637 方法一&#xff1a;使用pyvtk 要使用Python读取VTK文件&#xff0c;可以使用pyvtk库。首先&#xff0c;确保已经安装了pyvtk。如果没有安装&#xff0c;可以通过pip安装&#xff1a; csharp pip ins…

Github基本功能和使用技巧

基础功能 创建仓库&#xff08;Repository&#xff09;&#xff1a;在GitHub上创建一个新的仓库&#xff0c;可以通过点击页面右上角的“New”按钮开始。选择仓库的名称、描述和许可证等信息&#xff0c;并选择是否将仓库设置为公开或私有。 克隆仓库&#xff08;Clone&#x…

基于stable diffusion的IP海报生成

【AIGC】只要10秒&#xff0c;AI生成IP海报&#xff0c;解放双手&#xff01;&#xff01;&#xff01;在AIGC市场发展的趋势下&#xff0c;如何帮助设计工作者解放双手。本文将从图像生成方向切入&#xff0c;帮助大家体系化的学习Stable diffusion的使用&#xff0c;完成自有…

php 对接IronSource海外广告平台收益接口Reporting API

今天对接的是IronSource广告reporting api接口&#xff0c;拉取广告收益回来自己做统计。记录分享给大家 首先是文档地址,进入到IronSource后台就能看到文档地址以及参数&#xff1a; 文档地址&#xff1a;https://developers.is.com/ironsource-mobile/air/reporting/ 在这里插…

【Rust】——String集合

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

高新技术企业培育认定条件

高新技术企业认定申报条件主要包括企业基本条件、技术创新能力和成果、知识产权、人才队伍建设等方面。 1.企业基本条件 &#xff08;1&#xff09;具有独立法人资格&#xff1b; &#xff08;2&#xff09;注册地在中国境内&#xff1b; &#xff08;3&#xff09;注册资本…

Mybatis一级缓存和二级缓存区别

Mybatis一级缓存 1.为什么需要Mybatis一级缓存 当我们使用Mybatis进行数据库的操作时候&#xff0c;会创建一个SqlSession来进行一次数据库的会话&#xff0c;会话结束则关闭SqlSession对象。 如果我们很有可能多次查询完全相同的sql语句&#xff0c;每一次查询都查询一次数据…