我们需要从从两方面入手: 第⼀:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
2. 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(局部对象在栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。 构造函数有如下特点: 1. 函数名与类型同名 2. 可以重载 3. 没有返回值(不用写void) 4. 如果用户没有显式写构造函数,编译器会生成一个默认的无参构造函数,一旦用户显式定义编译器将不再生成。
代码语言:javascript
AI代码解释
// 构造函数 // 1. 函数名和类名同名 2. 可以重载 3. 没有返回值 4. 用户不写编译器会默认生成无参的构造函数 class Date { public: // 无参构造 Date() { _year = 1; _month = 1; _day = 1; } // 带参数构造 Date(size_t year, size_t month, size_t day) { _year = year; _month = month; _day = day; } //// 全缺省构造 //Date(size_t year = 1, size_t month = 1, size_t day = 1) //{ // _year = year; // _month = month; // _day = day; //} // void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; }; int main() { // 调用带参数的构造 Date d1(2025,7,5); d1.Print(); //// 无参构造和全缺省构造会产生调用歧义 //Date d2; //d2.Print(); // 无参的不能这么写 会和函数声明搞混 eg: void func // 这是函数声明还是函数定义呢? /*Date d2(); d2.Print();*/ //// 如果注释掉无参的构造和全缺省构造,会报错 //// C2512 没有合适的默认构造函数可用 //Date d2; //d2.Print(); // 调用无参的构造函数 Date d3; d3.Print(); return 0; }默认构造函数分为三类:
- 全缺省构造函数
- 无参构造函数
- 编译器默认生成的构造函数总结一下:不传参的构造函数就是默认构造函数,这三个函数不能同时存在 而全缺省构造函数和无参构造函数虽然构成函数重载,但是调用时会产生调用歧义我们不显式写构造函数,编译器默认生成的构造函数会如何处理数据?
代码语言:javascript
AI代码解释
using namespace std; class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } private: size_t _hour; size_t _minute; size_t _second; }; class Date { public: // 不写构造函数 编译器会自动生成默认构造函数 // 对于内置类型 编译器是否处理没有明确要求 // 对于自定义类型 调用该类型的默认构造函数 void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; Time _t; }; int main() { Date d1; d1.Print(); return 0; }在这里插入图片描述
观察调试结果,我们可以得到如下结论: 对于编译器默认生成的构造函数,处理不同类型数据有不同行为:
- 对于内置类型,编译器没有特别要求,对于
VS环境,给出随机值 - 对于自定义类型,该类型会调用它默认的构造函数 如果把
Time类的无参构造函数注释掉,会有如下现象:
在这里插入图片描述
Time类调用它的默认构造函数,而Time类的默认构造函数是编译器生成的,又是处理内置类型,所以VS不做处理,给出随机值 针对这个问题C++11打了个补丁:内置类型成员变量在声明时给缺省值,用缺省值初始化
代码语言:javascript
AI代码解释
using namespace std; class Time { public: /*Time() { _hour = 1; _minute = 1; _second = 1; }*/ private: // C++11 在声明时给缺省值 size_t _hour = 1; size_t _minute = 1; size_t _second = 1; }; class Date { public: // 不写构造函数 编译器会自动生成默认构造函数 // 对于内置类型 编译器是否处理没有明确要求 // 对于自定义类型 调用该类型的默认构造函数 void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: // C++11 在声明时给缺省值 size_t _year = 1; size_t _month = 1; size_t _day = 1; Time _t; }; int main() { // 此时 Time类和Date类只有编译器默认生成的构造函数 Date d1; d1.Print(); return 0; }![[Pasted image 20250707095835.png]]
总结:什么时候要显式定义构造函数?
- 一般情况构造函数都要显式实现
- 只有成员全为自定义类型的类不用显式实现
3. 析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的 析构函数有如下特点: 1. 函数名和类名相同,在函数名前加~2. 没有返回值 3. 不能重载,意味着一个类只有一个析构函数 4. 如果用户没有显式写,编译器会默认生成析构函数 5. 对象的生命周期结束,编译器自动调用析构函数
代码语言:javascript
AI代码解释
class Stack { public: Stack(size_t n = 4) { cout << "Stack(size_t n = 4) 析构" << endl; _arr = (int*)malloc(sizeof(int) * n); if (_arr == nullptr) { perror("malloc err!"); return; } _capacity = n; _top = 0; } ~Stack() { cout << "~Stack() 析构" << endl; assert(_arr); free(_arr); _arr = nullptr; _capacity = _top = 0; } private: int* _arr; int _capacity; int _top; }; int main() { Stack st1; return 0; }和构造函数一样,如果我们不显式实现析构函数,编译器生成的析构函数对于内置类型不做处理,对于定义类型会调用它的析构函数,值得一提的是,是我们显式写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数
代码语言:javascript
AI代码解释
class tmp { public: ~tmp() { cout << "~tmp() 析构" << endl; } private: int _num; }; class Stack { public: Stack(size_t n = 4) { cout << "Stack(size_t n = 4) 构造" << endl; _arr = (int*)malloc(sizeof(int) * n); if (_arr == nullptr) { perror("malloc err!"); return; } _capacity = n; _top = 0; } /*~Stack() { cout << "~Stack() 析构" << endl; assert(_arr); free(_arr); _arr = nullptr; _capacity = _top = 0; }*/ private: int* _arr; int _capacity; int _top; tmp _t; }; int main() { Stack st1; return 0; }我们可以通过调试观察:
在这里插入图片描述
总结:什么时候需要显式实现析构函数?
- 有资源需要清理,就必须写析构函数,例如:
StackList… - 无资源要清理,可以不写
- 内置类型成员没有资源要清理,剩下全是自定义类型,可以不写 还有一个重要的点:一个局部域的多个对象,后定义的先析构
代码语言:javascript
AI代码解释
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( ) C c; int main() { A a; B b; static D d; return 0; }- 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意
static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象 - 全局对象先于局部对象进行构造
- 局部对象按照出现的顺序进行构造,无论是否为
static - 所以构造的顺序为
c a b d - 析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构
- 因此析构顺序为
B A D C
4. 拷贝构造函数
拷贝构造函数的第一个参数是自身类型的引用,且任何额外的参数都有缺省值,这样的函数叫做拷贝构造函数,用于同类对象的拷贝初始化,是构造函数的重载。 本文以最常规情况的拷贝构造函数展开,即有且仅有一个参数:类类型对象的引用拷贝构造函数有如下特点:
- 拷贝构造函数是构造函数的一个重载
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会报错(会引发无穷递归调用),拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值
代码语言:javascript
AI代码解释
// 拷贝构造函数 // 构造函数的重载,第一个参数必须是类类型对象的引用 // 用于同类对象的拷贝初始化 class Date { public: Date() { _year = 1; _month = 1; _day = 1; } Date(Date& d) { cout << "call Date(Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; }; int main() { Date d1; // 两种写法都可以 Date d2 = d1; // d是d1的别名,d3是this指针 Date d3(d1); d1.Print(); d2.Print(); d3.Print(); return 0; }再来看一段代码:
代码语言:javascript
AI代码解释
Date(Date& d) { cout << "call Date(Date& d)" << endl; // 如果不小心写反了会发生什么? d._year = _year; d._month = _month; d._day = _day; }其余部分不变
在这里插入图片描述
初始的d1也被修改成随机值了,我们进行拷贝构造,提供拷贝值的对象是不能被修改的,所以为了防止这样的情况发生,我们做如下处理:Date(const Date& d)保证d的只读性
代码语言:javascript
AI代码解释
// 拷贝构造函数 // 构造函数的重载,第一个参数必须是类类型对象的引用 // 用于同类对象的拷贝初始化 class Date { public: Date() { _year = 1; _month = 1; _day = 1; } Date(const Date& d) { cout << "call Date(Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; }; int main() { Date d1; // 两种写法都可以 Date d2 = d1; // d是d1的别名,d3是this指针 Date d3(d1); d1.Print(); d2.Print(); d3.Print(); return 0; }C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造
代码语言:javascript
AI代码解释
// 自定义类型对象进行拷贝行为必须调用拷贝构造 // 自定定义类型传值传参和传值返回都会调用拷贝构造完成 class Date { public: Date(size_t year, size_t month, size_t day) { _year = year; _month = month; _day = day; } Date(const Date& d) { cout << "调用 Date(const Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; }; void func(Date d) { d.Print(); } int main() { Date d1(2025, 7, 9); func(d1); return 0; }在这里插入图片描述
调试看一下函数行为:
而传指针和传引用可避免拷贝构造:
在这里插入图片描述
在这里插入图片描述
C++推荐使用传引用的方式,因为引用语义更清晰、不能为 null、更安全也更简洁,适合绝大多数函数参数传递场景,除非参数可以为空或需要修改指针本身,否则优先使用引用传参为什么传值会引发无穷递归?
在这里插入图片描述
- 如果不显式写拷贝构造,编译器会默认生成拷贝构造,⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷贝/浅拷贝:一个字节一个字节地拷贝),对自定义类型成员变量会调用它的拷贝构造
代码语言:javascript
AI代码解释
// 如果不显式写拷贝构造,编译器会默认生成拷贝构造, // 自动⽣成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝), // 对自定义类型成员变量会调用它的拷贝构造 class Stack { public: Stack(size_t n = 4) { // cout << "Stack(size_t n = 4) 构造" << endl; _arr = (int*)malloc(sizeof(int) * n); if (_arr == nullptr) { perror("malloc err!"); return; } _capacity = n; _top = 0; } void Push(int data) { _arr[_top++] = data; } ~Stack() { // cout << "~Stack() 析构" << endl; assert(_arr); free(_arr); _arr = nullptr; _capacity = _top = 0; } private: int* _arr; int _capacity; int _top; }; int main() { Stack st1; st1.Push(1); st1.Push(1); st1.Push(1); Stack st2 = st1; return 0; }在这里插入图片描述
崩溃了
在这里插入图片描述
完成了拷贝,程序却崩溃了,为什么?我们没有显式写拷贝构造,默认生成的拷贝构造调用栈对象的拷贝构造,进行了浅拷贝。
![[Pasted image 20250709174150.png]]
而深拷贝,会复制一个相同的空间,这样就不会冲突
![[Pasted image 20250709175310.png]]
代码语言:javascript
AI代码解释
class Stack { public: Stack(size_t n = 4) { // cout << "Stack(size_t n = 4) 构造" << endl; _arr = (int*)malloc(sizeof(int) * n); if (_arr == nullptr) { perror("malloc err!"); return; } _capacity = n; _top = 0; } // 深拷贝 // Stack st2 = st1; Stack(const Stack& st) { _arr = (int*)malloc(sizeof(int) * st._capacity); if (_arr == nullptr) { perror("malloc err!"); return; } memcpy(_arr, st._arr, sizeof(int)* st._capacity); _capacity = st._capacity; _top = st._top; } void Push(int data) { _arr[_top++] = data; } ~Stack() { // cout << "~Stack() 析构" << endl; assert(_arr); free(_arr); _arr = nullptr; _capacity = _top = 0; } private: int* _arr; int _capacity; int _top; }; int main() { Stack st1; st1.Push(1); st1.Push(1); st1.Push(1); Stack st2 = st1; return 0; }![[Pasted image 20250709175225.png]]
完成深拷贝,二者有自己的独立空间
总结:什么时候需要显式写拷贝构造?
- 若无资源管理,不用显式写拷贝构造,eg:
Date - 若类的成员变量都是自定义类型,并且内置类型的的成员没有指向的资源,不用显示写拷贝构造tips:不显式写析构,就不用写拷贝
- 若类内部有指针或者一些值指向资源,要写析构释放,就需要显式写深拷贝,eg:
Stack
5. 赋值重载
5.1. 运算符重载
代码语言:javascript
AI代码解释
int main() { int num1 = 10; int num2 = 30; int mul = num1 * num2; int plus = num1 + num2; return 0; }对于内置类型,使用操作符会调用具体的指令
在这里插入图片描述
而对于自定义类型,我们需要使用函数自己控制运算符的作用,这里就引入了运算符重载,格式如下:返回类型 operator操作符(参数列表),这就是函数,只不过函数名是operator操作符运算符重载有以下注意事项:
- 不能创造新的操作符,例如
C++不支持@这个符号 - 重载操作符必须有一个类类型的参数
void operator-(int a, int b)是不行的
代码语言:javascript
AI代码解释
// 参数类型必须要有一个是类类型 // “operator -”必须至少有一个类类型的形参 void operator-(int a, int b){}![[Pasted image 20250720145107.png]]
- 有五个操作符不能重载:
.* . sizeof ?: ::.*这个运算符的场景很少见:
代码语言:javascript
AI代码解释
// .*不能重载 class A { public: void func() { cout << "A::func()" << endl; } }; typedef void(A::* PF)(); //成员函数指针类型 int main() { // C++规定成员函数要加&才能取到函数指针 PF pf = &A::func; A obj;//定义ob类对象temp // 对象调⽤成员函数指针时,使⽤.*运算符 (obj.*pf)(); return 0; }我们看个案例:
代码语言:javascript
AI代码解释
// 自定义类型呢? class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } ~Date() { _year = -1; _month = -1; _day = -1; } // private: int _year; int _month; int _day; }; bool operator>(const Date& d1, const Date& d2) { if (d1._year > d2._year) { if (d1._month > d2._month) { if (d1._day > d2._day) { return true; } } } else { return false; } } int main() { Date d1(2025, 7, 17); Date d2(2025, 7, 20); // 转换成调用 operator>(d1, d2) cout << (d1 > d2) << endl; // 可以显式调用 cout << operator>(d1, d2) << endl; return 0; }不推荐显式调用,因为这样就突出不了运算符重载的优势了
![[Pasted image 20250720145708.png]]
注意看:private被我注释掉了,此时数据是公开访问的
![[Pasted image 20250720145837.png]]
而如果数据被private保护,我们如何重载运算符呢?这里提供三种方法
- 实现私有成员的
GetSet函数,Java使用这个方法偏多
代码语言:javascript
AI代码解释
//eg: int GetYear() { return _year; }- 使用友元函数(这个会在后文中详细讲)
- 重载成类的成员函数,这里要注意,重载成成员函数,会默认多一个
this,要调整参数个数
代码语言:javascript
AI代码解释
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } ~Date() { _year = -1; _month = -1; _day = -1; } bool operator>(const Date& d2) { if (this->_year > d2._year) { if (this->_month > d2._month) { if (this->_day > d2._day) { return true; } } } else { return false; } } private: int _year; int _month; int _day; }; int main() { Date d1(2025, 7, 17); Date d2(2025, 7, 20); // 转换成调用 operator>(d1, d2) cout << (d1 > d2) << endl; // 可以显式调用 cout << d1.operator>(d2) << endl; return 0; }5.2. 赋值运算符重载
赋值运算符重载用于一个已经存在的对象拷贝复制给另一个已经存在的对象,这里要和拷贝构造函数区分开。以下是赋值运算符重载的特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数,参数建议写成
const 当前类类型引用,否则会传值传参产生拷贝 - 有返回值,建议写成当前类类型引用,引用返回可以减少拷贝,提高效率,有返回值的目的是为了支持连续赋值的场景 对于内置类型:
在这里插入图片描述
而对于自定义类型:
代码语言:javascript
AI代码解释
class Date { public: // 构造 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } // 拷贝构造 Date(const Date& d) { cout << "Date(const Date & d)" << endl; _year = d._year; _month = d._month; _day = d._day; } // 赋值运算符重载 Date& operator=(const Date& d) { // 防止自己给自己赋值 if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } // 返回值是Date类 return *this; } // 析构 ~Date() { _year = _month = _day = -1; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2025, 7, 20); Date d2(2024, 7, 28); // 拷贝构造 Date d3 = d1; // 赋值运算符重载 d1 = d2 = d3; d1.Print(); d2.Print(); d3.Print(); }![[Pasted image 20250720152705.png]]
如果返回值为空,就会报错:
代码语言:javascript
AI代码解释
// 二元“=”: 没有找到接受“void”类型的右操作数的运算符(或没有可接受的转换)` d1 = d2 = d3;![[Pasted image 20250720152925.png]]
我们现在来着重讨论传值返回和传引用返回的区别:
代码语言:javascript
AI代码解释
// 传值返回(非优化版本,强制拷贝构造) Date func() { Date d1; Date d2; if (time(0) % 2) { return d1; // 编译器无法预测 取消RVO优化 } else { return d2; } } int main() { const Date& ref = func(); return 0; }![[Pasted image 20250720160957.png]]
看一下具体行为(非优化版本):
传值返回
画一个函数栈帧理解一下(优化版本):
代码语言:javascript
AI代码解释
┌──────────────────────────────┐ │ main() │ │ ┌──────────────────────────┐ │ │ │ ref = func() │ │ │ │ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ func() │ │ │ │ │ │ ┌───────────────┐ │ │ │ │ │ │ │ Date d; │ │ │ │ │ │ │ │ (构造#1) │ │ │ │ │ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌───────────────┐ │ │ │ │ │ │ │ return d; │ │ │ │ │ │ │ │ => 拷贝构造#2 │ │ │ │ │ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ d 析构 (析构#1) │ │ │ │ │ └─────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ const Date& ref │ │ │ │ │ │ 绑定返回值的临时 │ │ │ │ │ │ (有时会额外复制) │ │ │ │ │ │ => 析构#2, 析构#3 │ │ │ │ │ └──────────────────────┘ │ │ │ └──────────────────────────┘ │ └──────────────────────────────┘总结:
代码语言:javascript
AI代码解释
[func 调用栈] ┌───────────────┐ │ Date d; │ ← 局部变量 d │ │ │ return d; │ ← 拷贝构造!临时#1 └───────────────┘ │ ▼ [main 调用栈] ┌───────────────┐ │ const Date& ref; │ ← 绑定返回值临时 └───────────────┘ 析构: 局部 d 析构(离开 func) 返回值临时析构(ref 生命周期结束) 可能还有: 有的编译器生成额外延续临时,也会析构C++值返回需要拷贝,RVO 省掉拷贝;const& 会延续临时,多次析构是自然现象。d析构 1 次(func结束),return时临时值析构 1 次,main里ref绑定的延续临时析构 1 次
而使用引用返回会生成d1d2的别名,减少了拷贝
![[Pasted image 20250720162717.png]]
ref是tmp,是d1d2的别名 看一下具体行为:
传引用返回
分析:
代码语言:javascript
AI代码解释
+----------------------------+ | main() | | | | const Date& ref = func(); | | (悬空引用) | +----------------------------+ | ▼ +----------------------------+ | func() | | | | +----------------------+ | | | Date d1; | | | | (栈上局部变量) | | | +----------------------+ | | | | +----------------------+ | | | Date d2; | | | | (栈上局部变量) | | | +----------------------+ | | | | if (...) return d1; | | else return d2; | | | | 【func() 结束】 | | ├─ d1 析构 | | ├─ d2 析构 | +----------------------------+为什么没有拷贝?func的返回值是Date&(引用),不是按值返回。 所以return d1/return d2返回的就是局部变量本身的别名,不需要拷贝,也就不会走拷贝构造。为什么会析构两次?
d1和d2都是func的局部变量,存在func的栈帧里。if分支随机选择:return d1;时,main拿到d1的引用return d2;时,main拿到d2的引用 无论你拿到谁的引用,func一旦返回,d1和d2都会自动析构。 所以func结束时:d1会析构d2会析构
再加一段代码:
代码语言:javascript
AI代码解释
// 传引用返回 Date& func() { Date d1; Date d2; if (time(0) % 2) { return d1; // 编译器无法预测 取消优化 } else { return d2; } } int func1() { int a = 1; int b = 2; int c = 3; return a + b + c; } int main() { Date& ref = func(); ref.Print(); return 0; }此时输出结果为:
在这里插入图片描述
为什么?我们看下函数栈帧
代码语言:javascript
AI代码解释
Step 1: [func 栈帧] ┌───────────────┐ │ Date d1; │ │ Date d2; │ └───────────────┘ Step 2: return d1/d2 的地址 ----> 被 ref 接住 [main 栈帧] ┌───────────────┐ │ Date& ref ----┼────────┐ └───────────────┘ │ │ ▼ 指向 func 的局部变量 Step 3: func 返回后 --> func 栈帧销毁 --> d1/d2 内存仍然是旧的,但悬空 Step 4: 调用 func1() [func1 栈帧] ┌───────────────┐ │ int a; │ │ int b; │ │ int c; │ └───────────────┘ ⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️ 这里可能会复用func 原来的栈空间 Step 5: ref.Print() 访问到的其实是 a,b,c 占用的区域里的数据 ===> 输出被覆盖的脏数据C++函数如果返回局部变量引用,栈帧销毁后,这个引用就悬空。如果后面又有新的函数调用分配局部变量,就可能覆盖原来的内存区域。这会导致引用指向的内容被篡改,输出是垃圾值,属于未定义行为。总结: 在 C++ 里,传值返回和传引用返回的选择,核心看返回值要不要和原来的对象共享。 如果是局部变量或者新建对象,比如operator+,就必须传值返回,这样才能把局部结果安全拷贝或者移动出来。 如果是内部状态或者链式调用,比如operator=或vector::operator[],就传引用返回,这样可以直接在原对象上操作,省掉拷贝。 唯一需要注意的是局部变量绝不能传引用返回,不然栈帧一结束,引用就悬空了,行为是未定义的。 另外现代编译器对值返回会做 RVO 优化,很多时候根本不会产生拷贝开销。
6. 取地址重载
6.1const成员函数
- 将
const修饰的成员函数称作const成员函数,const放到成员函数参数列表的后面 const修饰该成员函数的this指针,表明该成员函数不能对类的任何成员进行修改操作,eg:Date* const this变为const Date* const this
代码语言:javascript
AI代码解释
// const成员函数 class Date { public: Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) { // ... } // Date* const this -> const Date* const this void Print()const { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { // d1可以调用Print 权限的缩小 Date d1(2025, 8, 13); d1.Print(); // 权限的平移 const Date d2(2025, 8, 14); d2.Print(); return 0; }6.2 取地址运算符重载
取地址运算符重载分为普通和const,一般这两个函数编译器自动生成的就够用了,不用显式实现,但在一些特殊场景,例如我们不想让别人取到当前类对象的地址,就可以自己实现,返回一个虚拟地址
代码语言:javascript
AI代码解释
// 取地址运算符重载 class show { public: // 非const对象的取地址运算符重载 show* operator&() { // 返回当前对象的地址 return this; } // 构造 show(int num = 0, char ch = '\0') : _num(num), _ch(ch) { // ... } // const对象的取地址运算符重载 const show* operator&() const { // 返回空指针作为示例,实际中应根据需求返回有意义的地址 return nullptr; } private: int _num; char _ch; }; int main() { show obj1; const show obj2; std::cout << "非const对象的地址: " << &obj1 << std::endl; std::cout << "const对象的地址: " << &obj2 << std::endl; return 0; }