C++类和对象(中)(2)

news2024/11/19 19:17:14

一、拷贝构造函数

1.1 拷贝构造函数的概念

在现实生活中我们对于两个一模一样的人我们将他们称之为双胞胎,那么我们在创建对象的时候,能不能创建一个和已经存在的对象一模一样的新对象呢?这种做法是可以的,通过拷贝构造函数我们就能完成这样的操作。

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

注意:1.使用拷贝构造函数进行初始化时,要用同类型的对象进行拷贝初始化。

2.自定义类型的传值传参要调用拷贝构造。

1.2 拷贝构造函数的特征

拷贝构造函数也是特殊的成员函数,它的特征如下所示:

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

2.函数名与类名相同

3.参数只有一个并且必须是同类型对象的引用,采用传值方式编译器会直接报错,因为会无限引发无穷递归调用

我们来看下面的拷贝构造实例:

class Date
{
public:
	//全缺省带参构造
	Date(int year = 2004, int month = 11, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	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;
};
int main()
{
	Date d1;
	d1.Print();

	//两种调用拷贝构造的方式
	//传参
	Date d2(d1);
	d2.Print();
	//使用赋值符号
	Date d3 = d1;
	d3.Print();

	return 0;
}

 我们通过结果可以看出我们写的拷贝构造是可行的,对于拷贝构造的调用方法有两种,一种是在创建的对象后面加括号,另一中是直接用赋值符号,将已存在的对象“赋值”给创建的新对象。

4. 若为显式定义,编译器会生成默认的拷贝构造函数,默认的拷贝构造函数对对象按内存存储按字节序完成拷贝,这种拷贝的方法被称为浅拷贝,或者值拷贝。

我们来看一下编译器的默认拷贝构造函数是怎么样的:

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(const Time& t) " << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	//内置类型
	int _year = 2004;
	int _month = 12;
	int _day = 1;

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



int main()
{
	Date d1;
	
	Date d2(d1);


	return 0;
}

 从这里我们看到这个系统默认的拷贝构造函数对内置类型进行浅拷贝,对自定义类型去调用它的拷贝构造函数。

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

5.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,对于日期类这样的类我们就没有必要再去显式实现拷贝构造了,但是如果是下面这样的类呢?

typedef int DataType;
class Stack
{
public:
	//全缺省的构造函数
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (_array == nullptr)
		{
			perror("malloc申请空间失败!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		_array[_size] = data;
		_size++;
	}

	~Stack()
	{
		cout << "~Stack" << endl;
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

	void Print()
	{
		cout << _capacity << endl;
		cout << _size << endl;
	}

private:
	DataType* _array;
	int _capacity;
	int _size;

};


int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	
	Stack s2(s1);
	return 0;
}

我们来运行这段代码验证一下:

 可以看到程序直接崩溃掉了。

其实崩溃的原因就是:

1.s1对象调用构造函数创建,在构造函数中默认申请了10个元素的空间,然后里面存了三个元素1, 2, 3,

2.s2对象使用了s1拷贝构造,而Stack类没有显式定义拷贝构造函数,则按这着值拷贝进行拷贝,将s1中的内容原封不动的拷贝到s2中,因此s1和s2指向了同一块内存空间

3.程序退出时,s2 和 s1 都要销毁,s2先销毁,s2销毁时调用析构函数,已经将空间释放,到s1销毁时,会将这块空间再次释放一次,一块内存空间被多次释放,必然会造成程序崩溃。

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一但涉及到资源申请的情况时就必须得写拷贝构造函数,否则就是浅拷贝,就会造成程序的崩溃

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

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

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

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

我们来看下下面的实例代码段:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;


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)
{
	//使用d来拷贝构造创建新对象
	Date temp(d);
	return temp;
}


int main()
{
	Date d1(2004, 11, 13);
	Test(d1);


	return 0;
}

