C++多态及底层实现原理

前两天去面试,面试官问到多态是如何实现的,以下依次讲解多态基本概念、虚函数表的工作原理,以及利用IDA反编译exe讲解汇编指令来清晰底层实现原理。

多态性(Polymorphism)是面向对象编程中的一个重要概念,它允许对象以多种形式存在。C++中的多态性主要通过继承和虚函数来实现。多态性使得一个基类指针或引用能够指向派生类的对象,并调用派生类的成员函数。这样,程序可以在运行时决定调用哪个函数,从而实现动态绑定。

以下是多态性的主要类型:

  • 编译时多态性(静态多态性):通过函数重载和运算符重载实现。
  • 运行时多态性(动态多态性):通过虚函数和继承实现。

1. 编译时多态

1.1函数重载

函数重载是指在同一个作用域内,可以有多个同名函数,但它们的参数列表(参数的类型、个数或顺序)不同。编译器会根据调用时传递的参数类型和个数来选择合适的函数版本。

// 重载函数
void Connect(int ip) {
    std::cout << "Connect by ip: " << ip << std::endl;
}

void Connect(int ip, int portId) {
    std::cout << "Connect by ip and portId:" << ip << portId << std::endl;
}

void Connect(const std::string& deviceName) {
    std::cout << "String: " << deviceName << std::endl;
}

int main() {
    Connect(10651);        // 调用 Connect(int)
    Connect(10652, 1);     // 调用 Connect(int, int)
    Connect("Hello");      // 调用 Connect(const std::string&)
    return 0;
}

1.2 模板

模板是C++中实现泛型编程的工具,允许编写与类型无关的代码。模板可以用于函数和类,通过在编译时生成特定类型的实例来实现多态性。

函数模板示例

template <typename T>
void printVector(const std::vector<T>& v) {
    for (const T& item : v) {
        std::cout << item << std::endl;
    }
}

int main() {
    printVector(std::vector<int>{1, 2, 3, 4, 5});
    printVector(std::vector<double>{1.1, 2.1, 3.2, 4.4, 5.5});

    return 0;
}

类模板示例

template <typename T>
class MyClass {
private:
    T value;
public:
    MyClass(T value) : value(value) {};

    T GetValue() const {
        return value;
    }
};

int main() {
    MyClass<int> myClass(10);
    std::cout << "int class:" << myClass.GetValue() << std::endl;

    MyClass<std::string> myClass2("Hello");
    std::cout << "string class:" << myClass2.GetValue() << std::endl;

    return 0;
}

编译时多态性(静态多态性)通过函数重载和模板实现,允许在编译阶段确定函数调用的具体实现。与运行时多态性不同,编译时多态性不依赖于继承和虚函数,而是通过编译器在编译时解析和选择合适的函数或模板实例。这种多态性提高了代码的灵活性和可重用性,同时也避免了运行时的开销。

2. 运行时多态

运行时多态性是通过虚函数实现的。虚函数是在基类中使用 virtual 关键字声明的函数。派生类可以重写这些虚函数。当通过基类指针或引用调用虚函数时,实际调用的是派生类的实现。

简单示例:

#include <iostream>

class Base {
public:
    virtual void Run() {
        std::cout << "Base::Run() called." << std::endl;
    }

    virtual ~Base() = default; // 虚析构函数确保派生类对象被正确销毁
};

class Sub : public Base {
public:
    void Run() override {
        std::cout << "Sub::Run() called." << std::endl;
    }
};

int main() {
    Base* instance = new Sub(); // 基类指针指向派生类对象
    instance->Run();            // 调用派生类的 Run() 函数

    delete instance;            // 删除对象,调用虚析构函数
    return 0;
}

2.1 运行时多态性实现

基类指针调用派生类的虚函数是通过虚函数表(Virtual Table,简称vtable)和虚函数指针(Virtual Pointer,简称vptr)机制实现的。下面是这个机制的工作原理:

虚函数表(vtable)
每个包含虚函数的类都有一个虚函数表(vtable),它是一个指针数组,数组中的每个元素指向该类的虚函数实现。对于每个类,编译器会生成一个唯一的vtable。

