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地址: 0x4022d0
  vtable[0]: 0x4016aa -> 函数地址: 0x4016aa
  vtable[1]: 0x4016e4 -> 函数地址: 0x4016e4
  vtable[2]: 0x40171e -> 函数地址: 0x40171e

=== Derived对象 ===
对象地址: 0x7ffd4a8b6a20
vptr地址: 0x7ffd4a8b6a20
vtable地址: 0x4022f0
  vtable[0]: 0x401758 -> 函数地址: 0x401758
  vtable[1]: 0x401792 -> 函数地址: 0x401792
  vtable[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::funcA
    cout << "B* 调用funcA: "; b_ptr->funcA();  // C::funcA  
    cout << "B* 调用funcB: "; b_ptr->funcB();  // C::funcB
    cout << "C* 调用funcA: "; c_ptr->funcA();  // C::funcA
    cout << "C* 调用funcB: "; c_ptr->funcB();  // C::funcB
    cout << "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 虚函数调用开销

虚函数调用的开销主要来自:

  1. 间接内存访问:通过vptr访问vtable

  2. 指令缓存不命中:函数地址不固定

  3. 无法内联:编译时无法确定具体函数

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. 总结

运行时多态的核心原理:

  1. vtable(虚函数表):每个包含虚函数的类都有一个虚函数表

  2. vptr(虚函数表指针):每个对象包含指向对应vtable的指针

  3. 动态绑定:通过vptr在运行时查找并调用正确的函数

  4. 继承关系:派生类的vtable基于基类的vtable构建

关键特点:

  • ✅ 灵活性强,支持运行时类型确定

  • ✅ 接口统一,易于扩展

  • ❌ 有性能开销(间接调用)

  • ❌ 内存开销(每个对象多一个vptr)

  • ❌ 无法内联优化

相关推荐
Cx330❀8 小时前
《C++ 多态》三大面向对象编程——多态:虚函数机制、重写规范与现代C++多态控制全概要
开发语言·数据结构·c++·算法·面试
_dindong9 小时前
【递归、回溯、搜索】专题六:记忆化搜索
数据结构·c++·笔记·学习·算法·深度优先·哈希算法
seabirdssss9 小时前
JDK 11 环境正确,端口未被占用,但是运行 Tomcat 11 仍然闪退
java·开发语言·tomcat
Mr YiRan9 小时前
SYN关键字辨析,各种锁优缺点分析和面试题讲解
java·开发语言
列逍9 小时前
list的模拟实现
数据结构·c++·list
superior tigre9 小时前
(huawei)最小栈
c++·华为·面试
云外天ノ☼9 小时前
一、Node.js入门实战指南:从零搭建你的第一个后端
前端·javascript·笔记·node.js
oioihoii9 小时前
智驾“请抬脚”提示感悟 - 当工程师思维遇见用户思维
开发语言·程序员创富
m0_7369270410 小时前
Spring Boot项目中如何实现接口幂等
java·开发语言·spring boot·后端·spring·面试·职场和发展