使用C中的指针循环遍历结构元素

时间:2018-10-17 16:03:26

标签: c pointers structure

我编写了这段代码以遍历结构的成员。工作正常。是否可以对混合类型元素(即一些整数,一些浮点数和...)的结构使用类似的方法?

maximumBy (\(a, _) (b, _) -> compare a b)

5 个答案:

答案 0 :(得分:7)

在C语言中,“效果很好”还不够好。因为您的编译器可以执行此操作:

struct newData
{
    int x;
    char padding1[523];
    int y;
    char padding2[364];
    int z;
    char padding3[251];
};

当然,这是一个极端的例子。但是,您得到了一般的想法;不能保证循环会正常工作,因为不能保证struct newData等效于int[3]

所以不,在一般情况下是不可能的,因为在特定情况下并不总是可能!


现在,您可能会想:“白痴决定了什么?!”好吧,我不能告诉你,但是我可以告诉你原因。计算机之间的差异很大,如果您希望代码快速运行,则编译器必须能够选择如何编译代码。这是一个示例:

处理器8有一条指令来获取各个字节,并将其放入寄存器中:

GETBYTE addr, reg

此结构可以很好地工作:

struct some_bytes {
   char age;
   char data;
   char stuff;
}

struct some_bytes可以愉快地占用3个字节,并且代码速度很快。但是处理器16呢?它没有GETBYTE,但是GETWORD

GETWORD even_addr, reghl

这仅接受一个偶数地址,并读取两个字节;一进入寄存器的“高”部分,一进入寄存器的“低”部分。为了使代码更快,编译器必须执行以下操作:

struct some_bytes {
   char age;
   char pad1;
   char data;
   char pad2;
   char stuff;
   char pad3;
}

这意味着代码可以运行得更快,但是这也意味着您的循环将无法工作。没关系,因为它叫做“未定义的行为”。允许编译器假定它永远不会发生,并且如果确实发生,则行为是不确定的。

实际上,您已经遇到了这种行为!您的特定编译器正在执行此操作:

struct newData
{
    int x;
    int pad1;
    int y;
    int pad2;
    int z;
    int pad3;
};

由于您的特定编译器将long int定义为int长度的两倍,因此您可以这样做:

|  x  | pad |  y  | pad |  z  | pad |

| long no.1 | long no.2 | long no.3 |
| int |     | int |     | int |     

正如您通过我不稳定的图表可以看出的那样,该代码是不稳定的。它可能无法在其他任何地方使用。更糟糕的是,如果您的编译器很聪明,将能够做到这一点:

for (int i=0; i<3; i++)
{
    printf("%d \n", *(addr+i));
}
     

嗯... addr来自data2,而data1来自指向struct newData的指针。 C规范说,只有指向结构开头的指针才会被取消引用,因此我可以假设在此循环中,i始终是0

for (int i=0; i<3 && i == 0; i++)
{
    printf("%d \n", *(addr+i));
}
     

这意味着它只能运行一次!哇!

printf("%d \n", *(addr + 0));
     

我需要编译的是这个

int main()
{
    printf("%d \n", 10);
}
     

哇,程序员会非常高兴,以至于我设法使这段代码加快了很多速度!

您不会满意。实际上,您将获得意想不到的行为,并且将无法找出原因。但是,如果您编写的代码没有未定义的行为,并且编译器做了类似的事情,您会感到很高兴。所以就这样。

答案 1 :(得分:4)

您正在调用undefined behavior。仅仅因为它看起来有效并不意味着它有效。

仅当原始点和结果点都指向同一数组对象(或指向数组对象末尾的指针)时,指针算术才有效。您有多个不同的对象(即使它们是同一结构的成员),因此,不能合法地使用指向一个对象的指针来获取指向另一个对象的指针。

C standard的6.5.6p8部分对此进行了详细说明:

  

将具有整数类型的表达式添加到或   从指针中减去,结果为指针的类型   操作数。如果指针操作数指向数组的元素   对象,并且数组足够大,结果指向一个元素   与原始元素的偏移量,使得   结果数组元素和原始数组元素的下标等于   整数表达式。换句话说,如果表达式P指向   数组对象的第i个元素,表达式(P)+ N(相当于N +(P))和(P)-N(其中N的值为n)指向,   数组对象的第i + n个元素和第i-n个元素(如果存在)。此外,如果表达式P指向   数组对象,表达式(P)+1指向最后一个元素   数组对象,如果表达式Q指向1之后   数组对象的最后一个元素,表达式(Q)-1指向   数组对象的最后一个元素。如果两个指针都   操作数和结果指向同一数组的元素   对象或数组对象最后一个元素之后的那个   评估不应产生溢出;否则,行为是   未定义。如果结果指向最后一个元素   数组对象,不得将其用作一元操作数   *被评估的运算符。

答案 2 :(得分:3)

您不仅不能对混合类型执行此操作,甚至有关代码也不明智。您的代码

  • 假定成员之间没有填充
  • 具有严格的别名冲突(intlong不兼容)
  • 分配long int *addr = data2;时没有显式强制转换
  • 假设intlong的大小相同(在64位Linux上不是这样)
  • 具有超出范围的数组访问:即使将其强制转换为指向第一个成员(int *addr = (int*)data;)的指针,执行addr[1]也会超出范围访问数组。

