C++类与对象(3)—拷贝构造函数运算符重载

目录

一、拷贝构造函数

1、定义

2、特征

3、内置与自定义类型 

4、const修饰参数

5、默认生成

浅拷贝

深拷贝

6、总结

二、运算符重载

1、定义 

2、判断是否相等

3、比较大小

4、赋值

5、总结


一、拷贝构造函数

1、定义

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存

在的类类型对象创建新对象时由编译器自动调用。

2、特征

  • 拷贝构造函数是构造函数的一个重载形式。
  • 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 拷贝构造
	Date(const Date& d)
	{

		d._year = _year;
		d._month = _month;
		d._day = _day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	date d1(2023, 2, 3);
	date d2(d1);

	return 0;
}

如果不加&引用符号,编译器会报错。 

 使用拷贝构造也可以用这种方式:

Date d3 = d1;

3、内置与自定义类型 

  • 内置类型的拷贝和传参,编译器可以直接拷贝,
  • 自定义类型的拷贝和传参,需要调用拷贝构造。

为什么需要拷贝构造呢?


用栈实现队列的时候,可能一会对st1进行析构一会对st2进行析构,成员变量指针_a指向的空间不能析构两次,而且如果分别对st1和st2赋初值,后赋值的会覆盖先赋值的数据。所以就要求它们不能指向同一块空间,各自要有各自的空间,所以C++规定:自定义类型的拷贝和传参,需要调用拷贝构造,对于栈这种需要深拷贝的拷贝构造(后续学习),现阶段只需要知道需要拷贝构造即可。

自定义类型传参:

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

	// 拷贝构造
	// Date d2(d1);
	Date(Date& d)
	{
		d._year = _year;
		d._month = _month;
		d._day = _day;
	}
private:
	int _year;
	int _month;
	int _day;
};

// 传值传参
void Func1(Date d)
{

}
// 传引用传参
void Func2(Date& d)
{

}
// 传指针
void Func3(Date* d)
{

}
int main()
{
	Date d1(2023, 2, 3);
	Func1(d1);

    //Func2(d1);
    //Func3(&d1);
	return 0;
}

使用Func1传值传参 :

在Func1(d1)处按F11会直接跳转到拷贝构造。 

拷贝构造结束会进行Func1函数。 

如果用Func2引用传参,就不需要拷贝构造了。 

直接跳转Func2函数。 

此外还可以使用以前C语言中常用的指针传参,但这种方法有点啰嗦。

Func3(&d1);

4、const修饰参数

拷贝构造一般会加const,如果不加,比如下面的情况,赋值方向反了,会导致原数据变成随机值。

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

加上const可以避免拷贝方向错误时,原数据被修改,所以一般习惯参数前加上const。

	Date(const Date& d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}

此外,如果被拷贝的变量是被const修饰,如果拷贝构造的参数不被const修饰,会造成引用的权限扩大,所以一定要用const修饰参数。

5、默认生成

浅拷贝

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按

字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

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()
{
	Date d1(2023, 11, 20);
	d1.Print();
	Date d2(d1);
	d2.Print();
	return 0;
}

通过输出结果我们可以发现,没写拷贝函数,编译器会自动对内置类型拷贝。

编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

当然像日期类这样的类是没必要的。

如果是自定义类型呢?程序会怎么处理?

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		cout << "Stack(size_t capacity = 10)" << endl;

		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			exit(-1);
		}

		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t    _size;
	size_t    _capacity;
};

int main()
{
	Stack st1;
	Stack st2(st1);

	return 0;
}

 输出后程序会报错:

如果按照内置类型的方式对自定义类型进行拷贝, 两个Stack类的变量st1和st2的成员变量*array都指向同一块空间,这样会导致两个问题:

  1. 插入和删除数据会互相影响,比如st1先插入三个数据,st1的size为3,这时st2要插入数据,但st2的size为0,如果插入数据则会造成st1插入的数据被覆盖;同时,在析构st1时,对st1的空间进行释放,由于st1和st2的成员变量*array都指向同一块空间,这时st2再插入数据会造成对野指针的访问。
  2. 程序销毁*_array的空间时,会析构两次,程序会崩溃。

深拷贝

这时只有深拷贝才能解决自定义类型的拷贝构造。

	Stack(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, st._array, sizeof(DataType) * st._size);
		_size = st._size;
		_capacity = st._capacity;
	}

st2成功实现了拷贝构造,st2与st1的地址不同,它们不在指向同一空间了。

什么时候需要自己实现拷贝构造? 

  • 当自己实现了析构函数释放空间,就需要实现拷贝构造。
class MyQueue
{
public:
	// 默认生成构造
	// 默认生成析构
	// 默认生成拷贝构造

private:
	Stack _pushST;
	Stack _popST;
	int _size = 0;//缺省值处理
};
  •  两个Stack类型的成员变量初始化调用默认生成构造,销毁调用它的析构函数,拷贝使用它的拷贝构造。
  • 整型变量_size初始化通过缺省值处理,内置类型出磊之后会自动销毁,不需要析构处理,可以使用默认生成拷贝构造。

6、总结

拷贝构造函数典型调用场景:

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

默认生成拷贝构造:

  • 内置类型完成浅拷贝/值拷贝(按字节一个一个拷贝)
  • 自定义类型去调用这个成员的拷贝构造。

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

尽量使用引用。

二、运算符重载

1、定义 

C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

  • 函数名字为:关键字operator后面接需要重载的运算符符号
  • 函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*  ::  sizeof  ? :  . 注意以上5个运算符不能重载。

我们通过代码一点一点理解:

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

//private:
	int _year;
	int _month;
	int _day;
};
//运算符重载
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
int main()
{
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 1);
    //下面两种使用方式均可
	operator==(d1, d2);
	d1 == d2;//编译器会转换成去调用operator==(d1,d2);
	return 0;
}

如果要输出函数返回值,方式如下:

cout << (d1 == d2) << endl;//必须加括号,保证优先级
cout << operator==(d1, d2) << endl;

这里会发现,如果运算符重载成全局的就需要成员变量是公有的,否则会报错。

那么问题来了,封装性如何保证?

我们可以选择后面学习的友元处理,现阶段直接把运算符重载函数重载成成员函数即可。

2、判断是否相等

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//运算符重载
// bool operator==(Date* this, const Date& d)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 1);
//operator内置只能使用这种形式(d1 == d2,
	cout << (d1 == d2) << endl;
	return 0;
}

3、比较大小

	bool operator<(const Date& d)
	{
		if (_year < d._year)
		{
			return true;
		}
		else if (_year == d._year && _month < d._month)
		{
			return true;
		}
		else if (_year == d._year && _month == d._month && _day < d._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

还可以写成这种简便形式,但上述较长的方式更直观。

        return _year < d._year
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day);

判断小于等于我们可以借助隐藏的this指针,使用运算符重载嵌套的方式,在判断小于等于中分别调用判断小于和判断等于的运算符重载。

// d1 <= d2
bool operator<=(const Date& d)
{
	return *this < d || *this == d;
}

同理,判断大于、大于等于和不等于,我们也对上面的运算符重载进行复用。 

// d1 > d2
bool operator>(const Date& d)
{
	return !(*this <= d);
}

bool operator>=(const Date& d)
{
	return !(*this < d);
}

bool operator!=(const Date& d)
{
	return !(*this == d);
}

 4、赋值

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 6, 6);
	Date d2(1, 1, 1);

	d2 = d1;
	d1.Print();
	d2.Print();

	return 0;
}

 operator=就是我们的赋值运算符重载,如果我们不使用引用传值,那自定义类型会调用拷贝构造,我们使用引用传值,就避免了这些拷贝操作。

 

如果三个数赋值呢?

d3 = d2 = d1;

 根据赋值操作符的右结合性,d1赋值给d2需要一个返回值才能对d3赋值。

  返回值使用引用返回,避免传值返回过程中,创建临时变量和不必要的拷贝。

 

	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		return *this;
	}

如果自己给自己赋值呢?那就可以不进行赋值操作了,我们添加一个判断即可。 

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

上面就是赋值运算符的完整版了。

