簡體   English   中英

使用C中的指針循環結構元素

[英]Looping over structure elements using pointers in C

我編寫了這段代碼來迭代結構的成員。 它工作正常。 我可以對具有混合類型元素的結構使用類似的方法,即一些整數,一些浮點數和......?

#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中,“它工作正常”還不夠好。 因為允許您的編譯器執行此操作:

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

哇,程序員會非常高興我已經設法加快了這個代碼的速度!

你不會高興的。 事實上,你會得到意想不到的行為,並且無法解決原因。 但是如果您編寫的代碼沒有未定義的行為,並且您的編譯器已經做了類似的事情,那么您很高興。 所以它保持不變。

您正在調用未定義的行為 僅僅因為它似乎工作並不意味着它是有效的。

指針算法僅在原始點和結果點都指向同一個數組對象(或者超過數組對象末尾的一個)時才有效。 您有多個不同的對象(即使它們是同一結構的成員),因此指向一個對象的指針不能合法地用於獲取指向另一個的指針。

這在C標准的 6.5.6p8節中詳細說明:

當一個具有整數類型的表達式被添加到指針或從指針中減去時,結果具有指針操作數的類型。 如果指針操作數指向數組對象的元素,並且數組足夠大,則結果指向偏離原始元素的元素,使得結果元素和原始數組元素的下標的差異等於整數表達式。 換句話說,如果表達式P指向數組對象的第i個元素,則表達式(P)+ N(等效地,N +(P))和(P)-N(其中N具有值n)指向分別為數組對象的第i + n和第i-n個元素,只要它們存在。 此外,如果表達式P指向數組對象的最后一個元素,則表達式(P)+1指向一個超過數組對象的最后一個元素,如果表達式Q指向一個超過數組對象的最后一個元素,表達式(Q)-1指向數組對象的最后一個元素。 如果指針操作數和結果都指向同一個數組對象的元素,或者指向數組對象的最后一個元素,則評估不應產生溢出; 否則,行為未定義。 如果結果指向數組對象的最后一個元素之后,則不應將其用作已計算的一元*運算符的操作數。

您不僅可以使用混合類型執行此操作,即使有問題的代碼也是不明智的。 你的代碼

  • 假設成員之間沒有填充
  • 有嚴格的別名沖突( intlong不兼容)
  • 在賦值long int *addr = data2;時沒有顯式轉換long int *addr = data2;
  • 假設intlong的大小相同(在64位Linux上不是這樣)
  • 有數組訪問超出范圍:即使被轉換為指向第一個成員的指針( int *addr = (int*)data; ),執行addr[1]訪問數組超出范圍。

TL; DR:在C“它的工作原理”並不意味着它是正確的。 因此,如果您的計划不穩定,請不要感到驚訝,如果某個時間,某個地方,某個地方,當您最不期望它,有人走近您說,微笑! 你在這里有未定義的行為。

最簡潔的答案是不”。

更長的答案:你的“工作”的例子也不合法。 無論出於何種原因,如果您真的希望能夠遍歷多種類型,那么您可以通過結構和聯合獲得創意。 例如,具有一個成員的結構通知另一個成員持有的數據類型。 另一個成員將是所有可能的數據類型的聯合。 像這樣的東西:

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

然而,在開始做任何這些之前,我建議再次考慮你的設計,並考慮你是否真的需要循環不同類型的元素,或者是否有更好的方法來進行你的設計,如循環每種類型的元素分開。

上面所有的好答案。 但是在您的代碼中還有另一件事是危險的:

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事物不匹配,這至少會終止你的程序。 還是糟糕的代碼。

您可以通過對大小和結構成員地址進行更詳細的逐步檢查來擴展對結構中可能填充的檢查,但這確實非常糟糕。 以下指針算法來獲取個別成員會得到越來越多的混淆,如下所示:

(假設您已經計算了(相同!)struct成員之間的一些填充值:

#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,不保證結構占用的內存是連續的,無論填充問題如何。 我想堆棧上的(小)結構在大多數情況下都是連續的,但是堆上的大型結構很可能會在不同的內存位置上散布。

因此,在任何時候將指針算法運行到結構中是非常危險的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM