【C++】—— 类与对象(三)

news2024/11/16 1:52:35

【C++】—— 类与对象(三)

  • 4、拷贝构造函数
    • 4.1、初识拷贝构造
      • 4.1.1、为什么要传引用
      • 4.1.2、引用尽量加上 const
    • 4.2、深入拷贝构造
      • 4.2.1、为什么要自己实现拷贝构造
      • 4.2.2、传值返回先调用拷贝构造的原因
      • 4.2.3、躺赢的 MyQueue
      • 4.2.4、传值返回与引用返回
    • 4.3、总结
  • 5、取地址运算符重载
    • 5.1、const 成员函数
    • 5.2、取地址运算符重载

4、拷贝构造函数

4.1、初识拷贝构造

  我们要先知道,拷贝构造是一个特殊的构造函数
  拷贝构造的作用是:用一个自身类的对象初始化 当前的对象

  
拷贝构造基本特点

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是自身类类型的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。其他任何额外的参数都要有缺省值(默认值)
  • C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参传值返回都会调用拷贝构造完成

  
我们先用 D a t e Date Date 类型来感受一下拷贝构造:

在这里插入图片描述

运行结果:

在这里插入图片描述

  
拷贝构造的 调用方式 有两种:

  • 一种是像上述代码一样类似于构造函数的调用方法Date d2(d1);
  • 另一种是类似赋值的调用:Date d2 = d1;

  

4.1.1、为什么要传引用

  • C++ 规定:对自定义类型传值传参要先调用拷贝构造
class date
{
public:
	date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	date(const date& d)
	{
		*this = d;
	}

private:
	int _year;
	int _month;
	int _day;
};

void func(const date d)
{
	cout << "heallo world" << endl;
}

int main()
{
	date d1(2024, 1, 1);
	func(d1);
	return 0;
}

  
我们来调试来验证 一下:

在这里插入图片描述
对自定义类型,传值传参都会调用拷贝构造函数。

  
  那这样的话,拷贝构造用传值传参会发生什么呢?
  答: 无穷递归

  
  我们通过图来理解一下:

在这里插入图片描述

  
  而如果是传引用的话, d d d d 1 d1 d1 的别名,就不会形成新的拷贝构造。

 所以,对于自定义类型,传参都不建议使用传值传参。用传值传参还需要先调用拷贝构造,尤其是当实参特别大时,太费劲了

  为什么传值传参要先调用拷贝构造呢?别急,我们学习完下一个知识点就来回答

  

4.1.2、引用尽量加上 const

  使用引用传参,当函数体不需要改变外面的实参时,尽量都使用 c o n s t const const 引用!

  • 因为加上 c o n s t const const 可以保护形参不被改变

  假设,我要写一个判断逻辑,结果 “==” 不小心写成了 “=”。如果没加 c o n s t const const,那形参 d d d 就真被改了,我去给别人拷贝,结果我自己被改了,这合适吗?

Date(Date& d)
{
	if (d._year = _year)
	{
		//···
	}
}

  

  • 而且,不使用 c o n s t const const 引用,当传的实参是只读性质时,会造成权限放大,编译不过去。使用了 c o n s t const const无论实参是不是只读,都能编过去

  

4.2、深入拷贝构造

拷贝构造的进阶特性:

  • 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的拷贝构造
      
  • 传值返回会产生一个临时对象,产生临时对象会调用拷贝构造传引用返回,返回的是返回对象的别名(引用),没有产生拷贝
      
  • 与前面的构造函数析构函数不同,如果我们没有显式实现,编译器默认生成的拷贝构造会对内置类型进行处理,会对内置类型进行值拷贝/浅拷贝。所谓值拷贝(也叫浅拷贝)就是一个字节一个字节进行拷贝,相当于 m e m c p y memcpy memcpy函数的功能。

  

4.2.1、为什么要自己实现拷贝构造

  那默认生成的拷贝构造不是挺好的吗,它都给你完成拷贝了,那我们还需要自己写吗?
  
  我们来看下面这种情况:
  现在,我们实现一个栈类,栈类的成员变量都是内置类型,看看编译器生成的拷贝构造能不能完成任务

