释放已使用数据的内存会导致分段错误

Posted

技术标签:

【中文标题】释放已使用数据的内存会导致分段错误【英文标题】:Freeing memory of used data leads to Segmentation Fault 【发布时间】:2012-06-15 21:04:28 【问题描述】:

我写了一个哈希表,它基本上由这两种结构组成:

typedef struct dictEntry 
    void *key;
    void *value;
    struct dictEntry *next;
 dictEntry;

typedef struct dict 
    dictEntry **table;
    unsigned long size;
    unsigned long items;
 dict;

dict.table是一个多维数组,包含了所有存储的键/值对,又是一个链表。

如果哈希表的一半已满,我将其扩大一倍并重新散列:

dict *_dictRehash(dict *d) 
    int i;
    dict *_d;
    dictEntry *dit;

    _d = dictCreate(d->size * 2);

    for (i = 0; i < d->size; i++) 
        for (dit = d->table[i]; dit != NULL; dit = dit->next) 
            _dictAddRaw(_d, dit);
        
    

    /* FIXME memory leak because the old dict can never be freed */
    free(d); // seg fault

    return _d;

上面的函数使用旧哈希表中的指针并将其存储在新创建的哈希表中。释放旧的dict d 时会发生分段错误。

我怎样才能释放旧的哈希表结构而不必再次为键/值对分配内存?

为了完整性,编辑:

dict *dictCreate(unsigned long size) 
    dict *d;

    d = malloc(sizeof(dict));
    d->size  = size;
    d->items = 0;
    d->table = calloc(size, sizeof(dictEntry*));

    return d;


void dictAdd(dict *d, void *key, void *value) 
    dictEntry *entry;

    entry = malloc(sizeof *entry);

    entry->key   = key;
    entry->value = value;
    entry->next  = '\0';

    if ((((float)d->items) / d->size) > 0.5) d = _dictRehash(d);

    _dictAddRaw(d, entry);


void _dictAddRaw(dict *d, dictEntry *entry) 
    int index = (hash(entry->key) & (d->size - 1));

    if (d->table[index]) 
        dictEntry *next, *prev;

        for (next = d->table[index]; next != NULL; next = next->next) 
            prev = next;

        

        prev->next = entry;
     else 
        d->table[index] = entry;
    
    d->items++;

【问题讨论】:

不清楚;你的问题是“为什么我会出现段错误?”? 在任何地方使用 _underscores 会使你的代码 _very _hard _to _read。 是的,实际上,这就是为什么我会出现 seg 错误!会尽快改变。 那么 dictCreate 是做什么的呢?发布代码;-) 【参考方案1】:

为了阐明 Graham 的观点,您需要注意在这个库中内存是如何被访问的。用户有一个指向其字典的指针。当您重新哈希时,您释放了该指针引用的内存。虽然你为他们分配了一个新的字典,但新的指针永远不会返回给他们,所以他们不知道不要使用旧的。当他们再次尝试访问他们的字典时,它指向的是已释放的内存。

一种可能性是不要完全丢弃旧字典,而只丢弃您在字典中分配的 dictEntry 表。这样,您的用户将永远不必更新他们的指针,但您可以重新调整表以适应更有效的访问。试试这样的:

void _dictRehash(dict *d) 
    printf("rehashing!\n");
    int i;
    dictEntry *dit;

    int old_size = d->size;
    dictEntry** old_table = d->table;
    int size = old_size * 2;

    d->table = calloc(size, sizeof(dictEntry*));
    d->size = size;
    d->items = 0;

    for (i = 0; i < old_size; i++) 
        for (dit = old_table[i]; dit != NULL; dit = dit->next) 
            _dictAddRaw(d, dit);
        
    

    free(old_table);
    return;



顺便说一句,我不确定你的哈希函数是做什么的,但在我看来,这条线

int index = (hash(entry->key) & (d->size - 1));

有点不正统。你得到一个哈希值并按位和表的大小进行操作,我想这在某种意义上是有效的,因为它可以保证在(我认为?)[0, max_size) 之内,我想你可能是指%模数。

【讨论】:

我认为这非常正确 - 但还有另一个问题:_dictAddRaw 添加了 整个链 条目,因为条目的 next 字段从未设置为 NULL (并且要小心它在哪里被 NULL 化,因为你需要它在 for 循环中。所以重建的表将是一团相互链接的链......此外,&amp; (size-1) 应该工作假设表大小始终是 2 的幂。但是模数要好得多!【参考方案2】:
    调试此问题的最佳方法是针对 valgrind 运行您的代码。

但是给你一些观点:

    当您 free(d) 时,您期望在您的 struct dict 上调用更多的 destructor,这将在内部释放分配给指向 dictEntry 的指针的内存

    为什么要删除整个has表才能展开呢?你有一个next 指针,为什么不直接向它附加新的哈希条目呢?

解决方案不是释放d,而是通过分配更多struct dictEntry 并将它们分配给适当的next 来扩展d

当收缩d 时,您将不得不遍历next 以到达末尾,然后开始为struct dictEntrys 释放d 内部的内存。

【讨论】:

谢谢,你的第一点就是解决方案。扩展列表不是想要的行为。 如果能解决您的问题,投赞成票并接受答案会很好:D 还没有完全解决。如果我不能释放旧的字典,那就是内存泄漏,对吧?有什么解决方案可以避免这种情况?【参考方案3】:

dictCreate 究竟做了什么?

我认为您对(固定大小的)dict 对象和指向dictEntries 中的dictEntries 的指针数组(可能是可变大小的)dict.table 感到困惑。

也许你可以只 realloc()dict.table 指向的内存,而不是创建一个新的“dict”对象并释放旧的对象(顺便说一句,这并没有释放 dictentries 表!)

【讨论】:

我避免使用 realloc 因为它应该很慢。但是,我知道条目没有被释放 - 指针只是在新的哈希表中再次使用。 这就是所谓的“过早优化”。无论如何,速度是您目前最不担心的问题。【参考方案4】:

您正在释放一个传递给您的函数的指针。仅当您知道调用您的函数的人并未仍在尝试使用 d 的旧值时,这才是安全的。检查所有调用_dictRehash() 的代码并确保没有任何东西挂在旧指针上。

【讨论】:

Hmmn,我调用dictAdd(d, key, value),它检查hashtable是否已满,然后调用_dictRehash(),会不会是这个问题? 是的,除非dictAdd() 将新的字典指针返回给它的调用者。

以上是关于释放已使用数据的内存会导致分段错误的主要内容,如果未能解决你的问题,请参考以下文章

由于 C 中的内存不足导致的分段错误

释放时填充结构数据会导致段错误

为啥我的字符串分配会导致分段错误?

尝试锁定共享内存互斥体时出现分段错误

为啥重新声明 std::cout 会导致分段错误?

为什么这个取消引用别名内存区域的C程序会导致分段错误?