【C++初阶】类与对象(中) ---- 四个重要默认成员函数

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


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

如果一个类中什么成员都没有,简称为空类。 然而,空类中真的什么都没有吗?其实并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数:编译器会自动生成函数

在这里插入图片描述

二、为什么会存在构造函数和析构函数

在使用类的函数的时候,某些粗心的程序员可能会 忘记初始化和销毁。特别是销毁,如果程序结束后没有及时销毁,可能会存在内存泄漏。为了能一劳永逸解决这个问题,C++就有了构造函数和析构函数,它们可以自动完成初始化工作和销毁。

以下是栈(部分)的实现,缺少栈的初始化和销毁

class Stack
{
public:
	// 栈的初始化
	void Init(int defaultCapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * defaultCapacity);
		if (nullptr == a)
		{
			return;
		}
		capacity = defaultCapacity;
		top = 0;
	}

	// 栈的销毁
	void Destroy()
	{
		free(a);
		a = nullptr;
		top = capacity;
	}
private:
	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack s1;
	//假设粗心的程序员未给栈初始化(已注释掉)
	//s1.Init();

	//假设粗心的程序员为进行栈的销毁(已注释掉)
	//s1.Destroy();

	return 0;
}

未给栈初始化会导致程序崩溃,未给栈销毁会导致内存泄漏。那么构造函数和析构函数又是如何帮助我们初始化和销毁的?以及它们的写法是怎么样的呢?让我们接着往下看:

三、构造函数(默认成员函数)

3.1 什么是构造函数

注意:析构函数不是自动实例化类对象,而是初始化对象。

3.2 构造函数的特性(重点)

  1. 函数名与类名相同。
  2. 不用写返回类型。
  3. 对象实例化时编译器会自动调用默认构造函数
  4. 构造函数支持函数重载 -> 点击跳转
  5. 特性5:如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数 -> 点击跳转

根据以特征,我们可以轻松对以上栈的代码进行修改如下:

#include <iostream>
#include <stdlib.h>
using namespace std;

class Stack
{
public:
	// 构造函数
	Stack(int defaultCapacity = 4)
	{
		//其逻辑和栈的初始化一模一样
		a = (int*)malloc(sizeof(int) * defaultCapacity);
		if (nullptr == a)
		{
			return;
		}
		capacity = defaultCapacity;
		top = 0;
	}

	// 栈的初始化
	// 有了构造函数以下代码可以屏蔽
	/*void Init(int defaultCapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * defaultCapacity);
		if (nullptr == a)
		{
			return;
		}
		capacity = defaultCapacity;
		top = 0;
	}*/
	
	// 栈的销毁
	void Destroy()
	{
		free(a);
		a = nullptr;
		top = capacity;
	}

private:
	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack s1;
	//假设粗心的程序员未给栈初始化(已注释掉)
	//s1.Init();

	//假设粗心的程序员为进行栈的销毁(已注释掉)
	//s1.Destroy();

	return 0;
}

【程序结果】

在这里插入图片描述

特性4:构造函数支持函数重载。注意:默认构造函数只能有一个(避免无参调用存在歧义)。其中默认构造函数包括:无参构造函数、全缺省构造函数和编译器自动生成

【例如】

class Date
{
public:
	// 1.无参构造函数
	Date()
	{}

	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		Year = year;
		Month = month;
		Day = day;
	}

private:
	int Year;
	int Month;
	int Day;
};

int main()
{
	Date d1; // 调用无参构造函数
	Date d2(2015, 1, 1); // 调用带参的构造函数

	Date d3();//错误
	// warning: 未调用原型函数(是否是有意用变量定义的?)

	return 0;
}

【构造函数的调用】

构造函数的调用和普通函数的调用是不一样的!构造函数的调用是在对象后加上参数列表或者不加参数列表。如果要调用无参构造函数时,对象后面不用跟括号,否则编译器无法区分是否是对象还是函数名。如以上d3的错误

特性5:如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数

#include <iostream>
using namespace std;

class Date
{
public:
	void Print()
	{
		cout << Year << ' ' << Month << ' ' << Day << endl;
	}
private:
	//声明
	int Year;
	int Month;
	int Day;
};

int main()
{
	Date d1;
	d1.Print();

	return 0;
}

在这里插入图片描述

