【问题标题】:Hashmap implementation problem in C with void pointer as valueC中以void指针为值的Hashmap实现问题
【发布时间】:2018-11-08 23:02:11
【问题描述】:

您好,我正在尝试在常规 C 中实现一个非常简单的哈希映射,其中一个字符串作为键,一个 void 指针作为值,因为我希望将映射用于多种数据类型。

到目前为止我有这个

struct node{
    void * value;
    char * key;
};

unsigned long strhash(char *string)
{   
    unsigned long hash = 5381;
    int c;

    while ((c = *string++))
    {   
        hash = ((hash << 5) + hash) + c;
    }   
    return hash;
}


map_t *map_create(int maxSize){

    map_t *map = malloc(sizeof(map_t));
    map->curSize = 0;
    map->maxSize = maxSize;
    map->nodes = calloc(map->maxSize, sizeof(node_t *));

    return map;
}


node_t *node_create(char *key, void *value){

    node_t *node = malloc(sizeof(node_t));
    node->key = key;
    node->value = value;
    return node;
}

void map_insert(map_t *map, char *key, void *value){

    node_t *node = node_create(key, value);

    int idx = strhash(key) % map->maxSize;
    if(map->nodes[idx] == NULL){
        map->nodes[idx] = node;
    }else{
        while(map->nodes[idx] != NULL){
            idx++%map->maxSize;
        }
        map->nodes[idx] = node;
    }   
    return;
}

void map_print(map_t *map){

    for(int i = 0; i < map->maxSize; i++){
        if(map->nodes[i] != NULL){
            printf("index: %d\t value: %d\n",i, *(int*)map->nodes[i]->value);
        }
    }
    return;
}

void map_destroy(map_t *map){
     for(int i = 0; i < map->maxSize; i++){
        if(map->nodes[i] != NULL){
            free(map->nodes[i]);
        }
    }
    free(map->nodes);
    free(map);
    return;
}



int main(){

    map_t *map = map_create(32);
    for(int i = 0; i < 30; i++){
        map_insert(map, (char*)&i, &i);
    }
    map_print(map);
    map_destroy(map);
    return 0;
}

问题是当地图被打印时,输出并不像我所期望的那样,所有检索到的是所有索引上的值“30”,这是插入到地图中的最后一个数字。如果我将值更改为 int 类型,则映射按预期工作,那么在指针方面我必须缺少一些关键的东西。

我在 C 方面不是最优秀的,所以任何可以阐明这一点的观点都将不胜感激。

【问题讨论】:

  • 您每次都在存储i 变量的地址。该地址不会改变,但每次分配给它时都会改变内容。
  • ^ 这解释了为什么更改为 int value 有效,因为 i 的值被复制了
  • 更糟的是,一旦到达map_printi 就不再在作用域内,因此这些条目中的每一个现在都承载了一个悬空指针。取消引用它会调用 未定义的行为
  • 仅供参考,如果您正在寻找有用的东西,您可以在这里找到一个不错的免费方法:github.com/rxi/map

标签: c pointers hashmap void-pointers


【解决方案1】:

问题是每次调用map_insert() 时都使用相同的指针。它只存储指针,不复制数据。每次通过循环时,您都会更改该内存的内容,因此所有哈希映射元素都指向相同的值。

有两种方法可以解决它。一种方法是在调用 map_insert() 之前始终制作数据的动态分配副本:

for (int i = 0; i < 30; i++) {
    int *i_copy = malloc(sizeof *i_copy);
    *i_copy = i;
    map_insert(map, (char *)i_copy, (char *)i_copy);
}

另一个选项是将值的大小添加到map_insert()node_create() 参数。然后node_create调用malloc()memcpy()将值复制到动态内存中。

顺便说一句,还有另一个问题。键应该是一个以空字符结尾的字符串(strhash() 取决于此),但您使用的是&amp;i,它是一个指向整数的指针。将指向整数的指针转换为 char* 不会返回字符串,它只会返回指向具有不同数据类型的相同位置的指针。我没有在上面解决这个问题。

