C++类和对象进阶:const及取地址重载深度解析

前言

在这里插入图片描述
本文将介绍const成员函数和C++6个默认成员函数中的最后两个,取地址重载操作符重载和const取地址重载。


const对象问题引入

有如下Date类及程序

class Date {
    
    
public:
	Date(int year = 2025, int month = 2, int day = 22) {
    
    
		//检查数据的合理性
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print() {
    
    
		cout << this->_day << "--" << this->_month << "--" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main(){
    
    
	Date d1(2025, 2, 14);
	d1.Print();
	
	const Date d2(2024, 2, 14);	
	d2.Print();	
}

以上程序会报错:
在这里插入图片描述
可以看到,d2.Print()调用处报错,这是为什么呢?
难道const对象就不能调用类内的成员函数了吗?
原因是:

参数类型不匹配!!!(权限不匹配)


解决const对象问题

问题的根源是形参this和实参的类型不匹配

对象调用成员函数的实质

int main(){
    
    
	Date d1(2025, 2, 14);
	d1.Print();	//会被转换成 d1.Print(&d1);
	
	const Date d2(2024, 2, 14);	
	d2.Print();	//会被转换成 d2.Print(&d2);
}

对象调用函数的实质:
根据往期文章中对this指针的讲解,我们得知对象调用成员函数,本质上会将当前对象的地址传入给形参this。则对象调用函数可以理解为:

  • d1.Print()被转化为 d1.Print(&d1),这层转化工作是由编译器来完成的。
  • d2.Print()被转化为 d2.Print(&d2),这层转化工作是由编译器来完成的。

既然是传参,我们就需要形参和实参的类型匹配。
我们需要明晰以下概念:

  • Date d1(2025, 2, 14),d1的类型是Date,那么d1的地址的类型就是Date*,即,Date* pDate2 = &d1
  • const Date d2(2024, 2, 14),d2的类型是const Date,那么d2的地址的类型就是const Date*,即,const Date* pDate2 = &d2

this指针的类型

我们得知对象调用成员函数,本质上会将当前对象的地址传入给形参this,且this指针的类型是Date* const this(在当前Date类的情形下) ,

Date* const this,const修饰this,表示this不可修改,即当前对象的地址不能通过this修改。而我们的代码中是const Date d2(2024, 2, 14)const修饰的是d2,表示d2的内容不可修改,与d2的地址无关。因此,为了便于理解,我们在传参过程的分析中,认为this的类型是Date*


传参过程

从类型不匹配的角度来理解:

实际上 Print() 函数原型为 Print(Date* this), 隐含的this指针类型是 Date*,也就是指向非const Date对象的指针。
传参时有:

  • d1.Print() ------》 Date* this = &d1;
  • d2.Print() ------》 Date* this = &d2; 而d2的地址是const Date* 类型的,因此类型不匹配

从权限改变的角度来理解:

用往期文章C++引用深度详解中的权限来理解这个问题:
首先回顾一下:

  1. 权限不能放大
  2. 权限可以平移
  3. 权限可以缩小
  • d1.Print()中,this指针的类型是Date*d1的权限是可读可写。即:可以访问,可以修改
  • d2.Print()中,this指针的类型是Date*,而const Date d2中,d2的权限是只可访问不可修改。即:可以访问,不可修改
int main(){
    
    
	Date d1(2025, 2, 14);
	d1.Print();	//会被转换成 d1.Print(&d1);
	
	const Date d2(2024, 2, 14);	
	d2.Print();	//会被转换成 d2.Print(&d2);
}
  • d1调用Print()时,有Date* this = &d1d1的地址类型是Date*权限可读可写,是权限的平移,正确。
  • d2调用Print()时,有Date* this = &d2d2的地址类型是const Date*权限只可读,不可修改,是权限的放大,错误。

因此会报错。


一些权限问题:

在这里插入图片描述


解决方案

既然权限不能放大,但可以平移和缩小。那么我们自然地可以想到,在this指针的前面加上const不就可以了吗?

int main(){
    
    
	Date d1(2025, 2, 14);
	d1.Print();	//会被转换成 d1.Print(&d1);
	
	const Date d2(2024, 2, 14);	
	d2.Print();	//会被转换成 d2.Print(&d2);
}

如果this指针前加了const,则有:

  • const Date* this = &d1&d1的类型是Date*,从Date* const Date*,是权限的缩小,正确。
  • const Date* this = &d2&d2的类型是const Date*,从const Date* const Date*,是权限的平移,正确。

那么如何在this前加上const呢?
C++有如下规定:

  • 将const修饰的“成员函数”称之为const成员函数
  • const修饰类成员函数,实际上是修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
    在这里插入图片描述

修改后的Date类

class Date {
    
    
public:
	Date(int year = 2025, int month = 2, int day = 22) {
    
    
		//检查数据的合理性
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print() const {
    
    
		cout << this->_day << "--" << this->_month << "--" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

在成员函数的()后面加上 const,实际上是修饰该成员函数隐含的this指针。成员函数加上const,在Date类中,this指针的类型从Date*变成了const Date*

因此,使用如上解决方案后,const修饰的对象和非const修饰的对象就都可以调用成员函数了

  • 注意:如果const成员函数的声明和定义分离,成员函数的声明和定义都应该加上const
    例如:
class Date {
    
    
public:
	Date(int year = 2025, int month = 2, int day = 22) {
    
    
		//检查数据的合理性
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print() const;
	
private:
	int _year;
	int _month;
	int _day;
};

void Date::Print() const {
    
    
		cout << this->_day << "--" << this->_month << "--" << this->_day << endl;
	}

思考?

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其它的非const成员函数吗?
  4. 非const成员函数内可以调用其它的const成员函数吗?

解答:
在这里插入图片描述


结论

  • 一方面:const成员函数可以使const对象和非const对象都能调用类的成员函数
  • 另一方面:const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
  • const Date* this = &d1; 只会限制通过 this 指针修改 d1 的内容,不会影响 d1 自身的可修改性

能不能所有的成员函数都加const?
不能!

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

取地址重载和const取地址重载

在C++类设计中,取地址运算符(&)的重载及其const版本是两个特殊的默认成员函数。下文将解析其工作原理,并通过代码示例演示如何自定义这些运算符,探讨它们的应用场景和注意事项。
接下来介绍类中的最后两个默认成员函数,取地址重载const取地址重载

1. 默认的&重载

观察如下程序

class Date {
    
    
private:
	int _year = 2025;
	int _month = 2;
	int _day = 17;
};
int main() {
    
    
	Date d1;
	const Date d2;	//const类型对象声明时必须初始化,因为只有这一次的初始化机会
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

在这里插入图片描述
可以看到,&操作符是内置类型的取地址操作符,但这里也对对象d1和d2进行了取地址,原因是什么?
C++规定自定义类型使用内置类型的操作符时,必须要重载相应的操作符。
这里其实是编译器自动生成了两个操作符重载函数。
当对Date对象取地址时,编译器生成的默认运算符仍能正常工作,验证了默认机制的存在。

Date* operator&() {
    
    
		return this;
	}
	const Date* operator&() const {
    
    
		return this;
	}

对于任意自定义类型,当我们使用取地址运算符&时,即使没有显式定义,编译器也会自动生成两个重载版本:

// 非const对象版本
ClassName* operator&() {
    
     return this; }

// const对象版本
const ClassName* operator&() const {
    
     return this; }

2. 自己实现&的重载

class Date {
    
    
public:
	//这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
	//这两个运算符一般不需要自己实现重载,使用编译器生成的默认取地址的重载即可,
	Date* operator&() {
    
    
		cout << "Date* operator&()" << endl;
		return this;
		//return nullptr;
		// 只有特殊情况,才需要重载。
		//比如不想让别人获取到可修改对象的地址
	}
	//两个函数构成函数重载(参数 this 类型不同)
	//Date* const this 和 const Date* const this
	const Date* operator&() const {
    
    
		cout << "const Date* operator&() const" << endl;
		return this;
	}
private:
	int _year = 2025;
	int _month = 2;
	int _day = 17;
};
int main() {
    
    
	Date d1;
	const Date d2;	//const类型对象声明时必须初始化,因为只有这一次的初始化机会
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

2.1 关键特性解析

特性 非const版本 const版本
返回类型 Date* const Date*
this指针类型 Date* const const Date* const
调用对象 非const对象 const对象
修改权限 可修改成员 不可修改成员

3. 运行机制深度剖析

3.1 重载决策规则

Date d1;
const Date d2;

cout << &d1; // 调用非const版本
cout << &d2; // 调用const版本

3.2 地址获取流程图

对象取地址
对象是否const?
调用const operator&
调用非const operator&
返回const指针
返回普通指针

4. 应用场景与注意事项

4.1 典型应用场景

  1. 调试追踪:记录取地址操作日志
    Date* operator&() {
          
          
        cout << "对象地址被获取:" << this << endl;
        return this;
    }
    
  2. 地址伪装:返回特定地址
    Date* operator&() {
          
          
        return &this->_year; // 返回成员变量地址
    }
    

4.2 重要注意事项

  1. 返回有效性:必须返回合法地址
  2. 避免循环调用:禁用return &(*this)
  3. const正确性:严格区分const版本
  4. 内存安全:返回栈地址需谨慎

文章到此结束啦,欢迎各位大佬在评论区讨论交流。至此,我们类中的六个默认成员函数已经全部介绍完啦!如果觉得文章写的不错,还请留下免费的赞和收藏

猜你喜欢

转载自blog.csdn.net/2301_80064645/article/details/145669098
今日推荐