typedef int STDataType;
class Stack
{
public :
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		} 
		_capacity = n;
		_top = 0;
	} 

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{
	Stack st1(10);
	
	//两种都行
	//Stack st2(st1);
	Stack st2 = st1
	return 0;
}

我们调试来看一下:

在这里插入图片描述

  好像没有问题, s t 2 st2 st2 是完成了初始化的。
  
  但我们继续往下运行,发现程序崩了

在这里插入图片描述

  为什么呢?
  因为编译器 仅仅完成了值拷贝/浅拷贝
  
  对于_ t o p top top 和 _ c a p a c i t y capacity capacity 来说他们并没有指向什么资源,只进行值拷贝/浅拷贝没有问题
  但对于 _ a a a 来说,虽然他是内置类型,但是 它指向一块开辟的空间 s t 1 st1 st1 的 _ a a a 中存放的是指向的块空间的地址,将 s t 1 st1 st1 中 _ a a a 的值拷贝给 s t 2 st2 st2 的 _a,此时 s t 2 st2 st2 的 _ a a a 也存这那块空间的地址。也就是说 s t 1 st1 st1 s t 2 st2 st2 的 _ a a a 指向同一块空间
  

在这里插入图片描述

  我们本来想的是他们指向不同的空间,空间中存放的是不同的数据(虽然现在没放数据)。虽然现在和我们想的有点不一样,但指向同一块空间也不至于让程序崩溃啊
  答案出现在析构函数那。

   m a i n main main 函数结束,要销毁两个对象,销毁对象前先调用自身析构函数
  后定义的先析构 s t 2 st2 st2 调用自身析构,将 _ a a a 指向的空间释放,后 s t 1 st1 st1 也调用析构,也要对 _ a a a 指向的空间进行释放,但此时空间已经被释放掉了,也就是说 同一块空间被释放了两次,自然程序崩溃了。
  其实,不仅仅是析构两次,它的问题是很多的。比如:在函数内插入一个 1,再在函数外插入一个 2,2 会将 1 给覆盖
  

  现在,我们自己给它加上拷贝构造函数

	Stack(const Stack& st)
	{
		// 需要对_a指向资源创建同样⼤的资源再拷⻉值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		} 
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

  

4.2.2、传值返回先调用拷贝构造的原因

  现在,我们可以回答为什么传值传参要先调用拷贝构造了

  想一想,传值传参传的是实参的拷贝,但这拷贝仅仅只是浅拷贝。像是拷贝上面的 s t a c k stack stack 类,要是在函数里面将 _ a a a 空间释放了,而你以为是传值传参,里面不影响外面,在外面再次将 _ a a a 指向的空间释放,程序就崩溃了。而 C语言只有浅拷贝,是很坑
  所以 C++ 规定,传值传参要先调用默认构造函数,在默认构造中实现深拷贝( _ a a a 指向的空间也拷贝一份),这样就没这些问题啦

  当然,对于自定义类型,函数传参是不建议用传值传参的。毕竟就算是正确拷贝,当拷贝的内容太大,也会占用很大空间,而且效率不高

  • 自定义类型传参,尽可能用引用,如果不改变,尽可能加 c o n s t const const

  

4.2.3、躺赢的 MyQueue

  当然,也不是所有有指向资源的类都需要自己写拷贝构造,比如 M y Q u e u e MyQueue MyQueue 类(用两个栈模拟实现队列)

// 两个Stack实现队列
class MyQueue
{
	public :
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq1;
	MyQueue mq2 = mq1;
	return 0;
}

在这里插入图片描述

   m q 2 mq2 mq2 是正常完成初始化的。
  因为对自定义类型成员,编译器会调用它自身的拷贝构造
  
  虽然MyQueue是躺赢,但这一切都是有 S t a c k Stack Stack 替他负重前行
  

4.2.4、传值返回与引用返回

  • 传值返回产生一个临时对象调用拷贝构造传引用返回,返回的是返回对象的别名(引用),没有产生拷贝

  
  什么意思呢?我们来看看

Stack func()
{
	Stack st;
	return st;
}

int main()
{
	Stack ret = func();
	return 0;
}

  
  像上述代码,调用 f u n c func func 函数,使用传值返回。返回时,先调用拷贝构造 s t st st 拷贝到一个临时对象,后再调用拷贝构造将临时对象中的值拷贝到 ret 中。(实际编译器会进行优化,不会真的执行两个拷贝,但从语法层面来讲是会执行两次拷贝的)

在这里插入图片描述

  
  为了减少拷贝,我们会使用传引用返回,返回 s t st st 的别名

Stack& func()
{
	Stack st;
	return st;
}

  但是这是不对的,st 出函数作用域就销毁了,此时返回的是野引用,类似于野指针的东西

  使用引用返回,一定要确保返回的对象,当函数结束后还在,才能用引用返回

例如下面两种情况:

//情况一
Stack& func()
{
	static Stack st;
	return st;
}


//情况二
Stack& func(Stack& st)
{
	st.push(1);
	st.push(1);
	st.push(1);

	return st;
}

int main()
{
	Stack st1;
	Stack st2 = func(st1);
	return 0;
}

  
  

4.3、总结

  如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数
  
拷贝构造的特点:

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是自身类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发 无穷递归 。若有其他参数必须给缺省值
  • C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参传值返回都会调用拷贝构造完成
  • 未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造
  • Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。像 Stack 这样的类,虽然也都是内置类型,但是 _a 指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 S t a c k Stack Stack 成员,编译器自动生成的拷贝构造会调用 S t a c k Stack Stack 的拷贝构造,也不需要我们显式实现 M y Q u e u e MyQueue MyQueue 的拷贝构造。
  • 这里有一个小技巧,如果一个类显式实现了析构函数并释放了资源,那么他就需要写拷贝构造,否则不需要
  • 传值返回产生一个临时对象调用拷贝构造传值引用返回,返回的对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回

  

5、取地址运算符重载

5.1、const 成员函数

当类对象被 c o n s t const const 修饰会发生什么呢?

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	} 

	void Print() 
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	const Date d(2024, 1, 1);
	d.Print();
	return 0;
}

  
  d.Print();这句代码,我们知道到调用成员函数要传递地址给 t h i s this this 指针的,这句代码实际上是d.Print(&d);
  但是 d d d类型const Date,因此 & d d d 传递的类型const Date*
  而 t h i s this this 指针类型Date* const this(这里 c o n s t const const 修饰的是指针本身,不是对象,可以直接忽略)
  

