【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 :要复合连续赋值的含义

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

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

默认生成的函数行为总结

在这里插入图片描述

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

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

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

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

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

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

相关文章

SAFEnet加密机的加密算法和技术

SAFEnet加密机是一款功能强大、安全可靠的加密设备&#xff0c;它在网络安全领域发挥着不可替代的作用。下面将从特点、功能、应用及优势等方面对SAFEnet加密机进行详细介绍。 一、特点 先进的加密算法和技术&#xff1a;SAFEnet加密机采用了最先进的加密算法和技术&#xff0c…

学懂C#编程:从一个简单的例子理解事件处理

在C#中&#xff0c;事件是一种特殊的委托类型&#xff0c;用于在对象上发生某些事情时通知订阅者。事件的处理通常包括定义事件&#xff0c;创建触发事件的条件&#xff0c;以及订阅该事件的事件处理程序。 以下是一个简单的C#事件处理示例&#xff1a; using System;// 定义…

12 物理层解析

物理层解析 一、物理层功能 ​ 物理层主要功能 功能一&#xff1a;为数据端设备提供传送数据的通路 功能二&#xff1a;传输数据 二、物理层关心的问题 &#xff08;一&#xff09;信号 ​ 信息是人对现实世界事物存在方式或运动状态的某种认识 ​ 数据是用于描述事物的…

网络安全:什么是SQL注入

文章目录 网络安全&#xff1a;什么是SQL注入引言SQL注入简介工作原理示例代码 攻击类型为什么SQL注入危险结语 网络安全&#xff1a;什么是SQL注入 引言 在数字化时代&#xff0c;数据安全成为了企业和个人最关心的问题之一。SQL注入&#xff08;SQL Injection&#xff09;是…

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

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

Debian的系统启动过程

1 启动过程概述 1.1 第一阶段&#xff1a;BIOS BIOS是启动过程的第一阶段&#xff0c;在上电事件后开始。CPU的程序计数器在上电事件后被初始化为一个特定的内存地址&#xff0c;驻留在只读存储器&#xff08;ROM&#xff09;中的BIOS就是从这个特定的内存地址开始执行。 BIO…

厚膜电阻电路丝网印刷

厚膜丝网印刷 该技术用于需要长寿命、热耐久性、机械强度、导热性、高密度电气互连、低介电损耗等的苛刻应用 特征&#xff1a; 陶瓷标准工艺从前到后的通孔连接 正面和背面的丝网印刷电阻器是标准工艺 金导体可以用金线和/或氧化铝线进行线键合 可焊接金属化&#xff0c;…

Java 将不同的List集合复制到另一个集合

在Java中&#xff0c;你可以使用多种方法将一个或多个List集合的元素复制到另一个List集合中。 以下是一些常见的方法&#xff1a; 使用addAll方法&#xff1a; 如果要将一个List的所有元素添加到另一个List的末尾&#xff0c;可以使用addAll方法。 List<String> list1 …

第三十四章 添加和使用自定义标题元素 - 支持的标头元素

文章目录 第三十四章 添加和使用自定义标题元素 - 支持的标头元素支持的标头元素标头元素和 WSDL必需的标题元素 定义自定义标题元素 第三十四章 添加和使用自定义标题元素 - 支持的标头元素 支持的标头元素 IRIS 网络服务和客户端自动支持 WS-Addressing 和 WS-Security 标头…

oracle 数据库导入dmp文件

荆轲刺秦王 从线上正式环境导出的 dmp 文件&#xff0c;导入到本地 oracle 数据库。 1. 创建用户: CREATE USER hf_chip IDENTIFIED BY hf_chip; 2. 授予 CONNECT 和 RESOURCE 基本权限给新用户。 GRANT CONNECT, RESOURCE TO hf_chip; 3. 创建表空间 CREATE TABLESPACE…

python技术面试题(其三)

二十一&#xff0c; 谈谈正则的贪婪模式和非贪婪模式 在数量级匹配符中使用 默认为贪婪模式&#xff0c;用最多量的方式完成匹配 数量级匹配符后加&#xff1f;即为非贪婪 用最少的方式完成匹配 二十二&#xff0c; a1 aa1 的堆栈内存过程 不可变类型&#xff0c;在调用栈里…

【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;初步了解 二叉搜索树 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀map与set &#x1f4d2;1.…

Scala Iterator(迭代器)

Scala Iterator(迭代器) Scala Iterator(迭代器)是一种用于访问集合元素序列的抽象数据类型。它提供了一种高效的方式来逐个访问集合中的元素,而不需要暴露其底层结构。Scala中的迭代器是不可变的,这意味着它们只能被单向遍历一次。一旦迭代器被消耗完,就不能再重新使用…

C# 实战-Controls属性

在C#窗体应用中&#xff0c;Controls是Control类或其派生类&#xff08;如Form、Panel、GroupBox等&#xff09;的一个属性。它表示控件集合&#xff0c;这个集合包含了控件的所有子控件。通过Controls属性可以访问、添加或删除控件。以下是几个详细的例子来说明如何理解和使用…

开发uniapp插件包aar文件,使uniapp可以调用jar包

背景 使用 uniapp 开发应用时&#xff0c;很多时候都需要调用第三方的sdk&#xff0c;一般以 jar 为主。为了应对这个问题&#xff0c;官方提供了插件方案&#xff0c;可以将第三方 jar 包进行封装为 aar 包后&#xff0c;再集成到 uniapp 中使用。 一、环境安装工具 1、jdk…

AI大模型是真的贵

背景&#xff1a; 微软和谷歌母公司Alphabet最新公布的季度财报均显示&#xff0c;由于企业客户在人工智能服务上的投入增加&#xff0c;其云计算业务实现了显著的收入增长。尽管Meta在将人工智能技术转化为收益方面稍显滞后&#xff0c;但它表示&#xff0c;其相关努力对提高…

C语言练习02-数组

一、求最值 已知数组元素为{33,5,22,44,55}&#xff0c;找出数组中的最大值并打印在控制台 #include<stdio.h> #include<math.h>int main() {int arr[] {33,5,22,44,55};int max arr[0];int len sizeof(arr) / sizeof(int); //注意&#xff1a;sizeof(arr)计算…

力扣(2024.06.20)

1. 48——旋转图像 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在原地旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。 标签&#xff1a;数组&#xff0c;数学&#xff0c;矩阵 代码…

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

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

Linux系统安装Dify结合内网穿透实现远程访问本地LLM开发平台

文章目录 前言1. Docker部署Dify2. 本地访问Dify3. Ubuntu安装Cpolar4. 配置公网地址5. 远程访问6. 固定Cpolar公网地址7. 固定地址访问 前言 本文主要介绍如何在Linux Ubuntu系统使用Docker快速部署大语言模型应用开发平台Dify,并结合cpolar内网穿透工具实现公网环境远程访问…