3

我正在尝试测试我对 C++ 内存分配的理解。

对于以下程序:

{
    int a=0;
}

由于a是在堆栈外分配的,所以当变量超出范围时应该释放它,对吗?

好吧,够简单的。这个案子怎么办:

{
    Matrix m(50, 20);
}

假设有一个矩阵类,我正在创建一个具有 50 行和 20 列的新类。显然,并不是所有的内存都可以在堆栈外分配,因为 50 和 20 可以在运行时填充。所以我猜在构造函数的某个地方,他们从堆中分配内存。

当超出范围时m调用析构函数?那个析构函数应该释放(删除)它分配的内存吗?

现在真的很难了:

{
    Matrix t;
    {
        Matrix m(50, 20);
        t=m;
    }
}

那会发生什么?t 是否被分配到 m 的内存位置?还是它会复制 m 中的数据?如果 t 是对 m 的引用,那么当 m 超出范围时会发生什么?m 上的析构函数是否被调用?还是等到 t 超出范围才调用 t/m 的析构函数?

4

5 回答 5

6

当超出范围时,调用 m 上的析构函数?那个析构函数应该释放(删除)它分配的内存吗?

是的,通常是的。

现在真的很难了:

{
    Matrix t;
    {
        Matrix m(50, 20);
        t=m;
     }
}

那会发生什么?t 是否被分配到 m 的内存位置?还是它会复制 m 中的数据?

发生的情况是调用了赋值运算符:

t.operator=(m);

Matrix确保有效语义由您( 的实现者)决定。有几种可能的方法:

  1. 赋值运算符可以复制m的数据。在这种情况下,寿命和所有权没有任何困难。然而,在这种方法中,分配是昂贵的。
  2. 赋值运算符可以t指向与 相同的数据m。这可能是可行的,但需要非常小心以确保正确管理数据的生命周期,并且修改一个矩阵不会意外修改另一个矩阵。一种方法是保留一个指向数据的引用计数指针,并在修改数据时使用写时复制。一些较旧的实现std::string属于这种类型。
于 2013-01-21T17:53:34.390 回答
1

实际上,这很容易。

在第一种情况下,你是对的。自动分配,自动解除分配。

在第二种情况下,它并没有什么不同。Matrix 类构造函数处理它需要的任何额外内存,它的析构函数应该释放它。

在第三类中,内部范围变量被复制到外部范围变量。Matrix 类应遵循三规则,因此应正确处理副本。

所有这些都假设 Matrix 的正确实现。

于 2013-01-21T17:50:56.253 回答
0

C++ 内存分配非常简单,对象范围也是如此。C++ 类设计没有那么简单。

在您的示例中,每次关闭大括号时,本地对象 a 或 m 或 t (对于上一个示例中的外部大括号集)超出范围并调用它们的析构函数。在 int 的情况下,析构函数很简单,会删除堆栈上的对象。对于 m,它是类Matrix的自定义析构函数,对于任何受人尊敬的库,您可以假设它正确地释放了对象。

使 t 复杂化的不是析构函数,而是来自 m 的赋值。在大多数矩阵类的实现中,t = m会导致 m 的内容被复制到 t 中,而 t 将在堆上拥有自己的内存。然后随着每个对象超出范围,它们相应的内存被释放。

如果实现更复杂,那么由类设计器来确保它正确处理每个对象的销毁(并且您要正确使用库以便它可以)。

于 2013-01-21T18:02:22.563 回答
0
What happens then? Does t get assigned to the memory location of m?

这是对复制构造函数的调用,默认情况下会进行成员明智的复制。

但是如果您重载了赋值运算符,那么您可以分配引用或创建一个完整的新对象,这取决于您。

对于其余的问题:

调用析构函数取决于您是显式调用它还是将其保留为默认值。默认情况下,当变量或对象超出范围时将调用它。

于 2013-01-21T18:09:51.643 回答
0

你对 int 是正确的。调用分配时,堆栈上会占用 int 大小的空间,当变量超出范围时,该 int 将“弹出”堆栈,并且内存可以再次使用。

使用 Matrix,您并非 100% 正确。仅仅因为数据是在运行时分配的,并不意味着它不能在堆栈上分配,只要堆栈上有空间放置它。这里发生的是,当您创建矩阵时,被推入堆栈。它在堆栈上占用的空间量取决于它的管理方式。在其构造函数中,它可能会为堆分配内存,或者它可能只是占用一大块堆栈空间。如果堆栈上没有空间,您将在运行时收到堆栈溢出异常。

你是对的,如果它确实在构造时分配内存,它应该在销毁时清理它。但它不必,并且在某些情况下不应该(例如,如果它实际上不再“拥有”该内存,因为它已与另一个对象共享/共享它)。

在你的最后一种情况下,会发生什么:

Matrix t;

这将使用类的默认构造函数创建一个 Matrix 。它是否在此处保留任何内存取决于构造函数在其中的内容。它可能会保留内存,但可能不会。

Matrix m(50, 20);

这将使用该类的不同构造函数创建一个 Matrix。同样,它可能会保留内存,但也可能不会(例如,在将一些“真实”数据添加到其中之前,它可能不会占用堆栈上的空间以外的任何空间)

t=m;

这里发生的事情再次取决于班级。此时,this不是构造函数,而是t的赋值函数:

Matrix& operator=(const Matrix& m){
    ....
}

被调用。有些赋值构造函数会复制数据,有些会复制指向数据的指针,但不会复制实际数据本身。然而,在后一种情况下,类应该通过拒绝删除数据,而是依靠 t 的构造函数来处理 m 超出范围的情况。但同样,他们不必这样做,而且可能做得很糟糕(尤其是在控制变得复杂的情况下)

关键的一点是,一旦达到类创建和销毁的级别,内存的处理方式取决于类的实现。

于 2013-01-21T18:17:48.360 回答