类和对象(中)

发布于:2024-05-25 ⋅ 阅读:(46) ⋅ 点赞:(0)

嘿嘿,家人们,今天咱们来剖析类和对象(中)这部分的知识,好啦,废话不多讲,开干!

目录

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

2:构造函数

2.1:概念

2.2:特性

2.2.1:特征5

2.2.2:特征6

2.2.3:特征7 

2.2.3.1:代码1

2.2.3.2:代码2

3:析构函数

3.1:概念

3.2:特性

3.3:析构顺序

3.3.1:代码1

4.:拷贝构造函数

4.1:概念

4.2:特征

4.2.1:代码1

4.2.2:代码2

4.2.3:代码3(浅拷贝)

4.2.4:代码4

4.2.5:代码5(深拷贝)

4.3:总结

5:运算符重载

5.1:概念

5.1.1:代码1(在全局作用域重载)

5.1.2:代码2(在类域重载)

5.2:注意事项

5.3:赋值运算符重载

5.3.1:赋值运算符重载格式

5.3.1.1:代码1

5.3.1.2:代码2

5.3.2:特性

5.3.2.1:特性1

5.3.2.2:特性2

5.3.3:赋值运算符与拷贝构造的区别

5.4:前置++与后置++重载

​编辑

6:默认生成的函数行为总结

7:const成员

7.1:概念

7.1.1:代码1

7.2:常见笔试题

7.2.1:代码1

7.2.2:代码2

7.2.3:代码3

7.2.3:代码4

7.3:总结

8:取地址以及const取地址操作符重载


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

如果一个类中什么都没有,简称为空类.

那么有的uu就会想,既然是个空类,那么里面应该是什么都没有的,但是空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数.

默认成员函数:用户没有显式实现,编译器会生成的成员函数叫作默认成员函数.

以上就是6个默认成员函数,我们一个一个来看.

2:构造函数

2.1:概念

在讲构造函数的概念以前,我们来看下面这段代码

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

class Date
{
public:
	void Init(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;
	d1.Init(2024, 3, 2);
	d2.Init(2024, 3, 8);
	d1.Print();
	d2.Print();
	return 0;
}

上面则是一个Date类,我们可以在类里面定义一个成员函数Init来给对象设置日期,但如果每次创建对象时都调用该方法设置信息,这样子一点点小麻烦,那么能否在创建对象时,就将信息设置进去呢?其实是可以的,这里就要用到类的6个默认成员函数之一构造函数.
构造函数是一个特殊的成员函数,名字与类名相同,创建类的类型对象时由编译器自动调用,以保证每一个对象都有一个合适的初始值,并且在对象的整个生命周期内只调用一次.

2.2:特性

构造函数是特殊的成员函数,这里有一个小注意点,构造函数虽然名称叫做构造,但是构造函数的主要任务并不是开辟内存空间创建对象,而是初始化对象.

特征如下

1.函数名与类名相同

2.无返回值.

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

4.构造函数可以对其进行函数重载.

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

class Date
{
public:
	//无参构造函数
	Date()
	{
		cout << "Date()" << endl;
	}
	//带参构造函数
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2024, 5, 8);
	d2.Print();
	return 0;
}

上面的代码,d1调用了无参构造函数,d2则调用了带参的构造函数.

这里有一个小注意点

当对象调用无参构造函数时,后面不需要跟(),因为这样子的话无法跟函数声明进行区分.

2.2.1:特征5

C++把类型分成内置类型(基本类型)和自定义类型.内置类型就是语言提供的数据类型,如:int/char/float/double.....,自定义类型就是我们使用class/struct/union等自己定义的类型,然后我们来看下面这段代码.

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

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		cout << _hour <<"/" << _minute << "/"<<_second << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	//无参构造函数
	Date()
	{
		cout << "Date()" << endl;
	}
	//带参构造函数
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time T1;
};
int main()
{
	Date d1(2022, 3, 8);
	d1.Print();
	return 0;
}

在上述代码中,Date类的成员变量中,不仅仅声明了_year,_month,_day,还有一个自定义类型T1,那么我们来观察下这段代码的运行结果.

