大返回值(如字符串)的内存释放如何在C++中发生

How does the memory release for big return values (such as string) happen in C++?

本文关键字:C++ 释放 内存 返回值 字符串      更新时间:2023-10-16

假设我在C++中有一个函数A(),它调用另一个函数B()。B() 打开一个文件并读取一个长字符串,然后将此字符串返回到 A()。Than A() 使用此字符串作为 C() 的输入参数。C() 需要对字符串的引用。

为此,我看到了两种解决方案:

  1. B() 获取对字符串的引用作为输入参数(称为 str)。B() 编辑 str,因此 A() 获取字符串。释放分配的内存是 A() 的责任。当 A() 使用字符串完成时,它将内存返回给系统。这对我来说很清楚。

  2. B() 返回一个字符串。我对这个选项感到困惑。那么谁释放分配的内存呢?B() 创建一个本地字符串,因此无法返回其地址。B() 必须按值返回参数。据我所知,由于返回值优化,没有创建字符串的实际副本。在后台,参数仍然通过引用传递。当我写这样的伪东西时:

A() {
  C(B(filename))
}

与分配的内存相比会发生什么?B() 获取文件名,打开文件,为它从文件中读取的字符串分配一个内存块,然后返回此字符串。这是B()生命范围的结束。A() 没有定义一个变量来寻址此字符串。谁把这个内存块还给了?我可以依靠RVO吗?性能可以按值返回吗?每个编译器都有吗?

是否使用 RVO 是一种优化,而且是透明的优化。我将在最后介绍这一点,因为它对按值返回对象的机制没有区别。

现在,当您按值返回对象时,编译器实际上所做的是添加一个不可见的参数作为指针。此内存在调用方的堆栈上分配。您的函数使用编写的代码执行其操作(请记住,没有 RVO),然后返回使用本地对象调用调用者对象上的复制构造函数的对象。然后当地的那个被摧毁了。

谁清除调用方对象?与其他所有对象相同,它遵循 RAII 规则:当作用域结束时调用其析构函数,当函数结束时,堆栈指针会倒退到它之外。

现在关于RVO。RVO所做的只是避免函数中的内部创建+复制+desctructor 调用 - 它直接与函数外部的对象一起工作。它将在其上调用正确的构造函数(想想放置new),然后它将使用它的字段和函数来完成工作,最后没有什么可做的了。它不会被摧毁或释放,因为这就是结果 - 它已经在调用者手中。

编辑:至于RVO的普遍程度,任何理智的PC编译器都支持它。它和#pragma once一样得到支持,尽管古怪的纯粹主义者总是说它不是"标准"。

假设使用 RVO,对于您的代码:

A() {
  C(B(filename))
}

编译器在 A 的上下文中创建类型 std::string 的临时。它基本上在调用该临时时传递一个隐藏的引用 B . B写入引用的字符串,然后返回。

然后将该临时对象传递给 C 。因此,最终代码大致等同于此顺序上的内容:

// string B(); is turned into:
void B(string &ret) { 
    ret = "a really long string";
}
void A() { 
    {
        std::string temporary_object;
        B(temporary_object);
        C(temporary_object);
    }
}

我在表达式的扩展周围添加了一个额外的块,以强调临时对象是在表达式C(B(filename))开始执行时创建的,并在表达式结束时再次销毁。

我上面展示的内容在某些方面与真实内容不同。例如,如果你有一个不支持默认构造的类,即使涉及 RVO,它仍然可以工作(上面的代码要求类型是默认可构造的,这对于std::string很好,但对于其他任何东西可能不太有效)。如果你想更准确一点,你可能会传递一个指向原始内存的指针,B会使用放置 new 来就地创建返回值(但你可以显示的任何内容都不会C++精确地复制编译器的功能)。