5、总结

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
  • 注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值,所以日期类就不需要写运算符重载函数,而类似用栈实现队列则需要。
  • 注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pnpm 管理依赖包是如何节省磁盘空间的?

npm 存在的问题 我们经常使用 npm 来管理 node 项目中的包&#xff0c;从 package.json 中读取配置将依赖下载到本地&#xff0c;以保障项目的正常运行。 当项目数量多时&#xff0c;这样的包管理方式会非常的占用电脑内存。由于每个项目都有属于自己的依赖&#xff0c;每个项…

【Zabbix监控二】之zabbix自定义监控内容案例(自动发现、自动注册)

一、自定义监控内容 案例&#xff1a;自定义监控客户端服务器登录的人数 需求&#xff1a;限制登录人数不超过3个人&#xff0c;超过5个人就发出报警 1、在客户端创建自定义key 明确需要执行的linux命令 创建zabbix监控项配置文件&#xff0c;用于自定义Key #在zabbix的配…

支持对协议和会话分享动作进行授权,新增API Key白名单功能,JumpServer堡垒机v3.9.0发布

2023年11月20日&#xff0c;JumpServer开源堡垒机正式发布v3.9.0版本。在这一版本中&#xff0c;JumpServer对授权功能进行了优化&#xff0c;增加了对“会话分享”及“资产协议”的配置&#xff0c;方便管理员以更细的颗粒度对各种资源进行控制&#xff1b;针对使用API Key与J…

基于yolov8的车牌检测训练全流程

YOLOv8 是Ultralytics的YOLO的最新版本。作为一种前沿、最先进(SOTA)的模型&#xff0c;YOLOv8在之前版本的成功基础上引入了新功能和改进&#xff0c;以提高性能、灵活性和效率。YOLOv8支持全范围的视觉AI任务&#xff0c;包括检测, 分割, 姿态估计, 跟踪, 和分类。这种多功能…

多目标应用:基于非支配排序的蜣螂优化算法NSDBO求解微电网多目标优化调度(MATLAB)

一、微网系统运行优化模型 微电网优化模型介绍&#xff1a; 微电网多目标优化调度模型简介_IT猿手的博客-CSDN博客 二、基于非支配排序的蜣螂优化算法NSDBO 基于非支配排序的蜣螂优化算法NSDBO简介&#xff1a; https://blog.csdn.net/weixin46204734/article/details/128…

【LCM(潜在一致性模型)-5步即可高质量出图】

https://tianfeng.space/ 前言 由潜在一致性模型 (LCM) 生成的图像。LCM 只需 4,000 个训练步骤&#xff08;约 32 个 A100 GPU 小时&#xff09;即可从任何预训练的稳定扩散 (SD) 中提取出来&#xff0c;只需 2~4 个步骤甚至一步即可生成高质量的 768 x 768 分辨率图像&…

数据结构与算法 | 图(Graph)

图的分类&#xff08;Types Of Graph&#xff09; 可以看到图的基本的结构非常简单&#xff0c;约束也很少&#xff0c;如果在其中加上各种条件约束就可以定义各种类型的图。 约束边或者顶点个数来分类&#xff1a; 零图&#xff08;Null graph&#xff09;&#xff1a;只有顶…

指令系统、流水线

指令系统 分类 寻址方式 设计 能够改变控制流的指令&#xff1a;分支、跳转、过程调用、过程返回 操作码设计 MIPS 流水线 MIPS流水线 改进后 取指&#xff08;IF&#xff09; 译码&#xff08;ID&#xff09; 执行&#xff08;EX&#xff09; 存储器访问 寄存器-寄存器 A…

LabVIEW如何获取波形图上游标所在位置的数值

LabVIEW如何获取波形图上游标所在位置的数值 获取游标所在位置数值的一种方法是利用波形图的游标列表属性。 在VI的程序框图中&#xff0c;右键单击波形图并选择创建引用 &#xff0c;然后将创建的引用节点放在程序框图上。 在程序框图上放置一个属性节点&#xff0c;并将其…

Java制作俄罗斯方块

Java俄罗斯方块小游戏 import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.List; imp…

open3d ICP 配准

