如何使用C语言遍历结构体的成员,就像遍历数组一样?

12

假设我有一个向量类:

typedef struct vec3_s
{
    float x, y, z;
}
vec3;

但是,我希望能够在不将其转换为浮点数组的情况下迭代它。虽然在这种情况下进行类型转换是可接受的,但我很好奇是否有类似于C ++功能的东西可以在纯C中完成。例如,在C ++中,由于std :: vector<T>已经重载了下标[]运算符,我可以将其第一个索引的地址传递给一个接受void *的函数。

i.e.,

void do_something_with_pointer_to_mem( void* mem )
{
    // do stuff
}

int main( void )
{
    std::vector< float > v;

    // fill v with values here

    // pass to do_something_with_pointer_to_mem

    do_some_with_pointer_to_mem( &v[ 0 ] );

    return;
}

另一个更具体的例子是在使用OpenGL(C++)时调用glBufferData(...)

glBufferData(GL_ARRAY_BUFFER, sizeof(somevector), &somevector[0], GL_STREAM_DRAW);

那么,在C语言中是否可能使用下标运算符实现类似的功能呢?如果不行,如果我不得不编写一个函数(例如float vec3_value_at(unsigned int i)),那么将其static inline到定义它的头文件中是否有意义?

可能是在C中迭代相同类型的结构成员的重复问题。 - phuclv
3个回答

30

如果您的结构体中所有字段都是相同类型的,您可以使用如下联合体:

typedef union vec3_u
{
    struct vec3_s {
        float x, y, z;
    };
    float vect3_a[3];
}
vec3;

这种方式可以让您独立访问每个x、y或z字段,或者使用vect3_a数组对它们进行迭代。这种解决方案在内存或计算方面不需要任何成本,但我们可能离C ++类似的解决方案有一点远。


这很优雅。不过,您介绍一下它是如何工作的吗?我以前从未使用过联合体;虽然我对C++相当熟悉,但我在C方面还是比较新手。 - zeboidlund
2
未定义行为?“6.2.6.1.7:当一个值被存储在联合类型对象的成员中时,与该成员不对应但与其他成员对应的对象表示的字节取未指定值。” - Paul Hankin
实际上,正如上面的评论所述,这可能取决于编译器。但据我所知,大多数编译器将在不同联合成员的字节之间进行简单映射。因此,在上面的示例中,vect3_a [0] 映射到 x,vect3_a [1] 映射到 y,vect3_a [2] 映射到 z。这只是访问结构数据的一种语法方式。整个 vec3 实例仍然是 3*sizeof(float)。另一种迭代解决方案是简单地使用指针递增。例如,&x + 1 将对应于 &y。这基本上与使用数组相同。 - greydet
我已经在GCC x64(Linux)上进行了测试,如果有人有兴趣看代码的话。它非常简单,我没有遇到任何问题:)。实际上,这段代码在C++中使用没有什么实际意义,因为GLM几乎可以处理所有这些内容。 - zeboidlund

6

如果你不理解C++的语法糖,那么将你在C++中编写的函数写成operator[]也很容易。

float get_vec3(v *vec3, int i) {
   switch(i) {
   case 0: return v->x;
   case 1: return v->y;
   case 2: return v->z;
   }
   assert(0);
 }

现在,您可以迭代任何vec3。
 for (int i = 0; i < 3; i++) {
     printf("%f\n", get_vec3(v, i));
 }

将其设置为staticinline或两者结合起来是否有优势? - zeboidlund
“static” 会使其具有静态而非全局作用域(因此我不明白为什么您认为这是好的)。 “inline” 可能是一种优化方式,但您需要进行性能分析。 - Paul Hankin

