在 C 中存储和使用类型信息

Storing and using type information in C

我来自 Java,我正在尝试在 C 中实现双向链表作为练习。我想做一些类似于 Java 泛型的事情,我将指针类型传递给列表初始化,这个指针类型将用于转换列表 void 指针,但我不确定这是否可能?

我正在寻找的是可以存储在列表结构中并用于将 *data 从节点转换为正确类型的东西。我正在考虑使用双指针,但后来我需要将其声明为空指针,我会遇到同样的问题。

typedef struct node {
    void *data;
    struct node *next;
    struct node *previous;
} node;

typedef struct list {
    node *head;
    node *tail;
   //??? is there any way to store the data type of *data? 
} list;

无法在 C 中做您想做的事。无法在变量中存储类型,并且 C 没有像 C++ 这样的模板系统,允许您在预处理器中伪造它。

您可以定义自己的类似模板的宏,这些宏可以快速定义您需要的任何类型的 nodelist 结构,但我认为这种 hackery 通常不受欢迎,除非您真的 需要一大堆链表,只是它们存储的类型不同。

与 Java 或 C++ 不同,C 不提供任何类型安全。要简洁地回答您的问题,请以这种方式重新排列您的节点类型:

struct node {
   node* prev;  /* put these at front */
   node* next;
   /* no data here */
};

然后您可以单独声明携带任何数据的节点

struct data_node {.
    data_node *prev;            // keep these two data members at the front 
    data_node *next;            // and in the same order as in struct list.

    // you can add more data members here.
};
/* OR... */
enter code here
struct data_node2 {
  node node_data;    /* WANING: this may look a bit safer, but is _only_ if placed at the front.
  /*  more data ... */
};

然后您可以创建一个对 node 的无数据列表进行操作的库。

void list_add(list* l, node* n);
void list_remove(list* l, node* n);
/* etc... */

然后通过转换,使用此 'generic lists' api 对您的列表进行操作

您可以在列表声明中包含某种类型信息,这是值得的,因为 C 不提供有意义的类型保护。

struct data_list
{
  data_node* head;    /* this makes intent clear. */
  data_node* tail;
};

struct data2_list
{
  data_node2* head;
  data_node2* tail;
};

/* ... */

data_node* my_data_node = malloc(sizeof(data_node));
data_node2* my_data_node2 = malloc(sizeof(data_node2));

/* ... */

list_add((list*)&my_list, (node*)my_data_node); 
list_add((list*)&my_list2, &(my_data_node2->node_data)); 

/* warning above is because one could write this */
list_add((list*)&my_list2, (node*)my_data_node2); 


/* etc... */

这两种技术生成相同的目标代码,所以您选择哪一种完全取决于您。

顺便说一句,如果你的编译器允许,请避免使用 typedef struct 表示法,现在大多数编译器都允许。恕我直言,它增加了长 运行 的可读性。你可以肯定有些人不会,有些人会同意我在这个问题上的看法。

C 没有任何运行时类型信息,也没有类型 "Type"。一旦代码被编译,类型就没有意义了。因此,语言

没有提供解决方案

您希望在运行时提供一个类型的一个常见原因是您有一些代码可能会看到容器的不同实例,并且必须对存储在容器中的不同类型执行不同的操作。您可以使用 enum 轻松解决这种情况,例如

enum ElementType
{
    ET_INT; // int
    ET_DOUBLE; // double
    ET_CAR; // struct Car
    // ...
 };

并在此处枚举任何应该放入您的容器的类型。另一个原因是如果您的容器应该拥有存储在其中的对象的所有权,因此必须知道如何销毁它们(有时如何克隆它们)。对于这种情况,我推荐使用函数指针:

typedef void (*ElementDeleter)(void *element);
typedef void *(*ElementCloner)(const void *element);

然后扩展您的结构以包含这些:

typedef struct list {
    node *head;
    node *tail;
    ElementDeleter deleter;
    ElementCloner cloner;
} list;

确保将它们设置为实际删除相应的功能。克隆要存储在容器中的类型的元素,然后在需要的地方使用它们,例如在删除函数中,你可以做类似

的事情
myList->deleter(myNode->data);
// delete the contained element without knowing its type

通常,使用如下所示的特定函数。

void    List_Put_int(list *L, int *i);
void    List_Put_double(list *L, double *d);
int *   List_Get_int(list *L);
double *List_Get_double(list *L);

一种对学习者来说不太容易的方法使用 _Generic。 C11 提供 _Generic 允许在编译时根据类型根据需要控制代码。

下面提供了 save/fetch 到 3 种指针的基本代码。宏需要为每个新类型扩展。 _Generic 不允许列出可能与 unsigned *size_t * 相同的 2 种类型。所以有局限性。

type_id(X) 宏为 3 种类型创建枚举,可用于检查 运行 时间问题,如下面的 LIST_POP(L, &d); 所示。

typedef struct node {
  void *data;
  int type;
} node;

typedef struct list {
  node *head;
  node *tail;
} list;

node node_var;
void List_Push(list *l, void *p, int type) {
  // tbd code - simplistic use of global for illustration only
  node_var.data = p;
  node_var.type = type;
}

void *List_Pop(list *l, int type) {
  // tbd code
  assert(node_var.type == type);
  return node_var.data;
}

#define cast(X,ptr) _Generic((X), \
  double *: (double *) (ptr), \
  unsigned *: (unsigned *) (ptr), \
  int *: (int *) (ptr) \
  )

#define type_id(X) _Generic((X), \
  double *: 1, \
  unsigned *: 2, \
  int *: 3 \
  )

#define LIST_PUSH(L, data)  { List_Push((L),(data), type_id(data)); }
#define LIST_POP(L, dataptr) (*(dataptr)=cast(*dataptr, List_Pop((L), type_id(*dataptr))) )

使用示例和输出

int main() {
  list *L = 0; // tbd initialization
  int i = 42;
  printf("%p %d\n", (void*) &i, i);
  LIST_PUSH(L, &i);
  int *j;
  LIST_POP(L, &j);
  printf("%p %d\n", (void*) j, *j);
  double *d;
  LIST_POP(L, &d);
}

42
42
assertion error

创建枚举类型,将根据此枚举存储数据类型和分配内存。这可以在 switch/case 构造中完成。