C++初阶-类和对象(中)

发布于:2025-04-21 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

1.类的默认成员函数

2.构造函数(难度较高)

​编辑

​编辑

​编辑

3.析构函数

4.拷贝构造函数

5.赋值运算符重载

5.1运算符重载

5.2赋值运算符重载

6.取地址运算符重载

6.1const成员函数

6.2取地址运算符重载

7.总结



1.类的默认成员函数

类的默认成员函数即用户没有显式实现,编译器就会自动生成的成员函数。一个类我们不写的情况下会默认生成以下6个默认成员函数:

最重要的是前面四个函数,后面两个分别为移动构造和移动赋值,以后会讲。

默认成员函数很重要,也比较复杂。我们要从两个方面去学习:

(1)我们不写时,编译器默认生成的函数行为是什么?是否满足我们的需求。

(2)编译器默认生成的函数不满足我们的需求,我们需要自己实现,如何自己实现?

所以类和对象中主要讲的就是这四个成员函数的手动实现和运算符重载。

2.构造函数(难度较高)

构造函数是特殊的成员函数,但构造函数主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是替代Init函数的功能,构造函数自动调用的特点就完美替代的了Init。

构造函数的特点:

(1)函数名与类名相同。

(2)无返回值(返回值啥都不要给,也不需要写void)。

(3)对象实例化时,系统会自动调用对应的构造函数。

如:

#include<iostream>
using namespace std;
class Date
{
public:
	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;
	d1.Print();
	return 0;
}

我们发现:在我们不主动调用这个构造函数的情况下,我们还是能进行初始化,这是构造函数一些基本特点。

那我们如果想手动传入日期怎么办?

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(2025,4,15);
	d1.Print();
	Date d2;
	d2.Print();
	return 0;
}

我们发现如果在不传参的情况下就会直接用我们的缺省值来作为参数,否则就会用我们的自己传的参数进行初始化,而我们在实例化对象的时候就在实例化对象加入所传的参数即可以把我们想要初始化的值直接传入进去。

但是我们不能在实例化对象的时候加上一个()而不传参,因为这样无法区分这是函数声明还是类的对象定义。

其次,我们写构造函数的时候能全缺省也可以不加一个形参,也可以直接不写(构造函数的第六个特点)。

(4)构造函数可以重载。

我们如果写了多个构造函数,我们的函数名是一样的,但是参数类型和参数个数都可以进行改变,如我们可以直接指定哪一年哪个月等等。

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

我们可以自己写,但是编译器生成的可能把初始值置为空,也可以置为随机值,如下:

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(2025,4,15);
	d1.Print();*/
	Date d2;
	d2.Print();
	return 0;
}

在VS编译器会生成一个很小的值,这主要取决于编译器,如果我们之后想要把值修改的话可以通

过其他函数的访问或者自己写,因为我们如果直接这样写:

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2025,4,15);
	d1.Print();
	Date d2(2025,4,15);
	//d2.Date(2025, 4, 15);
	d2.Print();
	return 0;
}

则会报错:

因为我们没有手动定义这个Date,所以会有报错,我们只能根据编译器自己生成的构造函数的初始化来进行初始化。所以建议自己写或者之后写一些函数来进行手动初始化。

(6)我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说:是否初始化时不确定的,看编译器。对于自定义类型的变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。

内置类型可以说是编译器本来就有的类型,如:int/double/……/指针 ,自定义类型:class/struct/union/……

默认生成的构造函数不符合我们的预期,所以要我们自己写:如栈、日期这样的类。

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

我们发现,如果我们不写构造函数的情况下,我们并不能申请内存空间去初始化栈,所以我们需要自己写这个函数。而在我们之前写过的题目中有一个题目很方便,不用写构造函数,就是:用两个栈实现一个队列,我们可以直接写出以下的类:

// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};

我们之前自定义了Stack这个类,在使用MyQueue这个类中,我们全部都是Stack类型,所以我们在调用MyQueue这个类的构造函数的时候,直接用栈这个类的构造函数就可以得到结果了,所以这是一个不用自己写的构造(也可称为不需要写默认生成就可以用的构造)。

(7)无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫默认构造函数。但三个函数有且只能有一个存在,不能同时存在。总结一下就是:不传实参就可以调用的构造就叫默认构造。

但是大多数情况下我们都要自己去写默认构造函数,但是都要遵循(1)-(4)这四个特点。

3.析构函数

