c++虚表和虚基类和析构和构造和继承相关机制

抽象类和虚函数和纯虚函数

抽象类(Abstract Class)

抽象类是一种不能直接实例化的类,其存在的目的是作为基类,为派生类提供一个公共接口。抽象类中可以包含具体的方法(即有实现的方法)和抽象方法(即没有实现的方法)。抽象类通常用来定义一个接口规范,强制要求继承它的子类必须实现某些方法。

在C++中,一个类只要包含至少一个纯虚函数,它就是抽象类。抽象类的实例化是不被允许的,即不能直接创建抽象类的对象。

纯虚函数(Pure Virtual Function)

纯虚函数是在基类中声明的虚函数,它在基类中没有具体的实现,而是留给派生类去实现。纯虚函数的声明语法是在函数声明后面加上= 0,例如:

virtual void func() = 0;

含有纯虚函数的类被称为抽象类。派生类必须实现所有的纯虚函数,否则这个派生类也将成为抽象类。

虚函数(Virtual Function)

虚函数是一种特殊类型的成员函数,允许在派生类中重写基类中的该函数,从而实现动态多态性。当通过基类指针或引用来调用虚函数时,实际调用的是指针或引用所指向的具体对象的类的那个版本的函数。

虚函数的声明不需要= 0,例如:

virtual void func();

使用虚函数的主要目的是为了实现多态,使得基类的指针或引用可以调用派生类中的函数,这样可以在运行时决定调用哪个函数,增加了程序的灵活性和可扩展性。

总结

  • 抽象类:不能直接实例化,包含至少一个纯虚函数,为派生类提供接口规范。
  • 纯虚函数:没有具体实现的虚函数,强制派生类必须实现,使得一个类成为抽象类。
  • 虚函数:允许在派生类中重写,实现动态多态,使得基类指针或引用来调用时能根据对象的实际类型执行对应的函数。

c++ 虚函数和继承相关补充记录https://wsxk.github.io/c++reverse/

虚函数的析构和构造

在这里插入图片描述

调用析构函数切换基表的原因(构造函数也一样)

在子类构造函数中,会先调用父类构造函数,父类构造函数首先将子类对象中的虚表填为父类对象虚表地址,在父类构造函数中,如果存在虚函数调用,就能够成功的调用父类的函数,而不是子类函数

class Base {
public:
    Base() { initTable(); }
    virtual ~Base() {
        std::cout << "Before sayHello in Base destructor" << std::endl;
        sayHello(); // 假设这里有个逻辑错误,实际上不应直接调用虚函数
        std::cout << "After sayHello in Base destructor" << std::endl;
    }
    virtual void sayHello() { std::cout << "Hello from Base" << std::endl; }

private:
    void initTable() { /* 假设这里有初始化虚表的操作 */ }
};

class Derived : public Base {
public:
    Derived() { initTable(); }
    ~Derived() override { std::cout << "Derived destructor" << std::endl; }
    void sayHello() override { std::cout << "Hello from Derived" << std::endl; }

private:
    void initTable() { /* 初始化包含 Derived 版本 sayHello 的虚表 */ }
};

现在,如果我们创建一个Derived对象并通过基类指针删除它,会发生什么?

int main() {
    Base* ptr = new Derived();
    delete ptr;
    return 0;
}

预期行为

  • 首先,Derived的析构函数被调用,清理Derived特有的资源。
  • 紧接着,控制权转到Base的析构函数,因为Derived析构完后,需要继续销毁基类部分。

问题所在

Base的析构函数中,假设有一个逻辑错误,它直接调用了sayHello()。如果不进行虚表的切换,按照原本的对象类型(即Derived),将会调用到Derived版本的sayHello(),这与我们期望的在析构Base部分时只表现出Base的行为相违背。

解决方案:虚表的临时切换

为了确保在执行Base的析构函数时,即使调用虚函数也能反映出Base的行为,编译器会在进入Base的析构函数之前,临时将对象的虚表指针切换到Base类的虚表。这样一来,当在Base的析构函数内部调用sayHello()时,它会调用到Base版本的sayHello(),而不是Derived的,从而保证了行为的正确性。