在这里插入图片描述

  很显然,const Date*传给Date*发生了权限放大,编译是无法通过的

  这时要把 t h i s this this指针 的类型变为const Date*
  怎么做呢?要知道 C++ 规定,我们是不能在形参的位置显式写this指针的,这样我们就不能直接通过形参来修改 t h i s this this 指针

  为此,C++就给了一个偏方:在函数参数列表后面加 c o n s t const const
如:

void Print() const
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

  那如果对象是非 const 还能不能调用 P r i n t Print Print 呢?

int main()
{
	Date d1(2024, 7, 5);
	d1.Print();
	
	const Date d2(2024, 8, 5);
	d2.Print();

	return 0;
}

  可以的,权限虽然不能放大,但是能缩小

  所以,对于不用修改成员变量的成员函数,建议都加const,原因与引用加const类似

  

5.2、取地址运算符重载

  取地址运算符重载分为普通取地址运算符重载 c o n s t const const 取地址运算符重载

	Date* operator&()
	{
		return this;
	}

	const Date* operator&() const
	{
		return this;
	}

:两个都要写,因为普通对象返回Date* c o n s t const const 对象要返回const Date*

  取地址运算符重载也是一个默认成员函数,编译器会默认生成,往往不需要我们自己显式实现
  
  除非你不想让别人取到该对象地址,那你就可以这样写:

	Date* operator&()
	{
		return nullptr;
	}

	const Date* operator&() const
	{
		return nullptr;
	}

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

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

