跳到主要内容
C++ 多态详解:编译时与运行时机制、虚函数原理及最佳实践 | 极客日志
C++ 算法
C++ 多态详解:编译时与运行时机制、虚函数原理及最佳实践 C++ 多态机制涵盖编译时静态多态与运行时动态多态。内容解析虚函数表生成规则、动态绑定原理、虚析构函数必要性及协变返回类型等核心概念,通过代码示例对比重载、重写与隐藏的区别,揭示面向对象编程中多态的底层实现细节与内存布局。
全栈工匠 发布于 2026/3/22 更新于 2026/5/22 13 浏览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
double : 3.14
const char *: Hello
运算符重载(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 () { return _elements.back (); }
private :
std::vector<T> _elements;
};
编译时多态的特点
静态绑定(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 ;
}
无运行时开销 每个类型特化生成独立的机器代码,调用时直接跳转到具体函数,无间接寻址,提高运行效率。
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) 虚函数是实现运行时多态的核心机制。它允许通过基类指针或引用调用派生类的重写函数。
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) 虚函数重写是指派生类重新定义基类的虚函数,实现同签名不同行为。
条件 说明 基类函数为虚函数 基类函数必须使用 virtual 声明 函数签名一致 派生类函数必须与基类的函数名、参数类型/数量/顺序、const 限定符完全一致 访问权限允许 派生类函数访问权限不能比基类更严格
派生类在重写基类虚函数时,即使不加 virtual 关键字,继承下来的虚函数属性依然保持,可以构成重写,但建议规范使用 override。
override 是 C++11 引入的特性,用于强制编译器检查覆盖是否正确。
final 关键字用于禁止派生类继续重写该虚函数。
class Car {
public :
virtual void Drive () final {}
};
class Benz : public Car {
public :
virtual void Drive () {
std::cout << "Benz" << std::endl;
}
};
函数签名不一致 :导致隐藏而非覆盖。
class Base {
public :
virtual void func (int ) {}
};
class Derived : public Base {
public :
void func (double ) {}
};
没有使用 override 关键字 :若函数签名错误,编译器可能不会报错,导致隐藏而非覆盖。
基类虚函数未声明为 virtual :派生类函数与基类函数是独立的,无法通过基类指针/引用调用多态。
纯虚函数(Pure Virtual Function)和抽象类 纯虚函数是一种没有具体实现的虚函数,其存在的目的是强制派生类必须实现该函数。声明方式为在虚函数声明末尾添加 = 0。
virtual 返回类型 函数名 (参数) = 0 ;
定义接口规范:为所有派生类定义一个必须实现的统一接口。
创建抽象类:包含纯虚函数的类称为抽象类,不能被直接实例化。
强制派生类实现:所有直接继承自抽象类的派生类必须重写纯虚函数。
class Animal {
public :
virtual void sound () const = 0 ;
virtual ~Animal () {}
};
class Dog : public Animal {
public :
void sound () const override {
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 ;
}
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 ;
}
虽然基类与派生类析构函数名称不同,但编译器对析构函数名称做了特殊处理,编译后统⼀处理成 destructor。因此,基类析构函数加了 virtual 修饰,派生类的析构函数就构成重写。
协变(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 ();
}
delete fruit;
delete cloned;
return 0 ;
}
重载、重写、隐藏的对比 特性 重载(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) 规则:派生类重新定义基类的虚函数,要求函数名、参数列表、返回类型完全一致。必须使用 virtual 声明基类函数,派生类建议使用 override。
隐藏(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)
虚函数表的概念 虚函数表是在编译期间,编译器为每个包含虚函数的类生成的静态表,存储该类所有虚函数的地址。虚函数指针(vptr)是每个对象实例中隐含的指针,指向其所属类的虚函数表。
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 ;
}
成员变量占 8 字节(含对齐填充),另外 4 字节是 __vfptr(虚函数表指针)。对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针。
基类对象的虚函数表中存放基类所有虚函数的地址。
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
虚函数表的生成规则 虚表在编译期间由编译器生成,程序启动时加载到内存。
派生类的虚函数表基于基类的虚函数表扩展。若派生类重写基类虚函数,替换表中对应的函数指针;若新增虚函数,追加到表末尾。
class Derived : public Base {
public :
void func1 () override {}
virtual void func3 () {}
};
每个基类对应独立的虚函数表,派生类合并所有基类的表并调整 vptr 偏移。
多态是如何实现的 多态主要通过虚函数和虚函数表实现,核心是动态绑定。
动态绑定和静态绑定 特性 静态绑定(早绑定) 动态绑定(晚绑定) 解析时机 编译时确定调用的具体函数 运行时根据对象实际类型确定调用的函数 实现机制 函数地址直接硬编码到代码中 通过虚函数表(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 ;
}
通过虚函数表动态查找函数地址。只有虚函数支持动态绑定。
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 ;
}
虚析构函数通过动态绑定确保调用实际对象类型的析构函数。delete 操作符通过 vptr 找到实际对象的析构函数,实现动态调用。
从派生类到基类的逆向构造顺序。先执行派生类的析构函数,再自动调用基类的析构函数。
虚函数的默认参数是静态绑定的。默认参数的取值由调用方的静态类型决定,与动态绑定的函数实现无关。
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 () {
B *p = new B;
p->test ();
return 0 ;
}
B 重写了 A 的 func() 虚函数。执行 p->test() 时,由于 test() 在 A 类中定义,其内部的 func() 调用根据 A 的静态类型确定默认参数为 1。而 func() 本身会根据指针 p 指向的实际类型执行动态绑定,调用 B 中重写的 func() 函数。
完整的调用链:p->test() → A::test() → func()(动态绑定到 B::func(),但默认参数来自 A 的声明)。
虚函数表的位置 关于虚函数表的存放位置,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::func3" << 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 平台下,虚函数表通常存储在常量区。
总结多态调用
定义基类虚函数 :基类中声明虚函数 virtual void func();
派生类重写虚函数 :派生类中 override:void func() override;
编译器生成虚函数表 :基类和派生类各自有独立的虚表,重写后替换地址。
对象内存布局 :对象内存首部包含 vptr,指向虚表。
动态绑定过程 :通过基类指针调用虚函数时,访问 vptr 找到 vtable,查表调用实际函数地址。
虚析构函数保障 :基类声明虚析构函数,确保 delete 基类指针时触发完整析构链。
C++ 多态通过虚函数表和动态绑定机制实现,允许基类指针或引用在运行时根据实际对象类型调用对应的派生类方法。编译器为每个含虚函数的类生成虚函数表,对象内置虚表指针,当通过基类指针调用虚函数时,程序通过 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