如何在C语言中根据变量整数访问结构体成员?

9
假设我有这个struct(顺便提一下,它包含位域,但您不必在意):
struct Element {
    unsigned int a1 : 1;
    unsigned int a2 : 1;
    ...
    unsigned int an : 1;
};

我希望以便捷的方式访问第i个成员,这与it技术有关。我们来看一下检索解决方案。
我想出了这个函数:

int getval(struct Element *ep, int n)
{
    int val;
    switch(n) { 
         case 1: val = ep->a1; break;
         case 2: val = ep->a2; break;
         ...
         case n: val = ep->an; break;
    }
    return val;
}

我猜想有一个更简单的解决方案,可能类似于数组访问方式。
我尝试做了这样的事情:
 #define getval(s,n)   s.a##n

但是预期的结果并没有得到实现。
有更好的解决方案吗?


1
很抱歉,我们必须关注是否存在位字段:您无法定义指向位字段成员的指针,而数组访问样式意味着指针处理。 - mouviciel
12个回答

13

除非你对结构体的底层结构有特定的了解,否则无法在C中实现这样的方法。会出现各种问题,包括:

  • 成员大小不同
  • 打包问题
  • 对齐问题
  • 像位域这样的技巧将会引起问题

最好手动为你的结构体实现一个方法,并深入了解内部成员的结构。


我认为结构体应该保证连续分配。如果这是正确的,并且您知道需要偏移的位数,则似乎完全可以简单地解引用结构指针并直接跳转到特定值。 - DevinB
@devinb,连续的没错。但是变量大小的成员会让你头疼。如果结构体中有不同大小的成员,除非你知道关于结构体的所有信息,否则无法计算给定成员的偏移量。因此,无法定义一个通用的宏。 - JaredPar
我误读了你的回复,对此我感到抱歉。我同意这个想法。它完全可以在C语言中实现,但必须手动完成,并需要对结构体具有特定详细知识。 - DevinB
6
结构体并不保证连续分配内存。它们确保按顺序分配内存,成员之间可能存在填充空间。否则,类似 {double a; char b; double c;} 的结构体将导致 c 的内存对齐出现严重问题。 - David Thornley
很好的发现,David。这在所有操作系统中都是一致的吗?也就是说,即使包括填充,你的结构体组件始终会在结构体起始位置的相同偏移量处分配吗? - DevinB
@devinb:结构体元素的顺序是C标准所要求的(因此是保证的),但相邻元素之间的填充则取决于实现。这意味着在不同平台或编译器之间,给定元素的偏移可能会不同。如果您想要一个元素的偏移量,则应该使用offsetof()宏。位域使事情变得非常复杂,因为它们在很大程度上是由实现定义的,因此完全不可移植。 - Michael Carman

7
如果您的结构体中的每个字段都是int类型,那么您基本上应该能够说:
int getval(struct Element *ep, int n)
{
    return *(((int*)ep) + n);
}

这将指向您的结构体指针转换为整数数组的指针,然后访问该数组的第n个元素。由于您的结构体中似乎所有内容都是整数,因此这是完全有效的。请注意,如果您有一个非整数成员,则此方法将失败。
更通用的解决方案是维护字段偏移量的数组:
int offsets[3];
void initOffsets()
{
    struct Element e;
    offsets[0] = (int)&e.x - (int)&e;
    offsets[1] = (int)&e.y - (int)&e;
    offsets[2] = (int)&e.z - (int)&e;
}

int getval(struct Element *ep, int n)
{
    return *((int*)((int)ep+offsets[n]));
}

这将起作用,因为您可以调用getval来获取结构体中任何int字段的值,即使您的结构体中有其他非int字段,因为偏移量都是正确的。然而,如果您尝试在其中一个非int字段上调用getval,它将返回完全错误的值。
当然,您可以为每种数据类型编写不同的函数,例如:
double getDoubleVal(struct Element *ep, int n)
{
    return *((double*)((int)ep+offsets[n]));
}

然后只需调用适当的函数,就可以处理您想要的任何数据类型。顺便说一句,如果您使用的是C ++,您可以这样说

template<typename T>
T getval(struct Element *ep, int n)
{
    return *((T*)((int)ep+offsets[n]));
}

然后它将适用于您想要的任何数据类型。


这段代码有问题。原帖使用了1位大小的位域(bit field),而你将返回一个4字节大小的指针(pointer)。这会产生未对齐(un-aligned)的指针,当在适当对齐方式下进行解引用(dereference)时,它将从结构体(struct)读取4个字节(在大多数平台上),而不是指定的1位。 - JaredPar
2
@JaredPar:这个答案的第一行是“如果你的结构体中每个字段都是int”。但实际上并不是这样,但是提问者也说“你不应该关心字段的类型”,而我们确实很关心。因为位域很奇怪。 - Steve Jessop
有趣;这是我不熟悉的C语法领域。当他谈到位域时,我认为他指的是常规的int字段,可以对其应用位掩码来提取某些值(例如,前四位是序列号,接下来的17位是ID标记等)。即使我的解决方案无法与他使用的奇怪结构类型配合使用,我仍将保留它作为如何进行此类指针操作的一般示例。 - Eli Courtwright
@onebyone,这主要是我想指出的,因为OP的问题存在内在的矛盾。此外,当Eli说“如果每个字段都是int”时,会让人感到困惑,因为位域技术上被标记为int,但实际上是位域。 - JaredPar
如果不是自动化init函数,而是手动完成并维护一个“偏移数组”,那么这将起作用。每当结构体发生变化时,它都必须更改,但它将使您能够以他所描述的方式访问事物。 - DevinB