虚函数指针(vptr)
每个包含虚函数的对象都有一个隐藏的指针(vptr),它指向该对象所属类的vtable。这个vptr在对象创建时由编译器自动设置。

实现步骤:
对象创建:当创建一个包含虚函数的类的对象时,编译器会将该对象的vptr指向该类的vtable。
虚函数调用:当通过基类指针调用虚函数时,在运行时,程序根据 basePtr 实际指向的对象类型查找虚函数表,并调用 其对象的函数。这就是运行时多态的实现。

#include <iostream>

class Base {
public:
    virtual void Run() {
        std::cout << "Base::Run() called." << std::endl;
    }

    virtual ~Base() = default; // 虚析构函数确保派生类对象被正确销毁
};

class Sub : public Base {
public:
    void Run() override {
        std::cout << "Sub::Run() called." << std::endl;
    }
};

int main() {
    Base* instance = new Sub(); // 基类指针指向派生类对象
    instance->Run();            // 调用派生类的 Run() 函数

    delete instance;            // 删除对象,调用虚析构函数
    return 0;
}

输出:
在这里插入图片描述

详细解释上述代码:
(1)类定义阶段:
编译器为Base类生成一个vtable,其中包含一个指向Base::Run函数的指针。
编译器为Sub类生成一个vtable,其中包含一个指向Sub::Run函数的指针。

(2)对象创建阶段:
当创建Sub类对象时,编译器将该对象的vptr指向Sub类的vtable。

(3)函数调用阶段:
当通过Base* instance调用Run函数时,instance实际上指向Sub类对象。
运行时程序通过instance的vptr找到Sub类的vtable,并调用其中的Run函数指针,实际调用的是Sub::Run函数。

2.2 查看汇编了解底层实现

可以使用IDA反编译软件打开生成的exe文件(编译优化开的-o2级),可以看到汇编代码如下(从main函数入口查看):
在这里插入图片描述
(1)函数开始

.text:0000000140001030                 sub     rsp, 28h

sub rsp, 28h:调整栈指针,在rsp栈指针寄存器分配 40 字节的栈空间用于局部变量和临时数据。(28h的h表示十六进制的28,即为十进制的40)

(2)动态内存分配

.text:0000000140001034                 mov     ecx, 8          ; size
.text:0000000140001039                 call    ??2@YAPEAX_K@Z  ; operator new(unsigned __int64)

mov ecx, 8:将 8 存入 ecx寄存器,表示要分配的字节数。
call ??2@YAPEAX_K@Z:调用 operator new,分配 8 字节的内存。返回的Sub对象指针存储在 rax 中。(对应代码中的new Sub()

(3) 设置虚函数表指针

.text:000000014000103E                 lea     rcx, ??_7Sub@@6B@ ; const Sub::`vftable'
.text:0000000140001045                 mov     [rax], rcx

lea rcx, ??_7Sub@@6B@:将 Sub 类的虚函数表地址加载到 rcx 中。
mov [rax], rcx:将虚函数表地址存储到新分配的Sub对象内存的起始位置。

(4)调用虚函数

.text:0000000140001048                 mov     rcx, rax        ; this
.text:000000014000104B                 mov     rax, cs:??_7Sub@@6B@+8 ; const Sub::`vftable'
.text:0000000140001052                 call    rax ; Sub::Run(void) ; Sub::Run(void)

mov rcx, rax:将Sub对象指针(this 指针)存储到 rcx 中。
mov rax, cs:??_7Sub@@6B@+8:将虚函数表中 Sub::Run 函数的地址加载到 rax寄存器 中。
call rax:调用 Sub::Run 函数。

(5)函数结束

.text:0000000140001054                 xor     eax, eax
.text:0000000140001056                 add     rsp, 28h
.text:000000014000105A                 retn
.text:000000014000105A main            endp

xor eax, eax:将 eax寄存器 清零,设置返回值为 0。
add rsp, 28h:恢复栈40字节的空间。
retn:返回调用者。

通过虚函数表和虚函数指针,C++实现了运行时多态性,使得基类指针能够调用派生类的虚函数。

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值