【C++】类和对象 - 中

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

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

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认员函数。
在这里插入图片描述
其中,前四个默认成员函数是非常重要的。

2. 构造函数

每创建一个对象或者变量,第一步也是很重要的一步是对其进行初始化,赋予一个初始值。

但实际编写代码时,尤其在C语言阶段手搓数据结构,往往非常容易忘掉这一步,后续使用这个未初始化的对象是会报错,此时才注意到忘记了初始化。

C++的设计者显然也发现了这个问题,因此便设计出了构造函数来解决这种情况。

2.1 概念

对于以下Date类:

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, d2;
	 d1.Init(2022,1,11);
	 d2.Init(2022, 1, 12);
	 return 0;
}

对于Date类,可以通过 调用Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

这里则需要用到构造函数。

2.2 特性

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

需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象,对象的空间是由系统开辟的

其特征如下:

  1. 函数名与类名相同。
  2. 无返回值(不需要写void)。
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载
class Date{
public:
	//构造函数
	Date(int year = 2023, int month = 7, int day = 30) {
		cout << "Date(int year, int month, int day)" << endl;
		_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(2023, 10, 1);
	 d1.Print();
	 d2.Print();
	 return 0;
}

在这里插入图片描述
编译器自动调用构造函数,完成成员变量的初始化。

  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
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;
}

编译器自动生成的一个无参的默认构造函数干了什么呢?
在这里插入图片描述
结果是随机值,好像并没有完成初始化的工作?

这是因为C++标准规定,默认生成的构造函数对语言的内置类型不做初始化处理(intfloat…),对于自定义类型(struct、union、class…)会去调用它们的默认构造函数。

class A {
public:
	A() {
		cout << "A()" << endl;
	}
	~A() {
		cout << "~A()" << endl;
	}
private:
	int _a;
};
class Date {
public:
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A _a;
};
int main()
{
	 Date d1;
	 d1.Print();
	 return 0;
}

定义了一个新的类类型A,在Date类中声明了一个A类型的对象_a。
在这里插入图片描述
可以发现,对于自定义类型,编译器会去调用该类中的默认构造和析构函数,内置类型是不做初始化处理的。

有些编译器可能会对内置类型做处理,但这是个性化行为

针对这种情况,其中一种做法是:如果类中包含内置类型成员,那么建议去显式地去写构造函数,如果全是自定义类型且这些类型都有默认构造可用则考虑可以不写

除此之外,在C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给缺省值。这种给了缺省值且符合初始化要求的也可以不写构造。

类似于函数传参的缺省参数,如果不传则使用缺省值,否则使用传递的参数

class Date {
public:
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//注意这里不是初始化
	//只是给内置类型成员声明一个默认值
private:
	int _year = 2023;
	int _month = 7;
	int _day = 30;
};
int main()
{
	 Date d1;
	 d1.Print();
	 return 0;
}

在这里插入图片描述
默认使用的是缺省值。

3.3 默认构造函数

在上面的特性中提到过,构造函数也是支持函数重载的:

class Date {
public:
	///无参构造
	Date() {
		_year = 1;
		_month = 1;
		_day = 1;
	}
	//全缺省构造
	Date(int year = 1, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year = 2023;
	int _month = 7;
	int _day = 30;
};
int main()
{
	 Date d1;
	 return 0;
}

这俩构造函数,在语法上构成函数重载,但是在无参调用时会引发歧义,编译器不知道调哪个,因为都可以不传递参数。而这两个无参即可调用的构造函数称之为默认构造函数,一个类中可以有多个重载的构造函数,但是只能一个默认构造!

除了上面两个默认构造函数,还有一个,当不显式地写让编译器默认生成的也是默认构造函数,调用时无需传参,这三个都叫做默认构造函数,只能存在一个

3. 析构函数

与初始化相对,每次对象使用完毕时,若对象中的成员使用了动态开辟的内存资源时,需要手动释放其使用的资源,不然会造成内存泄漏,计算机的内存资源是有限的,泄露的多了会很大程度会影响其余进程的正常运行。

同样为了避免忘记释放资源的问题,设计出了析构函数来解决这种情况。

3.1 概念

析构函数也是一个特殊的成员函数,析构函数不是用来完成对对象本身的空间进行销毁,局部对象空间的销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

销毁是指对象的生命周期结束,空间被系统回收

3.2 特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译器会自动调用析构函数
class Date{
public:

	Date(int year = 2023, int month = 7, int day = 30) {
		cout << "Date(int year, int month, int day)" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	~Date() {
		cout << "~Date()" << endl;
		//释放资源...
	}
private:
	 int _year; 
	 int _month;
	 int _day;
};
int main()
{
	 Date d1;	 
	 return 0;
}

在这里插入图片描述
对象的生命周期结束时自动调用,不会存在忘记释放资源这种问题了。

当然了Data类中并没有动态申请的资源可以释放

但需要注意的是,系统默认生成的析构函数与系统默认生成的构造函数在处理成员的方法上一样,对与内置类型也不做处理,对于自定义类型会去调用它的析构函数。因此如果有动态申请的内存资源,那么就需要显式地去写析构,完成内存资源释放,否则会造成内存泄漏。

哪些情况不需要写析构函数?

  1. 没有动态开辟资源。
  2. 需要释放资源的都是自定义类型。

具体写不写还是要根据实际的需求来决定。

4. 拷贝构造函数

如果想对对象进行修改操作,但是又不想修改原对象,此时就可以实例化一个新对象的同时把原对象的数据拷贝给它。

4.2 概念

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

4.3 特性

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

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

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 = 2023;
	int _month = 7;
	int _day = 30;
};
int main()
{
	 Date d1;
	 //调用拷贝构造
	 Date d2(d1);
	 return 0;
}

由于函数中并不会修改形参,建议前面加上const来修饰

引发无穷递归的原因是:如果是传值传参,那么要把实参的值拷贝给形参,既然需要拷贝,那么就需要调用拷贝构造,而调用拷贝构造又需要进行传参,形参是实参的一份临时拷贝,拷贝又需要调用拷贝构造,调用拷贝构造又需要先传参,形参是…
所以要传该类型对象的引用,因为引用是该对象的别名,不会发生拷贝行为

在这里插入图片描述
C语言对于自定义类型之间的拷贝,做法是把原对象中的数据,以字节为单位依次拷贝给新对象,而C++则规定,自定义类型的对象之间进行拷贝时,必须要调用拷贝构造来完成赋值
在这里插入图片描述
之所以这么规定,是为了后面的深拷贝做铺垫。

与浅拷贝相对,浅拷贝仅仅简单的按字节完成值拷贝,对于上面的日期类,浅拷贝即可满足需求,但是对于有动态申请资源的类对象时,则无法满足,需要用到深拷贝

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

拷贝构造与构造和析构函数对于成员的处理有些差别,对于内置类型,拷贝构造也会处理,只不过是对其进行浅拷贝,对于自定义类型会去调用它的拷贝构造函数。

对于下面的类,使用编译器自动生成的拷贝构造就会出现问题:

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

问题在于,完成浅拷贝后,两个指针指向了堆上的同一块空间:
在这里插入图片描述
当两个对象销毁的时候会依次去调用它们的析构函数,这会造成对同一块空间析构(释放)两次,然后造成程序崩溃的问题,这是主要问题,其次由于指向了同一块空间,当一个对象插入数据时,也会影响另一个对象。

对象的析构顺序符合栈后进先出的性质,也就是后定义的先析构

因此对于这种情况,使用编译器默认生成的拷贝构造,仅仅完成浅(值)拷贝是无法满足条件的,这就需要显式地去写拷贝构造。

正确的做法是,新对象需要重新申请一块与原对象同样大的一块空间,然后把原对象中的数据依次拷贝到新空间中,这样便完成了拷贝并且两个对象中的指针都指向了不同的空间,互不影响,如图:

在这里插入图片描述

Stack(const Stack& st) {
	cout << "Stack(const Stack& st)" << endl;
	//重开空间
	_a = (int*)malloc(sizeof(int) * st._capacity);
	if (nullptr == _a) {
		perror("malloc申请空间失败");
		return;
	}
	//把数据拷贝到新空间
	memcpy(_a, st._a, sizeof(int) * st._top);
	_capacity = st._capacity;
	_top = st._top;
}

以上这种拷贝方式是深拷贝里最简单的一种,后续还会有更多更复杂的深拷贝的场景。

拷贝构造函数就是为了解决这种深拷贝问题而设计的

什么情况下要显式写拷贝构造?

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

4.3 传参或作返回值

有了对于深浅拷贝的部分理解后不难看出,若一个对象需要进行拷贝构造,浅拷贝还好尤其是深拷贝时,效率是比较低的,所以当类类型对象进行传参或者作为函数的返回值时,能用引用就要用引用,因为引用不会发生拷贝(不会调用拷贝构造),效率会很高。

上面说引用不会发生拷贝是在语法层面上,但引用是用指针实现的,所以也会发生拷贝,但此拷贝是不同与拷贝构造的拷贝,引用拷贝的代价相较于拷贝构造要低很多

5. 赋值运算符重载

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

对于日期类,若想实现两个对象比较日期大小的函数,C语言只能这么做:

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

通过函数名即可了解到该函数的功能,可读性还可以,若用了一个与函数功能无关的函数名,那别人很难知道这个函数是干什么的,可读性很差。

那是否能让自定义类型像内置类型一样来使用运算符,来提高代码的可读性?

5.1 运算符重载

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

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

然后把上面的代码按照运算符重载的方式来修改一下:

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);
	//两种调用方式
	//1.像内置类型一样
	cout << (d1 < d2) << endl;
	
	//2.显式调用
	cout << (operator<(d1, d2)) << endl;
}

