banner
cells

cells

为美好的世界献上 code

虚函数机制

静态绑定#

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;
    }
};

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

int main() {
    Base *base = new Derive1();
    base->foo();
    
    base = new Derive2();
    base->foo();
    
    delete base;
    return 0;
}

运行结果:

Base::foo
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;
    }
};

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

int main() {
    Base *base = new Derive1();
    base->foo();
    
    base = new Derive2();
    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 (*vf)() = (void(*)())vfptr[3];

    vf();

    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();

    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 字节内存。
  • 调用开销:调用虚函数时,会通过虚函数表来查找实际需要调用函数的地址,与直接通过函数地址调用函数相比略慢。
  • 可能影响优化:静态绑定可以在编译阶段确定调用函数的地址,编译器可以进行内联优化。而对于动态绑定,编译器无法在编译阶段确定具体的函数实现,只能在运行阶段通过虚函数表访问实际调用函数的地址。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。