全局变量的构造函数和析构函数执行

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这段代码是C++程序在编译后的启动和关闭阶段处理全局构造函数与析构函数的一个简化示例。它通常由编译器自动生成,用于管理全局对象的构造与析构顺序。这里涉及到几个关键的概念和变量,我会逐一解释,并给出一个概念性的例子来帮助理解。

关键概念和变量解释

  • _do_global_ctors:此函数负责调用全局对象的构造函数。在程序启动时由编译器/链接器自动调用。
  • _do_global_dtors:对应函数,负责调用全局对象的析构函数,在程序结束时调用。
  • refptr___CTOR_LIST__:这是一个指针,指向一个由编译器维护的全局构造函数指针数组。数组的每个元素是一个函数指针,指向一个全局对象的构造函数。
  • refptr___DTOR_LIST__:类似地,指向析构函数指针数组,但这段代码未直接展示。
  • eaxebxrsi:这些是x86-64架构下的寄存器名称,用于存储临时变量或指针。

函数流程

  1. 初始化检查:首先,检查refptr___CTOR_LIST__[0](即i)是否等于-1。这个值通常表示构造函数列表未初始化或为空。如果为-1,则跳过后续构造函数调用步骤。

  2. 遍历构造函数列表:如果构造函数列表非空,计算列表的实际长度(即构造函数的数量),然后通过双指针技术(一个向前移动,一个保持不动直到第一个指针到达末尾)依次调用每个构造函数。

  3. 注册析构函数:最后,使用atexit函数注册_do_global_dtors,确保程序正常退出时调用全局对象的析构函数。

例子解释

假设我们有如下两个全局对象,它们分别有自己的构造和析构函数:

class ObjA {
public:
    ObjA() { std::cout << "ObjA constructed" << std::endl; }
    ~ObjA() { std::cout << "ObjA destructed" << std::endl; }
};

class ObjB {
public:
    ObjB() { std::cout << "ObjB constructed" << std::endl; }
    ~ObjB() { std::cout << "ObjB destructed" << std::endl; }
};

ObjA globalA;
ObjB globalB;

在程序启动时,_do_global_ctors会被调用,按照编译器确定的顺序分别调用ObjAObjB的构造函数,输出可能是:

ObjA constructed
ObjB constructed

当程序正常结束时,_do_global_dtors会被调用,逆序调用析构函数,输出可能是:

ObjB destructed
ObjA destructed

通过这种方式,C++确保了全局对象的构造和析构有序进行,即使对象之间存在依赖关系。

继承

在构造函数中:
构造顺序:先构造父类,然后按声明顺序构造成员对象和初始化列表中指定的成员,最后才是自身的构造代码
析构顺序:首先调用自身的析构函数,然后调用成员对象的析构函数,最后调用父类的析构函数

在这里插入图片描述
在这里插入图片描述
根据继承关系的顺序,先调用父类Sofa的构造函数。在调用另一个父类Bed时,并不是直接将对象的首地址作为this指针传递,而是向后调整了父类Sofa的长度,以调整后的地址值作为this指针,最后再调用父类Bed的构造函数。因为有了两个父类,所以子类在继承时也将它们的虚表指针一起继承了过来,也就有了两个虚表指针。可见,在多重继承中,子类虚表指针的个数取决于继承的父类的个数,有几个父类便会出现几个虚表指针
在进行多态操作时,会把指针进行前移或后移,指向那个的位置。

抽象类的继承

