<C++>类和对象-中

<C++>类和对象-中_第1张图片


目录

前言 

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

二、构造函数

2.1 概念

2.2 特性

 三、析构函数

1. 概念

2. 特性

 四、拷贝构造函数

1. 概念

2. 特征

五、赋值运算符重载 

1. 运算符重载

2. 赋值运算符重载

六、实现一个完整的日期类

Date.h

Date.cpp 

总结


前言 

上一节,我们学习了类和对象的上部分内容,难度一般,这一节我们要学习类和对象重点部分,理解难度大,望大家认真学习,真正进入C++的大门。

[本节目标]

1. 类的6个默认成员函数

2. 构造函数

3. 析构函数

4. 拷贝构造函数

5. 赋值操作符重载

6. 默认拷贝构造与赋值运算符重载的问题

7. const成员函数

8. 取地址及const取地址操作符重载


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

        如果一个类中什么成员都没有,简称为空类。那么空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

<C++>类和对象-中_第2张图片

        我们在写栈这一结构时,对栈的初始化和销毁经常忘记,初始化忘记会导致程序直接错误,忘记销毁会导致内存泄露,虽然刷题时网站不会提醒你内存泄露,但是如果在一个服务器上持续不断的跑,时间一长就会崩溃,这是极其严重的错误,所以,“祖师爷”做出了构造函数和析构函数,分别完成初始化工作和清理工作,替代了人工操作。

<C++>类和对象-中_第3张图片

        默认成员函数是:我们不写,那么编译器会自动生成 

二、构造函数

2.1 概念

        构造函数是一个特殊的成员函数名字与类名相同, 创建类类型对象时由编译器自动调用没有返回值,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次

2.2 特性

        构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。例如,栈的定义需要初始化函数Init,构造函数替代了我们每次建立栈时的初始化操作。

其特征如下:

s1. 函数名与类名相同

s2. 无返回值。

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

s4. 构造函数可以重载,因为构造函数可以有多种参数

typedef int DataType;
class Stack
{
public:

	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	/*void Init()
	{
		_array = (DataType*)malloc(sizeof(DataType) * 4);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = 4;
		_size = 0;
	}*/

	void Push(DataType data)
	{
		CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}

	DataType Top() { return _array[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }

	void Destroy()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}


private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
			if (temp == NULL)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

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

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明
	// 这里给的是默认的缺省值,给编译器生成默认构造函数用
	int _year;
	int _month;
	int _day;

	// 自定义类型
	//Stack _st;
};

// 1、一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的。
// 2、全部都是自定义类型成员,可以考虑让编译器自己生成

int main()
{
	// 构造函数的调用跟普通函数也不一样
	Date d1;
	d1.Print();


	return 0;
}

        调用 printf函数,对打印出三个随机值,这是为什么呢?编译器不是自动生成了构造函数进行初始化对象操作了吗?为什么不默认初始化为0

        s7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用??

C++中分为两种类型:

  1. 内置类型(基本类型),语言本身定义的基础类型,如int、char、double等等任何指针类型都是内置类型。
  2. 自定义类型,用struct或class等定义的类型。

我们不写构造函数,那么编译器会默认自动生成构造函数,

  1. 若成员变量是内置类型,则不会对其做处理。
  2. 若是自定义类型,则会调用它的默认构造(该类内部的构造函数,就像用两个栈实现一个队列,栈定义在一个Stack类中,队列中定义两个自定义类型stack1,stack2,如果我们在队列中不写构造函数,那么编译器会自动处理自定义类型 ,调用Stack的构造函数对stack1,stack2进行初始化)C++规定必须处理自定义类型。

        这就是C++的规定,但这里我们认为有些许不合理,我们会更期望内置类型默认赋值为0,而不是随机值(注意:有些编译器会对内置类型做处理,但该行为是个性化行为,不是所有编译器都会处理,编译器版本不同处理也会不同) 

        所以,在C++11的标准发布的时候,对这里打了补丁:在成员声明的时候可以给缺省值,即在没有初始值时,用缺省值

