C++ 面向对象
C++作为面向对象编程(OOP)的经典语言,其核心优势在于通过封装、继承、多态三大特性实现代码的复用、扩展与维护。本文 主要介绍C++面向对象核心知识点,搭配代码示例和表格总结。
一、面向对象三大核心特性
面向对象编程的本质是"将数据与操作数据的方法绑定",通过三大特性解决代码冗余、扩展性差等问题。
1.1 封装(Encapsulation):数据安全
定义
将类的数据(成员变量) 和操作数据的方法(成员函数) 打包在一起,通过访问修饰符限制外部对内部数据的直接访问,仅暴露指定接口供外部调用。

核心目的
- 隐藏实现细节,提高代码安全性(避免外部误修改内部数据);
- 降低代码耦合度,外部仅需关注接口功能,无需关心内部实现。
访问修饰符
C++提供3种访问修饰符,控制成员的可见性,具体规则如下:
| 访问修饰符 | 类内部访问 | 子类(派生类)访问 | 外部(类实例/其他类)访问 | 典型用途 |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | 暴露接口(成员函数/常用变量) |
private |
✅ | ❌ | ❌ | 隐藏核心数据(如动态分配的内存) |
protected |
✅ | ✅ | ❌ | 子类需要继承的中间数据/方法 |
代码示例
cpp
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
// 私有成员:外部无法直接访问,仅类内可操作
string name;
int age;
public:
// 公有接口:外部通过接口操作私有数据
Person(string n, int a) : name(n), age(a) {} // 构造函数初始化
// 读取私有数据
string getName() const { return name; }
int getAge() const { return age; }
// 修改私有数据(可添加逻辑校验)
void setAge(int a) {
if (a >= 0 && a <= 120) { // 避免设置不合理年龄
age = a;
} else {
cout << "年龄输入非法!" << endl;
}
}
};
int main() {
Person p("张三", 20);
cout << "姓名:" << p.getName() << endl; // 输出:张三
cout << "初始年龄:" << p.getAge() << endl; // 输出:20
p.setAge(25); // 合法修改
cout << "修改后年龄:" << p.getAge() << endl; // 输出:25
p.setAge(150); // 非法修改,触发提示
return 0;
}
1.2 继承(Inheritance):代码复用
定义
让一个类(子类/派生类 )继承另一个类(父类/基类 )的属性和方法,并可在父类基础上扩展新功能,实现代码复用。

继承的类型
| 继承类型 | 核心目的 | 实现方式 |
|---|---|---|
| 实现继承 | 复用父类的代码与实现 | 子类继承父类的非虚成员函数与数据 |
| 接口继承 | 构建多态基础,统一接口规范 | 子类继承父类的纯虚函数(抽象类) |
核心规则
- 子类默认继承父类的所有成员(
public/protected/private),但访问权限受继承方式影响(如private继承会将父类public成员变为子类private); - 子类构造时会先调用父类构造函数,析构时先调用子类析构函数;
- 支持多层继承(如
A→B→C)和多重继承(如C→A且C→B)。
代码示例
cpp
#include <iostream>
#include <string>
using namespace std;
// 父类(基类)
class Animal {
protected:
string name; // 受保护成员:子类可访问,外部不可访问
public:
// 父类构造函数
Animal(string n) : name(n) {}
// 父类普通方法
void eat() {
cout << name << " 在吃东西" << endl;
}
};
// 子类(派生类):公有继承Animal
class Dog : public Animal {
public:
// 子类构造函数:必须先初始化父类
Dog(string n) : Animal(n) {}
// 子类扩展新方法
void bark() {
cout << name << " 汪汪叫!" << endl;
}
};
int main() {
Dog dog("旺财");
dog.eat(); // 继承父类方法:输出"旺财 在吃东西"
dog.bark(); // 子类扩展方法:输出"旺财 汪汪叫!"
return 0;
}
1.3 多态(Polymorphism):灵活扩展
定义
"同一接口,不同实现"------通过父类指针/引用指向子类对象,调用相同函数时,根据对象的实际类型执行不同逻辑,大幅提升代码扩展性。

