众所周知,面向对象有三大特性,封装、继承和多态!
多态(
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(int)
print(3.14); // 调用 print(double)
print("Hello"); // 调用 print(const char*)
}
输出:
int: 10
double: 3.14
const char *: Hello
有关函数重载的原理及应用不再过多说明,详见博文
👉 C++函数重载
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();
}
输出:
Complex addition: 4 + 6i
同样有关运算符重载的知识这里也不再过多讲解,在类和对象中已经讲的很详细了,忘了就快去复习一下吧!!
👉 C++ 类和对象 进阶篇
Templates
)函数模板示例:
template <typename T>
T Max(T a, T b)
{
return (a > b) ? a : b;
}
调用策略:
int main()
{
std::cout << Max(3, 5) << std::endl; // 生成 int 版本
std::cout << Max(2.7, 3.14) << std::endl; // 生成 double 版本
return 0;
}
输出:
5
3.14
有关函数模板更深入的讲解,请参考博文
👉 C++ 函数模板
类模板示例:
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<string> str_Stack; // 存储字符串的栈
return 0;
}
Static Binding
)原理:
int add(int a, int b); // 函数A
double add(double a, double b); // 函数B
int main()
{
add(3, 5); // 编译时直接绑定到函数A
add(3.0, 5.0); // 编译时直接绑定到函数B
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); // 编译出错:T 同时推导为 int 和 double,类型不一致
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); // 生成 int square(int x) { return x*x; }
square(3.14); // 生成 double square(double x) { return x*x; }
return 0;
}
以上都是编译时多态的优点,其实编译时多态也有缺点,就是会导致代码膨胀,二进制文件的体积过大!
Code bloat
)模板实例化机制:
template <typename T>
class Wrapper
{
T data; /*...*/
};
int main()
{
Wrapper<int> w1; // 生成 int 特化版本
Wrapper<double> w2; // 生成 double 特化版本
return 0;
}
凡事都有两面性,直接生成代码,避免了间接寻址带来的性能损耗,无运行时开销,提高了效率,但同时也生成多种不同版本的二进制代码,代码膨胀,会导致编译链接后生成的可执行文件体积增大!
要理解运行时多态,首先要知道虚函数的概念,因为C++多态的核心机制就是派生类对基类虚函数的重写
。
Virtual function
)虚函数(Virtual Function) 是实现 运行时多态(动态多态) 的核心机制。它允许通过基类指针或引用调用派生类的重写函数,是面向对象编程中实现“一个接口,多种实现”的关键工具。
虚函数的概念:
virtual
关键字声明的成员函数,用于实现 运行时多态。virtual
修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加 virtual
修饰。Override
) 基类的函数实现,并通过基类指针/引用调用派生类的版本。虚函数的定义:
基类声明虚函数:
class Animal
{
public:
virtual void speak()
{ // 使用 virtual 关键字
std::cout << "Animal sound\n";
}
};
派生类重写虚函数:
class Dog : public Animal
{
public:
void speak() override
{ // 使用 override 明确重写(C++11)
std::cout << "Woof!\n";
}
};
通过基类调用:通过基类指针/引用调用虚函数时,实际调用的是 对象实际类型 的函数:
int main()
{
Animal *animal = new Dog();
animal->speak(); // 输出 "Woof!"(调用 Dog 的实现)
delete animal;
return 0;
}
输出:
Woof!
override
和final
)虚函数重写(
Override
) 是指派生类重新定义基类的虚函数,实现 同签名不同行为。它是实现运行时多态的关键机制。
虚函数重写的必要条件
条件 | 说明 |
---|---|
基类函数为虚函数 | 基类函数必须使用 virtual 声明 |
函数签名一致 | 派生类函数必须与基类的 函数名、参数类型/数量/顺序、const 限定符 完全一致 |
访问权限允许 | 派生类函数访问权限不能比基类更严格(如基类为 public,派生类不能为 private) |
注意:
virtual
关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。virtual
关键字也可以重写基类的虚函数,构成多态!虚函数重写时的常见错误:
函数签名不一致
:
hide
)。这个之前讲过,派生类会隐藏和基类中函数签名相同的函数。示例:
class Base {
public:
virtual void func(int) {}
};
class Derived : public Base {
public:
void func(double) {} // 参数类型不一致,未覆盖基类 func(int)
};
没有使用 override 关键字
:
override
关键字明确标记重写。class Derived : public Base {
public:
void func(int) override {} // 使用 override 强制编译器检查覆盖
};
基类虚函数未声明为 virtual
:
virtual
修饰,但派生类试图重写。示例:
class Base {
public:
void func() {} // 非虚函数
};
class Derived : public Base {
public:
void func() {} // 隐藏基类函数,无法多态调用
};
注意:
override
是 C++11 引入的一个新特性,它并非强制要求。在没有 override
关键字时,满足条件重写的条件同样可以构成虚函数的重写,只是若派生类函数使用 override
声明,但未正确重写基类虚函数(如函数名、参数列表或常量性不匹配),编译器会报错,有助于在编译阶段发现错误。final
,如果我们不想让派生类重写这个虚函数,那么可以用 final
去修饰。class Car
{
public:
virtual void Drive() final {}
};
class Benz : public Car
{
public:
virtual void Drive()
{
std::cout << "Benz" << std::endl;// 编译出错,无法重写final修饰的虚函数
}
};
趁热打铁,接下来我们看一道有关多态场景的选择题:
以下程序输出结果是什么()
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; // 纯虚函数
纯虚函数的作用:
简单来说就是,如果基类定义了纯虚函数,那么这个基类被称为抽象类,不能用来创建对象,同时继承了该类的派生类必须重写该纯虚函数,否则派生类也将成为抽象类。这样一来,基类可以提供一个统一的接口,具体实现交给不同的派生类实现。
例如:
// 抽象基类(包含纯虚函数)
class Animal
{
public:
virtual void sound() const = 0; // 纯虚函数
virtual ~Animal() {} // 虚析构函数(重要!)
};
// 派生类必须实现 sound()
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; // 仅调用了Base基类的析构函数
return 0;
}
输出:
Base destructor
不难发现,当我们用基类的指针指向派生类,释放基类指针时,只调用了基类的析构函数,释放了基类中的资源,并没有调用派生类的析构函数,这会导致什么问题呢?
是不是会导致资源没有正确释放,派生类中的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; // 正确调用Derived和Base的析构函数
return 0;
}
输出:
Derived destroyed
Base destroyed
还记得之前讲到的,虚函数重写的要求吗?派生类的析构函数与基类的析构函数名称都不一样!怎么能构成重写呢?
实际上,基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加
virtual
关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor
,所以基类的析构函数加了virltual
修饰,派⽣类的析构函数就构成重写。
虚析构函数的原理:(TODO)
涉及到虚函数表,我们放在多态的原理中讲,相信大家看到那里自然就会明白!
Covariant Return Types
)C++虚函数的协变允许派生类在重写基类虚函数时,将返回类型替换为基类函数返回类型的派生类指针或引用。 简单来说就是: 派生类重写基类虚函数时,可以与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解⼀下即可。
协变条件:
举个例子:
// 基类
class Fruit {
public:
virtual Fruit* clone() const { // 虚函数,返回Fruit*(基类指针)
return new Fruit(*this);
}
virtual ~Fruit() {}
};
// 派生类
class Apple : public Fruit {
public:
Apple* clone() const override { // 协变:返回Apple*(派生类指针)
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(); // 调用派生类的clone()
// 验证协变特性
if (Apple* apple = dynamic_cast<Apple*>(cloned)) {
apple->sayName(); // 成功调用Apple特有方法
} else {
std::cout << "Cloning failed!" << std::endl;
}
delete fruit;
delete cloned;
return 0;
}
输出:
I am an Apple!
返回类型
多态行为
动态类型验证
对于协变大家了解即可,其实底层原理也是多态的虚函数表指针。
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:
// 隐藏基类的所有 func 函数(包括重载)
void func() { std::cout << "Derived::func()\n"; }
};
int main() {
Derived d;
d.func(); // 正确:调用 Derived::func()
// d.func(1); // 错误!Base::func(int) 被隐藏
d.Base::func(1); // 正确:显式调用基类函数
return 0;
}
vtable
)vtable
) 是在编译期间,编译器为每个包含虚函数的类生成的静态表,存储该类所有虚函数的地址,生成后不可修改。vptr
) 是每个对象实例中隐含的指针,指向其所属类的虚函数表。内存布局示例
看下面一道题:
下面程序 在32位程序的运行结果是什么()
(32位下指针大小为4个字节
)
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;
}
正确答案为D,12个字节,你回答对了吗?
成员变量内存布局:
内存对齐:
C++中类和C语言中的结构体都满足内存对齐的规则,所以成员变量一共占了8个字节,那么还有另外4个字节是什么呢?
还多⼀个
__vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v 代表virtual
,f代表function
)。⼀个含有虚函数的类中都至少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
派生类的虚函数表中包含:
举个例子:这是一个基类
class Base {
public:
virtual void func1() {}
virtual void func2() {}
int data;
};
对象内存布局:
+----------------+
| vptr | --> 指向Base的虚函数表
| int data |
+----------------+
虚函数表内容:
Base的vtable:
+----------------+
| &Base::func1 |
| &Base::func2 |
+----------------+
虚表在编译期间由编译器生成,程序启动时加载到内存,生命周期与程序一致。
单继承场景
派生类的虚函数表基于基类的虚函数表扩展:
class Derived : public Base {
public:
void func1() override {} // 重写基类的func1
virtual void func3() {} // 新增虚函数
};
Derived的vtable:
+----------------+
| &Derived::func1| // 重写基类func1
| &Base::func2 | // 未重写,保留基类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 {}
};
Derived对象内存布局:
+----------------+
| Base1的vptr | --> [&Derived::f1]
| Base1成员变量 |
| Base2的vptr | --> [&Derived::f2]
| Base2成员变量 |
| Derived成员变量 |
+----------------+
多态(
Polymorphism
) 主要通过 虚函数(virtual functions
) 和 虚函数表(vtable
) 实现,核心是动态绑定(Dynamic Binding
)。
动态绑定(
Dynamic Binding
) 和 静态绑定(Static Binding
) 是函数调用的两种不同解析机制,直接影响程序的执行行为。
特性 | 静态绑定(早绑定) | 动态绑定(晚绑定) |
---|---|---|
解析时机 | 编译时确定调用的具体函数 | 运行时根据对象实际类型确定调用的函数 |
实现机制 | 函数地址直接硬编码到代码中 | 通过虚函数表(vtable)和虚指针(vptr)动态查找 |
性能 | 高(无运行时开销) | 较低(需要查表和间接调用) |
灵活性 | 低(固定行为) | 高(支持多态) |
应用场景 | 普通函数、非虚成员函数、模板函数 | 虚函数(多态调用) |
静态绑定(Static Binding
)
工作机制
典型场景
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(); // 输出 "Base"(静态绑定)
delete obj;
return 0;
}
对于上面这个例子:
函数未声明为虚函数
指针类型决定调用
Base*
,编译器在编译时根据指针的静态类型(即 Base*
)确定调用 Base::nonVirtualFunc()
,与指针实际指向的对象类型无关。派生类函数是“隐藏”而非“重写”
void func() { /* ... */ }
func(); // 静态绑定
template <typename T>
void templateFunc(T t) { /* ... */ }
templateFunc(42); // 编译时生成针对int的版本
动态绑定(Dynamic Binding
)
工作机制
典型场景
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(); // 输出 "Derived"(动态绑定)
delete obj;
return 0;
}
- 多态对象销毁:(虚析构函数)
class Base {
public:
virtual ~Base() {} // 虚析构函数
};
class Derived : public Base {
public:
~Derived() override { /* 释放派生类资源 */ }
};
Base* obj = new Derived();
delete obj; // 动态调用~Derived()
这里我们就可以讲解虚析构函数的原理了,虚析构函数通过动态绑定(运行时多态)确保调用实际对象类型的析构函数。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(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的声明)
所以最终的输出结果就是:
B->1
动态绑定的实现依赖虚函数表
真实流程:
最后补充一个知识点,本质上虚函数也是函数,编译后也是一段指令,虚函数的地址放在了对象的虚函数表中,那么虚函数表存放在哪里呢?
关于虚函数表的存放位置,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:
// 重写基类的func1
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 查表定位实际函数地址,实现运行时决议,同时虚析构函数确保对象销毁时正确调用派生类析构逻辑,从而支持面向对象中"同一接口,多种实现"的核心特性。
本文到这里就结束了,有关C++更深入的讲解,还有更多的文章为大家讲解,敬请期待!感谢您的观看!