C++基础(4)——类与对象(默认成员函数)

news2024/9/23 7:26:00

       

目录

1.拷贝构造函数:

1.1 为什么要引入拷贝构造:

1.2 拷贝构造函数的定义及特性:

1.3 什么类可以不用编写拷贝构造:

2. 赋值运算符重载:

2.1 为社么要引入运算符重载:

2.2运算符重载的定义以及特性:


       在前面的文章中,引入了C++中类的概念,对于一个类而言,如果其中不存在成员,则该类被称之为空类。但是空类中并不是不存在任何内容,而是编译器会自动生成以下6默认成员函数,即:用户没有显性显示,编译器会生成的函数。

      6个函数大致可以分为以下三类,分别是用于初始化和清理的构造函数析构函数,用于拷贝赋值的拷贝构造函数赋值运算符重载,以及         

     在上一篇文章中,对于用于初始化构造函数的定义其特点进行介绍,即:函数名和类名相同、没有返回值、对象实例化时编译器会自动调用构造函数,并且针对于自定义类型和内置类型的作用不同、可以构成重载。并且介绍了用于清理的析构函数的定义及其特点,即:函数名是类名之前加~,无参数无返回值类型、一个类中只能由一个析构函数(所以析构函数不能构成重载)并且在未显性显示的情况下,编译器会自动生成析构函数、编译器会自动调用析构函数。

     在本文中,将继续介绍默认成员函数中的其他函数:

1.拷贝构造函数:

1.1 为什么要引入拷贝构造:

       在正式介绍拷贝构造函数的定义以及性质之前,需要先说明为什么要引入拷贝构造,为了解释此问题,首先提及数据结构中,对于函数的传参方式。例如在栈中,向各个功能函数传递栈这个数据结构的参数时,一般采用传址调用而非传值调用,这是因为传址调用在速度和大小方面都优于传值调用。但是,这并不意味着传值调用不可以使用,例如在下面的代码中:

#include<iostream>
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2023, int month = 11, int day = 21)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

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

void func(Date dd)
{
    cout << "func(Date dd)" << endl;
	dd.Print();
}
int main()
{
	//类的实例化
	Date d1;
	func(d1);

	return 0;
}

运行结果如下:

      不难发现,向函数func传递参数时,并没有传递指针或者采用引用,而是直接将类作为参数传递。对于这种直接传值的方式,可以称为浅拷贝或者值拷贝。对于上述代码所给出的日期类,浅拷贝并不会造成程序的错误。

      但是在不同的情况下,浅拷贝可能会造成程序的错误,例如上篇文章中提到的栈:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		_array = nullptr;
		_capacity = 0;
		_top = 0;
	}
private:
	DataType* _array;
	int _capacity;
	int _top;
};

为了方便演示,这里专门创建一个函数func2用于检测,具体如下:
 

void func2(Stack s1)
{
	//......
}
int main()
{
	//类的实例化
	Date d1;
	func(d1);

	Stack s;
	func(s);

	return 0;
}

运行上述程序,编译器会显示错误:

     通过对上述日期类和栈类的调用,会发现,在日期类进行传值调用或者说进行浅拷贝时,并不会出现错误,而对于栈这个类则会报错。导致两者不同的原因,就在于栈这个类中,有一个成员变量是指针_array。 对于传值调用,是直接将变量的值进行传递,对于指针也不例外,通过监视窗口,可以观察SS1中指针_array的地址。

        通过图片不难发现,再向函数func2传递参数时,直接将对象S作为参数传递, 因此,对象S中的成员变量的值也传给了形参S1。所以,SS1中的指针_array指向同一块地址,具体可以有下面的图片表示:

       在上一篇文章及文章开头,提及了析构函数的一个特点:对象生命周期结束时,会自动调用析构函数。因此,当函数func2调用结束后,此时对象S1的生命周期结束,因此,析构函数会清理对象S1中指针_array指向的空间。

      当函数运行结束后,当主函数main运行结束时,此时对象S的生命周期结束,编译器会再次调用析构函数,清理对象S中指针_array指向的空间。上面提到,两个对象中的指针指向了同一块空间,因此,本次清理时,会造成错误,因为指针_array指向的空间被清理了两次。

      在C++中,为了解决浅拷贝这种方式在上述情况下会引起错误的问题,因此,C++规定自定义对象在进行拷贝时,需要调用拷贝构造函数