【讨论】:

    【解决方案2】:

    OP 存储对相同值的引用,因此当然所有查找都会产生相同的值(这甚至不是字符串,而是变量 i 的值的存储表示恰好是什么)。

    我更喜欢链接哈希映射条目,并在条目中保留哈希的副本:

    struct entry {
        struct entry *next;
        size_t        hash;
        void         *data;
        size_t        data_size;
        int           data_type;
        unsigned char name[];
    };
    
    typedef struct {
        size_t         size;
        size_t         used;  /* Number of entries, total */
        struct entry **slot;  /* Array of entry pointers */
        size_t       (*hash)(const unsigned char *, size_t);
    } hashmap;
    
    int hashmap_new(hashmap *hmap, const size_t size,
                    size_t (*hash)(const unsigned char *, size_t))
    {
        if (!hmap)
            return -1; /* No hashmap specified */
    
        hmap->size = 0;
        hmap->used = 0;
        hmap->slot = NULL;
        hmap->hash = NULL;
    
        if (size < 1)
            return -1; /* Invalid size */
        if (!hash)
            return -1; /* No hash function specified. */
    
        hmap->slot = calloc(size, sizeof hmap->slot[0]);
        if (!hmap->slot)
            return -1; /* Not enough memory */
    
        hmap->size = size;
        hmap->hash = hash;
    
        return 0;
    }
    
    void hashmap_free(hashmap *hmap)
    {
        if (hmap) {
            size_t  i = hmap->size;
            while (i-->0) {
                struct entry *next = hmap->slot[i];
                struct entry *curr;
    
                while (next) {
                    curr = next;
                    next = next->next;
    
                    free(curr->data);
    
                    /* Poison the entry, to help detect use-after-free bugs. */
                    curr->next = NULL;
                    curr->data = NULL;
                    curr->hash = 0;
                    curr->data_size = 0;
                    curr->data_type = 0;
                    curr->name[0] = '\0';
    
                    free(curr);
                }
            }
        }
    
        free(hmap->slot);
        hmap->size = 0;
        hmap->used = 0;
        hmap->slot = NULL;
        hmap->hash = NULL;
    }
    

    要插入一个键值对,函数要么使用原样指定的数据,在这种情况下,调用者有责任确保每个键都有自己的唯一数据,以后不会被覆盖;或者我们复制用户数据。在上面的hashmap_free() 函数中,你会看到free(curr-&gt;data);;它假设我们动态分配内存,并在那里复制用户数据。所以:

    int hashmap_add(hashmap *hmap, const unsigned char *name,
                    const void *data, const size_t data_size,
                    const int data_type)
    {
        const size_t  namelen = (name) ? strlen(name) : 0;
        struct entry *curr;
        size_t        i;
    
        if (!hmap)
            return -1; /* No hashmap specified. */
    
        if (name_len < 1)
            return -1; /* NULL or empty name. */
    
        /* Allocate memory for the hashmap entry,
           including enough room for the name, and end of string '\0'. */
        curr = malloc(sizeof (struct entry) + namelen + 1;
        if (!curr)
            return -1; /* Out of memory. */
    
        /* Copy data, if any. */
        if (data_size > 0) {
            curr->data = malloc(data_size);
            if (!curr->data) {
                free(curr);
                return -1; /* Out of memory. */
            }
            memcpy(curr->data, data, data_size);
        } else {
            curr->data      = NULL;
            curr->data_size = 0;
        }
    
        curr->data_type = data_type;
    
        /* Calculate the hash of the name. */
        curr->hash = hmap->hash(name, namelen);
    
        /* Copy name, including the trailing '\0'. */
        memcpy(curr->name, name, namelen + 1);
    
        /* Slot to prepend to. */
        i = curr->hash % hmap->size;
    
        curr->next = hmap->slot[i];
        hmap->slot[i] = curr;
    
        /* An additional node added. */
        hmap->used++;
    
        return 0;
    }
    

    data_type 的含义完全取决于代码的用户。 可以根据哈希和数据类型进行查找:

    /* Returns 0 if found. */
    int hashmap_find(hashmap *hmap, const unsigned char *name,
                     const int data_type,
                     void **dataptr_to, size_t *size_to)
    {
        struct entry  *curr;
        size_t         hash;
    
        if (size_to)
            *size_to = 0;
        if (dataptr_to)
            *dataptr_to = NULL;
    
        if (!hmap)
            return -1; /* No hashmap specified. */
        if (!name || !*name)
            return -1; /* NULL or empty name. */
    
        hash = hmap->hash(name, strlen(name));
        curr = hmap->slot[hash % hmap->size];
    
        for (curr = hmap->slot[hash % hmap->size]; curr != NULL; curr = curr->next) {
            if (curr->data_type == data_type && curr->hash == hash &&
                !strcmp(curr->name, name)) {
                /* Data type an name matches. Save size if requested. */
                if (size_to)
                    *size_to = curr->data_size;
                if (dataptr_to)
                    *dataptr_to = curr->data;
                return 0; /* Found. */
            }
        }
    
        return -1; /* Not found. */
    }
    

    如果找到,上述查找返回 0,如果错误或未找到,则返回非零。 (这样,即使是零大小的 NULL 数据也可以存储在 hash map 中。)

    如果支持的数据类型数量很少,例如 32,则使用 unsigned int 为特定类型保留每个位(1U&lt;&lt;0 == 11U&lt;&lt;1 == 21U&lt;&lt;2 == 4 等),您可以使用掩码进行查找,只允许指定的类型。类似地,data_type 可以是一个掩码,描述该值可以解释为哪些类型(几乎总是只设置一个位)。

    此方案还允许通过分配一个新的slot 指针数组并将每个旧条目移动到新条目来动态调整哈希图的大小。密钥不需要重新散列,因为原始散列存储在每个条目中。为了查找效率,链(挂在每个插槽上)应该尽可能短。一个常见的“经验法则”是hashmap-&gt;size 应该在hashmap-&gt;used2 * hashmap-&gt;used 之间。

    【讨论】:

      【解决方案3】:

      当您调用map_insert(map, (char*)&amp;i, &amp;i); 时,插入hasmap 的值是指向i 变量的指针,即它在内存中的地址,而不是i 的值。 因此,当您在 for 循环中更改 i 值时,哈希图中的所有条目都会产生副作用,并且在循环结束时您只能看到分配的最后一个值。

      【讨论】:

      • 或多或少。 “更少”是更改 i 的值对地图条目根本没有影响i 的值不是其中的一部分。这实际上是 OP 应该带走的关键。
      • 是的,我说的很糟糕:我想说的是,因为所有条目都具有相同的 &amp;i 值,所以更改为 i 会导致延迟所有 hashmap 值返回最后设置的值到i
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-03
      • 2020-10-18
      • 2019-02-03
      • 2016-01-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多