序言
我这样做的方式是使用函数指针和一个“应用”函数,该函数给出一个列表、一个函数指针和一些(通用)数据,这些数据与指向数据的指针一起传递给函数指针一个列表节点。
apply 函数解决了如何打印列表内容的问题(尽管下面显示的代码有两个 apply 函数,用于以列表的正序和逆序应用数据)。
您询问是否能够提供值而不是必须将指针传递给listPushBack() 函数。除了为每种类型提供覆盖函数之外,没有简单的方法可以做到这一点。因此,您可以:
static inline void listPushBack_int(list *lp, int i)
{
listPushBack(lp, &i);
}
对于任何其他类型也是如此。
问题代码中的错误
请注意,问题中的代码在内存分配中存在错误 - 在listCreate() 中,您有:
list* l = malloc(sizeof(l));
你需要:
list *l = malloc(sizeof(*l));
genlist.h
这是标题。注意struct list 类型,又名list,是不透明的;使用标头的代码对结构类型的内部一无所知,所有访问都是通过代码中定义的函数进行的。我将listPushBack() 重命名为listPushTail() 并添加了listPushHead()。我添加了listApplyForward() 和listApplyReverse() 和listDestroy()。我删除了listPrint(),因为它不是通用的,不能轻易地被通用化。
#ifndef GENLIST_H_INCLUDED
#define GENLIST_H_INCLUDED
#include <stddef.h> /* size_t */
typedef struct list list;
typedef void (*Apply)(const void *vp, void *thunk);
extern list *listCreate(size_t typeSize);
extern void listDestroy(list *lp);
extern void listPushHead(list *lp, void *val);
extern void listPushTail(list *lp, void *val);
extern void listApplyForward(const list *lp, Apply apply, void *thunk);
extern void listApplyReverse(const list *lp, Apply apply, void *thunk);
#endif /* GENLIST_H_INCLUDED */
这显然不是一个完整的列表界面——它是一组非常简单的函数。它没有代码来访问列表中的单个元素,或者检索列表中的值,或者从列表中删除值,或者......但是,可以根据需要设计和添加这些功能。
genlist.c
节点类型也完全隐藏,因此nodeCreate() 函数(从nCreate() 重命名)是静态的。可以说,也应该有一个 nodeDestroy() 函数——但它被包含在 listDestroy() 内的代码中。
#include "genlist.h"
#include <assert.h> /* DEBUG */
#ifdef DEBUG
#include <inttypes.h> /* DEBUG */
#include <stdio.h> /* DEBUG */
#endif /* DEBUG */
#include <stdlib.h>
#include <string.h>
typedef struct node node;
struct node
{
void *data;
node *next;
node *prev;
};
struct list
{
node *head;
node *tail;
size_t elementSize;
};
static node *nodeCreate(void *data, size_t typeSize)
{
node *np = malloc(sizeof(node));
assert(np != NULL);
np->data = malloc(typeSize);
assert(np->data != NULL);
memcpy(np->data, data, typeSize);
np->prev = np->next = NULL;
#ifdef DEBUG
printf("Node: %zu: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
typeSize, (uintptr_t)np, (uintptr_t)np->prev, (uintptr_t)np->next);
fflush(stdout);
#endif /* DEBUG */
return np;
}
list *listCreate(size_t typeSize)
{
list *lp = malloc(sizeof(*lp));
assert(lp != NULL);
lp->head = lp->tail = NULL;
lp->elementSize = typeSize;
#ifdef DEBUG
printf("List: %zu: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
lp->elementSize, (uintptr_t)lp, (uintptr_t)lp->head, (uintptr_t)lp->tail);
fflush(stdout);
#endif /* DEBUG */
return lp;
}
void listDestroy(list *lp)
{
assert(lp != NULL);
#ifdef DEBUG
printf("Destroy List: %zu: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
lp->elementSize, (uintptr_t)lp, (uintptr_t)lp->head, (uintptr_t)lp->tail);
fflush(stdout);
#endif /* DEBUG */
node *np = lp->head;
while (np != NULL)
{
#ifdef DEBUG
printf("Destroy Node: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
(uintptr_t)np, (uintptr_t)np->prev, (uintptr_t)np->next);
fflush(stdout);
#endif /* DEBUG */
node *next = np->next;
free(np->data);
free(np);
np = next;
}
free(lp);
}
void listPushTail(list *lp, void *val)
{
node *newNode = nodeCreate(val, lp->elementSize);
if (lp->head == NULL)
{
lp->head = newNode;
lp->tail = newNode;
}
else
{
lp->tail->next = newNode;
newNode->prev = lp->tail;
lp->tail = newNode;
}
}
void listPushHead(list *lp, void *val)
{
node *newNode = nodeCreate(val, lp->elementSize);
if (lp->head == NULL)
{
lp->head = newNode;
lp->tail = newNode;
}
else
{
lp->head->prev = newNode;
newNode->next = lp->head;
lp->head = newNode;
}
}
void listApplyForward(const list *lp, Apply apply, void *thunk)
{
node *ptr = lp->head;
while (ptr != NULL)
{
// apply(ptr->data, thunk);
(*apply)(ptr->data, thunk);
ptr = ptr->next;
}
}
void listApplyReverse(const list *lp, Apply apply, void *thunk)
{
node *ptr = lp->tail;
while (ptr != NULL)
{
// apply(ptr->data, thunk);
(*apply)(ptr->data, thunk);
ptr = ptr->prev;
}
}
“apply”函数以外的函数是双向链表的直接实现,空指针标记列表的末尾(其他设计也是可能的,例如使用循环列表)。
函数listApplyForward() 和listApplyReverse() 遍历列表,调用apply 参数指定的函数,并带有指向当前节点数据的指针和thunk——指向任意数据的指针被调用的函数知道如何处理。 ('thunk' 的字典定义没有帮助;Thunk 的维基百科定义更接近目标,但适用于函数(子例程)而不是数据。但是,(macOS 和)FreeBSD 的手册页 @987654322 @ 使用名称 thunk 与我使用的含义相同——传递给回调函数的通用数据指针。)
当通过函数指针调用函数时,我仍然更喜欢显式的预标准符号(*pointer)(arg1, …)。最近学习 C 的人可能更喜欢使用pointer(arg1, …)。当我学习 C 时,这种表示法不起作用。
该代码包含一些我在追踪分配错误问题时使用的调试代码。可以说,打印应该是stderr而不是stdout,但添加fflush(stdout)就足够了。
请注意,此代码假定您可以安全地按位复制数据而不会遇到所有权问题。这意味着它可以适用于不包含指针的结构,例如 - 但如果复制的数据包含指向动态分配内存的指针,或指向调用函数中的局部变量,甚至指向结构内位置的指针,则会严重失败.您可以设计更复杂的方案来复制数据。例如,您可以将“复制构造函数”函数指针和“析构函数”传递给列表创建函数,并将它们记录在列表结构中,并在将节点添加到列表或从列表中删除节点等时使用它们。 OTOH,它们通常是不必要的——按位复制通常适用于嵌入在列表中的结构——因此对于需要或多或少内存管理的数据类型,您最终可能会得到几个“通用列表”类型。
main.c
此代码使用genlist.[ch] 中的函数的公共接口。它创建了一个int 列表,并使用函数print_int() 打印数据,并使用另一个函数sum_int() 显示如何对列表中的元素求和。它还创建了double 的列表,并使用函数print_dbl() 打印数据,并计算value1 / value2 * value3 / value … 的值,主要是为了演示用于控制计算的非平凡用户定义结构类型。根据列表是向前还是向后遍历,这会产生不同的答案。
代码使用rand() 生成随机数。它刻意不使用srand(time(0))或等价物来改变顺序——对于调试工作来说,每次相同数字的稳定性更有价值。
#include "genlist.h"
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
static void print_int(const void *data, void *thunk)
{
FILE *fp = thunk;
fprintf(fp, " %d", *(int *)data);
}
static void print_dbl(const void *data, void *thunk)
{
FILE *fp = thunk;
fprintf(fp, " %7.3f", *(double *)data);
}
static void int_sum(const void *data, void *thunk)
{
int *sum = thunk;
*sum += *(int *)data;
}
typedef struct DblFun
{
double val;
int idx;
} DblFun;
static void dbl_fun(const void *data, void *thunk)
{
DblFun *dp = thunk;
double val = *(double *)data;
if (dp->idx == 0)
dp->val = val;
else if (dp->idx % 2 == 1)
dp->val /= val;
else
dp->val *= val;
dp->idx++;
}
int main(void)
{
list *l1 = listCreate(sizeof(int));
for (int i = 0; i < 10; i++)
{
//int j = 300 + i;
//listPushTail(l1, &j);
listPushTail(l1, &i);
printf("List forward: ");
listApplyForward(l1, print_int, stdout);
putchar('\n');
printf("List reverse: ");
listApplyReverse(l1, print_int, stdout);
putchar('\n');
}
int sum = 0;
listApplyForward(l1, int_sum, &sum);
printf("Sum = %d\n", sum);
listDestroy(l1);
list *l2 = listCreate(sizeof(double));
for (int i = 0; i < 10; i++)
{
double d = (rand() % 20000) / 100.0 - 100.0;
listPushHead(l2, &d);
printf("List forward: ");
listApplyForward(l2, print_dbl, stdout);
putchar('\n');
printf("List reverse: ");
listApplyReverse(l2, print_dbl, stdout);
putchar('\n');
}
DblFun d1 = { 0.0, 0 };
DblFun d2 = { 0.0, 0 };
listApplyForward(l2, dbl_fun, &d1);
listApplyReverse(l2, dbl_fun, &d2);
printf("Fwd: %13.6g, Rev: %13.6g\n", d1.val, d2.val);
listDestroy(l2);
return 0;
}
测试输出
对于双向链表,能够测试正向和反向链接链是很有价值的。并且在构建列表时打印它可以确保它在退化的情况下也有效。
List forward: 0
List reverse: 0
List forward: 0 1
List reverse: 1 0
List forward: 0 1 2
List reverse: 2 1 0
List forward: 0 1 2 3
List reverse: 3 2 1 0
List forward: 0 1 2 3 4
List reverse: 4 3 2 1 0
List forward: 0 1 2 3 4 5
List reverse: 5 4 3 2 1 0
List forward: 0 1 2 3 4 5 6
List reverse: 6 5 4 3 2 1 0
List forward: 0 1 2 3 4 5 6 7
List reverse: 7 6 5 4 3 2 1 0
List forward: 0 1 2 3 4 5 6 7 8
List reverse: 8 7 6 5 4 3 2 1 0
List forward: 0 1 2 3 4 5 6 7 8 9
List reverse: 9 8 7 6 5 4 3 2 1 0
Sum = 45
List forward: 68.070
List reverse: 68.070
List forward: 52.490 68.070
List reverse: 68.070 52.490
List forward: 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730
List forward: -63.420 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730 -63.420
List forward: -10.700 -63.420 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730 -63.420 -10.700
List forward: 12.720 -10.700 -63.420 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730 -63.420 -10.700 12.720
List forward: -24.560 12.720 -10.700 -63.420 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730 -63.420 -10.700 12.720 -24.560
List forward: 8.780 -24.560 12.720 -10.700 -63.420 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730 -63.420 -10.700 12.720 -24.560 8.780
List forward: 79.230 8.780 -24.560 12.720 -10.700 -63.420 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730 -63.420 -10.700 12.720 -24.560 8.780 79.230
List forward: 77.090 79.230 8.780 -24.560 12.720 -10.700 -63.420 0.730 52.490 68.070
List reverse: 68.070 52.490 0.730 -63.420 -10.700 12.720 -24.560 8.780 79.230 77.090
Fwd: -27.7014, Rev: -0.0360992
此代码可在 GitHub 上的 SOQ(堆栈溢出问题)存储库中以文件 genlist.h、genlist.c 和 main.c 的形式在 src/so-6529-2945 子目录中找到。