1.2 拷贝构造函数的定义及特性:

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

      特性如下:

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

      为了便于解释特性2中,为什么采用传值方式会引发无穷递归,文章首先给定下面一个构造函数:

Date da(d1);
//拷贝构造函数:
	Date(Date dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}

       在采用上述拷贝方式的情况下,首先传递参数d1,由于C++规定自定义类型传值或者值拷贝需要调用拷贝构造,因此在第一次传参后,并没有直接去调用拷贝构造,而是编译器额外新生成一个拷贝构造函数,并且去调用新生成的拷贝构造函。为了调用拷贝构造函数,首先需要传递参数,但是在传递参数时,又会生成一个新的拷贝构造函数。。。。。。因此会引发无限递归。



 

在特性2中提到,拷贝构造函数的参数只有一个,并且必须是引用的方式,即:

//拷贝构造函数:
	Date(Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}

在这种情况下,再次运行上面的代码,就不会造成无穷递归,具体原理如下:

void func(Date d3)
{
	cout << "func(Date dd)" << endl;
	d3.Print();
}


func(d1);

       在调用func函数时,首先需要传递参数,此时传递参数的方式为传值拷贝,因为,会调用拷贝构造函数,由于拷贝构造函数的参数是类型对象的引用,因此参数dd就是d1的别名,此时的this指针指向d3,所以,在拷贝构造函数对日期类进行赋值时,通过this指针,直接将对象dd的成员变量赋值给this指针指向的对象d3的成员变量,完成赋值。

     并且,由于拷贝构造函数的参数是类型对象的引用,不是传值调用,所以,在向拷贝构造函数传递参数时,不会引发无穷递归(同理,传递指针也可以避免无穷递归)。

    在基本了解了拷贝构造函数的定义以及特性后,可以利用拷贝构造函数来解决上面栈类的问题,即:开辟的空间会被释放两次。解决问题的方法就是通过拷贝构造函数来实现深拷贝,即在拷贝时不只拷贝值,还将被拷贝对象的资源一起进行拷贝。代码如下:

Stack s2(s);
//拷贝构造函数:
	Stack(Stack& stt)
	{
		_array =(int*)malloc(sizeof(int)*stt._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, stt._array, sizeof(int) * stt._capacity);
		_capacity = stt._capacity;
		_top = stt._top;
	}

对于上述代码,this指针指向对象S2stt是对象s的引用,所以,根据深拷贝,需要将被拷贝对象的资源一起拷贝的原则,对对象S2中的指针_array再开辟一块空间,大小和对象stt中的指针指向的空间大小相同,但是两块空间的地址不同,即:


由于两块空间的地址不同,因此,不会出现析构函数将同一块空间释放两次的情况。

1.3 什么类可以不用编写拷贝构造:

针对这个问题,可以通过一个例子进行说明:
首先,将日期类中的拷贝构造删除,即:
 

class Date
{
public:
	//构造函数
	Date(int year = 2023, int month = 11, int day = 21)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	

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

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

void func(Date d3)
{
	cout << "func(Date dd)" << endl;
	d3.Print();
}

int main()
{
	//类的实例化
	Date d1;
	func(d1);

	return 0;
}

运行上述程序,通过监视窗口观察对象d1\, d3

不难发现,即使没有人为编写拷贝构造函数,两个对象依然完成了值拷贝。这是因为拷贝构造函数属于默认成员函数,在没有人为编写的情况下,针对内置类型会自动完成值拷贝。针对自定义类型会去调用此类型的拷贝构造,如果没有人为编写或者显性显示的拷贝构造,则编译器会自动生成。

例如,对于下面的自定义类型violent,类中并没有人为给出拷贝构造函数

int main()
{
	violent p1;
	violent p2(p1);

	return 0;
}
class violent
{
	Stack pp1;
	Stack pp2;
	int size;
};

此时运行程序,通过监视窗口观察类violent中的成员变量pp1\, \, pp2\, \, size

      可以发现,主函数中对象p1\, \, p2中的成员变量都被进行了拷贝,并且还进行了深拷贝。由于成员pp1 \, \, \, pp2的类型是Stack,因此编译器自动调用了成员相对类型的拷贝函数,这一点,可以通过下面的代码进行验证。

       即在拷贝构造函数的开头加上一行打印,如果编译器会自动调用成员相对类型的拷贝函数,即调用Stack中的拷贝函数。则会打印一次。

