深入篇【C++】类与对象:拷贝构造函数详解

news2024/11/17 13:46:49

深入篇【C++】类与对象:拷贝构造函数详解

  • ①.拷贝构造函数
    • Ⅰ.概念
    • Ⅱ.特征
      • 1.重载形式之一
      • 2.参数唯一
      • 3.形参必须传引用
      • 4.编译器的拷贝函数
      • 5.典型调用场景
  • ②.总结:

在这里插入图片描述

①.拷贝构造函数

Ⅰ.概念

在创建对象时,能否创建一个与已存在对象一模一样的新对象呢?

拷贝构造函数:只有单个形参,该形参是对本类型相同的对象的引用,一般是用const修饰,在用已存在的对象创建一个同类型的新对象时由编译器自动调用。

为什么要用const修饰?

防止将拷贝对象修改,我们只是要将拷贝对象,并不能将对象修改了。所以加上const来修饰拷贝的对象,防止错误修改。

Ⅱ.特征

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

1.重载形式之一

拷贝构造函数是构造函数的一个重载形式。
函数名字跟类名是一样的,只是参数列表不同

	Data(int year =2023, int month = 5, int day=4)//构造函数
	{
	 	_year = year;
		_month = month;
		_day = day;
	}
	//两个函数构成重载
	Data(const Data& d)//拷贝构造函数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

2.参数唯一

拷贝构造的函数参数只有一个,理论上是两个的,一个是已存在的要被拷贝的对象,一个是新创建的要拷贝的对象,但新创建的对象传给了隐藏的this指针了。所以显示的只有一个参数。
在这里插入图片描述

3.形参必须传引用

构造函数的参数只有一个且必须是类类型对象的引用,如果使用传值方式编译器会直接报错,因为这样会引发无穷递归调用。

class Data
{

public:
	Data(int year =2023, int month = 5, int day=4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Data (const Data d)//这种形式是错误的,不可以这样写,不能传值过去
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Data(const Data& d)//正确的写法是这样,传引用过去
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year <<"-"<< _month <<"-"<< _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1(2023, 5, 3);
	Data d2(d1);
	d2.Print();

}

我们知道调用函数需要先传参,而对应内置类型,传参是直接以字节形式拷贝过去,但自定义类型就必须要用拷贝构造形式去完成。

也就是C++规定:在传参过程中
1.内置类型是直接拷贝过去的。
2.自定义类型必须调用拷贝构造完成拷贝。

所以对于自定义类型传参,如果使用传值形式,则传参就相当于又形参了一个新的拷贝构造函数,为什么呢?
因为采用传值形式的话,那么形参就是实参的一份拷贝。
将实参传过去,那么就必须调用一次拷贝构造函数形成形参。
所以如果传值过去,编译器会强制检查发现这样会引发无穷递归调用拷贝构造函数,然后报错。
在这里插入图片描述
所以形参必须给该类对象的引用。
当调用拷贝构造函数时,传参就不需要再调用拷贝函数了,因为传参使用的是引用传参,参数不是实参的一份临时拷贝,而就是实参本身,只不过是别名。
在这里插入图片描述
因为拷贝构造函数也是特殊的成员函数,是由编译器自动调用的,所以我们可以不去显示的去调用,编译器会帮我们调用,这是在拷贝构造函数已经写的情况下。

4.编译器的拷贝函数

如果没有显式的定义拷贝函数,那么编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数对象按照内存存储按照字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

默认生成的拷贝函数:
1.内置类型完成值拷贝/浅拷贝。
2.自定义类型会调用相对应的拷贝构造。

对于不需要申请动态资源的对象,浅拷贝就可以完成工作。

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& s)
	{
		_hour = s._hour;
		_minute = s._minute;
		_second = s._second;
		cout << "Time(const Time& s)" << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};
class Data
{

public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private://内置类型
	int _year=1;
	int _month=1;
	int _day=1;
	//自定义类型
	Time _t;
};
int main()
{
	Data d1;
	//用已存在的d1拷贝构造d2,这里会调用Data类的拷贝构造
	//但Data类没有显式的定义,所以编译器会生成一个默认的拷贝构造。
	//默认生成拷贝构造能否完成拷贝工作呢?
	Data d2(d1);

	d2.Print();
}

