使用std::list<std::string>时出现std::string内存泄漏问题

19

我正在处理当前项目中的std::list<std::string>。但是与此相关的某处存在内存泄漏问题。因此,我已经单独测试了有问题的代码:

#include <iostream>
#include <string>
#include <list>

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
}

Line::~Line() {
    //mString->clear(); // should not be neccessary
    delete mString;
}

int main(int argc, char** argv)
{
    // no memory leak
    while (1==1) {
        std::string *test = new std::string("XXXXXXXXXXXXXXXXXXXXXXXX");
        delete test;
    }

    // LEAK!
    // This causes a memory overflow, because the string thats added
    // to the list is not deleted when the list is deleted.
    while (1==1) {
        std::list<std::string> *sl = new std::list<std::string>;
        std::string *s = new std::string("XXXXXXXXXXXXXXXXXXXXXXX");
        sl->push_back(*s);
        //sl->pop_back(); //doesn't delete the string?- just the pointer
        delete sl;
    }

    // LEAK!
    // Here the string IS deleted, but the memory does still fill up
    // but slower
    while (1==1) {
        std::list<Line> *sl = new std::list<Line>;
        Line *s = new Line();
        sl->push_back(*s);
        //sl->pop_back(); //does delete the Line-Element
        sl->clear();
        delete sl;
    }
    return 0;

    // this does not cause any noticable memory leak
    while (1==1) {
        std::list<int> *sl = new std::list<int>;
        int i = 0xFFFF;
        sl->push_back(i);
        sl->clear();
        delete sl;
    }
    return 0;

    // This does not cause any overflow or leak
    while (1==1) {
        int *i;
        i= new int [9999];
        delete[] i;
    }

}

为什么我的字符串列表会导致内存泄漏?删除列表时,不应该对每个包含的字符串调用析构函数吗?

384
不要过多使用new关键字。我看不出你在任何地方使用new的原因。在C++中,您可以通过值创建对象,并且这是使用该语言的巨大优势之一。您不必将所有内容都分配到堆上。停止像Java程序员一样思考。 - Omnifarious
5个回答

31

在第一个例子中,list 类并不知道你使用 new 分配了字符串,并且无法删除它。特别地,这个列表中只包含你传入的字符串的一个副本。

同样,在第二个例子中,你从未释放 s 这个 Line 对象,因此会泄露内存。无法正确实现复制构造函数的原因是内部字符串没有被删除。因此,如果你复制一个 Line 对象,两个对象都将引用相同的字符串指针,如果尝试同时删除它们,就会有问题。


14
您的Line类需要一个正确处理字符串指针的复制构造函数和赋值运算符。
或者,只需拥有一个std :: string成员而不是指针,并让string类处理内存(这就是它的作用)。

是的,在类中进行的任何动态分配都意味着你需要实现一个复制构造函数和赋值运算符。当你推入到列表时,它会推入一个对象的“副本”。使用当前代码,将会有两个包含指向同一位置的指针的对象实例;所以你可能会冒着删除字符串两次的风险。我非常推荐这本书: https://www.amazon.co.uk/Professional-C-Marc-Gregoire/dp/1119421306/ref=dp_ob_title_bk - Den-Jason

8

这是您的漏洞:

while (1==1) {
    std::list<Line> *sl = new std::list<Line>;
    Line *s = new Line();
    sl->push_back(*s);
    //sl->pop_back(); //does delete the Line-Element
    sl->clear();
    delete sl;
}

STL集合通过值进行存储,为其分配和释放空间。你需要显式地释放你所分配的空间。只需在循环结束时添加delete s即可。

如果你必须存储指针,则考虑存储像boost::shared_ptr这样的管理指针,或者查看Boost指针容器库

仔细检查后,你根本不需要在堆上分配Line。只需将其更改为:

sl->push_back(Line());

同时,正如其他人所指出的那样,请确保在拷贝构造函数、拷贝赋值运算符和析构函数中正确管理Line的指针成员。


2
    std::list<Line> *sl = new std::list<Line>;
    Line *s = new Line();
    sl->push_back(*s);
    //sl->pop_back(); //does delete the Line-Element
    sl->clear();
    delete sl;

你忘记删除 s 了。你new了它,你必须删除它。由于你在Line类中管理内存时会复制对象(通过将它们塞到列表中),因此你还需要为Line类提供一个拷贝构造函数和赋值运算符。


2
其他人已经具体解释了为什么会发生泄漏——删除指针列表不会删除所指向的对象,因为简单指针无法表明它是否是该对象的唯一引用。但是,在删除时确保迭代列表以删除指针并不是唯一的方法。
就像这里的例子展示的那样,根本没有理由使用指向任何东西的指针,因为当它们进入作用域并在离开作用域时被丢弃——只需在堆栈上创建所有内容,编译器将在退出作用域时正确处理所有内容。例如:
while (1==1) {
    std::list<std::string> sl;
    std::string s = std::string("XXXXXXXXXXXXXXXXXXXXXXX");
    sl.push_back(s);
}

如果您确实需要指针行为(以避免重复链接到许多对象等等),则应查看智能指针,因为这些指针可以自动处理所需的引用计数和语义,从而消除许多陷阱。 (具体来说,请参见 boost 智能指针

根据特定需求和所有权语义,可以使用许多类型的智能指针来表示。

std::auto_ptr 具有严格的所有权 - 如果“复制”指针,则原始指针将被置空并转移所有权 - 只会有一个有效的 auto_ptr 对象。当拥有所有权的智能指针超出范围时,所指向的对象将被删除。

还有 boost shared pointers 和 weak pointers 使用引用计数来知道何时释放所指向的对象。对于 Shared pointers,每个指针副本都会增加一个引用计数,并且当所有共享指针超出范围时,所指向的对象将被删除。弱指针指向由共享指针管理的对象,但不会增加引用计数,如果所有父共享指针都被删除,则尝试取消引用弱指针将抛出易于捕获的异常。

显然,智能指针的范围还有很多,但我强烈建议您查看它们作为管理内存的解决方案。


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