C++ 面向对象核心重难点,面试重中之重,笔试、代码题、底层原理追问高频出现,答题话术标准化,直接背诵即可满分作答。
1. 类与对象基础、成员分类
1.1 类与对象核心概念
- 类:抽象模板,描述事物的属性与行为,是一种自定义数据类型。
- 对象:类的实例,占用独立内存空间,拥有类定义的成员。
- C++ 中
struct与class仅默认访问权限不同:struct默认public,class默认private,其余特性完全一致。
1.2 类成员分类
- 按访问权限划分
private:私有成员,仅本类内部可访问,外部、派生类均无法访问。protected:保护成员,本类、派生类可访问,外部无法访问。public:公有成员,任意位置均可访问,对外暴露接口。
- 按存储属性划分
- 普通成员:属于对象,每个对象独有一份,随对象创建/销毁。包含成员变量、普通成员函数。
- 静态成员:属于类,所有对象共享同一份,程序运行期间一直存在。包含静态成员变量、静态成员函数。
- 特殊成员:构造函数、析构函数、拷贝构造、赋值运算符重载等编译器默认生成的成员函数。
高频追问
问 :静态成员函数能访问普通成员变量吗?
答 :不能。静态成员函数无this指针,无法指向具体对象,因此不能访问非静态成员。
2. 构造函数、拷贝构造、析构函数
2.1 构造函数
- 特性:函数名与类名相同,无返回值,对象创建时自动调用,用于初始化成员。
- 规则:可重载;若类未手动定义,编译器会生成默认无参构造。
- 分类:无参构造、有参构造、拷贝构造。
2.2 拷贝构造函数
- 格式:
类名(const 类名& 源对象) - 触发场景(3 种必考)
- 使用已有对象初始化新对象
- 对象以值传递方式作为函数形参
- 函数以值方式返回对象
- 规则:若未手动实现,编译器生成默认拷贝构造,执行逐字节拷贝。
2.3 析构函数
- 特性:函数名
~类名,无返回值、无参数,不能重载。 - 执行时机:对象生命周期结束时自动调用,主要用于释放堆资源、关闭句柄等收尾工作。
- 规则:编译器默认生成空析构函数;有动态内存申请时,必须手动自定义析构。
面试结论
构造负责初始化,拷贝构造负责对象复制,析构负责资源释放,三者是类的三大基础特殊函数。
3. 深拷贝 vs 浅拷贝
3.1 浅拷贝(默认拷贝)
- 原理:逐字节赋值,仅拷贝变量本身的值。若成员包含指针,只会拷贝指针地址,不会拷贝指针指向的堆内存。
- 问题:多个对象的指针指向同一块堆内存;对象析构时会重复释放内存,触发程序崩溃、内存错误。
- 适用场景:类中无堆内存、无指针成员。
3.2 深拷贝(自定义拷贝)
- 原理:除拷贝指针变量本身,额外重新开辟一块堆内存,拷贝原指针指向的数据,两个对象指针指向不同内存。
- 特点:资源相互独立,析构不会冲突,彻底解决浅拷贝隐患。
- 实现方式:手动重写拷贝构造函数 + 赋值运算符重载。
速记口诀
浅拷贝:只抄地址,共享内存;深拷贝:另开空间,数据独立。
4. 赋值运算符重载
4.1 基础说明
- 运算符:
=,默认赋值同样执行浅拷贝,存在默认拷贝构造一致的内存风险。 - 必须作为类的成员函数重载,不能全局重载。
- 标准函数原型:
类名& operator=(const 类名& 源对象)
4.2 四大必要步骤(答题踩分点)
- 判断自赋值 :
if (this == &src) return *this;,防止自己给自己赋值引发内存异常。 - 释放自身原有堆内存:避免内存泄漏。
- 重新分配内存:根据源对象大小开辟新空间。
- 拷贝数据 ,最后
return *this支持连续赋值。
4.3 补充区分
- 拷贝构造:创建新对象时调用。
- 赋值重载:已有对象之间赋值时调用。
5. 三种继承方式(public/protected/private)
核心规则:继承方式决定基类成员在派生类中的访问权限
- public 公有继承(工程最常用)
- 基类
public→ 派生类public - 基类
protected→ 派生类protected - 基类
private→ 派生类不可见
- 基类
- protected 保护继承
- 基类
public→ 派生类protected - 基类
protected→ 派生类protected - 基类
private→ 派生类不可见
- 基类
- private 私有继承
- 基类
public/protected→ 派生类private - 基类
private→ 派生类不可见
- 基类
总结
- 基类
private成员,无论哪种继承,派生类都无法直接访问。 - 公有继承保持原有权限;保护/私有继承会收紧访问权限。
6. 菱形继承问题与虚继承
6.1 菱形继承(钻石继承)
- 结构:一个基类被两个派生类继承,这两个派生类又共同派生出一个新类。
- 两大问题
- 数据冗余 :最终派生类会保留多份基类成员,占用多余内存。
- 二义性:访问基类成员时,编译器无法判断调用哪一份父类数据,编译报错。
6.2 虚继承解决方案
- 语法:在继承前加关键字
virtual,class 派生类 : virtual 继承方式 基类 - 原理:通过虚基类表 ,让所有派生类共享同一份基类数据,只保留一份基类成员。
- 作用:彻底解决菱形继承的数据冗余与访问二义性。
- 代价:引入虚基类指针,轻微增加内存开销。
7. 虚函数与多态实现原理
7.1 多态概念
- 分类:静态多态 (编译期)、动态多态(运行期)。
- 动态多态三要素(必考):公有继承 + 虚函数重写 + 父类指针/引用指向子类对象。
7.2 虚函数底层原理
- 包含虚函数的类,会生成一张虚函数表 (vtable),存储虚函数入口地址。
- 类对象内部会新增一个虚表指针 (vptr),指向所属类的虚函数表。
- 继承关系中,子类会继承父类虚表;重写虚函数后,子类虚表对应位置会替换为自身函数地址。
7.3 执行流程
程序运行时,通过对象的虚表指针找到虚函数表,查表调用对应函数,实现运行时动态绑定。
补充
- 虚函数表属于类 ,全局唯一;虚表指针属于对象,每个对象独有。
8. 虚析构函数作用与必要性
8.1 问题场景
当父类指针指向子类对象 ,使用父类指针delete释放对象时:
- 若析构非虚函数:只调用父类析构,子类析构不执行,子类堆资源无法释放,造成内存泄漏。
8.2 虚析构函数
- 语法:
virtual ~类名() - 作用:实现析构函数的多态。通过父类指针释放子类对象时,先调用子类析构,再调用父类析构,完整释放所有资源。
八股结论
只要类有可能被继承,并且会用父类指针/引用管理子类对象,析构函数必须声明为虚析构。
拓展
构造函数不能声明为虚函数。对象创建时虚表指针还未初始化,无法实现动态绑定。
9. 纯虚函数 & 抽象类
9.1 纯虚函数
- 语法:
virtual 返回值 函数名(参数) = 0; - 特点:只有声明,无具体实现。
9.2 抽象类
- 定义:包含至少一个纯虚函数的类,称为抽象类。
- 核心规则
- 无法实例化对象,只能作为基类被继承。
- 派生类必须重写所有纯虚函数,否则派生类也会变成抽象类。
9.3 应用场景
用于定义接口规范,统一派生类的行为标准,常见于框架、接口设计。
10. 重载、重写 (覆盖)、隐藏三者区别
10.1 函数重载(静态多态 / 同作用域)
- 范围:同一作用域
- 规则:函数名相同,参数列表不同(个数、类型、顺序);返回值不同不构成重载。
- 时机:编译期绑定。
10.2 重写/覆盖(动态多态 / 继承关系)
- 范围:父子类继承关系
- 规则:父类为
virtual虚函数,子类函数名、参数列表、返回值完全一致。 - 时机:运行期绑定,实现多态。
10.3 隐藏(重定义)
- 范围:父子类继承关系
- 规则:子类函数与父类函数名相同,不满足重写条件 ,直接隐藏父类同名函数。
- 父类非虚函数,子类同名函数 → 隐藏
- 父子类参数列表不一致 → 隐藏
- 特点:无多态,父类函数被屏蔽,通过作用域分辨符
::才可访问父类函数。
极简区分口诀
- 同域不同参 = 重载
- 跨域虚函数、三要素一致 = 重写
- 跨域同名、不满足重写 = 隐藏
🔥 本章综合高频追问
-
问 :虚函数、虚基类、虚析构分别解决什么问题?
答 :虚函数 实现动态多态;虚基类 解决菱形继承数据冗余与二义性;虚析构保证父类指针释放子类对象时完整析构。
-
问 :抽象类可以有构造函数吗?
答:可以。抽象类虽不能实例化,但派生类创建对象时,会调用父类构造初始化成员。
-
问 :为什么构造函数不能是虚函数?
答:虚函数依赖虚表指针,而虚表指针在构造函数执行阶段才初始化,顺序矛盾,因此构造不能虚。
-
问 :默认的拷贝构造和赋值重载一定安全吗?
答:不安全。类含有指针/堆成员时,默认函数执行浅拷贝,会造成内存重复释放、泄漏。
📝 模块总结
本模块围绕封装、继承、多态三大面向对象核心特性展开,重点考察特殊成员函数、拷贝机制、继承规则、虚函数底层、接口设计。是 C++ 面试核心拉分模块,全部掌握可应对绝大多数 OOP 与多态相关笔试、面试考题。
