【问题标题】:Looping over structure elements using pointers in C使用 C 中的指针循环结构元素
【发布时间】:2019-03-22 09:14:23
【问题描述】:

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

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

struct newData
{
    int x;
    int y;
    int z;
}  ;

int main()
{
    struct newData data1;
    data1.x = 10;
    data1.y = 20;
    data1.z = 30;

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

【问题讨论】:

  • 不要那样做。如果你想循环一系列元素,它们应该在一个数组中。
  • 这是未定义的行为。使用联合。

标签: c pointers structure


【解决方案1】:

在 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 来自data2data1 是指向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);
}

哇,程序员会很高兴我能大大加快这段代码的速度!

你不会高兴的。事实上,你会得到意想不到的行为,并且无法找出原因。但是如果您编写的代码没有未定义行为,并且您的编译器也做了类似的事情,您很高兴。所以它保持不变。

【讨论】:

    【解决方案2】:

    您正在调用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 指向 数组对象的最后一个元素,表达式 (Q)-1 指向 数组对象的最后一个元素。如果两个指针 操作数和结果指向同一个数组的元素 对象,或数组对象的最后一个元素, 评估不应产生溢出;否则,行为是 不明确的。如果结果指向最后一个元素 数组对象,不得用作一元的操作数 * 被评估的运算符。

    【讨论】:

    • 值得一提,因为你包括了关于数组对象过去结束的事情的注释,虽然它的指针算法是有效的,但取消引用仍然是非法的超出数组末尾的指针。
    【解决方案3】:

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

    • 假设成员之间没有填充
    • 存在严格的别名违规(intlong 不兼容)
    • 在分配long int *addr = data2; 时没有显式转换
    • 假设 intlong 大小相同(在 64 位 Linux 上不是这样)
    • 数组访问越界:即使转换为指向第一个成员 (int *addr = (int*)data;) 的指针,addr[1] 也会越界访问数组。

    TL;DR:在 C 中“它有效”并不意味着它是正确的。因此,如果您的程序有问题,请不要感到惊讶,如果某个时候、某个地方、某个您最意想不到的地方,有人走到您面前对您说,微笑!你在这里有未定义的行为。

    【讨论】:

      【解决方案4】:

      简短的回答是“不”。

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

      #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;
      }
      

      不过,在开始执行任何操作之前,我建议您重新考虑一下您的设计,并考虑您是否真的需要循环不同类型的元素,或者是否有更好的方法关于您的设计,例如分别循环遍历每种类型的元素。

      【讨论】:

      • 我可能很奇怪,但我倾向于编写不同指针的联合,而不是使用 (void *) 指针。这并不是说我不强制转换,但我喜欢向编译器明确说明,以防它发现一个聪明的捷径。
      • @wizzwizz4 是的,对于这种性质的东西,你确切地知道可能性是什么,这将是一个有意义的选择。
      【解决方案5】:

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

      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 不保证结构占用的内存是连续的,无论填充问题如何。我想堆栈上的(小)结构大部分时间都是连续的,但堆上的大结构很可能会分散在不同的内存位置。

      所以在任何时候对结构进行指针运算都是非常危险的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-02-02
        • 2017-01-04
        • 1970-01-01
        • 1970-01-01
        • 2011-06-04
        • 2023-02-16
        相关资源
        最近更新 更多