什么时候在堆栈上实例化C++类?

1 c++ memory-management

我想澄清一下在堆栈上实例化类时会发生什么.

在堆上实例化C++类时:

MyClass *myclass = new MyClass();
Run Code Online (Sandbox Code Playgroud)

创建一个类型为MyClass的指针,并通过"new MyClass();"在同一行上实例化该类.像这样伸展它:

MyClass *myclass;
myclass = new MyClass();
Run Code Online (Sandbox Code Playgroud)

如果我没弄错的话,在第1行创建一个指针,然后在第二行为实例分配内存,并将指向实例地址的指针分配给myclass.

这是否意味着当一个类以这种方式在堆栈上实例化时:

MyClass myclass = MyClass();
Run Code Online (Sandbox Code Playgroud)

它创建了两次?

Pub*_*bby 7

"堆栈"和"堆"在C++中没有定义,不需要在任何地方分配内存.

以你为例:

MyClass myclass = MyClass();
Run Code Online (Sandbox Code Playgroud)

您正在myclass通过复制构造函数初始化到临时对象.myclass(和临时的)有自动存储,你可以考虑在"堆栈"上分配,如果这让你开心.

在这种情况下允许复制省略,基本上将其优化为MyClass myClass,但请注意,它不能总是这样做,例如当复制构造函数是私有的时.

以下是您可以测试的示例:

struct obj {
  static int c;
  int myc;
  obj() : myc(c++) {
    std::cout << "ctor of " << myc << '\n';
  }
  obj(const obj&) : myc(c++){
    std::cout << "copy ctor of " << myc << '\n';
  }
  ~obj() {
    std::cout << "dtor of " << myc << '\n';
  }
};
int obj::c = 1;

int main(int argc, char** argv)
{
  obj x = obj();
}
Run Code Online (Sandbox Code Playgroud)

如果副本被删除,您将看到:

ctor of 1
dtor of 1
Run Code Online (Sandbox Code Playgroud)

否则(gcc选项-fno-elide-constructors,以防止出现省略):

ctor of 1
copy ctor of 2
dtor of 1
dtor of 2
Run Code Online (Sandbox Code Playgroud)

此外,将复制构造函数设置为私有将导致编译器错误.

  • @Santiago大多数编译器使用硬件堆栈实现自动存储,但这不是C++的属性,而是特定编译器的属性。并非所有编译器都会做同样的事情 - 例如,有些编译器将局部变量分配到全局存储中。这就是为什么 C++ 本身没有根据堆栈/堆来定义。 (2认同)

Cas*_*Cow 6

在使用指针的情况下,第一行只是声明一个指针.使用new的行不只是分配内存,它还将调用要调用的MyClass的默认构造函数.您可以通过以下方式执行以下操作:

MyClass * myClass = new MyClass;

MyClass之后的括号在没有参数的情况下构造时是可选的.

该对象不会在堆栈上创建,并且将一直存在,直到在指针上调用delete.如果这种情况没有发生,你就会有泄漏.

如果是 MyClass myClass = MyClass();

该类将创建两次,首先使用默认构造函数,然后使用复制构造函数.编译器可能会将其优化为单个构造,但您应该只是初始化:

MyClass myClass;

请注意,您不得在声明中使用括号,否则它将声明一个函数而不是该类的实例.

在这种情况下,将在堆栈上创建一个实例.在复制构造的过程中,您的方法可能会创建一个"临时",它是一种堆栈变量.您可以将临时视为由构造函数"返回"并返回值,这是一个棘手的区域,通常是自动的并使用堆栈空间.

  • 当人们引用堆栈和堆时,它们通常意味着自动和免费存储.当然,在自由存储中创建的对象的"自动"成员变量(即使用new)肯定不会物理地存在于堆栈中.从技术上讲,MyClass myClass = MyClass()应该调用默认构造函数和复制构造函数,因为它们必须都可以访问,但实际上副本可能永远不会发生.它肯定不会调用的一件事是赋值. (2认同)

Mat*_* M. 5

就标准而言,没有堆栈和堆的概念。但是,我所知道的所有 C++ 实现都将概念“自动存储持续时间”和“动态存储”分别映射到堆栈 (*) 和堆中。

(*) 如 所述@MooingDuck,这仅适用于函数变量。全局变量和静态变量(可能)具有自动存储持续时间,但它们不在堆栈中。


现在清除了:

  • 函数体中的变量存储在堆栈中
  • 创建的对象new存储在堆中(并返回它们的地址)

举个例子,更直观一点:

void f0() {
  Class* c = new Class();
}

void f1() {
  Class* c = 0;
  c = new Class();
}
Run Code Online (Sandbox Code Playgroud)

这里c(类型Class*)存储在堆栈上并指向存储在堆上的对象(类型Class

void f2() {
  Class c = Class();
}

void f3() {
  Class c;
}
Run Code Online (Sandbox Code Playgroud)

这里c存储在堆栈中。在f2可能是临时的(没有名字的对象)由表达式创建Class(),然后复制到c(取决于编译器elides复制或没有),临时对象的存储是没有解决的标准......他们通常使用的堆栈尽管。


最后一句话:这是否最终实际使用了堆栈上的一些空间是另一回事。

  • 编译器可能完全不需要对象
  • 变量可以存储在堆栈或寄存器中(CPU 特定的“插槽”)

在行动:

// Simple test.cpp
#include <cstdio>

struct Class { void foo(int& a) { a += 1; } };

int main() {
  Class c;

  int a = 0;

  c.foo(a);

  printf("%d", a);
}
Run Code Online (Sandbox Code Playgroud)

编译器(使用 Clang/LLVM...稍微修改)生成:

@.str = private unnamed_addr constant [3 x i8] c"%d\00", align 1

define i32 @main() nounwind uwtable {
  %1 = tail call i32 (i8*, ...)* @printf(@.str, i32 1)
  ret i32 0
}
Run Code Online (Sandbox Code Playgroud)

请注意如何:1. 类已被删除,2. 调用foo已被删除,3 a. 甚至没有出现。转换回 C++,我们得到:

#include <cstdio>

int main() {
  printf("%d", 1);
}
Run Code Online (Sandbox Code Playgroud)

如果我们生成程序集(64 位 X86):

main:                        # @main
pushq   %rax             # save content of 'rax' on the stack
movl    $.L.str, %edi    # move address of "%d" into the 'edi' register
movl    $1, %esi         # move 1 into the 'esi' register
xorb    %al, %al         # --
callq   printf           # call printf, it'll look up its parameters in registers
xorl    %eax, %eax       # --
popq    %rdx             # restore content from stack to 'rdx'
ret                      # return
Run Code Online (Sandbox Code Playgroud)

请注意常量($1$.L.str)是如何被推入寄存器(%esi%esi响应)并且永远不会“命中”堆栈的。唯一的堆栈操作是pushqpopq(我不知道它们实际上保存/恢复了什么。