C++内存释放

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的析构函数?

“m 的析构函数会被调用吗?” 是的,“还是它会等到 t 超出范围后才调用 t/m 的析构函数?” 不会。其他问题涉及 Matrix 的程序员所决定的内容,我们无法知道。 - Cheers and hth. - Alf
把它想象成结束大括号 '}' 的位置,任何栈变量的析构函数都会在这里被调用。 - James
这完全取决于Matrix的编码方式。 - juanchopanza
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实现就属于这种类型。

在示例中Matrix m(50,20)中是否有堆分配? - David G
1
@David:很有可能是这样。然而,可以构建一种实现方式,在这种方式中不会有任何内存(其中内存来自堆栈,或直接从操作系统绕过堆分配)。 - NPE
@David 可能是这样,但如果不查看代码就无法确定。 - juanchopanza
std::string 的实现大多已经放弃了引用计数和 CoW,转而采用小字符串优化。首先,C++11 不再允许 CoW,而且在多线程程序中共享会影响性能。 - bames53

1

实际上,这很容易。

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

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

在第三个类中,内部作用域变量被复制到外部作用域变量。Matrix类应该遵循三大法则,因此应正确处理副本。

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


0

你对于int的理解是正确的。当分配被调用时,栈上会占用一定数量的空间,当变量超出作用域时,该int就会从栈中“弹出”,内存就可以再次使用。

对于矩阵,你并不完全正确。虽然数据是在运行时分配的,但只要栈上有足够的空间,它就可以在栈上分配。在创建矩阵时,它会被推到栈上。它在栈上占用的空间取决于它的管理方式。在其构造函数中,它可能会将内存分配给堆,或者它可能只是占用了大块的栈空间。如果栈上没有足够的空间,你将在运行时得到一个栈溢出异常。

你说的没错,如果它在构造时分配了内存,那么在析构时应该清理它。但是它不必这样做,在某些情况下,它甚至不应该这样做(例如,如果它实际上已经将内存转移/共享给另一个对象)。

在最后一种情况下,发生的情况如下:

Matrix t;

这将使用类的默认构造函数创建一个矩阵。它是否在此处保留任何内存取决于该构造函数中包含什么。它可能会保留内存,但也可能不会。

Matrix m(50, 20);

这使用类的不同构造函数创建矩阵。同样,它可能会保留内存,但也可能不会(例如,除了堆栈上的空间外,它可能不占用任何其他空间,直到添加了一些“真实”数据为止)

t=m;

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

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

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

这里至关重要的是,一旦你达到了类创建和销毁的层次,内存如何处理取决于类的实现。


0

C++的内存分配非常简单,对象作用域也是如此。但C++类设计并不像这么简单。

在你的例子中,每次你关闭一个大括号时,局部对象a、m或t(对于你最后一个例子中的外部大括号集)就会超出作用域,并调用它们的析构函数。对于int来说,析构函数很简单,只需删除堆栈上的对象即可。对于m来说,它是Matrix类的自定义析构函数,对于任何值得尊敬的库,您可以假设它正确地释放了对象。

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

如果实现更加复杂,则由类设计人员确保正确处理每个对象的销毁(您需要正确使用库以便它能够正常工作)。


0
What happens then? Does t get assigned to the memory location of m?

这是对默认成员浅拷贝的复制构造函数的调用。

但是如果您已经重载了赋值运算符,那么可以选择分配引用或创建一个完全新的对象。

对于其余问题:

析构函数的调用取决于您是显式调用还是让它使用默认设置。 在默认情况下,当变量或对象超出范围时会调用它。


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