banner
cells

cells

为美好的世界献上 bug

虚函数机制

静态绑定#

C++ 中函数的默认绑定是静态绑定,也称为早绑定编译期绑定

静态绑定将函数的查找、关联的过程放在编译期间完成,可以提升程序的运行时的性能。

动态绑定#

#include <iostream>

class Base {
public:
    void foo() {
        std::cout << "Base::foo" << std::endl;
    }
};

class Derive1 : public Base {
public:
    void foo() {
        std::cout << "Derive1::foo" << std::endl;
    }
};

int main() {
    Base *base = new Derive1();
    base->foo();

    delete base;
    return 0;
}

运行结果:

Base::foo

这里并没有调用派生类的函数,因为编译器在这里默认进行静态绑定,编译阶段编译器无法确定 Base 类型指针会指向什么类型,无法根据对象指针实际指向的对象类型来进行函数调用。

所以需要将函数的绑定从编译阶段推迟到运行阶段,从而使对象指针能够指向实际指向的类型。这就是动态绑定,也称晚绑定运行时绑定

将基类不进行静态绑定的成员函数声明为 virtual 虚函数,从而实现动态绑定。

#include <iostream>

class Base {
public:
    virtual void foo() {
        std::cout << "Base::foo" << std::endl;
    }
    
    virtual ~Base() {}
};

class Derive1 : public Base {
public:
    void foo() {
        std::cout << "Derive1::foo" << std::endl;
    }
};

int main() {
    Base *base = new Derive1();
    base->foo();

    delete base;
    return 0;
}

运行结果:

Derive1::foo
Derive2::foo

虚函数表#

虚函数的动态绑定基于虚函数表(vftable)实现,当类中存在虚函数时,编译器会在对象的第一个数据成员位置插入 vfptr 指针,指向一个包含所有虚函数地址的数组。

image-20240619090111400

编译器会为每个包含虚函数的类生成虚函数表,该类的所有对象共享一个虚函数表,观察下面代码:

#include <iostream>

class Base {
public:
    virtual void foo() {}
};

int main() {
    Base b1;
    void **b1_vfptr = *(void ***)&b1;

    Base b2;
    void **b2_vfptr = *(void ***)&b2;

    std::cout << (b1_vfptr == b2_vfptr) << std::endl;

    return 0;
}

运行结果:

1

虚函数表包含该类及基类中所有虚函数的地址,观察下面代码:

#include <iostream>

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

    virtual ~Base() {}
};

class Derive : public Base {
public:
    void foo() override {
        std::cout << "Derive::foo" << std::endl;
    }

    virtual void foo2() {
        std::cout << "Derive::foo2" << std::endl;
    }
};

int main() {
    Base *base = new Derive;

    void **vfptr = *(void ***)base;

    // [0] &Base::foo
    // [1] &Base::~Base
    // [2] &Derive::foo
    // [3] &Derive::foo2
    void (*f)() = (void(*)())vfptr[3];

    f();

    delete base;
    return 0;
}

运行结果:

Derive::foo2

虚函数表由编译器负责初始化和销毁,在构造函数中初始化,在析构函数中销毁。

在多重继承下,可能会有多个虚函数表。

#include <iostream>

class Base1 {
public:
    virtual void foo() {} 
};

class Base2 {
public:
    virtual void foo() {}
};

class Derive : public Base1, public Base2 {
public:
    void foo() override {}
};

int main() {
    std::cout << sizeof(Derive) << std::endl;
    return 0;
}

64 bit OS 运行结果:

16

当子类没有重写父类虚函数时,子类的虚函数表会继承父类的虚函数地址。

当子类重写了父类的虚函数,子类从父类继承的虚函数表就会产生对应的覆盖行为。

#include <iostream>

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

    virtual ~Base() {}
};

class Derive1 : public Base {
public:
    
};

class Derive2 : public Base {
public:
    virtual void foo() override {
        std::cout << "Derive2::foo" << std::endl;
    }
};

int main() {
    Base *base = new Derive1();
    base->foo();

    delete base;
  
    base = new Derive2();
    base->foo();

    delete base;
    return 0;
}

运行结果:

Base::foo
Derive2::foo

运行时类型识别#

C++ 是静态类型语言,数据类型在编译阶段确定,但在某些场景下(多态),编译阶段无法确定数据类型,需要在运行阶段才能确定。

运行时类型识别(RTTI)是在运行阶段确定数据类型的机制。

typeid#

typeid 运算符用于获取变量类型,可以在编译阶段获取变量类型,也可以在运行阶段获取变量类型。

运行阶段确定变量类型:

class Base {
public:
    virtual ~Base() {}
};

class Derive : public Base {
public:
    
};

int main() {
    Base *base = new Derive;
    std::cout << typeid(*base).name() << std::endl;
    delete base;
    return 0;
}

g++ 运行结果:

6Derive

dynamic_cast#

dynamic_cast 用于类型转换,能够检测具有继承关系的父子类型的指针、引用的类型转换是否安全,可以在编译阶段转换,也可以在运行阶段转换。

编译阶段转换:

#include <iostream>

class Base {};

class Derive : public Base {};

int main() {
    Derive *derive = new Derive();
    Base *base = dynamic_cast<Base *>(derive);

    delete base;
    return 0;
}

将一个较大的寻址范围的指针转换为较小的范围,不会导致内存越界,所以是安全的。

运行阶段转换:

#include <iostream>

class Base {
public:
    virtual ~Base() {}
};

class Derive : public Base {};

int main() {
    Base *base1 = new Base(); // base1 指向基类
    Derive *derive1 = dynamic_cast<Derive *>(base1); // 基类转换为派生类
    std::cout << derive1 << std::endl; // 0 转换失败

    Base *base2 = new Derive(); // base2 指向派生类
    Derive *derive2 = dynamic_cast<Derive *>(base2); // 派生类转换为基类
    std::cout << derive2 << std::endl; // 非 0 转换成功

    delete derive1;
    delete derive2;
    return 0;
}

将一个较小的寻址范围的指针转换为较大的范围,可能会导致内存越界,所以是不安全的。

typeiddynamic_cast 在运行阶段进行类型识别时,依赖于虚函数机制。

在多态的情况下,可能会出现父类指针指向子类对象的情况。

虚函数表中的每一个指针通常指向 std::type_info 对象,可以通过 vfptr 访问到对象的类型信息 &class_meta

虚函数机制的优缺点#

优点:

  • 动态多态性: 允许通过基类指针或引用调用派生类中重写的虚函数。
  • 代码重用: 基类定义公共接口,派生类可以根据需要重写虚函数,实现不同的功能。
  • 可扩展性: 可以添加新的派生类,并重写虚函数,而无需修改基类代码。
  • 解耦: 虚函数机制可以帮助解耦代码,使代码更易于维护和修改。

缺点:

  • 内存开销:每包含虚函数的类都有一个虚函数表,虚函数表是一个函数指针数组,在 64 位操作系统中消耗 8 字节内存。
  • 调用开销:调用虚函数时,会通过虚函数表来查找实际需要调用函数的地址,与直接通过函数地址调用函数相比略慢。
  • 可能影响优化:静态绑定可以在编译阶段确定调用函数的地址,编译器可以进行内联优化。而对于动态绑定,编译器无法在编译阶段确定具体的函数实现,只能在运行阶段通过虚函数表访问实际调用函数的地址。
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。