 通过这里我们看到这里调用了一次构造函数,和三次的拷贝构造函数,这里是因为:

1.调用构造函数创建d1

2.Test函数是以值传递的方式进行的,因此传参的时候调用拷贝构造函数

3.调用拷贝构造函数创建temp对象

4.函数以值返回的方式进行,返回时使用temp拷贝构造临时对象用来返回

后面调用了四次析构函数,分别是销毁了test函数中的temp,test中的参数d,test函数返回时创建的临时变量,最后是main函数中的d1对象。

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

二、赋值运算符重载

2.1 运算符重载

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

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

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

注意:

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

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

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

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

5. .*  ::  sizeof  ?:  .       这五个符号不能进行重载

对于类还有一个点需要注意:普通函数的函数名就是其地址,但是类里的成员函数的地址要加上去取地址符

下面我们来看下一个全局的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(2008, 11, 12);
	Date d2(2009, 11, 13);
	cout << (d1 == d2) << endl;

	Date d3(2004, 11, 13);
	Date d4(2004, 11, 13);
	cout << (d3 == d4) << endl;
}


int main()
{
	
	test();

	return 0;
}

这是一个全局的重载==,我们将类的成员变量的权限给放开了,因此可以访问类中的成员,但是这样就破坏了封装性,我们以后可以使用友元函数来解决这个问题 ,虽然使用友元也有一些破坏封装,但是比放开权限要好一点。

下面我们在来实现以下在类里面的operator==:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//类内的operator==
	bool operator==(const Date& d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}

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

这样写的话我们的代码封装就没有被破坏,因此推荐这种写法。

2.2 赋值运算符重载

1.赋值运算符重载格式

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

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

(3)检测是否是自己给自己赋值

(4)返回*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;
	}

	//类内的operator==
	bool operator==(const Date& d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._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;
};

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

赋值运算符如果重载成全局的话,就没有this指针了,需要给两个参数,但是这就不符合赋值的意义了,编译也会失败。

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

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

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

下面就是调用默认赋值重载的实例:

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time& operator=(const Time& t)
	{
		if (this != &t)
		{
			_hour = t._hour;
			_minute = t._minute;
			_second = t._second;
		}

		return *this;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	//内置类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time _t;
};



int main()
{
	Date d1(2000, 11, 12);
	Date d2(2000, 11, 13);
	d1 = d2;
	

	return 0;
}

 这里我们可以看到对于默认生成的赋值重载函数对于内置类型进行了值拷贝,对于自定义类型则是调用了自定义类型自己的赋值重载函数。

那么既然编译器默认生成的赋值运算重载,就已经可以完成字节序的值拷贝了,那么我们还需要自己去实现赋值重载函数吗?

对于Date类这种是没有必要的但是如果类中涉及内存资源管理,就必须要我们自己实现赋值运算重载,否则程序可能会崩溃,如果没有涉及资源管理的话,是否实现都可以。

2.3 前置++和后置++重载

我们先将前置++,后后置++的代码实现出来然后再来说明它们之间的区别:

class Date
{
public:
	Date(int year = 2001, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//前置++,返回+1之后的结果
	//注意这里的this指向的对象在函数结束后不会被销毁,因此用引用来提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}

	//由于前置++和后置++都是一元运算符,为了让它们能够正确重载
	//c++规定:后置++重载时多增加一个int类型的参数,调用函数时该参数不用传递,编译器自动传递
	//后置++是先使用后++,返回临时对象,使用传值返回的方式
	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}


private:
	//内置类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time _t;
};



int main()
{
	Date d;
	Date d1(2022, 11, 13);
	d = d1++;
	d = ++d1;

	return 0;
}

前置++,返回+1之后的结果:
 注意这里的this指向的对象在函数结束后不会被销毁,因此用引用来提高效率,由于前置++和后置++都是一元运算符,为了让它们能够正确重载

后置++,先使用再++:
c++规定:后置++重载时多增加一个int类型的参数,调用函数时该参数不用传递,编译器自动传递
后置++是先使用后++,返回临时对象,使用传值返回的方式

结语:

这篇博客到这里就结束了,介绍了C++中的拷贝构造函数以及运算符重载,希望大家能够通过这篇博客获得对C++学习的一点帮助。

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

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