其功能与构造函数功能相反,析构函数不是完成对对象本身的销毁,而是完成对象中资源的清理释放工作,C++规定对象在销毁时会自动调用析构函数。析构函数功能类比我们之前Stack实现的Destroy功能(本质上就是动态申请内存空间的释放),而像Date类没有Destroy。其实就是没有资源的释放,所以严格说Date是不需要析构函数的。(也就是说只有我们向内存中动态开辟了空间的时候我们才要写析构函数的(因为编译器自动生成的析构函数就够了))。析构函数的特点:

(1)析构函数名是在类名前加上字符'~'(这不是一个横线,和我们的C语言中的按位取反是一样的符号)。

(2)无参数,无返回值。(不要加void)

(3)一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

(4)对象的生命周期结束时,系统会自动调用析构函数。

(5)我们不写,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数。

如:~Date(){free(_year);free(_month);free(_day);}

这种写法是错误的,无资源释放,所以不需要析构。

如:~Stack(){free(_a);_a=nullptr;_top=_capacity=0;}

这个就是正确的写析构函数的方法,只有动态开辟资源的才需要写析构!如果成员变量是自定义类型,则如果自定义类型成员的析构函数已经满足我们使用,我们就不要写这个析构函数了,否则我们需要自己写一个析构函数。

(6)还要注意的是,我们显式写析构函数,对自定义类型成员也会调用它的析构,也就是说自定义类型成员无论如何都会自动调用析构函数。

构造函数与析构函数的好处:不用Init和Destroy了,我们也不需要主动调用了,虽然有时候需要我们自己写这两个函数,但是还是有一定的作用的。

(7)如果类中没有申请资源时,析构函数可以不写,直接使用编译器自动生成的析构函数,如:Date;如过默认生成的析构就可以用,也就不需要显式写析构,如:MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄露,如:Stack。

(8)一个局部域的多个对象,C++规定后定义的先析构。

由于析构函数只有一些需要注意的点,所以我在这里就不用代码演示了。

4.拷贝构造函数

如果一个构造函数的第一个参数就是自身类型的引用,且任何额外的参数都有默认值,则此构造函数也叫拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。

拷贝构造函数的特点:

(1)拷贝构造函数是构造函数的一个重载。

也就是说拷贝构造函数名也是类名,但是参数不是一样的,如:

class Date
{
public:
	//构造函数
	Date(int year = 1,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(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(2025, 4, 19);
	//调用拷贝构造函数
	Date d2(d1);
	return 0;
}

我们发现d2在实例化时传参是d1,这就是拷贝构造,而这个参数就是Date类,也就是说,我们通过已经实例化的对象作为参数去实例化另外一个对象这个过程就是拷贝构造,而拷贝构造就把开始的对象的每一个值拷贝到另外一个对象上。但是形参必须是这个类的类型对象的引用(见第二个特点)。

(2)拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值的方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以有多个参数,但是第一个参数必须是类类型的对象的引用,后面的参数必须有缺省值!

如:void func1(Date d ){……}Date d1(2025,4,19);func1(d1);.

若我们是这种写法的话,我们就会发现:(1)还没运行就会出现:类 "Date" 的复制构造函数不能带有 "Date" 类型的参数;(2)运行时发现,有一堆的报错:

为什么?

C++规定:无论你是在传参,还是初始化时,只要用一个自定义类型对象去初始化另外一个自定义类型的对象,要调用拷贝构造。

也就是说d1和形参d在func1(d1)语句会直接去调用拷贝构造函数,而拷贝构造函数又有形参类型为自定义类型,又会调用自身的拷贝构造函数,也就是一直调用自己,所以会报错。而解决这个问题的方法就是自定义类型的拷贝构造函数的第一个参数就是自定义类型的引用。

所以之后写拷贝构造函数的时候第一个形参必须是自身类类型的对象的引用。

(3)C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

要验证这个特点就是,把之前代码改为:

void func1(Date d)
{

}
Date func2()
{
	Date d;
	return d;
}
int main()
{
	//调用构造函数
	Date d1(2025, 4, 19);
	//调用拷贝构造函数
	//Date d2(d1);
	//d2.Print();
	func1(d1);
	Date ret = func2();
	return 0;
}

发现最终结果会为:Date(Date& d),如果把func1(d1)这个语句去掉,则没有打印,因为在Vs2022上会对这个语句产生优化,而且会使Date ret = func2();和func2();两个句子都报错,所以我们不能在Vs2022上验证这个结论,所以传值返回会报错,因为传值返回返回的不是返回d,而是d的拷贝(产生临时对象),调用完后会销毁。得出结论:自定义类型传值传参和传值返回比内置类型付出更大的代价。因为内置类型是系统自己定义的类型,它在指定时已经完成拷贝,它的对象比较小,但自定义类型更复杂一些。所以我们应该在func2中改返回值为Date&,而且我们一般都返回的是Date&,而不是Date。

为什么用引用?

引用是取别名,在语法上可以视为没有开辟空间,且不会因为函数结束时对象销毁而报错。

推荐在形参类型前加const,因为我们可能会出现代码有误的问题,防止对象被修改了(可以增强程序的健壮性)!(一般传引用基本上加const)

如果把拷贝构造写成这样的形式:Date(const Date& d,int x = 1){},则不会有问题,但是我们必须要给第二个参数一个缺省值(规定,原因不要深究)。

(4)若未显式定义拷贝构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。

在没有显式定义拷贝构造的情况下,如果没有自定义类型对象,就基本上没有问题;但是如果在有自定义类型对象的情况下,则会出大问题。如:栈。

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	st1.Push(4);
	Stack st2(st1);
	return 0;
}

会报错,因为我们浅拷贝会把_a的地址也拷贝了(本质就是浅拷贝的问题),但是析构会出问题:堆数组(动态开辟的数组)空间释放两次(有st1、st2两个对象,我们在main函数销毁时会出现二者都要析构的情况,所以会出现两次释放),会报错,或者这个空间释放后又被申请了,但是又释放了结果导致这个东西的地址也没了;还有一个问题:一个改变影响另外一个。如果我们把st1出栈两次,而我们st2如果访问第三个元素,就会报错,会导致数组越界的问题。或者我们把栈顶数据修改,我们如果想用st1去访问和用st2访问,则两个结果不会相同,所以这就是浅拷贝的一个很严重的问题。所以我们现在要写深拷贝(内部有资源的就需要写深拷贝),就是把指向的资源也进行拷贝。我们需要自己再开辟一个相同的空间去存储数据。

我们在现阶段一般用以下方式来进行深拷贝:

Stack(const Stack& st)
{
	_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
	if (nullptr == _a)
	{
		perror("malloc申请空间失败!!!");
		return;
	}
	memcpy(_a, st._a, sizeof(STDataType) * st._top);
	_top = st._top;
	_capacity = st._capacity;
}

如果是链表我们还需要把结点什么的都拷贝过来,我们是先开辟和st一样大小的空间,再进行拷贝数据。

之后学了string才会进行更深入的理解,这个深拷贝比较简单。

如果一个类只要你必须显式的写析构函数,就要显式写拷贝构造。(都有资源的申请)

(5)像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现
MyQueue的拷⻉构造。

(6) 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

这是之前写的代码的解释的详尽化。

5.赋值运算符重载

5.1运算符重载

(1)当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。

运算符重载和函数重载意思差不多,一个东西可以用作这个,也可以用作另外一个。如,我们重载之前+只能用于内置类型之间的加法运算,而我们重载后我们就可以用于内置类型与类类型的加法运算,或者类与类之间的加法运算。

(2)运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。

(3)重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

如:+需要左边和右边俩个参数,所以叫做二元运算符,所以第一个参数需要为+的左边操作数,第二个参数需要为右边的操作数。~是按位取反,则我们只有一个参数,就只要指定一个值就可以了。而重载+函数的定义为int operator+(int x,int y){return x+y;}(这样是一个示例,本来+就有这个功能,我们没必要去真的重载一下这两个int类型的加法)。

(4)如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。

(5)运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。

(6)不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。

(7).*  :: sizeof  ?: .这5个运算符不能进行重载。

.*的用法:若我们定义了A这个类,且这个类里面还有func()函数,又typedef  void(*PF)();在主函数中用PF pf=&A::func;A obj;(obj.*pf)();所以.*是调用成员函数指针的,而obj.*pf就是相当于func。

.的用法:就是和我们用类实例化对象然后对象.成员。

(8)重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int
operator+(int x, int y)

我们不能重载+后却是实现-的功能,也就是我们不能改变它的功能。

(9)⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator+就没有意义。

(10)重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。

(11)重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

下一讲我将用这些学过的知识来实现一个系统,就是日期管理系统,我们可以通过这个系统来进行复习之前的知识和介绍新知识,而且里面有很多运算符重载的函数,所以在这一讲我不会用很多代码来说明。

5.2赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意的跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。如:

class Date
{
public:
	//构造函数
	Date(int year = 1,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "Date(Date&d)" << endl;
	}
	//=运算符重载
	void 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(2024, 11, 14);
	Date d3(2025, 11, 14);
	d1 = d3;
	return 0;
}

特点:

(1)赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引⽤,否则传值传参会有拷⻉。

赋值运算符重载不支持连续赋值,因为d1=d3相当于函数调用,d2=d1=d3相当于d2把返回值拿走了,所以我们返回值类型为Date,返回值为*this,但是结合之前的说法,我们要返回的是Date&,提高效率,而*this出了这个语句后还在,所以我们用*this是没有问题的。

(2)有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。

(3)没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的赋值重载函数。

(4)像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。

由于我们的栈是需要自动实现这个赋值运算符功能的,所以需要自己写,这是一个基本样例:

//赋值运算符重载
Stack& operator=(const Stack& st)
{
	//先释放原空间
	free(_a);
	//再让创建一个新空间
	_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
	//把另外一个栈中的数据拷贝过来
	//我们拷贝的只有top个数据
	memcpy(_a, st._a, sizeof(STDataType) * st._top);
	//再把_top和_capacity拷贝过去,完成深拷贝
	_top = st._top;
	_capacity = st._capacity;
	return *this;
}

如果我们是用链表或者二叉树来进行赋值运算符重载的话,我们就需要进行深拷贝的操作,就是把所有的东西包括结点都要创建一个,这个之后会涉及到的。

6.取地址运算符重载

6.1const成员函数

(1)将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表后面。

如果我们这样写:

class Date
{
public:
	//构造函数
	Date(int year = 1,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "Date(Date&d)" << endl;
	}
	//Date& operator+=(int t)
	//{
	//	_day += t;
	//	return *this;
	//}
	void Print() 
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	const Date d1(2025, 4, 20);
	d1.Print();
	//d1 += 10;
	return 0;
}

那么这里的d1.Print()是用不了的,如果我们用其他的成员函数基本上也用不了的,因为每一个成员函数都包含了一个this指针,this类型是Date*,而如果调用Print这个成员函数,则传地址是&d1->const Date*相当于this是传的为const Date*,所以这里涉及到权限放大的问题,所以我们要么就在d1初始化时把const去掉,或者在成员函数定义时如果有成员变量不会修改的成员函数,那么就在那个参数列表后加const,这样哪怕d1不是const Date类型也不会报错,我们发现Print函数只是打印这个对象中的成员变量,所以我们把它改为:

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

但是如果我们有成员变量修改的部分则我们不要加const,而且在之后的对象成员函数调用时我们就不要调用它了(如果对象是具有常性的情况下)。

(2)const实际修饰改成员函数隐含的this指针,表明在改成员函数不能对类的任何成员进行修改,const修饰的Date类的Print成员函数,Print隐含的this指针有Date* const this改为 const Date* const this。

第二个const是修饰this的,防止this被修改,第一个const是修饰指针所指向的对象,表明this指针指向的对象是不可以被改变的,无法通过this指针来修改所指向的Date对象的成员变量。

6.2取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

这也是另外两个默认成员函数,这两个成员函数我们不需要自己显式实现,注意的点不是很多,所以代码我也不怎么多讲解,只要知道有这个东西就可以了。

//普通
Date* operator&()
{
	return this;
}
//const
const Date* operator&() const
{
	//返回值不可以省略const,因为会涉及到权限放大问题
	return this;
	//最后的const也不能省略(见const修饰成员函数的含义)
}

如果我们不想让外界取到地址,我们就可以返回nullptr或者一个随机的地址。

当然,还有一个要注意的点:如果我们只写一个取地址运算符重载的成员函数,另外一个是不会自动生成的!所以要么不写,要么全部都写上。

7.总结

类和对象中涉及的点太多了,远远不及我讲的这些,而且构造函数还有一些点也是没有讲完的,这些点将会放到类和对象下去讲,至于其他的补充知识也要看之后的内容。之后我会把之前学过的内容应用到我实现的系统里面,这个系统是日期管理系统,能实现日期加减天数,和日期减日期的功能,里面还会涉及到初始化列表的东西,这个会在类和对象下再讲,这个代码你们只要copy就可以了。喜欢的可以一键三连哦!


网站公告

今日签到

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