多态的分类
| 多态类型 | 别称 | 绑定时机 | 实现方式 | 适用场景 |
|---|---|---|---|---|
| 编译时多态 | 静态多态 | 编译期 | 函数重载、运算符重载 | 参数类型/数量已知的场景 |
| 运行时多态 | 动态多态 | 运行期 | 虚函数(virtual)+ 重写 |
统一接口处理不同对象的场景 |
运行时多态的核心条件
- 父类声明虚函数(
virtual关键字); - 子类重写(
override)父类虚函数(函数名、参数、返回值完全一致); - 通过父类指针或引用调用虚函数。
代码示例(运行时多态)
cpp
#include <iostream>
#include <string>
using namespace std;
// 父类:含虚函数
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
// 虚函数:允许子类重写
virtual void speak() {
cout << name << " 发出声音" << endl;
}
};
// 子类Dog:重写虚函数
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
void speak() override { // override关键字:确保重写正确(C++11+)
cout << name << " 汪汪汪!" << endl;
}
};
// 子类Cat:重写虚函数
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
void speak() override {
cout << name << " 喵喵喵!" << endl;
}
};
// 统一接口:接收父类指针
void animalSpeak(Animal* animal) {
animal->speak(); // 运行时根据实际对象类型调用对应方法
}
int main() {
Dog dog("旺财");
Cat cat("咪宝");
animalSpeak(&dog); // 输出:旺财 汪汪汪!
animalSpeak(&cat); // 输出:咪宝 喵喵喵!
return 0;
}
三大特性对比总结
| 特性 | 核心思想 | 核心作用 | 关键关键字/技术 |
|---|---|---|---|
| 封装 | 数据+方法打包,隐藏细节 | 提高安全性、降低耦合度 | public/private/protected |
| 继承 | 子类复用父类功能 | 减少代码冗余、支持扩展 | public/private/protected(继承方式) |
| 多态 | 同一接口,不同实现 | 提升代码扩展性、灵活性 | virtual/override、虚函数表 |
二、核心概念辨析(易混淆点)
2.1 重载(Overload)vs 重写(Override)
重载 : 函数名相同,参数不同,发生在同一个作用域(类内)。
重写:派生类中重写父类的虚函数,函数名、参数、返回值完全相同,使用virtual 和 overide 关键字 。

对比表如下:
| 对比维度 | 重载(Overload) | 重写(Override) |
|---|---|---|
| 作用域 | 同一类内部 | 父类与子类之间(继承关系) |
| 函数签名 | 函数名相同,参数(类型/数量/顺序)不同 | 函数名、参数、返回值完全相同 |
| 返回值影响 | 与返回值无关(不能靠返回值区分) | 必须与父类一致(协变除外) |
| 关键字要求 | 无 | 父类需virtual,子类可加override |
| 绑定时机 | 编译期(静态绑定) | 运行期(动态绑定) |
| 多态类型 | 编译时多态 | 运行时多态 |
重载代码示例
cpp
//重载
class A {
public:
void show(int x) {}
void show(double x) {}
};
重写代码示例
cpp
//重写
class A {
public:
virtual void speak() {}
};
class B : public A {
public:
void speak() override {}
};
2.2 深拷贝(Deep Copy)vs 浅拷贝(Shallow Copy)
当类包含动态分配资源 (如堆内存指针)时,拷贝对象需区分深拷贝与浅拷贝,否则会导致内存泄漏或重复释放。
深拷贝是对对象的完全独立复制,包括对象内部动态分配的资源。在深拷贝中,不仅复制对象的值,还会复制对象所指向的堆上的数据。