6
如果您的结构体不是位域,您可以使用数组访问。如果我没记错的话,C语言保证了一个结构体中一系列相同类型的成员具有与数组相同的布局。如果您知道编译器将位域存储到整数类型中的哪些位以及顺序,那么您可以使用移位/掩码操作,但这取决于具体实现。
如果您想通过变量索引访问位,最好用包含标志位的整数替换位域。按变量访问并不是位域的目的:a1 ... an 基本上是独立的成员,而不是一组位。
您可以尝试以下代码:
struct Element {
    unsigned int a1 : 1;
    unsigned int a2 : 1;
    ...
    unsigned int an : 1;
};

typedef unsigned int (*get_fn)(const struct Element*);

#define DEFINE_GETTER(ARG) \
    unsigned int getter_##ARG (const struct Element *ep) { \
        return ep-> a##ARG ; \
    }

DEFINE_GETTER(1);
DEFINE_GETTER(2);
...
DEFINE_GETTER(N);

get_fn jump_table[n] = { getter_1, getter_2, ... getter_n};

int getval(struct Element *ep, int n) {
    return jump_table[n-1](ep);
}

一些重复的内容可以通过巧妙地多次包含相同的头文件来避免,每次都定义一个不同的宏。该头文件为1到N扩展该宏。

但我并不认为这值得。

它确实解决了JaredPar的观点,即如果您的结构混合了不同类型,则会遇到麻烦 - 在此,通过特定跳转表访问的所有成员当然必须是相同类型,但它们之间可以有任何旧垃圾。尽管如此,这仍然存在JaredPar的其他观点,并且与switch相比,这是很多代码膨胀而又没有好处。


哇,这真是一项了不起的构建。它看起来肯定不实用,但我会采纳你提出的一些想法。谢谢。 - Ori Popowski
如果我没记错的话,C语言保证结构体中一系列相同类型的成员和一个数组具有相同的布局。这个对我来说是新闻。还有其他人听说过吗?想象一下,你定义了一个5字节长的结构体,在32位机器上,通常会留下3字节的空隙。我想知道数组是否也会显示这些空隙? - user82238
取决于结构体的成员。一个包含5个字符的5字节结构体可以在数组或结构体中不留空隙地占用5个字节的大小。一个包含int32和char的5字节结构体在这两种情况下都会有8个字节的大小和空隙。 - Steve Jessop
这里有一些代码可以在你的编译器上尝试:http://pastebin.com/m4db82d26。如果数组没有填充,那么 bloo 或 blee 的 int 成员将在任何数组的第二、第三、第四、第六等元素中错位对齐。 - Steve Jessop
使用模板怎么样?我尝试过,但无法使其工作。 - Anil

3
没有简单的方法可以更轻松地完成这个任务,尤其是对于位域,通过指针间接访问它们很困难(您无法获取位域的地址)。
当然,您可以将该函数简化为以下内容:
int getval(const struct Element *ep, int n)
{
    switch(n)
    {
      case 1: return ep->a1;
      case 2: return ep->a2;
      /* And so on ... */
    }
    return -1; /* Indicates illegal field index. */
}

看起来很明显,通过使用一个预处理器宏来扩展case行,可以进一步简化实现,但这只是


0

基于eli-courtwright的解决方案,但不使用字段偏移数组...... 如果您有一个包含指针字段的结构体,可以这样编写:

struct  int_pointers
 {
   int  *ptr1;
   int  *ptr2;
   long *ptr3;
   double *ptr4;
   std::string * strDescrPtr;

};

那么你知道每个指针都有一个相对于结构体指针的4字节偏移量,因此你可以这样写:

struct int_pointers  ptrs;
int  i1 = 154;
int i2 = -97;
long i3 = 100000;
double i4  = (double)i1/i2;
std::string strDescr = "sample-string";
ptrs.ptr1 =  &i1;
ptrs.ptr2 =  &i2;
ptrs.ptr3 = &i3;
ptrs.ptr4 = &i4;
ptrs.strDescrPtr = &strDescr;

那么,例如对于一个整数值你可以这样写:

int GetIntVal (struct int_pointers *ep, int intByteOffset) 
{ 
   int * intValuePtr =  (int *)(*(int*)((int)ep + intByteOffset)); 
   return *intValuePtr; 
}

通过以下方式调用:

int intResult = GetIntVal(&ptrs,0) //to retrieve the first int value in ptrs structure variable

