类与对象(中(2))

ops/2024/9/22 21:30:24/

开头

大家好啊,上一期内容我们介绍了类与对象中六大默认成员函数中的两种--->构造函数与析构函数,相信大家多少都形成了自己的独到见解。那么今天,我将继续就拷贝构造函数与运算符重载函数来展开讲解,话不多说,我们进入正题~~

拷贝构造

概念

相信大家在生活中多少都见过双胞胎吧,由于某些原因,他们的外形几乎一致。那么在类与对象中,我们是否可以创建一个与已存在对象一致的新对象呢?---Of course!它便是拷贝构造。

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

特征

*特别说明:拷贝构造函数也是一种特殊的成员函数。

1. 拷贝构造函数是构造函数的一个重载形式

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

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(const Date& d)   // 正确写法Date(const Date& d)   // 错误写法:编译报错,会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;Date d2(d1);return 0;
}

C++规定:

内置类型直接拷贝(浅拷贝/值拷贝)

自定义类型必须调用拷贝构造完成拷贝

这就能很好解释为什么不能传值调用(会引发无限递归,编译器一般会直接报错)

3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

关于深拷贝与浅拷贝,后期会专门拎出来出一期博客,此处不过多解释~

下面我们通过代码进行更深入的理解:

class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
}

如图,由于d1为自定义类型,但由于其没有显示定义的拷贝构造函数,编译器先给Date类生成一个默认拷贝构造函数并赋予其默认值:

调用时由于Date类型内有一个自定义类型Time,故编译器会先跳入Time类调用拷贝构造,如下图:

看一下运行结构:

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝

我们再来看一串代码:

// 这里会发现下面的程序会崩溃掉?这里就需要用到深拷贝去解决。
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 s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}

我们用一张图来理解:

简单来说,浅拷贝的缺点

1.拷贝的对象与原对象共用一块空间,等到程序结束时自动调用的析构函数将会对同一块空间释放两次

2.修改其中一个对象时将会影响另一个对象

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

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

1.使用已存在对象创建新对象

2.函数参数类型为类类型对象

3.函数返回值类型为类类型对象

此图对应下方代码:

class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d)
{Date temp(d);return temp;
}
int main()
{Date d1(2022, 1, 13);Test(d1);return 0;
}

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


补充

我们就构造函数与运算符重载函数做一个简单的区分:

构造函数:用一个已经存在的对象初始化另一个对象。

运算符重载函数:已经存在的两个对象之间的赋值拷贝。

默认成员函数不能写到全局。

运算符重载函数建议写到类里边,否则易受到访问限定符的限制


赋值运算符重载

运算符重载

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

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意:

1.不能通过连接其他符号来创建新的操作符:比如operator@

2.重载操作符必须有一个类类型参数

3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义

4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this

5.  (.*)  (::)   (sizeof)   (? :)  ( . )   注意以上5个运算符不能重载。

// 全局的operator==
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;
}
void Test()
{Date d1(2018, 9, 26);Date d2(2018, 9, 27);//(d1==d2)等价于 d1.operator==(d2)cout << (d1 == d2) << endl;
}
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& d2)// 这里需要注意的是,左操作数是this,指向调用函数的对象bool operator==(const Date & d2){return  _year == d2._year&& _month == d2._month&& _day == d2._day;}
private:int _year;int _month;int _day;
};

大家可以细细品一下上方的代码哦~


赋值运算符重载

1.运算符重载格式

(*下方T为自定义类型)

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

检测是否自己给自己赋值

返回*this :要复合连续赋值的含义

特别的,任何指针类型都是内置类型~

来看代码:

class Date
{
public://构造函数Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//拷贝构造函数Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//运算符重载Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};

上方便是赋值运算符重载函数的格式,它的调用方式如下:

Date d1(2024,8,20);
Date d2(2024,8,19);
d1.operator=(d2);

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

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

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

默认生成赋值重载跟拷贝构造行为一样:
1、内置类型成员--值拷贝/浅拷贝(Date和MyQueue不需要我们自己实现赋值重载)
2、自定义类型成员会去调用他的赋值重载(Stack需要我们自己实现,因为默认生成是浅拷贝)

class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time& operator=(const Time& t){//this指针指向的是d1的地址if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2;d1 = d2;return 0;
}

那么问题来了,既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,那还需要我们自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?我们来验证一下:

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 s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2;s2 = s1;return 0;
}

可以看到运行崩溃了,这又是为什么呢?这里先买个关子,后面的深拷贝会继续讲解~

这里我们先用一张图来理解为什么:

我们可以得出一个结论:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。


前置++和后置++重载

通过上面的分享,不知你是否对运算符重载有了一定的了解----我们趁热打铁,来了解一下它的延伸用法。

先来看看同一个函数的不同实现方法:

int main()
{Date d;Date d1(2022, 1, 13);d = d1++;    // d: 2022,1,13   d1:2022,1,14d = ++d1;    // d: 2022,1,15   d1:2022,1,15return 0;
}

我们不妨来思考一下,哪种实现方式更为高效呢?

是的!毋庸置疑是第二种(它所开辟的临时变量相较于第一种明显更少)。

日期类的实现

接下来我们再来看看日期类的实现:

class Date
{
public:// 获取某年某月的天数int GetMonthDay(int year, int month){static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,31 };int day = days[month];if (month == 2&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){day += 1;}return day;}// 全缺省的构造函数Date(int year = 1900, int month = 1, int day = 1);// 拷贝构造函数// d2(d1)Date(const Date& d);// 赋值运算符重载// d2 = d3 -> d2.operator=(&d2, d3)Date& operator=(const Date& d);// 析构函数~Date();// 日期+=天数Date& operator+=(int day);// 日期+天数Date operator+(int day);// 日期-天数Date operator-(int day);// 日期-=天数Date& operator-=(int day);// 前置++Date& operator++();// 后置++Date operator++(int);// 后置--Date operator--(int);// 前置--Date& operator--();// >运算符重载bool operator>(const Date& d);// ==运算符重载bool operator==(const Date& d);// >=运算符重载bool operator >= (const Date& d);// <运算符重载bool operator < (const Date& d);// <=运算符重载bool operator <= (const Date& d);// !=运算符重载bool operator != (const Date& d);// 日期-日期 返回天数int operator-(const Date& d);
private:int _year;int _month;int _day;
};

配套的实现来咯~我们仅需写一个operator<与operator==,而后便只需要运用重载知识对其进行调用,便能一一实现。

bool Date::operator<(const Date& x)
{if (_year < x._year){return true;}else if (_year == x._year && _month < x._month){return true;}else if (_year == x._year && _month == x._month && _day < x._day){return true;}return false;
}bool Date::operator==(const Date& x)
{return _year == x._year&& _month == x._month&& _day == x._day;
}// 复用
// d1 <= d2
bool Date::operator<=(const Date& x)
{return *this < x || *this == x;
}bool Date::operator>(const Date& x)
{return !(*this <= x);
}bool Date::operator>=(const Date & x)
{return !(*this < x);
}bool Date::operator!=(const Date& x)
{return !(*this == x);
}int Date::GetMonthDay(int year, int month)
{static int daysArr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//if (((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) && month == 2)if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}else{return daysArr[month];}
}Date& Date::operator+=(int day)
{_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){++_year;_month = 1;}}return *this;
}// d1 + 100
Date Date::operator+(int day)
{Date tmp(*this);tmp += day;return tmp;
}// 前置++
Date& Date::operator++()
{*this += 1;return *this;
}// 后置++
// 增加这个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成重载
Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}

结尾

好了,以上便是本期的全部内容,如果对您有帮助,请动动您的小手指为我点个赞,万分感谢,我们一同进步!


http://www.ppmy.cn/ops/97644.html

相关文章

[数据集][目标检测]瞳孔虹膜检测数据集VOC+YOLO格式8768张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;8768 标注数量(xml文件个数)&#xff1a;8768 标注数量(txt文件个数)&#xff1a;8768 标注…

MySQL列表分区分区表

什么是列表分区分区表&#xff1f; 列表分区是一种根据某个列的离散值将表数据分割成多个分区的分区方式。在列表分区中&#xff0c;每个分区都有自己的离散值集合&#xff0c;当插入数据时&#xff0c;MySQL会根据指定的列值将数据分配到相应的分区中。这种分区方式可以使得表…

Python 将单词拆分为单个字母组成的列表对象

Python 将单词拆分为单个字母组成的列表对象 正文 正文 这里介绍一个简单算法&#xff0c;将英文单词拆分为其对应字母组成的列表。 str1 ACG lst1 [i for i in str1] lst2 list(str1)# Method 1 print(lst1) # Method 2 print(lst2) """ result: [A, C, G…

公开数据库汇总及下载(1)-TCGA

文章目录 1. 常用数据库2. TCGA数据下载2.1 TCGA介绍2.2 数据类型详解2.3 TCGA数据等级2.4 TCGA网页筛选页面介绍2.5 各文件每个字段的含义2.6 biospecimen IDs介绍(样本ID)2.7 下载代码 参考文件 本文的内容介绍、代码下载主要参考了网上多个文件汇总而成&#xff0c;本文仅作…

集团数字化转型方案(三)

集团数字化转型方案通过系统整合人工智能&#xff08;AI&#xff09;、大数据、云计算和物联网&#xff08;IoT&#xff09;技术&#xff0c;建立了一个全面智能化的业务管理平台&#xff0c;涵盖从业务流程自动化、数据驱动决策支持&#xff0c;到客户体验优化和供应链管理的各…

C++之多态(下)

目录 多态的实现原理 多态的拓展 单继承中的多态 多继承中的多态 上期&#xff0c;我们学习了多态的基本概念&#xff0c;本期我们来学习多态的实现原理。 多态的实现原理 class Base { public:virtual void func1(){cout << "Base::func1()" <<…

2024.8.21 作业

一个服务器和两个客户端聊天 代码&#xff1a; /*******************************************/ 文件名&#xff1a;server.c /*******************************************/ #include <myhead.h> #define SER_IP "192.168.2.7" // 服务器IP #define SER…

【鸿蒙学习】HarmonyOS应用开发者高级认证 - 一次开发,多端部署

一、学习目的 掌握鸿蒙的核心概念和端云一体化开发、数据、网络、媒体、并发、分布式、多设备协同等关键技术能力&#xff0c;具备独立设计和开发鸿蒙应用能力。 二、总体介绍 HarmonyOS 系统面向多终端提供了“一次开发&#xff0c;多端部署”&#xff08;后文中简称为“一…