33.总结
33.1 知识点

学习的主要内容

强调


33.2 核心要点速览
面向对象基本概念
- 核心理念:万物皆对象,用类抽象特征和行为。
封装
类和对象
| 核心要点 | 说明 |
|---|---|
| 类的定义 | 用class声明,包含成员变量、构造/析构函数、成员方法,是对象的模板。 |
| 对象实例化 | - 栈上:ClassName obj; - 堆上: ClassName* ptr = new ClassName(); |
| 内存分布 | 类定义在代码段,对象实例存储在栈或堆,成员变量占用实际内存。 |
访问修饰符(3P)
| 修饰符 | 作用 | 访问范围 |
|---|---|---|
public |
公共成员,类内外均可访问。 | 类内、类外、子类(继承时) |
private |
私有成员,仅类内部可访问(默认修饰符)。 | 仅限类内 |
protected |
保护成员,类内和子类可访问。 | 类内、子类(继承时) |
成员方法
| 核心要点 | 说明 |
|---|---|
| 声明与定义 | - 声明在类内,定义可在类内(内联)或类外(.cpp)。 |
| 内联函数 | 用inline修饰,编译时插入代码,减少函数调用开销,适合短函数。 |
| this指针 | 指向当前对象,用于区分参数与成员变量(如this->age = age;)。 |
构造函数与析构函数
| 类型 | 构造函数 | 析构函数 |
|---|---|---|
| 声明 | ClassName(); / ClassName(int a); |
~ClassName(); |
| 作用 | 初始化对象成员,可重载。 | 释放对象资源(如堆内存),不可重载。 |
| 调用时机 | 实例化对象时自动调用。 | 对象释放时自动调用(栈对象出作用域/堆对象delete)。 |
静态成员
| 类型 | 静态成员变量 | 静态成员函数 |
|---|---|---|
| 声明 | static int count; |
static void func(); |
| 初始化 | 在.cpp中定义:int ClassName::count = 0; |
直接定义在类内或.cpp,无this指针。 |
| 访问方式 | ClassName::count / obj.count |
ClassName::func() / obj.func() |
| 特性 | 所有对象共享,生命周期贯穿程序运行。 | 不能访问非静态成员,属于类而非对象。 |
const 成员变量 vs const 成员函数
| 特性 | const 成员变量 | const 成员函数 |
|---|---|---|
| 声明示例 | const int age = 18;const int id;(需初始化列表) |
void print() const; |
| 初始化规则 | - 声明时直接赋值 - 构造函数初始化列表赋值 |
- 在函数声明和定义处均需加 const 后缀 |
| 修改限制 | 初始化后不可修改(即使在构造函数体内也不可赋值) | - 不可修改非 mutable 成员变量- 可修改 mutable 成员变量 |
| 调用限制 | — | - const 对象只能调用 const 成员函数- 非 const 对象可调用 const 与非 const 成员函数 |
| 典型用途 | - 固定常量值(如 PI) - 对象标识(如 ID) |
- 访问器(getter)方法 - 不修改状态的操作 |
const 修饰函数参数
| 参数类型 | 语法示例 | 限制 | 作用 |
|---|---|---|---|
| 基本类型 | void func(const int i); |
函数内不可修改参数值(值传递) | 防止意外修改,无实际数据保护(值拷贝) |
| 指向常量的指针 | void func(const int* ptr); |
可改指针指向,不可改指向的值 | 保护数据不被修改,允许指针重定向 |
| 指针常量 | void func(int* const ptr); |
不可改指针指向,可改指向的值 | 固定指针指向,允许修改数据 |
| 指向常量的指针常量 | void func(const int* const ptr); |
不可改指针指向和指向的值 | 完全保护数据和指针指向 |
| 常量引用 | void func(const int& ref); |
不可修改引用对象,避免拷贝开销 | 高效传递对象,保护数据不被修改 |
友元
| 类型 | 友元函数 | 友元类 | 友元成员函数 |
|---|---|---|---|
| 声明 | friend void func(Class& obj); |
friend class FriendClass; |
friend void FriendClass::func(Class& obj); |
| 本质 | 全局函数,需在类中声明为friend |
整个类被声明为友元,所有成员可访问 | 某类的成员函数被声明为友元,允许访问 |
| 作用 | 允许单个函数访问类的私有/保护成员 | 允许整个FriendClass类访问另一个类的私有/保护成员 | 允许某个类的特定成员函数访问私有/保护成员 |
| 访问范围 | 函数内可访问目标类的私有/保护成员 | 友元类的所有成员函数均可访问目标类成员 | 仅被声明的成员函数可访问目标类成员 |
| 调用方式 | 直接调用(全局函数) | 通过友元类对象调用成员函数 | 通过友元类对象调用被声明的成员函数 |
| 注意事项 | - 非类成员,需通过对象参数访问 - 无this指针 |
- 友元关系单向(A是B的友元≠B是A的友元) - 不继承 |
- 需明确作用域(FriendClass::func)- 属于友元类的成员 |
| 代码示例 | class A { friend void f(A& a); }; void f(A& a) { a.privateMember; } |
class A { friend class B; }; class B { void access(A& a) { a.privateMember; } }; |
class A { friend void B::access(A& a); }; class B { void access(A& a) { a.privateMember; } }; |
嵌套类
| 核心要点 | 说明 |
|---|---|
| 定义 | 在类内声明的类,如class Outer { class Inner { ... }; }; |
| 访问限制 | 受外层类访问修饰符影响(public可外部使用,private仅限外层类内)。 |
| 独立性 | 与外层类相互独立,可定义自己的成员和方法。 |
命名空间
| 核心要点 | 说明 |
|---|---|
| 作用 | 避免命名冲突,封装相关功能(如namespace Math { int add(); })。 |
| 声明 | namespace Name { int val; void func(); } |
| 使用 | - 限定访问:Name::val - 导入: using namespace Name; |
| 嵌套 | 可多层嵌套,如namespace A::B::C { ... },用A::B::C::func();访问。 |
运算符重载
| 核心要点 | 说明 |
|---|---|
| 语法 | 返回类型 operator符号(参数列表),如Point operator+(const Point& p); |
| 类型 | - 成员函数:左操作数为类对象 - 非成员函数:可定义其他类型左操作数 |
| 常见重载 | + - * / == != ++ -- [] ()等,不可重载. :: ?:等符号。 |
| 注意 | 需保持运算符原始语义,避免滥用,建议成对实现关系运算符(如==与!=)。 |
继承
继承的基本规则
| 特性 | 说明 | 注意 |
|---|---|---|
| “是一个”关系 | 子类获得父类字段、方法、属性;不含构造、析构、友元 | 支持多继承(C++)、传递性(前提是访问权限允许) |
| 语法 | class 子 : 访问修饰 父 { … }; |
访问修饰符决定基类成员在子类中的新可见性 |
| 同名隐藏 | 子类定义同名成员会隐藏基类所有同名重载 | 用 父类::member 或在子类写 using 父类::member; 恢复访问 |
| 禁止继承 | C++ 用 final,C# 用 sealed |
只对类生效,不影响已有成员访问 |
继承方式与访问控制
| 父类原级别 | public 继承后 | protected 继承后 | private 继承后 |
|---|---|---|---|
| public | public | protected | 不可访问 |
| protected | protected | protected | 不可访问 |
| private | 不可访问 | 不可访问 | 不可访问 |
继承后的构造函数和析构函数
| 阶段 | 执行顺序 | 要点/注意 |
|---|---|---|
| 构造 | ① 递归调用顶层基类 ② 构造子类成员(字段/对象,声明顺序) ③ 执行子类自身构造体 |
默认调用基类无参构造;若无,则初始化列表必须显式调用带参基构造;C++ 可 using 父::父; 引入基类构造 |
| 析构 | ① 执行子类自身析构 ② 析构子类成员 ③ 调用各级基类析构 |
析构不继承,顺序与构造相反 |
多重继承
| 特性 | 说明 | 关键点/注意 |
|---|---|---|
| 语法 | class 子 : 父1, 父2 { … }; |
构造按声明顺序调用;析构逆序;若同名需 父::成员 指定 |
| 同名二义 | 多个父有同名成员 | 必须 父类::member |
菱形继承 & 虚继承
| 类型 | 问题或方案 | 关键点/注意 |
|---|---|---|
| 普通菱形继承 | B、C 各继 A ⇒ D 得到两份 A | 成员访问二义、内存冗余 |
| 虚继承 | class B : virtual public A… |
D 只保留一份 A;虚基类由最底层派生类负责构造(中间 A(...) 不生效);访问时需额外偏移/指针跳转,开销略增 |
继承中的友元 & 运算符重载
| 特性 | 继承行为 | 关键点/注意 |
|---|---|---|
友元 (friend) |
不会被继承 | 父子友元关系独立;子类私有/保护成员仍不可由父友元访问 |
运算符重载 (operator) |
会被继承 | 子类可直接使用父类重载运算符 |
里氏替换原则
| 场景 | 表现 | 关键点/注意 |
|---|---|---|
| 栈上替换 | GameObject obj = Player(); |
对象切片(子类部分丢失)、不可再转回子类,不推荐 |
| 堆上替换 | GameObject* p = new Player(); |
不切片,可 dynamic_cast<子*>(父*) 安全下转;常用于多态场景 |
多态
虚函数与多态实现
| 特性 | 说明 | 关键点/注意事项 |
|---|---|---|
| 多态本质 | 同一父类的不同子类对象,执行相同方法时表现不同行为(运行时多态) | 通过虚函数(virtual)和重写(override)实现 |
| 虚函数声明 | virtual 返回值 函数名(参数) {}(需在父类声明,子类用override重写) |
- 父类参与多态的成员函数需 virtual;子类重写建议写 override(编译期检查签名)- 重写时参数/返回值须与父类虚函数一致(协变返回等规则除外) |
| 保留父类逻辑 | 子类重写函数中用父类名::函数名()显式调用父类实现 |
Son::Test() { Father::Test(); cout << “子类逻辑”; } |
| 多态调用场景 | 父类指针/引用指向子类对象时,自动调用子类重写方法(堆上里氏替换) | 栈上对象切片会丢失多态性(仅调用父类方法) |
虚析构函数
| 特性 | 说明 | 核心作用 |
|---|---|---|
| 必要性 | 基类析构函数声明为 virtual 后,通过基类指针 delete 派生类对象会先调用派生类析构再调用基类析构。若基类析构非虚,该场景下派生类析构可能不被调用。工程上多态基类通常提供虚析构。 |
避免派生类资源未释放(如堆成员)造成泄漏或使用已释放资源 |
| 执行顺序 | 子类析构 → 父类析构(与构造顺序相反) | 多层继承时只需顶层基类析构为虚函数即可 |
| 栈上调用 | 栈上对象按正常析构顺序调用(先子后父),但切片时子类特有析构可能不执行 | Father* f = new Son(); delete f; // 正确调用Son析构 Father f2 = Son(); // 切片,仅调用Father析构 |
抽象类与纯虚函数
| 特性 | 说明 | 规则 |
|---|---|---|
| 抽象类定义 | 包含至少一个纯虚函数的类(不能直接实例化,仅作基类) | class Shape { virtual void draw() = 0; // 纯虚函数 }; |
| 纯虚函数语法 | virtual 函数() = 0;(无函数体,子类必须重写) |
子类未实现纯虚函数则自身也为抽象类 |
| 应用场景 | 定义接口规范(如图形基类、游戏对象基类) | 抽象类可包含普通函数和成员变量,子类继承后需实现所有纯虚函数 |
禁止重写(final关键字)
| 特性 | 说明 | 语法示例 |
|---|---|---|
| 禁止类继承 | class 类名 final {};(防止类被继承) |
class FinalClass final {}; |
| 禁止函数重写 | 虚函数后加final(virtual void func() override final;) |
class Father { virtual void func() final; // 子类不可重写 }; |
虚函数表(V-Table)
| 特性 | 说明 | 底层原理 |
|---|---|---|
| 本质 | 存储虚函数指针的数组,每个含虚函数的类对应一张表 | 类的所有对象共享虚函数表,对象含虚表指针(vptr)指向该表 |
| 内存位置 | 存储在数据段/常量段(全局共享) | 对象sizeof会增加一个指针大小(64位8字节) |
| 多继承实现 | 每个父类对应一张虚表,子类对象含多个vptr指向各父类虚表 | 子类虚函数放入第一个父类虚表中(主流编译器实现) |
面向对象关联知识点
接口(C++模拟实现)
| 核心要点 | 说明 | 实现方式 |
|---|---|---|
| 本质 | 行为的抽象规范,C++中通过纯虚函数的抽象类模拟接口 | class IFly { virtual void Fly()=0; };(仅含纯虚函数的抽象类) |
| 命名规范 | 接口类名前缀加I(如IFly) |
class IWalk { virtual void Walk()=0; }; |
| 继承与实现 | 子类继承接口类后必须实现所有纯虚函数 | class Bird : public IFly { void Fly() override { ... } }; |
| 应用场景 | 统一管理不同类型但有相同行为的对象(如飞机、鸟、超人都实现IFly接口) |
IFly* obj[2] = {new Plane(), new Bird()};(里氏替换原则应用) |
多继承同名虚函数
| 核心要点 | 说明 | 实现规则 |
|---|---|---|
| 同名处理 | 多继承时父类若有同名虚函数,子类只需重写一次即可覆盖所有父类版本 | class Father { virtual void Eat(); }; class Mother { virtual void Eat(); }; class Son : public Father, public Mother { void Eat() override { Father::Eat(); Mother::Eat(); } } |
| 底层原理 | 多个父类虚函数表中的该函数指针均指向子类重写的函数 | 子类虚函数表中该函数地址统一指向子类实现,调用时自动分发 |
空类型的内存占用
| 核心要点 | 说明 | 示例 |
|---|---|---|
| 空类大小 | 空类/结构体占1字节(确保对象有唯一内存地址) | sizeof(EmptyClass) → 1(64位/32位平台均为1) |
| 继承优化 | 子类包含成员时,空基类的1字节会被优化(不额外占用空间) | class Base {}; // sizeof=1 class Derived : Base { int i; }; // sizeof=4(Base的1字节被优化) |
| 含虚函数空类 | 包含虚函数的空类占8字节(64位平台,虚表指针大小) | class VirtualEmpty { virtual void Fun(); }; → sizeof=8(64位) |
结构体与类的区别
| 核心要点 | 结构体(struct) |
类(class) |
|---|---|---|
| 默认访问权限 | public(成员和继承默认均为public) | private(成员和继承默认均为private) |
| 设计初衷 | 侧重数据集合(如坐标、配置数据) | 侧重面向对象封装(数据+行为) |
| 继承语法 | struct Son : Father {};(默认public继承) |
class Son : Father {};(默认private继承) |
| 应用场景 | 简单数据结构(如struct Point { int x,y; }) |
复杂对象(如class Player { int hp; void Attack(); }) |
字符数组与string对比
| 核心要点 | 字符数组(char[]) |
string类 |
|---|---|---|
| 内存管理 | 固定长度,需手动管理(易越界) | 动态扩容,自动管理内存 |
| 安全性 | 无边界检查,易导致缓冲区溢出 | 有边界检查,安全系数高 |
| 常用操作 | 需要调用C函数(如strcat_s、strcpy_s) |
支持运算符重载(+、+=)和成员函数(append、substr) |
| 空终止符 | 需手动保证\0终止,否则打印乱码 |
自动管理终止符,size()返回有效长度 |
| 推荐场景 | 底层开发、嵌入式(空间敏感) | 日常字符串处理、C++标准库交互 |
33.3 面试题精选
基础题
1. const 成员函数的调用规则
题目
const 成员函数谁能调用?const 对象能调用哪些成员函数?
深入解析
const成员函数承诺不修改对象逻辑状态(mutable除外)。const对象只能调用const成员函数;非const对象既能调用const成员函数,也能调用非const成员函数。- 常见踩坑:仅有非
const版本的成员函数时,const对象无法调用。
答题示例
const成员函数给的是「只读接口」:const对象只能用这组接口。非const对象权限更大,const和非const重载版本都能调。设计 API 时,能只读就标const,既方便const对象使用,也约束实现里别乱改状态。
参考文章
- 9.面向对象-封装-const成员.md
2. 静态成员变量为什么通常要在类外定义
题目
类里声明了 static 成员变量,为什么还要在 .cpp 里再写一遍定义?
深入解析
- 类内声明是「声明存在」,不占每个对象实例的份;整体程序里仍需唯一定义以分配存储。
- 类外写法
int ClassName::count = 0;完成定义与初始化。 - 静态成员函数一般类内类外定义均可,但同样不访问非静态成员、无
this。
答题示例
static成员变量属于类、不属于某个对象,所以类里只是声明。链接期要有一块真实内存,就在类外做唯一定义并给初值。静态成员函数不绑对象,只能碰静态数据,这是和实例方法的根本区别。
参考文章
- 8.面向对象-封装-静态成员.md
3. 友元关系会不会被继承
题目
父类的友元函数或友元类,能否自然访问子类的私有成员?
深入解析
- 友元关系不继承;父类的友元不等于子类的友元。
- 若需访问子类私有实现,需在子类上另行声明友元,或提供受保护/公有接口。
- 友元破坏封装,仅用于边界清晰的协作(如运算符重载、紧密协作类)。
答题示例
友元是单向、不继承的契约:A 把 B 当友元,只解决 A 这一层;子类 C 的私有成员,父类友元 B 照样不能随便碰。要访问就再开友元或走接口,否则设计会越来越「全友元」难以维护。
参考文章
- 11.面向对象-封装-友元函数.md
- 21.面向对象-继承-继承中的友元和运算符重载.md
进阶题
1. 为什么多态基类常见写法是虚析构
题目
什么时候必须把基类析构声明为 virtual?不声明会怎样?
深入解析
- 存在「基类指针/引用实际指向派生类对象」,且可能通过基类指针
delete时,基类析构应为virtual。 - 非虚析构时,该
delete可能按基类类型析构,派生类析构链不完整 → 派生类资源泄漏/C++ 未定义行为风险。 - 无多态、不会用基类指针删派生对象的类型,不必强行虚析构(仍有「非多态基类」的实践讨论,但面试重点在多态场景)。
答题示例
只要存在用基类指针删真实派生对象的用法,基类析构就该是虚的,这样析构从派生类一层层往上走。没写
virtual的话,释放路径可能停在基类,派生类里托管的资源可能泄漏,也可能触发未定义行为。
参考文章
- 24.面向对象-多态-虚析构函数.md
2. 什么是对象切片,和多态有什么关系
题目
栈上 Base b = Derived(); 与堆上 Base* p = new Derived(); 在虚函数表现上差在哪?
深入解析
- 按值赋值/拷贝到基类对象时发生切片:派生类独有成员被切掉,对象静态类型为基类。
- 通过基类对象调用虚函数通常没有通过指针/引用那样稳定的动态绑定语义预期(对象本身已是基类子对象形态)。
- 多态惯用法是基类指针/引用指向派生类;需要向下转型时用
dynamic_cast并做好失败处理。
答题示例
切片是「按基类大小拷贝了一份」,子类多出来的数据和虚表布局预期一起丢了,所以别指望靠值语义玩多态。多态要靠指针或引用保留完整动态类型,这也是和里氏替换、虚函数一起记的一套话。
参考文章
- 22.面向对象-继承-里氏替换原则.md
- 23.面向对象-多态-虚函数.md
3. 菱形继承出了什么问题,虚继承怎么修
题目
普通多重继承下的菱形结构有什么隐患?virtual 继承在语义上改变了什么?
深入解析
- 无虚继承时,最末派生类可能包含多份间接基类子对象 → 二义性、冗余存储。
- 虚继承(中间层
virtual基类)合并为一份共享基类子对象;构造上由最终派生类负责虚基初始化等规则需结合编译器实现理解。 - 仍有性能与布局复杂度成本,设计上优先评估是否改用组合、接口拆层。
答题示例
菱形不处理的话,远端基类在子对象里重复两份,访问同一成员都要写
B::x/C::x消歧义。虚继承把远端基类收成一份共享,代价是对象布局和构造规则更绕,所以能不用菱形就不用,用也要团队约定清楚初始化责任。
参考文章
- 20.面向对象-继承-菱形继承.md
深度题
1. 虚函数表大致如何工作,为何会影响 sizeof
题目
有虚函数的类为什么实例往往更大?多继承时虚表可能有几张?
深入解析
- 编译器为含虚函数的类生成虚函数表(函数指针数组),每个对象藏一个(或多个)虚表指针指向对应表。
- 单继承常见「一颗 vptr + 一张主虚表」;多继承常见实现了多张虚表 / 多个 vptr(与 ABI、编译器相关),调用虚函数通过 vptr 间接寻址。
- 动态绑定的成本:多一次间接跳转;收益:运行时按真实类型分派。
答题示例
虚函数把「调哪个实现」从编译期挪到运行期:对象里塞 vptr,类对应一张虚表。多继承时往往不止一张表,所以布局更复杂、
sizeof里也得多算指针。面试把「vptr + vtable + 间接调用」说清就够扎底。
参考文章
- 27.面向对象-多态-虚函数表.md
2. override 与 final 分别在防什么 bug
题目
子类重写时为什么要写 override?final 用在类或虚函数上各限制什么?
深入解析
override:编译器检查是否确实重写基类虚函数,签名一改就报错,避免「以为 override 了其实没有」的静默 bug。final虚函数:禁止派生类再重写该虚函数。final类:禁止被继承,用于封闭实现或安全审计边界。
答题示例
override是编译期安全带:名字或参数飘了一点,你以为多态了,其实藏了个新函数。final一边是锁虚函数,一边是锁类,用途是「到这里不许再派生/重写」,和接口演进策略有关。
参考文章
- 26.面向对象-多态-禁止重写.md
- 23.面向对象-多态-虚函数.md
3. 抽象类与纯虚函数在设计上的用途
题目
含纯虚函数的类为什么不能直接实例化?和「接口」类比怎么用 C++ 表达?
深入解析
- 纯虚函数
= 0使类变为抽象类:仅规定接口契约,强制派生类实现。 - 仍可有数据成员、非虚函数、 protected 辅助;与「只含纯虚的抽象类」模拟接口习惯用法不同。
- 抽象类用于分层:上层依赖抽象,下层换实现,配合智能指针/工厂可落地。
答题示例
纯虚函数等于告诉编译器「这个行为还没实现」,所以抽象类不能
new出完整对象。C++ 没有interface关键字,常用「全是纯虚的类」当协议。设计上是把变化点塞到派生层,基类稳住调用代码。
参考文章
- 25.面向对象-多态-抽象类.md
- 28.面向对象关联知识点-接口.md
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com