在C语言中创建一个接受任何类型的动态数组

22

我正在尝试找到一种方法来创建一个结构体,可以持有任何数据类型(包括用户定义的数据类型)的动态数组。目前我想到的是:

#define Vector(DATATYPE) struct {   DATATYPE* data; size_t size; size_t used; }

typedef Vector(int) int_Vector;

int main(int argc, char* argv[]){
    int_Vector vec;
    return 0;
}

虽然这样可行,但我在想,这种做法好吗?我应该像这样做些什么,还是有更好的方法?另外,有没有一种方法可以实现这个而不需要 typedef Vector(int) int_vector 部分。基本上,是否有一种方法使我可以像 c++ 使用模板一样使用数组,它看起来像这样:

#define Vector(DATATYPE) struct {   DATATYPE* data; size_t size; size_t used; }

int main(int argc, char* argv[]){
    Vector(int) vec;
    return 0;
}

主要是为了避免使用太多的typedef,并将它们都归为一个名称下。


有各种方法可以做到这一点,坦白地说,它们都不是特别干净的。另一个技巧是将数据存储为灵活的数组成员,并使向量接口通过隐式转换的void指针传递和返回第一个数据元素的地址。还有一种方法是使用X宏来生成整个特定于类型的接口以实现类型安全。 - doynax
这不是一个答案,但FreeBSD的开发人员在sys/queue.h中大量依赖宏来获得您所需的行为。 - farsil
5个回答

17

不,C语言没有模板系统,因此无法使用。

你可以像你所做的那样使用宏来模仿效果(相当聪明的解决方案),但这当然有点非标准,并且需要你代码的用户学习宏及其限制。

通常情况下,C语言的代码不会尝试这样做,因为这样很麻烦。

最“通用”的典型向量是类似于glib的GArray,但它并不假装了解每个元素的类型。相反,这留给用户在访问时关心,数组仅将每个元素建模为n字节。

C11中有_Generic(),可能会有所帮助,但我对此并不是很有经验。


@zee gcc 已经支持 C11 很长时间了。 - 2501
2
@zee gcc具有有效、符合C11标准的实现。库threads.h是一种条件特性。 - 2501
@unwind 或许你可以加上一个关于 Linux 内核实现列表的参考。虽然这不完全是 OP 要做的事情,但我觉得它相当聪明,并且适用于任何结构体。 - JDurstberger
@2501 哦,原来如此,谢谢你的信息。但是这还涉及到vs编译器的问题。我不确定现在应该使用哪个。据我所知,从我阅读的一些论坛帖子中得知,使用c99通常更好,因为几乎每个编译器都支持它,但正如我们所看到的,我对标准和哪些编译器完全实现了什么并不熟悉。那么,现在有没有理由实际使用c99,或者我应该切换到c11? - zee
@zee 如果你在使用Windows系统,可以使用MinGW-w64和gcc/clang。 - 2501
显示剩余2条评论

6
第二个示例无法工作,因为两个变量虽然成员相同,但它们被定义为不同的类型。为什么会这样呢?这在我的现有答案中已经涵盖了。
然而,可以使用稍微不同的方法保持语法相同:
#include <stdlib.h>

#define vector(type)    struct vector_##type

struct vector_int
{
    int* array;
    size_t count;
} ;

int main(void)
{
    vector(int) one = { 0 };
    vector(int) two = { 0 };

    one = two;
    ( void )one ;

    return 0;
}

这个用法非常类似于 C++ 的 vector<int>,这里可以查看完整示例:

#include <stdlib.h>

#define vector_var(type)    struct vector_##type

struct vector_int
{
    int* array;
    size_t count;
};

void vector_int_Push( struct vector_int* object , int value ) 
{
    //implement it here
}

int vector_int_Pop( struct vector_int* object ) 
{
    //implement it here
    return 0;
}    

struct vector_int_table
{
    void( *Push )( struct vector_int* , int );
    int( *Pop )( struct vector_int* );

} vector_int_table = { 
                         .Push = vector_int_Push ,
                         .Pop = vector_int_Pop 
                     };

#define vector(type)   vector_##type##_table

int main(void)
{
    vector_var(int) one = { 0 };
    vector_var(int) two = { 0 };

    one = two;

    vector(int).Push( &one , 1 );
    int value = vector(int).Pop( &one );
    ( void )value;

    return 0;
}

@doynax 是的,我有些犹豫使用 vector(int).Push,而不是直接在函数名前加前缀并直接调用实际函数:vector_int_Push - 2501
@zee 将你的代码发布到 ideone.com 并确保它能正常运行。 - 2501
@zee 那样行不通:https://ideone.com/t0J8Sl 我的第一个示例可以正确地执行此操作,而且没有使用typedefs。 - 2501
@2501 这很奇怪,它在我的机器上编译通过了(mingw on win10),当我在网站上按下执行时,它也正常运行。 - zee
@zee 那是因为您没有点击我的链接。 - 2501
显示剩余6条评论

5

Vector(DATATYPE)结构体定义为: struct { DATATYPE* data; size_t size; size_t used; },但对于指向函数的指针来说,该结构体并不适用。

void*可以用于任何对象的指针,但对于指向函数的指针来说就不是这样了。