class Date
{
public:

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

private:

	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明
	// 这里给的是默认的缺省值,给编译器生成默认构造函数用
	int _year = 1;
	int _month = 1;
	int _day = 1;

	// 自定义类型
	//Stack _st;
};

      也可在实例化时给初始值

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:
	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明
	// 这里给的是默认的缺省值,给编译器生成默认构造函数用

	int _year = 1;
	int _month = 1;
	int _day = 1;

	// 自定义类型
	//Stack _st;
};

int main()
{

	Date d1;
    //构造函数的调用跟普通函数也不一样
	//Date d1();
    //给出初始值
	Date d2(2023, 11, 11);

	return 0;
}

        这里我们可能又有疑惑了, 为什么调用构造函数要这么写?为什么不写成 d1.Date(2023, 11, 11) 呢?这样不是更明了吗?如果不传参,能写为 Date d1();吗?

  1. 我们不能写为Date d1()的形式因为该形式会与函数声明的形式冲突,d1 是函数名,形参列表无参,返回值是Date,这样编译器就区分不开了。若我们不想对构造函数传参,就直接实例化即可,不能带括号若要对构造函数传参,那么这时才需要括号进行传参。
  2. 如果我们改用d1.Date(2023, 11, 11)的形式,那么如果Date名字改为Init,我们是不是很熟悉呢?d1.Init(2023, 11, 11) 这不正是在没有构造函数之前,我们每次都要初始化的操作吗?这不就回到起点了吗?我们的初衷不就是免去我们每次实例化对象时的初始化操作吗?,因为我们可能一时疏忽而忘记初始化操作。所以,如果这样写,我们还要构造函数干什么呢?这就是为什么构造函数的调用形式跟我们的惯性思维冲突。

s6. 无参的构造函数全缺省的构造函数称为默认构造函数,并且默认构造函数只能有一个(全缺省和无参只能存在一个,不然会引起冲突,编译器判断不了调用哪一个构造函数)。注意:无参构造函数、全缺省构造函数以及我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

无参实例化时,编译器会调用默认构造函数,就是上述的三种构造函数。如果没有默认的构造函数,那么我们实例化时就必须要传参。

结论:

        1. 如果有内置类型,并且初始值我们不确定,那么就要自己写构造函数不要放手给编译器,因为编译会虽然会自动生成构造函数,但是他并不会处理内置类型,内置类型的值是随机值,会对程序造成影响。

        2. 内置类型成员都有缺省值且初始化我们的要求就可以不写构造函数如果初始化不是我们想要的,那么还是要手写构造函数,在构造函数内进行传参赋值,若要传指定参数,在实例化的时候传参即可;如果我们不写构造函数,还想要修改成员变量初始值(缺省值也不是自己想要的),唯一途径就是在对象外访问成员变量并进行修改,如果该成员变量是私有的,那么该成员变量就真的就改不了了。所以即使内置类型成员有缺省值,只要不合乎我们的意愿,我们该写构造函数还是要写的,这就是按需求进行操作

//对于TreeNode类,即使_val内置变量给了缺省值,只要不合乎我们的需求,我们都写写构造函数
struct TreeNode
{
	TreeNode* _left;
	TreeNode* _right;
	int _val;

	TreeNode(int val = 0)
	{
		_left = nullptr;
		_right = nullptr;
		_val = val;
	}
};

//对于Tree类中的内置变量root,因为它的初始值只需要置空就可,所以我们只用给缺省值,不用写构造函数
//完全是按需操作
class Tree
{
private:
	TreeNode* _root = nullptr;
};

        3. 如果全部都是自定义类型的构造,且这些类型都定义了默认构造如果带参的情况后面讲,那么可以放手给编译器,让编译器自动进行处理,不用自己写构造函数。

 三、析构函数

        前面通过构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

1. 概念

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

        析构函数,它释放的是动态申请的资源空间(一般都是堆上的空间,栈上的空间出了作用域自动收回),不是变量。构造函数也只是初始化操作,类的实例化才在栈上开辟空间,出了作用域栈上空间自动被收回,堆上的空间需要手动释放,这就是析构函数要做的事情。

   

2. 特性

析构函数是特殊的成员函数,其特征如下:

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

