一.类的默认成员函数与构造函数1.默认成员函数默认成员函数是指如果你没有显式定义编译器会为你的类自动生成的函数⼀个类我 们不写的情况下编译器会默认⽣成以下6个默认成员函数需要注意的是这6个中最重要的是前4个最后两个取地址重载不重要我们稍微了解⼀下即可。。其次就是C11以后还会增加两个默认成员函数移动构造和移动赋值这个以后再讲解。默认成员函数很重要也⽐较复杂我们要从两个⽅⾯ 去学习• 第⼀我们不写时编译器默认⽣成的函数⾏为是什么是否满⾜我们的需求。• 第⼆编译器默认⽣成的函数不满⾜我们的需求我们需要⾃⼰实现那么如何⾃⼰实现2.构造函数2.1.概念构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时空间就开好了)⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能构造函数⾃动调⽤的特点就完美的替代的了Init。2.2.特点1.函数名与类名完全相同。2.⽆返回值。(返回值啥都不需要给也不需要写voidC规定如此)3.对象实例化时系统会⾃动调⽤对应的构造函数4.构造函数可以重载例如#include iostream using namespace std; class Date { public: // 特点 1函数名 Date 与类名 Date 完全相同 // 特点 2无返回值前面既不写 int 也不写 void // 构造函数重载示例 1三个参数 Date(int year, int month, int day) { _year year; _month month; _day day; cout 调用了 [三参数构造] endl; } // 特点 4构造函数可以重载 // 构造函数重载示例 2无参数默认构造 Date() { _year 1; _month 1; _day 1; cout 调用了 [无参构造] endl; } void Print_Date() { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { // 特点 3对象实例化时系统会自动调用对应的构造函数 // 实例化 d1系统自动匹配 [无参构造] Date d1; d1.Print_Date(); // 实例化 d2系统自动匹配 [三参数构造] Date d2(2026, 1, 28); d2.Print_Date(); return 0; }输出结果5.如果类中没有显式定义构造函数则C编译器会⾃动⽣成⼀个⽆参的默认构造函数⼀旦⽤⼾显 式定义编译器将不再⽣成。例如#include iostream using namespace std; class Date { public: // Date(int year, int month, int day) // { // _year year; // _month month; // _day day; // } void Print_Date() { cout_year/_month/_dayendl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print_Date(); return 0; }此时我们将构造函数注释一下在观察一下结果可以看出这个值是个无用的值。6.无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数但是这三个函数有且只有⼀个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但是调用时会存在歧义总结⼀下就是不传实参就可以调用的构造就叫默认构造。注意我们不写编译器默认⽣成的构造对内置类型成员变量的初始化没有要求也就是说是否初始化是不确定的看编译器。对于自定义类型成员变量要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数那么就会报错我们要初始化这个成员变量需要用初始化列表才能解决我们下一篇再讲解。二.析构函数与拷贝构造函数1.析构函数1.1.概念析构函数与构造函数功能相反析构函数不是完成对对象本⾝的销毁比如局部对象是存在栈帧的 函数结束栈帧销毁他就释放了不需要我们管C规定对象在销毁时会自动调⽤析构函数完成对象中资源的清理释放⼯作。析构函数的功能与我们之前Stack实现的Destroy功能一样但是像Date没有资源需要释放所以Date是不需要析构函数的。那什么是析构函数中的资源的清理其实就是堆资源与系统资源如果不手动清理就会发生内存泄露。1.2.特点1.析构函数名是在类名前加上字符~。2.无参数无返回值(这⾥跟构造类似也不需要加void)。3.⼀个类只能有⼀个析构函数。若未显式定义系统会⾃动⽣成默认的析构函数4.对象⽣命周期结束时系统会⾃动调⽤析构函数。5.跟构造函数类似我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理⾃定义类型成员会调用他的析构函数。6.还需要注意的是我们显⽰写析构函数对于⾃定义类型成员也会调⽤他的析构也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数。7.如果类中没有申请资源时析构函数可以不写直接使⽤编译器⽣成的默认析构函数如Date如 果默认⽣成的析构就可以⽤也就不需要显⽰写析构如MyQueue但是有资源申请时⼀定要自己写析构否则会造成资源泄漏如Stack。8.⼀个局部域的多个对象C规定后定义的先析构。例如#include iostream using namespace std; typedef int STDataType; class Stack { public: Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); } _capacity n; _top 0; } //无参数无返回值 //一个类只能有一个析构函数 ~Stack() { // 对象生命周期结束时系统会自动调这个函数 //释放资源把 malloc 申请的内存还给系统 free(_a); //指针置空防止它变成野指针数据归零 _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; int main() { Stack st; return 0; }输出结果2.拷贝构造函数2.1.概念如果⼀个构造函数的第⼀个参数是自身类类型的引用且任何额外的参数都有默认值利用一个已经存在的对象去初始化另一个同类型的新对象。2.2.特点1拷贝构造函数是构造函数的⼀个重载。2.拷贝构造函数的第⼀个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值。如果第⼀个参数不是类类型对象的引用呢例如#include iostream using namespace std; class Date { public: //构造函数 Date(int year,int month,int day) { _yearyear; _monthmonth; _dayday; } //拷贝构造函数 Date(Date d) { _yeard._year; _monthd._month; _dayd._day; } //打印日期 void Print_Date() { cout_year/_month/_dayendl; } //无资源释放就无需析构函数。 private: int _year; int _month; int _day; }; int main() { Date d1(2026,1,30); d1.Print_Date(); Date d2(d1); d2.Print_Date(); return 0; }此时这串代码会报错因为它会引发无限递归。原因触发拷贝执行时编译器发现你需要调用拷贝构造函数。Date d2(d1);准备参数因为你的参数定义是 传值根据 C 规则“传值”意味着要创建一个形参的临时副本Date d。再次拷贝为了创建这个“临时副本”程序必须再次调用拷贝构造函数。死循环开始为了调用拷贝构造需要传值为了传值需要先拷贝为了拷贝又要调用拷贝构造…如图#include iostream using namespace std; class Date { public: //构造函数 Date(int year,int month,int day) { _yearyear; _monthmonth; _dayday; } //拷贝构造函数,用const防止修改实参的值 Date(const Date d) { _yeard._year; _monthd._month; _dayd._day; } //打印日期 void Print_Date() { cout_year/_month/_dayendl; } //无资源释放就无需析构函数。 private: int _year; int _month; int _day; }; int main() { Date d1(2026,1,30); d1.Print_Date(); Date d2(d1); //Date d2d1;拷贝构造时也可以这样写 d2.Print_Date(); return 0; }输出结果3.C规定自定义类型对象进行拷贝行为必须调⽤拷贝构造所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。4.若未显式定义拷贝构造编译器会生成自动生成拷贝构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造如果没有编译器就会自动生成拷贝构造函数。注意值拷贝/浅拷贝对内置类型成员变量没有大影响只是完成值拷贝但是如果内置类型中存在指针则会复制地址导致两个指针指向同一块堆内存例如#includeiostream using namespace std; typedef int STDataType; class Stack { public: //构造函数 Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } void Push(STDataType x) { if (_top _capacity) { int newcapacity _capacity * 2; STDataType* tmp (STDataType*)realloc(_a, newcapacity *sizeof(STDataType)); if (tmp NULL) { perror(realloc fail); return; } _a tmp; _capacity newcapacity; } _a[_top] x; } ~Stack() { cout ~Stack() endl; free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; int main() { Stack st1; st1.Push(1); st1.Push(2); Stack st2 st1; return 0; }在vs2022的监视窗口的结果此时编译器就会自动生成的拷贝构造函数可能会导致st1与st2都指向同一块空间此时就会发生危害1.如果st2修改栈里面的数据st1栈里面的数据也会变。2.当函数结束时 st1进行析构 时地址被还给了操作系统紧接着 st2去执行析构但此时指向的地址已经是无效的了相当于双重释放空间导致崩溃。所以6.像Date这样的类成员变量全是内置类型且没有指向什么资源编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉所以不需要我们显⽰实现拷⻉构造。像Stack这样的类虽然也都是内置类型但是_a指向了资源编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。义类型 Stack成员编译器⾃动⽣成的这⾥还有⼀个小技巧如果⼀个类显⽰实现了析构并释放资源那么他就需要显示写拷⻉构造否则就不需要。所以应给上面的代码添加一个拷贝构造函数//拷贝构造函数 Stack(const Stack st) { _a (STDataType*)malloc(sizeof(STDataType) * st._capacity); if (nullptr _a) { perror(malloc申请空间失败!!!); return; } memcpy(_a, st._a, sizeof(STDataType) * st._top); _top st._top; _capacity st._capacity; }此时在vs2022的监视窗口的结果7.传值返回会产⽣⼀个临时对象调⽤拷⻉构造传值引⽤返回返回的是返回对象的别名(引⽤)没 有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象函数结束就销毁了那么使⽤ 引⽤返回是有问题的这时的引⽤相当于⼀个野引⽤类似⼀个野指针⼀样。传引⽤返回可以减少 拷⻉但是⼀定要确保返回对象在当前函数结束后还在才能⽤引⽤返回。例如#includeiostream using namespace std; int GetNumber() { int a 10; // 局部对象存在于栈上 return a; // 警告返回局部变量的引用 } int main() { int res GetNumber(); // 函数结束a 的内存被系统收回 // res 此时指向一块已经不属于它的内存野引用 cout res endl; // 结果可能是 10也可能是随机值甚至直接程序崩溃 }三.赋值运算符重载1.运算符重载1.1.概念当运算符被⽤于类类型的对象时C语⾔允许我们通过运算符重载的形式指定新的含义。C规定类类型对象使⽤运算符时必须转换成调⽤对应运算符重载若没有对应的运算符重载则会编译报错。1.2.特点1.运算符重载是具有特殊名字的函数他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样它也具有其返回类型和参数列表以及函数体。例如void operator(const Dated) { //执行代码 ...... }2.重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数⼆元 运算符有两个参数⼆元运算符的左侧运算对象传给第⼀个参数右侧运算对象传给第⼆个参数。3.如果⼀个重载运算符函数是成员函数则它的第⼀个运算对象默认传给隐式的this指针因此运算 符重载作为成员函数时参数⽐运算对象少⼀个。例如#includeiostream using namespace std; class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // --- 重点 1运算符重载函数 --- // 编译器看到 d1 d2 时会自动转换成 d1.operator(d2) // bool 是返回值类型因为逻辑比较只有 真/假 bool operator(const Date d2) { // --- 重点 2这里的 _year 到底是谁的 --- // 成员函数内隐藏了一个 this 指针。 // _year 其实就是 this-_year (左操作数 d1 的成员) // d2._year 就是右操作数 (d2 的成员) return _year d2._year _month d2._month _day d2._day; } // --- 重点 3参数为什么用 const 和 --- // (引用)直接操作 d2不产生临时副本省去了调用“拷贝构造函数”的时间和空间开销。 // const告诉编译器我只是比较一下 d2 的内容绝对不会去修改 d2。 void Print() { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 1, 30); Date d2(2026, 2, 27); // --- 重点 4调用方式 --- // 方式 A (最常用)像内置类型一样自然 if (!(d1 d2)) { cout 方式 A: 不相等 endl; } // 方式 B (本质)像调用普通成员函数一样 // 这行代码和上面的 d1 d2 效果完全等价 if (!(d1.operator(d2))) { cout 方式 B: 本质是函数调用 endl; } return 0; }d1.operatord2像调用普通成员函数一样,而d1d2相当于直接判断比较建议用这个。4.运算符重载以后其优先级和结合性与对应的内置类型运算符保持⼀致。5.不能通过连接语法中没有的符号来创建新的操作符⽐如operator。6..*::sizeof?:.注意以上5个运算符不能重载。7.重载操作符⾄少有⼀个类类型参数不能通过运算符重载改变内置类型对象的含义。8.重载运算符时有前置和后置运算符重载函数名都是operator⽆法很好的区分。 C规定后置重载时增加⼀个int形参跟前置构成函数重载⽅便区分。例如//前置返回值返回增加后的自己引用无传值拷贝效率高 Date operator() { _day 1; //简单加1 return *this; } // 后置返回值返回增加前的旧值传值返回会产生拷贝开销 Date operator(int) { Date temp*this; // 1. 先记录当前的旧值拷贝 _day 1; // 2. 修改自己 return temp; // 3. 返回刚才记录的旧值 }9.重载时需要重载为全局函数因为重载为成员函数this指针默认抢占了第⼀个形参位 置第⼀个形参位置是左侧运算对象调⽤时就变成了对象cout不符合使⽤习惯和可读性但是重载为全局函数把ostream/istream放到第⼀个形参位置就可以了第⼆个形参位置当类类型对象但是要用友元函数声明一下。istream operator(istreamin,const Dated) { ind._yeard._monthd._day; return in; } ostream operator(ostreamout,const Dated) { outd._year/d._month/d.dayendl; return out; }返回in或out的引用原因编译器在处理 时实际上是从左向右分步执行的cout d1 d2;先执行operator(cout, d1);这个函数执行完后会返回out即cout的引用;此时原来的表达式就变成了 。(返回的cout) d2;继续执行 。2.赋值运算符重载2.1.概念就是用于完成两个已经存在的对象直接的拷贝赋值而拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。2.2.特点1.赋值运算符重载是⼀个运算符重载规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引⽤否则会传值传参会有拷⻉。2.有返回值且建议写成当前类类型引⽤引⽤返回可以提⾼效率有返回值⽬的是为了⽀持连续赋值场景。3.没有显式实现时编译器会⾃动⽣成⼀个默认赋值运算符重载默认赋值运算符重载⾏为跟默认拷贝构造函数类似对内置类型成员变量会完成值拷贝/浅拷⻉(⼀个字节⼀个字节的拷⻉)对⾃定义 类型成员变量会调⽤他的赋值重载函数。4.像Date这样的类成员变量全是内置类型且没有指向什么资源编译器⾃动⽣成的赋值运算符重载就 可以完成需要的拷⻉所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类虽然也都是 内置类型但是_a指向了资源编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我 们的需求所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。这⾥还有⼀个⼩技巧如果⼀个类显⽰实现 了析构并释放资源那么他就需要显⽰写赋值运算符重载否则就不需要。例如#includeiostream using namespace std; class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 1, 30); Date d2(2026, 2, 27); d2d1; d2.Print(); return 0; }输出结果四.取地址运算符重载1.const成员函数1.1.概念将const修饰的成员函数称之为const成员函数const修饰成员函数放到成员函数参数列表的后面。1.2.特点const实际修饰该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进⾏修改。例如const 修饰Date类的Print成员函数Print隐含的this指针由Date* const this变成const Date* const this。#includeiostream using namespace std; class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // void Print(const Date* const this) const void Print() const { //其实为this-_year2027; _year2027; cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; const int main() { Date d1(2024, 7, 5); d1.Print(); const Date d2(2024, 8, 5); d2.Print(); return 0; }报错显示2.取地址运算符重载取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了不需要去显⽰实现。除⾮⼀些很特殊的场景⽐如我们不想让别⼈取到当 前类对象的地址就可以⾃⼰实现⼀份胡乱返回⼀个地址。class Date { public: Date* operator() { return this; } const Date* operator()const { return this; } private: int _year; int _month; int _day; };