	//拷贝构造函数:
	Stack(Stack& stt)
	{
		cout << "Stack(Stack& stt)" << endl;
		_array =(int*)malloc(sizeof(int)*stt._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, stt._array, sizeof(int) * stt._capacity);
		_capacity = stt._capacity;
		_top = stt._top;
	}

运行结果如下:

    通过上面的例子,不难看出, 针对Dateviolent这两种类,使用编译器默认生成的拷贝函数即可。不过二者有稍有差别。因为Date中所有成员的类型都是内置类型,编译器默认生成的拷贝构造函数来完成值拷贝已经满足了Date的需求。针对violent这种类的成员变量的类型是自定义类型,需要调用该成员的拷贝构造函数,即StackStack种已经存在了人为编写的拷贝构造函数,编译器直接调用即可。

2. 赋值运算符重载:

2.1 为社么要引入运算符重载:

       在C++中,针对内置类型的变量,可以通过> ,= ,<等运算符来判断他们之间的关系。但是针对类这种这种较为复杂的类型,却不能通过运算符来判断他们之间的大小关系,例如:

bool ret = d1 > d2;

在C++中,如果需要使用运算符来判断类之间的关系,需要利用函数来完成,即:
 

bool Compare(Date x, Date y)
{
	return x._year == y._year &&
		x._month == y._month &&
		x._day == y._day;
		   
}
bool Comparebig(Date x, Date y)
{
	if (x._year > y._year)
	{
		return true;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return true;
	}
	else if (x._year == y._year && x._month == y._month && x._day > y._day)
	{
		return true;
	}

	else
	{
		return false;
	}

}

不过由于不同用户的使用及命名习惯不同,会导致函数的函数名可读性及规范性差。因此,在C++中,为了规范性以及可读性,引入了运算符重载

2.2运算符重载的定义以及特性:

定义如下:运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)

例如,将上面给出的用于比较两个类较大的函数进行改写:
 

bool operator>(Date x, Date y)
{
	if (x._year > y._year)
	{
		return true;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return true;
	}
	else if (x._year == y._year && x._month == y._month && x._day > y._day)
	{
		return true;
	}

	else
	{
		return false;
	}

}

判断两个类是否相等的函数改写为:

bool operator==(Date x, Date y)
{
	return x._year == y._year &&
		x._month == y._month &&
		x._day == y._day;
		   
}

       虽然利用关键字 operator规范了函数名的书写方式后,使得代码的可读性变高,但是在接收函数判断的结果时,例如:

	bool ret = operator>(d1, d2);
	bool ret1 = operator==(d1, d2);

	cout << operator>(d1, d2) << endl;
	cout << operator==(d1, d2) << endl;

代码的可读性仍然不高,因此,C++在此时再次进行了优化,即:

	bool ret = d1 > d2;
	bool ret1 = d1 == d2;

	cout << (d1 > d2) << endl;
	cout << (d1 == d2) << endl;

在这种情况下,编译器会去寻找,代码中是否存在相应的函数,即:operator>,operator==,如果存在则会自动调用,不存在则会报错。

虽然代码的可读性再一次提高,但是针对上述函数依旧存在两个问题:

1. 调用日期类的成员变量时,需要将类的访问限定符由private改为public

2. 函数的参数在传参时,由于传递的参数是自定义类型,并且传参的方式是传值(浅拷贝),因此需要调用拷贝构造函数。

针对问题一,只需要将函数都放在类种便可以解决,针对第二个问题,将函数的传参方式由传值拷贝改为传引用即可,即:
 

class Date
{
public:
	//构造函数
	Date(int year = 2023, int month = 11, int day = 21)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数:
	Date(Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}

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

	bool operator==(Date& x, Date& y)
	{
		return x._year == y._year &&
			x._month == y._month &&
			x._day == y._day;

	}

	bool operator>(Date& x, Date& y)
	{
		if (x._year > y._year)
		{
			return true;
		}
		else if (x._year == y._year && x._month > y._month)
		{
			return true;
		}
		else if (x._year == y._year && x._month == y._month && x._day > y._day)
		{
			return true;
		}

		else
		{
			return false;
		}

	}

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

不过此时运行代码,编译器会显示如下错误:

这是因为,对于成员函数的参数,都会有一个隐藏的参数,即this指针,因此,需要将上述函数的参数改为:
 

bool operator==(Date& y)
bool operator>(Date& y)

再进行函数的调用时,即

bool ret = d1 > d2;
	bool ret1 = d1 == d2;

编译器会自动将上述调用的形式进行转换,转换为:

bool ret = d1 > d2;
	//d1.operator>(&d1,d2)
	bool ret1 = d1 == d2;
	//d1.operaotr(&d1,d2);

对于上述形式,可以理解为,函数内部的参数由两个,一个是指向d1this指针,另一个则是上述函数中传递的参数Datey

在函数调用时,也可以用上述方式进行调用,即:
 

bool ret3 = d1.operator>(d2);

因此,对于上述函数,其正确写法为:
 

bool operator==(Date& y)
	{
		return _year == y._year &&
			_month == y._month &&
			_day == y._day;

	}