s2. 无参数无返回值,所以不可重载,因为销毁这一操作肯定不传参数

s3. 一个类有且只有一个析构函数若未显式定义,系统会自动生成默认的析构函数

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

typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	/*void Init()
	{
		_array = (DataType*)malloc(sizeof(DataType) * 4);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = 4;
		_size = 0;
	}*/

	void Push(DataType data)
	{
		CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}

	DataType Top() { return _array[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }

	//void Destroy()
	//{
	//	if (_array)
	//	{
	//		free(_array);
	//		_array = NULL;
	//		_capacity = 0;
	//		_size = 0;
	//	}
	//}

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

private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
			if (temp == NULL)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

 <C++>类和对象-中_第4张图片

s5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。

        对内置类型不做处理,对自定义类型去调用他们的默认析构对于静态的数组,不需要释放,因为它的栈上,出了作用域自动释放,只有malloc、calloc的堆上的,需要手动释放。

class String
{
public:
	String(const char* str = "jack")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};
class Person
{
private:
	String _name;
	int _age;
};
int main()
{
	Person p;
	return 0;
}

总结: 

  1.  一般情况下,有动态申请资源就需要显示写析构函数释放资源(例如栈)
  2. 如果没有动态申请的资源,就不需要写析构(例如日期类,它就没有动态开辟空间)
  3. 需要释放资源的成员都是自定义类型,就不需要写析构

<C++>类和对象-中_第5张图片

  

 四、拷贝构造函数

1. 概念

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

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

2. 特征

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

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

2. 拷贝构造函数的参数只有一个且必须使用引用传参若使用传值的方式,会引发无穷递归调用

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

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

int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}
C++规定:
  1. 对于内置类型直接拷贝
  2. 对于自定义类型,如果要拷贝自定义类型,就必须要调用其拷贝构造函数去完成
小知识:
  1. 在类里变量的使用不受限定符的限制,
  2. 在下面举例的拷贝构造里,_year 和成员变量_year不一样,拷贝构造里的_year是this->_year,成员变量的_year是图纸_year 
  3. 任何类型的指针都是内置类型
举例:
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	 //d2(d1)
	Date(Date& d)
	{
		cout << "Date(Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;

        //_year 和成员变量_year不一样,第一个是this->_year,第二个是图纸的_year 
	}
private:
	int _year;
	int _month;
	int _day;
};


void func(Date d)
{

}

void func(int i)
{

}


int main()
{
	//Stack st1;
	//Date d1;
	//MyQueue q;

	Date d1(2023, 4, 25);
	//Date d2(d1);

	func(d1);
	func(10);

	return 0;
}

        对于func(10)来说,由于10是内置类型int,所以直接按字节数拷贝。

        对于func(d1)来说,由于d1是自定义类型,实参传递给形参是一个拷贝过程,所以这里就需要调拷贝构造函数,所以在进入func之前,会先进入拷贝构造函数。

        所以,对于特征2里所说的 若使用传值的方式,会引发无穷递归调用就有了解释 首先它是自定义类型的传值调用,那么就要调用拷贝构造,想调用拷贝构造函数,先传参,又因为传参就是一个实参拷贝到形参的过程,所以传参还要调用拷贝构造,想调用拷贝构造函数,先传参,传参就要调用拷贝构造.......无限递归幸运的是,编译器会强制检查,这就避免了无限递归的发生
<C++>类和对象-中_第6张图片

 解决方案:

  • 指针,形参用指针接收,那么实参要传类的地址,是一个内置类型,所以直接拷贝即可,但是形式上没用引用方便。
  • 引用,实参传自定义类型,形参用引用接收,形参是实参的一个别名,不发生拷贝过程。另外,在拷贝构造中如果不注意,很可能d2和d1赋值关系写反,从而导致将随机值赋值给有值的变量,所以使用const修饰是不错的选择 Date(const Dte& d),如果需要改值,那就不要加const,按需操作。我们并没用将引用的权限放大。 
  • 注意:Date d2(d1)是d1赋值给d2

    如果是自定义类型的引用调用,那么就不需要调用拷贝构造,图中这里d就是d1的别名,d2就是this 