相关文章

Docker镜像的使用与操作

1、什么是镜像 Docker镜像是用于创建容器的只读模板&#xff0c;它包含文件系统。一个Docker镜像可以包括一个应用程序以及能够运行它的基本操作系统环境。 镜像是创建容器的基础&#xff0c;通过版本管理和联合文件系统&#xff0c;Docker提供了一套十分简单的机制来创建镜像…

二叉检索树的查找删除(替换删除)的实现

1、查找元素 当当前结点元素key小于要查找的元素的key时,该元素一定在当前结点的右子树中&#xff0c;以此递归的进行search()&#xff0c;直到key相等。反之亦然 2、查找最小元素 最小元素一定在根结点的左子树中&#xff08;在左子树递归&#xff09; 基准情形&#xff1…

Llama3本地部署实现模型对话

1. 从github下载目录文件 https://github.com/meta-llama/llama3 使用git下载或者直接从github项目地址下载压缩包文件 git clone https://github.com/meta-llama/llama3.git2.申请模型下载链接 到Meta Llama website填写表格申请,国家貌似得填写外国,组织随便填写即可 3.…

中医方解笔记

目录 大青龙汤小青龙汤金匮肾气丸逍遥丸君臣佐参考《方剂学》 加味逍遥丸&#xff08;丹栀逍遥丸&#xff09;使用情况组成丹栀逍遥丸为什么可以缓解口干&#xff1f; 补中益气丸 大青龙汤 小青龙汤 金匮肾气丸 逍遥丸 君 柴胡。疏肝解郁&#xff0c;使肝郁得以条达。 臣 当…

Java高级阶段面试题库(Redis数据库、MQ消息队列、kafka、SpringBoot + SpringCloud、MySQL、JVMJUC、其它)

文章目录 1. Redis数据库篇(忽略)1.1 简单介绍一下redis1.2 单线程的redis为什么读写速度快?1.3 redis为什么是单线程的?1.4 redis服务器的的内存是多大?1.5 为什么Redis的操作是原子性的&#xff0c;怎么保证原子性的&#xff1f;1.6 你还用过其他的缓存吗&#xff1f;这些…

STM32 USB HID报告描述符没有报告长度

STM32 USB HID设置(STM32CubeMX)_我也想成大侠的博客-CSDN博客 不影响鼠标功能

Golang内存、指针逃逸、垃圾回收机制概览

最近看到了一篇文章是关于go的内存、指针逃逸和垃圾回收机制的&#xff0c;发现自己并未很细致的了解过这方面的内容&#xff0c;于是在翻阅各种文章的情况下&#xff0c;写出了这篇总结&#xff0c;参考文章放在文末&#xff0c;可自取 内存 Go 语言使用一个自带的垃圾收集器…

密码学 | 椭圆曲线密码学 ECC 入门(四)

目录 正文 1 曲线方程 2 点的运算 3 求解过程 4 补充&#xff1a;有限域 ⚠️ 知乎&#xff1a;【密码专栏】动手计算双线性对&#xff08;中&#xff09; - 知乎 ⚠️ 写在前面&#xff1a;本文属搬运博客&#xff0c;自己留着学习。注意&#xff0c;这篇博客与前三…

qt设置TextEdit的提示性文字

提示性文字&#xff0c;就是用户在输入的时候自动消失的那种 比如&#xff1a; 可以这样设置&#xff1a; lineEdit->setPlaceholderText("我是提示性文字"); 但是我们觉的这样有点难看&#xff0c;可以用以下QSS来调整&#xff1a; 调整提示性文字的位置&…

Spring Boot集成fastdfs快速入门Demo

1.什么是fastdfs FastDFS 是一个开源的高性能分布式文件系统&#xff08;DFS&#xff09;。它的主要功能包括&#xff1a;文件存储&#xff0c;文件同步和文件访问&#xff0c;以及高容量和负载平衡。主要解决了海量数据存储问题&#xff0c;特别适合以中小文件&#xff08;建议…

