簡體   English   中英

為什么 offsetof() 的這種實現有效?

[英]Why does this implementation of offsetof() work?

在 ANSI C 中,offsetof 定義如下。

#define offsetof(st, m) \
    ((size_t) ( (char *)&((st *)(0))->m - (char *)0 ))

為什么這不會引發分段錯誤,因為我們正在取消引用 NULL 指針? 或者這是某種編譯器黑客,它看到只有偏移量的地址被取出,所以它靜態計算地址而不實際取消引用它? 這段代碼也可移植嗎?

上面代碼中的任何一點都沒有被取消引用。 當在地址值上使用*->以查找引用值時,會發生取消引用。 上面*的唯一用途是在類型聲明中用於強制轉換。

->運算符在上面使用,但不用於訪問值。 相反,它用於獲取值的地址。 這是一個非宏代碼示例,應該讓它更清楚一點

SomeType *pSomeType = GetTheValue();
int* pMember = &(pSomeType->SomeIntMember);

第二行實際上不會導致取消引用(取決於實現)。 它只是在pSomeType值中返回SomeIntMember的地址。

您看到的是任意類型和字符指針之間的大量轉換。 char 的原因是它是 C89 標准中唯一(可能是唯一)具有明確大小的類型之一。 大小為 1。通過確保大小為 1,上面的代碼可以實現計算值的真實偏移量的邪惡魔法。

雖然這是offsetof的典型實現,但標准並沒有強制要求,它只是說:

以下類型和宏定義在標准頭文件<stddef.h> [...]

offsetof( type , member-designator )

它擴展為具有類型size_t的整數常量表達式,其值是以字節為單位的偏移量,從其結構的開頭(由type member-designator )到結構成員(由member-designator指定)。 類型和成員代號應該是這樣的

static type t;

然后表達式&(t. member-designator )計算為地址常量。 (如果指定的成員是位域,則行為未定義。)

閱讀 PJ Plauger 的“標准 C 庫”以討論它和<stddef.h>的其他項目,這些都是可以(應該?)在語言中的邊界線特性,並且可能需要特殊的編譯器支持.

它僅具有歷史意義,但我在 386/IX 上使用了早期的 ANSI C 編譯器(參見,我告訴過您具有歷史意義,大約在 1990 年)該編譯器在該版本的offsetof上崩潰,但在我將其修改為:

#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024))

那是某種編譯器錯誤,尤其是因為頭文件與編譯器一起分發並且不起作用。

在 ANSI C 中, offsetof不是這樣定義的。 它沒有這樣定義的原因之一是某些環境確實會拋出空指針異常,或者以其他方式崩潰。 因此,ANSI C 將offsetof( )的實現留給編譯器構建者開放。

上面顯示的代碼是典型的編譯器/環境,它們不主動檢查 NULL 指針,但僅在從 NULL 指針讀取字節時才會失敗。

為了回答問題的最后一部分,代碼不可移植。

只有當兩個指針指向同一數組中的對象或指向數組最后一個對象之后的一個對象時,兩個指針相減的結果才被定義和移植(7.6.2 Additive Operators, H&S Fifth Edition)

清單 1:一組代表性的offsetof()宏定義

// Keil 8051 compiler
#define offsetof(s,m) (size_t)&(((s *)0)->m)

// Microsoft x86 compiler (version 7)
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)

// Diab Coldfire compiler
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))

typedef struct 
{
    int     i;
    float   f;
    char    c;
} SFOO;

int main(void)
{
  printf("Offset of 'f' is %zu\n", offsetof(SFOO, f));
}

宏中的各種運算符按順序計算,以便執行以下步驟:

  1. ((s *)0)取整數零並將其轉換為指向s的指針。
  2. ((s *)0)->m取消引用指向結構成員m指針。
  3. &(((s *)0)->m)計算的地址m
  4. (size_t)&(((s *)0)->m)將結果轉換為適當的數據類型。

根據定義,結構本身位於地址 0。因此指向的字段地址(上面的第 3 步)必須是從結構開始的偏移量(以字節為單位)。

它不會出現段錯誤,因為您沒有取消引用它。 指針地址被用作從另一個數字中減去的數字,而不是用於尋址內存操作。

它計算成員m相對於st類型對象表示的起始地址的偏移量。

((st *)(0))指的是st *類型的NULL指針。 &((st *)(0))->m指的是這個對象中成員m的地址。 由於此對象的起始地址為0 (NULL) ,因此成員 m 的地址正是偏移量。

char *轉換和差值計算以字節為單位的偏移量。 根據指針操作,當您在兩個T *類型的指針之間進行區分時,結果是操作數包含的兩個地址之間表示的T類型對象的數量。

引用offsetof宏的 C 標准:

C 標准,第 6.6 節,第 9 段

地址常量是空指針、指向指定靜態存儲持續時間對象的左值指針或指向函數指示符的指針; 它應使用一元&運算符或轉換為指針類型的整數常量顯式創建,或通過使用數組或函數類型的表達式隱式創建。 數組下標[]和成員訪問. ->運算符、地址&和間接*一元運算符以及指針強制轉換可用於創建地址常量,但不能使用這些運算符訪問對象的值。

宏定義為

#define offsetof(type, member)  ((size_t)&((type *)0)->member)

並且該表達式包括地址常量的創建。

雖然說實話,結果不是地址常量,因為它不指向靜態存儲持續時間的對象。 但這仍然約定不得訪問對象的值,因此不會取消引用轉換為指針類型的整數常量。

另外,請考慮 C 標准中的引用:

C 標准,第 7.19 節,第 3 段

類型和成員代號應該是這樣的

static type t;

然后表達式&(t.member-designator)計算為地址常量。 (如果指定的成員是位域,則行為未定義。)

C 中的 struct 是一種復合數據類型(或記錄)聲明,它在內存塊中以一個名稱定義了一個物理分組的變量列表,允許通過單個指針或通過返回的結構聲明名稱訪問不同的變量同一個地址。

從編譯器的角度來看,結構體聲明的名稱是一個地址,成員指示符是該地址的偏移量。

暫無
暫無

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

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