跳到主要内容
C++ 多态详解:编译时与运行时多态原理及实现 | 极客日志
C++ 算法
C++ 多态详解:编译时与运行时多态原理及实现 C++ 多态包含编译时多态与运行时多态。编译时多态利用函数重载、运算符重载及模板实现,具备类型安全与零运行时开销优势,但存在代码膨胀风险。运行时多态依赖虚函数与虚函数表,通过动态绑定实现基类指针调用派生类行为。虚析构函数保障多态对象资源安全释放。掌握虚函数表布局及动态绑定机制是深入理解 C++ 面向对象编程的关键。
芝士奶盖 发布于 2026/3/15 更新于 2026/4/27 7 浏览C++ 多态详解
多态的概念
众所周知,面向对象有三大特性,封装、继承和多态!
多态(Polymorphism)是面向对象编程的核心特性,允许用统一的接口操作不同类型的对象,并根据对象实际类型执行不同的行为。C++中的多态分为编译时多态和运行时多态。
编译时多态(静态多态)
编译时多态(Compile-Time Polymorphism)又称为静态多态,是一种在代码编译阶段就能确定具体调用行为的机制。它的核心特点是基于静态类型系统,通过代码结构直接决定调用哪个函数或操作,无需运行时动态查找。
编译时多态的实现方式
函数重载 (Function Overloading)
示例:
void print (int x) {
std::cout << "int: " << x << std::endl;
}
void print (double x) {
std::cout << "double: " << x << std::endl;
}
void print (const char * s) {
std::cout << "const char *: " << s << std::endl;
}
调用策略:
int main () {
print (10 );
print (3.14 );
print ("Hello" );
}
输出:
int : 10 : *: Hello
double
3.14
const
char
运算符重载 (Operator Overloading) class Complex {
private :
double real;
double imag;
public :
Complex (double r = 0 , double i = 0 ) : real (r), imag (i) {}
Complex operator +(const Complex &other) {
return Complex (real + other.real, imag + other.imag);
}
void print () {
std::cout << real << " + " << imag << "i" << std::endl;
}
};
int main () {
Complex c1 (1 , 2 ) ;
Complex c2 (3 , 4 ) ;
Complex c3 = c1 + c2;
std::cout << "Complex addition: " ;
c3. print ();
}
模板 (Templates) template <typename T>
T Max (T a, T b) {
return (a > b) ? a : b;
}
int main () {
std::cout << Max (3 , 5 ) << std::endl;
std::cout << Max (2.7 , 3.14 ) << std::endl;
return 0 ;
}
template <typename T>
class Stack {
public :
void push (const T &item) { _elements.push_back (item); }
T pop () { }
private :
std::vector<T> _elements;
};
int main () {
Stack<int > int_Stack;
Stack<std::string> str_Stack;
return 0 ;
}
编译时多态的特点
静态绑定 (Static Binding)
定义:函数调用在编译阶段确定,编译器根据调用时的静态类型直接绑定到具体实现。
int add (int a, int b) ;
double add (double a, double b) ;
int main () {
add (3 , 5 );
add (3.0 , 5.0 );
return 0 ;
}
编译器通过函数签名(函数名 + 参数类型)匹配最佳候选
生成的目标代码中直接写入函数地址,无运行时决策
特性 静态绑定 动态绑定(虚函数) 决策时机 编译时 运行时 性能开销 无额外开销 虚表查找(1~2 次指针跳转) 灵活性 固定 可动态切换
类型安全 (Type Safety) template <typename T>
T Max (T a, T b) {
return (a > b) ? a : b;
}
int main () {
Max (3 , 5.0 );
return 0 ;
}
编译出错:double 和 int 类型不一致,模板无法正确推导
编译器严格检查模板参数类型一致性
避免隐式类型转换导致的意外行为
std::vector<int > v;
v.push_back ("Hello, World" );
静态多态,又叫做编译时多态,显而易见在编译器进行编译时,就会对多态的正确性进行检测,如果发现有错误,则无法编译通过,所以是类型安全的,这也是其优点之一
无运行时开销
template <typename T>
T square (T x) {
return x * x;
}
int main () {
square (5 );
square (3.14 );
return 0 ;
}
每个类型特化生成独立的机器代码
调用时直接跳转到具体函数,无间接寻址,提高运行效率
以上都是编译时多态的优点,其实编译时多态也有缺点,就是会导致代码膨胀,二进制文件的体积过大!
代码膨胀 (Code bloat) template <typename T>
class Wrapper {
T data;
};
int main () {
Wrapper<int > w1;
Wrapper<double > w2;
return 0 ;
}
每个不同类型参数生成完全独立的二进制代码
可能导致可执行文件体积显著增大
凡事都有两面性,直接生成代码,避免了间接寻址带来的性能损耗,无运行时开销,提高了效率,但同时也生成多种不同版本的二进制代码,代码膨胀,会导致编译链接后生成的可执行文件体积增大!
运行时多态(动态多态) 要理解运行时多态,首先要知道虚函数的概念,因为 C++ 多态的核心机制就是派生类对基类虚函数的重写。
认识虚函数 (Virtual function)
虚函数(Virtual Function)是实现运行时多态(动态多态)的核心机制。它允许通过基类指针或引用调用派生类的重写函数,是面向对象编程中实现'一个接口,多种实现'的关键工具。
虚函数是用 virtual 关键字声明的成员函数,用于实现运行时多态。
类成员函数前面加 virtual 修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加 virtual 修饰。
它允许派生类重写(Override)基类的函数实现,并通过基类指针/引用调用派生类的版本。
class Animal {
public :
virtual void speak () {
std::cout << "Animal sound\n" ;
}
};
class Dog : public Animal {
public :
void speak () override {
std::cout << "Woof!\n" ;
}
};
通过基类调用:通过基类指针/引用调用虚函数时,实际调用的是对象实际类型的函数:
int main () {
Animal *animal = new Dog ();
animal->speak ();
delete animal;
return 0 ;
}
虚函数的重写/覆盖 (override 和 final)
虚函数重写(Override)是指派生类重新定义基类的虚函数,实现同签名不同行为。它是实现运行时多态的关键机制。
条件 说明 基类函数为虚函数 基类函数必须使用 virtual 声明 函数签名一致 派生类函数必须与基类的函数名、参数类型/数量/顺序、const 限定符完全一致 访问权限允许 派生类函数访问权限不能比基类更严格(如基类为 public,派生类不能为 private)
派生类在重写基类虚函数时,在不加 virtual 关键字时,虽然也可以构成重写 (因为继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
不过要牢记,即使派生类继承下来的虚函数即便不加 virtual 关键字也可以重写基类的虚函数,构成多态!
错误原因:派生类中重写的函数与基类虚函数的参数列表、返回类型(非协变类型)或 const 修饰符不一致。
后果:编译器不会报错,但函数不会正确覆盖基类虚函数,而是形成隐藏(hide)。这个之前讲过,派生类会隐藏和基类中函数签名相同的函数。
class Base {
public :
virtual void func (int ) {}
};
class Derived : public Base {
public :
void func (double ) {}
};
错误原因:未在派生类中使用 C++11 引入的 override 关键字明确标记重写。
后果:若函数签名错误,编译器可能不会提示错误,导致隐藏而非覆盖。
class Derived : public Base {
public :
void func (int ) override {}
};
错误原因:基类函数未用 virtual 修饰,但派生类试图重写。
后果:派生类函数与基类函数是独立的,无法通过基类指针/引用调用多态 。
class Base {
public :
void func () {}
};
class Derived : public Base {
public :
void func () {}
};
- override 是 C++11 引入的一个新特性,它并非强制要求。在没有 override 关键字时,满足条件重写的条件同样可以构成虚函数的重写,只是若派生类函数使用 override 声明,但未正确重写基类虚函数(如函数名、参数列表或常量性不匹配),编译器会报错,有助于在编译阶段发现错误。
C++11 还提供了一个关键字叫做 final,如果我们不想让派生类重写这个虚函数,那么可以用 final 去修饰。
class Car {
public :
virtual void Drive () final {}
};
class Benz : public Car {
public :
virtual void Drive () {
std::cout << "Benz" << std::endl;
}
};
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
class A {
public :
virtual void func (int val = 1 ) {
std::cout << "A->" << val << std::endl;
}
virtual void test () {
func ();
}
};
class B : public A {
public :
void func (int val = 0 ) {
std::cout << "B->" << val << std::endl;
}
};
int main (int argc, char * argv[]) {
B *p = new B;
p->test ();
return 0 ;
}
正确的答案是 B 哦!不知道你做对了没有,可以想想以下两个问题哦:
1. func 是否满足虚函数的重写条件?
2. 默认参数的确定实在编译时还是运行时呢?
纯虚函数 (Pure Virtual Function) 和抽象类
纯虚函数(Pure Virtual Function)
是一种没有具体实现的虚函数,其存在的目的是强制派生类必须实现该函数。它的声明方式是在虚函数声明末尾添加 = 0。
virtual 返回类型 函数名 (参数) = 0 ;
定义接口规范:纯虚函数为所有派生类定义一个必须实现的统一接口。
创建抽象类:包含纯虚函数的类称为抽象类,它不能被直接实例化(不能创建对象)。
强制派生类实现:所有直接继承自抽象类的派生类必须重写(override)纯虚函数,否则派生类也会成为抽象类。
简单来说就是,如果基类定义了纯虚函数,那么这个基类被称为抽象类,不能用来创建对象,同时继承了该类的派生类必须重写该纯虚函数,否则派生类也将成为抽象类。这样一来,基类可以提供一个统一的接口,具体实现交给不同的派生类实现。
class Animal {
public :
virtual void sound () const = 0 ;
virtual ~Animal () {}
};
class Dog : public Animal {
public :
void sound () const override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public :
void sound () const override {
std::cout << "Meow!" << std::endl;
}
};
- C++ 允许基类为纯虚函数提供默认实现,但派生类仍需显式重写,注意纯虚函数的实现必须在类外!
class Animal {
public :
virtual void sound () const = 0 ;
};
void Animal::sound () const {
std::cout << "Default animal sound" << std::endl;
}
class Dog : public Animal {
public :
void sound () const override {
Animal::sound ();
std::cout << "Woof!" << std::endl;
}
};
虚析构函数 (Virtual Destructor) class Base {
public :
~Base () {
std::cout << "Base destructor\n" ;
}
};
class Derived : public Base {
public :
int * data;
Derived () {
data = new int [100 ];
}
~Derived () {
delete [] data;
std::cout << "Derived destructor\n" ;
}
};
int main () {
Base *obj = new Derived ();
delete obj;
return 0 ;
}
不难发现,当我们用基类的指针指向派生类,释放基类指针时,只调用了基类的析构函数,释放了基类中的资源,并没有调用派生类的析构函数,这会导致什么问题呢?
是不是会导致资源没有正确释放,派生类中的 data 指向的 100 个 int 内存未被释放,会导致内存泄漏。
如果基类中还有其他动态资源,比如文件句柄、数据库连接等资源,这些资源也会泄漏。会对整个程序造成重大影响!这时就需要使用虚析构函数来解决问题 !
虚析构函数(Virtual Destructor)是 C++ 中用于解决多态对象资源释放问题的关键机制。它通过动态绑定确保通过基类指针删除派生类对象时,派生类和基类的析构函数都能被正确调用,避免资源泄漏。
虚析构函数是用 virtual 关键字声明的析构函数。
当基类指针指向派生类对象时,若基类的析构函数是虚函数,删除该指针会触发动态绑定,确保调用实际对象类型的析构函数。
class Base {
public :
virtual ~Base () {
std::cout << "Base destroyed\n" ;
}
};
class Derived : public Base {
public :
~Derived () override {
std::cout << "Derived destroyed\n" ;
}
};
int main () {
Base *obj = new Derived ();
delete obj;
return 0 ;
}
Derived destroyed
Base destroyed
还记得之前讲到的,虚函数重写的要求吗?派生类的析构函数与基类的析构函数名称都不一样!怎么能构成重写呢?
实际上,基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成 destructor,所以基类的析构函数加了 virltual 修饰,派⽣类的析构函数就构成重写。
协变 (Covariant Return Types)
C++ 虚函数的协变允许派生类在重写基类虚函数时,将返回类型替换为基类函数返回类型的派生类指针或引用。
简单来说就是:派生类重写基类虚函数时,可以与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解⼀下即可。
基类和派生类的虚函数返回类型必须为指针或引用。
派生类返回类型必须是基类返回类型的直接或间接派生类。
函数参数列表必须完全相同。
class Fruit {
public :
virtual Fruit* clone () const {
return new Fruit (*this );
}
virtual ~Fruit () {}
};
class Apple : public Fruit {
public :
Apple* clone () const override {
return new Apple (*this );
}
void sayName () const {
std::cout << "I am an Apple!" << std::endl;
}
};
int main () {
Fruit* fruit = new Apple ();
Fruit* cloned = fruit->clone ();
if (Apple* apple = dynamic_cast <Apple*>(cloned)) {
apple->sayName ();
} else {
std::cout << "Cloning failed!" << std::endl;
}
delete fruit;
delete cloned;
return 0 ;
}
基类虚函数返回 Fruit*
派生类重写时返回 Apple*(Apple 是 Fruit 的派生类)
符合协变要求:返回类型是基类返回类型的派生类指针/引用
通过基类指针 Fruit* 调用 clone(),实际调用 Apple::clone()
返回的 Apple* 可以隐式转换为 Fruit*,但保留了实际类型信息
使用 dynamic_cast 将 Fruit* 转回 Apple*,验证协变正确性
调用 Apple 类的特有方法 sayName()
对于协变大家了解即可,其实底层原理也是多态的虚函数表指针。
重载(Overload)、重写(Override)、隐藏(Hide)的对比 特性 重载(Overload) 重写(Override) 隐藏(Hide) 定义 同一作用域内,同名函数参数不同 派生类重写基类虚函数 派生类同名函数遮蔽基类同名函数 作用域 同一类或同一命名空间 基类与派生类之间 基类与派生类之间 函数签名要求 函数名相同,参数列表不同 函数名、参数、返回类型均相同 函数名相同,参数可同可不同 virtual 关键字 不需要 基类函数必须为虚函数 不需要 多态性 无 支持动态多态(运行时绑定) 无(静态绑定) 示例场景 同一类中的多个构造函数 派生类重写基类的虚函数 派生类定义与基类同名的非虚函数
重载(Overload)
重载规则:在同一作用域内,函数名相同,但参数列表不同(参数类型、数量、顺序不同)。
返回类型无关:仅返回类型不同不构成重载,会导致编译错误。
典型场景:同一类中的多个构造函数、工具函数。
class Calculator {
public :
int add (int a, int b) {
return a + b;
}
double add (double a, double b) {
return a + b;
}
int add (int a, int b, int c) {
return a + b + c;
}
};
重写(Override)
重写规则:派生类重新定义基类的虚函数,要求函数名、参数列表、返回类型完全一致(协变返回类型例外)。
多态性:通过虚函数表(vtable)实现运行时多态。
必须使用 virtual:基类函数声明为 virtual,派生类建议使用 override 明确意图(C++11+)。
隐藏(Hide)
隐藏规则:派生类定义与基类同名的函数(无论参数是否相同),导致基类同名函数被隐藏。
同名同参非虚函数:派生类函数隐藏基类函数(即使基类函数非虚)。
同名不同参:派生类函数隐藏基类所有同名函数(包括重载版本)。
class Base {
public :
void func () {
std::cout << "Base::func()\n" ;
}
void func (int ) {
std::cout << "Base::func(int)\n" ;
}
};
class Derived : public Base {
public :
void func () {
std::cout << "Derived::func()\n" ;
}
};
int main () {
Derived d;
d.func ();
d.Base::func (1 );
return 0 ;
}
多态的原理
虚函数表 (vtable)
虚函数表的概念
虚函数表(vtable)是在编译期间,编译器为每个包含虚函数的类生成的静态表,存储该类所有虚函数的地址,生成后不可修改。
虚函数指针(vptr)是每个对象实例中隐含的指针,指向其所属类的虚函数表。
A. 编译报错
B. 运行报错
C. 8
D. 12
class Base {
public :
virtual void Func1 () {
std::cout << "Func1()" << std::endl;
}
protected :
int _b = 1 ;
char _ch = 'x' ;
};
int main () {
Base b;
std::cout << sizeof (b) << std::endl;
return 0 ;
}
int _b:占 4 字节。
char _ch:占 1 字节。
结构体的总大小需对齐到 4 字节(int 的对齐要求)。
char _ch 后需填充 3 字节以满足对齐。
C++ 中类和 C 语言中的结构体都满足内存对齐的规则,所以成员变量一共占了 8 个字节,那么还有另外 4 个字节是什么呢?
还多⼀个 __vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针 (v 代表 virtual,f 代表 function)。⼀个含有虚函数的类中都至少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同⼀张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
派生类由两部分构成,继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
基类的虚函数地址,
派生类重写的虚函数地址完成覆盖,
派生类自己的虚函数地址三个部分。
class Base {
public :
virtual void func1 () {}
virtual void func2 () {}
int data;
};
+
| vptr |
| int data |
+
Base 的 vtable:
+----------------+
|&Base::func1 |
|&Base::func2 |
+----------------+
虚函数表的生成规则 虚表在编译期间由编译器生成,程序启动时加载到内存,生命周期与程序一致。
若派生类重写基类虚函数,替换表中对应的函数指针。
若派生类新增虚函数,追加到表末尾。
class Derived : public Base {
public :
void func1 () override {}
virtual void func3 () {}
};
+----------------+
|&Derived::func1|
|&Base::func2 |
|&Derived::func3|
+----------------+
每个基类对应独立的虚函数表,派生类合并所有基类的表并调整 vptr 偏移。
class Base1 {
virtual void f1 () ;
};
class Base2 {
virtual void f2 () ;
};
class Derived : public Base1, public Base2 {
void f1 () override {}
void f2 () override {}
};
+
| Base1 的 vptr |
| Base1 成员变量 ||
| Base2 的 vptr |
| Base2 成员变量 ||
| Derived 成员变量|
+
多态是如何实现的
多态(Polymorphism)主要通过虚函数(virtual functions)和虚函数表(vtable)实现,核心是动态绑定(Dynamic Binding)。
动态绑定和静态绑定
动态绑定(Dynamic Binding)和静态绑定(Static Binding)是函数调用的两种不同解析机制,直接影响程序的执行行为。
特性 静态绑定(早绑定) 动态绑定(晚绑定) 解析时机 编译时确定调用的具体函数 运行时根据对象实际类型确定调用的函数 实现机制 函数地址直接硬编码到代码中 通过虚函数表(vtable)和虚指针(vptr)动态查找 性能 高(无运行时开销) 较低(需要查表和间接调用) 灵活性 低(固定行为) 高(支持多态) 应用场景 普通函数、非虚成员函数、模板函数 虚函数(多态调用)
编译时确定:编译器根据调用者的静态类型(声明类型)直接绑定函数地址。
无运行时决策:无论实际对象是什么类型,调用的函数在编译时已固定。
class Base {
public :
void nonVirtualFunc () {
std::cout << "Base\n" ;
}
};
class Derived : public Base {
public :
void nonVirtualFunc () {
std::cout << "Derived\n" ;
}
};
int main () {
Base* obj = new Derived ();
obj->nonVirtualFunc ();
delete obj;
return 0 ;
}
Base 类的 nonVirtualFunc() 没有使用 virtual 关键字,因此它是一个普通成员函数,不具备多态性。
obj 的类型是 Base*,编译器在编译时根据指针的静态类型(即 Base*)确定调用 Base::nonVirtualFunc(),与指针实际指向的对象类型无关。
Derived 类中的 nonVirtualFunc() 并未覆盖基类函数,而是定义了一个同名函数。这种现象称为函数隐藏(NameHiding)。
若通过 Derived 类对象或指针调用 nonVirtualFunc(),才会调用派生类版本。
通过基类指针调用时,只能访问基类的函数。不具备多态性。
全局函数调用:
template <typename T>
void templateFunc (T t) { }
templateFunc (42 );
运行时确定:通过虚函数表(vtable)和虚指针(vptr)动态查找函数地址。
依赖虚函数:只有虚函数(标记为 virtual 的成员函数)支持动态绑定。
class Base {
public :
virtual void virtualFunc () {
std::cout << "Base\n" ;
}
};
class Derived : public Base {
public :
void virtualFunc () override {
std::cout << "Derived\n" ;
}
};
int main () {
Base* obj = new Derived ();
obj->virtualFunc ();
delete obj;
return 0 ;
}
class Base {
public :
virtual ~Base () {}
};
class Derived : public Base {
public :
~Derived () override { }
};
Base* obj = new Derived ();
delete obj;
这里我们就可以讲解虚析构函数的原理了,虚析构函数通过动态绑定(运行时多态)确保调用实际对象类型的析构函数。delete 操作符通过 vptr 找到实际对象的析构函数,实现动态调用。
当销毁一个派生类对象时,析构函数的调用顺序是'从派生类到基类'的逆向构造顺序:
先执行派生类的析构函数:释放派生类独有的资源(如动态内存、文件句柄等)。
自动调用基类的析构函数:释放基类的资源。
这种顺序由编译器自动管理,确保所有资源按正确顺序释放。
如果基类的析构函数没有被定义为虚函数,那么在析构时就不会触发动态绑定,实际上会通过静态绑定直接指向该指针的类型对象,即基类,从而只调用基类的析构函数,释放基类的资源,导致派生类的析构函数无法被调用,造成内存泄漏等问题。
看到这里,相信大家已经明白了为什么析构函数要定义为虚函数。也明白了多态的核心机制——动态绑定
还有一个值得注意的点是:虚函数的默认参数是静态绑定的
虚函数的默认参数在编译时确定,与动态绑定的函数体无关
再看我们之前的那道题,相信现在这道题对你来说已经是小菜一碟了
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
class A {
public :
virtual void func (int val = 1 ) {
std::cout << "A->" << val << std::endl;
}
virtual void test () {
func ();
}
};
class B : public A {
public :
void func (int val = 0 ) {
std::cout << "B->" << val << std::endl;
}
};
int main (int argc, char * argv[]) {
B *p = new B;
p->test ();
return 0 ;
}
结合我们上面讲到的知识点,现在已经不难理解,B 重写了 A 的 func() 虚函数,当执行 p->test(),这句代码时,会调用 B 中继承下来的基类的 test() 函数,从而执行 func() 函数,由于类 B 重写了基类的 func() 函数,会根据指针 p 指向的实际类型,执行动态绑定,实际上执行的是 B 中重写的 func() 函数,但由于默认参数是静态绑定,默认参数的取值由调用方的静态类型决定,与动态绑定的函数实现无关。
test() 函数在 A 类中定义,其内部的 func() 调用根据 A 的静态类型确定默认参数为 1。在编译时就已经确定了,所以 val 的值是基类中的默认参数的值,为 1。
p ->test() → A ::test () → func ()(动态绑定到 B::func (),但默认参数来自 A 的声明)
通过实际指向的对象找到 vptr。
通过 vptr 找到虚函数表。
根据函数在表中的位置调用实际函数。
虚函数表的位置 最后补充一个知识点,本质上虚函数也是函数,编译后也是一段指令,虚函数的地址放在了对象的虚函数表中,那么虚函数表存放在哪里呢?
关于虚函数表的存放位置,C++ 标准并没有明确规定,是交给编译器来实现的,不同的编译器实现可能不同,但是一般情况都存放在程序的只读数据段(.rodata ),虚函数表的内容在编译期就已确定,且在运行时不可修改,适合存放在只读内存中。
class Base {
public :
virtual void func1 () {
cout << "Base::func1" << endl;
}
virtual void func2 () {
cout << "Base::func2" << endl;
}
void func5 () {
cout << "Base::func5" << endl;
}
protected :
int a = 1 ;
};
class Derive : public Base {
public :
virtual void func1 () {
cout << "Derive::func1" << endl;
}
virtual void func3 () {
cout << "Derive::func1" << endl;
}
void func4 () {
cout << "Derive::func4" << endl;
}
protected :
int b = 2 ;
};
int main () {
int i = 0 ;
static int j = 1 ;
int *p1 = new int ;
const char *p2 = "xxxxxxxx" ;
printf ("栈:%p\n" , &i);
printf ("静态区:%p\n" , &j);
printf ("堆:%p\n" , p1);
printf ("常量区:%p\n" , p2);
Base b;
Derive d;
Base *p3 = &b;
Derive *p4 = &d;
printf ("Person 虚表地址:%p\n" , *(int *)p3);
printf ("Student 虚表地址:%p\n" , *(int *)p4);
printf ("虚函数地址:%p\n" , &Base::func1);
printf ("普通函数地址:%p\n" , &Base::func5);
return 0 ;
}
Visual Studio 2022 平台下输出:
栈:010FF954 静态区:0071D000 堆:0126D740 常量区:0071ABA4 Person 虚表地址:0071AB44 Student 虚表地址:0071AB84 虚函数地址:00711488 普通函数地址:007114BF
在 Visual Studio 2022 平台下虚函数表是存储在常量区的。
总结多态调用 +----------------------------------------------------------------+
| 多态实现流程总结 |
+----------------------------------------------------------------+
|| ||
|1. 定义基类虚函数 ||
| - 基类中声明虚函数:virtual void func () ; ||
|| ||
|2. 派生类重写虚函数 ||
| - 派生类中 override : void func () override ; ||
|| ||
|3. 编译器生成虚函数表(vtable) ||
| +---------------------------+ ||
||| 基类 vtable ||
|||| - &Base::func ||
| +---------------------------+ ||
||| 派生类 vtable ||
|||| - &Derived::func (重写后替换基类地址) ||
| +---------------------------+ ||
|| ||
|4. 对象内存布局 ||
| +---------------------------+ ||
||| vptr | --> 指向 vtable ||
||| 基类成员变量 ||
|||| 派生类成员变量 ||
| +---------------------------+ ||
|(vptr 在对象内存首部,占 4 /8 字节) ||
|| ||
|5. 动态绑定过程(运行时) ||
| +----------------------------------------------------------+ ||
||| 通过基类指针调用虚函数:obj->func (); ||
|||| ||
||| a. 访问 obj 的 vptr ||
||| b. 通过 vptr 找到 vtable ||
||| c. 查表调用实际函数地址 &Derived::func ||
| +----------------------------------------------------------+ ||
|| ||
|6. 虚析构函数保障 ||
| - 基类声明虚析构函数:virtual ~Base () ||
| - 派生类析构函数自动重写 ||
| - delete 基类指针时,触发完整析构链 ||
|| ||
+----------------------------------------------------------------+
C++ 多态通过虚函数表和动态绑定机制实现,允许基类指针或引用在运行时根据实际对象类型调用对应的派生类方法:编译器为每个含虚函数的类生成虚函数表(存储函数地址),对象内置虚表指针(vptr)指向所属类的虚表,当通过基类指针调用虚函数时,程序通过 vptr 查表定位实际函数地址,实现运行时决议,同时虚析构函数确保对象销毁时正确调用派生类析构逻辑,从而支持面向对象中'同一接口,多种实现'的核心特性。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online