上述类中并没有显式定义构造函数,因此编译器会自动生成一个无参的默认构造函数。但d1的成员变量通过编辑器初始化后打印的结果是一个随机值,这是为什么呢?

【解答】

  • 在C++中,一般把类型分为两类:第一类是 内置类型(基本类型),像intchardouble指针这些语言本身定义的类型。
  • 第二种是 自定义类型,就是自己定义的类型。诸如用structclassunion等定义的类型。
  • 因此,若用户不自己定义构造函数,编译器默认生成的构造函数对内置类型不做处理而自定义类型会去调用自己的默认构造函数

【总结】

  1. 当成员变量有内置类型的时候,需要自己写一个构造函数
  2. 若成员变量全都是自定义类型时可以考虑使用编译器默认的构造函数(注意:自定义类型一定要有它的默认构造(无参、全缺省、自动生成),不然会报错)。当然也可以自己写一个构造函数,具体看要求。

后来,C++11中针对编译器默认构造函数对内置类型成员不初始化的缺陷做出了优化,即:内置类型成员变量在类中声明时可以给缺省值

在这里插入图片描述

再次提醒:以上的成员变量都是声明,而不是初始化。这里给的是默认缺省值,仅供编译器生成默认构造函数使用。如果定义了构造函数,那么这个缺省值就没用了。

四、析构函数(默认成员函数)

4.1 什么是析构函数

对象在销毁时会【自动】调用析构函数,完成对象中资源的清理工作(释放堆上申请的空间)

4.2 析构函数的特性

  1. 析构函数的函数名和类名相同、无返回值,但是在类名前加上 ~
  2. 注意:析构函数不能有参数。因此析构函数不能重载,所以一个类只能有一个析构函数
  3. 对象生命周期结束时,C++编译系统系统自动调用析构函数
  4. 若未显式定义析构函数,系统会自动生成默认的析构函数注意:系统自动生成默认的析构函数对内置类型成员不做处理,而自定义类型会去调用它的析构函数点击跳转

根据以上特性,我们对一开始栈的代码末尾未初始化进行修改如下:

class Stack
{
public:
	//构造函数(初始化)
	Stack(int defaultCapacity = 4)
	{
		//其逻辑和栈的初始化一模一样
		a = (int*)malloc(sizeof(int) * defaultCapacity);
		if (nullptr == a)
		{
			return;
		}
		capacity = defaultCapacity;
		top = 0;

	}

	//析构函数(销毁)
	~Stack()
	{
		cout << "空间已被销毁" << endl;
		free(a);
		a = nullptr;
		top = capacity;
	}

	// 栈的初始化
	//有了构造函数以下代码可以屏蔽
	/*void Init(int defaultCapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * defaultCapacity);
		if (nullptr == a)
		{
			return;
		}
		capacity = defaultCapacity;
		top = 0;
	}*/

	// 栈的销毁
	//void Destroy()
	//{
	//	free(a);
	//	a = nullptr;
	//	top = capacity;
	//}

private:
	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack s1;

	//假设粗心的程序员为进行栈的销毁(已注释掉)
	//s1.Destroy();

	return 0;
}

为了验证【对象生命周期结束时,C++编译系统自动调用析构函数】,我在析构函数加了【“空间已被销毁”】,最后让我们来看看结果:
在这里插入图片描述

特性5: 若未显式定义析构函数,系统会自动生成默认的析构函数注意:系统自动生成默认的析构函数对内置类型成员不做处理,而自定义类型会去调用它的析构函数

因此,什么时候写析构我做了一下总结:

  1. 如果类没有动态申请时(出了作用域自动销毁),析构函数就可以不写
  2. 如果成员变量都是自定义类型,析构函数就可以不写。
  3. 如果有动态申请资源,就需要显示写析构函数释放。

在这里插入图片描述

五、拷贝构造函数(默认成员函数)

5.1 什么是拷贝构造函数

用【已存在】的同类型对象创建新对象时,由编译器自动调用。

5.2 拷贝构造函数的特性

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

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个必须是类类型对象的引用。注意:如果形参没有引用会直接报错,因为会引发无穷递归调用。

根据以上特性,举一个拷贝构造函数的例子:

#include <iostream>
using namespace std;

class Date
{
public:
	// 构造函数(对d1初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		Year = year;
		Month = month;
		Day = day;
	}

	// 拷贝构造函数
	//d是d1的别名
	Date(const Date& d)
	{
		this->Year = d.Year;
		this->Month = d.Month;
		this->Day = d.Day;
	}

	void Print()
	{
		cout << this->Year << '-' << this->Month << '-' << this->Day << endl;
	}

