C++编译器创建对象需要做什么?

3

In C code like such:

{
   int i = 5;
   /* ....... */
}

编译器将会通过将栈指针向下移动(对于向下生长的栈)一个int的大小,然后在那个内存位置上放置值为5的数据,来代替这段代码。

同样地,在C++代码中,如果创建了一个对象,编译器会做什么呢?例如:

class b
{
   public :
           int p;
           virtual void fun();
};

main()
{
   b   obj;
}

编译器会为以上代码做什么?有人能解释一下内存何时被分配,虚拟表的内存何时被分配以及默认构造函数何时被调用吗?


在C语言的示例中,您建议编译器保留并填充值为5的内存。这不是编译器产品即可执行文件的行为吗? - Ewan Todd
6个回答

6

构造函数

从逻辑上讲,这两种情况没有区别:

在两种情况下,堆栈都足够大以容纳对象,并对对象调用构造函数。

请注意:

  • POD类型的构造函数不执行任何操作。
  • 没有构造函数的用户定义类型具有编译器生成的默认构造函数。

可以这样想:

int   x;  // stack frame increased by sizeof(int) default construct (do nothing)
B     a;  // stack frame increased by sizeof(B)   default construct.

同时,

int   y(6);  // stack frame increased by sizeof(int) Copy constructor called
B     b(a);  // stack frame increased by sizeof(B)   Copy constructor called

好的。当然,对于POD类型来说,构造函数非常简单,编译器会进行很多优化(甚至可能删除任何实际代码和内存地址),但从逻辑上讲,这样想也是可以的。

注意:所有类型都有一个复制构造函数(如果没有定义,编译器会自动生成),对于POD类型,你可以将其视为没有任何问题的复制构造。

关于虚表:

首先,让我指出这是一种实现细节,并不是所有编译器都使用它们。
但是虚表本身通常在编译时生成。任何需要虚表的对象都会在结构体中添加一个不可见的指针(这被包含在对象的大小中)。然后,在构造过程中,指针被设置为指向虚表。

注意:无法定义何时设置虚表,因为这不是标准规定的,并且因此每个编译器都可以在任何时候进行设置。如果你有一个多级继承体系,则虚表可能由从基类到最派生类的每个构造函数进行设置,因此在最终构造函数完成之前可能是错误的。

注意:不能在构造函数/析构函数中调用虚函数。因此,唯一能够说的是,只有构造函数完全完成后,虚表才会被正确初始化。


3

从语义上讲,堆栈指针会被减少(对于向下增长的堆栈),以sizeof b为单位,然后调用默认构造函数来设置实例。

在实践中,取决于您的体系结构和编译器(以及传递给它的标志),像您的int示例中的基本类型可能不会在堆栈上分配实际的内存空间,除非确实需要。 他们将住在寄存器中,直到需要真正的内存地址的操作出现(如&运算符)。


1
取决于你的处理器。如果你只有6个通用寄存器(x86),变量通常会存储在栈上。 - erikkallen
+1 @erikkallen。完全同意 - 这也取决于您的函数有多复杂。在ARM上,有很多GP寄存器,大多数简单的函数根本不使用堆栈。 - Carl Norum
这也可能取决于编译器的优化设置以及变量的生命周期,是否将其放入堆栈中。 - Daniel Brotherston
@erikkallen 和 Carl:提到寄存器是无关紧要的细节。它在逻辑上进入"堆栈"(由标准定义)。编译器实现物理堆栈的方式是程序无关的实现细节(它可能在寄存器中,具体取决于架构和/或编译器的成熟度)。 - Martin York
这显然不是无关紧要的;如果我在一个内存有限的嵌入式系统上,并且我想尽可能地压缩我的堆栈,该怎么办? - Carl Norum
显示剩余2条评论

2

关于虚表何时分配的问题,通常情况下是在编译时完成(尽管这取决于编译器)。

对于任何给定的类,虚表都是静态的。因此编译器可以在编译时生成表。在类的初始化期间,指向虚表的指针被设置为存储的表。

因此,相同类的不同实例将指向同一虚表。


1

关于

   b   obj;

与int一样,堆栈指针增加了b的大小。然后调用构造函数。构造函数可能会调用new或任何其他函数来分配内存,也可能不会。这取决于b的实现。调用本身不会启动任何分配。

vftable是一个静态对象。它不是随着对象本身创建的。相反,对象包含一个“不可见”的指针,指向其匹配的vftable。它的大小包括在sizeof(b)中。


请注意,当没有构造函数时,编译器很可能会生成并内联它。在这种情况下,构造函数无事可做,因此通过内联它,实际上完全省略了它。 - bdonlan
1
@ bdonlan:内联是一个红色的干扰项,在这种情况下没有用处。无论构造函数是由生成器还是由用户定义,都不重要(只会有一个)。构造函数有很多工作要做。它调用基类构造函数初始化所有成员变量(在这个特定实例中这什么也不是,但在一般情况下你的陈述是误导性的)。并且在某些时候,它可能需要初始化指向虚表的指针。 - Martin York

0
仅作为之前答案的补充,一旦对象被构造,编译器将根据其约定执行必要的魔法以确保在对象超出范围时调用析构函数。语言保证了这一点,但大多数编译器必须采取一些措施来遵循这个保证(例如设置一个指向析构函数的指针表和关于何时调用各种对象的各种析构函数的规则)。

0

根据Itanium C++ ABI标准(例如,GCC遵循该标准),虚函数表存储在单独的内存中,对于翻译单元全局可见。

对于每个动态类别构造一个虚函数表,并在对象文件中以特定名称存储,例如_ZTV5Class。 运行时类型完全为Class的类将包含指向此表的指针。 这些指针将被初始化、调整和访问,但没有任何类别包含其虚函数表在其实例中。

因此,答案是虚函数表是在编译时分配的,并且在构造期间只设置了指向它们的指针。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接