【C++】类和对象【中篇】--C++六个默认成员函数以及const成员函数

一、类的6个默认成员函数

在用C语言实现数据结构的时候,对于链表,栈,队列,二叉树等等,我们可能会犯这两个错误:

1.在使用数据结构创建变量的时候忘记对其进行初始化操作就开始插入数据等一系列操作。

2.使用完毕之后我们可能忘记对动态开辟的空间进行释放,从而可能造成内存泄漏的问题。

C++是在C语言的基础上生长出来的,C++大佬在设计语言的时候也考虑到了这个问题,在C++设计出了默认成员函数:

默认成员函数:当用户没有显式实现,编译器会生成的成员函数称为默认成员函数

在这里插入图片描述

其中,构造函数和析构函数就解决了我们上面提到的问题,对面后面的四个默认成员函数,则是适用在其他的场景下,我们将一一介绍他们的功能和用法。

【注意】

如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?

并不是,任何类在什么都不写时,编译器会自动生成以上6个默认成员函数。

特殊成员函数,我们不写,编译器会直接生成一个,我们写了编译器就不会生成。隐含的意思,对于有些类需要我们自己写,对于另外一些类,编译器生成的就可以用

二、构造函数

1.概念

构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

2.特性

特征如下:

1.函数名与类名相同

2.无返回值(void都不用写)

3.对象实例化时编译器自动调用对应的构造函数

4.构造函数可以重载(提供多个构造函数,多种初始化方式)

5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

6.C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。构造函数对内置类型不做处理,对自定义类型会调用它自身的默认构造

7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

【注意】:无参构造函数全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参数就可以调用的构造函数,就叫默认构造)

我们以Date类说明:

#include <iostream>
using namespace std;

class Date
{
    
    
public:
	//有参构造
	Date(int year, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}

	//无参构造
	Date()
	{
    
    
		_year = 1;
		_month = 1;
		_day = 1;
	}

	//打印
	void Print()
	{
    
    
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	Date d1(2023, 1, 1);
	Date d2;
	d1.Print();
	d2.Print();
	return 0;
}

在这里插入图片描述

我们可以看到,我们没有显示地去调用构造函数,而是由编译器自动调用,构造函数的功能和数据结构中的Init功能相似

【注意】

1.构造函数支持重载和缺省参数,这样提供了多种初始化的方式,但是无参构造和全缺省不能同时存在,因为这样存在调用的二义性:

在这里插入图片描述

此外,当需要用多个参数进行初始化的时候,我们提供全缺省,半缺省和无参时使得我们的构造函数写得比较冗余,我们可以只提供全缺省的构造,这样无参和带参都可以进行初始化。所以我们简化的构造函数如下:

在这里插入图片描述

2.当我们不传参数进行初始化的时候,不要在对象后面加一个括号,因为这样编译器分不清这是在实例化对象还是函数调用。

在这里插入图片描述

并且在调用Print函数时会报错:

在这里插入图片描述

2.1特征分析——自动生成

对于构造函数的第五点特性,我们知道如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成,我们下面进行证明:

class Date
{
    
    
public:
	//打印
	void Print()
	{
    
    
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	Date d;
	d.Print();
	return 0;
}

在这里插入图片描述

在这里插入图片描述

我们可以看到,默认的构造函数好像并没有帮我们完成初始化工作,_year,_month,_day依然是一个随机值,好像编译器的默认初始化并没有什么用,这就要结合构造函数的第六点特性(构造函数对内置类型不做处理,对自定义类型会调用它自身的默认构造)。所以_year,_month,_day仍然是一个随机值。

2.2.特征分析——选择处理

根据构造函数的第六点特性,我们知道C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。构造函数对内置类型不做处理,对自定义类型会调用它自身的默认构造

对于这个特性,我们用A,Date,Stack,Myqueue三个类来进行说明,其中Myqueue是用两个栈实现的队列。

A:

class A
{
    
    
public:
	A()
	{
    
    
		_a = 0;
		cout << "A()构造函数" << endl;
	}
private:
	int _a;
};

Date:

class Date
{
    
    
public:
	//打印
	void Print()
	{
    
    
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// 内置类型
	int _year;
	int _month;
	int _day;

	// 自定义类型
	A _aa;
};

Stack:

class Stack
{
    
    
public:
	Stack(int capacity = 4)
	{
    
    
        cout << "Stack(int capacity = 4)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
    
    
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;

	}

