【C++】类和对象(四)拷贝构造、赋值运算符重载

文章目录

  • 四、拷贝构造函数
    • 干嘛的?
    • 写拷贝构造函数的注意事项
      • 正确写法
    • 不显示定义拷贝构造函数的情况
      • 浅拷贝
        • :one:示例:内置类型
        • :two:示例:自定义类型
        • 一个提问
      • 深拷贝
  • 五、赋值运算符重载
    • 运算符重载
      • 函数原型
      • 注意
      • 调用时的两种书写方式
      • 完整实现代码
    • 赋值运算符重载
      • 干嘛的?
      • 连续赋值
      • 总结赋值运算符重载格式
      • 默认生成的复制重载函数的行为
      • 默认生成的函数行为总结
      • 赋值运算符是否可以重载为全局函数

书接上回: 【C++】类和对象(三)构造与析构

四、拷贝构造函数

干嘛的?

拷贝构造函数:用同类型的其他对象 构造一个(初始化 )新的对象
代码演示:

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

	//Date d2(d1);
	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, 1, 28);
	Date d2(d1);

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

	return 0;
}

写拷贝构造函数的注意事项

注意:拷贝构造函数 参数位置必须要传引用

为什么参数位置必须要传引用?
传值做拷贝构造函数的参数 🆚 传引用做拷贝构造函数的参数

C++规定 调用拷贝构造函数时,
1️⃣自定义类型本身 作为实参传递过去(传值传参),都会先调用拷贝构造。
🌰例如:
在这里插入图片描述
如果传值传参 会发生无穷递归的问题:
在这里插入图片描述
2️⃣自定义类型的引用 作为实参传递过去(传引用传参),就不会先调用拷贝构造。

除此之外,为了 防止写反方向的类似问题,我们一般给 引用前面加const修饰
例如:可能出现赋值方向写反的问题

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

	//Date d2(d1);
	Date(Date& d)
	{
		//赋值方向写反的问题
		d._year = this->_year;
		d._month = this->_month;
		d._day = this->_day;
		//_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, 1, 28);
	Date d2(d1);

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

	return 0;
}

写反方向的情况如下:
在这里插入图片描述

正确写法

加入const修饰 保护要赋值给别人的对象

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

不显示定义拷贝构造函数的情况

浅拷贝

程序员不显示定义拷贝构造函数 ,则编译器会自动生成拷贝构造函数。并且

1️⃣ 对内置类型的成员变量进行值拷贝(浅拷贝)。
2️⃣对自定义类型的成员变量 调用它的拷贝构造

1️⃣示例:内置类型

在这里插入图片描述