我们可以清晰地发现,d1不仅仅调用了自己的构造函数,在实例化的时候,还调用了T1的构造函数,这是因为,如果在一个类中声明了另外一个自定义类型,那么在对象实例化的时候,就会调用另一个自定义类型的构造函数.

uu们仔细观察上面的代码话,我们可以清晰地发现,内置类型没有进行初始化,那么C++11 中针对内置类型成员不初始化的缺陷,又打了补丁:即内置类型成员变量在类中声明时可以给默认的缺省值我们来看下面这段代码.

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

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		cout << _hour <<"/" << _minute << "/"<<_second << endl;
	}
private:
	int _hour = 1;
	int _minute = 1;
	int _second;
};

class Date
{
public:
	//无参构造函数
	Date()
	{
		cout << "Date()" << endl;
	}
	//带参构造函数
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year = 2021;
	int _month = 3;
	int _day = 8;
	Time T1;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

现在呢,我在声明成员变量时给了缺省值,对象d1在实例化的时候,会调用自己的无参构造函数,由于没有对其传参,所以默认使用的是缺省值去进行初始化,那么T1也是同理滴.

2.2.2:特征6

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器就不再生成默认构造函数。我们来看下面这段代码.

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
using namespace std;
class Date
{
public:
	//带参构造函数
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year = 2021;
	int _month = 3;
	int _day = 8;
};

int main()
{
	Date d1;
	return 0;
}