private:
	int Year;
	int Month;
	int Day;
};

int main()
{
	Date d1(2024, 5, 15);
	d1.Print();

	Date d2(d1); // d1已存在,拷贝构造d2
	d2.Print();

	return 0;
}

【程序结果】

在这里插入图片描述

  • 为什么形成无引用编译器直接报错,会引发无穷递归调用呢?
    解答:
    对于C++传参有两个规定:① 如果传参传的是内置类型,则直接拷贝给形参② 如果传的是自定义类型,首先必须先调用拷贝构造函数拷贝,再传给形参 不能口说无凭,可以调试证明
  • 内置类型传参
    在这里插入图片描述
    注意看调试时的黄色箭头,内置类型传参时是直接拷贝的
  • 自定义类型传参
    在这里插入图片描述
    如上所示,在调用func函数时,并没有马上调用func,而是先调用了拷贝构造函数

【总结】
因此对于自定义类型,如果采用传值调用,由于形参是实参的一份临时拷贝,这个临时拷贝的过程会调用拷贝构造函数,而调用拷贝构造这个过程有需要传参,也就又需要调用拷贝构造函数,一直下去就会发生死递归。因此,在C++中通常使用 引用 来破局
在这里插入图片描述

特性3:若未显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数。

如果不手动编写,编译器会默认生成一个浅拷贝的拷贝构造函数,即将所有成员变量逐一拷贝到新对象中。

  1. 对于全是内置类型成员会完成浅拷贝,因此可以不显示写出。
  2. 对于全是自定义类型的成员变量,它会调用自己的拷贝构造函数,因此也可以不显示写出
  3. 如果类中有动态分配内存的成员变量则需要手动编写深拷贝的拷贝构造函数即在拷贝对象时对指针变量进行新的内存分配,并将原指针变量指向的内容复制到新分配的内存中。

注意,这里要区分构造函数和析构函数,默认拷贝构造函数的内置类型和自定义类型编译器都会处理

  1. 内置类型是按照字节方式直接拷贝的,叫做值拷贝,或者也叫浅拷贝
    在这里插入图片描述
  2. 自定义类型会调用它的拷贝构造函数
    在这里插入图片描述
    通过上图我们发现程序报错了,这是为什么呢?
    原因是:通过拷贝后,s2的成员变量a也会指向s1成员变量a所指向的空间。接下来问题就来了,第一个问题:当对象s2生命周期结束时(先构造的后析构),s2所指向的空间就被销毁了;紧接着,对象s1生命周期结束,又对其空间进行销毁,导致同一块空间析构了两次;第二个问题:一个修改会影响另一个。就比如s1增加了一个值,由于指向同一块空间,s2自然而然也会增加一个值。(空间不是独立的)
    在这里插入图片描述
    为了解决此问题,我们需要采用 深拷贝
    在这里插入图片描述

5.3 效率问题

class Date
{
public:
	Date(int year, int month, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}
	
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

Date Test(Date d)
{
	Date temp(d);
	return temp;
}

int main()
{
	Date d1(2022,1,13);
	Test(d1);
	return 0;
}

在这里插入图片描述

因此,为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,如果对象出了作用域还在,那么可以使用引用返回;如果不存在,就只能使用值返回。

六、运算符重载(不是默认成员函数)

6.1 问题引入

假设要比较日期的大小,正常情况下,我们会封装一个比较函数来实现它:

class Date
{
public:

	// 构造函数(负责初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		this->Year = year;
		this->Month = month;
		this->Day = day;
	}

// 类外可以访问
//private:
	int Year; // 年
	int Month; // 月
	int Day; // 日
};

bool cmp(const Date& x1, const Date& x2)
{
	// 假设默认x2最大
	
	// 1. 年份大的则大
	if (x1.Year < x2.Year)
		return true;
	// 2. 月份大的则大(前提是年份相等)
	else if (x1.Year == x2.Year && x1.Month < x2.Month)
		return true;
	// 3. 日大则大(前提是年和月份都相等)
	else if (x1.Year == x2.Year && x1.Month == x2.Month && x1.Day < x2.Day)
		return true;
	// 4. 以上要求都没满足返回false
	return false;
}