默认生成的拷贝构造是可以完成上面的拷贝任务的,因为默认的拷贝构造会对对象进行浅拷贝,而该场景就适合浅拷贝,因为没有动态资源的开辟,虽然Time定义的是自定义类型,但是浅拷贝可以完成任务就行了。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那还需要自己显式的写拷贝构造函数吗?当然对于Data日期类的是没有写的必要,但并不是每个类都是像日期类一样的。
当对象的拷贝需要深度拷贝时,就不能单单使用浅拷贝,这样会出问题的。
比如下面这个栈:

typedef int DataType;
struct stack//class可以定义一个类
{
public://访问限定符
	stack(int capacity = 4)//缺省值
	{
		cout << "stack(int capacipty=4)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		_array[_size] = data;
		_size++;
	}
	void Pop()
	{
		if (Empty())
		{
			return;
		}
		--_size;

	}
	int Empty()
	{
		return _size == 0;
	}

	~stack()
	{
		cout << "~stack()" << endl;
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private://访问限定符
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
	stack s1;//定义一个对象
	stack s2(s1);

根据调试可以发现d1初始化,然后将已存在的d1拷贝给新创建的d2。看起来都完美,但其实有一个致命的错误。
在这里插入图片描述
我们知道默认生成的拷贝函数是按照浅拷贝进行拷贝的,浅拷贝就是完全一样,全部复制过来。
这样是存在危险的,因为可能存在这样的情况:拷贝对象与被拷贝对象的成员指向了同一块空间。
在这里插入图片描述
这种情况会存在这样的问题:
1.同一块空间会析构两次,会报错。
当s2对象生命周期结束时,系统自动调用析构函数来清理数据,那么*a指向的空间就被销毁了。
而当s1对象生命周期结束时,又析构一次相同的空间,这样同一块空间就析构两次了。
在这里插入图片描述
编译器会出错的。

2.一个变量修改会影响另一个变量,因为两个变量都存在同一块空间里。

在这里插入图片描述

【注意:】
=类中如果没有涉及资源的申请时,拷贝构造函数是否写都是可以的;一旦涉及资源的申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝了。

编译器默认生成的拷贝构造只能完成浅拷贝,而需要深度拷贝时还必须用我们自己写的拷贝构造函数。
其实自己写的拷贝构造函数就是为自定义类型的深度拷贝准备的。
所以总结一下,适合深度拷贝和浅拷贝的场景:

1.对于Data和MyQueue这样的类我们不需要自己写拷贝构造,因为浅拷贝就可以完成任务。
(MyQueue就是用栈来实现队列,而栈里面是要用自己写的拷贝函数,但实现队列时就不需要了)

class MyQueue
{
private:

	stack pushst;
	stack popst;
};

2.对于Stack这样的类,我们是需要自己写拷贝构造的,因为里面涉及要深度拷贝,有动态资源的开辟。

5.典型调用场景

1.使用已存在的对象创建新对象。
2.函数参数类型为类类型对象。
3.函数的返回值类型为类类型对象。

class Data
{

public:
	Data(int year =2023, int month = 5, int day=4)
	{
	 	_year = year;
		_month = month;
		_day = day;
	}
	Data(const Data& d)//正确的写法是这样,传引用过去
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year <<"-"<< _month <<"-"<< _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
//典型调用场景:返回值是类类型对象,参数是类类型对象
Data Test(Data d)
{
	Data tmp(d);//用已存在的d(其实是d2)来创建新对象tmp
	return tmp;//返回对象tmp
}
int main()
{
	Data d1(2023, 5, 3);
	Data d2(d1);
	Data tmp=Test(d2);
	d2.Print();
}

在这里插入图片描述

在这里插入图片描述
注意:
为了提高此程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
就比如上面的Test函数,对象传参我们最好使用引用类型,那样就减少了一次调用拷贝函数的工作,提高了效率

Data Test(Data& d)
{
	Data tmp(d);//用已存在的d(其实是d2)来创建新对象tmp
	return tmp;//返回对象tmp
}

那能不能给返回值也使用引用呢?
答案是不能,要根据实际场景来对返回值使用引用,这样是对局部对象返回,不能使用引用,因为局部对象返回后,这个对象就销毁了,使用引用取别名那就对已经销毁的空间的非法访问了。所以不可以。

②.总结:

  • 1.拷贝构造函数是构造函数的一个重载形式。
  • 2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
    因为会引发无穷递归调用。
  • 3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  • 4.在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
    义类型是调用其拷贝构造函数完成拷贝的。
  • 5.类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
    时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
  • 6.拷贝构造函数典型调用场景:
    使用已存在对象创建新对象
    函数参数类型为类类型对象
    函数返回值类型为类类型对象

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

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

相关文章

[架构之路-186]-《软考-系统分析师》-5-数据库系统 - 关系型数据库、操作型数据库、数据集市、数据仓库的区别?

目录 总体架构&#xff1a; 一、数据库 二、关系型数据库 三、SQL与数据库 四、 分布式数据库 五、数据仓库 1. 概述 2. 架构 3、数据仓库的主要特征 3、1 面向主题性 3、2 集成性 3、3 非易失性 3、4 时变性 总体架构&#xff1a; 一、数据库 数据库是“按照数据…

【链表OJ题 1】反转链表

目录 题目来源&#xff1a; 代码实现 1、方法一 1.1分析 2、方法二 2.1 分析 题目来源&#xff1a; 力扣 题目描述&#xff1a; 代码实现 1、方法一 struct ListNode* reverseList(struct ListNode* head) {struct ListNode* prev NULL, * cur head;while (cur){st…

基础IO+文件

基础IO 回顾文件回顾文件操作库函数调用接口写文件-w读文件-r 系统调用打开文件-open写文件-write 文件操作本质文件描述符fd文件描述符的分配规则重定向 FILE缓冲区模拟实现缓冲区总结 理解文件系统磁盘物理结构存储结构逻辑结构 文件系统软硬链接 动静态库动态库和静态库生成…

Kafka上的优化经验

1. 平滑扩容 Motivation kafka扩容⼀台新机器的流程 假如集群有 3 个 broker &#xff0c;⼀共有 4 个 TP &#xff0c;每个 3 副本&#xff0c;均匀分布。现在要扩容⼀台机器&#xff0c; 新 broker 加⼊集群后需要通过⼯具进⾏ TP 的迁移。⼀共迁移 3 个 TP 的副…

JavaScript高级程序设计(第2版)——读书笔记

文章目录 第1章 JavaScript简介第2章 在HTML中使用JavaScript第3章 基本概念第4章 变量、作用域、内存问题第5章 引用类型第6章 面向对象的程序设计第7章 匿名函数第8章 BOM第9章 客户端检测第10章 DOM第11章 DOM2和DOM3第12章 事件第13章 表单脚本第14章 错误处理与调试第15章…

java 的参数传递

一、疑惑引入 首先&#xff0c;我们从一个例子来引出这个问题&#xff1a; public static void main(String[] args) throws IOException {List<String> mockList Lists.newArrayList("a", "b");System.out.println("1: " mockList);L…

【GAMES101】04 Viewing Transformation

1.View/Camera Transformation&#xff08;视图变换&#xff09; 1、将准备拍摄的对象移动到场景中指定位置。&#xff08;模型变换&#xff0c;Model Transform&#xff09; - 模型导入场景中从模型坐标系转换成世界坐标系 2、将相机移动到准备拍摄的位置&#xff0c;将它对准…

【网络】-- TCP协议

其中TCP就属于传输层&#xff0c;并且端口号也是在传输层起作用。 目录 TCP协议报头 可靠性 32位序号 16位窗口大小 六个标记位 三次握手四次挥手 RST PSH URG 16位紧急指针 FIN socksetopt 可靠性机制 确认应答(ACK)机制 超时重传机制 连接管理机制 三大机…

03-角色维护 尚筹网

一、分页操作 目标 将角色数据进行分页显示 思路 点击后台主页面的权限管理->角色维护&#xff0c;通过view-controller进入角色分页显示的页面&#xff0c;浏览器加载页面的数据并初始化一些数据&#xff08;页码、页大小、关键词等&#xff09;&#xff0c;调用分页函…

基于Open3D的点云处理3-可视化

可视化接口 API open3d.visualization.draw_geometries(*args, **kwargs)重载函数1 draw_geometries(geometry_list, window_name’Open3D’, width1920, height1080, left50, top50, point_show_normalFalse, mesh_show_wireframeFalse, mesh_show_back_faceFalse)geometry…

B树

文章目录 B树的定义和性质为什么需要B树B树的定义 B树的模拟实现节点的数据结构B树的插入B树的删除 B树的模拟实现 B树的定义和性质 我们之前已经对 平衡搜索二叉树有了一定的了解&#xff0c;学习了两种树——AVL树 和 红黑树&#xff0c;下面介绍一下B树 为什么需要B树 数…

Nacos 服务网格⽣态

博主介绍&#xff1a;✌全网粉丝4W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战、定制、远程&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面…

为一加七Pro(LineageOs17.1 4.14内核版本)编译KernelSu

编译内核 因为一加七的内核版本是4.14&#xff0c;所以想使用kernelsu&#xff0c;需要自己将kernelsu编译到内核里。 我使用的系统是&#xff1a;LineageOS17.1&#xff0c;对于之后的lineage版本同样适用&#xff0c;只是拉取的源代码不一样。刷机教程请看&#xff1a;wsl2…

vue diff算法与虚拟dom知识整理(3) 了解h函数和虚拟节点概念 实现虚拟节点上dom树

虚拟dom之前我们也有了基本的了解 简单说 就是用js数据结构来描述html的dom结构树 首先 为什么要用虚拟dom啊&#xff1f; 官方给出的回答是 diff最小量精细化算法是发生在虚拟dom上的 也就是 我们之前说的 节点与节点比较 并不是发生在html元素上的 而是发生在js中的虚拟dom上…

【C++学习】类和对象--多态【待补充】

多态的基本语法 多态是C面向对象三大特性之一 静态多态&#xff1a; 函数重载和运算符重载属于静态多态&#xff0c;复用函数名&#xff08;函数地址早绑定&#xff0c;编译阶段确定函数地址&#xff09; 动态多态&#xff1a; 派生类和虚函数实现运行时多态&#xff08;函数地…

centos7.6 yum 安装mysql

目录 1. 删 mariadb / 自带MySQL 2 安装wget命名 3 下载并安装MySQL官方的 Yum Repository 4 使用yum安装mysql 5 启动 6 获取密码 7 登录 -> 没有获取到 就直接按回车 不输入 8 设置密码 及权限 --> root 账号所有语句报错 9 参考 1. 删 mariadb / 自带MySQL…

《计算机网络——自顶向下方法》精炼——2.7.2(TCP套接字编程)

“学习的敌人是自己的满足。” —— 叶圣陶 文章目录 TCP套接字编程TCP套接字编程概述客户进程服务器进程 TCP套接字编程 TCP套接字编程概述 TCP是一个面向连接的运输层协议&#xff0c;因此可以分为发起连接的阶段和传输阶段。 发起连接时&#xff0c;客户进程创建一个客户…

【LeetCode】221.最大正方形

221.最大正方形&#xff08;中等&#xff09; 题解 对于在矩阵内搜索正方形或长方形的题型&#xff0c;一种常见的做法是&#xff1a;定义一个二维 dp 数组&#xff0c;其中 dp[i][j] 表示满足题目条件的、以&#xff08;i,j&#xff09;为右下角的正方形或长方形属性。在本题中…

【备战蓝桥杯国赛-国赛真题】费用报销

题目链接&#xff1a;https://www.dotcpp.com/oj/problem2696.html 思路 读完题&#xff0c;再看一眼数据范围&#xff0c;这道题的做法也就确定了——DP。 DP的题目往往很容易辨识出来&#xff0c;所以我们就往DP上想了&#xff0c;第一要素是选出的所有票据里面&#xff0c…

【LeetCode】64. 最小路径和

64. 最小路径和&#xff08;中等&#xff09; 方法一&#xff1a;常规动态规划 思路 定义一个二维 dp 数组&#xff0c;其中 dp[i][j]表示从左上角开始到&#xff08;i, j&#xff09;位置的最优路径的数字和。因为每次都只能向下或者向右移动&#xff0c;所以很容易发现 dp数组…