int intResult = GetIntVal(&ptrs,4) //to retrieve the second int value in ptrs structure variable

对于其他结构字段值,依此类推(编写其他特定函数并使用正确的字节偏移值(4的倍数))。


0
为什么不把 getval() 内置到结构体中呢?
struct Whang {
    int a1;
    int a2;
    int getIth(int i) {
        int rval;
        switch (i) {
            case 1: rval = a1; break;
            case 2: rval = a2; break;
            default : rval = -1; break;
        }
        return rval;
    }
};    

int _tmain(int argc, _TCHAR* argv[])  
{  
        Whang w;  
    w.a1 = 1;  
    w.a2 = 200;

    int r = w.getIth(1);

    r = w.getIth(2);

    return 0;
}

getIth() 会了解 Whang 的内部结构,并能够处理其中包含的任何内容。


2
为什么不将getval()构建到结构体中呢?因为这是C语言,而不是C++。 - Steve Jessop
抱歉,好久没用纯C了...对于这次的误导感到抱歉。 - Number8

0
我建议使用代码生成。如果您的结构体中没有大量字段,您可以为每个字段或一系列字段自动生成例程,并像这样使用它们:
val = getfield_aN( myobject, n );

或者

val = getfield_foo( myobject );

0

如果你想使用元素索引来访问你的结构体:

int getval(struct Element *ep, int n)

以名称为准:

ep->a1

那么你就会被困在一些难以维护的 switch 像方法中,这是大家都建议的。

如果只想通过索引而非名称访问,则可以更有创意地进行操作。

首先,定义一个字段类型:

typedef struct _FieldType
{
    int size_in_bits;
} FieldType;

然后创建一个结构定义:

FieldType structure_def [] = { {1}, {1}, {1}, {4}, {1}, {0} };

以上定义了一个结构体,包含五个元素,分别占据 1、1、1、4 和 1 位。最后的 {0} 标志着定义的结束。

现在创建一个元素类型:

typedef struct _Element
{
    FieldType *fields;
} Element;

创建一个 Element 实例的方法如下:
Element *CreateElement (FieldType *field_defs)
{
  /* calculate number of bits defined by field_defs */
  int size = ?;
  /* allocate memory */
  Element *element = malloc (sizeof (Element) + (size + 7) / 8); /* replace 7 and 8 with bits per char */
  element->fields = field_defs;
  return element;
}

然后访问一个元素:

int GetValue (Element *element, int field)
{
   /* get number of bits in fields 0..(field - 1) */
   int bit_offset = ?;
   /* get char offset */
   int byte_offset = sizeof (Element) + bit_offset / 8;
   /* get pointer to byte containing start of data */
   char *ptr = ((char *) element) + byte_offset;
   /* extract bits of interest */
   int value = ?;
   return value;
}

设置值与获取值类似,只需要更改最后一部分。

您可以通过扩展FieldType结构来增强上述功能,以包括有关存储的值类型的信息:char、int、float等,然后编写每种类型的访问器,检查所需类型是否与定义的类型相匹配。


0
如果您的结构体中只有位域,或者所有位域都在最前面,并且少于32(或64)个位域,则此解决方案适用于您。
#include <stdio.h>
#include <stdint.h>

struct Element {
  unsigned int a1 : 1;
  unsigned int a2 : 1;
  unsigned int a3 : 1;
  unsigned int a4 : 1;
};

#define ELEMENT_COUNT 4 /* the number of bit fields in the struct */

/* returns the bit at position N, or -1 on error (n out of bounds) */
int getval(struct Element* ep, int n) 
{
  if(n > ELEMENT_COUNT || n < 1)
    return -1;

  /* this union makes it possible to access bit fields at the beginning of 
     the struct Element as if they were a number.
   */
  union {
    struct Element el;
    uint32_t bits;
  } comb;

  comb.el = *ep;
  /* check if nth bit is set */
  if(comb.bits & (1<<(n-1))) {
    return 1;
  } else {
    return 0;
  }
}

int main(int argc, char** argv)
{
  int i;
  struct Element el;

  el.a1 = 0;
  el.a2 = 1;
  el.a3 = 1;
  el.a4 = 0;

  for(i = 1; i <= ELEMENT_COUNT; ++i) {
    printf("el.a%d = %d\n", i, getval(&el, i));
  }  

  printf("el.a%d = %d\n", 8, getval(&el, 8));

  return 0;
}

这个解决方案是否可移植?这取决于单元内部位的对齐方式,是吧? - Ori Popowski

0

尽管 OP 指定我们不应关心结构的内容,因为它们只是位字段,但在这种情况下是否可能使用 char 或 int(或具有所需大小的任何数据类型)创建一个 n 位“数组”?

void writebit(char *array, int n)
{
  char mask = (1 << n);
  *array = *array & mask;
}

如果需要更长的“数组”,则使用较大的类型替换char类型。不确定这是否是其他结构的明确解决方案,但在此处应该可以使用类似的readbit函数。


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