关于在C中使用void* 作为动态数据类型的模块化代码的几个问题

bqf10yzr  于 2023-03-17  发布在  其他
关注(0)|答案(5)|浏览(121)

几天前我发布了this question,每个人都建议我使用void*,我照做了。我想他们中的一些人也指出了一些我需要注意的事情,但我不确定具体是什么。然而,我在这方面遇到了一些问题...
我不打算把我所有的代码都贴在那里,因为它很大,相反,我会贴一些我认为重要的东西,希望这些东西足以让你帮我解决问题。
我的哈希表结构是这样的:

typedef void * HashKey;
typedef void * HashValue;

typedef struct sHashItem {
    HashKey key;
    HashValue value;

    char status;
} HashItem;

typedef struct sHashTable {
    HashItem *items;

    int count;
    float load;
    int size;

    Bool (*compare)(HashKey, HashKey);
    unsigned (*hash)(void *);
} HashTable;

插入函数的签名如下所示:

Bool hashInsert(HashTable * const table, HashKey key, HashValue value);

在函数的某个地方,当我在哈希表中找到一个空闲的桶时,我会这样做:

table->items[index].key = key;
table->items[index].value = value;
table->items[index].status = USED;
table->load = ++table->count / (float)table->size;

所有这些都带来了一些问题:

**1)**正如你在上面看到的,我只是简单地将空闲桶的每一个键/值对设置为与键/值hashInsert函数参数传递的指针相同的指针,这就带来了一个你可能已经注意到的问题......例如,做这样的事情:

char str[50];
scanf("%s%*c", str);
hashInsert(t1, (HashKey)str, (HashValue)5);
scanf("%s%*c", str);
hashInsert(t1, (HashKey)str, (HashValue)3);

如果输入是“KeyA”,然后是“KeyB,”两者都将“KeyB”作为桶键,同样的道理也适用于值,而不仅仅是键,因为它们基本上是相同的类型,因为我想让我的代码完全模块化,适用于任何数据类型。
我该怎么解决这个问题?
我的第一个想法是使用strdup(str)并将其传递给hashInsert函数,这样就可以解决问题,因为这是在主代码中处理的,所以我可以很容易地将malloc()用于任何其他需要作为值传递的数据类型(键可能总是字符串或int)。
但是这个解决方案带来了另一个问题...

**2)**我应该如何释放这个分配的内存?当然,它是由“主程序员”而不是“哈希表模块程序员”分配的,所以,“主程序员”应该在主代码中释放它,对吗?然而,在我看来,这不太像模块化代码。

我的代码也有一个hashDestroy函数,用来释放所有分配的内存。但是我怎样才能使用这个函数来释放所有的内存呢?我不能只是迭代每个键/值,然后在它们上面使用free(),因为可能它们中的一些一开始就不是任何程序员使用的malloc'd,所以我不需要释放它们。
我怎样才能知道我的hashDestroy必须释放哪些,不应该释放哪些?

**3)**最后,我想我也可以把这个问题放在一起......在第一点中,我的建议是使用strdup()malloc来“修复”这个特定的问题(同时引入另一个问题),但在我看来,这也不是很模块化。这个内存分配应该在哈希表模块代码中完成,而不是由“主程序员”在主代码中完成。

你建议我怎么解决这个问题呢?我的意思是,数据类型可以是任何类型,虽然使用strdup()很有帮助,但它只适用于字符串。如果我需要为一些特定的结构或只是一个int分配内存呢?
很抱歉发这么大的帖子,但是我认为这些问题都是相关的,我需要一些帮助来解决它们,因为我的C知识不是那么极端。我最近才了解void*,所以...

oyjwcjzk

oyjwcjzk1#

Wow:这需要一些完整的答案,然而,你需要知道的关键之一是你正在处理的对象的大小使用void指针是可以的,但是你需要知道你正在接收的对象的地址有多大。
[...]每个人都建议我使用void*,我照做了。[...]
我的哈希表结构是这样的:

typedef void * HashKey;
typedef void * HashValue;

typedef struct sHashItem {
    HashKey key;
    HashValue value;
    char status;
} HashItem;

typedef struct sHashTable {
    HashItem *items;
    int count;
    float load;
    int size;

    Bool (*compare)(HashKey, HashKey);
    unsigned (*hash)(void *);
} HashTable;

您可能需要size_t key_sz;HashItem中的size_t val_sz;成员,您的散列函数指针需要知道要散列的键有多大。
关于HashKey应该是什么,我有两个想法。这部分取决于你如何使用它。看起来你想要:

  • 给定我选择的这个键值,
  • 存储/返回与之关联的数据。

在这种情况下,您可能还需要将散列数存储在HashItem中的某个位置;这是散列函数返回的值--显然是一个无符号整数。我不确定compare函数(函数指针)的签名应该是什么;我怀疑它应该接受一对HashKey和size值,或者可能接受一对HashItem指针。
插入函数的签名如下所示:

Bool hashInsert(HashTable * const table, HashKey key, HashValue value);

在函数的某个地方,当我在哈希表中找到一个空闲的桶时,我会这样做:

table->items[index].key = key;
table->items[index].value = value;
table->items[index].status = USED;
table->load = ++table->count / (float)table->size;

