内存分配和构造函数

Memory allocation and constructor

很抱歉,如果在标准中明确说明之前已经问过,但是我没有找到自动存储对象的内存是在封闭块的开头分配还是在执行构造函数之前立即分配?

我问这个是因为 https://en.cppreference.com/w/cpp/language/storage_duration 这么说。

Storage duration All objects in a program have one of the following storage durations:

automatic storage duration. The storage for the object is allocated at the beginning of the enclosing code block and deallocated at the end. All local objects have this storage duration, except those declared static, extern or thread_local.

现在,这是否意味着即使由于某种原因未调用构造函数,也会分配存储空间 space?

比如我有这样的东西。

{
     if(somecondition1) throw something;
     MyHugeObject o{};
     /// do something
}

因此,有可能不需要构造 MyHugeObject,但根据我引用的来源,它的内存仍然被分配,尽管该对象可能永远不会被构造。是这样还是基于实现?

C++ 标准在 [basic.stc] 中有以下说明:

2 Static, thread, and automatic storage durations are associated with objects introduced by declarations (6.1) and implicitly created by the implementation (6.6.7).

6.6.7 引用指的是 [class.temporary],这是关于临时文件的。临时对象不是完全相同的概念,但该部分是这样说的:

2 The materialization of a temporary object is generally delayed as long as possible in order to avoid creating unnecessary temporary objects.

我没有找到任何其他可以解决您的问题的方法,因此该标准似乎为实现提供了一些回旋余地,以决定何时为对象分配存储空间。

请注意,这不适用于初始化对象时 - 根据 [stmt.dcl] 执行声明语句时会发生这种情况:

2 Variables with automatic storage duration (6.6.5.3) are initialized each time their declaration-statement is executed. Variables with automatic storage duration declared in the block are destroyed on exit from the block (8.6).

您提到的 cppreference link 可能讨论了一个典型的实现,其中具有自动存储持续时间的对象分配在堆栈上。在这样的实现中,在封闭块的开头分配存储是有意义的(毕竟它只是堆栈指针的简单 (in/de)crement,对它们进行分组是有益的)。

如果您想避免在不需要时为巨大的对象分配存储空间,重组代码是一种选择。在某些实现中,引入一个额外的块范围将实现这一点:

{
    if(somecondition1) throw something;
    {
        MyHugeObject o{};
        /// do something
    }
}

在其他实现中,可能需要其他方法。 @DanielLangr 下面的评论指出了分配发生在封闭函数开始处而不是块开始处的实现。

系统 回收内存的时刻取决于实现。标准唯一规定的是调用构造函数的时刻以及可以安全使用对象的时刻。

常见的实现使用堆栈来自动存储持续时间对象,并且大部分时间在块的开头分配整个帧并在块的末尾弹出它.即使堆栈操作很快,限制它们的数量也更简单,而且越简单越健壮。

但无论如何,即使使用堆栈进行自动存储持续时间也不是标准强制要求的,更不用说帧 分配到 弹出的时刻来自那个堆栈。

首先,从语言标准的角度来看,你不能在对象的生命周期之外访问对象的存储。在创建对象之前,您不知道对象位于何处,并且在它被销毁之后,访问存储会产生未定义的行为。简而言之:符合标准的 C++ 程序无法观察到存储分配时间的差异

自动存储通常意味着 "on the call-stack"。 IE。分配是通过递减堆栈指针来实现的,而释放是通过重新递增它来实现的。编译器 可以 发出代码,在对象 starts/ends 的生命周期精确调整堆栈指针,但这是低效的:它会用两条额外的指令使生成的代码混乱使用的每个对象。对于在循环中创建的对象,这尤其是一个问题:堆栈指针会不断地在两个或多个位置之间来回跳转。

为了提高效率,编译器将所有可能的对象分配集中到一个单一的堆栈帧分配:编译器为函数内的每个变量分配一个偏移量,确定最大值。存储函数中存在的所有变量所需的大小,并在函数执行开始时使用单个堆栈指针递减指令分配所有内存。然后清理是相应的堆栈指针增量。这消除了循环的任何 allocation/deallocation 开销,因为下一次迭代中的变量将简单地重用堆栈帧中与前一次迭代使用的相同位置。 这是一项重要的优化,因为许多循环至少声明了一个变量。

C++ 标准不关心。由于在对象生命周期之外使用存储是 UB,因此编译器可以随意使用存储来做任何它想做的事情。程序员也不应该关心,但他们确实倾向于关心他们的程序执行时间。这就是大多数编译器通过使用堆栈帧分配进行优化的目的。