	void Push(int x)
	{
    
    
		_a[_top++] = x;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

Myqueue:

class Myqueue
{
    
    
public:
	void Push(int x)
	{
    
    
		_PushST.Push(x);
	}
private:
	Stack _PushST;
	Stack _PopST;
};

1,对于日期类来说,既有自定义类型又有内置类型,当我们创建一个日期类对象时,会有如下结果:

在这里插入图片描述

我们可以看到,当一个类既有内置类型又有自定义类型时,自定义类型会去调用它自身的默认构造,而内置类型不做处理。

2.对于栈这个类来说,只有自定义类型,我们也显示写了它的默认构造,我们分为显示写和不显示写两种情况进行讨论:

2.1显示写:

在这里插入图片描述

2.2不显示写:

在这里插入图片描述

当编译器自动生成的构造函数不能满足我们需求的时候,我们需要手动去写构造函数

3.对于Myqueue类来说,它的成员都是自定义类型,所以我们不提供构造函数,编译器自动生成的构造函数会去调用自定义类型的构造函数。

在这里插入图片描述

【总结】

所以根据上面的案例,我们到底什么时候需要我们自己提供默认构造函数,什么时候使用编译器自动生成的默认构造呢?是内置类型我们自己写,自定义类型使用编译器默认的吗?事实上不是这样的,我们需要面向需求,当编译器的默认生成的构造函数能满足我们的需求时就不需要我们自己写,比如案例中的Myqueue反之,当编译器默认生成的构造函数不能满足我们的需求时,就需要我们自己写,比如案例中的Date,Stack。

2.3特征分析——默认构造

对于构造函数第七点特征:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(无参构造函数全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参数就可以调用的构造函数,就叫默认构造),如果我们没有提供默认构造函数编译器会报错。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

但是如果类中没有默认构造函数,那么我们实例化对象的时候就不许传递参数,否则就会报错。

在这里插入图片描述

3.C++11补丁——缺省值

由构造函数的第六个特性我们知道,编译器自动生成的默认构造函数对内置类型不做处理,对自定义类型会调用它自身的默认构造,这就使得编译器自动生成的默认构造在有的时候看起来没有什么作用,所以C++11针对编译器对内置类型不做处理这个缺陷,打了一个补丁,就是内置类型成员在声明的时候可以给一个缺省值,达到在没有显示写构造函数的时候,能够用缺省值对变量进行初始化。

在这里插入图片描述

缺省值不仅可以一个变量进行缺省,还可以缺省一块动态申请的空间

在这里插入图片描述

【注意】

这里我们给成员变量缺省值并不是对其进行初始化,因为类中的成员

三、析构函数

1.概念

析构函数:与构造函数功能相反,析构函数完成对象中资源的清理工作。对象在销毁时会自动调用析构函数,析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。

【注意】

我们知道,当变量的声明周期结束时变量被销毁,所以位于函数中的局部对象会随着函数栈帧的销毁而销毁,而位于main函数栈帧里的全局对象会在main函数调用完成时销毁,注意,后定义的对象会被先销毁

2.特征

1.析构函数名是在类名前加上字符 ~

2.无参数无返回值类型

3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

4.对象生命周期结束时,C++编译系统系统自动调用析构函数

5.编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。析构函数对内置类型不做处理,对自定义内心会调用它的析构函数。

1.对于date:

class Date
{
    
    
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

在这里插入图片描述

编译器自动生成的析构函数对内置类型不做处理,而且Date也没有进行资源的申请(malloc内存,fopen文件等操作),所以我们不用显示的写析构函数,直接使用编译器自动生成的析构函数即可。

2.对于Stack:

class Stack
{
    
    
public:
	Stack(int capacity = 4)
	{
    
    
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
    
    
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	void Push(int x)
	{
    
    
		_a[_top++] = x;
	}

	~Stack()
	{
    
    
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a = (int*)malloc(sizeof(int) * 4);
	int _top = 0;
	int _capacity = 4;
};

在这里插入图片描述

Stack构造的时候向内存动态申请了一块空间,如果我们没有自己写析构函数,而编译器自动生成的析构函数对内置类型不做处理,那么_a申请的空间则没有被释放掉,就会造成内存泄漏,所以我们需要自己写析构函数。

3.对于Myqueue:

class Myqueue
{
    
    
public:
	void Push(int x)
	{
    
    
		_PushST.Push(x);
	}
private:
	Stack _PushST;
	Stack _PopST;
};

在这里插入图片描述

Myqueue的两个成员都是自定义类型,所以编译器会调用他们自己的析构函数,所以Myqueue动态申请的空间也会得到释放,不需要我们手动去写析构函数。

【总结】

如果类中没有动态申请的内存,此时我们不需要手动去写析构函数,用编译器自动生成的就可以,比如Date类,对于像Stack像内存动态申请了空间,我们就需要自己写,而Myqueue的会调用Stack的析构函数,所以也不需要我们自己写,总之,编译器自动生成的够用我们就不需要自己写,不够用就需要自己写,最终是看自己的需求。

四、拷贝构造函数

1.概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎

在这里插入图片描述

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

C++拷贝构造函数实现了这个功能

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

2.特征

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

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

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

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

4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,对内置类型以字节为单位直接进行拷贝–浅拷贝,对自定义类型调用其自身的拷贝构造函数

Date类的拷贝构造:

class Date
{
    
    
public:
	Date(int year = 1, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	Date d1(2023, 1, 1);
	Date d2(d1);
	return 0;
}

在这里插入图片描述

2.1引用分析——引用做参数

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

在这里插入图片描述

原因如下:当我们使用d1来拷贝构造创建d2的时候,编译器会自动调用拷贝构造函数,而函数传值传参需要进行临时拷贝,即形参d是d1的一份拷贝,所以也会调用拷贝构造,那么就会一直循环下去,从而引发了无穷的递归,
在这里插入图片描述

但是我们使用引用传参的话,形参是实参的别名,不需要进行拷贝,所以就不会出现无穷递归的现象

此外,拷贝构造函数的参数通常使用const修饰,这是避免函数内部写错从而出现错误,比如下面的错误:
在这里插入图片描述

不仅没有正确的拷贝到d2,还把d1的值修改了,所以我们加上const,此时编译器就会报错,起到了很好的提示左右,避免了这样的错误

在这里插入图片描述

2.2特征分析——深浅拷贝

拷贝构造的第四个特性:编译器生成的默认拷贝构造函数,对内置类型以字节为单位直接进行拷贝–浅拷贝,对自定义类型调用其自身的拷贝构造函数

对于深浅拷贝,我们以Date,Stack,Myqueue三个类为例进行说明:

Date:

class Date
{
    
    
public:
	Date(int year = 1, 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;
	//}
private:
	int _year;
	int _month;
	int _day;
};

在这里插入图片描述

我们可以看到,我们没有显示的定义Date的拷贝构造函数,那么编译器自动生成的拷贝构造函数,将d1的_year,_month,_day按照字节拷贝到了d2中。

Stack:

class Stack
{
    
    
public:
	Stack(int capacity = 4)
	{
    
    
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
    
    
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
    
	~Stack()
	{
    
    
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
    
    
		// ....
		// 扩容
		_a[_top++] = x;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

我们可以看到,我们没有显示的调用Stack的拷贝构造函数,那么编译器自动生成的拷贝构造函数把Satck的成员变量_a,_top,_capacity,拷贝到了st2中:

在这里插入图片描述

但是如果我们继续进行调试,就会触发一个异常:

在这里插入图片描述

我们知道,编译器将st1的内存拷贝到st2中,但是成员变量_a是一块动态申请的空间,即_a中存放的是动态空间的起始地址,那么st1的_a拷贝给了st2的_a了之后,此时,他们指向了同一块空间,当main函数栈帧销毁的时候,编译器会自动调用Stack的析构函数,此时同一块空间就被释放了两次,从而引发了异常,同时st1插入数据时也会改变st2的内容。

在这里插入图片描述

所以我们正确的做法应该给st2重新开辟一块空间,再把st1的内容拷贝到st2中:

class Stack
{
    
    
public:
	Stack(int capacity = 4)
	{
    
    
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
    
    
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
	
	// st2(st1)
	Stack(const Stack& st)
	{
    
    
		cout << "Stack(const Stack& st)" << endl;

		_a = (int*)malloc(sizeof(int)*st._capacity);
		if (_a == nullptr)
		{
    
    
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int)*st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	~Stack()
	{
    
    
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
    
    
		// ....
		// 扩容
		_a[_top++] = x;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

在这里插入图片描述

在这里插入图片描述

Myqueue:

class MyQueue
{
    
    
public:
	void push(int x)
	{
    
    
		_pushST.Push(x);
	}
private:
	Stack _pushST;
	Stack _popST;
	size_t _size = 0;
};

对于Myqueu类来说,它的成员函数既有自定义类型又有内置类型,所以对于自定义类型来说,它会去调用它自身的拷贝构造,即Stack的拷贝构造,而对于内置类型来说,它会按照字节进行拷贝,编译器生成的就足够了,所以Myueue的构造函数不需要我们自己去显示写。

在这里插入图片描述

【总结】

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

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

需要写析构函数的类,都需要写深拷贝的拷贝构造,比如Stack,不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用,比如Date,MyQueue。

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

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

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

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

五、运算符重载

1.概念

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

引入运算符重载,能够让自定义类型能用运算符,比如日期类+天数,日期类比较大小,日期-日期等等,我们进行这些操作只能定义函数去完成,但是呢函数的可读性始终没有±><这些符号的可读性高,就比如一个日期+天数,我们定义的函数可能为AddDay之类的,而且不同的程序员定义的名称也各不相同,我们为了统一,C++便引入了运算符重载。

2.特征分析

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

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

【注意】

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

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

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

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

5.以下5个运算符不能重载 .* :: sizeof ?: . 这个经常在笔试选择题中出现。

我们以日期类比较两个日期是否相等为例:

当我们把运算符重载设置为全局函数时:

class Date
{
    
    
public:

	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}

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

// 全局的operator==
bool operator==(const Date& d1, const Date& d2)
{
    
    
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

int main()
{
    
    
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 2);
	cout << (d1 == d2) << endl;
	return 0;
}

在这里插入图片描述

我们发现,类中的成员_year,_month,-day都是私有的,我们在类外面不能直接访问他们,但是我们又不能直接把成员变量设置为公有的,这样类的封装性就得不到保证,当我们把函数放在类里面呢,比如像下面这样:

class Date
{
    
    
public:

	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}

	bool operator==(const Date& d1, const Date& d2)
	{
    
    
		return d1._year == d2._year
			&& d1._month == d2._month
			&& d1._day == d2._day;
	}

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


int main()
{
    
    
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 2);
	cout << (d1 == d2) << endl;
	return 0;
}

在这里插入图片描述

造成上面的原因是this指针引起的,我们知道,类中的每个成员函数的第一个参数都是一个隐藏的this指针,它指向类的某一个具体的对象,而且this指针不能显示的传递,也不能显示的写出,但可以在函数内部显示的使用。也就是说,我们比较两个日期是否相等需要传递两个参数,但是由于我们把函数放在了类的里面,this指针已经占了第一个参数的位置,所以造成函数的参数过多,为了解决这个问题,我们只需要传递一个参数即可,C++规定,this指针为左操作数,所以我们只需要传递一个右操作数即可。所以正确的代码如下:

class Date
{
    
    
public:

	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}

	bool operator==(const Date& d)
	{
    
    
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

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

在这里插入图片描述

【注意】

1.一个类中的成员函数,不管函数参数有几个,this指针占了第一个参数的位置。

2.对于在类外部无法访问类中私有的成员变量的问题,我们可以在类中提供获取成员变量的共有函数,但是这样破坏了类的封装性,也可以使用友元来解决。

3.运算符重载函数既可以像上面d1d2这样的方式调用,也可以显示调用d1.operator(d2)

六.赋值运算符重载

1.概念

赋值重载函数也是C++的六个默认成员函数之一,它也是运算符重载的一种,它的作用是两个已存在的对象直接的赋值。其特征如下:

1.赋值运算符重载格式

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

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

检测是否自己给自己赋值

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

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

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

4.默认的赋值重载函数对内置类型成员变量是直接赋值的,即以字节为单位进行拷贝,而对自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

2.特征分析–函数格式

赋值运算符重载格式的要求

1.使用引用做参数,并以const修饰

我们知道,使用传值传参时函数形参是实参的一份临时拷贝,所以传值传参会调用拷贝构造函数,而使用引用传参时,形参是实参的别名,从而减少了调用拷贝构造在时间和空间上的消耗,此外,赋值重载只会改变被赋值的对象,而不会改变用于赋值的对象,为了防止书写错误导致用于赋值的对象被改变和没有达到正确赋值的结果,所以我们使用const来防止出现这样的错误。

void operator=(const Date& d)

2.使用引用做返回值且返回值为*this

我们可以对内置类型进行连续赋值,比如int i,j; i = j = 10;那么对于自定义类型来说,我们也可以使用运算符重载来支持连续赋值,那么重载函数必须要有一个返回值,我们在类外部调用重载函数,所以重载函数调用结束后该对象仍然存在,那么我们就可以使用引用返回,这样也减少了一次拷贝,提高了程序运行的效率。我们一般使用左操作数作为函数的返回值,也就是this指针指向的对象。对于刚才我们的举例来说,i = j =10; 把把j = 10的返回值赋值给i,j = 10的返回值为左值的结果。

Date& operator=(const Date& d)

3.检查是否自己给自己赋值

我们在使用赋值重载的时候,可能因为书写错误而写这样的代码:Date d1; Date d2; d1 = d1;这种自己给自己赋值好像并没有什么问题,对于一般变量来说,拷贝是浅拷贝,并没有什么问题,但是对于那种动态开辟空间的变量,则需要进行深拷贝,显然此时需要检查,这个我们下面的特征的第四点会进行详细的讲解

if(this == &d)
{
    
    
    return *this;
}

Date类的赋值重载函数如下:

Date& operator=(const Date& d)
	{
    
    
        //自我赋值
		if (this == &d)
		{
    
    
			return *this;
		}

		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

在这里插入图片描述

3.特性分析-重载为成员函数

赋值运算符只能重载成类的成员函数不能重载成全局函数。这是因为赋值重载函数作为六个默认成员函数之一,如果我们不显示的实现,赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,从而造成链接错误故赋值运算符重载只能是类的成员函数

《C++ prime》中对此特性的说明如下:

在这里插入图片描述

class Date
{
    
    
public:

	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}

};

// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& d1, const Date& d2)
{
    
    
	if (&d1 != &d2)
	{
    
    
		d1._year = d2._year;
		d1._month = d2._month;
		d1._day = d2._day;
	}

	return d1;
}

在这里插入图片描述

4.特性分析–深浅拷贝

赋值重载函数的特性和拷贝构造函数十分相似:默认的赋值重载函数对内置类型成员变量是直接赋值的,即以字节为单位进行拷贝,而对自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值

所以我们对于没有资源申请的类来说,我们不需要直接去写拷贝构造函数,直接使用编译器默认生成的即可,但是对于Stack这种有资源申请的类来说,我们就需要自己显示的去写,进行深拷贝。

Stack:

class Stack
{
    
    
public:
	Stack(int capacity = 4)
	{
    
    
		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
    
    
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
    
    
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
    
    
		// ....
		// 扩容
		_a[_top++] = x;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

当我们不显示去写的时候,就会出现下面的问题:

在这里插入图片描述

在这里插入图片描述

如图,这种情况和Stack默认析构函数的情况十分类似,st1._a和st2._a指向同一块空间,在st1和st2对象销毁的时候编译器会自动调用析构函数,导致str._a被析构了两次,而str1._a 指向的那块空间没有释放,就会造成内存泄漏,所以我们对于类中有资源申请的类我们需要显示定义赋值重载函数,所以Stack类的赋值重载函数如下:

Stack& operator=(const Stack& st)
	{
    
    
		free(_a);
		_a = (int*)malloc(sizeof(int)*st._capacity);
		if (_a == nullptr)
		{
    
    
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int)*st._top);
		_top = st._top;
		_capacity = st._capacity;

	return *this;
	}

在这里插入图片描述

对于我们现在写的赋值重载函数,我们可不可以直接对st1._a的空间直接进行扩容呢,答案是不行了,因为我们不知道st1._a和st2._a哪个空间更大,如果小,我们此时调用realloc就是缩容,而缩容需要重新开辟空间并进行拷贝数据,此时的效率就会很低,而如果我们不缩容空间直接进行拷贝的话就会造成空间的浪费,所以我们选择直接释放原来的空间,重新开辟一块一样大的空间,不用分类哪个空间大。

方法如下图所示:

在这里插入图片描述

但是对于我们上面写的赋值重载函数来说,我们自己给自己赋值的时候就会出现下面的问题:

在这里插入图片描述

我们发现,当我们使用st2自己给自己赋值的时候,st2._a中的数据编程了随机值,这是因为我们写的赋值重载函数会先释放st2._a的空间,然后重新开辟一块空间,此时st2._a中的数据是随机值,再自己给自己赋值,最终使得st2._a中的数据都是随机值,所以我们在写赋值重载函数的时候一定要考虑自己给直接赋值的情况,Stack正确的赋值重载函数如下:

Stack& operator=(const Stack& st)
	{
    
    
		cout << "	Stack& operator=(const Stack& st)" << endl;
		if (this != &st)
		{
    
    
			free(_a);
			_a = (int*)malloc(sizeof(int)*st._capacity);
			if (_a == nullptr)
			{
    
    
				perror("malloc fail");
				exit(-1);
			}
			memcpy(_a, st._a, sizeof(int)*st._top);
			_top = st._top;
			_capacity = st._capacity;
		}

		return *this;
	}

此外,和拷贝构造一样,并不是有资源申请我们就一定要显示下赋值重载函数,这是因为对于自定义类型的成员,编译器回去调它自身的赋值重载函数,就比如我们的Myqueue类:

在这里插入图片描述

【注意】

对于下面的代码:

int main()
{
    
    
	Date d0;
	Date d1;
	Date d2(2022, 10, 8);

	Date d3(d2);  // 拷贝构造(初始化)  一个拷贝初始化另一个马上要创建的对象
	Date d4 = d2; // 拷贝构造

	d0 = d1 = d2;  // 赋值重载(复制拷贝) 已经存在两个对象之间拷贝
	return 0;
}

拷贝构造完成的是初始化工作,在创建对象的时候自动调用,赋值重载完成的是已经存在的对象直接的拷贝,需要手动调用,在上面的代码中,Date d4 = d2;是拷贝构造,尽管是用的"=",但是我们根据定义,这里是在完成初始化的工作,所以是拷贝构造,而不是赋值重载。

【总结】

自动生成的赋值重载函数对成员变量的处理规则和拷贝构造相同,对内置类型按照字节为单位进行拷贝,对于自定义类型会调用它的赋值重载和拷贝构造函数,我们可以总结为:需要写析构函数的类就需要写赋值重载和拷贝构造函数,不需要写析构函数的类就不需要写。

七、const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

在这里插入图片描述

class Date
{
    
    
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
    
    
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
    
    
	Date d1(2023, 1, 1);
	d1.Print();
	const Date d2(2022, 1, 2);
	d2.Print();
	return 0;
}

在这里插入图片描述

我们看到,我们定义了一个只读的d2的Date对象,当我们去调用Print函数的时候编译器就会报错,原因是类成员函数的第一个参数默认的是this指针,而this指针的类型是Date* const this ,而d2的地址&d2的类型是const Date*,这里调用就出现了权限的放大,从而导致编译报错。

【注意】

成员函数默认的第一个参数是Date* const this,这里的const放在*号的后面,修饰的是指针本身,表示指针不能被修改,而this指向的内容可以被修改。

为了解决这个问题,C++允许我们定义const成员函数,即在函数的后面使用const修饰,该const修饰的是函数的第一个参数,即this指针,使得this指针的类型变成为const Date* const this,而函数的其他参数不受影响。

当我们在上面的Print函数后面加上const指针,d2也能够调用Print函数了。

在这里插入图片描述

我们将成员函数的this指针修饰为const Date* const this之后,不仅const Date 的对象可以调用相应的成员函数,非const对象也可以调用,这是因为指针的权限不可以放大,但是权限可以缩小。所以我们在实现一个类的时候,如果我们不需要改变类的成员变量时,即不改变*this,那么我们就应该使用cons来修饰this指针,即使用const成员函数,那么const对象和非const对象都可以调用。

【总结】凡是内部不改变成员变量,即*this对象的数据,这些成员函数都应该加const

对于下面的几个问题:

1.const对象可以调用非const成员函数吗?不可以,权限放大

2.非const对象可以调用const成员函数吗? 可以,权限缩小

3.const成员函数内可以调用其它的非const成员函数吗? 不可以,权限放大

4.非const成员函数内可以调用其它的const成员函数吗? 可以,权限缩小

八、取地址及const取地址操作符重载

取地址重载函数也是C++的六个默认成员,分为const 和非const取地址重载函数,同时它也是运算符重载的一种,它的作用是返回对象的地址

1.const取地址重载

const Date* operator&() const
{
    
    
	return this;
}

返回值加const 的原因是this指针此时类型类const Date* const ,所以需要用const修饰,否则会出现权限的放大

2.非const取地址重载

Date* operator&()
{
    
    
	return this;
}

如果我们没有显示的定义const取地址重载和非const取地址重载,那么编译器会自动生成,所以这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

我们要求这个类的对象不让取地址时可以返回一个空指针nullptr

//const取地址重载
const Date* operator&() const
{
    
    
	return nullptr;
}
//非const取地址重载
Date* operator&()
{
    
    
	return nullptr;
}

九、日期类的实现

1.部分代码实现讲解

1.日期+天数

日期加天数就会考虑到进位的问题,而每个月的天数又不一样,所以我们可以先实现一个GetMonthDay的函数,来获取每个月的天数。日期加天数,先把日期加在天上,然后进行进位调整,月份加1,然后天减去那个月份的天数,使得最终是一个规范的日期。注意月份加到13的时候应该年份加1,月份置为1;

代码实现

// d1 += 100
Date& Date::operator+=(int day) 
{
    
    
	//如果day是负数相当于-=
	if (day < 0)
	{
    
    
		//return *this -= -day;
		return *this -= abs(day);
	}

	_day += day;
	//日期不合法就继续进位
	while (_day > GetMonthDay(_year, _month))
	{
    
    
		_day -= GetMonthDay(_year, _month);
		++_month;
		//月满了进年
		if (_month == 13)
		{
    
    
			++_year;
			_month = 1;
		}
	}

	return *this;
}

2.日期-天数

日期减天数和日期加天数的思路是一样的,先减天数,然后进行日期调整,注意循环的条件是天数小于等于0,且应该先月份减1,再加那个月份的天数,月份减到0的时候,应该年减1,月份置为12。

代码实现

// d1 -= 100
Date& Date::operator-=(int day)
{
    
    
	//如果day是负数相当于+=
	if (day < 0)
	{
    
    
		//return *this += -day;
		return *this += abs(day);
	}

	_day -= day;

	//天数小于0就继续调整
	while (_day <= 0)
	{
    
    
		--_month;
		//月份为0时,把-month值为12,_year--
		if (_month == 0)
		{
    
    
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;

}

3.日期-日期(获取两个日期相隔的天数)

这里我们使用最简单的方式进行计算,我们先获取小的那个日期,然后小的日期一直++,直到等于大的日期,加的次数就是两个日期相隔的天数。我们也可以先计算出两个日期距离当年1月1日的天数,然后年份相减(考虑闰年和平年),再加上两个天数的差值(大的年份的减小的年份的)

代码实现

// d1 - d2
int Date::operator-(const Date& d) const
{
    
    
	Date max = *this;
	Date min = d;
	//用来作为标记,*this < d 的时候应该返回负数
	int flag = 1;

	if(*this < d)
	{
    
    
		max = d;
		min = *this;
		flag = -1;
	}

	int count = 0;  //用来记录天数
	while (min != max)
	{
    
    
		++min;
		count++;
	}

	return count * flag;
}

4.流插入

流插入我们调用的时候是cout<<d1这样的,而在类中成员函数的第一个参数被this指针占据,所以如果写成成员函数,我们在使用的时候应该写成d1<<cout这样就特别别扭,所以我们考虑写在类的外面,但是我们这样我们在类的外面又无法访问私有的成员,我们把私有写成共有又不太好,或者在类中提供获取成员的共有函数,这样都破坏了封装性,所以C++提供了友元函数,可以在类的外面访问类的私有成员,只需要在类的任何地方声明友元函数即可(以friend作为友元的关键字),同时我们如果声明和定义分离,那么我们在.h实现时候就会出现链接错误,因为在两个.cpp都会展开,我们有两种解决方式,一种是用static修饰,用static修饰之后就只能当前文件可见,另一种是+inline,写成内联函数,这样函数会直接在调用的地方展开,不会进符号表,也解决了这个问题。我们这里使用内联函数。cout是ostream类的对象。我们可能出现连续调用的情况,所以需要返回值(左操作数)

代码实现

// operator<<(cout, d1) cout<<d1
// ostream& operator<<(ostream& out, const Date& d);
// 流插入
inline ostream& operator<<(ostream& out, const Date& d)
{
    
    
	out << d._year << "-" << d._month << "-" << d._day << endl;
	//连续打印,返回out;
	return out;
}

5.流提取

流提取和流插入十分相似,我们就不再赘述,这里不同的是流提取可能改变输入的值,所以参数Date& d 不能加const修饰,cin是istream的对象,同时可能连续输入,所以也需要返回值(左操作数)

代码实现

// cin >> d1  operator(cin, d1)
// 流提取

inline istream& operator >>(istream& in, Date& d)
{
    
    
	in >> d._year >> d._month >> d._day;
	//连续输入
	return in;
}

2.完整代码

1.Date.h

#pragma once 
#include <iostream>
using namespace std;

class Date
{
    
    
private:
	int _year;
	int _month;
	int _day;

public:
	//友元声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator >>(istream& in, Date& d);
	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;

		//检查日期是否合法
		if (!(year >= 1
			&& (month >= 1 && month <= 12)
			&& (day >= 1 && day <= GetMonthDay(year, month))))
		{
    
    
			cout << "非法日期" << endl;
		}
	}

	int GetMonthDay(int year, int month)
	{
    
    
		static int monthDayArray[13] = {
    
     0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		{
    
    
			return 29;
		}
		else
		{
    
    
			return monthDayArray[month];
		}
	}

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

	// d1==d2
	bool operator == (const Date & d) const;
	// d1>d2
	bool operator>(const Date& d) const;
	// d1 >= d2
	bool operator>=(const Date& d) const;
	// d1 <= d2
	bool operator<=(const Date& d) const;
	// d1 < d2
	bool operator<(const Date& d) const;
	// d1 != d2
	bool operator!=(const Date& d) const;

	// d1 += 100
	Date& operator+=(int day);

	// d1 + 100
	Date operator+(int day) const;

	// d1 -= 100
	Date& operator-=(int day);

	// d1 - 100
	Date operator-(int day) const;

	// 前置
	Date& operator++();

	// 后置
	Date operator++(int);

	// 前置
	Date& operator--();

	// 后置
	Date operator--(int);

	// d1 - d2
	int operator-(const Date& d) const;

	// 要求这个类的对象不让取地址
	Date* operator&()
	{
    
    
		return nullptr;
		//return this;
	}

	const Date* operator&() const
	{
    
    
		return nullptr;
		//return this;
	}
};


// operator<<(cout, d1) cout<<d1
// ostream& operator<<(ostream& out, const Date& d);
// 流插入
inline ostream& operator<<(ostream& out, const Date& d)
{
    
    
	out << d._year << "-" << d._month << "-" << d._day << endl;
	//连续打印,返回out;
	return out;
}

// cin >> d1  operator(cin, d1)
// 流提取

inline istream& operator >>(istream& in, Date& d)
{
    
    
	in >> d._year >> d._month >> d._day;
	//连续输入
	return in;
}

2.Date.cpp

#include "Date.h"

// d1 == d2
bool Date::operator == (const Date& d) const
{
    
    
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

// d1 > d2
bool Date::operator>(const Date& d) const
{
    
    
	if (_year > d._year)
	{
    
    
		return true;
	}
	else if (_year == d._year && _month > d._month)
	{
    
    
		return true;
	}
	else if (_year == d._year && _month == d._month && _day > d._day)
	{
    
    
		return true;
	}

	//其他情况返回false
	return false;
}

// d1 >= d2
bool Date::operator>=(const Date& d) const
{
    
    
	return *this > d || *this == d;
}

// d1 <= d2
bool Date::operator<=(const Date& d) const
{
    
    
	return !(*this > d);
}

// d1 < d2
bool Date::operator<(const Date& d) const
{
    
    
	return !(*this >= d);
}

// d1 != d2
bool Date::operator!=(const Date& d) const
{
    
    
	return !(*this == d);
}

// d1 += 100
Date& Date::operator+=(int day) 
{
    
    
	//如果day是负数相当于-=
	if (day < 0)
	{
    
    
		//return *this -= -day;
		return *this -= abs(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) const
{
    
    
	Date ret(*this); //拷贝构造
	ret += day;
	return ret;  //临时变量出了作用域就不存在了,所以用引用返回
}

// d1 -= 100
Date& Date::operator-=(int day)
{
    
    
	//如果day是负数相当于+=
	if (day < 0)
	{
    
    
		//return *this += -day;
		return *this += abs(day);
	}

	_day -= day;

	//天数小于0就继续调整
	while (_day <= 0)
	{
    
    
		--_month;
		//月份为0时,把-month值为12,_year--
		if (_month == 0)
		{
    
    
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;

}

// d1 - 100
Date Date::operator-(int day) const
{
    
    
	Date ret(*this);  //拷贝构造 可以写成  Date ret = *this
	ret -= day;
	return ret;  //临时变量出了作用域就不存在了,所以用引用返回
}

// 前置
Date& Date::operator++()
{
    
    
	*this += 1;
	return *this;
}

//后置 ++ 多一个int参数主要是为了和跟前置区分
// 后置
Date Date::operator++(int)
{
    
    
	Date tmp(*this);
	*this += 1;
	return tmp;  //临时变量出了作用域就不存在了,所以用引用返回
}

// 前置
Date& Date::operator--()
{
    
    
	*this -= 1;
	return *this;
}

// 后置 -- 多一个int参数主要是为了跟前置区分
Date Date::operator--(int)
{
    
    
	Date tmp = *this;
	*this -= 1;
	return tmp;  //临时变量出了作用域就不存在了,所以用引用返回
}

// d1 - d2
int Date::operator-(const Date& d) const
{
    
    
	Date max = *this;
	Date min = d;
	//用来作为标记,*this < d 的时候应该返回负数
	int flag = 1;

	if(*this < d)
	{
    
    
		max = d;
		min = *this;
		flag = -1;
	}

	int count = 0;  //用来记录天数
	while (min != max)
	{
    
    
		++min;
		count++;
	}

	return count * flag;
}

3.Test.cpp

#include "Date.h"

void TestDate1()
{
    
    
	Date d1(2023, 1, 1);
	Date d3(d1);
	Date d4(d1);

	d1 -= 1000;
	d1.Print();

	Date d2(d1);
	(d2 - 10000).Print();
	d2.Print();

	d3 -= -10000;
	d3.Print();

	d4 += -10000;
	d4.Print();
}

void TestDate2()
{
    
    
	Date d1(2023, 1, 1);
	Date d2(d1);
	Date d3(d1);
	Date d4(d1);

	(++d1).Print(); // d1.operator++()
	d1.Print();

	(d2++).Print(); // d2.operator++(1)
	d2.Print();


	(--d1).Print(); // d1.operator--()
	d1.Print();

	(d2--).Print(); // d2.operator--(1)
	d2.Print();
}
void TestDate3()
{
    
    
	Date d1, d2;
	//cin >> d1;  // 流提取
	//cout << d1;   // 流插入

	//d1 << cout; // d1.operator<<(cout);

	cin >> d1 >> d2;
	cout << d1 << d2 << endl; // operator<<(cout, d1);
	cout << d1 - d2 << endl;
}

int main()
{
    
    
	TestDate1();
	//TestDate2();
	//TestDate3();
	return 0;
}

十、总结

我们需要掌握前四个默认成员函数(构造函数,析构函数,拷贝构造,赋值重载)的特点,后两个成员函数(取地址重载,const取地址重载)通常不要我们显示的去写,使用编译器自动生成的即可。

1.构造函数

(1)构造函数完成对象的初始化工作,由编译器在实例化对象时自动调用;

(2)默认构造函数是指不需要传递参数就可以调用的构造函数,包括编译器自动生成的,显示定义不带参数的,显示定义全缺省的;

(3)如果用户显示定义了构造函数,那么编译器会根据显示的构造函数对对象进行初始化,如果没有显示定义构造函数,那么编译器会调用编译器默认生成的构造函数;

(4)默认构造函数对内置类型不做处理,对自定义类型会调用其自身的默认构造函数;

(5)为了弥补构造函数对内置类型不处理的缺陷,C++11打了一个补丁–允许在成员函数声明的时候给缺省值,如果构造函数没有对成员变量进行初始化,那么编译器就会使用初始值进行初始化。

2.析构函数

(1)析构函数完成对象中资源清理的工作,由编译器在对象销毁的时候自动调用;

(2)如果用户显示定义了析构函数,那么编译器会根据显示的析构函数对对象进行析构,如果用户没有显示的定义析构函数,那么编译器会调用编译器默认生成的析构函数去完成析构;

(3)默认生成的析构函数对内置类型不做处理,对自定义类型会去调用其自身的析构函数;

(4)不是类中有资源的申请,就一定要写析构函数,比如上文案例中的Myqueue类,它的成员就是自定义类型,就不需要显示的去写析构函数,总之,我们要面向需求,编译器默认的析构函数够用,我们就不需要自己去显示的写析构函数,反之,我们就需要自己写。

3.拷贝构造

(1)拷贝构造是用一个已经存在的对象去初始化另一个对象,由编译器在实例化对象的时候自动调用;

(2)拷贝构造的参数必须为引用类型,否则编译器会报错–值传递会调用拷贝构造,拷贝构造又需要传参,则又要调用拷贝构造…这样就会引发无穷的递归;

(3)如果用户显示的定义了拷贝构造函数,编译器会根据显示的拷贝构造函数去进行拷贝,如果用户没有显示定义,那么编译器会调用编译器默认生成的拷贝构造去完成;

(4)默认生成的拷贝构造函数对于内置类型以字节为单位完成值拷贝(浅拷贝),对于自定义类型会去调用其自身的拷贝构造函数;

(5)当类中有动态开辟的空间时,直接进行值拷贝就会让两个指针指向同一块空间,所以在对象被销毁调用析构函数的时候会对一块空间进行析构两次,引发报错,所以我们在这种情况在,我们需要自己显示定义拷贝构造函数来完成深拷贝。

4.运算符重载

(1)运算符重载是C++为了增强代码的可读性而引入的语法,它只能对自定义类型使用,不能对内置类型使用,其函数名为operator关键字+相关运算符,函数原型:返回值类型 operator操作符(参数列表);

(2)由于运算符重载函数通常都要访问类中的成员变量,所以我们一般将其定义为类的成员函数,同时,因为类的成员函数为隐藏的this指针,所以我们要少传递一个参数;

(3)同一运算符的重载函数直接也可以构成函数重载,比如 opertor++() 与 operator++(int),后者函数调用的时候并不会接收int参数,只是为了构成函数重载,让编译器便于区分,且只能为int,不能为char,float之类,这是C++的规定。

5.赋值重载

(1)赋值重载函数是将一个已经存在的对象中的数据赋值给另一个已经存在的对象,需要自己显示调用,它也属于运算符重载的一种;

(2)如果用户显示定义了赋值重载函数,那么编译器会根据赋值重载函数的方式进行赋值,如果用户没有显示的定义,满编译器就会调用编译器默认生成的赋值重载函数进行赋值;

(3)默认生成的赋值重载函数对于内置类型会以字节为单位进行值拷贝(浅拷贝),对于自定义类型会去调用自定义类型的赋值重载函数

(4)赋值重载函数和拷贝构造函数一样,也存在着深浅拷贝的问题,且与其拷贝构造函数不同的地方在于它还有可能造成内存泄漏,所以当类中有动态申请时我们需要自己显示定义赋值重载函数来释放原空间和完成深拷贝;

(5)为了提高程序的效率和保护对象,通常使用引用做参数,并以const修饰,同时为了满足连续赋值,通常使用引用做返回值,且一般返回做操作数,即*this;

(6)赋值重载函数必须定义为类的成员函数,否则编译器默认生成的赋值重载函数会与类外定义的赋值重载冲突。

6.const成员函数

(1)由于指针和引用传递参数的时候在权限的扩大和缩小,缩小和平移的问题,所以const类型的对象不能调用成员函数,因为成员函数的this指针默认是非const的,二者之间传参存在权限放大的问题;

(2)我们为了提高程序运行的效率以及保护对象,一般都会将成员函数的第二个参数使用const修饰,这就导致了该对象该对象在成员函数内也不能调用其他成员函数,为了解决这个问题,C++设计出了const成员函数–在函数最后面添加const只修饰this指针,不修饰函数的其他参数;所以我们在设计一个类时,只有成员函数不改变第一个对象,我们建议最后都用const修饰。

7.取地址重载和const取地址重载

据显示的析构函数对对象进行析构,如果用户没有显示的定义析构函数,那么编译器会调用编译器默认生成的析构函数去完成析构;

(3)默认生成的析构函数对内置类型不做处理,对自定义类型会去调用其自身的析构函数;

(4)不是类中有资源的申请,就一定要写析构函数,比如上文案例中的Myqueue类,它的成员就是自定义类型,就不需要显示的去写析构函数,总之,我们要面向需求,编译器默认的析构函数够用,我们就不需要自己去显示的写析构函数,反之,我们就需要自己写。

猜你喜欢

转载自blog.csdn.net/qq_67582098/article/details/128656315