3
在 C 语言中,您想要做的问题是您需要知道如何浏览结构(即您需要知道类型)。std::vector<T> 之所以能够按照其方式工作是因为它使用了模板(一个 C++ 概念)。现在,话虽如此,您可以尝试比您建议的略微不同的方法。如果您不想使用任何数组,您可以存储通用类型。但是,在检索数据并使用它时,用户必须知道他或她期望的数据类型。以下避免了数组(尽管在使用它们时存在潜在的更清晰的解决方案),并具有链表实现的东西,它给您几乎与 std::vector<T> 相同的灵活性(除了性能优势之外,因为这是一个具有 O(n) 操作的链表,对于所有内容(您可以聪明地反转列表以实现,也许,O(1) 插入,但这仅仅是举个例子))。
#include <stdio.h>
#include <stdlib.h>
typedef struct _item3_t
{
  void *x, *y, *z;
  struct _item3_t* next;
} item3_t;

typedef struct
{
  item3_t* head;
} vec3_t;

void insert_vec3(vec3_t* vec, void* x, void* y, void* z)
{
  item3_t* item = NULL;
  item3_t* tmp  = NULL;
  int i = 0;
  if(vec == NULL)
    return;

  item = malloc(sizeof(item3_t));
  item->x = x;
  item->y = y;
  item->z = z;
  item->next = NULL;

  tmp = vec->head;
  if(tmp == NULL) { // First element
    vec->head = item;
  } else {
    while(tmp->next != NULL)
      tmp = item->next;
    tmp->next = item;
  }
}

// This is one method which simply relies on the generic method above
void insert_vec3_float(vec3_t* vec, float x, float y, float z)
{
  float* xv, *yv, *zv;
  if(vec == NULL)
    return;
  xv = malloc(sizeof(float));
  yv = malloc(sizeof(float));
  zv = malloc(sizeof(float));

  *xv = x;
  *yv = y;
  *zv = z;

  insert_vec3(vec, xv, yv, zv);
}

void init_vec3(vec3_t* vec)
{
  if(vec == NULL)
    return;
  vec->head = NULL;
}

void destroy_vec3(vec3_t* vec)
{
  item3_t* item = NULL, *next = NULL;
  if(vec == NULL)
    return;

  item = vec->head;
  while(item != NULL) {
    next = item->next;
    free(item->x);
    free(item->y);
    free(item->z);
    free(item);
    item = next;
  }
}

item3_t* vec3_get(vec3_t* vec, int idx)
{
  int i = 0;
  item3_t* item = NULL;
  if(vec == NULL)
    return NULL;
  item = vec->head;
  for(i = 0 ; i < idx && item != NULL ; ++i)
    item = item->next;
  return item;
}

void do_something(item3_t* item)
{
  if(item == NULL)
    return;
  float x = *((float*)item->x);
  float y = *((float*)item->y);
  float z = *((float*)item->z);

  // To do - something? Note, to manipulate the actual
  // values in the vector, you need to modify their values
  // at their mem addresses
}

int main()
{
  vec3_t vector;

  init_vec3(&vector);

  insert_vec3_float(&vector, 1.2, 2.3, 3.4);

  printf("%f %f %f\n", *((float*)vec3_get(&vector, 0)->x), *((float*)vec3_get(&vector, 0)->y), *((float*)vec3_get(&vector, 0)->z));

  do_something(vec3_get(&vector, 0));

  destroy_vec3(&vector);

  return 0;
}

这段代码应该可以直接编译。你所拥有的是一个链表,它是你的“向量”(特别是vec3结构)。链表中的每个节点(即在std::vector<T>中的每个元素)都有三个元素,它们都是void指针。因此,你可以在这里存储任何数据类型。唯一的问题是,你需要为那些指针分配内存,并且在删除元素时,你需要释放那些内存(参考vec3_destroy方法的示例)。希望这能更好地帮助理解这些void指针在你的情况下如何工作。
要检索数据,你将无法使用[]符号,但你可以以同样的方式使用vec3_get方法。do_something方法是一种类似于你在OP中提到的方法的示例存根。

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