相关文章

云HIS,云HIS源码

医学领域的信息系统平台种类繁多。在很大程度上&#xff0c;对于一些在医疗机构的区域一体化信息平台&#xff0c;在微观层面上&#xff0c;传统的医疗信息系统已经建立了许多医院(HIS)或数字医院系统&#xff0c;包括子系统提供了一个单一的功能&#xff0c;如注册和形象&…

【H3C(HCL)网络模拟器网络桥接】进入网络设备Web页面

H3C模拟器网络桥接 1.模拟器选择Host&#xff0c;添加 2.选中Host主机的网卡&#xff0c;这里我选的是华三的Virtual Box的网卡 选中后连线至防火墙对应接口&#xff0c;建议连接到G1/0/1&#xff0c;这个接口是默认配置的接口&#xff0c;拥有默认地址 3.修改防火墙配置 [F…

Windows 中 PIN 和密码的区别是什么?各有各的优点

PIN PIN 即个人识别号码&#xff08;Personal Identification Number&#xff09;&#xff0c;在 Windows 系统中通常由 4 到 6 位数字组成。它是 Windows Hello 的一部分&#xff0c;设计用于提供快速、安全的身份验证。 密码 密码是一种更为传统的身份验证方法&#xff0c;…

提升生产效率:APS高级计划排程系统在车间工序级排程的革命性应用

在制造业的数字化转型浪潮中&#xff0c;APS高级计划排程系统以凭借自身卓越的排程运算能力和应用灵活性&#xff0c;已经成为中大型制造业提升生产效率的关键工具。APS系统的介入&#xff0c;打通了传统ERP和MES等各类业务系统运营平台&#xff0c;并且通过产能均衡规划&#…

聚观早报 | 搜狐2024年Q2财报;一加Open推出深红色版本

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 8月6日消息 搜狐2024年Q2财报 一加Open推出深红色版本 smart精灵#5将在澳洲首秀 OpenAI为ChatGPT测试文本水印 …

手动部署内网穿透

关于内网穿透&#xff0c;主要针对什么是公网和内网&#xff1f;NAT转化技术等引出内网穿透方法。 本文主要技术是利用frp部署内网穿透、以及nagix部署web服务。 测试环境&#xff1a; 服务器&#xff1a;Linux云服务内网&#xff1a;用本地WM充当内网云服务器Linux&#xf…

伯克利Linux系统管理: 脚本编写学习 课堂与实验(系统简洁保姆级学习)

Linux系列文章目录 补充内容 Windows通过SSH连接Linux 第一章 Linux基本命令的学习与Linux历史 第二章(上) Vim课堂与实验 文章目录 Linux系列文章目录一、前言二、学习内容&#xff1a;2.1 上课内容2.1.1 为什么要学习脚本编写&#xff1f;2.1.2 Bash编程语言2.1.3 SheBang2.…

半导体PEEK纳米级钻孔,用德国高精密主轴

在半导体行业&#xff0c;对精度、效率与稳定性的要求近乎苛刻。其中&#xff0c;PEEK&#xff08;聚醚醚酮&#xff09;材料因其优异的耐热性、耐化学性和机械性能&#xff0c;在高端半导体封装、微流控芯片等领域得到了广泛应用。然而&#xff0c;PEEK材料的硬度与韧性并存&a…

Armv8/Armv9的Pstate寄存器介绍

PSTATE概述 在Armv7及其之前&#xff0c;有一个重要的寄存器叫做程序状态寄存器CPSR&#xff0c;但是到了Armv8/Armv9的aarch64架构时&#xff0c;删除了CPSR寄存器&#xff0c;改为了PSTATE&#xff08;PE状态寄存器&#xff09;。 PSTATE表示一组小寄存器的集合&#xff0c;…

隐私指纹浏览器产品系列 — 什么是指纹(一)