3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝 这种拷贝我们叫做浅拷贝 ,或者值拷贝。
系统生成的默认拷贝构造对两种类型都会处理

  1. 内置类型成员完成值拷贝(按字节拷贝 ,类似memcpy),浅拷贝
  2. 自定义类型调用其类的拷贝构造,深拷贝

        拷贝构造是为自定义类型深拷贝而生的,浅拷贝基本就不需要这个函数

        系统生成的拷贝构造函数并不适合所有类型对于开辟的数组进行值拷贝,即浅拷贝,会使两个指针同时指向一个数组,这会引发什么问题呢?问题就是在析构时该空间析构了两次从而导致程序错误,另外,在一个数组内修改值会影响另一个数组。

<C++>类和对象-中_第7张图片

<C++>类和对象-中_第8张图片

        st2先析构,然后st1再次析构一次

        所以,这里只能用深拷贝,对st2的_a开一个自己的空间,并将st1的_a里的的数据拷贝到st2的_a里。

	// Stack st2(st1)
Stack(const Stack& st)
{
	_a = (int*)malloc(sizeof(int) * st._capacity);
	if (nullptr == _a)
	{
		perror("malloc申请空间失败");
		return;
	}

	memcpy(_a, st._a, sizeof(int) * st._top);
	_top = st._top;
	_capacity = st._capacity;
}

最终为:

#include
#include

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

		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = capacity;
		_top = 0;
	}

	// st2(st1)
	Stack(const Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		memcpy(_a, st._a, sizeof(int) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}

private:
	int* _a = nullptr;
	int _top = 0;
	int _capacity;
};

int main()
{

	// 必须自己实现,实现深拷贝
	Stack st1;
	Stack st2(st1);

	return 0;
}

<C++>类和对象-中_第9张图片

解释:st1先调用构造函数,st2再调用拷贝构造函数而不会去调用构造函数,然后st2再调用析构函数销毁st2._a,最后st1再调用析构函数销毁st1._a 

浅拷贝问题:

1、析构两次,报错

2、在一个数组内修改值会影响另一个数组

        所以,拷贝构造函数的出现是为了避免值拷贝使两个指针指向同一块空间,使该空间释放两次而发生错误。它是为了自定义类型的深拷贝而生。

 
我们来看这里的引用:
Stack func()
{
	Stack st;
	return st;
}

// 错误的
//Stack& func()
//{
//	Stack st;
//	return st;
//}

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

	Stack st1;
	func(st1);

	func();

	Stack ret = func();

	return 0;
}

        如果func的返回值是引用,那么会出现一个问题,return后st会自动调用析构函数,_a置空,这时候就和之前在引用那里讲的 “有可能正确” 不一样了,因为这里有析构函数,直接释放了st的_a,即使栈帧没有销毁数据还有保留,_a也访问不到了,这时候返回引用啥用也没,反而会使新的对象的_a成为野指针,所以我们不能返回局部对象的引用,只能返回值,返回值就会有临时拷贝。

五、赋值运算符重载 

1. 运算符重载

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

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

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

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@,C++内并没有@该符号
  • 重载操作符函数参数列表内必须有一个类类型自定义类型或者枚举类型的操作数
//例如该形参就不可以
bool operator<(const int& x1, const int& x2)
{
}
  • 用于内置类型的操作符,其含义不能改变例如:内置的整型+,不能改变其含义
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为默认的形参this
  •  .*     ::     sizeof      ?     :   注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
  • 操作符有几个操作数,重载函数就有几个参数,又因为形参列表里第一个参数默认为形参this指针,所以形参列表显示参数个数的比操作数少一个参数,例如 < 的操作数是2,那么形参显示的只有一个参数  bool operator(const Date&x)
  • 操作符重载也可进行函数重载

举例: 如何比较一个日期类的大小?

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 Less(const Date& x1, const Date& x2)
{
	if (x1._year < x2._year)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month < x2._month)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
	{
		return true;
	}

	return false;
}

int main()
{
	Date d1(2025, 4, 25);
	Date d2(2023, 5, 25);
	cout << Less(d1, d2) << endl;

	return 0;
}

        对于内置类型编译器知道该怎么比较大小

        但对于自定义类型编译器不知道如何比较大小,所以我们引入关键字operator