上面两种调用方式是完全等价的,编译器会统一处理。
但使用第二种方式可读性就低了,运算符重载的目的就是为了提高代码的可读性,因此只需要使用第一种调用方式就好

再把这段代码与C语言实现的进行比较,很容易发现,有了运算符重载,大大提高了代码的可读性,并且实现了自定义类型能像内置类型"一样"使用运算符。

但又出现了另一个问题,类中的成员变量是私有的被private修饰,因此类域外部无法直接访问对象中的成员变量,由于该函数是全局函数,所以它是无权访问的。

对于这个问题,有两种方法可以解决:

  1. 把该函数在类中声明为友元函数。
  2. 在类中定义,让其成为成员函数。

最好的方法是采用第二种,定义为成员函数,而友元函数能不用则不用,它会破坏封装。

class Date {
public:
	bool operator<(const Date& d) {
		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;
		return false;
	}
private:
	int _year;
	int _month;
	int _day;
};

如果把其定义在类中作为成员函数,则参数需要调整,因为非静态的成员函数第一个参数默认传递的是this指针,所以看似参数列表只有一个参数,实际上还隐藏了一个this

在调用方法上也有一些差别:

int main() {
	Date d1(2025, 4, 25);
	Date d2(2023, 5, 25);
	//1.间接调用
	d1 < d2;
	//2.显式调用
	d1.operator<(d2);
	return 0;
}

两种形式的函数,只有在第二种调用上有区别,因为是非静态的成员函数,因此只能通过对象名.函数名()的方式来调用,并且传参只要传d2,d1是编译器传的,不用手动传了。

同样的,上面的两种调用方式也是完全等价的

关于运算符重载还有一些注意事项:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数,也就是说运算符重载是针对与自定义类型的。
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义。
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this并且运算符需要几个操作数,重载后的函数就有几个参数
  • .* :: sizeof ?: . 注意这5个运算符不能重载。

5.2 赋值运算符重载

赋值运算符重载是一个默认成员函数,它的作用是完成两个对象之间的拷贝赋值操作,与拷贝构造函数类似,区别在于:
拷贝构造是用一个已经存在的对象初始化一个新创建的对象,而赋值重载则是把一个已经存在的对象拷贝赋值给另一个已经存在的对象。