// C++ 源码
#include <stdio.h>
class AbstractBase {
public:
AbstractBase() {
printf("AbstractBase()");
}
virtual void show() = 0; //定义纯虚函数
};
class VirtualChild : public AbstractBase { //定义继承抽象类的子类
public:
virtual void show() { //实现纯虚函数
printf("抽象类分析\n");
}
};
int main(int argc, char* argv[]) {
VirtualChild obj;
obj.show();
return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
发现虚表的函数指针指向的地方没有具体的实现
在这里插入图片描述
而子类的虚表的函数指针指向的地方有具体实现
在这里插入图片描述
在这里插入图片描述

虚继承

基础设定

假设我们有如下四个类:

  1. Vehicle(交通工具):一个基类,有一个成员变量color表示颜色。

    class Vehicle {
    public:
        std::string color;
        Vehicle(std::string c) : color(c) {}
    };
    
  2. Car(汽车)Motorcycle(摩托车):两个继承自Vehicle的类,没有新增成员,直接继承。

    class Car : public Vehicle {
    public:
        Car(std::string c) : Vehicle(c) {}
    };
    
    class Motorcycle : public Vehicle {
    public:
        Motorcycle(std::string c) : Vehicle(c) {}
    };
    
  3. ConvertibleCar(敞篷车):一个特殊类型的汽车,同时也是一种摩托车(想象一个非常科幻的场景),直接继承自CarMotorcycle

    如果不使用虚继承,按照常规方式继承,ConvertibleCar将会有两个color成员,分别来自CarMotorcycle

无虚继承的问题

class ConvertibleCar : public Car, public Motorcycle {
public:
    ConvertibleCar(std::string c) : Car(c), Motorcycle(c) {}
    
    // 这里访问color就会有二义性,因为不清楚是Car的color还是Motorcycle的color
    // std::cout << color; // 错误,编译时会报错
};

使用虚继承解决问题

为了让ConvertibleCar只有一个color成员,我们需要让CarMotorcycle虚继承自Vehicle

class Car : virtual public Vehicle { ... };
class Motorcycle : virtual public Vehicle { ... };

class ConvertibleCar : public Car, public Motorcycle {
public:
    ConvertibleCar(std::string c) : Car(c), Motorcycle(c) {}
    
    // 现在访问color是明确的,因为只有一个实例
    std::cout << color; // 正确,输出ConvertibleCar的颜色
};

通过虚继承,ConvertibleCar实例中只有一个共享的Vehicle基类部分,从而避免了数据冗余和访问的二义性。

虚基类偏移表(vbtable)

让我们通过一个具体的例子来解释虚基类偏移表(vbtable)的概念。假设我们有以下类结构:

class Vehicle {
public:
    int wheels;
    virtual void drive() { std::cout << "Driving a vehicle." << std::endl; }
};

class Car : virtual public Vehicle {
public:
    int doors;
    virtual void drive() override { std::cout << "Driving a car." << std::endl; }
    virtual void honk() { std::cout << "Honking the horn." << std::endl; }
};

class Truck : virtual public Vehicle {
public:
    int payloadCapacity;
    virtual void drive() override { std::cout << "Driving a truck." << std::endl; }
    virtual void loadCargo() { std::cout << "Loading cargo." << std::endl; }
};

class SUV : public Car, public Truck {
public:
    int seatingCapacity;
    virtual void drive() override { std::cout << "Driving an SUV." << std::endl; }
};

在这个例子中,Vehicle是一个虚基类,被CarTruck两个类虚继承。然后,SUV类从CarTruck两个类中继承,形成了菱形继承结构。

内存布局与虚基类偏移表

SUV类的对象中,内存布局将会包括:

  1. 虚函数指针(vptr):对于SUVCarTruck以及从Vehicle继承的虚函数,每个具有虚函数的类都会有一个vptr指向各自的虚函数表。

  2. 虚基类指针(vbptr):由于Vehicle是虚继承的,SUV对象中会有一个vbptr指向虚基类偏移表(vbtable)。

  3. 数据成员SUVCarTruck以及Vehicle的数据成员按各自的顺序存储。

虚基类偏移表的作用

虚基类偏移表(vbtable)中的条目记录了从SUV对象的开始地址到Vehicle子对象实际开始地址的偏移量。当需要访问Vehicle类的成员(比如wheels)时,编译器会遵循以下步骤:

  1. 获取vbptr:首先,通过SUV对象中的vbptr找到虚基类偏移表。

  2. 计算偏移量:从虚基类偏移表中读取到Vehicle子对象相对于vbptr的偏移量。

  3. 访问成员:使用这个偏移量,加上vbptr的地址,就可以得到Vehicle子对象的起始地址,从而可以正确访问到wheels成员。

示例访问

如果我们要访问SUV对象的wheels成员,尽管它直接继承自CarTruck,但实际上编译器会通过vbptr和vbtable找到Vehicle的正确实例,然后计算出wheels的地址,进行访问。

这样,虚基类偏移表保证了在复杂继承结构中,虚基类成员的访问能够准确无误,避免了由于多重继承导致的同一基类数据的重复问题。

  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

看星猩的柴狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值