	bool operator>(Date& y)
	{
		if (_year > y._year)
		{
			return true;
		}
		else if (_year == y._year && _month > y._month)
		{
			return true;
		}
		else if (_year == y._year && _month == y._month && _day > y._day)
		{
			return true;
		}

		else
		{
			return false;
		}

	}

此时,两个函数内部均有两个参数,即上面所说的传递的参数y和一个指向xthis指针。编译器会通过this指针自动完成函数的整个运行过程。

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

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

相关文章

EasyExcel 注解fillForegroundColor

EasyExcel 注解fillForegroundColor 对应的颜色值 /** * 样式的数据类 * * author leiyiDong **/ Data // 头背景设置成红色 IndexedColors.RED.getIndex() HeadStyle(fillPatternType FillPatternType.SOLID_FOREGROUND, fillForegroundColor 10) // 头字体设置成20 Hea…

qml ParticleSystem3D使用介绍

在 Qt Quick 3D 中,ParticleSystem3D 是用来创建和控制3D粒子系统的元素。粒子系统是图形编程中用于模拟液体、烟雾、火、星空等现象的技术,它通过生成大量小粒子来模拟这些效果。ParticleSystem3D 提供了一个框架,允许开发者定义粒子的各种属性,如生命周期、速度、颜色、大…

Vue3+vite 处理静态资源,解决服务器不显示动态循环img问题

注意&#xff1a; vue2webpack中&#xff0c;通常使用require来动态渲染静态资源。但在vue3vite中&#xff0c;不支持require语法&#xff0c;因此使用require会报undefined&#xff0c;所以官方推荐使用import来动态渲染静态资源。 实现方式动态渲染静态资源 vue2webpack 使…

GitHub桌面版

GitHub桌面版 一、GitHub 桌面版二、clone 仓库三、更新仓库 一、GitHub 桌面版 二、clone 仓库 三、更新仓库

Netty Review - 探索ByteBuf的内部机制

文章目录 概念ByteBuf VS Java NIO BufferByteBuf实现类HeapByteBuf vs DirectByteBufPooledByteBuf vs UnpooledByteBuf其他 ByteBuf的实现机制 概念 ByteBuf是Netty中用于处理二进制数据的缓冲区 Netty的ByteBuf是一个可用于高效存储和操作字节数据的数据结构。与传统的Byt…

OpenStack云计算平台-计算服务

目录 一、计算服务概览 二、安装并配置控制节点 1、先决条件 2、安全并配置组件 3、完成安装 三、安装和配置计算节点 1、安全并配置组件 2、完成安装 四、验证操作 一、计算服务概览 使用OpenStack计算服务来托管和管理云计算系统。OpenStack计算服务是基础设施即服务…

新的预测模型的局部评价指标-pAUROCc

新的预测模型的局部评价指标-pAUROCc Background 局部评价主要是用在不平衡数据上&#xff0c;其合理性&#xff1a;1.局部评价比全局评价敏感&#xff0c;更容易区分模型的优劣&#xff1b;2.临床决策曲线&#xff08;DCA&#xff09;可知&#xff0c;模型使用过程中&#x…

【SpringCloud】从单体架构到微服务架构

今天来看看架构的演变过程 一、单体架构 从图中可以看到&#xff0c;所有服务耦合在一起&#xff0c;数据库存在单点&#xff0c;一旦其中一个服务出现问题时&#xff0c;整个工程都需要重新发布&#xff0c;从而导致整个业务不能提供响应 这种架构对于小项目而言是没有什么…

bclinux aarch64 ceph 14.2.10 云主机 性能对比FastCFS vdbench

部署参考 ceph-deploy bclinux aarch64 ceph 14.2.10-CSDN博客 ceph-deploy bclinux aarch64 ceph 14.2.10【3】vdbench fsd 文件系统测试-CSDN博客 ceph 14.2.10 aarch64 非集群内 客户端 挂载块设备-CSDN博客 FastCFS vdbench数据参考 bclinux aarch64 openeuler 20.03 …

C++初阶 | [五] 内存管理

摘要&#xff1a;new and delete&#xff0c;定位new&#xff0c;&#xff08;C内存管理的方式&#xff09;&#xff0c;malloc/free和new/delete的区别&#xff0c;内存泄漏 关于内存&#xff1a; 栈又叫堆栈——非静态局部变量/函数参数/返回值等等&#xff0c;栈是向下增长…

Modbus RTU协议及modbus库函数使用

一、与Modbus TCP的区别 在一般工业场景使用modbus RTU的场景还是更多一些&#xff0c;modbus RTU基于串行协议进行收发数据&#xff0c;包括RS232/485等工业总线协议。 与modbus TCP不同的是RTU没有报文头MBAP字段&#xff0c;但是在尾部增加了两个CRC检验字节&#xff08;CRC…

红队攻防实战之内网穿透隐秘隧道搭建

别低头&#xff0c;皇冠会掉&#xff1b;别流泪&#xff0c;贱人会笑。 本文首发于先知社区&#xff0c;原创作者即是本人 0x00 前言 构建内网隐蔽通道&#xff0c;从而突破各种安全策略限制&#xff0c;实现对目标服务器的完美控制。 当我们从外网成功获得攻击点的时候&…

【性能优化】JVM调优与写出JVM友好高效的代码

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化&#xff0c;文章内容兼具广度、深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于…

git clone -mirror 和 git clone 的区别

目录 前言两则区别git clone --mirrorgit clone 获取到的文件有什么不同瘦身仓库如何选择结语开源项目 前言 Git是一款强大的版本控制系统&#xff0c;通过Git可以方便地管理代码的版本和协作开发。在使用Git时&#xff0c;常见的操作之一就是通过git clone命令将远程仓库克隆…

ky10 server sp3 解决/boot/grub2/grub.cfg 找不到

现象 /boot/grub2 目录下不存在grub.cfg 配置文件 解决 执行下面脚本即可 yum install -y grub2 grub2-mkconfig -o /boot/grub2/grub.cfg 执行完成第一条命令 执行完成第二条命令 查看效果 已经生成这个文件了

Visual Studio 2019 C# System.BadImageFormatException 解决方法

文章目录 1.DLL文件缺失或不匹配原因解决方法 2.系统环境变量Path下内容过多原因解决方法 3.位数错误原因解决方法 分析几种可能因素 1.DLL文件缺失或不匹配 原因 检查对应Debug路径下的DLL文件是否有缺失 解决方法 将对应的DLL文件放到Debug文件夹里面&#xff0c;检查冗余…

成为AI产品经理——模型构建过程(上)

目录 一、背景 1.对内 2.对外 二、模型构建过程 1.模型设计 2.特征工程 ① 数据清洗 ② 特征提取 数值型数据 标签/描述类数据特征 非结构化数据&#xff08;处理文本特征&#xff09; 网络关系型数据 ③ 特征选择 ④ 训练集/测试集 一、背景 虽然产品经理不…

飞翔的鸟游戏

一.准备工作 首先创建一个新的Java项目命名为“飞翔的鸟”&#xff0c;并在src中创建一个包命名为“com.qiku.bird"&#xff0c;在这个包内分别创建4个类命名为“Bird”、“BirdGame”、“Column”、“Ground”&#xff0c;并向需要的图片素材导入到包内。 二.代码呈现 pa…

深入了解Java8新特性-日期时间API

阅读建议 嗨&#xff0c;伙计&#xff01;刷到这篇文章咱们就是有缘人&#xff0c;在阅读这篇文章前我有一些建议&#xff1a; 本篇文章大概2000多字&#xff0c;预计阅读时间长需要3分钟。本篇文章的实战性、理论性较强&#xff0c;是一篇质量分数较高的技术干货文章&#x…

爬虫逆向你应该懂得Javascript知识

背景 大家在学习爬虫逆向的时候&#xff0c;一般都会涉及到对js源文件进行代码扣去&#xff0c;但是有的时候&#xff0c;你最好有js基础&#xff0c;能发现加密或者解密在那个位置&#xff0c;或者是能用python改写js代码&#xff0c;这就对个人的Javascript的能力有一定要求…