 在上面的代码中,博主显式定义了一个带参构造函数,这个时候我实例化了一个d1对象,然后我们运行代码会发现,编译器会报错,因为这个时候我们已经显式定义了一个构造函数,那么编译器就不会再帮我们去生成构造函数了,而实例化d1所需要的构造函数是无参,这个时候就匹配不上了.

2.2.3:特征7 

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

PS:无参构造函数、全缺省构造函数、程序员没写编译器默认生成的构造函数,都可以认为是默认的构造函数(即不需要传参就可以调用的构造函数,称为默认构造函数).我们来通过下面这段代码来理解下.

2.2.3.1:代码1
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
using namespace std;
class Date
{
public:
	Date()
	{
		cout << "Date" << endl;
	}
	void Print()
	{
		cout << _year <<"/" << _month <<"/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

2.2.3.2:代码2
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
using namespace std;
class Date
{
public:
	Date(int year = 2023,int month = 3,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;
	d1.Print();
	return 0;
}

3:析构函数

3.1:概念

通过了解了构造函数后,我们知道了对象是怎么来的,那么一个对象又是怎么没的呢?这个时候就要谈到另一个默认成员函数----->析构函数

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

这就好比,我们去酒店住房间,退房这个工作是由我们自己去前台完成的,但是打扫房间的任务是交给了酒店的工作人员来进行完成的.

3.2:特性

析构函数是特殊的成员函数,有如下特性

  1. 析构函数名是在类名前加上~.
  2. 无参数并且无返回值类型.
  3. 一个类只能有一个析构函数.若未进行显式定义,系统会自动生成默认的析构函数.

PS:析构函数不能重载.

     4.对象生命周期结束时,编译器会自动调用析构函数.

了解了析构函数的基本特性后,我们来看下面这段代码

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 2021,int month = 3,int day = 15)
	{
		cout << "Date()" << endl;
	}

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

	~Date()
	{
		cout << this << endl;
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

在上述代码中,日期类Date则调用了析构函数~Date(),但是刚刚不是说析构函数会对对象资源进行清理吗,但是上述代码中没有清理哎,我们再来看下面这段代码.

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
};

class Date
{
public:
	Date(int year = 2021,int month = 3,int day = 15)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t1;
};

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

我们可以看到上述代码中不仅仅调用了Date类的析构函数,而且还调用了Time类的析构函数,有的uu就会很奇怪,main函数中没有定义Time类的对象,为什么最后会调用Time类的析构函数,

原因:

main方法中创建了Date类的对象d1,而d1中包含四个成员变量, _year,_month,_day,Time _t1;其中_year,_month,_day是内置类型成员,销毁时不需要进行资源清理,最后直接由系统将其内存回收即可;而_t1是Time类的对象,所以在d1销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数.

PS:创建哪个类的对象则调用该类的析构对象,若该类里的成员变量中包含另外一个类的对象,那么销毁该类的对象时会调用该类的析构函数.

那么有的uu就会有这么一个问题,析构函数也是6个默认成员函数之一,与构造函数类似,我们不写的话,编译器会生成一个默认的析构函数,那么在什么情况下我们要去显式定义析构函数呢?我们来看下面这段代码.

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	Stack st;
	st.Push(1);
	st.Push(2);
	
}

这是使用C++语言实现的数据结构栈,若是在C语言阶段,我们开辟空间的话,则需要自己定义函数,自己调用函数,释放空间也是同理,而在C++中,开辟与释放空间则可以使用构造与析构函数,因为在创建对象的时候,会自动调用构造函数,销毁对象的时候,编译器会自动调用析构函数,因此,则可以在显式定义构造函数的时候开辟空间,显式定义析构函数的时候释放空间.

因此,如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,,有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类.

3.3:析构顺序

了解了析构函数后,接下来我们来看下面这段代码,看看析构的顺序.

3.3.1:代码1

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
	int _year;
	int _month;
	int _day;
public:
	//默认构造函数
	Date(int year = 2024, int month = 3, int day = 25)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	//析构函数
	~Date()
	{
		cout << "~Date()" << _year << endl;
	}
};

int main()
{
	Date d1(1);
	Date d2(2);
	static Date d3(3);
	return 0;
}

在C语言阶段,我们知道,函数会在栈区上开辟栈帧,因此在函数内部创建对象也就是在栈区上创建对象,栈区要遵从先进后出的原则,也就是说,先实例化的对象后析构.

上面的代码中创建对象的顺序是 d1-->d2-->d3,原本析构的顺序应该是d3,d2,d1,但是由于d3是由static关键字修饰,static关键字修饰的局部对象放在了内存的静态区,静态区的变量的生命周期和全局变量的生命周期一样。因此d3最后析构.

4.:拷贝构造函数

4.1:概念

在生活中,有些伟大的母亲在生孩子时,生出来两个一模一样的小孩,我们将其称为双胞胎.那么在创建时,能否创建一个与已经存在的对象一模一样的新对象呢?那么这就要讲到拷贝构造函数了.

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

4.2:特征

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

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

了解了拷贝构造的基本概念后,我们来看下面几段代码

4.2.1:代码1

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
	int _year;
	int _month;
	int _day;
public:
	//构造函数
	Date(int year = 1000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	//d2(d1),this为d2,d为d1
	Date(const Date&d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
};
int main()
{
	Date d1(2022,3,2);
	Date d2(d1);
	d1.Print();
	d2.Print();
	return 0;
}

上面这段代码则是使用了d1去拷贝构造d2,既然是拷贝构造,那么d1与d2的内容是一样的.

有的uu可能会比较奇怪,为什么拷贝构造在传参时,要传类型的引用呢?在讲这点之前,首先我们来看下面这段代码

4.2.2:代码2

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
	int _year;
	int _month;
	int _day;
public:
	//构造函数
	Date(int year = 1000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	//d2(d1),this为d2,d为d1
	Date(const Date&d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
};

void Func1(Date d1)
{

}

void Func2(Date&d1)
{

}
int main()
{
	Date d1(2022, 3, 2);
	Func1(d1);
	Func2(d1);
	return 0;
}

这段代码呢,博主将采用调试的方式带着大家来看.

首先让程序走Func1函数这里,按照正常逻辑来讲的话,按F11应该是直接进入了Func1函数的内部去调用Func1函数,那么我们往下走.

我们可以清晰地发现,此时函数并没有直接进入Func1而是调用了拷贝构造函数,这是因为C++规定自定义类型在传值传参时会先调用拷贝构造.

PS:博主这里使用的VS2022是在64位环境下调试下,换到32位环境的话,有可能是看不到这种现象的,可能编译器对其进行了优化.

了解了上面这个的小知识后,我们再回到最初的问题.现在呢,在拷贝构造函数的参数里头不使用引用,那么根据上面小知识:自定义类型传值传参会调用拷贝构造.那么就会发生下图的情况.

若使用传值传参,那么每次在进行传值传参的时候,会形成一个新的拷贝的构造,然后新的拷贝构造再进行传值传参,这样子又会形成一个新的拷贝构造,那么周而复始就会不断地形成拷贝构造,引发无穷递归.

  • 3.若我们没有自己去写拷贝构造函数,编译器会生成默认的拷贝构造函数.默认的拷贝构造函数是浅拷贝也可以说是值拷贝.那么我们来看下面几段代码

4.2.3:代码3(浅拷贝)

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
	//内置类型
	int _year;
	int _month;
	int _day;
public:
	//构造函数
	Date(int year = 1000,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
};

int main()
{
	Date d1(2018, 3, 1);
	Date d2(d1);
	d1.Print();
	d2.Print();
}

在上述代码中,日期类没有显式定义构造函数,那么此时编译器会帮助我们生成一个默认的拷贝构造函数,默认的拷贝构造函数对于内置类型是按照字节方式(一个字节一个字节地去进行拷贝)拷贝的.

4.2.4:代码4

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Time
{
	int _hour;
	int _minute;
	int _second;
public:
	//强制生成默认的构造函数
	Time() = default;
	//拷贝构造是构造函数的重载形式
	Time(const Time& T1)
	{
		this->_hour = T1._hour;
		this->_minute = T1._minute;
		this->_second = T1._second;
		cout << "const Time & T1" << endl;
	}
};
class Date
{
	//内置类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time T0;
public:
	//构造函数
	Date(int year = 1000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
};

int main()
{
	Date d1(2018, 3, 1);
	Date d2(d1);
}

相较于代码3,此时代码中的成员变量多了个自定义类型,然后使用d1去拷贝构造d2,通过观察结果我们能够清晰地发现,此时调用了Time类的的拷贝构造函数,也就是说在默认生成的拷贝构造函数中,自定义类型是调用其拷贝构造函数完成拷贝的.

4.2.5:代码5(深拷贝)

在上面我们有讲到过,编译器会生成默认的拷贝构造函数.默认的拷贝构造函数是浅拷贝也可以说是值拷贝,那么有浅拷贝,同样也存在深拷贝,至于深拷贝,在后面具体细讲,这里首先让uu简单了解一下

#include <iostream> 
using namespace std;

typedef int STDateType;
class Stack
{
	STDateType* _arr;
	STDateType _top;
	STDateType _capacity;
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_arr = (STDateType*)malloc(sizeof(Stack) * capacity);
		if (nullptr == _arr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = -1;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_arr);
		_arr = nullptr;
		_top = -1;
		_capacity = 0;
	}
	void Push(int value)
	{
		_arr[++_top] = value;
	}
};

int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

当我们运行上面这段代码时,我们可以清晰地发现,此时出错了,那么报错的原因为什么呢?我们通过调试观察变量来看

我们可以清晰地观察到,st1与st2里面成员变量_arr的地址是一样的即指向同一块内存空间,而_arr是动态开辟的,那么在调用析构函数的时候,会释放两次,对于动态内存开辟的空间,是不可进行多次释放的,因此这里会发生报错.

4.3:总结

  1. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;但是一旦涉及到了资源申请,那么此时拷贝构造函数一定要写,否则就是浅拷贝.
  2. 为了提高程序效率,一般对象传参时,尽量使用引用类型,因为自定义类型在传值传参时会调用拷贝构造函数,返回时根据实际场景,能使用引用尽量使用引用.

5:运算符重载

5.1:概念

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

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

函数原型:返回值类型 operator操作符(参数1,参数2.......).

了解了赋值运算符的基本概念后,接下来我们来看两段代码.

5.1.1:代码1(在全局作用域重载)

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

class Date
{
public:
	//构造函数
	Date(int year = 2000,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数,d2(d1),this 为d2,d为d1
	Date(const Date & d)
	{
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}
	int _year;
	int _month;
	int _day;
};

bool DateGrater(const Date & d1,const Date &d2)
{
	//先比较年,再比较月,再比较日
	if (d1._year > d2._year)
		return true;
	else if (d1._month > d2._month)
		return true;
	else if (d1._day > d1._day)
		return true;
	//其他情况均返回false
	else
		return false;
}

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


bool operator>(const Date & d1,const Date & d2)
{
	//先比较年,再比较月,再比较日
	if (d1._year > d2._year)
		return true;
	else if (d1._month > d2._month)
		return true;
	else if (d1._day > d1._day)
		return true;
	//其他情况均返回false
	else
		return false;
}

bool operator==(const Date & d1,const Date & d2)
{
    //年月日必须三者都相等,因此使用逻辑与操作符
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d1._day;
}
int main()
{
	Date d1(2020, 1, 2);
	Date d2(2020, 1, 3);

	Date d3(1998, 1, 1);
	Date d4(1998, 1, 1);
	cout << DateGrater(d1,d2) << endl;
	cout << DateEqual(d3, d4) << endl;	
	cout << endl;
	cout << (d1 > d2)  << endl;
	cout << (d3 == d4) << endl;
	return 0;
}

在C语言阶段,我们如果要比较自定义类型的大小的话,那么得自己写一个函数,然后通过调用函数去实现这样的操作,但是这样子会有些麻烦,因为我们在比较大小时通常是比较倾向于使用>,<,==这些操作符的, 所以在C++则是引入了运算符重载的概念,博主上面的代码,则是先比较日期的大小.

5.1.2:代码2(在类域重载)

这里会发现运算符重载成全局的就需要成员变量是公有的,那么就很难保证封装性,当我们按照在全局作用域重载的方式在类域重载时,此时我们编译会发现,它会进行报错,说参数太多,在之前我们有讲到过,非静态的成员函数存在隐藏的this指针,因此此时相当于有了三个参数.

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

class Date
{
public:
	//构造函数
	Date(int year = 2000,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数,d2(d1),this 为d2,d为d1
	Date(const Date & d)
	{
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}


	bool operator>(const Date& d)
	{
		//先比较年,再比较月,再比较日
		if (_year > d._year)
			return true;
		else if (_month > d._month)
			return true;
		else if (_day > d._day)
			return true;
		//其他情况均返回false
		else
			return false;
	}
	//d1(d2);this相当于d1,d为d2
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2020, 1, 2);
	Date d2(2020, 1, 3);
	Date d3(1998, 1, 1);
	Date d4(1998, 1, 1);

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

	cout << endl;
	cout << d3.operator==(d4) << endl;
	cout << (d3 == d4) << endl;
	return 0;
}

 

5.2:注意事项

  1. 不能通过连接其他符号来创建新的操作符(必须是C/C++中语法存在的),比如operator@.
  2. 重载操作符必须有一个自定义参数,不能去重载运算符改变其内置类型的行为(譬如将>重载成==)
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的类型+,不能改变其含义.
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为非静态的成员函数的第一个参数为隐藏的this指针.
  5.   .*  , ::(域作用限定符) , sizeof , ?:(三目操作符)  .(对象成员调用),注意以上5个运算符不能重载.

5.3:赋值运算符重载

5.3.1:赋值运算符重载格式

  • 参数类型:const T &,传引用的目的是提高效率,因为自定义类型在进行传值传参时会调用拷贝构造.
  • 返回值类型:T &,返回引用同样可以提高效率,有返回值的目的是为了支持连续赋值.
  • 检测是否自己给自己赋值
  • 返回 *this:  原因---->要符合连续赋值的含义.

了解了赋值运算符的基本定义后,接下来我们来看几段代码.

5.3.1.1:代码1
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

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

	//d1 = d2,this为d1,d为d2
	void operator=(const Date & d)
	{
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2(2022,3,6);
	cout << "赋值前" << endl;
	d1.Print();
	d2.Print();
	d1 = d2;
	d1.operator=(d2);
	cout << "赋值后" << endl;
	d1.Print();
	d2.Print();
	return 0; 
}

通过观察上面的结果,我们能够清晰地发现,此时的赋值运算符重载就成功实现啦,但是真的有这么简单吗?不知道uu们还记得不,在C语言阶段,我们应该见到过下面这种语句.

上面这种情况则是连续赋值,我们首先回想一下赋值运算符的运算顺序,赋值运算符的顺序是从右向左进行的,那么就会出现下面的情况.

回顾了赋值运算符的连续赋值后,接下来再回到上面那段代码,试试看上面的代码能否实现连续赋值.

当我们使用上面的代码进行连续赋值时,我们能够发现,此时编译器发生了报错,uu们仔细观察的话,博主在重载赋值运算符的时候,将其返回值类型写成了void,而根据之前讲的,赋值运算符如果要实现连续赋值的话,得有一个返回值,这个返回值得是左操作数.左操作数作为返回值,再去对下一个变量进行赋值,那么要修改的话,就得按照下面的代码2的方式.

5.3.1.2:代码2
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

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

	//d1 = d2,this为d1,d为d2
	Date& operator=(const Date & d)
	{
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
		return *this;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2;
	Date d3(2022,3,7);
	cout << "赋值前" << endl;
	d1.Print();
	d2.Print();
	d3.Print();
	d1 = d2 = d3;
	cout << "赋值后" << endl;
	d1.Print();
	d2.Print();
	d3.Print();
	return 0; 
}

5.3.2:特性

5.3.2.1:特性1

赋值运算符重载不能重载成全局函数,uu们可能对这段话不是很明白,我们来看下面这段代码.

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

//赋值运算符不能重载成全局的
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}

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

通过观察我们能够清晰地发现,当赋值运算符重载成全局的时候,此时就要给两个参数了,因为这个时候没有隐藏的this指针了.

原因:赋值运算符若不显式实现,那么编译器会生成一个默认的.此时若我们在类域外面写一个的全局的赋值运算符重载,在编译的时候就会和编译器在类中生成的默认的赋值运算符重载冲突了,因此,赋值运算符重载只能是类的成员函数.

5.3.2.2:特性2

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

PS:默认生成的赋值运算符重载,对于成员变量是直接赋值的,而自定义类型成员变量需要调用类的赋值运算符重载完成赋值.

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

class Time
{
public:
	Time(int hour = 12,int minute = 30,int second = 15)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}
	Time operator=(const Time& 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 = 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;
		_t = d._t;

		return *this;
	}
	//内置类型成员变量是直接赋值的
	int _year;
	int _month;
	int _day;
	//自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
	Time _t;
};

int main()
{
	Date d1;
	Date d2(1998, 1, 1);
	Date d3;
	d1 = d3 = d2;
	return 0;
}

既然 编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了 ,那么我们需要自己实
现吗?当然像日期类这样的类是没必要的,那么我们来看下面这段代码.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;


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

	void Push(const int & value)
	{
		//CheckCapacity
		_arr[++_top] = value;
	}
	~Stack()
	{
		free(_arr);
		_arr = nullptr;
		_top = -1;
	}
private:
	int _top = -1;
	int* _arr;

};

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

当我们运行这段代码时,会发现它报错了,那么是因为什么呢?我们来调试分析下.

我们可以清晰地观察到,s1赋值给s2了以后,s1与s2里面成员变量_arr的地址是一样的即指向同一块内存空间,而_arr是动态开辟的,那么在调用析构函数的时候,会释放两次,对于动态内存开辟的空间,是不可进行多次释放的,因此这里会发生报错.

PS:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到了资源挂管理,那么则必须要手动实现赋值运算符.

5.3.3:赋值运算符与拷贝构造的区别

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
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;
	}

	//赋值运算符重载,d1.operator(d2)--->d1 = d2
	Date& operator=(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(2020,3,8);

	//拷贝构造:同类型一个已经存在的对象去对另外一个要创建的对象的进行初始化
	Date d2(d1);

	Date d3;
	d1.Print();
	d2.Print();
	d3.Print();

	cout << endl;
	//赋值运算符重载:已经存在的对象,拷贝赋值给另外一个
	d3 = d1;

	d1.Print();
	d2.Print();
	d3.Print();
	return 0;
}