int main()
{
	Date d1(2023, 5, 16);
	Date d2(2022, 5, 16);

	cout << cmp(d1, d2) << endl;

	return 0;
}

【程序结果】

在这里插入图片描述

以上代码虽然实现了比较两个日期的大小,但有没有发现代码好像有点缺陷 — 可读性差。原因是:cmp(d1, d2)这串代码,结果一定会是10。那么问题来了,单单通过10如何区分d1大还是d2大?难道调用者还要到函数定义里查看代码逻辑?因此,为了解决此问题,C++引入了 运算符重载

6.2 运算符重载的概念

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,主要注意的是:它不是默认成员函数(编译器不会自动生成)

【函数原型】

返回值类型  operator操作符(参数列表)
{
	// ...
}

所以,在【问题引入】的代码可以运用运算符重载:

#include <iostream>

using namespace std;

class Date
{
public:

	// 构造函数(负责初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		this->Year = year;
		this->Month = month;
		this->Day = day;
	}

//private:
	int Year; // 年
	int Month; // 月
	int Day; // 日
};

// 运算符重载
bool operator<(const Date& x1, const Date& x2)
{
	// 1. 年份大的则大
	if (x1.Year < x2.Year)
		return true;
	// 2. 月份大的则大(前提是年份相等)
	else if (x1.Year == x2.Year && x1.Month < x2.Month)
		return true;
	// 3. 日大则大(前提是年和月份都相等)
	else if (x1.Year == x2.Year && x1.Month == x2.Month && x1.Day < x2.Day)
		return true;
	// 4. 以上要求都没满足返回false
	return false;
}

int main()
{
	Date d1(2023, 5, 16);
	Date d2(2022, 5, 16);

	cout <<(d1 < d2) << endl;

	return 0;
}

【程序结果】

在这里插入图片描述

细节问题:

  1. 为什么在65行为什么要对d1 < d2加括号?
    原因是:流插入操作符<<的优先级高于<
  2. 为什么可以直接写d1 < d2
    原因是:d1 < d2本质上就是在调用operator<,编译器在编译的时候会把d1 < d2 转化为operator<(d1, d2),它们是等价的,可以通过反汇编来查看代码底层:
    在这里插入图片描述

6.3 运算符重载如何做成员函数

大家有没有发现,在【例6.2】中,类中我把成员变量变成公有了,如果不将其设置公有,会导致编译错误,原因是:类外是无法访问私有的成员变量的,那么问题来了,封装性该如何保证?

既然类外没有访问私有的成员变量的权限,但类中的成员函数可以访问私有的成员变量,所以可以将operator<函数放入类中

在这里插入图片描述

虽然把函数放在类中,但是代码还是编译不过。原因是:当运算符重载作为类成员函数重载时,操作数的个数要等于形参的个数而这里的形参其实有三个:x1x2,还有一个隐藏的 this指针

因此,完整代码如下

#include <iostream>
using namespace std;

class Date
{
public:
	// 构造函数(负责初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		this->Year = year;
		this->Month = month;
		this->Day = day;
	}
	// 运算符重载
	// d1 < d2
	// 左操作数是this,指向调用函数的对象
	bool operator<(const Date& x)
	{	
		// 1. 年份大的则大
		if (this->Year < x.Year)
			return true;
		// 2. 月份大的则大(前提是年份相等)
		else if (this->Year == x.Year && this->Month < x.Month)
			return true;
		// 3. 日大则大(前提是年和月份都相等)
		else if (this->Year == x.Year && this->Month == x.Month && this->Day < x.Day)
			return true;
		// 4. 以上要求都没满足返回false
		return false;
	}

private:
	int Year; // 年
	int Month; // 月
	int Day; // 日
};

int main()
{
	Date d1(2023, 5, 16);
	Date d2(2022, 5, 16);

	cout << d1.operator<(d2) << endl;
	// 上下两行代码本质一样
	// 不过更建议下面这样写
	// 因为运算符重载就是为了提高代码可读性
	cout << (d1 < d2) << endl;

	return 0;
}

【程序结果】

在这里插入图片描述

总结

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • 另外,重载的运算符必须要对类有意义。举个例子,后面会模拟实现STL中的链表list,当使用操作遍历链表时,由于链表的内存空间不是连续的,因此我们可以使用运算符重载++来控制,以便可以找到下一个内存空间。