2️⃣示例:自定义类型
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
	// 注意:拷贝构造函数 也属于构造函数 编译器就不会自动生成 构造函数
	// 但是下面一句代码 可以强制编译器生成默认构造
	Time() = default;

	Time(const Time& t)
	{
		cout << "Time(const Time& t)" << endl;
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
	}

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

	// Date d2(d1);
	/*Date(const 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;

	// 自定义类型
	Time _t;
};

int main()
{
	Date d1(2024, 1, 28);
	Date d2(d1);

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

	return 0;
}

代码演示:对自定义类型的成员变量 调用它的拷贝构造
在这里插入图片描述

一个提问

❓对于内置类型、自定义类型的成员变量,即使程序员不提供拷贝构造函数,编译器都会自动进行拷贝,那么拷贝构造函数是不是就可以不用我们写了呢?

答案当然不是的。在有些情况,编译器默认生成的拷贝构造会出现问题,我们可以看看如下的问题情况。
问题代码:

#include<iostream>
using namespace std;
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 st1;
	Stack st2(st1);
	return 0;
}

在这里插入图片描述
问题描述:经过调试,我们发现 _array的值是一个地址也被拷贝过来 ,这意味着两个对象指向同一块空间。
在这里插入图片描述

出了函数作用域,会对他们析构,把st2对象的 _array空间释放。但st2置空并不影响st1,st1依然指向那块空间,导致st1成为野指针,析构st1时,_array空间再次被释放,相当于同一块空间被释放了两次。

所以,对于动态开辟的内存空间 都要使用深拷贝。深拷贝就是,为要拷贝st1的st2再开辟一块同样大小的新空间。

深拷贝

对于动态开辟的内存空间,必须程序员自己实现深拷贝!
代码如下:

#include<iostream>
using namespace std;
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;
	}
	Stack(const Stack& s)
    {
        DataType* tmp = malloc(sizeof(s._capacity * sizeof(DataType)));
        if(tmp == nullptr)
        {
            perror("malloc fail");
            exit(-1);
        }
        memcpy(tmp , a._array,sizeof(DataType)*s._size);
        _array = tmp;
        _size = s._size;
        _capacity = s._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 st1;
	Stack st2(st1);
	return 0;
}

在这里插入图片描述
可以看到经过深度拷贝的两个指针分别开辟了两块不同的空间。

五、赋值运算符重载

运算符重载

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

函数原型

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

🌰

bool operator==(const Date& y)
bool operator<(const Date& y)

注意

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* (点星运算符) :: (域作用限定符) sizeof (计算变量大小的运算符) ?: (三目运算符) . (点操作符) 注意以上5个运算符不能重载。这个经常在笔试选择题中出 现。

注意:区分 运算符重载 跟 函数重载,二者没有关系。

运算符重载:让自定义类型的对象可以使用运算符。通过函数定义了该运算符的行为

函数重载:允许函数名相同 参数不同的函数存在,通过函数名修饰规则可以找到对应的函数

代码示例🌰
这里展示两个​运算符重载函数 他们是用来 比较自定义对象 日期年月日的大小

调用时的两种书写方式

d1.operator==(d2)
d1 == d2

完整实现代码

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
	// operator运算符 做函数名
	bool operator==(const Date& y)
	{
		return _year == y._year
			&& _month == y._month
			&& _day == y._day;
	}
	// operator运算符 做函数名
	bool operator<(const Date& y)
	
	{
        //如果 年小就小
		if (_year < y._year)
		{
			return true;
		}
        //如果年相等
		else if (_year == y._year)
		{
            //如果月小就小
			if (_month < y._month)
			{
				return true;
			}
			else if (_month == y._month)
			{
				return _day < y._day;
			}
		}

		return false;
	}

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


int main()
{
	Date d1(2024, 1, 28);
	Date d2(2024, 2, 27);
	//通过对象调用成员函数的方式 调用运算符重载函数 比较日期对象d1和d2的大小
	cout << d1.operator==(d2) << endl;
	cout << d1.operator<(d2) << endl;
	//另一种调用方式 加括号的原因:流插入运算符的优先级高于等于号
	cout << (d1 == d2) << endl; // cout << (d1.operator==(d2)) << endl;
	cout << (d1 < d2) << endl;  // cout << (d1.operator==(d2)) << endl;
    
    bool ret1 = d1 < d2;
    bool ret2 = d1.operator<(d2);
    
    int i = 0;
    int j = 1;
    bool ret3 =i<j;

	return 0;
}

在这里插入图片描述
通过反汇编窗口,我们可以看到

对于自定义类型的运算符重载函数的两种调用方式底层的汇编代码都是一样的。
而对于内置类型的运算符比较,是直接有cmp指令支持的,不用程序员自己规定大小比较方式。

赋值运算符重载

干嘛的?

赋值运算符重载:对已经存在的同类型对象,一个拷贝赋值给另一个,就用到了赋值运算符重载

代码示例

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//d2 = d1;
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

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

int main()
{
	Date d1(2024, 1, 28);
	Date d2(1990, 2, 1);
	Date d3(d2);  // 拷贝构造,同类型一个存在的对象进行初始化要创建的对象

	d2 = d1;// 已经存在的对象,一个拷贝赋值给另一个
	d2.Print();
	d3.Print();
	return 0;
}

栗子结果
在这里插入图片描述

连续赋值

int  i = 0, j = 1;
i = j = 10;

注意:

  1. 对于内置类型,10先赋值给j ,表达式的返回值为j ,j再作为下一次赋值的右操作数 以此支持连续赋值。
    同理,对于自定义类型为了支持连续赋值,我们需要拿到表达式的结果,所以在写赋值重载函数时,我们要返回被赋值好的对象 *this。
  2. 函数传值返回会调用拷贝构造函数,为了避免浪费,赋值运算符函数的返回值使用引用返回
  3. 为了防止自己给自己赋值,我们需要加一个判断。

总结赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义

默认生成的复制重载函数的行为

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

默认生成的函数行为总结

在这里插入图片描述

赋值运算符是否可以重载为全局函数

❓运算符重载可以在全局重载,那赋值运算符重载可以在全局重载嘛?

注意: 赋值运算符只能重载成类的成员函数不能重载成全局函数

原因::赋值运算符如果不显式在类里面实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数

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

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

相关文章

Ant design Vue 表格中显示不同的状态(多条件显示)

比如&#xff1a;后端一个字段有多种状态&#xff1a; 那么后端接口会返回&#xff1a;0 或者 1 或者 2 其中一个&#xff0c;前端需要展示的是对应的文字&#xff0c;像简单的只有两个状态的可以直接在列里面操作&#xff1a; {title: 状态,dataIndex: usable,customRender: …

国产测温速度快且功耗低的温度传感芯片MY18E20可Pin-Pin替换DS18B20

MY18E20是一款国产高精度可编程的数字模拟混合信号温度传感芯片&#xff1b;感温原理基于CMOS半导体PN节温度与带隙电压的特性关系&#xff0c;经过小信号放大、模数转换、数字校准补偿后&#xff0c;数字总线输出&#xff0c;具有精度高、一致性好、测温快、功耗低、可编程配置…

剖析影响大米码垛机性能的关键因素

在现代化粮食加工产业链中&#xff0c;大米码垛机以其高效、精准的自动化操作&#xff0c;成为提升生产效率、降低劳动强度的得力助手。然而&#xff0c;要想充分发挥大米码垛机的性能优势&#xff0c;我们必须深入了解影响其性能的关键因素。星派将深入剖析这些关键因素&#…

1.XSS-反射型(get)

反射型XSS或不持久型XSS 含义&#xff1a;交互的数据一般不会被存在在数据库里面,只是简单的把用户输入的数据反射给浏览器&#xff0c;一次性&#xff0c;所见即所得。 进行测试一下页面是否存在xss漏洞&#xff0c;进行测试一下 "<>6666输入一些字符或者符号&am…

YOLOv10改进 | Neck | 添加双向特征金字塔BiFPN【含二次独家创新】

&#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录&#xff1a;《YOLOv8改进有效涨点》专栏介绍 & 专栏目录 | 目前已有40篇内容&#xff0c;内含各种Head检测头、损失函数Loss、B…

【面试干货】Java的基础类型和字节大小

【面试干货】Java的基础类型和字节大小 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Java编程语言中&#xff0c;有八种基本数据类型&#xff0c;它们分别是&#xff1a;布尔型&#xff08;boolean&#xff09;、字节型&#xff08;byt…

前端架构(含演进历程、设计内容、AI辅助设计、架构演进历程)

前端架构的演进历程 前端架构的设计内容 技术选型(库、工具、标准规范、性能、安全、扩展性 )设计模式及代码组织(模块化、分层架构、数据结构)构建与部署(性能优化、自动化、集成、测试) 项目体量 小型项目&#xff1a;对于小型项目&#xff0c;前端架构需要关注模块化、组件…

可视化大屏开发涉及到的8大技术栈,收藏起来不迷路。

可视化大屏开发设计涉及到的技术栈包括&#xff1a; 前端开发技术&#xff1a; HTML、CSS、JavaScript、TypeScript等&#xff0c;用于构建用户界面和实现交互效果。 数据可视化库&#xff1a; 如D3.js、ECharts、Highcharts等&#xff0c;用于将数据以图表、地图、仪表盘等…

Hibernate 框架进行对象关系映射(ORM)

Hibernate是一个广泛使用的Java对象关系映射&#xff08;ORM&#xff09;框架&#xff0c;它通过将Java类与数据库表关联起来&#xff0c;使得开发人员可以使用面向对象的编程方式进行数据库操作。Hibernate的主要目标是消除冗长的JDBC代码和手动处理SQL的需求&#xff0c;从而…

在C#中对 JSON进行序列化和反序列化处理

概述&#xff1a;在现代软件开发领域&#xff0c;不同系统和平台之间的数据交换是不可或缺的方面。JSON&#xff08;JavaScript 对象表示法&#xff09;因其轻量级、人类可读和易于解析的特性而成为一种无处不在的数据格式。使用 C# &#x1f680;编程的 JSON 序列化和反序列化…

C# 语言在AGI 赛道上能做什么

自从2022年11月OpenAI正式对外发布ChatGPT依赖&#xff0c;AGI 这条赛道上就挤满了重量级的选手&#xff0c;各大头部公司纷纷下场布局。原本就在机器学习、深度学习领域占据No.1的Python语言更是继续稳固了自己AI一哥的位置。凭借着Microsoft 和 OpenAI 长期以来一直是紧密相连…

团队管理五个基本法则,帮你打造高效团队

团队管理是一项至关重要的工作&#xff0c;作为一名管理者&#xff0c;应该如何在团队管理中发挥作用呢&#xff1f;以下是团队管理的五个基本原则&#xff0c;它们能够帮助你打造出一个高效协作的团队。 一、以身作则 团队要保持超强的执行力&#xff0c;管理者必须以身作则…

【分类讨论】899. 有序队列

本文涉及知识点 分类讨论 LeetCode899. 有序队列 给定一个字符串 s 和一个整数 k 。你可以从 s 的前 k 个字母中选择一个&#xff0c;并把它加到字符串的末尾。 返回 在应用上述步骤的任意数量的移动后&#xff0c;字典序最小的字符串 。 示例 1&#xff1a; 输入&#xff1…

工业智能网关如何与设备连接?天拓四方

随着工业4.0时代的来临&#xff0c;智能化、自动化已成为工业生产的标配。在这样的背景下&#xff0c;工业智能网关应运而生&#xff0c;成为连接工业设备、实现数据交互与管理的关键节点。本文将阐述工业智能网关如何与设备连接&#xff0c;旨在为读者提供一套清晰、实用的解决…

【node】启动本地打包文件的方式

前言 … 目标 1 初始化node文件 2 将打包文件通过node发布到本地 3 系列文件 【node】创建本地接口 一 node方式 1 在新建一个空的文件夹node 进入空文件夹在,文件夹的地址栏输入cmd回车,会自动跳转到命令行工具里 2 配置初始化文件 在命令行输入命令npm init,生成pac…

CSDN自定义模块全攻略,DIY系统原有样式打造专属个性化主页!

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 &#x1f4af;如何通过HTMLCSS自定义模板diy出自己的个性化csdn主页&#x…

# 消息中间件 RocketMQ 高级功能和源码分析(八)

消息中间件 RocketMQ 高级功能和源码分析&#xff08;八&#xff09; 一、消息中间件 RocketMQ 源码分析&#xff1a;实时更新消息消费队列与索引文件流程说明 1、实时更新消息消费队列与索引文件 消息消费队文件、消息属性索引文件都是基于 CommitLog 文件构建的&#xff0…

mock-前端数据模拟

简介 数据模拟不是开发流程中的必要一环 Json-server 简介&#xff1a; json-server 是一个简单的 Node.js 服务端应用程序&#xff0c;这个工具的主要作用是提供一个模拟的后端服务&#xff0c;可以在前端开发过程中独立于后端进行简单工作。 使用&#xff1a; 1、 安装…

Python重力弹弓流体晃动微分方程模型和交直流电阻电容电路

&#x1f3af;要点 &#x1f3af;计算地球大气层中热层金属坠物运动轨迹 | &#x1f3af;计算炮弹最佳弹射角度耦合微分方程 | &#x1f3af;计算电磁拉莫尔半径螺旋运动 | &#x1f3af;计算航天器重力弹弓运动力学微分方程 | &#x1f3af;计算双摆的混沌运动非线性微分方程…

【无线传感网】分簇路由算法介绍

目录 1、LEACH路由算法 2、PEGASIS 算法 3、TEEN 算法 5、APTEEN 5、LEACH-C 算法 无线传感网中的路由协议就是寻找一条路径让网络中节点沿着这条路径将数据信息传输出去。路由协议的两大关键要点就是路径的优化和数据的分组,在传统计算机网络中,是将网络的拓扑…