5.4:前置++与后置++重载

在C语言阶段,我们有学习到前置++与后置++

前置++:先+1后使用;

后置++的窍门:先使用后+1;

但是二者有个共性则是对于++的变量,是必须要自增的.

那么对于前置++与后置++该如何重载呢,我们来看下面这段代码.

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

class Date
{
public:
	Date(int year = 1900, int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//先+1再使用
	Date& operator++()
	{
		_day += 1;
		return *this;
	}

	//先使用再+1
	Date operator++(int)
	{
		//使用编译器生成的赋值运算符重载,进行拷贝
		Date temp = *this;
		_day += 1;
		return temp;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day <<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2003, 3, 9);
	Date d2 = d1++;
	cout << "后置++" << endl;
	d2.Print();

	cout << endl;

	d1.Print();

	cout << endl;

	Date d3 = ++d1;
	cout << "前置++" << endl;

	d3.Print();

	cout << endl;

	d1.Print();
	return 0;
}

上面的代码呢则是实现了前置++与后置++,但是,光看代码,uu们可能还会有些疑惑,博主将一一帮他们解答.

前置--与后置--也是同理滴,这里博主就不演示啦~

6:默认生成的函数行为总结

7:const成员

7.1:概念

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

了解了其基本概念后,接下来,我们来看几段代码

7.1.1:代码1

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

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

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

};

