C++ 虚拟继承的内存使用

C++ 虚拟继承的内存使用,c++,inheritance,memory-management,virtual,multiple-inheritance,C++,Inheritance,Memory Management,Virtual,Multiple Inheritance,我有一些类(大多数是抽象类,带有虚拟继承): 现在我使用它们: B * object = new MyB(); object->f1(); //declared in A, imp. in MyA object->f3(); //declared in B, imp. in MyB 一切正常,代码对我来说“很好”(我可以快速从MyB切换到YourB,只需更改一行) 但我的问题是: 与下面列出的类似代码相比,它使用了多少额外内存(相同的结果,不同的结构) 我不擅长使用内存布局/vTa

我有一些类(大多数是抽象类,带有虚拟继承):

现在我使用它们:

B * object = new MyB();
object->f1(); //declared in A, imp. in MyA
object->f3(); //declared in B, imp. in MyB
一切正常,代码对我来说“很好”(我可以快速从
MyB
切换到
YourB
,只需更改一行)

但我的问题是: 与下面列出的类似代码相比,它使用了多少额外内存(相同的结果,不同的结构)

我不擅长使用内存布局/vTables,因此请用一种简单的方式向我解释它-我想知道,我的应用程序是否会花费更多的资源(内存),以及可执行文件是否会变慢

我将该代码与该代码进行比较:

class MyA{
    public:
        virtual void f1(){ ... }
        virtual void f2(){ ... }
};

class MyB : public MyA{
    public:
        void f3(){ ... }
};

MyB * object = new MyB();
object->f1(); //declared in MyA, imp. in MyA
object->f3(); //declared in MyB, imp. in MyB

sizeof(object)
在两个示例中都返回4(winx32,visualstudio原生编译器),但我不确定它在这里是否权威。也许这不算什么-我不认为两个样本都是100%相等的。

这取决于实现,但通常虚拟继承需要:

  • 每个虚拟基类的额外指针(或偏移):转换(例如)
    B*
    A*
    的调整将取决于同一对象中的哪些其他子对象也从
    A
    虚拟派生。我认为这可以存储在vtable中,而不是对象本身,因此开销可以是每个类而不是每个对象
  • 构造函数和析构函数中的额外逻辑,用于确定虚拟基对象是否需要在该点初始化/销毁
  • 一些指针转换的额外工作,读取存储的指针/偏移量,而不是应用编译时常量

对于内存使用,您可以通过打印
sizeof(MyB)
来测量实现中的每个对象开销。每个类的开销可能可以忽略不计,除非您有大量的类。

这取决于实现,但通常虚拟继承需要:

  • 每个虚拟基类的额外指针(或偏移):转换(例如)
    B*
    A*
    的调整将取决于同一对象中的哪些其他子对象也从
    A
    虚拟派生。我认为这可以存储在vtable中,而不是对象本身,因此开销可以是每个类而不是每个对象
  • 构造函数和析构函数中的额外逻辑,用于确定虚拟基对象是否需要在该点初始化/销毁
  • 一些指针转换的额外工作,读取存储的指针/偏移量,而不是应用编译时常量

对于内存使用,您可以通过打印
sizeof(MyB)
来测量实现中的每个对象开销。除非您有大量的类,否则每个类的开销可能可以忽略不计。

在我开始之前,您的问题被称为过早优化。在使用大小和空间之前,还需要考虑其他问题:正确性和健壮性

继承通常通过虚拟函数表实现,本质上是函数的地址表。因此,额外的代码空间量取决于虚拟函数的数量

虚拟函数使用跳转表执行。通常的做法是使用函数表中的值加载程序计数器。这通常是两个装配说明

函数表的变量空间量可能小于结构中对齐填充所浪费的内存。浪费的执行时间小于调用函数的开销

编辑1:
顺便说一句,汇编指令通常在1微秒或更短的时间内执行。因此,通过函数表调用需要2微秒。将此与等待磁盘I/O或用户I/O进行比较


这就是为什么它是一个过早的优化:在分析之前进行优化。在担心代码和变量空间的浪费之前,先分析整个代码并关注瓶颈

在我开始之前,您的问题被称为过早优化。在使用大小和空间之前,还需要考虑其他问题:正确性和健壮性

继承通常通过虚拟函数表实现,本质上是函数的地址表。因此,额外的代码空间量取决于虚拟函数的数量

虚拟函数使用跳转表执行。通常的做法是使用函数表中的值加载程序计数器。这通常是两个装配说明

函数表的变量空间量可能小于结构中对齐填充所浪费的内存。浪费的执行时间小于调用函数的开销

编辑1:
顺便说一句,汇编指令通常在1微秒或更短的时间内执行。因此,通过函数表调用需要2微秒。将此与等待磁盘I/O或用户I/O进行比较


这就是为什么它是一个过早的优化:在分析之前进行优化。在担心代码和变量空间的浪费之前,先分析整个代码并关注瓶颈

对于常用的编译器,开销大致如下

  • 内存开销:指向每个父类的vtable的额外指针
  • 运行时开销:每个虚拟方法调用都将具有间接性,以针对调用对象的实际类型找到方法的右侧实现

  • sizeof(object)
    在两种情况下都返回4,因为您测量的是指向对象的指针的大小,而不是对象本身。无论指针指向哪个对象,指针大小都是相同的。

    对于常用的编译器,开销大致如下

  • 记忆
    class MyA{
        public:
            virtual void f1(){ ... }
            virtual void f2(){ ... }
    };
    
    class MyB : public MyA{
        public:
            void f3(){ ... }
    };
    
    MyB * object = new MyB();
    object->f1(); //declared in MyA, imp. in MyA
    object->f3(); //declared in MyB, imp. in MyB