为什么在trie中释放字符串会导致malloc错误?

时间:2018-04-21 20:15:11

标签: c malloc trie

我需要帮助理解我从malloc获得的调试消息。我编写了一个函数,通过在trie上递归遍历该字符串并释放未被另一个字符串使用的所有内容,从trie中删除指定的字符串。这包括转到最后一个节点并释放它,然后返回堆栈并检查每个级别以查看该级别是否也被其他字符串未使用,如果是,则释放它们。一旦它到达第一个被其他东西使用的东西,它就会停止。

当我只删除一个字符串时,该函数似乎工作正常,但是当我删除第二个字符串时,我开始遇到问题。这是我的程序到目前为止的整个代码(请注意,有些行是用于测试/调试目的而不是程序的组成部分):

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct node {
    char chr;
    bool end;
    struct node *children[128];
};

void add( struct node *, char * );
void del( struct node *, char * );
bool isMember( struct node *, char * );

bool recursiveDel( struct node *, char * );
// del is really just a dummy function that calls recursiveDel.

int main( int argc, char **argv ){
    struct node *trie = (struct node *) malloc( sizeof( struct node ) );
    for( int i = 1; i < argc; i++ ){
        add( trie, argv[i] );
    }
    del( trie, argv[1] );
    del( trie, argv[2] );
    for( int i = 1; i < argc; i++ ){
        printf( "%d\n", isMember( trie, argv[i] ) );
    }
    return 0;
}

void add( struct node *trie, char *str ){
    int i = 0;
    while( str[i] ){
        // Check/goto next node
        // If NULL, create next node
        if( trie->children[str[i]] == NULL )
            trie->children[str[i]] = (struct node *) malloc( sizeof( struct node ) );
        trie = trie->children[str[i++]];
    }
    trie->end = true;
}

void del( struct node *trie, char *str ){
    if( isMember( trie, str ) ){
        recursiveDel( trie, str );
    }
}

bool isMember( struct node *trie, char *str ){
    int i = 0;
    struct node *cur = trie;
    while( str[i] ){
        if( trie->children[str[i]] == NULL ) return false;
        else trie = trie->children[str[i++]];
    }
    return trie->end;
}

// Features of this function:
// When it gets to the leaf, it deletes that node and then starts going back up the call stack
// Each call passes a Boolean value back up the call stack.
// This boolean value indicates whether or not the node was deleted.
// If the value returned from the lower node is true, then that means check the next node up to see if it should be deleted.
// If false do nothing, because there are other strings using this node.
bool recursiveDel( struct node *trie, char *str ){
    printf( "%p, %d, %s\n", trie, trie->end, str );
    if( trie->end ){
        free( trie );
        return true;
    }
    bool deleted = recursiveDel( trie->children[str[0]], str+1 );
    if( deleted ){
        int used = 0;
        // Loop checks to see if the node
        // is used by any other strings.
        for( int i = 0; i < 128; i++ ){
            if( trie->children[i] ){
                used++;
                break;
            }
        }
        if( used <= 1 ){
            free( trie );
            return true;
        }
    }
    return false;
}

问题似乎发生在这个块中,我尝试释放字符串的终止节点:

    if( trie->end ){
        free( trie );
        return true;
    }

我收到一条消息,说该节点无法释放,因为它不存在......

bash-3.2$ ./trie hello world
0x7fd370802000, 0, hello
0x7fd370800600, 0, ello
0x7fd370802600, 0, llo
0x7fd370802c00, 0, lo
0x7fd370803200, 0, o
0x7fd370803800, 1,
0x7fd370802000, 0, world
0x7fd370803e00, 0, orld
0x7fd370804400, 0, rld
0x7fd370804a00, 0, ld
0x7fd370805000, 0, d
0x7fd370805600, 1,
trie(43216,0x7fff7818e000) malloc: *** error for object 0x7fd370802000: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6
bash-3.2$

似乎是这样的情况,当我尝试删除第二个字符串时,它会按预期进入最后一个节点,但是然后不再返回堆栈,它会继续运行并尝试释放下一个节点,显然它无法做到,因为这是一个叶子节点。

似乎它可能是未定义的行为,但同时,程序以一种非常可预测的方式失败 - 第一次删除字符串总是成功,而第二次删除字符串总是不成功。我无法做出正面或反面。