bool Less(const Date& x1, const Date& x2)
{
	if (x1._year < x2._year)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month < x2._month)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
	{
		return true;
	}

	return false;
}

bool operator<(const Date& x1, const Date& x2)
{
	if (x1._year < x2._year)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month < x2._month)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
	{
		return true;
	}

	return false;
}

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

	return false;
}

int main()
{
	Date d1(2025, 4, 25);
	Date d2(2023, 5, 25);

	/*cout << Func1(d1, d2) << endl;
	cout << Func2(d1, d2) << endl;*/

	// 为什么内置类型可以直接比较,自定义类型不可以直接比较
	cout << (d1 < d2) << endl;
	cout << (operator<(d1, d2)) << endl;
//两行代码等价,一般不写operator,因为编译器会将第一行自动转换为第二行

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

	return 0;
}

        d1 < d2 编译器会自动转换 operator<(d1, d2),两者指令相同 。<C++>类和对象-中_第10张图片

        运算符是否要重载要看是否有实际意义。例如日期相减是有意义的,但是日期相加就没有实际意义了。

        在上述的代码中,operator< 是全局的函数操作符有几个操作数参数列表就有几个参数 ,d1 < d2 编译器会自动转换为operator<(d1, d2),但是如果变量都是私有或保护变量,那么就无法在外部访问并使用成员变量,所以我们将operator放到类中,成为成员函数。又成员函数的operator内有隐含的this指针,所以操作符有几个操作数参数列表就有几个参数-1的参数。

        这时我们调用d1d1.operator<(d2)。

        即有全局使用全局,无全局使用成员函数。

<C++>类和对象-中_第11张图片

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

	bool operator<(const Date& x)
	{
		if (_year < x._year)
		{
			return true;
		}
		else if (_year == x._year && _month < x._month)
		{
			return true;
		}
		else if (_year == x._year && _month == x._month && _day < x._day)
		{
			return true;
		}

		return false;
	}

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


int main()
{
	Date d1(2023, 4, 26);
	Date d2(2023, 6, 21);

	//d1 < d2; // 转换成operator<(d1, d2);是在全局的情况
	//operator<(d1, d2);

	cout << (d1 < d2) << endl; // 转换成d1.operator<(d2);在成员函数的情况
	cout<

2. 赋值运算符重载

赋值运算符重载格式:

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

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

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

4. 返回*this

         我们再来看,在C语言中我们知道变量可以连续赋值,例如 i = j = k = 0;  这是因为赋值的返回值是左式的结果,所以可以连续赋值。

        现在我们写一个不是标准的赋值运算符重载进行连续赋值:

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

	void operator=(const Date& x)
	{
		_year = x._year;
		_month = x._month;
		_day = x._day;
	}

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

int main()
{
	Date d1(2023, 4, 26);
	Date d2(2023, 6, 21);

	Date d3, d4;
	d3 = d4 = d1;

	return 0;
}

         程序错误,因为operator=的返回值是void,所以不能连续赋值,问题解决的方案就是返回Date值,将operator=的返回值改为Date,返回被赋值的Date,又因为被赋值的Date被this指针指向,所以要返回Date就要返回解引用的this

	Date operator=(const Date& x)
	{
		_year = x._year;
		_month = x._month;
		_day = x._day;
    
        return *this;
	}

        连续赋值的问题解决了,但是又来了一个问题,返回Date的代价有点大,因为它不是返回的*this,而是拷贝后返回,如果Date的空间很大,那么拷贝Date会使程序的效率降低,代价有点大。我们加一个拷贝构造函数再来看

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

	Date operator=(const Date& x)
	{
		_year = x._year;
		_month = x._month;
		_day = x._day;

        return *this;
	}

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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


int main()
{
	Date d1(2023, 4, 26);
	Date d2(2023, 6, 21);

    d1 = d2;

	Date d3, d4;
	d3 = d4 = d1;

	return 0;
}

         可以看出程序调用了三次拷贝构造,这是因为赋值操作符重载函数返回的是*this值,是自定义类型,我们之前在讲解引用时讲过,函数如果返回的不是引用,就会创建临时变量来保存返回值,这就会发生一次拷贝,所以这里调用了三次拷贝构造。

        这时我们再来思考operator=函数,对于在全局和静态的变量,返回时空间不被销毁,这时候可以用引用返回,不会造成风险。这里,我们返回*this,虽然this出了operator=作用域会自动销毁,但是*this指的是对象,生命周期不在operator=内,我们使用引用返回,返回*this的别名,既不会产生引用造成的风险,又不会产生因拷贝而造成的效率问题。

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

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

	return *this;
}

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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


