C++ 多态与二叉搜索树 BST 完整复习笔记(含代码踩坑修正)
本文是 C++ 面向对象核心(多态、虚函数、绑定机制)与数据结构二叉搜索树(BST)的全程学习复习笔记,完整覆盖核心原理、代码实现、实操踩坑与 bug 修正,所有内容均来自学习过程中的真实疑问与易错点,适合期末复习、面试复盘使用,重点攻克动态绑定底层逻辑、BST 删除操作的核心难点。
第一部分 C++ 多态核心知识点
1.1 多态的核心定义
多态是面向对象三大特性(封装、继承、多态)的核心,本质是同一接口,不同实现:用基类的统一接口,调用不同子类的差异化实现,实现接口复用与逻辑解耦。
多态分为两类:
静态多态(编译期多态):编译期就确定函数调用地址,由静态绑定实现,比如函数重载、运算符重载
动态多态(运行期多态):编译期不确定调用地址,运行时才根据真实对象类型确定,由动态绑定实现,也是我们常说的 “多态”
1.2 静态绑定 vs 动态绑定(核心对比)
绑定的本质:确定函数调用的内存地址,二者的核心区别在于绑定时机不同。
表格
特性 静态绑定(早绑定) 动态绑定(晚绑定)
绑定时机 编译期 程序运行时
核心依赖 变量的编译时类型 继承 + virtual 虚函数 + 虚表 vtable
调用方式 直接 call 固定内存地址 取对象 vptr→查虚表→取函数指针→间接调用
触发场景 普通函数 / 成员函数、函数重载、对象直接调用、final 修饰的函数 / 类 1. 存在继承关系 2. 子类重写基类 virtual 虚函数 3. 基类指针 / 引用指向子类对象
性能 极高,编译器可内联优化 略低,有虚表寻址的轻微开销
核心特点 看编译时的指针类型,是什么类型就调谁的函数 看运行时的真实对象类型,指向谁就调谁的函数
必背口诀:静态绑定编译定地址,动态绑定运行查表找实现。
1.3 函数指针的完整获取方式
函数指针是实现动态绑定、虚表的底层基础,不同类型函数的指针获取方式有严格区别:
1.3.1 普通全局 / 自由函数
函数名本身就是函数的入口地址,两种写法等效:
cpp
运行
// 定义函数
void func(int a) { }
// 定义函数指针类型
void (*fp)(int);
// 获取指针
fp = func; // 简写,函数名隐式转为地址
fp = &func; // 标准取地址写法,效果完全一致
1.3.2 类普通成员函数(非静态)
成员函数自带隐含this指针,函数指针必须带类作用域,且必须显式取地址:
cpp
运行
class A {
public:
void fun(int x) { }
};
// 成员函数指针定义
void (A::*fp)(int) = &A::fun;
// 调用方式(必须绑定类对象)
A obj;
(obj.*fp)(10);
1.3.3 类静态成员函数
静态成员函数无隐含this指针,和全局函数用法完全一致:
cpp
运行
class A {
public:
static void staFun() {}
};
// 获取与调用
void (*fp)() = A::staFun;
fp();
1.3.4 虚函数指针
语法层面:无法直接通过&类名::虚函数获取到运行时的真实函数地址
底层实现:虚函数的指针会在编译期自动存入虚函数表(vtable,本质是函数指针数组),运行时通过对象头部的虚表指针 vptr找到虚表,再按下标取出对应的虚函数指针完成调用
1.4 派生类虚函数表的完整内容
虚表是存储虚函数指针的只读数组,每个有虚函数的类都有自己的虚表,派生类的虚表内容固定包含 4 类,顺序严格遵循规则:
从基类继承、未被子类重写的虚函数:直接沿用基类的函数指针
子类重写(override)的基类虚函数:同下标覆盖为子类自己的函数指针,不新增下标
子类自己新增的虚函数:按声明顺序追加到虚表的末尾
虚析构函数:只要基类声明了虚析构,派生类的析构函数会自动成为虚函数,存入虚表
示例与验证:
cpp
运行
class Base {
public:
virtual void f1(){}
virtual void f2(){}
virtual ~Base(){}
};
class Derive : public Base {
public:
void f2() override {} // 重写基类f2
virtual void f3(){} // 子类新增虚函数
};
Derive 派生类的虚表按顺序为:Base::f1 → Derive::f2 → Derive::~Derive → Derive::f3
核心规则:虚表前半部分顺序和基类完全一致,保证基类指针访问时的兼容性;重写仅覆盖对应下标,不改变顺序;新增虚函数永远追加在末尾。
1.5 多态实现三要素与高频易错点
实现三要素(缺一不可)
基类中声明virtual虚函数
子类 public 继承基类,并重写(override)对应的虚函数
用基类指针 / 引用指向子类对象,调用虚函数触发动态绑定
高频易错点
构造函数不能是虚函数,构造函数中调用虚函数不会触发多态(对象还未构造完成,虚表未初始化)
基类析构函数必须声明为虚函数,否则用基类指针删除子类对象时,只会调用基类析构,造成内存泄漏
子类重写虚函数时,函数签名(返回值、函数名、参数列表、const 属性)必须和基类完全一致,否则是函数重载而非重写
只有虚函数能触发多态,普通成员函数、静态成员函数都无法实现动态绑定
第二部分 二叉搜索树(BST)完整知识点
2.1 BST 核心定义与必背规则
二叉搜索树(Binary Search Tree,BST)也叫二叉排序树,是基于二分思想的树形数据结构,所有操作都必须遵循以下铁律:
节点的左子树所有节点值 < 该节点值
节点的右子树所有节点值 > 该节点值
左、右子树本身也必须是二叉搜索树
默认无重复节点(重复节点可自定义计数处理)
核心性质:对 BST 执行中序遍历(左→根→右),得到的结果是严格升序的有序序列。
2.2 BST 节点结构定义(C++ 模板)
cpp
运行
template<class T>
struct BSTNode {
T _data;
BSTNode<T>* _left;
BSTNode<T>* _right;
BSTNode(const T& key)
:_data(key)
,_left(nullptr)
,_right(nullptr)
{}
};
template<class T>
class BSTree {
typedef BSTNode<T> Node;
public:
// 成员函数:插入、查找、删除
private:
Node* _root = nullptr;
};
2.3 查找操作:逻辑 + 代码 + 口诀
核心逻辑
从根节点出发,遵循 “左小右大” 规则遍历:
目标值 == 当前节点值 → 查找成功,返回节点
目标值 < 当前节点值 → 进入左子树继续查找
目标值 > 当前节点值 → 进入右子树继续查找
遍历到空节点 → 查找失败,目标值不存在
完整代码
cpp
运行
// 查找:存在返回true,不存在返回false
bool Find(const T& key) {
Node* cur = _root;
while (cur) {
if (cur->_data == key) {
return true;
} else if (cur->_data > key) {
cur = cur->_left;
} else {
cur = cur->_right;
}
}
return false;
}
必背口诀:小往左,大往右,相等就命中,遇空就没有。
2.4 插入操作:逻辑 + 踩坑分析 + 修正代码
核心逻辑
空树:直接新建节点作为根节点,插入成功
非空树:按查找逻辑遍历,找到合适的空节点位置
新建节点,根据与父节点的大小关系,挂在父节点的左 / 右孩子位置
节点已存在 → 插入失败(BST 默认无重复节点)
核心特点:插入的节点一定是叶子节点,不会改动原有树结构。
踩坑记录(原错误代码)
cpp
运行
bool Insert(const T& key) {
if (_root == nullptr) {
_root = new Node(key);
return true;
}
Node* parent=nullptr;
Node* total=_root;
while (total) {
if (total->_data < key) {
parent = total;
total = total->_right;
}
else if (total->_data > key) {
parent = total;
total = total->_left;
}
else {
return false;
}
}
// 错误1:只在if分支新建节点,else分支total仍为nullptr
if (parent ->_data<key ) {
total = new Node(key);
}
// 错误2:无论key大小,全部挂在父节点左孩子,违背BST规则
parent->_left = total;
}
错误分析与修正后代码
核心错误:
新建节点逻辑不完整,仅在 key 大于父节点时新建节点
挂载节点错误,未区分左右孩子,全部挂在左孩子,导致树结构错乱
函数部分分支无返回值,存在未定义行为
修正后完整代码
cpp
运行
bool Insert(const T& key) {
// 空树直接插入根节点
if (_root == nullptr) {
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
// 查找插入位置
while (cur) {
if (cur->_data < key) {
parent = cur;
cur = cur->_right;
}
else if (cur->_data > key) {
parent = cur;
cur = cur->_left;
}
else {
return false; // 节点已存在,插入失败
}
}
// 新建节点,按大小挂载到对应位置
cur = new Node(key);
if (parent->_data < key) {
parent->_right = cur; // 比父节点大,挂右孩子
} else {
parent->_left = cur; // 比父节点小,挂左孩子
}
return true;
}
必背口诀:按查找往下走,遇空就新建,小挂左、大挂右,挂成叶子不走变。
2.5 删除操作:全场景拆解 + 核心难点 + 踩坑修正
删除是 BST 最复杂的操作,核心原则是:删除后仍需保持 BST 的 “左小右大” 规则,分三种场景处理。
场景 1:待删除节点是叶子节点(无左右孩子)
处理逻辑:直接断开父节点与该节点的连接,释放节点内存即可。
场景 2:待删除节点只有一个孩子(左孩子 或 右孩子)
处理逻辑:让父节点直接指向待删除节点的唯一孩子,用孩子节点顶替待删除节点的位置,再释放节点内存。
特殊处理:若待删除节点是根节点,直接让根节点指向其唯一孩子。
场景 3:待删除节点左右孩子都存在(核心难点)
这是最复杂的场景,不能直接删除节点,否则左右子树无法拼接,会破坏 BST 规则。
核心解决方案:替身替换法
核心思路:不直接删除中间节点,而是找一个 “好删除的替身节点”,交换数值后,删除替身节点即可。
替身节点的选择(二选一):
待删除节点左子树的最大值(左子树一路向右走到底的节点,也叫前驱节点)
待删除节点右子树的最小值(右子树一路向左走到底的节点,也叫后继节点)
为什么选这两个节点?
左子树最大值:一定小于待删除节点,大于左子树所有其他节点,替换后仍满足 BST 规则
右子树最小值:一定大于待删除节点,小于右子树所有其他节点,替换后仍满足 BST 规则
这两个节点最多只有一个孩子,属于前两种简单场景,删除难度极低
完整处理步骤
找到待删除节点的替身节点(左子树最大值)
交换待删除节点和替身节点的数值
此时待删除的目标转移到了替身节点
按前两种简单场景,删除替身节点即可
踩坑记录(原错误代码核心问题)
cpp
运行
// 错误核心代码
swap(min->_data, total->_data);
// 错误:默认min一定是minparent的右孩子,直接修改右指针
minparent->_right = min->_left;
delete min;
错误深度分析
核心错误:未判断替身节点 min 是父节点的左孩子还是右孩子,直接固定修改父节点的右指针。
触发 bug 的场景:当待删除节点的左子树没有右分支时,while(min->_right)循环一次都不会执行,此时min是minparent的左孩子,直接修改右指针会导致:
父节点左指针仍指向 min,树结构断链、错乱
父节点右指针被错误赋值,破坏 BST 规则
后续查找、遍历全部异常,甚至程序崩溃
修正后完整删除代码
cpp
运行
bool erase(const T& key) {
Node* parent = nullptr;
Node* cur = _root;
// 查找待删除节点
while (cur) {
if (cur->_data < key) {
parent = cur;
cur = cur->_right;
}
else if (cur->_data > key) {
parent = cur;
cur = cur->_left;
}
else {
// 找到待删除节点,分情况处理
if (cur->_left && cur->_right) {
// 场景3:左右孩子都存在,找左子树最大值(替身)
Node* minParent = cur;
Node* min = cur->_left;
// 找左子树最右节点(最大值)
while (min->_right) {
minParent = min;
min = min->_right;
}
// 交换数值,转移删除目标
swap(cur->_data, min->_data);
// 关键:判断替身是父节点的左孩子还是右孩子
if (minParent->_left == min) {
minParent->_left = min->_left;
} else {
minParent->_right = min->_left;
}
delete min;
}
else {
// 场景1+2:叶子节点 或 只有一个孩子
Node* child = cur->_left ? cur->_left : cur->_right;
// 特殊处理:删除根节点
if (cur == _root) {
_root = child;
} else {
// 挂载到父节点对应位置
if (cur == parent->_left) {
parent->_left = child;
} else {
parent->_right = child;
}
}
delete cur;
}
return true;
}
}
// 未找到待删除节点
return false;
}
必背口诀:无子直接删,单子直接替,双子找最值,赋值再删替;替身左右要判断,循环不进是左孩。
2.6 完整可运行的 BST 模板(含中序遍历)
cpp
运行
template<class T>
struct BSTNode {
T _data;
BSTNode<T>* _left;
BSTNode<T>* _right;
BSTNode(const T& key)
:_data(key)
,_left(nullptr)
,_right(nullptr)
{}
};
template<class T>
class BSTree {
typedef BSTNode<T> Node;
public:
// 插入
bool Insert(const T& key) {
if (_root == nullptr) {
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_data < key) {
parent = cur;
cur = cur->_right;
}
else if (cur->_data > key) {
parent = cur;
cur = cur->_left;
}
else {
return false;
}
}
cur = new Node(key);
if (parent->_data < key) {
parent->_right = cur;
} else {
parent->_left = cur;
}
return true;
}
// 查找
bool Find(const T& key) {
Node* cur = _root;
while (cur) {
if (cur->_data == key) return true;
else if (cur->_data > key) cur = cur->_left;
else cur = cur->_right;
}
return false;
}
// 删除
bool erase(const T& key) {
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_data < key) {
parent = cur;
cur = cur->_right;
}
else if (cur->_data > key) {
parent = cur;
cur = cur->_left;
}
else {
if (cur->_left && cur->_right) {
Node* minParent = cur;
Node* min = cur->_left;
while (min->_right) {
minParent = min;
min = min->_right;
}
swap(cur->_data, min->_data);
if (minParent->_left == min)
minParent->_left = min->_left;
else
minParent->_right = min->_left;
delete min;
}
else {
Node* child = cur->_left ? cur->_left : cur->_right;
if (cur == _root)
_root = child;
else {
if (cur == parent->_left)
parent->_left = child;
else
parent->_right = child;
}
delete cur;
}
return true;
}
}
return false;
}
// 中序遍历(升序输出,验证BST正确性)
void InOrder() {
_InOrder(_root);
cout << endl;
}
private:
Node* _root = nullptr;
void _InOrder(Node* root) {
if (!root) return;
_InOrder(root->_left);
cout << root->_data << " ";
_InOrder(root->_right);
}
};
2.7 BST 高频易错点汇总
插入节点时,必须先记录父节点,否则遍历到空节点后无法挂载新节点
删除节点时,必须处理根节点的特殊情况,避免空指针访问
双子节点删除时,必须判断替身节点是父节点的左 / 右孩子,不能固定修改某一个指针
所有操作必须保证不破坏 “左小右大” 的核心规则,否则 BST 会失效
函数所有分支必须有返回值,避免未定义行为
第三部分 终极复习背诵口诀
多态绑定:静态编译定地址,动态运行查表找
虚表规则:继承未改原样留,重写覆盖同下标,新增虚函数往后排
BST 核心:左小右大根中间,中序遍历升序排
BST 插入:小左大右找空位,遇空新建挂叶子
BST 删除:无子删、单子替,双子找最值,左右必判断
更多推荐



所有评论(0)