所有这些都带来了一些问题:
1)正如你在上面看到的,我只是简单地将空闲存储桶的每个键/值对设置为与键/值hashInsert函数参数传递的指针相同的指针,这就带来了一个你可能已经注意到的问题...例如,这样做:

char str[50];
scanf("%s%*c", str);
hashInsert(t1, (HashKey)str, (HashValue)5);
scanf("%s%*c", str);
hashInsert(t1, (HashKey)str, (HashValue)3);

使用void *的关键是传递地址。在C中,类型转换应该是不必要的。您还需要传递内容的大小。因此:

Bool hashInsert(HashTable * const table, HashKey key, size_t key_sz,
                HashValue value, size_t val_sz);

char str[50];
scanf("%s%*c", str);
int value = 5;
hashInsert(t1, str, strlen(str)+1, &value, sizeof(value));

在内部,您将复制数据-不使用'strdup()',因为您不知道其中是否有内部NUL '\0'字节。
如果输入是“KeyA”,然后是“KeyB,”两者都将“KeyB”作为桶键,同样的道理也适用于值,而不仅仅是键,因为它们基本上是相同的类型,因为我想让我的代码完全模块化,适用于任何数据类型。
我该怎么解决这个问题?
你必须定义谁拥有什么,以及容器是否(以及如何)复制数据。在C++中,容器复制它们所存储的任何东西。
我的第一个想法是使用strdup(str)并将其传递给hashInsert函数,这样就可以解决这个问题,因为这是在主代码中处理的,所以我也可以很容易地使用malloc()来处理任何其他需要作为值传递的数据类型(键可能总是字符串或整数)。
你不能使用'strdup()',因为一般来说,值和键都不是字符串。如果它们总是字符串,为什么要用'void *'而不是'char *'呢?
您可以决定复制值和键-只要您知道大小。
但是这个解决方案带来了另一个问题...
2)我应该如何释放这个内存?当然,它是由“主程序员”而不是“哈希表模块程序员”分配的,所以,“主程序员”应该在主代码中释放它,对吗?然而,在我看来,这并不像是模块化代码。
我的代码也有一个hashDestroy函数,用来释放所有分配的内存。但是我怎样才能使用这个函数来释放所有的内存呢?我不能只是遍历每个键/值,然后对它们使用free(),因为可能它们中的一些一开始并没有被任何程序员malloc,所以我不需要释放它们。
我怎样才能知道我的hashDestroy必须释放哪些,不应该释放哪些?
你做不到。你必须定义一个策略,并且只有当该策略允许你进行销毁时,你才应该这么做。如果你复制所有内容,你会过得很轻松。如果你什么都不复制,你会过得很轻松(可以说更轻松),但是你的消费者会过得很艰难,因为他们需要一个结构来跟踪他们需要发布的内容--也许是一个哈希列表......
3)最后,我想我也可以把这个问题放到一起......在第一点中,我的建议是使用strdup()或malloc来“修复”这个特定的问题(同时引入另一个问题),但在我看来,这也不是很模块化。这种内存分配应该在哈希表模块代码中完成,而不是由“主程序员”在主代码中完成。
是的......这基本上是我的建议。
你建议我怎么解决这个问题呢?我的意思是,数据类型可以是任何类型,虽然使用strdup()很有帮助,但它只适用于字符串。如果我需要为一些特定的结构或只是一个int分配内存呢?
注意复制只做浅拷贝,如果你要复制的结构包含指针,那么复制代码只会复制指针,而不会复制指向的数据。

所以,一个通用的解决方案需要某种复制函数。你可能需要用户给你一个“release”函数来释放一个项目中的内存。你可能需要让用户给你提供已经完全分配的数据。你需要考虑谁拥有搜索函数返回的内容--它仍然在哈希表中还是已经被删除了。仔细看看C++ STL系统--总的来说,它做得很好,并且根据它的要求来建模你的需求可能是有意义的。2但是记住,C++有构造函数和析构函数来帮助它。

wmomyfyw

wmomyfyw2#

我会malloc所有的数据,并允许客户端在哈希表初始化时注册一个item_free()函数,这样就由“主程序员”来处理了。

23c0lvtd

23c0lvtd3#

嗯,从我在您的示例中看到的情况来看,问题不在于哈希表冲突(虽然你似乎也有这个问题),它是如何管理存储在表中的项的内存。我认为做这类事情的标准方法是强迫数据结构的用户(哈希表)来完成为所有要放入表中的内容分配空间的工作,哈希表应该只需要关心指针。假设您确实进行了分配,然后在数据结构中复制:当从hastable中移除项时,用户如何知道如何释放内存?

c86crjj0

c86crjj04#

有两种处理哈希表中冲突的一般解决方案:
1.使用下一个空闲存储桶。
1.存储桶存储链接列表,因此可以在同一存储桶中存储多个项。
无论使用哪种方法,都不会出现什么时候释放什么的问题,因为所有类型的数据都是由哈希表或哈希表的客户端分配的,如果您仍然好奇,解决这个难题的简短答案是使用smart pointers

06odsfpq

06odsfpq5#

要实现一个哈希表,我们需要一组桶。由于多个元素可以哈希到同一个桶,每个桶需要一个链表。
是否

HashItem *items;

是否执行上述第二项要求?
从你的解释来看,不清楚是否有。
有关一个很好的示例,请参见K&R第6.6节。link,其中name = HashKey且defn = HashValue.

相关问题