此外,malloc失败时给出的地址似乎有些偏差。最后几个地址的差异均为0x600,但此地址与0xa00的最后一个地址不同。我知道堆内存分配是不可预测的,但我只是认为我指出了这一点。

比这更奇怪的是malloc给出的地址与上一个打印地址不同,尽管失败的free操作紧跟在最后printf之后。这几乎似乎表明编译器正在printf行和if( trie->end ) free( trie )部分之间插入指针前进操作。常识表明这很荒谬,但我不知道任何其他解释。

2 个答案:

答案 0 :(得分:5)

代码的相关部分:

int main( int argc, char **argv ){
    struct node *trie = (struct node *) malloc( sizeof( struct node ) );
    for( int i = 1; i < argc; i++ ){
        add( trie, argv[i] );
    }
    …
}

void add( struct node *trie, char *str ){
    int i = 0;
    while( str[i] ){
        if( trie->children[str[i]] == NULL )
//          ^^^^^^^^^^^^^^^^^^^^^^

您正在分配struct node,然后在不初始化的情况下访问其.children[...]指针。未定义的行为。

分配后,您需要初始化node

答案 1 :(得分:4)

此处add功能不正确:

trie->children[str[i]] = (struct node *) malloc( sizeof( struct node ) );

使用不初始化它返回的内存块的malloc()main()中的同样问题。实际上,为trie及其节点提供2种不同的结构类型是有意义的。

您应该使用calloc()memset()将内容初始化为所有位0,这对于大多数当前架构初始化children NULL指针来说已经足够了

另请注意,如果任何字符超出范围0 .. 127,则您有未定义的行为。您应该为children数组提供256个条目并将char强制转换为unsigned char,然后再使用它作为索引。

另一个问题:在recursiveDel中,您应该清除trie->end,并且只有在其所有children指针都为NULL时才释放该节点。中间节点的问题相同:在释放节点之前,您必须检查trie->end是否为假。

仔细观察recursiveDel函数,它会以多种方式被破坏。这是一个更正版本:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct node {
    char chr;
    bool end;
    struct node *children[256];
};

void add(struct node *trie, const char *str);
void del(struct node *trie, const char *str);
bool isMember(struct node *trie, const char *str);

bool recursiveDel(struct node *trie, const char *str);
// del is really just a dummy function that calls recursiveDel.

int main(int argc, char **argv) {
    struct node *trie = calloc(sizeof(struct node), 1);
    for(int i = 1; i < argc; i++) {
        add(trie, argv[i]);
    }
    if (argc > 1)
        del(trie, argv[1]);
    if (argc > 2)
        del(trie, argv[2]);
    for (int i = 1; i < argc; i++) {
        printf("%d\n", isMember(trie, argv[i]));
    }
    return 0;
}

void add(struct node *trie, const char *str) {
    for (int i = 0; str[i]; i++) {
        // Check/goto next node
        // If NULL, create next node
        if (trie->children[(unsigned char)str[i]] == NULL)
            trie->children[(unsigned char)str[i]] = calloc(sizeof(struct node), 1);
        trie = trie->children[(unsigned char)str[i]];
    }
    trie->end = true;
}

void del(struct node *trie, const char *str) {
    if (isMember(trie, str)) {
        recursiveDel(trie, str);
    }
}

bool isMember(struct node *trie, const char *str) {
    for (int i = 0; str[i]; i++) {
        if (trie->children[(unsigned char)str[i]] == NULL)
            return false;
        else
            trie = trie->children[(unsigned char)str[i]];
    }
    return trie->end;
}

// Features of this function:
// When it gets to the leaf, it resets the end flag
// and check if the node can be removed and returns true if so.
// Each call passes a Boolean value back up the call stack.
// This boolean value indicates whether or not the node can be deleted.
// If so, the caller frees it and clears the pointer
// If false do nothing, because there are other strings using this node.
bool recursiveDel(struct node *trie, const char *str) {
    //printf("%p, %d, %s\n", (void *)trie, trie->end, str);
    if (*str) {
        if (!recursiveDel(trie->children[(unsigned char)str[0]], str + 1))
            return false;
        free(trie->children[(unsigned char)str[0]]);
        trie->children[(unsigned char)str[0]] = NULL;
    } else {
        trie->end = false;
    }
    if (trie->end)
        return false;

    for (int i = 0; i < 256; i++) {
        if (trie->children[i])
            return false;
    }
    return true;
}