class Date {
public:
	//拷贝构造
	Date(const Date& d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	//赋值重载
	void operator=(const Date& d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main() {
	Date d1(2025, 4, 25);
	//调用拷贝构造函数
	Date d2(d1);
	//调用赋值重载函数
	d1 = d2;
	return 0;
}

这里的赋值重载只能完成最基本的赋值操作,还存在着部分问题:

  1. 连续赋值:
int i , j;
i = j = 0;

如上表达式的赋值顺序是从右向左,先把0赋值给j,在把j赋值给i,但是上面的赋值重载无法满足这种情况,因为没有返回值,因此需要把返回值类型改为该类类型,并且是引用返回,提高效率

  1. 可能会出现自己给自己赋值的操作,但是这样没有意义,需要在赋值前判断如果不是则进行赋值。

修改后:

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

*this是对this指针解引用,找到调用这个函数的对象后作为返回值返回,继续作为下一个赋值重载函数的参数。

this指针虽然不能显式地传递,但是在函数中可以显式地使用

上面说过,赋值运算符重载是一个默认成员函数,如果不显式地写,那么编译器就会自动生成一个,自动生成的这个对于成员变量的处理与拷贝构造函数是一样的。
赋值重载对于内置类型会直接赋值,也就是按字节完成浅(值)拷贝,对于自定义类型会去调用它的赋值重载

同样的,既然是默认成员函数,就需要考虑要不要自己实现一个?

这个是个拷贝构造一样的情况,可以认为要显式地写拷贝构造的话就需要显式的写赋值重载,因为赋值重载也是一种拷贝,那就需要考虑有些场景会进行深拷贝的情况,比如下面的栈类。

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

赋值重载的实现与拷贝构造函数的实现大体是一样的,但是有些许区别。
赋值重载是在两个已经存在的对象之间进行赋值操作,因此在赋值之前,需要先把原对象中旧的那块动态申请的空间给释放掉,然后在重新开辟一块空间,进行后续的拷贝操作,也就是说赋值重载总体上就比拷贝构造多了一步先释放旧空间的步骤,如果不释放,就会导致内存泄漏,因为不释放就直接赋值的话,那块空间就再也无法找到了。

在这里插入图片描述
注意:赋值运算符只能重载成类的成员函数,不能重载成全局函数,因为赋值重载是默认成员函数,与其他普通运算符重载是不同的

还有一个特殊的点:

Date d1;
Date d2(d1);
Date d3 = d2;

这两条语句都是拷贝构造,第二条虽然用的赋值但本质还是一个已经存在的对象去初始化一个新创建的对象,所以是拷贝构造。

5.3 前后置++ --重载

类对象在大多数情况下也需要像内置类型一样,需要使用前置或后置++ --操作,但是由于它们的操作符名称是一样的,为了区分是前置还是后置重载,C++规定,前置重载不需要特殊处理,而后置重载则需要在参数列表中声明一个整形变量。

// 前置++
Date& Date::operator++();
Date d1;
++d1;
// 后置++
Date Date::operator++(int);
Date d2;
d2++;

调用的时候则没有任何区别,正常调用即可。

后置运算符重载的这个参数不需要传递,编译器会自动传,并且在定义时也不需要给这个参数名称,仅仅作为占位符,来区分前后置重载
声明的类型必须是整形

5.4 自定义类型的输出输出

cout是库里定义的一个全局的ostream类类型的对象,可以调用它的<<运算符重载来向终端输出数据,但是该对象中只重载了内置类型,因此若想输出自定义类型,那么需要自己来重新重载一下<< 运算符:

class Date {
public:
	//各种成员函数...
	
	void operator<<(ostream& out) {
		out << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main() {
	Date d(2000, 1, 1);
	cout << d;
}

如果使用上面的方式调用会报错,因为若将其定义为成员函数,那么第一个参数则默认为隐藏的this指针,也就是说d要做为左操作数,而cout为右操作数,即采用d << cout;这种调用方式是正确的,编译器会转化为d.operator<<(cout);但是这种写法又不符合使用习惯,因此最合理的做法是把它定义为全局函数,而非成员函数。

void operator<<(ostream& ou, const Date& d) {
	ou << d._year << '-' << d._month << '-' << d._day << endl;
	return ou;
}

但是这种写法还有一些问题,比如:

int main() {
	Date d(2000, 1, 1), d1;
	cout << d << d1;
}

上面的写法无法满足这种连续输出,因此需要把out对象返回,继续作为左操作数传参调用运算符重载进行输出。

ostream& operator<<(ostream& ou, const Date& d) {
	ou << d._year << '-' << d._month << '-' << d._day << endl;
	return ou;
}

另一个问题,因为它是一个全局函数,没有办法直接访问到对象中的成员变量,因为它们是被private修饰符修饰,外部无法直接访问,针对这个情况有两种解决方法:

  1. 在类中写几个成员函数,功能为返回对象中的成员变量。
class Date {
public:
	int get_year() {
		return _year;
	}
	//...
private:
	int _year;
	int _month;
	int _day;
};
  1. 还可以将其在类中声明为友元函数
class Date {
public:
	friend ostream& operator<<(ostream& ou, const Date& d);
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& ou, const Date& d) {
	ou << d._year << '-' << d._month << '-' << d._day << endl;
	return ou;
}

前面要加关键字friend来声明,这种做法的含义是能让被声明的函数直接无视访问限定符的约束,进而直接访问对象中的成员,上述两种方法都可以。

同样对于cin也是类似的做法:

class Date {
public:
	friend ostream& operator<<(ostream& ou, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
private:
	int _year;
	int _month;
	int _day;
};
istream& operator>>(istream& in, Date& d) {
	in >> d._year >> d._month >> d._day;
	return in;
}

cin是全局的一个istream类类型的对象。

需要注意的是:输出时因为不需要改变d对象中的数据,因此可以考虑加上const修饰;而输入则要修改对象中的成员,因此不要加,对于coutcin则都不要加const,可以认为无论是输入还是输出,都会访问这两个对象中的成员然后做些修改,加了会出错。

6. const成员函数

在这里插入图片描述
用d2调用该成员函数会报错,是因为d2是const Date类型,那么调用成员函数时隐式生成的this指针也得是const Date*类型,因为权限可以缩小或者平移,而该函数实际生成的this指针的类型则为Date*,这样就造成了权限放大的问题,d2是只读的不能修改,但是this解引用后不仅可以读也可以写,这种情况是不允许的。
因此合理的做法是给不会修改成员变量的成员函数加上const来修饰,这样不仅非const对象能够调用,并且const对象也能调用。

由于this是编译器隐式传递的,因此没有办法给直接this加上const来修饰,所以要在成员函数的参数列表的括号后面加上const,这样this指针的类型就从Date*变为了const Date*

在这里插入图片描述
在这里插入图片描述

普通对象的this指针的类型写全了是Date* const this
如果是const对象为const Date* const this
第二个const修饰的是this指针本身不能被修改,始终指向调用这个函数的对象,不涉及对象成员权限的问题。

const修饰类成员函数,实际修饰该成员函数隐含的this指针指向的对象,表明在该成员函数中不能对类的任何成员进行修改

什么情况下可以给成员函数加上const修饰?

很明显,如果在函数内部不会对成员变量做任何修改,仅仅读取,这种情况下可以无脑加上const来修饰,否则不允许加const进行修饰

权限问题只存在于与指针和引用

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

这两个默认成员函数一般不用重新定义,编译器默认会生成,而且实际当中也不怎么能用到。

class Date
{
public:
	//普通对象取地址
	Date* operator&()
	{
		return this;
	}
	//const对象取地址
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; 
	int _month; 
	int _day; 
};
int main() {
	Date d1(2003, 9, 10);
	const Date d2(2023, 8, 1);
	cout << &d1 << endl;
	cout << &d2 << endl;
}

如果不想让别人访问到对象的实际地址,可以把返回值修改为空指针或者其它内容:

//普通对象取地址
Date* operator&()
{
	return nullptr;
}
//const对象取地址
const Date* operator&()const
{
	return nullptr;
}

注意:取地址运算符重载也是默认成员函数,只能定义在类中。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值