TL; DR:在C中“有效”并不意味着它是正确的。因此,如果您的程序很不稳定,那么在您最不希望它出现的某个时间,某个地方,某个地方,如果有人上前对您说,微笑,请不要感到惊讶!您在这里有未定义的行为。

答案 3 :(得分:1)

简短的回答是“否”。

更长的答案:关于“有效”的例子也不是真正合法的。如果出于某种原因,如果您确实希望能够遍历多种类型,则可以通过结构和联合来发挥创造力。例如,具有一个成员的结构告知另一成员所持有的数据类型。另一个成员将是所有可能的数据类型的并集。像这样:

#include <stdio.h>
#include <stdlib.h>

enum TYPE {INT, DOUBLE};

union some_union {
  int x;
  double y;
};

struct multi_type {
  enum TYPE type;
  union some_union u;
};

struct some_struct {
  struct multi_type array[2];
};

int main(void) {
   struct some_struct derp;

   derp.array[0].type = INT;
   derp.array[0].u.x = 5;
   derp.array[1].type = DOUBLE;
   derp.array[1].u.y = 5.5;

   for(int i = 0; i < 2; ++i) {
      switch (derp.array[i].type) {
         case INT:
            printf("Element %d is type 'int' with value %d\n", i, derp.array[i].u.x);
            break;
         case DOUBLE:
            printf("Element %d is type 'double' with value %lf\n", i, derp.array[i].u.y);
            break;
      }
   }
   return EXIT_SUCCESS;
}

当联合中元素类型的大小差异很大时,确实会浪费空间。例如,如果您不只是拥有intdouble,而是拥有一些占用千字节空间的大型复杂结构,那么即使是简单的int元素也将占用那么多空间。

或者,如果您可以将数据不直接放在结构中,而只保留指向数据的指针,那么可以使用类似的方法来破坏联合。

#include <stdio.h>
#include <stdlib.h>

enum TYPE {INT, DOUBLE};

struct multi_type {
  enum TYPE type;
  void *data;
};

struct some_struct {
  struct multi_type array[2];
};

int main(void) {
   struct some_struct derp;
   int x;
   double y;

   derp.array[0].type = INT;
   derp.array[0].data = &x;
   *(int *)(derp.array[0].data) = 5;
   derp.array[1].type = DOUBLE;
   derp.array[1].data = &y;
   *(double *)derp.array[1].data = 5.5;

   for(int i = 0; i < 2; ++i) {
      switch (derp.array[i].type) {
         case INT:
            printf("Element %d is type 'int' with value %d\n", i, *(int *)derp.array[i].data);
            break;
         case DOUBLE:
            printf("Element %d is type 'double' with value %lf\n", i, *(double *)derp.array[i].data);
            break;
      }
   }
   return EXIT_SUCCESS;
}

但是,在进行任何上述操作之前,我建议您重新考虑您的设计,并考虑是否真的需要遍历不同类型的元素,或者是否有更好的方法关于您的设计的信息,例如分别遍历每种类型的元素。

答案 4 :(得分:1)

以上所有好的答案。但是代码中还有另一件事很危险:

struct newData *data2 = &data1;
long int *addr = data2;

这里假设您可以在特定机器上将指针转换为结构体,并将其转换为long int指针。在现代机器上,这可能几乎总是正确的,但对此并不能保证,大多数编译器至少会向您发出警告。

除了对结构进行解引用以外的所有问题,您都可以使用类似这样的方法:

struct newData *data2 = &data1;
void * addr = data2;

for(int i=0; i < 3; i++){
    printf("%d \n", *((long int *)addr+i));
}

现在那仍然是错误的代码。您使用long int来补偿编译器放入结构中的填充;我想你是通过实验达到的。

您可以找到有关填充的信息(如果有的话),编译器适用于您的结构:

#include <assert.h>
.
.
.
assert(sizeof(struct newData) / sizeof(int) == 3);

如果发生任何混乱,这至少会终止您的程序,无论是通过填充还是由于您的结构与3 int事物不匹配。 仍然是错误的代码。

您可以通过逐步检查大小和结构成员地址来扩展对结构中可能的填充的检查,但这确实很可怕。下面的指向单个成员的指针算法会变得越来越混乱,像这样:

(假设您已经计算了(相同!)结构成员之间的填充值:

#include <assert.h>
.
.
.
//assert(sizeof(struct newData) / sizeof(int) == 3);

//Very ugly....don't really do this.
int padding = (sizeof(struct newData) / sizeof(int) / 3)  - 1;

.
.
.
struct newData *data2 = &data1;

// Use a void pointer, which can hold all other data pointers
void * addr = data2;

for(int i=0; i < 3; i++)
{
// Cast the pointer to (char*), because that is the only guaranteed
// type size - 1 byte
// Do your pointer arithmetic by using the actual size of int on your 
// machine, plus the padding

printf("%d \n", *((char *)addr + (i * (sizeof(int) + padding))));
}

但是它仍然是非常讨厌的代码。如果您想将特定的二进制输入(例如从音频文件中)读取到某种结构中,则可能需要执行类似的操作,但是有很多更好的方法可以做到这一点。

PS:AFAIK无法保证结构占用的内存是连续的,而不考虑填充问题。我猜想堆栈中的(小)结构在大多数情况下都是连续的,但是堆中的大结构很可能会散布在不同的内存位置。

因此随时将指针算术运算到一个结构中是非常危险的。