C++学习笔记(10)——拷贝构造与赋值运算符重载

系列文章目录


C++学习笔记

  • C++学习笔记(1)——C++的诞生
  • C++学习笔记(3)——缺省函数与函数重载
  • C++学习笔记(4)——引用
  • C++学习笔记(5)——inline内联
  • C++学习笔记(6)——auto关键字
  • C++学习笔记(7)——类与对象(上)
  • C++学习笔记(8)——类与对象(中)
  • C++学习笔记(9)——类与对象(中上)类的6个默认成员函数

文章目录

  • 系列文章目录
  • 前言
  • 比喻与理解
    • 1、拷贝构造
    • 2、赋值运算符重载
  • 一、拷贝构造函数
    • 1、概念
    • 2、特征
  • 二、使用步骤
  • 二、赋值运算符重载
    • 1、运算符重载
    • 2、赋值运算符重载
      • 1. 赋值运算符重载格式
      • 2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
      • 3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。


前言

比喻与理解

1、拷贝构造

打个比方,拷贝构造造成无穷递归的情况,就像是:我们想要一面镜子里照出画面,采用的方法却是通过另一面镜子和它对照,如此这般,用无穷面镜子一个接一个的两两对照,指望通过镜子两两对照来照出画面。

当参数采用传值方式时,拷贝构造的参数就必须是自产的,为了获得这个自产的参数就要启动新的拷贝构造,可新的拷贝构造也需要一个自产的参数,为了这个自产参数就又要启动一个新的拷贝构造。。。。。。结果就是生成了无穷个连串的拷贝构造,它们都希望自己生成的下一个拷贝构造可以产生一个参数给自己用,然而永远不可能产生一个参数给它用,因此形成了无穷递归调用。

2、赋值运算符重载

打个比方,赋值运算符重载就像是两个工程师拿同一份图纸盖房子,程序员就是工程师,程序员编写的类就是图纸,编译器就是工头,程序运行就是工人盖房子的过程。

两个工程师拿着相同的图纸(图纸内容相同,只有署名不同)去盖两个房子,他们两人雇佣了同一批工人,工人在盖第一个房子的时候没有问题,然而当工人在盖第二个房子的时候发现图纸和上一个一模一样,认为是工程事故于是拒绝施工,除非工程师到场确定造两次否则不开工,然而工程师偷懒没去,结果工人自动由此确定只需盖一座房子就可以完成任务了,于是对着一个房子拍了两张照片发给工头交差,工程师收到照片也认为两个房子造好了于是拿去售楼部卖;
(赋值运算符如果没有显示定义,就会自动生成赋值运算符重载来进行浅拷贝,浅拷贝只会直接拷贝地址和值,不会自动开辟空间存储内存信息)

后来,一家住户入住时没有发现问题,但当两家住户入住的时候发现只有一个房子可以住,就把前一家住户赶出来了;
(由于没有开辟新的空间,只是把地址赋给了两个指针,结果只能存一份后来的那个信息)

再后来,拆迁队拆房的时候发现一个房子要拆两次,自己还补了两份拆迁款,于是报警抓人;
(由于地址存了两份,当我们试图释放内存时会出现把同一份空间释放两次的情况,这是时候编译器会报错中止程序运行。)


一、拷贝构造函数

1、概念

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

2、特征

二、使用步骤

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

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

(关键在于拷贝构造的参数从何而来。当参数采用传值方式时,拷贝构造的参数就必须是自产的,为了获得这个自产的参数就要启动新的拷贝构造,可新的拷贝构造也需要一个自产的参数,为了这个自产参数就又要启动一个新的拷贝构造。。。。。。结果就是生成了无穷个连串的拷贝构造,它们都希望自己生成的下一个拷贝构造可以产生一个参数给自己用,然而永远不可能产生一个参数给它用,因此形成了无穷递归调用。
打个比方,拷贝构造造成无穷递归的情况,就像是:我们想要一面镜子里照出画面,采用的方法却是通过另一面镜子和它对照,如此这般,用无穷面镜子一个接一个的两两对照,指望通过镜子两两对照来照出画面。

代码如下(示例):

#include
using namespace std;

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(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++学习笔记(10)——拷贝构造与赋值运算符重载_第1张图片
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。


#include
using namespace std;

class Time
{
public:
	void Printf()
	{
		cout << "Time" << endl;
	}
};

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;
}

可以看到,当我们没有写显式定义时编译器只进行了值拷贝,没有调用成员函数。
C++学习笔记(10)——拷贝构造与赋值运算符重载_第2张图片

  1. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了。我们只需要对类似于栈的需要深拷贝的类编写拷贝函数。

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

  1. 拷贝构造函数典型调用场景:
  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

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

二、赋值运算符重载

1、运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
    藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出
    现。

如下代码,将**==**运算符重载为了可处理成员变量的运算符。

#include
using namespace std;

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;
};