int main()
{
	Date d1(2023, 4, 26);
	Date d2(2023, 6, 21);

    d1 = d2;

	Date d3, d4;
	d3 = d4 = d1;

	return 0;
}

<C++>类和对象-中_第12张图片

         对于d1 = d1;自己给自己赋值,没有意义,所以我们可以在operator=内检查,我们在这里比较地址就可以高效比较,&d是取d的地址。

Date& operator=(const Date& d)
{
	if(this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	return *this;
}

5. 一个类如果没有显式定义赋值运算符重载,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。 内置类型成员变量直接赋值,自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

默认生成赋值重载函数与拷贝构造函数行为一样:

        1、内置类型成员——值拷贝(浅拷贝)

        2、自定义类型成员去调用它的赋值重载

例如Date可以不写、MyQueue自动调用它的赋值重载

6. 赋值运算符只能重载为类的成员函数,不能重载为全局函数。 

        因为赋值运算符重载函数是默认成员函数,如果把它写到全局里,那么编译器又会在类内默认生成一个,会与全局的运算符重载函数起冲突。

        默认的成员函数都是同样道理,不能写在全局。运算符重载可以写在全局,因为它是自己实现的,但是可能会受成员函数是否公有的限制,一般是写在类里的。而赋值运算符重载就不能写在全局,因为它是默认成员函数。

六、实现一个完整的日期类

        为了标准些,我们将声明和定义分离

Date.h

        在这里我们实现运算符重载时,可以先实现 operator< 和 operator== 这两个函数,因为其他的>、>=、!=、<=都可以从这两种函数转换出来,提高复用性。我们可以将该方法推广到大多数类的运算符重载书写顺序。

        类内部写声明,定义在cpp文件。注意:定义时要使用类名和作用域限定符,以澄清该函数是类内部的成员函数的定义。

        由于Date类内部的成员变量都是内置类型,所以析构函数(变量在栈帧内不需要手动销毁)、拷贝构造函数(内置类型逐字节拷贝)、赋值操作符重载函数(内置类型逐字拷贝)都可以不用写,让编译器自动生成

#pragma once
#include
#include
using namespace std;

class Date
{
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);

	//拷贝构造不需要写,因为内置类型逐字节拷贝

	//赋值运算符重载不需要写,因为内置类型逐字节拷贝

	//析构函数不需要写,因为内置类型空间自动释放

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

	//复用
	bool operator==(const Date& x)const;
	bool operator<(const Date& x) const;
	bool operator<=(const Date& x)const;
	bool operator>(const Date& x)const;
	bool operator>=(const Date& x)const;
	bool operator!=(const Date& x)const;

	static int GetMonthDay(int year, int month);
	//类 + 多少天
	Date& operator+=(int day);
	Date operator+(int day)const;

	Date& operator-=(int day);
	Date operator-(int day) const;
	//两个类相减
	int operator-(const Date& x) const;

	//前置++
	Date& operator++();
	//后置++,为了与前置++做区分,进行重载,所以增加了int这个参数,它仅仅是为了占位
	//d1.operator++(0),相当于牺牲了后置++的方便性
	Date operator++(int);

	Date& operator--();
	Date operator--(int);


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

ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

Date.cpp 

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"

//类外定义,类内声明
//缺省值一般在声明处给
Date::Date(int year, int month, int day)
{
	if (month > 0 && month < 13 && _day > 0 && _day <= GetMonthDay(_year, _month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		_year = year;
		_month = month;
		_day = day;
	}
}

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

bool Date::operator<(const Date& x)const
{
	if (_year < x._year)
	{
		return true;
	}
	else if (_year == x._year && _month < x._month)
	{
		return true;
	}
	else if (_year == x._year && _month == x._month && _day < x._day)
	{
		return true;
	}
	
	return false;

}

bool Date::operator<=(const Date& x)const
{
	return *this < x || *this == x;
}

bool Date::operator>(const Date& x)const
{
	return !(*this <= x);
}

bool Date::operator>=(const Date& x)const
{
	return !(*this < x);
}

bool Date::operator!=(const Date& x)const
{
	return !(*this == x);
}

int Date::GetMonthDay(int year, int month)
{
	//如果不用static,那么每次都会创建一次数组,效率不高
	static int daysArr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	//先判断是不是特殊的2月,再判断是不是闰年,效率会比先判断闰年要高
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		return 29;
	}
	else
		return daysArr[month];
}

Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	/** this = *this + day;*/
	return *this;
}

