C++六大默认成员函数

发布时间:2026/7/24 20:09:03

C++六大默认成员函数 我们需要从从两方面入手 第⼀我们不写时编译器默认生成的函数行为是什么是否满足我们的需求。 第二编译器默认生成的函数不满足我们的需求我们需要自己实现那么如何自己实现2. 构造函数构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象(局部对象在栈帧创建时空间就开好了)而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能构造函数自动调用的特点就完美的替代的了Init。 构造函数有如下特点 1. 函数名与类型同名 2. 可以重载 3. 没有返回值不用写void 4. 如果用户没有显式写构造函数编译器会生成一个默认的无参构造函数一旦用户显式定义编译器将不再生成。代码语言javascriptAI代码解释// 构造函数 // 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; }默认构造函数分为三类全缺省构造函数无参构造函数编译器默认生成的构造函数总结一下不传参的构造函数就是默认构造函数这三个函数不能同时存在 而全缺省构造函数和无参构造函数虽然构成函数重载但是调用时会产生调用歧义我们不显式写构造函数编译器默认生成的构造函数会如何处理数据代码语言javascriptAI代码解释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不做处理给出随机值 针对这个问题C11打了个补丁内置类型成员变量在声明时给缺省值用缺省值初始化代码语言javascriptAI代码解释using namespace std; class Time { public: /*Time() { _hour 1; _minute 1; _second 1; }*/ private: // C11 在声明时给缺省值 size_t _hour 1; size_t _minute 1; size_t _second 1; }; class Date { public: // 不写构造函数 编译器会自动生成默认构造函数 // 对于内置类型 编译器是否处理没有明确要求 // 对于自定义类型 调用该类型的默认构造函数 void Print() { cout _year - _month - _day endl; } private: // C11 在声明时给缺省值 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. 对象的生命周期结束编译器自动调用析构函数代码语言javascriptAI代码解释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; }和构造函数一样如果我们不显式实现析构函数编译器生成的析构函数对于内置类型不做处理对于定义类型会调用它的析构函数值得一提的是是我们显式写析构函数对于自定义类型成员也会调用他的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数代码语言javascriptAI代码解释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…无资源要清理可以不写内置类型成员没有资源要清理剩下全是自定义类型可以不写 还有一个重要的点一个局部域的多个对象后定义的先析构代码语言javascriptAI代码解释设已经有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 C4. 拷贝构造函数拷贝构造函数的第一个参数是自身类型的引用且任何额外的参数都有缺省值这样的函数叫做拷贝构造函数用于同类对象的拷贝初始化是构造函数的重载。 本文以最常规情况的拷贝构造函数展开即有且仅有一个参数类类型对象的引用拷贝构造函数有如下特点拷贝构造函数是构造函数的一个重载拷贝构造函数的第一个参数必须是类类型对象的引用使用传值方式编译器会报错会引发无穷递归调用拷贝构造函数也可以多个参数但是第⼀个参数必须是类类型对象的引用后面的参数必须有缺省值代码语言javascriptAI代码解释// 拷贝构造函数 // 构造函数的重载第一个参数必须是类类型对象的引用 // 用于同类对象的拷贝初始化 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; }再来看一段代码代码语言javascriptAI代码解释Date(Date d) { cout call Date(Date d) endl; // 如果不小心写反了会发生什么 d._year _year; d._month _month; d._day _day; }其余部分不变在这里插入图片描述初始的d1也被修改成随机值了我们进行拷贝构造提供拷贝值的对象是不能被修改的所以为了防止这样的情况发生我们做如下处理Date(const Date d)保证d的只读性代码语言javascriptAI代码解释// 拷贝构造函数 // 构造函数的重载第一个参数必须是类类型对象的引用 // 用于同类对象的拷贝初始化 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规定自定义类型对象进行拷贝行为必须调用拷贝构造所以自定义类型传值传参和传值返回都会调用拷贝构造代码语言javascriptAI代码解释// 自定义类型对象进行拷贝行为必须调用拷贝构造 // 自定定义类型传值传参和传值返回都会调用拷贝构造完成 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、更安全也更简洁适合绝大多数函数参数传递场景除非参数可以为空或需要修改指针本身否则优先使用引用传参为什么传值会引发无穷递归在这里插入图片描述如果不显式写拷贝构造编译器会默认生成拷贝构造⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷贝/浅拷贝一个字节一个字节地拷贝)对自定义类型成员变量会调用它的拷贝构造代码语言javascriptAI代码解释// 如果不显式写拷贝构造编译器会默认生成拷贝构造 // 自动⽣成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝) // 对自定义类型成员变量会调用它的拷贝构造 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]]代码语言javascriptAI代码解释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]]完成深拷贝二者有自己的独立空间总结什么时候需要显式写拷贝构造若无资源管理不用显式写拷贝构造egDate若类的成员变量都是自定义类型并且内置类型的的成员没有指向的资源不用显示写拷贝构造tips不显式写析构就不用写拷贝若类内部有指针或者一些值指向资源要写析构释放就需要显式写深拷贝egStack5. 赋值重载5.1. 运算符重载代码语言javascriptAI代码解释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)是不行的代码语言javascriptAI代码解释// 参数类型必须要有一个是类类型 // “operator -”必须至少有一个类类型的形参 void operator-(int a, int b){}![[Pasted image 20250720145107.png]]有五个操作符不能重载.* . sizeof ?: ::.*这个运算符的场景很少见代码语言javascriptAI代码解释// .*不能重载 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; }我们看个案例代码语言javascriptAI代码解释// 自定义类型呢 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使用这个方法偏多代码语言javascriptAI代码解释//eg: int GetYear() { return _year; }使用友元函数这个会在后文中详细讲重载成类的成员函数这里要注意重载成成员函数会默认多一个this要调整参数个数代码语言javascriptAI代码解释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 当前类类型引用否则会传值传参产生拷贝有返回值建议写成当前类类型引用引用返回可以减少拷贝提高效率有返回值的目的是为了支持连续赋值的场景 对于内置类型在这里插入图片描述而对于自定义类型代码语言javascriptAI代码解释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]]如果返回值为空就会报错代码语言javascriptAI代码解释// 二元“”: 没有找到接受“void”类型的右操作数的运算符(或没有可接受的转换) d1 d2 d3;![[Pasted image 20250720152925.png]]我们现在来着重讨论传值返回和传引用返回的区别代码语言javascriptAI代码解释// 传值返回非优化版本强制拷贝构造 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]]看一下具体行为非优化版本传值返回画一个函数栈帧理解一下(优化版本)代码语言javascriptAI代码解释┌──────────────────────────────┐ │ main() │ │ ┌──────────────────────────┐ │ │ │ ref func() │ │ │ │ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ func() │ │ │ │ │ │ ┌───────────────┐ │ │ │ │ │ │ │ Date d; │ │ │ │ │ │ │ │ (构造#1 │ │ │ │ │ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌───────────────┐ │ │ │ │ │ │ │ return d; │ │ │ │ │ │ │ │ 拷贝构造#2 │ │ │ │ │ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ d 析构 (析构#1) │ │ │ │ │ └─────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ const Date ref │ │ │ │ │ │ 绑定返回值的临时 │ │ │ │ │ │ 有时会额外复制 │ │ │ │ │ │ 析构#2, 析构#3 │ │ │ │ │ └──────────────────────┘ │ │ │ └──────────────────────────┘ │ └──────────────────────────────┘总结代码语言javascriptAI代码解释[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的别名 看一下具体行为传引用返回分析代码语言javascriptAI代码解释---------------------------- | 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会析构再加一段代码代码语言javascriptAI代码解释// 传引用返回 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; }此时输出结果为在这里插入图片描述为什么我们看下函数栈帧代码语言javascriptAI代码解释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指针表明该成员函数不能对类的任何成员进行修改操作egDate* const this变为const Date* const this代码语言javascriptAI代码解释// 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一般这两个函数编译器自动生成的就够用了不用显式实现但在一些特殊场景例如我们不想让别人取到当前类对象的地址就可以自己实现返回一个虚拟地址代码语言javascriptAI代码解释// 取地址运算符重载 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; }

相关新闻