C语言允许将一个类型的函数指针保存为另一种类型的函数指针。通过使用下面两个结构体的联合体,代码有足够的空间来保存任何类型的指针。对于所使用的成员和类型的管理则保持开放状态。

union u_ptr {
  void *object;
  void (*function)();
}

2

不错。我没有看到任何缺点。再解释一种方法,通常在这种情况下使用联合:

typedef union { int i; long l; float f; double d; /*(and so on)*/} vdata;
typedef enum  {INT_T,LONG_T,FLOAT_T, /*(and so on)*/} vtype;
typedef struct 
{
    vtype t;
    vdata data
} vtoken;
typedef struct
{
    vtoken *tk;
    size_t sz;
   size_t n;
} Vector;

所以这是一种可行的方法。数据类型的枚举可以通过typedef避免,但如果您使用混合类型(例如:长整型、双精度浮点型、单精度浮点型等),您必须使用它们,因为int + double并不等于double + int;这也是使用联合来完成此任务更容易的原因。您保留所有算术规则不变。


1
这种基于联合的“变体”的缺点是,一个项目永远不能比最大可能类型更小。因此,当用户添加“bombastic_gigabyte_t”(实际上只需要1个实例)时,它将使您的所有变量实例都变得那么大,然后耗尽计算机中的所有RAM内存。这使得联合不适合这种通用编程的情况。 - Lundin
@Lundin 将 bombastic_gigabyte_t 模型建模为 fantastic_word_t 列表。 - DepressedDaniel

2

扩展这个答案,关于多态解决方案,我们同样可以将其包括指针类型或用户定义的类型。这种方法的主要优点是摆脱“数据类型”枚举和所有运行时检查开关语句。

variant.h

#ifndef VARIANT_H
#define VARIANT_H

#include <stdio.h>
#include <stdint.h>

typedef void print_data_t (const void* data);
typedef void print_type_t (void);

typedef struct 
{
  void* data;
  print_data_t* print_data;
  print_type_t* print_type;
} variant_t;

void print_data_char    (const void* data);
void print_data_short   (const void* data);
void print_data_int     (const void* data);
void print_data_ptr     (const void* data);
void print_data_nothing (const void* data);

void print_type_char        (void);
void print_type_short       (void);
void print_type_int         (void);
void print_type_int_p       (void);
void print_type_void_p      (void);
void print_type_void_f_void (void);

void print_data (const variant_t* var);
void print_type (const variant_t* var);

#define variant_init(var) {                \
  .data = &var,                            \
                                           \
  .print_data = _Generic((var),            \
    char:  print_data_char,                \
    short: print_data_short,               \
    int:   print_data_int,                 \
    int*:  print_data_ptr,                 \
    void*: print_data_ptr,                 \
    void(*)(void): print_data_nothing),    \
                                           \
  .print_type = _Generic((var),            \
    char:  print_type_char,                \
    short: print_type_short,               \
    int:   print_type_int,                 \
    int*:  print_type_int_p,               \
    void*: print_type_void_p,              \
    void(*)(void): print_type_void_f_void) \
}


#endif /* VARIANT_H */

variant.c

#include "variant.h"

void print_data_char    (const void* data) { printf("%c",  *(const char*)  data); }
void print_data_short   (const void* data) { printf("%hd", *(const short*) data); }
void print_data_int     (const void* data) { printf("%d",  *(const int*)   data); }
void print_data_ptr     (const void* data) { printf("%p",  data); }
void print_data_nothing (const void* data) {}

void print_type_char        (void) { printf("char");          }
void print_type_short       (void) { printf("short");         }
void print_type_int         (void) { printf("int");           }
void print_type_int_p       (void) { printf("int*");          }
void print_type_void_p      (void) { printf("void*");         }
void print_type_void_f_void (void) { printf("void(*)(void)"); }


void print_data (const variant_t* var)
{
  var->print_data(var->data);
}

void print_type (const variant_t* var)
{
  var->print_type();
}

main.c

#include <stdio.h>
#include "variant.h"

int main (void) 
{
  char c = 'A';
  short s = 3;
  int i = 5;
  int* iptr = &i;
  void* vptr= NULL;
  void (*fptr)(void) = NULL;

  variant_t var[] =
  {
    variant_init(c),
    variant_init(s),
    variant_init(i),
    variant_init(iptr),
    variant_init(vptr),
    variant_init(fptr)
  };

  for(size_t i=0; i<sizeof var / sizeof *var; i++)
  {
    printf("Type: ");
    print_type(&var[i]);
    printf("\tData: ");
    print_data(&var[i]);
    printf("\n");
  }

  return 0;
}

输出:

Type: char      Data: A
Type: short     Data: 3
Type: int       Data: 5
Type: int*      Data: 000000000022FD98
Type: void*     Data: 000000000022FDA0
Type: void(*)(void)     Data:
< p >使用_Generic的缺点是它会阻止我们使用私有封装,因为必须将其用作宏以传递类型信息。

另一方面,在这种情况下,“变量”必须针对所有新类型进行维护,因此并不是非常实用或通用。

尽管如此,了解这些技巧对于各种类似目的仍然是很有好处的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接