对比表如下:
| 对比维度 | 浅拷贝(Shallow Copy) | 深拷贝(Deep Copy) |
|---|---|---|
| 拷贝内容 | 仅复制成员变量的值(指针复制地址) | 复制成员变量+动态分配的资源(指针指向新内存) |
| 资源归属 | 原对象与新对象共享动态资源 | 原对象与新对象各自拥有独立资源 |
| 实现方式 | 编译器默认生成(拷贝构造函数/赋值运算符) | 手动实现拷贝构造函数和赋值运算符 |
| 风险 | 重复释放内存、内存泄漏 | 无(需手动管理内存) |
浅拷贝代码示例
cpp
class Shallow {
public:
int* data;
Shallow(int x) {
data = new int(x);
}
// 默认浅拷贝
};
深拷贝代码示例
cpp
class Deep {
public:
int* data;
Deep(int x) {
data = new int(x);
}
// 手动深拷贝
Deep(const Deep& other) {
data = new int(*other.data);
}
// 赋值运算符重载
Deep& operator=(const Deep& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
~Deep() {
delete data;
}
};
三、构造函数与析构函数:生命周期管理
构造函数负责对象的初始化,析构函数负责对象的资源清理,二者是面向对象中管理对象生命周期的核心。
3.1 构造函数(Constructor)
定义
对象创建时自动调用的特殊成员函数,用于初始化成员变量、分配资源(如堆内存、文件句柄)。

核心特点
- 函数名与类名完全一致,无返回类型(包括
void); - 可重载(支持多个构造函数,参数不同);
- 若未手动定义,编译器会生成默认构造函数(无参数、无实现);
- 构造顺序:先父类构造,再子类构造。
构造函数的类型
| 类型 | 定义与作用 |
|---|---|
| 默认构造函数 | 无参数,用于无初始化值的对象创建 |
| 带参数构造函数 | 含参数,支持自定义初始化 |
| 拷贝构造函数 | 接收同类型对象引用,创建副本 |
| 委托构造函数 | 一个构造函数调用同类另一个构造函数,减少冗余 |
默认构造函数代码示例
cpp
class A {
public:
A() {
cout << "默认构造" << endl;
}
};
A a; // 调用默认构造
带参数构造函数代码示例
cpp
class A {
public:
int x;
A(int val) : x(val) {} // 初始化列表
};
A a(10); // x=10
拷贝构造函数代码示例
cpp
class A {
public:
int x;
A(const A& other) : x(other.x) {}
};
A a1(10);
A a2 = a1; // 拷贝构造,a2.x=10
委托构造函数代码示例
cpp
class A {
public:
int x;
A(int val) : x(val) {}
A() : A(0) {} // 委托给带参数构造,x默认0
};
A a; // x=0
3.2 析构函数(Destructor)
定义
对象生命周期结束时(如出作用域、delete调用)自动调用的特殊成员函数,用于释放资源(如堆内存、关闭文件)。
核心特点
- 函数名:
~类名,无参数、无返回类型,不能重载(一个类仅一个析构函数); - 若未手动定义,编译器生成默认析构函数(仅释放栈内存,不处理堆资源);
- 析构顺序:先子类析构,再父类析构。
虚析构函数
作用
当通过父类指针删除子类对象时,确保子类析构函数被调用,避免子类资源泄漏。
代码示例(虚析构函数的必要性)
cpp
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base构造" << endl; }
// 非虚析构函数(错误示范)
~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int(10)) { cout << "Derived构造" << endl; }
~Derived() {
delete data; // 释放子类动态资源
cout << "Derived析构" << endl;
}
};
int main() {
Base* ptr = new Derived(); // 父类指针指向子类对象
delete ptr; // 仅调用Base析构,Derived析构未调用→内存泄漏
return 0;
}
修正:将父类析构改为虚析构
cpp
class Base {
public:
Base() { cout << "Base构造" << endl; }
virtual ~Base() { cout << "Base析构" << endl; } // 虚析构函数
};
// 输出结果(正确):
// Base构造
// Derived构造
// Derived析构
// Base析构
3.3 关键问题:为什么构造函数不能是虚函数?
-
vptr初始化时机矛盾 :虚函数依赖虚指针(
vptr)指向虚函数表(vtable),但vptr在构造函数执行前由编译器初始化,且先指向父类vtable;若构造函数是虚函数,调用时vptr未指向子类vtable,动态绑定无法生效。 -
构造逻辑冲突:构造函数的核心是"创建对象",需明确初始化顺序(父类→子类);而虚函数的核心是"动态绑定",依赖对象已完全创建,二者逻辑矛盾。