6.4 笔试题

  • .*
  • ::
  • sizeof
  • ?:
  • .

注意以上5个运算符不能重载。这个经常在笔试选择题中出现

七、赋值运算符重载(默认成员函数)

7.1 例子引入

#include <iostream>
using namespace std;

class Date
{
public:

	// 构造函数(负责初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		// 以下的this->均可省略
		this->Year = year;
		this->Month = month;
		this->Day = day;
	}

	// 打印函数
	void Print()
	{
		cout << Year << '-' << Month << '-' << Day << endl;
	}

	// 赋值运算符重载
	// d1 = d2; 
	void operator=(const Date& x)
	{
		// this是d1的地址
		// x是d2的别名
		this->Year = x.Year;
		this->Month = x.Month;
		this->Day = x.Day;
	}

private:
	int Year;
	int Month;
	int Day;
};

int  main()
{
	Date d1(2023, 5, 16);
	Date d2(2023, 5, 20);

	// 现在要将d2赋值给d1
	d1 = d2;// 本质上是d1.operator=(d2)
	
	d1.Print();
	d2.Print();
	
	return 0;
}

【程序结果】

在这里插入图片描述

温馨提示:要注意区分拷贝构造函数和赋值拷贝(赋值运算符重载)

  • 赋值拷贝(赋值运算符重载):已经存在的两个对象之间的赋值拷贝
  • 拷贝构造:是用一个已经存在的对象初始化创建另一个对象
    在这里插入图片描述

7.2 连续赋值

但以上的赋值运算符重载还不够完善:在C语言中,我们可以进行连续赋值。例如:

在这里插入图片描述

以上的代码赋值顺序是从右到左的,6先赋值给k,然后k = 6作为返回值,其类型是int,再赋值给j,依次类推。因此赋值重载也应该支持连续赋值。

#include <iostream>
using namespace std;

class Date
{
public:

	// 构造函数(负责初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		// 以下的this->均可省略
		this->Year = year;
		this->Month = month;
		Day = day;
	}

	// 打印函数
	void Print()
	{
		cout << Year << '-' << Month << '-' << Day << endl;
	}

	// 赋值运算符重载
	// d1 = d2; 
	void operator=(const Date& x)
	{
		// this是d1的地址
		// x是d2的别名
		this->Year = x.Year;
		this->Month = x.Month;
		this->Day = x.Day;
	}

private:
	int Year;
	int Month;
	int Day;
};

int  main()
{
	Date d1(2023, 5, 17);
	Date d2, d3;

	d3 = d2 = d1;
	return 0;
}

【程序报错】

在这里插入图片描述

报错原因:注意类中operator=的返回值类型是void,因此d1赋值给d2后,返回的类型是void,然后再赋值给d3,类型不匹配导致报错。

所以,正确的做法应该是返回Date类型,也就是函数内要返回*this对象。同时函数的返回值可以引用返回。如果是d1 = d2,返回的*this就是d1,由于d1的生命周期不在赋值运算符重载函数内,而是在main函数中,因此使用引用返回来提高效率。----- 只要返回的对象没有被销毁,尽量使用引用返回。

#include <iostream>
using namespace std;

class Date
{
public:

	// 构造函数(负责初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		// 以下的this->均可省略
		this->Year = year;
		this->Month = month;
		Day = day;
	}

	// 打印函数
	void Print()
	{
		cout << Year << '-' << Month << '-' << Day << endl;
	}

	// 赋值运算符重载
	// d1 = d2; 
	Date& operator=(const Date& x)
	{
		// this是d1的地址
		// x是d2的别名
		this->Year = x.Year;
		this->Month = x.Month;
		this->Day = x.Day;

		// 这里返回的是d1的类型
		return *this;
	}

private:
	int Year;
	int Month;
	int Day;
};

