【问题标题】:Valgrind indicates memory leak with shared_ptr c++11Valgrind 用 shared_ptr c++11 指示内存泄漏
【发布时间】:2018-03-25 16:38:25
【问题描述】:

我已经使用 C++11 智能指针实现了一个链接列表。这个实现使用 shared_ptr 来存储内部数据结构,用于实现它的隐式共享。我在下面提供源代码的相关部分:

namespace Algos {

template <typename T>
struct LinkedListData{
    struct Node {
        std::unique_ptr<Node> next;
        Node *prev = nullptr;
        T data;
    };

    std::unique_ptr<Node> root;
    Node *last = nullptr;
    int size = 0;

    LinkedListData() {
        root = std::make_unique<Node>();
        root->prev = nullptr; //last virtual element
        root->next = std::make_unique<Node>();
        last = root->next.get();
        last->prev = root.get();
        last->next = nullptr;
    }

    //deferr pointers manually to avoid stackoverflow due to 
    //recursion explosion
    static void cleanup(LinkedListData<T> *data) { 
        #ifdef DEBUG_TXT 
            int nodeCount=0;
        #endif
        Node *n = data->last;
        if(n==nullptr) { return; }
        while(n) {
            #ifdef DEBUG_TXT 
                if(n->next.get())
                    std::cout << "Release {n->next()} [" << ++nodeCount  << 
                    "]: "<< n->next.get() << std::endl;
            #endif
            n->next.release();
            ALGO_ASSERT(n->next.get() == nullptr, "Node reference not deferred");
            n = n->prev;
        }
        data->size = 0;
        #ifdef DEBUG_TXT
            std::cout << "Release {Root} [" << ++nodeCount  << "]: "<< data->root.get() << std::endl;
        #endif
        data->root.release();
    } 
};

template <class T>
class LinkedList {

    typedef typename LinkedListData<T>::Node node_type;
    std::shared_ptr<LinkedListData<T> > d;

    public:

        LinkedList() : d(new LinkedListData<T>(), LinkedListData<T>::cleanup){}

/* Code omitted .... */

};

}

由于使用new,以下代码在shared_pointer构造函数中触发valgrind上的内存泄漏错误:

#include <iostream>
#include <string>
#include <unistd.h>

// #define DEBUG_TXT

#include "global/assert.h"

#include "linked_list/linkedlist.h"

#define TEST_SIZE 5000

struct DataTest {
   int integer;
   bool boolean;
   std::string txt;
   DataTest& operator=(const DataTest& other) {
      integer = other.integer;
      boolean = other.boolean;
      txt = other.txt;
      return (*this);
   }

   bool operator==(const DataTest& other) const {
      return (
         integer == other.integer &&
         boolean == other.boolean &&
         txt == other.txt
      );
    }   
};

struct Data {
    DataTest data[TEST_SIZE];
    const int n = TEST_SIZE;

    static void initDataSample(Data &d) {
        for(int i=0; i<d.n; i++) {
            d.data[i].integer = i;
            d.data[i].boolean = (i%2 == 0);
            d.data[i].txt = "abc";
        }
    }
};

void appendElements(Algos::LinkedList<DataTest> &l, const Data& d){
    for(int i=0; i<d.n; i++) {
        l.append(d.data[i]);
    }
}

void prependElements(Algos::LinkedList<DataTest> &l, const Data& d) {
    for(int i=d.n-1; i>=0; i--) {
        l.prepend(d.data[i]);
    }
}



int main(int argv, char* argc[]) {

   Data d;  
    Data::initDataSample(d);

    Algos::LinkedList<DataTest> l1;
    {
        Algos::LinkedList<DataTest> l2;
        l1 = l2;
    }

    sleep(2);

    appendElements(l1, d);

    int removeSize = l1.size()/2;

    for(int i=0; i<removeSize; i++)
        l1.takeFirst();


    prependElements(l1, d);

    removeSize = l1.size()/2;
    for(int i=0; i<removeSize; i++)
        l1.takeLast();


   return 0;
}

这是我在 valgrind 控制台中收到的消息:

> ==8897== HEAP SUMMARY:
==8897==     in use at exit: 282,976 bytes in 3,757 blocks
==8897==   total heap usage: 10,009 allocs, 6,252 frees, 633,040 bytes allocated
==8897== 
==8897== 136 (24 direct, 112 indirect) bytes in 1 blocks are definitely lost in loss record 5 of 8
==8897==    at 0x4C2E216: operator new(unsigned long) (vg_replace_malloc.c:334)
==8897==    by 0x407C1C: Algos::LinkedList<DataTest>::LinkedList() (linkedlist.h:74)
==8897==    by 0x4074B4: main (insert_delete_rounds.cpp:63)
==8897== 
==8897== 210,136 (24 direct, 210,112 indirect) bytes in 1 blocks are definitely lost in loss record 8 of 8
==8897==    at 0x4C2E216: operator new(unsigned long) (vg_replace_malloc.c:334)
==8897==    by 0x407C1C: Algos::LinkedList<DataTest>::LinkedList() (linkedlist.h:74)
==8897==    by 0x4074C3: main (insert_delete_rounds.cpp:65)
==8897== 
==8897== LEAK SUMMARY:
==8897==    definitely lost: 48 bytes in 2 blocks
==8897==    indirectly lost: 210,224 bytes in 3,754 blocks
==8897==      possibly lost: 0 bytes in 0 blocks
==8897==    still reachable: 72,704 bytes in 1 blocks
==8897==         suppressed: 0 bytes in 0 blocks
==8897== Reachable blocks (those to which a pointer was found) are not shown.
==8897== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==8897== 
==8897== For counts of detected and suppressed errors, rerun with: -v
==8897== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

据我所知,没有其他方法可以在不使用 new 的情况下使用自定义 Deleter 来启动 shared_ptr > 在 C++11 中。在阅读了 stackoverflow 上的文档和不同的线程后,我注意到 std::make_shared() 不支持传递自定义 Deleter。所以,我的问题是:这个内存泄漏警告合法吗?如果是,是否有可能避免它?

开发工具设置:

  • gcc: 5.4.0
  • Valgrind:3.13.0
  • 操作系统:Linux Ubuntu 16.04

【问题讨论】:

    标签: c++ c++11 data-structures valgrind


    【解决方案1】:

    回答您的问题:

    1. 您使用allocate_shared 来允许使用自定义内存分配器。签名是相同的,只是它将分配器的 const 引用作为第一个参数。

    2. 虽然你可以清理LinkedListData 的内部结构,但你永远不会在delete 方法中分配delete 指针。 cleanup 应该从 LinkedListData 析构函数中调用,并且您应该使用常规删除器(或者如果使用自定义分配器,则使用自定义删除器)在 LinkedListData 指针上调用 delete。

    简而言之,这行总会导致内存泄漏:

    LinkedList() : d(new LinkedListData<T>(), LinkedListData<T>::cleanup){}
    

    【讨论】:

    • 所以,d 是一个 shared_ptr 的事实并没有在 refCount 达到零时为我提供指针本身的自动删除,因为我明确使用 new ,还是因为我使用了自定义删除器?
    • @FilipeCalasans 这不是因为您使用的是自定义删除器。您必须手动删除删除器中的指针。 shared_ptr 将在引用计数达到 0 时调用删除器。
    【解决方案2】:

    你创建了一个new LinkedListData&lt;T&gt;(),但你没有在cleanup里面调用delete

    【讨论】:

      【解决方案3】:

      由于您在内部使用智能指针,因此不应将 LinkedListData::cleanup 指定为自定义删除器,而应使用默认删除器。如果您尝试坚持使用智能指针,如果您没有明确调用delete,则避免使用new 调用可能是个好主意。因此,d 成员可以仅使用 d(std::make_shared&lt;LinkedListData&lt;T&gt;&gt;()) 进行初始化。此外,它可能可以声明为LinkedListData&lt;T&gt; d;,而没有任何智能指针。

      那么,你不应该在LinkedListData::cleanup 中调用release。此方法将原始指针从智能指针包装器中分离出来,并且不会释放关联的对象。您可能想要reset。但是再一次,这个方法根本不需要,因为你的所有指针都是智能的,并且在调用父析构函数时应该自动清理相关数据。

      【讨论】:

      • 我明确地调用了一种清理方法,以避免析构函数递归爆炸,正如演讲中指出的那样:C++ 中的无泄漏...默认情况下,由 Herb Sutter 在 cppcon 2016 @987654321 中给出@(18 分钟:17 秒的视频)。但是,我必须承认我使用了错误的功能:释放而不是重置。
      • 我明白了,好点子。然后只需从对象析构函数中调用此手动清理,而不是作为自定义删除器。
      • 完成,在析构函数中调用清理方法。在这种特定情况下,实际上我不需要析构函数。我没有实现一个更花哨的分配器,这需要我有一个自定义删除器。您的建议更安全,更优雅。谢谢@dewaffled
      • 完成,在析构函数中调用清理方法。在这种特定情况下,实际上我不需要自定义删除器。归根结底,我并没有实现更花哨的分配器。您的建议更安全,更优雅。谢谢@dewaffled(已编辑)
      猜你喜欢
      • 2020-03-31
      • 2016-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-28
      • 1970-01-01
      • 2013-06-24
      • 2015-11-19
      相关资源
      最近更新 更多