使用C++字符串可能会发生内存泄漏

10

考虑下面的C++程序:

#include <cstdlib> // for exit(3)
#include <string>
#include <iostream>
using namespace std;

void die()
{
    exit(0);
}

int main()
{
    string s("Hello, World!");
    cout << s << endl;
    die();
}

在valgrind中运行这个代码,输出结果如下(为了简洁省略了部分内容):

==1643== HEAP SUMMARY:
==1643==     in use at exit: 26 bytes in 1 blocks
==1643==   total heap usage: 1 allocs, 0 frees, 26 bytes allocated
==1643==
==1643== LEAK SUMMARY:
==1643==    definitely lost: 0 bytes in 0 blocks
==1643==    indirectly lost: 0 bytes in 0 blocks
==1643==      possibly lost: 26 bytes in 1 blocks
==1643==    still reachable: 0 bytes in 0 blocks
==1643==         suppressed: 0 bytes in 0 blocks

如您所见,有一个可能丢失了堆上的26个字节。我知道std :: string类有一个12字节的结构体(至少在我的32位x86架构和GNU编译器4.2.4上),加上一个以空终止符结束的“Hello, World!”一共是14个字节。如果我理解正确,这个12字节的结构包含一个指向字符串的指针,已分配的大小和引用计数(如果我在这里错了,请纠正我)。

现在我的问题是:C ++字符串在栈/堆中如何存储?当声明时,是否存在std :: string的堆栈对象(或其他STL容器)?

P.S. 我曾在某处读到过,valgrind在使用STL容器(以及“几乎容器”例如std :: string)的某些C ++程序中可能会报告内存泄漏的虚假阳性。我不太担心这个泄漏,但它确实引起了我对STL容器和内存管理的好奇。


2
@EboMike:我编写了die()函数,以将exit(0)调用与main()分离。现在,我的程序必须将控制权转移到被调用的函数中,在该函数中,exit(0)函数会“拔掉”此程序的执行。请记住,这个程序除了学术目的外没有任何有用的目的。 - pr1268
4
我的观点是,die()函数导致内存泄漏。如果你一定要使用die()函数,那么请将字符串放在自己的作用域内。 - EboMike
3
换句话说,“我朝脚开了枪,流血了。”我的建议是使用止血带。 - Edward Strange
5个回答

13

调用exit会"终止程序而不离开当前块,因此不会销毁具有自动存储期的任何对象。"

换句话说,无论是否泄漏,你都不应该真正关心。当你调用exit时,你的意思是“关闭此程序,我不再关心其中的任何东西。”所以别再关心了. :)

显然,它会泄漏资源,因为你从未让字符串的析构函数运行,无论它如何管理这些资源。


关于多线程,如果在子线程中调用exit,整个进程会停止吗?还是只有调用它的那个线程会停止? - Matthieu M.
@Matt: 就我所知,它好像是假装其他线程不存在,然后执行相同的操作。也就是说,线程本地静态变量会被清除,然后是程序静态变量等等。 - GManNickG

9

其他人是正确的,你泄漏了内存,因为你调用了exit。需要明确的是,泄漏不是在堆栈上分配的字符串,而是由字符串在堆上分配的内存。例如:

struct Foo { };

int main()
{
    Foo f;
    die();
}

不会导致valgrind报告泄漏。

泄漏是可能的(而不是确定的),因为您有一个指向在堆上分配的内存的内部指针。basic_string对此负责。从我的机器头文件中可以看到:

   *  A string looks like this:
   *
   *  @code
   *                                        [_Rep]
   *                                        _M_length
   *   [basic_string<char_type>]            _M_capacity
   *   _M_dataplus                          _M_refcount
   *   _M_p ---------------->               unnamed array of char_type
   *  @endcode
   *
   *  Where the _M_p points to the first character in the string, and
   *  you cast it to a pointer-to-_Rep and subtract 1 to get a
   *  pointer to the header.

关键在于_M_p并不指向堆上分配的内存的起始位置,它指向字符串中的第一个字符。这里是一个简单的例子:
struct Foo
{
    Foo()
    {
        // Allocate 4 ints.
        m_data = new int[4];
        // Move the pointer.
        ++m_data;
        // Null the pointer
        //m_data = 0;
    }
    ~Foo()
    {
        // Put the pointer back, then delete it.
        --m_data;
        delete [] m_data;
    }
    int* m_data;
};

int main()
{
    Foo f;
    die();
}

这将会报告valgrind中可能存在的泄漏。如果您注释掉我移动m_data的行,valgrind将会报告“仍然可达”。如果您取消注释我将m_data设置为0的行,您将获得一个明确的泄漏。

Valgrind 文档 中有关于可能泄漏和内部指针的更多信息。


谢谢您的回答。这解释了很多关于C++字符串及其分配的问题。 - pr1268

4
当然,这种情况会“泄漏”,因为在离开 s 的堆栈帧之前通过 exit,你没有给 s 的析构函数执行的机会。
至于关于 std::string 存储的问题:不同的实现有不同的做法。有些在堆栈上分配了12个字节,如果字符串长度小于等于12,则使用这些字节。更长的字符串则存储在堆上。其他实现总是存储在堆上。有些是引用计数的,并且具有写时复制的语义,而有些则没有。请参考 Scott Meyers 的《Effective STL》,第15项。

1

虽然在很多年前是真的,但现在GCC的标准库在默认配置下不使用任何内存池。 - Jonathan Wakely

1

我会避免使用exit(),因为我没有看到使用该调用的真正原因。不确定它是否会在清理内存之前立即停止进程,尽管valgrind似乎仍然可以运行。


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