类和对象(中)
文章目录
- 类和对象(中)
- 1. 类的默认成员函数(了解)
- 2. 构造函数
- 构造函数的特点
- 3. 析构函数
- 析构函数的特点
- 4、构造拷贝函数--- A(const A& a){};
- 拷贝构造函数的特点
- 5 、 运算符重载
- 一、核心概念和语法
- 二、语法硬性限制与原则
- 三、5 个绝对不能重载的运算符(重难点/常考题)
- 四、 两个经典特殊场景处理
- 1. 前置 `++` 与后置 `++` 的区分
- 2. 流插入 `<<` 和流提取 `>>` 的重载
- 3. 赋值运算符重载(operator=)
- 3.1、赋值重载 vs 拷贝构造
- 3.2、赋值运算符重载的标准写法与特性
- 3.3、默认赋值运算符重载的行为与深浅拷贝
- 3.4、 什么时候需要自己写赋值重载?(判断法则)
- 6、const成员函数
- 7、取地址运算符重载(operator&)
1. 类的默认成员函数(了解)
默认成员函数= 代码里不写,C++ 编译器偷偷自动帮你生成出来的成员函数。
C++98:一共 6 个默认成员函数
分成三大组:
- 初始化与清理:构造函数、析构函数。
- 拷贝复制:拷贝构造、赋值运算符重载
- 取地址重载:operator&、const operator&
我们要从两个方面去学习:
- 我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求
- 编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
2. 构造函数
构造函数是特殊的成员函数,构造函数不负责开辟对象空间(局部对象栈空间提前开好),它唯一任务:对象创建完成后,自动初始化成员变量。
构造函数的特点
- 函数名与类名相同。
- 没有返回值,不能写 void,什么都不写。
- 创建对象时,编译器自动调用,无法手动显式调用。
- 支持函数重载,一个类可以拥有多个构造函数。
- 如果你一个构造函数都不写,编译器自动生成一份无参默认构造;只要你手写任意构造函数,编译器不再自动生成默认构造。
classStack{public:// 构造1:带参构造Stack(intn){_a=newint[n];//向操作系统申请一块大小为4n字节的连续堆内存空间,成功后返回首元素的地址}// Stack(); // 编译器不会自动生成!因为你写了上面构造private:int*_a;};intmain(){Stack s1;// ❌报错,不存在无参构造}- 一个类只能存在一个默认构造,不能同时写无参构造 + 全缺省构造,调用会产生歧义!
- 什么是【默认构造函数】
能不传递任何参数直接调用的构造函数,统称为默认构造函数,分为三类
- 编译器自动生成的无参构造
- 手写的无参构造 Stack()
- 全缺省构造 Stack(int n = 4)
classStack{public:Stack()// 无参构造(默认构造){}Stack(intn=4)// 全缺省构造(也是默认构造){}};intmain(){Stack s;// ❌编译器不知道调用哪一个,歧义报错!}如果你什么构造都不写,编译器生成的无参构造规则:
- 内置类型(int、double、指针等):不初始化,里面是随机垃圾值。
- 自定义类型成员(别的类对象):自动调用这个成员自身的默认构造函数。
classA{public:A(inta){}// 只有带参构造,没有默认构造};classB{private:A _aa;// 成员变量声明};intmain(){B b;// ❌编译报错!// 调用 B 的默认构造时,必须初始化成员 _aa(A 类型对象);//初始化自定义类型成员的默认规则:调用这个类(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 a2=a1;// 调用拷贝构造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. 前置++与后置++的区分
因为函数名都是operator++,C++ 规定通过形参列表来区分:
- 前置
++(如++d):Date& operator++();(无形参) - 后置
++(如d++):Date operator++(int);(增加一个int形参占位,不传实参,仅用于构成重载)
// 1. 前置 ++:先自增,再返回自增后的自己Date&operator++(){_day+=1;return*this;// *this 离开函数依然存在,返回引用避免拷贝}// 2. 后置 ++:先存旧值,再自增,最后返回旧值Dateoperator++(int){Date temp=*this;// 记录旧状态_day+=1;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 d2=d1;// 【拷贝构造】:d2 本身不存在,正在被创建并初始化Dated3(2026,8,1);d3=d1;// 【赋值重载】:d3 和 d1 都已经存在,把 d1 赋值给 d33.2、赋值运算符重载的标准写法与特性
- 必须重载为类的成员函数:
- C++ 规定:赋值运算符不能重载为全局函数。若写成全局函数,编译器依然会在类内自动生成一个默认赋值重载,产生冲突。
- 参数推荐写成 const 类名&:
- 引用传递避免值传递时的拷贝开销;const 防止被赋值源对象被意外修改 。
- 返回值推荐写成 类名& 并返回 *this:
- 提高效率(避免拷贝临时对象),并支持连续赋值(如 d1 = d2 = d3;)。
- 必须进行自赋值检查
- 防止类似 d1 = d1; 时,在管理动态资源的场景下误将自身原本的内存提前释放。
classDate{public:Date&operator=(constDate&d){// 1. 检查是否为自己给自己赋值if(this!=&d){_year=d._year;_month=d._month;_day=d._day;}// 2. 返回当前对象的引用,支持连续赋值return*this;}private:int_year;int_month;int_day;};3.3、默认赋值运算符重载的行为与深浅拷贝
如果不显式实现 operator=,编译器会自动生成一个默认的:
- 内置类型成员:完成值拷贝 / 浅拷贝(按字节逐一拷贝)。
- 自定义类型成员:调用该自定义类型自身的 operator=
3.4、 什么时候需要自己写赋值重载?(判断法则)
- 不需要自己显式写的场景:
- 类似 Date:成员全是内置类型,且没有指向堆区动态资源。
- 类似 MyQueue:内部成员是由 Stack(自定义类型)组成,直接调用 Stack 的赋值重载即可。
- 必须自己显式实现的场景(深拷贝):
- 类似 Stack / String:成员变量包含指向堆区动态资源的指针(如 int* _a)。若采用默认浅拷贝,会导致多个对象指向同一块内存,引发析构时重复释放(Double Free)及数据相互覆盖。
- 类似 Stack / String:成员变量包含指向堆区动态资源的指针(如 int* _a)。若采用默认浅拷贝,会导致多个对象指向同一块内存,引发析构时重复释放(Double Free)及数据相互覆盖。
6、const成员函数
- 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
- const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
#include<iostream>usingnamespacestd;classDate{public:Date(intyear=1,intmonth=1,intday=1){_year=year;_month=month;_day=day;}/* const 修饰Date类的Print成员函数, Print隐含的this指针由 Date* const this变成了const Date* const this, 即 void Print(const Date* const this) const */voidPrint()const{cout<<_year<<"-"<<_month<<"-"<<_day<<endl;}private:int_year;int_month;int_day;};7、取地址运算符重载(operator&)
- 取地址运算符在干什么?
- &d1 取对象的地址时,在底层其实是在调用这个重载函数:
Date d1;Date*ptr=&d1;// 等价于 d1.operator&();- 取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载。取地址重载属于 C++ 类的 6 大默认成员函数 之一;这两个函数编译器自动生成的一般就够用。
- 为了保证 C++ 的类型安全与权限匹配,所以需要两个版本
- 普通对象调用普通版本:
Date*operator&(){returnthis;}d1是普通对象(可读可写),取出的指针是 Date*,之后可以通过该指针修改对象。
- 常量对象调用 const 版本:
constDate*operator&()const{returnthis;}d2 是 const Date(只读),取出的指针必须是 const Date*。防止通过指针把常量对象的权限放大。
- 什么时候才需要自己写?
只有在极少数“搞破坏”或“防止对象被获取真实地址”的特殊场景下才需要自己重写,比如:
//不想让外界拿到真实地址,防止他人通过原始指针直接篡改对象内部数据。Date*operator&(){returnnullptr;// 无论谁取地址,统统返回空指针}