1.引言 现在许多网站在努力的尝试标记互联网上的每一个访客&#xff0c;用以追踪用户的行为轨迹&#xff0c;分析行为习惯&#xff0c;以及确认是否为真实用户。除此之外&#xff0c;他们还利用这些标记&#xff0c;将多个可能是同一个用户身份的访客进行归一&#xff0c;关联…

中国高尔夫运动快速发展中,深圳高尔夫展邀您迎接机遇与挑战

在浩瀚的体育世界中&#xff0c;高尔夫以其悠久的历史、优雅的姿态和独特的魅力闻名于世&#xff0c;被誉为“古老的贵族运动”&#xff0c;而这个美誉却让很多人对它敬而远之。其实高尔夫被称作“贵族运动”&#xff0c;仅仅是因为早期它更多在贵族之间流行而已。 高尔夫&…

【TS】基本类型

基本类型 类型例子描述number1, -33, 2.5任意数字stringhi, "hi", hi任意字符串booleantrue、false布尔值true或false字面量其本身限制变量的值就是该字面量的值any*任意类型unknown*类型安全的anyvoid空值&#xff08;undefined&#xff09;没有值&#xff08;或und…

鸿蒙HarmonyOS开发:如何使用第三方库,加速应用开发

文章目录 一、如何安装 ohpm-cli二、如何安装三方库1、在 oh-package.json5 文件中声明三方库&#xff0c;以 ohos/crypto-js 为例&#xff1a;2、安装指定名称 pacakge_name 的三方库&#xff0c;执行以下命令&#xff0c;将自动在当前目录下的 oh-package.json5 文件中自动添…

面对电商渠道品牌要如何控价

在当今竞争激烈的市场环境中&#xff0c;品牌销售渠道的管理至关重要。线上和线下渠道的低价、窜货问题犹如侵蚀品牌根基的蚁穴&#xff0c;若不加以有效治理&#xff0c;品牌价值将受到严重损害。 线上渠道方面&#xff0c;除了利用系统精准监测低价情况外&#xff0c;还应注重…

手撕数据结构之二叉树

1.树 树的基本概念与结构 树是⼀种⾮线性的数据结构&#xff0c;它是由 n&#xff08;n>0&#xff09; 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;⽽叶朝下的。 • 有⼀个特殊的结点&…

python语言day2

字符串&#xff1a; 字符串的方法 字符串格式化&#xff1a; 输出结果&#xff1a; 姓名张三今年123岁&#xff0c;现在在北京工作,名字叫张三 text "姓名{0}今年{1}岁&#xff0c;现在在{2}工作,名字叫{0}".format("张三",123,"北京") print(t…

关于冒泡算法

一、前言 当谈及经典排序算法时&#xff0c;冒泡排序&#xff08;Bubble Sort&#xff09;无疑是最具代表性的一种。这种算法以其简单直观的特点&#xff0c;成为初学者理解排序基本概念的理想选择。本文将深入解析冒泡排序的原理、实现步骤&#xff0c;以及其在 C# 编程中的具…

【Vue】RuoYi-Vue 若依 vue3 版本安装 tailwindcss 不生效问题

解决方法 删除默认安装教程下的 postcss.config.js 将配置转移到 vite.config.js 中&#xff0c;不生效原因我推测是因为 vite.config.js 配置 postcss 这段覆盖了 postcss.config.js 所致 代码修改如下&#xff1a; 另外原博主友情提示&#xff0c;引入的TailwindCSS的样式…

已解决AttributeError: module ‘emoji‘ has no attribute ‘get_emoji_regexp‘

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

遥感技术在生态系统碳储量、碳收支、碳循环以及人为源排放反演等领域的技术发展

卫星遥感具有客观、连续、稳定、大范围、重复观测的优点&#xff0c;已成为监测全球碳盘查不可或缺的技术手段&#xff0c;卫星遥感也正在成为新一代 、国际认可的全球碳核查方法。目的就是梳理碳中和与碳达峰对卫星遥感的现实需求&#xff0c;系统总结遥感技术在生态系统碳储量…