int main()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout << (d1 == d2) << endl;
	return 0;
}

C++学习笔记(10)——拷贝构造与赋值运算符重载_第3张图片


2、赋值运算符重载

1. 赋值运算符重载格式

参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义

如下代码,我们将赋值运算符**=**重载为成员变量的赋值运算,d1的_day重载为d2的_day

#include
#include
using namespace std;

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)	//构造函数
	{
		_year = year;
		_month = month;
		_day = 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;
};

int main()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	d1 = d2;
	
	return 0;
}

C++学习笔记(10)——拷贝构造与赋值运算符重载_第4张图片C++学习笔记(10)——拷贝构造与赋值运算符重载_第5张图片

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

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
即,一个类可以给他的赋值运算符的功能做个性化定义,而在其他地方,赋值运算符会恢复它本来的作用。在全局域,运算符它本来的作用是不可以被更改的。
C++学习笔记(10)——拷贝构造与赋值运算符重载_第6张图片

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

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
如下:我们没有编写赋值运算符进行赋值操作,但我们的编译器自动生成了赋值操作符完成了赋值操作,对于比较简单的赋值操作,我们可以省略赋值操作符。

#include
using namespace std;
class Text
{
public:
	Text(int m = 1, int n = 2)
	{
		_m = m;
		_n = n;
	}
	void Print()
	{
		cout << _m<<" " << _n << endl;
	}

private:
	int _m;
	int _n;
};
int main()
{
	Text t;
	t.Print();
	Text t1(6, 6);
	t1.Print();
	return 0;
}

C++学习笔记(10)——拷贝构造与赋值运算符重载_第7张图片
C++学习笔记(10)——拷贝构造与赋值运算符重载_第8张图片
C++学习笔记(10)——拷贝构造与赋值运算符重载_第9张图片

还有一种情况,可以通过构造函数间接调用其他类的赋值运算符重载。

如下代码,d1=d2执行时,自动使用了Time重载过的赋值运算符。

#include
using namespace std;
class Time
{
public:
	Time& operator=(const Time& t)
	{
		if (this != &t)
		{
			_hour = t._hour;
			_minute = t._minute;
		}
		return *this;
	}
private:
	int _hour;
	int _minute;
};

class Date
{
public:
	Date(int m = 1,int n=2)
	{
		_year = m;
		_month = n;
	}
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	// 自定义类型
	Time _t;
};

int main()
{
	Date d1;
	Date d2(6,6);
	d1 = d2;
	return 0;
}

C++学习笔记(10)——拷贝构造与赋值运算符重载_第10张图片
C++学习笔记(10)——拷贝构造与赋值运算符重载_第11张图片

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
即,如果要实现的功能涉及到深拷贝,则必须显示定义,再开一片空间储存、拷贝。拷贝构造就相当于个性化复制粘贴,我们把两个对象给它,它把一个对象的内容拷贝给另一个对象,它可以自动进行简单拷贝工作,但为了避免内存泄露导致生产事故,它有意的不去进行内存信息的拷贝复制。

打个比方,程序员就是工程师,程序员编写的类就是图纸,编译器就是工头,程序运行就是工人盖房子的过程。
两个工程师拿着相同的图纸(图纸内容相同,只有署名不同)去盖两个房子,他们两人雇佣了同一批工人,工人在盖第一个房子的时候没有问题,然而当工人在盖第二个房子的时候发现图纸和上一个一模一样,认为是工程事故于是拒绝施工,除非工程师到场确定造两次否则不开工,然而工程师偷懒没去,结果工人自动由此确定只需盖一座房子就可以完成任务了,于是对着一个房子拍了两张照片发给工头交差,工程师收到照片也认为两个房子造好了于是拿去售楼部卖;
(赋值运算符如果没有显示定义,就会自动生成赋值运算符重载来进行浅拷贝,浅拷贝只会直接拷贝地址和值,不会自动开辟空间存储内存信息)
后来,一家住户入住时没有发现问题,但当两家住户入住的时候发现只有一个房子可以住,就把前一家住户赶出来了;
(由于没有开辟新的空间,只是把地址赋给了两个指针,结果只能存一份后来的那个信息)
再后来,拆迁队拆房的时候发现一个房子要拆两次,自己还补了两份拆迁款,于是报警抓人;
由于地址存了两份,当我们试图释放内存时会出现把同一份空间释放两次的情况,这是时候编译器会报错中止程序运行。

你可能感兴趣的:(c++,学习,笔记)