文章目录 Three common registration techniquesPoint-to-point techniquePoint-to-plane registration ICP registrationHelper visualization functionInputGlobal registrationExtract geometric featureInputRANSAC Point-to-point ICPPoint-to-plane ICP References Three…

搭建Android自动化python+appium环境

一. 需要软件 JDK:JAVA安装后配置JDK环境 SDK:SDK下载后配置adb环境 Python:pyhton语言 Pycharm:python脚本编译工具 Appium-python-client:pyhton中的库Appium客户端 二. 搭建步骤 1.配置JDK环境 ①. 下载安装java: https://www.oracle.com/java/technologies/javase-j…

语音特征提取: 梅尔频谱(Mel-spectrogram)与梅尔倒频系数(MFCCS)

1 核心概念 1.1 语音信号 语音信号是一个非平稳的时变信号&#xff0c;但语音信号是由声门的激励脉冲通过声道形成的&#xff0c;经过声道(人的三腔&#xff0c;咽口鼻)的调制&#xff0c;最后由口唇辐射而出。认为“短时间”(帧长/窗长&#xff1a;10~30ms)内语音信号是平稳…

Unity中Shader法线贴图(下)理论篇

文章目录 前言一、采样出错的原因二、切线空间是什么&#xff1f;切线空间图解&#xff1a; 三、计算方式1、统一变换到切线空间下进行计算2、统一变换到世界空间下进行计算 四、一般统一变换到世界空间下的坐标进行计算1、求M^-1^2、求出n~w~ 前言 这篇文章&#xff0c;主要解…

【Kettle实战】字符串处理及网络请求JSON格式处理

经过大量的kettle操作实践&#xff0c;我们会渐渐掌握一些技巧&#xff0c;大大减轻清洗的工作量。比如在哪里 处理字符串更方便&#xff0c;在哪儿处理更合理都是一个取舍问题。 字符串拼接 MySQL中使用concat(字段1,字段2)&#xff0c;但是如果“字段2”为NULL&#xff0c;结…

如何挖掘xss漏洞

如何挖掘xss漏洞 对于如何去挖掘一个xss漏洞我是这样理解的 在实战情况下不能一上来就使用xss语句来进行测试很容易被发现 那这种情况该怎么办呢 打开准备渗透测试的web网站&#xff0c;寻找可以收集用户输入的地方比如搜索框&#xff0c;url框等 发现后寻找注入点 选在输入…

【Q1—45min】

1.epoll除了边沿触发还有什么&#xff1f;与select区别. epoll 是Linux平台下的一种特有的多路复用IO实现方式&#xff0c;与传统的 select 相比&#xff0c;epoll 在性能上有很大的提升。 epoll是一种当文件描述符的内核缓冲区非空的时候&#xff0c;发出可读信号进行通知&…

Find My蓝牙耳机|苹果Find My技术与耳机结合,智能防丢,全球定位

蓝牙耳机就是将蓝牙技术应用在免持耳机上&#xff0c;让使用者可以免除恼人电线的牵绊&#xff0c;自在地以各种方式轻松通话。自从蓝牙耳机问世以来&#xff0c;一直是行动商务族提升效率的好工具。正是应为蓝牙耳机小巧无线&#xff0c;人们越来越喜欢随身携带蓝牙耳机出门&a…

人民网_领导留言板data2022年-2023年

人民网_领导留言板data_2022年全年-2023年11月数据_全国任意城市 包含且不限于&#xff1a;留言ID,留言对象,留言标题,种类名,领域名,目前状态,留言日期,留言内容,回复机构,回复时间,回复内容,满意度,解决力度,沟通态度,办理时效 对于有需要爬取领导留言板的朋友&#xff0c;…

【Qt开发流程之】布局管理

介绍 一个界面呈现&#xff0c;如果要让用户有更好的观感&#xff0c;布局必不可少。 【Qt之布局】QVBoxLayout、QHBoxLayout、QGridLayout、QFormLayout介绍及使用 链接: https://blog.csdn.net/MrHHHHHH/article/details/133915208 qt布局类图&#xff1a; Qt布局是Qt图形…