【吊打面试官系列】Java高并发篇 - notify()和 notifyAll()有什么区别?

大家好&#xff0c;我是锋哥。今天分享关于 【notify()和 notifyAll()有什么区别&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; notify()和 notifyAll()有什么区别&#xff1f; 当一个线程进入 wait 之后&#xff0c;就必须等其他线程 notify/notifyall,使用 …

Java web应用性能分析之服务端慢和优化概叙

前面已经分析了客户端慢、前端页面慢、入口Nginx慢&#xff0c;按照上图接下来就是我们服务端重点的接口慢分析优化、服务器资源性能瓶颈分析、服务器带宽性能瓶颈分析。 性能优化的目的 性能优化的目标是提高应用的性能&#xff0c;使其更加高效、稳定和可靠。性能优化包括服…

短视频流媒体平台的系统设计

1. 功能需求: 我们的系统有两类参与者 内容创作者 •上传任何类型的视频&#xff08;格式编解码器&#xff09;•视频可以被删除•视频元数据•必填项: 标题&#xff0c;作者&#xff0c;描述•选填项: 分类/标签列表•可以随时更新•当视频对观众可用时&#xff0c;向内容创作…

六边形酷科技特效单页源码

源码介绍 基于canvas画布绘制多个六边形追踪鼠标&#xff0c;科技感的几何图形酷炫动画特效&#xff0c; 单页html源码&#xff0c;可以做网站动态背景&#xff0c;喜欢的朋友可以拿去 效果截图 完整源码 <!doctype html> <html> <head> <meta charset…

IntelliJ-platform plugIn 插件开发专题内容介绍,学习指导(一)

这系列文章出炉对于笔者来说确实不容易&#xff0c;历时快两年了&#xff0c;先后迭代了3版本&#xff0c;暂时与官方最新版本API同步&#xff08;2024.03&#xff09;&#xff0c;文章内容覆盖2022~2024版内容 专题由来 最早接触插件开发是源于公司一个国际化项目&#xff0c…

Ubuntu 22.04.4安装Docker引擎

正文共&#xff1a;1024 字 13 图&#xff0c;预估阅读时间&#xff1a;1 分钟 我们前面安装了几次Ubuntu的操作系统&#xff08;Ubuntu 23.10通过APT安装Open vSwitch&#xff09;&#xff0c;在开始之前&#xff0c;我还是简单提醒一下&#xff0c;从Ubuntu下载页面&#xff…

python:元组,字符串,切片

一、元组# 列表可以修改内容&#xff0c;元组可以不被修改 # 在程序内封装数据&#xff0c;不希望数据被篡改&#xff0c;所以使用元组 # 语法&#xff1a; 不限制类型 # 定于元组的字面量&#xff1a; &#xff08;元素&#xff0c;元素&#xff0c;元素.....&#xff09; # 定…

【人工智能基础】状态空间搜索

状态空间法 状态空间&#xff1a;一个问题全部可能的状态以及其关系的集合。 状态空间图&#xff1a;以图的形式表示问题的状态空间&#xff0c;节点对应状态&#xff0c;边对应状态转移算子&#xff0c;边上的权对应转移所需的代价 问题的解&#xff1a;是从最开始状态到目…

Spring Boot 统一功能处理(三)

本篇主要介绍Spring Boot的统一异常处理。 目录 一、统一异常处理的使用 二、测试统一异常处理效果 三、浅析原理 ControllerAdvice简析 统一处理异常简析 一、统一异常处理的使用 在前面介绍统一数据返回时&#xff0c;我们在程序发生异常时会把整个报错信息都封装在da…

【SQL】DISTINCT GROUP BY

找到所有办公室里的所有角色&#xff08;包含没有雇员的&#xff09;,并做唯一输出(DISTINCT) 用DISTINCT : SELECT DISTINCT B.Building_name,E.Role FROM Buildings B LEFT JOIN Employees EON B.Building_name E.Building需要找到的结果&#xff1a;所有办公室名字&#…