//这里不用引用返回,tmp出函数就销毁
Date Date::operator+(int day)const
{
	Date tmp(*this);

	tmp += day;
	/*tmp._day = day;
	while (tmp._day > GetMonthDay(tmp._year, _month))
	{
		tmp._day -= GetMonthDay(tmp._year, tmp._month);
		++tmp._month;
		if (tmp._month == 13)
		{
			++tmp._year;
			tmp._month = 1;
		}
	}*/
	return tmp;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

Date Date::operator-(int day) const
{
	Date tmp = *this;
	tmp -= day;

	return tmp;
}

int Date::operator-(const Date& d)const
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int res = 0;
	while (min != max)
	{
		++min;
		++res;
	}

	return res;
}

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

//后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;

	return tmp;
}

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

//后置--
Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;

	return tmp;
}

//		流插入,全局函数
// 因为如果是成员函数,第一个参数默认是类,而不是cout,所以顺序必须是 d1 << cout才能输出,即为d1.operator<<(out)
// 为了改变,我们只能使用全局函数来改变参数顺序
// void operator<<(ostream& out, const Date& d)
// 但是又因为成员变量被private修饰,为了访问成员变量可以在类内写成员函数获得成员变量,但最方便的是友元函数
//out不加const是因为cout是要改变out的值,加了const就会报错

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

istream& operator>>(istream& in, Date& d)
{
	int year, month, day;
	in >> year >> month >>day;

	if (month > 0 && month < 13 && day > 0 && day <= d.GetMonthDay(year, month))
	{
		d._year = year;
		d._month = month;
		d._day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		assert(false);
	}
	return in;
}

operator+

  1. 返回日期类,日期相加具有实际意义,这儿又要考虑闰年的情况,所以写一个子函数GetMonthDay 来判断每个月的时长
  2. 在GetMonthDay函数定义一个静态数组储存每个月的时间因为该函数会被频繁调用,使用静态数组会提高效率,当然,也可以使用内联函数。
  3. GetMonthDay中如果为闰年且月份为2,那么返回29,注意将月份是否为2放最前判断,由短路功能,可以提高效率,不需要每次判断是否为闰年。
  4. 由于GetMonthDay的返回值为int,所以不需要引用返回,并且返回29时不能使用引用,当然也可以用const int &,但是就显得画蛇添足了。

注意:相加改变了原Date,我们实际实现的是功能是 += ,所以上面的operator+应改为operator+=此外+=有返回值,返回值是引用,目的是为了实现连续+=,那么我们怎么实现operator+函数呢?

Date Date::operator+(int day)
{
    //拷贝一份
	Date tmp(*this);
	tmp._day += day;
	while (tmp._day > GetMonthDay(tmp._year, tmp._month))
	{
		tmp._day -= GetMonthDay(tmp._year, tmp._month);
		++tmp._month;
		if (tmp._month == 13)
		{
			++tmp._year;
			tmp._month = 1;
		}
	}

    //返回的是临时变量,出了函数会销毁,不能返回引用
	return tmp;
	
}

        由于operator+中与operator+=代码重复,所以在operator+中使用operator+=很方便。

        也可以先定义operaotr+,再利用operator+来定义operator+= 

 注意:

  • 用一个已经存在的对象,初始化另一个对象——拷贝构造函数