一是构造函数执行时,虚指针(vptr)还未指向子类的虚函数表,动态绑定无法生效,虚函数失去意义;二是父类构造阶段子类成员未初始化,若强行通过虚构造调用子类逻辑,会访问未初始化的资源,引发程序崩溃。因此 C++ 直接禁止构造函数声明为虚函数。
四、虚函数与虚函数表:多态的底层实现
运行时多态的核心是虚函数表(vtable) 和虚指针(vptr),理解其底层机制是掌握C++多态的关键。
4.1 核心概念
| 概念 | 定义与作用 | 级别(类/对象) |
|---|---|---|
| 虚函数表(vtable) | 编译期为含虚函数的类生成的"函数地址数组",存储该类所有虚函数的地址 | 类级别(所有对象共享) |
| 虚指针(vptr) | 每个对象隐含的隐藏指针,指向所属类的vtable | 对象级别(每个对象独有) |
4.2 底层工作流程(以Animal→Dog为例)

- 编译期 :编译器为
Animal(含虚函数)和Dog(重写虚函数)分别生成vtable:Animal的vtable:存储Animal::speak()的地址;Dog的vtable:基于Animal的vtable构建,覆盖speak()为Dog::speak()的地址。
- 运行期构造阶段 :
- 创建
Dog对象时,先调用Animal构造函数,vptr指向Animal的vtable; Animal构造完成后,调用Dog构造函数,vptr更新为指向Dog的vtable。
- 创建
- 调用虚函数时 :
- 通过父类指针
Animal* ptr = new Dog()调用ptr->speak(); - 编译器通过
ptr指向的对象的vptr找到Dog的vtable; - 从vtable中取出
Dog::speak()的地址,执行对应逻辑。
- 通过父类指针
4.3 虚函数表常见问题
| 问题 | 答案 |
|---|---|
| 虚表会拖慢程序吗? | 影响极小。虚函数调用是"vptr→vtable→函数地址"的定向跳转,时间复杂度O(1),仅比普通函数调用慢微乎其微 |
| 虚表是编译期还是运行期生成? | 编译期生成。每个含虚函数的类在编译时生成唯一vtable,运行时仅通过vptr指向即可 |
| 无虚函数的类有vtable吗? | 无。仅当类含虚函数(或继承自含虚函数的类)时,编译器才生成vtable和vptr |
| 虚表是对象级别的吗? | 不是,是类级别。同一类的所有对象共享一张vtable,仅需各自存储vptr即可(节省内存) |
| 非虚函数会进入vtable吗? | 不会。仅虚函数会被存入vtable,非虚函数调用是静态绑定,与vtable无关 |
| 构造函数/静态成员函数能是虚函数吗? | 不能。构造函数因vptr初始化时机矛盾;静态成员函数无this指针,无法指向vtable |
五、抽象类与纯虚函数:接口规范
5.1 纯虚函数(Pure Virtual Function)
定义
没有实现的虚函数,声明格式为virtual 返回值 函数名(参数) = 0;,强制子类提供实现。
核心特点
- 无函数体(仅声明);
- 包含纯虚函数的类无法实例化;
- 子类必须重写纯虚函数,否则子类也无法实例化(成为抽象类)。
5.2 抽象类(Abstract Class)
定义
包含至少一个纯虚函数的类,本质是"接口规范",用于定义子类必须实现的功能,不能直接创建对象。
代码示例
cpp
#include <iostream>
using namespace std;
// 抽象类:含纯虚函数
class Shape {
public:
// 纯虚函数:定义接口,无实现
virtual void draw() const = 0;
virtual double area() const = 0;
};
// 子类Circle:实现纯虚函数
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
void draw() const override {
cout << "绘制圆形" << endl;
}
double area() const override {
return 3.14 * radius * radius;
}
};
int main() {
// Shape s; // 错误:抽象类不能实例化
Shape* circle = new Circle(5);
circle->draw(); // 输出:绘制圆形
cout << "面积:" << circle->area() << endl; // 输出:78.5
delete circle;
return 0;
}
5.3 虚函数vs纯虚函数对比表
| 对比维度 | 虚函数(Virtual Function) | 纯虚函数(Pure Virtual Function) |
|---|---|---|
| 实现情况 | 有默认实现(基类中提供) | 无实现(仅声明) |
| 子类要求 | 可选重写(不重写则使用基类实现) | 必须重写(否则子类为抽象类) |
| 类的实例化 | 所在类可实例化 | 所在类为抽象类,不可实例化 |
| 核心作用 | 实现运行时多态 | 定义接口规范,强制子类实现功能 |
| 声明格式 | virtual void func() {} |
virtual void func() = 0; |
六、运算符重载:让自定义类型支持运算符
6.1 定义与本质
运算符重载是"将运算符重新定义为成员函数或非成员函数",本质是函数调用,让自定义类型(如Person、Circle)支持+、==、<<等运算符。
核心规则
- 不能改变运算符的优先级、结合性、操作数个数;
- 不能发明新运算符(如
@、#); - 部分运算符必须作为成员函数(如
=、[]、->、()); - 运算符函数名格式:
operator+(+为要重载的运算符)。
6.2 常见运算符重载
| 运算符类型 | 建议实现方式 |
|---|---|
| 算术运算符(+、-) | 非成员函数(友元) |
| 赋值运算符(=) | 成员函数 |
| 输出运算符(<<) | 非成员函数(友元) |
| 下标运算符([]) | 成员函数 |
算术运算符(+)重载代码示例(以Point类为例)
cpp
class Point {
public:
int x, y;
Point(int x=0, int y=0) : x(x), y(y) {}
// 友元函数重载+
friend Point operator+(const Point& p1, const Point& p2) {
return Point(p1.x+p2.x, p1.y+p2.y);
}
};
Point p1(1,2), p2(3,4);
Point p3 = p1 + p2; // (4,6)
赋值运算符(=)重载代码示例
cpp
Point& operator=(const Point& other) {
if (this != &other) {
x = other.x;
y = other.y;
}
return *this;
}
输出运算符(<<)重载代码示例
cpp
friend ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.x << "," << p.y << ")";
return os;
}
cout << p1; // 输出(1,2)
下标运算符([])重载代码示例
cpp
int& operator[](int idx) {
if (idx == 0) return x;
else if (idx == 1) return y;
throw "下标越界";
}
Point p(1,2);
cout << p[0]; // 输出1
6.3 不建议重载的运算符
- 逗号运算符(
,):本身定义了求值顺序,重载后易混淆; - 逻辑与(
&&)、逻辑或(||):重载后失去短路求值特性; - 取地址运算符(
&):默认行为已满足大多数场景,重载易出错。
七、静态成员:类级别的共享资源
静态成员(静态成员变量、静态成员函数)属于类本身,而非对象,所有对象共享同一资源,常用于实现全局计数器、工具函数等。
7.1 静态成员变量(Static Member Variable)
核心特点
- 存储在全局数据区,而非对象内存中;
- 必须在类外定义并初始化(类内仅声明);
- 可通过
类名::变量名或对象.变量名访问(推荐类名访问)。
代码示例
cpp
#include <iostream>
using namespace std;
class Student {
public:
static int count; // 类内声明静态成员变量(计数器)
string name;
Student(string n) : name(n) {
count++; // 每创建一个对象,计数器+1
}
};
// 类外定义并初始化静态成员变量
int Student::count = 0;
int main() {
Student s1("张三");
Student s2("李四");
cout << "学生总数:" << Student::count << endl; // 输出2(推荐)
cout << "学生总数:" << s1.count << endl; // 输出2(不推荐)
return 0;
}
7.2 静态成员函数(Static Member Function)
核心特点
- 无隐含
this指针(无法访问非静态成员变量/函数); - 可通过
类名::函数名直接调用,无需创建对象; - 不能是虚函数(无
this指针,无法指向vtable)。
代码示例
cpp
class MathTool {
public:
// 静态成员函数:工具函数(无需对象)
static int max(int a, int b) {
return a > b ? a : b;
}
};
int main() {
// 直接通过类名调用,无需创建MathTool对象
cout << MathTool::max(10, 20) << endl; // 输出20
return 0;
}
7.3 静态成员vs普通成员对比表
| 对比维度 | 静态成员(变量/函数) | 普通成员(变量/函数) |
|---|---|---|
| 归属 | 类级别(所有对象共享) | 对象级别(每个对象独有) |
| 访问方式 | 类名::成员名(推荐)、对象.成员名 | 对象.成员名、指针->成员名 |
| this指针 | 无 | 有(隐含参数) |
| 访问限制 | 只能访问静态成员 | 可访问静态+非静态成员 |
| 内存分配 | 全局数据区(程序启动时分配) | 对象内存中(创建对象时分配) |
| 虚函数支持 | 不支持(静态函数不能是虚函数) | 支持(普通成员函数可声明为虚函数) |
八、多重继承与菱形问题
8.1 多重继承(Multiple Inheritance)
定义
一个子类同时继承多个父类(如class C : public A, public B),可复用多个父类的功能,但易引发二义性。
代码示例(未解决菱形问题)
cpp
#include <iostream>
using namespace std;
// 顶层父类
class Animal {
public:
void eat() {
cout << "Animal 吃东西" << endl;
}
};
// 中间父类1
class Mammal : public Animal {};
// 中间父类2
class Bird : public Animal {};
// 子类:同时继承Mammal和Bird(菱形继承)
class Bat : public Mammal, public Bird {};
int main() {
Bat bat;
// bat.eat(); // 错误:二义性(Mammal::eat和Bird::eat都继承自Animal)
return 0;
}
8.2 菱形问题(Diamond Problem)
现象
当子类间接继承自同一个顶层父类(如Bat→Mammal→Animal且Bat→Bird→Animal)时,子类会包含顶层父类的两个副本,导致调用父类成员时出现二义性。
解决方案:虚继承(Virtual Inheritance)
在中间父类继承顶层父类时,添加virtual关键字,确保子类仅包含顶层父类的一个副本,消除二义性。
修正后代码
cpp
#include <iostream>
using namespace std;
class Animal {
public:
void eat() {
cout << "Animal 吃东西" << endl;
}
};
// 虚继承:Mammal共享Animal副本
class Mammal : virtual public Animal {};
// 虚继承:Bird共享Animal副本
class Bird : virtual public Animal {};
class Bat : public Mammal, public Bird {};
int main() {
Bat bat;
bat.eat(); // 正确:仅一个Animal副本,输出"Animal 吃东西"
return 0;
}
九、常见面试题
| 面试问题 | 核心答案 |
|---|---|
| C++面向对象三大特性是什么?分别作用? | 封装(数据安全)、继承(代码复用)、多态(灵活扩展) |
| 重载和重写的区别? | 重载:同作用域、参数不同、编译期绑定;重写:继承关系、签名相同、运行期绑定 |
| 构造函数为什么不能是虚函数? | vptr未初始化,动态绑定无法生效;构造逻辑需明确顺序,与虚函数矛盾 |
| 析构函数为什么要声明为虚函数? | 避免父类指针删除子类对象时,子类析构函数未调用导致资源泄漏 |
| 虚函数表是类级别还是对象级别? | 类级别(所有对象共享),vptr是对象级别 |
| 深拷贝和浅拷贝的区别? | 浅拷贝共享动态资源(风险),深拷贝复制动态资源(安全) |
| 抽象类的特点? | 含纯虚函数,不能实例化,子类必须重写纯虚函数 |
| 哪些函数不能声明为虚函数? | 构造函数、静态成员函数、内联函数、友元函数、普通非成员函数 |
| 多重继承的问题及解决方案? | 菱形继承导致二义性;解决方案:虚继承(virtual关键字) |
十、总结
- C++面向对象核心是封装、继承、多态,其中多态依赖虚函数表(vtable)和虚指针(vptr)实现运行时动态绑定;
- 易混淆概念中,重载是同作用域参数不同的静态绑定,重写是继承关系下的动态绑定,深拷贝需手动实现以避免动态资源共享风险;
- 关键细节上,构造函数不能为虚函数,析构函数建议设为虚函数,抽象类含纯虚函数不可实例化,多重继承需用虚继承解决菱形问题。.
C++多态的本质是"接口统一,实现各异 ",其底层依赖vtable与vptr的协作,核心流程可总结为:
- 编译期:为含虚函数的类生成vtable(存储虚函数地址);
- 运行期构造:对象的vptr在父类构造时指向父类vtable,子类构造时更新为子类vtable;
- 调用虚函数:通过父类指针/引用找到对象的vptr,再通过vtable定位到实际要执行的函数地址,实现动态绑定。