int  main()
{
	Date d1(2023, 5, 17);
	Date d2, d3;

	d3 = d2 = d1;

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

【程序结果】

在这里插入图片描述

但是以上代码还是不完整,假设出现d1 = d1(自己和自己赋值),虽然可以,但是没有必要。因此可以加一层判断:比较地址

Date& operator=(const Date& x)
{
	if (&x != this)
	{
		this->Year = x.Year;
		this->Month = x.Month;
		this->Day = x.Day;
	}	
	return *this;
}

【总结赋值运算符重载函数模板】

类型& operator=(const 类型& 变量名)
{
	if (&变量名 != this)
	{
		// ...
	}
    return *this;  
}

7.3 浅拷贝问题

赋值运算符重载函数是默认成员函数,没有显示定义,编译器自动会生成,默认以值的方式逐字节拷贝(浅拷贝),而自定义类型成员会去调用它的默认函数。但是要注意动态开辟的成员变量,要完成深拷贝。

因此,日期类就可以不用显示定义赋值运算符重载,使用编译器默认提供的即可:

#include <iostream>
using namespace std;

class Date
{
public:

	// 构造函数(负责初始化)
	Date(int year = 1, int month = 1, int day = 1)
	{
		// 以下的this->均可省略
		this->Year = year;
		this->Month = month;
		Day = day;
	}

	// 打印函数
	void Print()
	{
		cout << Year << '-' << Month << '-' << Day << endl;
	}
	// 如果没有显式实现时,编译器会生成一个默认赋值运算符重载函数

private:
	int Year;
	int Month;
	int Day;
};

int  main()
{
	Date d1(2023, 5, 17);
	Date d2, d3;

	d3 = d2 = d1;

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

【程序结果】

在这里插入图片描述

八、const对象和const成员

如果是const对象调用普通成员函数是否可以呢?

#include <iostream>
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;
	int Month;
	int Day;
};

int main()
{
	Date d1(2023, 5, 20);
	d1.Print();

	const Date d2(2023, 5, 21);
	d2.Print();

	return 0;
}

【程序结果】

在这里插入图片描述

  • 以上代码对于d2是编译不通过的,这是因为:
  1. 当对象d1调用成员函数Print时,编译器会隐式自动地将该对象的地址作为第一个参数传递给成员函数,因此,在一个类的成员函数内部,可以使用this指针来访问该对象的成员变量和成员函数。this指针的类型:Date* const thisconst修饰this,即成员函数中,不能修改this指针;
  2. 而对于对象d2,当调用成员函数时,编译器同样会自动将该对象作为第一个参数传递给成员函数,但不同的是:由于const修饰d2,那么*this就是d2,可以推出*this的类型就是const,因此this的类型:const Date* const this。而Print函数的类型是Date* const this,因此会导致权限放大。

因此,为了解决以上问题。C++是允许在函数后加上一个const

在这里插入图片描述
加上在函数后加上const后,在编译器眼中Print的形参是这样的:

void Print(const Date* const Date)

这样不但保证const对象可以调用;普通对象也可以调用,因为权限可以缩小!

但并不是所有的函数后面都可以加const

  1. 但注意:由于const修饰的对象,其形参中的*thisconst修饰,也就说明不能对*this进行修改。因此,如果成员函数内要修改对象千万不要用const修饰对象。这样const对象和普通对象都能调用
  2. 如果声明和定义分离,声明的函数和定义的函数都要加const

九、(了解)取地址及const取地址操作符重载(默认成员函数)

9.1 例子引入

首先先来看看一下代码:

#include <iostream>
using namespace std;

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

int main()
{
	Date d1(2023, 5, 20);
	const Date d2(2023, 5, 21);
	
	cout << &d1 << endl;
	cout << &d2 << endl;
	
	return 0;
}

【程序结果】

在这里插入图片描述

通过以上结果我们发现:const取地址操作符重载不用重新定义,编译器默认会生成。

当然,如果想自己定义也是可以的,代码如下:

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		Year = year;
		Month = month;
		Day = day;
	}
	
	Date* operator&()
	{
		return this;
	}
	
	const Date* operator&() const
	{
		return this;
	}

private:
	int Year; 
	int Month; 
	int Day; 
};

int main()
{
	Date d1(2023, 5, 20);
	const Date d2(2023, 5, 21);
	
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

【程序结果】

在这里插入图片描述

需要注意的是:重载const取地址操作符时,返回的指针类型必须是const指针,否则会导致编译错误。

9.2 特殊使用情况

虽然这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如 不想让别人获取到指定的内容

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		Year = year;
		Month = month;
		Day = day;
	}

	Date* operator&()
	{
		return nullptr;
	}

	const Date* operator&() const
	{
		return nullptr;
	}
private:
	int Year; 
	int Month; 
	int Day; 
};

int main()
{
	Date d1(2023, 5, 20);
	const Date d2(2023, 5, 21);
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

【程序结果】

在这里插入图片描述

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值