簡體   English   中英

結構成員之間的指針差異?

[英]Pointer difference across members of a struct?

C99標准規定:

當減去兩個指針時,兩個指針都指向同一個數組對象的元素,或者指向數組對象的最后一個元素的元素

請考慮以下代碼:

struct test {
    int x[5];
    char something;
    short y[5];
};

...

struct test s = { ... };
char *p = (char *) s.x;
char *q = (char *) s.y;
printf("%td\n", q - p);

這顯然打破了上述規則,因為pq指針指向不同的“數組對象”,並且根據規則, q - p差異是未定義的。

但在實踐中,為什么這樣的事情會導致未定義的行為? 畢竟,struct成員按順序排列(就像數組元素一樣),成員之間有任何潛在的填充。 確實,填充量會因實現而異,這會影響計算結果,但為什么結果應該是“未定義”?

我的問題是,我們可以假設標准只是對這個問題“無知”,還是有充分的理由不擴大這個規則? 難道不能將上述規則改為“ 兩者都應指向同一數組對象的元素或同一結構的成員 ”嗎?

我唯一懷疑的是分段內存架構,其中成員可能最終分成不同的段。 是這樣的嗎?

我還懷疑這就是為什么GCC定義自己的__builtin_offsetof ,以便為offsetof宏定義“標准兼容”的原因。

編輯:

正如已經指出的那樣,標准不允許對void指針進行算術運算。 它是一個GNU擴展,僅在GCC傳遞-std=c99 -pedantic時才會觸發警告。 我正在用char *指針替換void * char *指針。

明確定義了同一結構的成員地址之間的減法和關系運算符(在類型char* )。

可以將任何對象視為unsigned char數組。

引用N1570 6.2.6.1第4段:

存儲在任何其他對象類型的非位字段對象中的值由n × CHAR_BIT位組成,其中n是該類型對象的大小(以字節為單位)。 可以將該值復制到unsigned char [ n ]類型的對象中(例如,通過memcpy ); 生成的字節集稱為值的對象表示。

...

我唯一懷疑的是分段內存架構,其中成員可能最終分成不同的段。 是這樣的嗎?

對於具有分段內存架構的系統,通常編譯器會強制限制每個對象必須適合單個段。 或者它可以允許占用多個段的對象,但它仍然必須確保指針算術和比較正常工作。

指針算術要求將兩個指針相加或相減為同一個對象的一部分,否則它就沒有意義。 引用的標准部分具體指兩個不相關的對象,如int a[b]; int b[5] 指針算法需要知道指針所指向的對象的類型(我相信你已經知道了)。

int a[5];
int *p = &a[1]+1; 

這里p是通過知道&a[1]引用一個int對象並因此增加到4個字節(假設sizeof(int)是4)來計算的。

來到結構示例,我認為它不可能以一種方式定義,使結構成員之間的指針算術合法。

我們舉個例子,

struct test {
    int x[5];
    char something;
    short y[5];
};

C標准的void指針不允許使用指針gcc -Wall -pedantic test.c (使用gcc -Wall -pedantic test.c編譯會捕獲它)。 我認為你使用gcc,假設void*類似於char*並允許它。 所以,

printf("%zu\n", q - p);

相當於

printf("%zu", (char*)q - (char*)p);

如果指針指向同一個對象並且是字符指針( char*unsigned char* ),則指針算術被很好地定義。

使用正確的類型,它將是:

struct test s = { ... };
int *p = s.x;
short *q = s.y;
printf("%td\n", q - p);

現在,如何執行qp 基於sizeof(int)sizeof(short) char something;的大小怎么char something; 在這兩個數組的中間計算?

這應該解釋為不可能對不同類型的對象執行指針運算。

即使所有成員都是相同的類型(因此沒有上面提到的類型問題),那么最好使用標准宏offsetof (來自<stddef.h> )來獲得結構成員之間的區別,它具有與指針算法類似的效果成員之間:

printf("%zu\n", offsetof(struct test, y) - offsetof(struct test, x));

所以我認為沒有必要通過C標准在struct成員之間定義指針算法。

是的,您可以在結構字節上執行指針算術:

N1570 - 6.3.2.3指針p7:

...當指向對象的指針轉換為指向字符類型的指針時,結果指向對象的最低尋址字節。 結果的連續增量(直到對象的大小)產生指向對象的剩余字節的指針。

這意味着對於程序員來說,結構的字節應被視為連續區域,無論它如何在硬件中實現。

但不是使用void*指針,那是非標准的編譯器擴展。 如標准段落中所述,它僅適用於字符類型指針。

編輯:

正如mafso在評論中指出的那樣,只要減法結果類型ptrdiff_t具有足夠的結果范圍,上述情況才有效。 由於size_t范圍可能大於ptrdiff_t ,並且如果結構足夠大,則地址可能相距太遠。

因此,最好在結構成員上使用offsetof宏並從中計算結果。

我相信這個問題的答案比看起來簡單,OP問道:

但為什么這個結果應該“未定義”?

好吧,讓我們看看未定義行為的定義是在草案C99標准第3.4.3節中:

使用不可移植或錯誤的程序結構或錯誤數據時的行為,本國際標准不對此要求

它只是標准沒有強制要求的行為,這完全適合這種情況,結果將根據體系結構而變化,並且試圖指定結果可能是困難的,如果不是不可能以便攜方式。 這就留下了一個問題,為什么他們會選擇未定義的行為而不是讓我們說未實現的行為的實現呢?

最有可能是為了限制無效指針的創建方式,這是一種未定義的行為,這與我們提供offsetof以消除不相關對象的指針減法的一個潛在需求這一事實是一致的。

雖然標准沒有真正定義術語無效指針,但我們在國際標准編程語言-C的基本原理中得到了很好的描述,在6.3.2.3節中指針強調我的 ):

標准中隱含的是無效指針的概念。 在討論指針時,標准通常引用“指向對象的指針”或“指向函數的指針”或“空指針”。地址算術中的一種特殊情況允許指針剛好超過數組的末尾。 任何其他指針都無效。

C99理由進一步補充:

無論如何創建無效指針,任何使用它都會產生未定義的行為 甚至賦值,與空指針常量的比較或與自身的比較, 在某些系統上可能會導致異常。

這強烈地暗示我們,一個指向填充將是一個無效的指針 ,雖然這是很難證明填充是不是一個對象對象的定義說:

執行環境中的數據存儲區域,其內容可以表示值

並注意到:

引用時,對象可以被解釋為具有特定類型; 見6.3.2.1。

我沒有看到我們如何推斷結構元素之間填充的類型 ,因此它們不是對象,或者至少強烈表示填充不應被視為對象

我應該指出以下幾點:

根據C99標准,第6.7.2.1節:

在結構對象中,非位字段成員和位字段所在的單元具有按聲明順序增加的地址。 指向適當轉換的結構對象的指針指向其初始成員(或者如果該成員是位字段,則指向它所在的單元),反之亦然。 結構對象中可能存在未命名的填充,但不是在其開頭。

成員之間的指針減法的結果並不是那么多,因為它是不可靠的(即,當應用相同的算法時,不保證在相同結構類型的不同實例之間相同)。

暫無
暫無

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

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