int main()
{
	Date d1(2022, 3, 8);
	d1.Print();
	return 0;
}

7.2:常见笔试题

了解了const成员后,接下来,我们将通过下面这段代码来看几个问题.

7.2.1:代码1

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//const修饰隐含的this指针,const Date * this
	void Print() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

 

不知道uu们还记得博主在C++入门的时候,讲引用的时候,讲到过权限相关的知识.这里带着大家简单回顾下.

  1. 权限能够平移,缩小,但权限不能够放大.
  2. 权限放大---->指针和引用赋值才存在权限的放大.

 那么上面的Date类中,Print()函数是const成员,那么为什么非const对象能够调用呢,因为它遵循以下的规则

7.2.2:代码2

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//const修饰隐含的this指针,const Date * this
	void Print() 
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

当我们运行这段代码时,会发现它报错了,那么是为什么呢?

7.2.3:代码3

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//const修饰隐含的this指针,const Date * this
	void Print()  const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	void Add() 
	{
		_year += 1;
		_month += 1;
		_day += 1;
		Print();
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d3(2022, 1, 2);
	d3.Add();
	return 0;
}

我们可以清晰地看到,在Add这个非const成员函数,能够调用const成员函数,那么是为什么呢?

7.2.3:代码4

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//const修饰隐含的this指针,const Date * this
	void Print()  const
	{
		Add();
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	void Add() 
	{
		_year += 1;
		_month += 1;
		_day += 1;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d3(2022, 1, 2);
	d3.Add();
	return 0;
}

我们可以清晰地看到,在Print这个const成员函数,不能够调用非const成员函数,那么是为什么呢?

7.3:总结

  1. const对象不能调用非const成员函数.(权限发生了放大)
  2. 非const对象可以调用const成员函数.(权限缩小)
  3. const成员函数不能调用其他的非const成员函数.(权限放大)
  4. 非const成员函数内能够调用其他的const成员函数.(权限缩小)
  5. 成员函数,如果对成员变量只进行读取访问的话---->建议加const修饰----->这样子const对象与非const对象都可以使用.
  6. 成员函数,如果对成员变量要进行读写访问的话---->不能加const----->否则不能修改成员变量.

8:取地址以及const取地址操作符重载

这两个默认成员函数一般不用重新定义, 编译器默认会生成.
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
		//取地址操作符重载
	Date* operator&()
	{
		return this;
	}
	//const取地址操作符重载
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year = 2008; // 年
	int _month = 1; // 月
	int _day = 1; // 日
};

int main()
{
	Date d1;
	const Date d2;
	cout << d1.operator&() << endl;

	cout << d2.operator&() << endl;
	return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如 想让别人获取到指定的内容!
好啦,uu们,关于类和对象(中)这部分滴详细内容博主就讲到这里啦,如果uu们觉得博主讲的不错的话,请动动你们滴小手给博主点点赞,你们滴鼓励将成为博主源源不断滴动力,同时也欢迎大家来指正博主滴错误~

网站公告

今日签到

点亮在社区的每一天
去签到