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++引用深度详解中的权限来理解这个问题:
首先回顾一下:
- 权限不能放大。
- 权限可以平移。
- 权限可以缩小。
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 = &d1
,d1的地址类型是Date*
权限可读可写,是权限的平移,正确。 - d2调用Print()时,有
Date* this = &d2
而d2的地址类型是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;
}
思考?
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非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 地址获取流程图
4. 应用场景与注意事项
4.1 典型应用场景
- 调试追踪:记录取地址操作日志
Date* operator&() { cout << "对象地址被获取:" << this << endl; return this; }
- 地址伪装:返回特定地址
Date* operator&() { return &this->_year; // 返回成员变量地址 }
4.2 重要注意事项
- 返回有效性:必须返回合法地址
- 避免循环调用:禁用
return &(*this)
- const正确性:严格区分const版本
- 内存安全:返回栈地址需谨慎
文章到此结束啦,欢迎各位大佬在评论区讨论交流。至此,我们类中的六个默认成员函数已经全部介绍完啦!如果觉得文章写的不错,还请留下免费的赞和收藏!