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

       

目录

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指针自动完成函数的整个运行过程。

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

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

相关文章

轻松管理文件名:文件批量重命名的技巧与操作

在日常工作中&#xff0c;文件管理是一项至关重要的任务。其中&#xff0c;文件名的管理更是关键。文件名是在查找文件时最直观的线索。一个好的文件名简短而准确地反映文件的内容或用途。然而&#xff0c;随着时间的推移&#xff0c;可能会发现文件名变得冗长、混乱甚至无法反…

指针变量与指针类型的深入理解

1.知识总结 相关代码展示 #include <stdio.h> int main() {int n 0x11223344;int *pi &n; *pi 0; return 0; } #include <stdio.h> int main() {int n 0x11223344;char *pc (char *)&n;*pc 0;return 0; } #include <stdio.h> int main() {i…

SSM家具个性定制管理系统开发mysql数据库web结构java编程计算机网页源码eclipse项目

一、源码特点 SSM 家具个性定制管理系统是一套完善的信息系统&#xff0c;结合springMVC框架完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用…

【jvm】虚拟机之堆

目录 一、堆的核心概述二、堆的内存细分&#xff08;按分代收集理论设计&#xff09;2.1 java7及以前2.2 java8及以后 三、堆内存大小3.1 说明3.2 参数设置3.3 默认大小3.4 手动设置3.5 jps3.6 jstat3.7 OutOfMemory举例 四、年轻代与老年代4.1 说明 五、对象分配过程5.1 说明5…

Jackson无缝替换Fastjson

目录 文章目录 一&#xff0c;Fastjson到Jackson的替换方案方案代码序列化反序列化通过key获取某种类型的值类型替换 二&#xff0c;Springboot工程中序列化的使用场景三&#xff0c;SpringMVC框架中的Http消息转换器1&#xff0c;原理&#xff1a;2&#xff0c;自定义消息转换…

Visio学习笔记

1. 常用素材 1.1 立方体&#xff1a;张量, tensor 操作路径&#xff1a;更多形状 ⇒ 常规 ⇒ 基本形状 自动配色 在选择【填充】后Visio会自动进行配色&#xff1b;

【C/C++】排序算法代码实现

这里&#xff0c;汇总了常见的排序算法具体代码实现。使用C语言编写。 排序算法实现 插入排序冒泡排序选择排序快速排序希尔排序归并排序 插入排序 #include <stdio.h> #include <stdlib.h>void InsertSort(int arr[],int n){int i,j,temp;for(i 1;i < n;i){ …

初识Java 18-3 泛型

目录 边界 通配符 编译器的能力范畴 逆变性 无界通配符 捕获转换 本笔记参考自&#xff1a; 《On Java 中文版》 边界 在泛型中&#xff0c;边界的作用是&#xff1a;在参数类型上增加限制。这么做可以强制执行应用泛型的类型规则&#xff0c;但还有一个更重要的潜在效果…

vue el-table (固定列+滚动列)【横向滚动条】确定滚动条是在列头还是列尾

效果图&#xff1a; 代码实现&#xff1a; html&#xff1a; <script src"//unpkg.com/vue2/dist/vue.js"></script> <script src"//unpkg.com/element-ui2.15.14/lib/index.js"></script> <div id"app" style&quo…

实战JVM高CPU、内存问题分析定位

背景&#xff1a; 业务中台组件MOSC开展压测工作&#xff0c;并发场景下发现CPU使用率达到100%&#xff0c;虽然程序没有报错&#xff0c;但是这种情况显然已经达到性能瓶颈&#xff0c;对服务带来了验证的效能影响&#xff0c;所以针对该CPU问题必须进行详细的根因分析处理。…

浅谈Python中的鸭子类型和猴子补丁

文章目录 前言一、鸭子类型二、猴子补丁关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 前言 Python 开发者可能…

AdaBoost提升分类器性能

目录 AdaBoost算法原理 AdaBoost工作详情 初始权重分配 第一轮 第二轮 后续轮次 最终模型 AdaBoost的API解释 AdaBoost 对房价进行预测 AdaBoost 与决策树模型的比较 结论 AdaBoost算法原理 在数据挖掘中&#xff0c;分类算法可以说是核心算法&#xff0c;其中 Ada…

如何应用ChatGPT撰写、修改论文及工作报告,提供写作能力及优化工作??

如果我想让gpt从pdf文档中提取相关关键词的内容&#xff0c;可以怎么做呢&#xff1f;&#xff1f;我们评论区讨论 ChatGPT 在论文写作与编程方面也具备强大的能力。无论是进行代码生成、错误调试还是解决编程难题&#xff0c;ChatGPT都能为您提供实用且高质量的建议和指导&am…

flink和机器学习模型的常用组合方式

背景 flink是一个低延迟高吞吐的系统&#xff0c;每秒处理的数据量高达数百万&#xff0c;而机器模型一般比较笨重&#xff0c;虽然功能强大&#xff0c;但是qps一般都比较低&#xff0c;日常工作中&#xff0c;我们一般是如何把flink和机器学习模型组合起来一起使用呢? fli…

9.Docker的虚悬镜像-Dangling Image

1.虚悬镜像的概念 虚悬镜像 (Dangling Image) 指的是仓库名 (镜像名) 和标签 TAG 都是 的镜像。 2.构建本地虚悬镜像 这里我以unbuntu为例来说明。 2.1 编写Dockerfile文件 FROM ubuntu:22.042.2 根据Dockerfile文件构建虚悬镜像 docker build .上面这段命令&#xff0c…

C#开发的OpenRA游戏之属性RenderSprites(8)

C#开发的OpenRA游戏之属性RenderSprites(8) 本文开始学习RenderSprites属性,这个属性是跟渲染有关的,因此它就摄及颜色相关的内容,所以我们先来学习一下调色板,这是旧游戏的图片文件保存的格式,如果放在现代来看,不会再采用这种方法,毕竟现在存储空间变大,便宜了,并…

做流体分析需要知道的两大核心问题:内流和外流

SOLIDWORKS Flow Simulation 是直观的流体力学 (CFD) 分析软件&#xff0c;可以快速轻松的分析产品内部或外部流体的流动情况&#xff0c;以用来改善产品性能和功能。SOLIDWORKS Flow Simulation将专业的流体分析进行功能优化&#xff0c;让普通机械设计师也能进行流体力学分析…

【Linux系统编程二十】:(进程通信2)--命名管道/共享内存

【Linux系统编程二十】&#xff1a;命名管道/共享内存 一.命名管道1.创建管道2.打开管道3.进行通信(server/client) 二.共享内存1.实现原理2.申请内存3.挂接4.通信5.去关联6.释放共享内存7.特性&#xff1a; 一.命名管道 上一篇介绍的一个管道是没有名字的 因为你打开那个文件…

在Python中调用imageJ开发

文章目录 一、在ImageJ中进行Python开发二、在Python中调用imageJ开发2.1、简介2.2、环境配置2.3、测试一2.4、测试二 Python imageJ 解决方案&#xff0c;采坑记录 一、在ImageJ中进行Python开发 原生ImageJ仅支持JS脚本&#xff08;JAVAScript&#xff09;&#xff0c;而Im…

蓝桥杯物联网竞赛_STM32L071_2_继电器控制

Stm32l071原理图&#xff1a; PA11与PA12连接着UNL2803 ULN2803是一种集成电路芯片&#xff0c;通常被用作高电压和高电流负载的驱动器。 ULN2803是一个达林顿阵列&#xff0c;当输入引脚&#xff08;IN1至IN8&#xff09;被连接到正电源时&#xff0c;相应的输出引脚&#xff…