:6 大默认成员函数与运算符重载全方位深度剖析)
类和对象中文章目录类和对象中1. 类的默认成员函数了解2. 构造函数构造函数的特点3. 析构函数析构函数的特点4、构造拷贝函数--- A(const A a){}拷贝构造函数的特点5 、 运算符重载一、核心概念和语法二、语法硬性限制与原则三、5 个绝对不能重载的运算符重难点/常考题四、 两个经典特殊场景处理1. 前置 与后置 的区分2. 流插入 和流提取 的重载3. 赋值运算符重载operator3.1、赋值重载 vs 拷贝构造3.2、赋值运算符重载的标准写法与特性3.3、默认赋值运算符重载的行为与深浅拷贝3.4、 什么时候需要自己写赋值重载判断法则6、const成员函数7、取地址运算符重载operator1. 类的默认成员函数了解默认成员函数 代码里不写C 编译器偷偷自动帮你生成出来的成员函数。C98一共 6 个默认成员函数分成三大组初始化与清理构造函数、析构函数。拷贝复制拷贝构造、赋值运算符重载取地址重载operator、const operator我们要从两个方面去学习我们不写时编译器默认生成的函数行为是什么是否满足我们的需求编译器默认生成的函数不满足我们的需求我们需要自己实现那么如何自己实现2. 构造函数构造函数是特殊的成员函数,构造函数不负责开辟对象空间局部对象栈空间提前开好它唯一任务对象创建完成后自动初始化成员变量。构造函数的特点函数名与类名相同。没有返回值不能写 void什么都不写。创建对象时编译器自动调用无法手动显式调用。支持函数重载一个类可以拥有多个构造函数。如果你一个构造函数都不写编译器自动生成一份无参默认构造只要你手写任意构造函数编译器不再自动生成默认构造。classStack{public:// 构造1带参构造Stack(intn){_anewint[n];//向操作系统申请一块大小为4n字节的连续堆内存空间成功后返回首元素的地址}// Stack(); // 编译器不会自动生成因为你写了上面构造private:int*_a;};intmain(){Stack s1;// ❌报错不存在无参构造}一个类只能存在一个默认构造不能同时写无参构造 全缺省构造调用会产生歧义什么是【默认构造函数】能不传递任何参数直接调用的构造函数统称为默认构造函数分为三类编译器自动生成的无参构造手写的无参构造 Stack()全缺省构造 Stack(int n 4)classStack{public:Stack()// 无参构造默认构造{}Stack(intn4)// 全缺省构造也是默认构造{}};intmain(){Stack s;// ❌编译器不知道调用哪一个歧义报错}如果你什么构造都不写编译器生成的无参构造规则内置类型int、double、指针等不初始化里面是随机垃圾值。自定义类型成员别的类对象自动调用这个成员自身的默认构造函数。classA{public:A(inta){}// 只有带参构造没有默认构造};classB{private:A _aa;// 成员变量声明};intmain(){B b;// ❌编译报错// 调用 B 的默认构造时必须初始化成员 _aaA 类型对象//初始化自定义类型成员的默认规则调用这个类A的默认构造函数。//可是 A 只有 A(int a)没有默认构造编译器找不到直接报错。}C 把类型分成内置类型 (基本类型)和自定义类型。内置类型就是语言提供的原生数据类型如int/char/double/ 指针等自定义类型就是我们使用class/struct等关键字自己定义的类型。3. 析构函数析构函数与构造函数作用相反对象生命周期结束时自动调用负责清理对象持有的资源堆内存等注意析构函数不销毁对象本身局部对象本身由栈帧销毁析构只是清理对象内部申请的资源无堆资源申请的类如Date不需要显式写析构持有堆资源的类如Stack必须自定义析构否则造成内存泄漏。析构函数的特点命名规则函数名 ~类名前缀波浪号 ~ 。函数签名限制无参数、无返回值不能写void无法重载 。数量限制一个类只能有一个析构函数不手动定义时编译器自动生成默认析构函数 。调用时机对象生命周期结束编译器自动调用不需要手动调用特殊场景除外。默认析构函数行为内置类型成员不做任何处理自定义类型成员自动调用该成员自身的析构函数。显式定义析构函数行为哪怕自己手写析构类内自定义类型成员依然会自动调用自身析构函数。什么时候需要自己写析构 类中没有主动向堆申请资源可以使用默认析构不用手写类中存在堆内存、文件等资源必须显式实现析构释放资源否则资源泄漏。多个局部对象析构顺序同一作用域内先创建的对象后析构后创建的对象先析构栈后进先出规则voidtest(){A a1;// 先定义A a2;// 后定义// 函数结束先析构a2再析构a1}4、构造拷贝函数— A(const A a){}如果⼀个构造函数的第⼀个参数是自身类类型的引用且任何额外的参数都有默认值则此构造函数也叫做拷贝构造函数也就是说拷贝构造是⼀个特殊的构造函数。拷贝构造函数的特点拷贝构造是构造函数的重载构造函数可以多个默认构造、带参构造、拷贝构造构成函数重载。第一个参数必须是同类对象引用不能传值A(A a)拷贝构造函数也可以多个参数但是第⼀个参数必须是类类型对象的引用后⾯的参数必须有缺省值。建议加const确保不去改动原来的对象为什么不能传值呢如果是传值我们知道传参本质是值的拷贝创造形参a时需要调用拷贝构造复制实参又要创建形参无限递归调用拷贝构造 → 编译器直接报错。什么时候自动调用拷贝构造1. 使用一个对象初始化另一个新对象A a1;A a2a1;// 调用拷贝构造Aa3(a1);// 调用拷贝构造2. 函数传参自定义类对象值传递传值传参voidfunc(A tmp){}func(a1);// 实参a1拷贝构造形参tmp3. 函数返回自定义类对象返回传值返回Atest(){A a;returna;// 返回时拷贝构造临时对象}没有显式写拷贝构造函数编译器会自动合成一份默认拷贝构造自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造。注意当类内有堆内存指针成员时默认拷贝构造只会拷贝指针地址两个对象指向同一块堆空间析构时会重复释放同一块内存程序崩溃。5 、 运算符重载一、核心概念和语法1.本质运算符重载是具有特殊名称的函数函数名为 operator 要重载的运算符如 operator 。2. C规定类类型对象使用运算符时必须转换成调用对应运算符重载若没有对应的运算符重载则会编译报错。3.参数与操作数-全局函数参数个数 操作数个数如二元运算符有两个参数左操作数传给第一个参数右操作数传给第二个。-成员函数参数个数 操作数个数 - 1因为第一个操作数默认传给隐式的 this 指针。4.优先级与结合性运算符重载以后其优先级和结合性与对应的内置类型运算符保持⼀致。二、语法硬性限制与原则不能创造新运算符不能拼接 C 语法中不存在的符号如 operator。必须包含自定义类型重载函数至少有一个参数是类类型/自定义类型。×错误示例int operator(int x, int y)不能改变内置类型的含义。遵循语义逻辑只有在逻辑上有意义时才重载。例如 Date - Date 表示计算相差天数有意义但 Date Date 毫无意义三、5 个绝对不能重载的运算符重难点/常考题符号名称.成员访问运算符.*成员指针访问运算符::作用域限定符sizeof长度计算运算符?:条件运算符三目运算符四、 两个经典特殊场景处理1. 前置与后置的区分因为函数名都是operatorC 规定通过形参列表来区分前置如dDate operator();无形参后置如dDate operator(int);增加一个int形参占位不传实参仅用于构成重载// 1. 前置 先自增再返回自增后的自己Dateoperator(){_day1;return*this;// *this 离开函数依然存在返回引用避免拷贝}// 2. 后置 先存旧值再自增最后返回旧值Dateoperator(int){Date temp*this;// 记录旧状态_day1;returntemp;// temp 是局部临时变量必须传值返回不能返回引用}为什么一个加 一个不加前置 返回 Date返回的是已经改变的 *this生命周期超出函数本身引用返回可以避免临时对象的拷贝构造效率高且支持 d。后置 返回 Date返回的是局部变量 temp离开函数即被销毁返回引用会导致野引用崩溃故必须按值拷贝返回。2. 流插入和流提取的重载必须重载为全局函数。原因若重载为成员函数this指针会强制抢占第一个形参位置左操作数导致调用时必须写成d1 cout严重破坏可读性。正确做法全局函数可以将ostream/istream放在第一个形参位置使调用保持为传统的cout d1。3. 赋值运算符重载operator赋值运算符重载是类的 6 个默认成员函数之一用于将一个已存在对象的数据拷贝给另一个已经存在的对象。3.1、赋值重载 vs 拷贝构造Dated1(2026,1,1);Date d2d1;// 【拷贝构造】d2 本身不存在正在被创建并初始化Dated3(2026,8,1);d3d1;// 【赋值重载】d3 和 d1 都已经存在把 d1 赋值给 d33.2、赋值运算符重载的标准写法与特性必须重载为类的成员函数C 规定赋值运算符不能重载为全局函数。若写成全局函数编译器依然会在类内自动生成一个默认赋值重载产生冲突。参数推荐写成 const 类名引用传递避免值传递时的拷贝开销const 防止被赋值源对象被意外修改 。返回值推荐写成 类名 并返回 *this提高效率避免拷贝临时对象并支持连续赋值如 d1 d2 d3;。必须进行自赋值检查防止类似 d1 d1; 时在管理动态资源的场景下误将自身原本的内存提前释放。classDate{public:Dateoperator(constDated){// 1. 检查是否为自己给自己赋值if(this!d){_yeard._year;_monthd._month;_dayd._day;}// 2. 返回当前对象的引用支持连续赋值return*this;}private:int_year;int_month;int_day;};3.3、默认赋值运算符重载的行为与深浅拷贝如果不显式实现 operator编译器会自动生成一个默认的内置类型成员完成值拷贝 / 浅拷贝按字节逐一拷贝。自定义类型成员调用该自定义类型自身的 operator3.4、 什么时候需要自己写赋值重载判断法则不需要自己显式写的场景类似 Date成员全是内置类型且没有指向堆区动态资源。类似 MyQueue内部成员是由 Stack自定义类型组成直接调用 Stack 的赋值重载即可。必须自己显式实现的场景深拷贝类似 Stack / String成员变量包含指向堆区动态资源的指针如 int* _a。若采用默认浅拷贝会导致多个对象指向同一块内存引发析构时重复释放Double Free及数据相互覆盖。6、const成员函数将const修饰的成员函数称之为const成员函数const修饰成员函数放到成员函数参数列表的后面。const实际修饰该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进行修改。#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}/* const 修饰Date类的Print成员函数 Print隐含的this指针由 Date* const this变成了const Date* const this, 即 void Print(const Date* const this) const */voidPrint()const{cout_year-_month-_dayendl;}private:int_year;int_month;int_day;};7、取地址运算符重载operator取地址运算符在干什么d1 取对象的地址时在底层其实是在调用这个重载函数Date d1;Date*ptrd1;// 等价于 d1.operator();取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载。取地址重载属于 C 类的 6 大默认成员函数 之一这两个函数编译器自动生成的一般就够用。为了保证 C 的类型安全与权限匹配所以需要两个版本普通对象调用普通版本Date*operator(){returnthis;}d1是普通对象可读可写取出的指针是 Date*之后可以通过该指针修改对象。常量对象调用 const 版本constDate*operator()const{returnthis;}d2 是 const Date只读取出的指针必须是 const Date*。防止通过指针把常量对象的权限放大。什么时候才需要自己写只有在极少数“搞破坏”或“防止对象被获取真实地址”的特殊场景下才需要自己重写比如//不想让外界拿到真实地址防止他人通过原始指针直接篡改对象内部数据。Date*operator(){returnnullptr;// 无论谁取地址统统返回空指针}