【C++】第二站:类和对象(中)拷贝构造函数

news2024/11/16 9:48:31

文章目录

  • 一、拷贝构造函数的概念
  • 二、拷贝构造函数的特性
  • 三、深度剖析拷贝构造函数不采用引用会无限递归的原因
    • 1.C++对于传参的两个规定
    • 2.如何解开这个无穷递归
  • 四、拷贝构造函数的其他特性
  • 五、拷贝构造的一些使用场景

一、拷贝构造函数的概念

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

二、拷贝构造函数的特性

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

根据以上的两个特性,那么我们先来简单的写一个拷贝构造函数

class Date
{
public:
	Date(int year = 2023, int month = 5, int day = 6)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& 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()
{
	Date d1(2024, 5, 6);
	d1.Print();
	Date d2(d1);
	d2.Print();
	return 0;
}

在这里插入图片描述
根据上面的这些,我们已经不难理解,拷贝构造函数其实就是一个构造函数。而一旦确定了拷贝构造函数,那么有没有了默认构造函数了,我们就得写一个默认构造函数。

三、深度剖析拷贝构造函数不采用引用会无限递归的原因

1.C++对于传参的两个规定

  1. 如果传的是内置类型,则直接拷贝给形参
  2. 如果传的是自定义类型,则通过调用拷贝构造函数拷贝给形参

根据这两条规矩,我们可以写出如下代码来进行测试

class Date
{
public:
	Date(int year = 2023, int month = 5, int day = 6)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		cout << "Date(Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void func(int i)
{

}
void func(Date d)
{

}
int main()
{
	//Date d1(2024, 5, 6);
	//d1.Print();
	//Date d2(d1);
	//d2.Print();
	Date d1;
	func(10);
	func(d1);
	return 0;
}

运行结果如下所示:
在这里插入图片描述可见确实在传参的时候调用了拷贝构造函数
在这里插入图片描述

这样一来我们就明白了,一旦,我们是不采用引用,直接传值的话,那么在拷贝给形参的过程中又要去调用拷贝构造函数,这个拷贝构造又要传参去调用新的拷贝构造…所以自然就会发生无穷递归了
在这里插入图片描述

2.如何解开这个无穷递归

  1. 采用指针,指针是内置类型,所以是直接拷贝。不会引发无穷递归,但是我们在使用拷贝构造函数的时候就需要传地址过去了
  2. 采用引用,这样一来我们直接传的是它的别名。不会产生新的空间消耗,自然就不会发生无穷递归了,C++中也是最好采用这种方法,在我们使用引用的时候,最好加上const进行修饰,这样别名的权限就缩小了。使得我们不会做出一些危害原本空间的事情.

四、拷贝构造函数的其他特性

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:内置类型成员变量是直接拷贝的/浅拷贝
而自定义类型成员变量需要调用它的拷贝构造函数

我们来测试一下这条规则
在这里插入图片描述

事实上,上面的情形是基于第一条的场景,如果对于自定义类型,如果是栈的话,会出现一些意想不到的情形

typedef int DataType;
class Stack
{
public:
	Stack()
	{
		cout << "Stack" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * 4);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = 4;
		_size = 0;
	}

	void Push(DataType data)
	{
		CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}
	DataType Top() { return _array[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }
	~Stack()
	{
		cout << "~Stack" << endl;
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
			if (temp == nullptr)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}
private:
	DataType * _array;
	int _capacity;
	int _size;
};
class Date
{
public:
	Date(int year = 2023, int month = 5, int day = 6)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(Date& d)
	//{
	//	cout << "Date(Date& d)" << endl;
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void func(int i)
{

}
void func(Date d)
{

}
int main()
{
	Date d1(2024, 5, 6);
	d1.Print();
	Date d2(d1);
	d2.Print();
	//Date d1;
	//func(10);
	//func(d1);
	Stack s1;
	Stack s2(s1);
	return 0;
}

在这里插入图片描述这里我们发现它报错了,具体报错的原因,我们可以通过调试分析,我们可以发现,它在析构函数中出现了错误。我们可以发现,我们调用了两次析构,一次是对是说s2的释放,一次是对是s1的释放,而巧的是,由于我们的栈中是由指针控制的数组,所以当成了值拷贝,仅仅只是拷贝了指针指向的地址。就会产生同一块空间释放两次的结果,自然就会报错了。需要注意的是,调用析构的时候,是按照栈的特性,后定义的先释放。
在这里插入图片描述在这里插入图片描述
这也就是我们为什么所说的编译器生成的其实是浅拷贝的原因。
这时候就需要我们自己去写一个深拷贝来解决了,我们先来简单的写一个简单的深拷贝

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

在这里插入图片描述事实上,这时候我们也理解了为什么C++规定,自定义类型必须调用拷贝构造函数,如果不调用拷贝构造函数,那么如果在一个函数中传参传的是一个栈的话,就会产生将同一块空间析构两次的现象。而且也会产生一个栈的值改变了,影响另外一个栈的值的现象

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

我们在来分析一下,对于这个队列,事实上其实就需要写自己的拷贝构造函数了,因为可以直接调用它们的拷贝构造函数

class myQueue
{
private:
	Stack push_st;
	Stack pop_st;
};

五、拷贝构造的一些使用场景

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

使用对象创建新对象和函数参数我们已经了解了。在这期间会调用拷贝构造函数来实现拷贝
那么如果是对于返回值类型为类类型对象的话

如下所示,该过程比较复杂:

  1. 先进入func函数,调用s的构造函数,打印Stack
  2. 打印func函数中的字符串
  3. 由于是传值返回,会产生临时变量,将s的值调用拷贝构造函数给这个临时变量,打印深拷贝构造中的字符串
  4. 释放s,调用析构函数,打印析构的字符串
  5. 将临时变量中的数据直接拷贝给ret,然后释放临时变量调用析构函数,打印析构中的字符串
    在这里插入图片描述

如果我们选择传引用返回的话,程序直接崩溃。
在这里插入图片描述
下面具体分析原因:

  1. 进入func函数,创建s,调用构造函数
  2. 打印func中的字符串
  3. 由于传引用返回不创建临时变量,所以直接调用析构
  4. 此时s的空间已经归还,继续将s的别名返回,相当于将s拷贝给ret,调用拷贝构造函数
  5. 由于在析构函数中,已经将指针置空。我们在拷贝构造函数中的memcpy已经是野指针访问了,报错
  6. 野指针访问虽然是直接原因,但如果没有这个原因的话,也会因为拷贝后的第二次析构而报错

好了本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!

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

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

相关文章

2.2 Linux控制台访问CLI

系列文章目录 第1章 Linux Shell简介 第2章 Shell基础 <本章所在位置> 第3章 Bash Shell基础命令 第4章 Bash Shell命令进阶 第5章 Linux Shell深度理解 第6章 Linux环境变量 第7章 Linux文件权限 第8章 Linux文件系统的管理 第9章 Linux软件安装 第10章 Linux文本编辑器…

【MySQL】搭建出高可用性、高性能的MySQL集群要考虑的事是蛮多的,你看看会不会?

MySQL 架构设计数据同步负载均衡安全性监控和维护注意的点1. 确定节点数量和配置2. 选择合适的硬件和网络设备3. 避免单点故障4. 定期备份和恢复测试5. 定期更新和升级 Java工程师使用集群步骤最后 MySQL集群是一种高可用性、高性能的数据库解决方案&#xff0c;它可以通过多个…

基于Django实现的TMS物流管理系统(附源码下载)

基于Django实现的物流管理系统&#xff08;TMS&#xff0c;Transportation Management System&#xff09; 特点 前端基于Bootstrap 4框架和AdminLTE框架。使用MySQL作为数据库后端。实现了运单录入、发车出库、到货签收、客户签收等基本功能。拥有较为完善的报表功能和财务管…

Java—JDK8新特性—Lambda表达式【内含思维导图】

目录 JDK8新特性 2.Lambda表达式 思维导图 2.1 什么是Lambda表达式 2.2 为什么使用Lamdba表达式 2.3 Lambda表达式基本语法 2.4 类型推断 2.5 Lambda练习 2.6 Lambda常用场景 JDK8新特性 官网提供网址&#xff1a;JDK 8 Features 2.Lambda表达式 思维导图 2.1 什么是…

浅谈Dom和Bom(清晰易懂版)

DOM&#xff08;文档对象模型&#xff09; DOM 是浏览器提供的一种操作网页内容和结构的 API&#xff0c;它将 Web 页面表示为一个树形结构&#xff0c;其中每一个 HTML 元素都是一个节点&#xff0c;可以通过 DOM API 对其进行访问和操作。DOM API 包括了一系列方法和属性&am…

Shapes布局-文字环绕动画

文章目录 说明实现以及语法动画渐变裁切图形变换的动画效果 说明 Shapes也有形状、图形的意思&#xff0c;我们可以在页面中创建图形&#xff0c;并让内容环绕在定义的图形边上。 Shapes的官方文档&#xff1a;https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Shapes/F…

YOLOv8 来了,快速上手实操

目录 YOLOv8的优点安装ultralytics使用YOLOv8n在图像上进行PredictTasks与 ModesModes - 模式分类Tasks - 任务分类 &#x1f468;‍&#x1f4bb; 作者简介&#xff1a;程序员半夏 , 一名全栈程序员&#xff0c;擅长使用各种编程语言和框架&#xff0c;如JavaScript、React、N…

SpringBoot集成Redis—缓存穿透解决方案与哨兵模式实战

目录 1、环境准备 1&#xff09;pom.xml引入Redis依赖 2) 演示业务场景 2、SpringBoot集成Redis单机模式 1&#xff09; 通过MyBatis逆向工程生成实体Bean和数据持久层 2) application.yml 中配置redis连接信息 3) 启动redis服务 4) XinTuProductRedisController类 5…

一图看懂 yarl 模块:为URL解析和更改提供了方便的URL类, 资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 yarl 模块&#xff1a;为URL解析和更改提供了方便的URL类, 资料整理笔记&#xff08;大全&#xff09; 摘要模块图类关系图模块全展开【yarl】统计常量模块1 yarl._quoting…

Python图形界面开发——系统资源监视器System-Monitor

Python图形界面程序怎么开发呢&#xff1f;很多人推荐python自带的tkinter自带库&#xff0c;还有pyqt这个这种拖拽式界面开发方案&#xff0c;但是他们开发界面比较难定制界面样式。现在web前端这么多框架用来开发python的图形界面其实不是很好&#xff1f;下面这么案例就是用…

Python爬虫 | 一文解决文章付费限制问题

本文概要 本篇文章主要介绍利用Python爬虫爬取付费文章&#xff0c;适合练习爬虫基础同学&#xff0c;文中描述和代码示例很详细&#xff0c;干货满满&#xff0c;感兴趣的小伙伴快来一起学习吧&#xff01; &#x1f31f;&#x1f31f;&#x1f31f;个人简介&#x1f31f;&…

项目内训(2023.5.6)

目录 Nacos是什么&#xff1f; 领域模型是什么&#xff1f; domain模块一般是干什么的&#xff1f; 在小乌龟中合并其他分支的作用是什么&#xff1f; nacos的配置文件 服务集群、服务提供、服务更加灵活庞大、消费服务、访问比较麻烦&#xff0c;A和B服务一起访问 系统结…

Qt5.9学习笔记-事件(四)Qt5.9中常见事件

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的在读研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三…

5月1日 9H45min|5月2日 8H20min+30min|时间轴复盘

8:00 起床 8:00-8:30 洗漱吃饭 8:30-10:40 temporary pools阅读真题精读 (真的很慢了 不知道什么原因 感觉也没有彻底完全弄懂)【2h+10min】 10:40-11:10 午餐+酸奶(423+174KJ) 11:20-12:30 三篇阅读【1h+10min】 13:10-14:50 健身 14:50-15:45诵默写list…

【Stable Diffusion】ControlNet基本教程(四)

本文概要 接上篇【Stable Diffusion】ControlNet基本教程&#xff08;三&#xff09;&#xff0c;本篇再介绍两个ControlNet常见的基本用法&#xff1a;控制人物动作和建筑/室内生成。让人物摆出特定的动作&#xff0c;这是ControlNet最神级的操作&#xff01;这意味着可以自定…

密码学【java】初探究之springboo集成mybatis,swagger,数字签名

文章目录 项目环境一 swagger技术的补充1.1 [swagger](&#xff08;https://github.com/OAI/OpenAPI-Specification&#xff09;)介绍1.2 swagger的基础注解1.3 controller添加swagger注解 二 项目搭建2.1 创建数据库2.2 引入项目依赖2.3 配置数据库的连接2.4 配置swagger的配置…

USB 字节序,编码格式及位填充

字节序 LSB 发送一个字节时&#xff0c;先发送低位数据&#xff0c;再发送高位数据发送一个字时&#xff0c;先发送低字节数据&#xff0c;再发送高字节数据 例如&#xff1a; 发送 0x2D&#xff0c;发送的顺序为&#xff1a;10110100(低位在前&#xff0c;高位在后)发送 0…

项目前置准备

目录 项目前置准备 总体架构 CVPR2022是什么 一个项目架构图要如何进行看和学习呢&#xff1f;内容有点多有些摸不着头脑 我该如何理解架构图中的组件 Jenkins是什么&#xff1f; Docker是什么&#xff1f; FastDFS是什么&#xff1f; 项目前置准备 总体架构 CVPR2022是什…

golang grpc配置使用实战教程

什么是PRC&GRPC RPC是远程过程调用&#xff08;Remote Procedure Call&#xff09;的缩写形式, RPC 的主要功能目标是让构建分布式计算&#xff08;应用&#xff09;更容易&#xff0c;在提供强大的远程调用能力时不损失本地调用的语义简洁性。通俗地讲&#xff0c;使用RP…

软考 软件设计师 数据结构

大O表示法 常数阶&#xff0c;他的次数不会随着n的变大而变长 抓大头 取次方最大的 时间复杂度 没有循环 没有递归没有跟n相关的东西&#xff0c;那么他的复杂度就是o&#xff08;1&#xff09; 为什么ii*2那里会加1阿&#xff1f; 因为需要加一次才能跳出循环1 2 4 8 中间加…