【问题标题】:How does the memory release for big return values (such as string) happen in C++?大返回值(如字符串)的内存释放在 C++ 中是如何发生的?
【发布时间】:2015-01-29 18:58:21
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 按值返回:请阅读返回值优化(复制省略)和右值引用。
  • @DieterLücking RVO 在问题中被反复提及,所以显然 OP 知道这一点。
  • 你不用担心编译器是否支持RVO。从 C++11 开始,移动语义(由右值引用提供)保证返回字符串(或任何具有移动构造函数的对象)非常有效。
  • @Ferrucio Tnahk 你,RVO 部分现在很清楚。释放内存怎么样?在 Java 中,我会依赖垃圾收集器并说如果没有人使用这些数据,垃圾收集器会处理它。但是在 C++ 中会发生什么?
  • @lulijeta:在 C++ 中,主要的范例是 RAIIstd::string 紧随其后,因此当使用它的 std::string 对象超出范围(或以其他方式销毁)时,内存将被释放。

标签: c++ string memory-management return-value return-value-optimization


【解决方案1】:

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

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

谁清除调用者对象?和其他所有对象一样,它遵循 RAII 规则:它的析构函数在作用域结束时被调用,而堆栈指针在函数结束时回绕过去。

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

编辑:至于 RVO 有多普遍,任何理智的 PC 编译器都支持它。它与 #pragma once 一样受到支持,尽管古怪的纯粹主义者总是说它不“标准”。

【讨论】:

  • Oddball purist here:该标准在 14882:1999 §12.8.15 中指定了 RVO(“编译器可以省略复制/移动返回值”),并在 14882:2011 §12.8.31- 中对其进行了扩展32 包括移动省略。
  • “可能”意味着许可,而不是强制执行。也许我误解了你的意思,这是什么?
  • 我的观点是,实现 RVO 的许可从一开始就在标准中,16 年后,可以假设大多数(如果不是全部)编译器都会实现它,但很少有例外。
  • 这正是我所说的。我很困惑你为什么提出这个问题,但我们在这里达成了一致!
  • 我认为令人困惑的是,如果我理解正确的话,您最初的“古怪纯粹主义者”言论是针对那些可能会说#pragma once 不标准的人。但 grayfade 将其解释为针对可能(错误地)说 RVO 不标准的人。
【解决方案2】:

假设您的代码使用了 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 将使用placement new 来就地创建返回值(但你在C++ 中显示的任何内容都不能精确复制编译器的内容确实)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-05-23
    • 1970-01-01
    • 2011-11-11
    • 2015-07-07
    • 1970-01-01
    • 1970-01-01
    • 2017-07-27
    • 1970-01-01
    相关资源
    最近更新 更多