Date d2(2023, 4, 26);

Date d4 = d2;//等价于Date d4(d2);
  • 已经存在的两个对象之间赋值拷贝 —— 赋值运算符重载函数

operator++

        该函数的关键在于如何区分前置++、后置++,因为++操作数为1形参的参数列表无参,根据函数重载的性质,我们只能通过参数的不同使编译器来区分前置还是后置采取的不同是将其中一个函数的参数列表增加一个int参数,目的不是为了接收具体的值,它起到的作用仅仅是占位,又因为前置++使用频率高,所以我们将该措施实施在后置++中。

        从前置和后置++的代码中,我们可以看到自定义类型前置++的效率更高,因为不创建临时变量,返回值是引用,所以在日常使用中我们常用自定义类型的前置++。

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

// 后置++
// 增加这个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成重载
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	
	return tmp;
}

operator+=

operator-= 

Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	/** this = *this + day;*/
	return *this;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

        在 - = 运算符重载中,如果日期类  - = 负数天,会出现错误,这时应该    +=(-day)

同理,在  += 运算符重载中,如果日期类+=负数天也会出现错误,这时应该  - =(-day)

        在两函数中加上if判断day是否为负数即可。

 operator-

        重载 operator- ,实现两个日期类的相减,直接让小的日期类++,直到两个日期类相等,记录++次数

int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int res = 0;
	while (min != max)
	{
		++min;
		++res;
	}

	return res;
}

operator<<  流插入

        由于每次都要调用Print()函数,并不是很方便,所以我们可以重载流插入运算符<< ,直接cout日期类

流插入--全局函数+友元函数

  1. 因为如果是成员函数,第一个参数默认是类,而不是cout,所以顺序必须是 d1 << cout才能输出,即为d1.operator<<(out),为了改变顺序,我们只能使用全局函数来改变参数顺序
  2. 但是又因为成员变量被private修饰,为了访问成员变量可以在类内写成员函数获得成员变量,但最方便的是友元函数
  3. out不加const是因为cout是要改变out的值,加了const就会报错
  4. 为了实现连续输出,我们将返回值设置为ostream,这样每次输出后返回值都是cout,将继续输出。

//类内的友元函数声明
	friend ostream& operator<<(ostream& out, const Date& d);


ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

 operator>>  流提取

        cin >> d;

        直接使用cin输入类的信息属性,并且判断是否合法

//类内的友元函数声明
friend istream& operator>>(istream& in, Date& d);

istream& operator>>(istream& in, Date& d)
{
	int year, month, day;
	in >> year >> month >>day;

	if (month > 0 && month < 13 && day > 0 && day <= d.GetMonthDay(year, month))
	{
		d._year = year;
		d._month = month;
		d._day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		assert(false);
	}
	return in;
}

七 、const成员

1. const修饰类的成员函数

        将const 修饰的类成员函数称之为 const 成员函数 const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
        
        由于this本身为Date* const this,所以当被const修饰类的成员函数时,const修饰的是*this,即const Date* const this,第一个const保证this指向的内容不被修改,并且防止传参时权限的放大

        如果一个对象被const修饰,那么当它调用成员函数时,由于this指针没有被const修饰,所以传参时const权限放大,出现错误。

        只有指针和引用才涉及权限的放大、缩小、平移问题

        那么怎么样才能让隐含的this被const修饰呢?祖师爷找到了一个位置:在成员函数后加上const,来修饰this,这样就能防止权限的放大,既能使被const修饰的对象调用成员函数,又能保证没有被const修饰的对象调用成员函数。

那么能不能所有成员函数都加上const呢?

        不能!例如,要修改对象成员变量的函数后面就不能加上const

        所以,只要成员函数内部不修改成员变量,都应该加上const,这样const对象和普通对象都可以调用成员函数


总结

类和对象的概念多,关键点多,一时之间不清楚是很正常的,多看多练习可以帮助我们逐渐理解和掌握。

最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

你可能感兴趣的:(c++)