C++笔记(面向对象)定义虚函数规则 运行时多态原理
运行时多态的设计思想:
对于相关的类型,确定它们之间的一些共同特征,(属性和方法),将共同特征被转移到基类中,
然后在基类中,把这些共同的函数或方法声明为公有的虚函数接口。然后使用派生类继承基类,并且在派生类中重写这些虚函数,以完成具体的功能。这种设计使得共性很清楚,避免了代码重复,将来容易增强功能,并易于长期维护。
客户端的代码(操作函数)通过基类的引用或指针来指向这些派生类型对象,对虚函数的调用会自
动绑定到派生类对象上重写的虚函数。
虚函数的定义:
虚函数是一个类的成员函数,定义格式如下:
virtual 返回类型 函数名(参数表);
关键字virtual指明该成员函数为虚函数。只能将类的成员函数定义为虚函数。当某一个类的成员函
数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。
总结:运行时的多态性: 公有继承 + 虚函数 + (指针或引用调用虚函数)。
定义虚函数的规则
类的成员函数定义为虚函数,但必须注意以下几条:
1. 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
2. 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
3. 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
4. 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
5. 构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
6. 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
7. 实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
8. 在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价, 但通用性是一个更高的目标。
9. 如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。
1. 运行时多态的核心:虚函数表(vtable)
1.1 基本概念
当类中包含虚函数时,编译器会为该类生成一个虚函数表(vtable),每个对象会包含一个指向这个表的指针(vptr)。
cpp
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }virtual void func3() { cout << "Base::func3" << endl; }int base_data;
};class Derived : public Base {
public:void func1() override { cout << "Derived::func1" << endl; }void func2() override { cout << "Derived::func2" << endl; }// 不重写 func3,使用基类的版本int derived_data;
};1.2 内存布局详解
Base类的内存布局:
text
Base对象: +----------------+ | vptr | --> 指向Base的vtable +----------------+ | base_data | +----------------+Base的vtable: +----------------+ | &Base::func1 | +----------------+ | &Base::func2 | +----------------+ | &Base::func3 | +----------------+
Derived类的内存布局:
text
Derived对象: +----------------+ | vptr | --> 指向Derived的vtable +----------------+ | base_data | (继承自Base) +----------------+ | derived_data | (Derived自有成员) +----------------+Derived的vtable: +-------------------+ | &Derived::func1 | // 重写的函数 +-------------------+ | &Derived::func2 | // 重写的函数 +-------------------+ | &Base::func3 | // 继承基类的函数 +-------------------+
2. 底层实现机制
2.1 编译器如何生成vtable
让我们通过实际代码来观察:
cpp
#include <iostream>
using namespace std;// 用于观察内存布局的辅助类
class MemoryInspector {
public:static void printVTable(void** vptr) {cout << "vtable地址: " << vptr << endl;for (int i = 0; i < 3; ++i) {cout << " vtable[" << i << "]: " << vptr[i] << " -> 函数地址: " << (void*)vptr[i] << endl;}}
};class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }virtual void func3() { cout << "Base::func3" << endl; }virtual ~Base() {cout << "Base destructor" << endl;}int base_data = 100;
};class Derived : public Base {
public:void func1() override { cout << "Derived::func1" << endl; }void func2() override { cout << "Derived::func2" << endl; }// 不重写func3,使用基类版本int derived_data = 200;
};int main() {Base base;Derived derived;cout << "=== Base对象 ===" << endl;cout << "对象地址: " << &base << endl;cout << "vptr地址: " << (void**)(&base) << endl;MemoryInspector::printVTable(*(void***)(&base));cout << "\n=== Derived对象 ===" << endl;cout << "对象地址: " << &derived << endl;cout << "vptr地址: " << (void**)(&derived) << endl;MemoryInspector::printVTable(*(void***)(&derived));return 0;
}可能的输出:
text
=== Base对象 === 对象地址: 0x7ffd4a8b6a10 vptr地址: 0x7ffd4a8b6a10 vtable地址: 0x4022d0vtable[0]: 0x4016aa -> 函数地址: 0x4016aavtable[1]: 0x4016e4 -> 函数地址: 0x4016e4vtable[2]: 0x40171e -> 函数地址: 0x40171e=== Derived对象 === 对象地址: 0x7ffd4a8b6a20 vptr地址: 0x7ffd4a8b6a20 vtable地址: 0x4022f0vtable[0]: 0x401758 -> 函数地址: 0x401758vtable[1]: 0x401792 -> 函数地址: 0x401792vtable[2]: 0x40171e -> 函数地址: 0x40171e
注意:Derived的vtable中前两个条目指向Derived的重写版本,第三个条目指向Base的func3。
3. 函数调用过程分析
3.1 虚函数调用的底层步骤
cpp
Base* ptr = new Derived(); ptr->func1(); // 这个调用在底层发生了什么?
调用过程的汇编级别分析:
assembly
; 1. 获取对象的vptr mov rax, qword ptr [ptr] ; 获取对象地址 mov rax, qword ptr [rax] ; 获取vptr(对象的前8字节); 2. 通过vptr找到vtable中的函数地址 mov rax, qword ptr [rax] ; 获取vtable第一个条目(func1的地址); 3. 调用函数 call rax ; 间接调用
等效的C++伪代码:
cpp
// ptr->func1() 的实际执行过程: void*** vptr = (void***)ptr; // 获取vptr void* func_address = (*vptr)[0]; // 获取func1的地址 typedef void (*FuncPtr)(); // 函数指针类型 FuncPtr func = (FuncPtr)func_address; // 转换为函数指针 func(); // 调用函数
4. 多级继承的vtable结构
4.1 多层继承示例
cpp
class A {
public:virtual void funcA() { cout << "A::funcA" << endl; }int a_data;
};class B : public A {
public:virtual void funcA() override { cout << "B::funcA" << endl; }virtual void funcB() { cout << "B::funcB" << endl; }int b_data;
};class C : public B {
public:virtual void funcA() override { cout << "C::funcA" << endl; }virtual void funcB() override { cout << "C::funcB" << endl; }virtual void funcC() { cout << "C::funcC" << endl; }int c_data;
};4.2 内存布局分析
C对象的内存布局:
text
C对象: +----------------+ | vptr | --> 指向C的vtable +----------------+ | a_data | (来自A) +----------------+ | b_data | (来自B) +----------------+ | c_data | (来自C) +----------------+C的vtable: +----------------+ | &C::funcA | // 重写A::funcA +----------------+ | &B::funcB? | // 不对!应该是&C::funcB +----------------+ | &C::funcC | // C的新虚函数 +----------------+
实际验证代码:
cpp
void analyzeHierarchy() {C c;A* a_ptr = &c;B* b_ptr = &c;C* c_ptr = &c;cout << "A* 调用funcA: "; a_ptr->funcA(); // C::funcAcout << "B* 调用funcA: "; b_ptr->funcA(); // C::funcA cout << "B* 调用funcB: "; b_ptr->funcB(); // C::funcBcout << "C* 调用funcA: "; c_ptr->funcA(); // C::funcAcout << "C* 调用funcB: "; c_ptr->funcB(); // C::funcBcout << "C* 调用funcC: "; c_ptr->funcC(); // C::funcC
}5. 多重继承的vtable复杂性
5.1 多重继承示例
cpp
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }int data1;
};class Base2 {
public:virtual void func2() { cout << "Base2::func2" << endl; }int data2;
};class Derived : public Base1, public Base2 {
public:void func1() override { cout << "Derived::func1" << endl; }void func2() override { cout << "Derived::func2" << endl; }virtual void func3() { cout << "Derived::func3" << endl; }int data3;
};5.2 多重继承的内存布局
text
Derived对象: +----------------+ | vptr1 | --> 指向Derived/Base1的vtable +----------------+ | data1 | (Base1的数据) +----------------+ | vptr2 | --> 指向Derived/Base2的vtable +----------------+ | data2 | (Base2的数据) +----------------+ | data3 | (Derived的数据) +----------------+vtable1 (对应Base1): +-------------------+ | &Derived::func1 | +-------------------+ | &Derived::func3 | // Derived的新虚函数 +-------------------+vtable2 (对应Base2): +-------------------+ | &Derived::func2 | +-------------------+ | 特殊thunk函数? | // 可能包含调整this指针的代码 +-------------------+
5.3 指针调整的验证
cpp
void testMultipleInheritance() {Derived d;Base1* b1 = &d;Base2* b2 = &d;cout << "Derived地址: " << &d << endl;cout << "Base1* 地址: " << b1 << endl; cout << "Base2* 地址: " << b2 << endl;// 两个Base指针的地址不同!// b2的地址 = b1的地址 + sizeof(Base1) + vptr大小
}6. 性能分析与优化
6.1 虚函数调用开销
虚函数调用的开销主要来自:
间接内存访问:通过vptr访问vtable
指令缓存不命中:函数地址不固定
无法内联:编译时无法确定具体函数
6.2 性能测试对比
cpp
#include <chrono>
using namespace std::chrono;class NonVirtual {
public:void func() { /* 做一些工作 */ }
};class Virtual {
public:virtual void func() { /* 做同样的工作 */ }
};void performanceTest() {const int iterations = 1000000000;NonVirtual nv;Virtual v;Virtual* v_ptr = &v;// 测试非虚函数auto start1 = high_resolution_clock::now();for (int i = 0; i < iterations; ++i) {nv.func();}auto end1 = high_resolution_clock::now();// 测试虚函数auto start2 = high_resolution_clock::now();for (int i = 0; i < iterations; ++i) {v_ptr->func();}auto end2 = high_resolution_clock::now();cout << "非虚函数: " << duration_cast<milliseconds>(end1 - start1).count() << "ms" << endl;cout << "虚函数: " << duration_cast<milliseconds>(end2 - start2).count() << "ms" << endl;
}7. 虚析构函数的原理
7.1 为什么需要虚析构函数
cpp
class Base {
public:// virtual ~Base() { cout << "Base析构" << endl; }~Base() { cout << "Base析构" << endl; } // 非虚析构函数
};class Derived : public Base {
public:~Derived() { cout << "Derived析构" << endl; }
};int main() {Base* ptr = new Derived();delete ptr; // 只调用Base的析构函数!内存泄漏!
}7.2 虚析构函数在vtable中的位置
text
Base的vtable: +----------------+ | &Base::~Base | // 虚析构函数 +----------------+ | &Base::func1 | +----------------+Derived的vtable: +-------------------+ | &Derived::~Derived| // 完整的析构序列 +-------------------+ | &Derived::func1 | +-------------------+
8. 总结
运行时多态的核心原理:
vtable(虚函数表):每个包含虚函数的类都有一个虚函数表
vptr(虚函数表指针):每个对象包含指向对应vtable的指针
动态绑定:通过vptr在运行时查找并调用正确的函数
继承关系:派生类的vtable基于基类的vtable构建
关键特点:
✅ 灵活性强,支持运行时类型确定
✅ 接口统一,易于扩展
❌ 有性能开销(间接调用)
❌ 内存开销(每个对象多一个vptr)
❌ 无法内联优化
