#define OFFSETOF(type, field) ((size_t)&(((type *)0)->field))
(type *)0:把0地址當成type類型的指針。
((type *)0)->field:對應域的變量。
&((type *)0)->field:取該變量的地址,其實就等於該域相對於0地址的偏移量。
(size_t)&(((type *)0)->field):將該地址(偏移量)轉化爲size_t型數據。
ANSI C標準允許任何值爲0的常量被強制轉換成任何一種類型的指針,並且轉換結果是一個NULL指針,因此((s*)0)的結果就是一個類型爲s*的NULL指針。如果利用這個NULL指針來訪問s的成員當然是非法的,但&(((s*)0)->m)的意圖並非想存取s字段內容,而僅僅是計算當結構體實例的首址爲((s*)0)時m字段的地址。聰明的編譯器根本就不生成訪問m的代碼,而僅僅是根據s的內存佈局和結構體實例首址在編譯期計算這個(常量)地址,這樣就完全避免了通過NULL指針訪問內存的問題。
注:
1.
有人這樣表達:
#define OFFSETOF(type, field) ((size_t) \
((char *)&((type *)0)->field - (char *)(type *)0))
我認爲效果是一樣的,多增加的那部分就是0地址,相減後就是偏移量。
2.
爲什麼要增加size_t呢?
首先size_t的定義是什麼呢,在文件stddef.h中可以找到答案。
typedef unsigned int size_t; /*mine is 32bit machine*/
可見就是將偏移量轉化爲無符整型,其實32位機器的地址就是無符號的32位整數。一般情況下,不進行size_t類型轉化也是沒有問題的(後面的實驗可證)。我認爲,只有偏移量足夠大,當大於0x80000000時纔有影響,因爲這時候的偏移量最高位是1,機器默認爲是負數了。似乎上面宏定義OFFSETOF中更能說明這個問題,因爲這個宏定義是一個差值,最高位是1就肯定是負數了。使用printf("%d", &var);打印一個變量的地址就是個負數。這只是我的看法,網上基本沒有什麼人分析爲什麼添加size_t的強制類型轉化。因爲系統對數組長度的大小是有限制的,所以也不能實驗得到數據。
插一句數組長度的問題(引述):
理論上來說沒有限制,但是內核一般配置允許每個進程擁有有限的內存空間,可以用系統調用函數getrlimit(int resource, struct rlimit *rlim)
獲得系統的資源限制。系統的資源限制分爲軟件限制和硬件限制,軟件限制最大值不能超過硬件限制。數組靜態獲得的存儲空間是分配在stack,只要知道stack的限制就知道答案了。可以使用如下代碼獲得:
struct rlimit resource_limit;
getrlimit(RLIMIT_STACK, &resource_limit);
printf("STACK: soft_limit - %ld hard_limit - %ld\n", resource_limit.rlim_cur, resource_limit.rlim_max);
分配大數量的數組,若是系統找不到該大小的一段連續的存儲空間,系統就會產生一個SIGSEGV信號,這時調用函數int sigaltstack(const stack_t *ss, stack_t *oss)來處理這個信號。sigaltstack儲存信號SIGSEGV到一個alternate stack結構ss中,內核會先於進程運行前檢查這個信號。
3.
由此,到一個結構體中field所佔用的字節數就很簡單了。
#define FIELD_SIZE(type, field) sizeof(((type *)0)->field)
4.
其實,系統給提供了一個相同的宏定義,在文件stddef.h中:
在嵌入式系統裏,不同開發商,不同架構處理器和編譯器都有不同的offsetof定義形式:
/* Keil 8051 */
#define offsetof(s,m) (size_t)&(((s *)0)->m)
/* Microsoft x86 */
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)
/* Motorola coldfire */
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))
/* GNU GCC 4.0.2 */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)
雖然定義形式不同,但功能都是返回成員在數據結構中的偏移量,都是爲了提高代碼的可移植性。
5.
offsetof雖然同樣適用於union結構,但它不能用於計算位域(bitfield)成員在數據結構中的偏移量。
typedef struct
{
unsigned int a:3;
unsigned int b:13;
unsigned int c:16;
}foo;
使用offset(foo,a)計算a在foo中的偏移量,編譯器會報錯。
6.應用(引述)
offsetof與EEPROM
我們許多人可能都使用過一些非揮發性的存儲器,如常見的EEPROM。我們經常使用它們在存儲一些系統的配置參數和設備信息。在所有的EEPROM中,通串過口訪問的佔了大多數。一般來說,對串口的訪問都是按字節進行的,這使得我們不可避免會設計出下面的接口去訪問EEPROM的信息:
/*從EEPROM 偏移量offset處讀取nBytes到RAM地址dest*/
ee_rd(uint16_t offset, uint16_t nBytes, uint8_t * dest);
然而,這種接口必須要知道偏移量offset和讀取字節數nBytes。可能你會採用下面的方法解決方法解決這個問題:
定義一個數據結構和一個指向這個數據結構的指針,並初始化這個指針爲EEPROM的起始地址EEPROM_BASE.
#define EEPROM_BASE 0x0000000/*配置信息的起始地址*/
typedef struct
{
int i;
float f;
char c;
} EEPROM;
EEPROM * const pEE = EEPROM_BASE
ee_rd(&(pEE->f), sizeof(pEE->f), dest);
沒錯,這種方法的確可以達到訪問指定地址的信息。不過這種方法也存在下面的問題:
a.容易使代碼維護人員人誤以爲在ee_rd接口內部也存在EEPROM的數據結構。
b.當你編寫一些自己感覺良好編譯器不報錯的代碼,比如pEE->f = 3.2,你可能意想不到災難將要來臨。
c.這個接口沒有很好地體現EEPROM所隱含的硬件特性。
到這裏,有人可能會想到offsetof來解決這個問題:
#define offsetof(type, f) ((size_t) \
((char *)&((type *)0)->f - (char *)(type *)0))
typedef struct
{
int i;
float f;
char c;
} EEPROM;
ee_rd(offsetof(EEPROM,f), 4, dest);
如果讓編譯器來計算nBytes而不是我們自己給出那就更好了。這時,一定有人會馬上提到sizeof。可是怎麼使用呢,我們不能用sizeof(EEPROM.f)來計算nBytes吧?!因爲EEPROM是數據類型,不是對象,沒有辦法操作f域呀。
/*類似於offsetof的定義*/
#define SIZEOF(s,m) ((size_t) sizeof(((s *)0)->m))
ee_rd(offsetof(EEPROM, f), SIZEOF(EEPROM, f), &dest);
其實還可以精簡爲下面的最終形式:
#define EE_RD(M,D) ee_rd(offsetof(EEPROM,M), SIZEOF(EEPROM,M), D)
EE_RD(f, &dest);
哈哈,這樣我們只用傳遞兩個參數,不用再考慮應該從那裏讀取數據以及讀取多少的問題。
有人會說這種簡化都是建立在EEPROM_BASE爲0x0000000基礎之上的,可能會反問,如果配置信息不是從0地址開始的呢?
其實我們可以通過下面的方法解決。
#define EEPROM_BASE 0x00000a10
typedef struct
{
char pad[EEPROM_BASE];/*使數據結構的前EEPROM_BASE個字節填"空"*/
int i;
float f;
char c;
} EEPROM;
使用offsetof簡化EEPROM的串口訪問的確很妙。這裏還有一個很好的例子。在嵌入式應用中,我們時常將一些I/O寄存器映射到內存地址空間進行訪問。這種映射使原本複雜的寄存器訪問變得象訪問普通的RAM地址一樣方便。PowerPC 8250訪問外部的ROM控制器(ROM controller)的寄存器就是通過這種方式實現的。ROM控制器所有的寄存器被映射到從I/O寄存器空間基地址0x10000000(IO_BASE)偏移0x60000(ROMCONOffset)字節的一段內存。每個寄存器佔用四個字節,並有一個數據結構與它們對應。比如控制ROM控制器工作狀態的寄存器對應數據結構ROMCON_ROM_CONTROL,配置PCI總線A的寄存器對應數據結構ROMCON_CONFIG_A,下面先看看這些數據結構的定義:
#define IO_BASE 0x10000000
#define ROMCONOffset 0x60000
typedef unsigned int NW_UINT32;
typedef struct _ROMCON_CONFIG_A {
union {
struct {
UINT32 pad4:21; /* unused */
UINT32 pad3:2; /* reserved */
UINT32 pad2:5; /* unused */
UINT32 EnablePCIA:1;
UINT32 pad1:1; /* reserved */
UINT32 EnableBoot:1;
UINT32 EnableCpu:1; /*bit to enable cpu*/
} nlstruct;
struct {
UINT32 ConfigA;
} nlstruct4;
} nlunion;
} ROMCON_CONFIG_A, *PROMCON_CONFIG_A;
typedef struct _ROMCON_ROM_CONTROL {
union {
struct {
UINT32 TransferComplete:1;
UINT32 pad3:1; /* unused */
UINT32 BondPad3To2:2;
UINT32 Advance:3;
UINT32 VersaPortDisable:1;
UINT32 pad2:1; /* unused */
UINT32 FastClks:1;
UINT32 pad1:7; /* unused */
UINT32 CsToFinClks:2;
UINT32 OeToCsClks:2;
UINT32 DataToOeClks:2;
UINT32 OeToDataClks:3;
UINT32 CsToOeClks:2;
UINT32 AddrToCsClks:2;
UINT32 AleWidth:2;
} nlstruct;
struct {
UINT32 RomControl;
} nlstruct4;
} nlunion;
} ROMCON_ROM_CONTROL, *PROMCON_ROM_CONTROL;
typedef struct
{
ROMCON_CONFIG_A ConfigA;
ROMCON_CONFIG_B ConfigB;
ROMCON_ROM_CONTROL RomControl;
...
}ROMCON, *PROMCON;
---------------------------- <-IO_BASE:0x10000000
| | | | | | |...
----------------------------
| | | | | | |...
...
---------------------------- <-ROMCONOffset(ROMCON):0x60000
| | | | | | |...
---------------------------- <-ROMCON_ROM_CONTROL
...
----------------------------
那麼如何訪問ROMCON_ROM_CONTROL對應寄存器呢,比如ROMCON_ROM_CONTROL對應寄存器的VersaPortDisable位?
估計有人可能會這樣做:
事先定義成員RomControl(ROMCON中用ROMCON_ROM_CONTROL定義的實例)相對於ROMCON的偏移量,
#define ROMCONRomControlOffset 0x8
然後設計訪問ROM的接口如下:
/*讀取ROM控制器位於src位置的寄存器數據到dest*/
typedef unsigned long dword_t;
void rom_read(dword_t* src, uint32_t* dest);
void rom_write(dword_t* src, uint32_t* dest);
最後利用這個偏移量做下面的操作:
ROMCON_ROM_CONTROL tRomCtrl={0};
dword_t* pReg=(dword_t*)(IO_BASE+ROMCONOffset+ROMCONRomControlOffset);
rom_read(pReg,(uint32_t)*(&tRomCtrl));
/*查看寄存器的VersaPortDisable位,如果該位沒有啓用就啓用它*/
if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)
{
tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;
rom_write(pReg,(uint32_t)*(&tRomCtrl));
}
這樣做確實可以達到訪問相應寄存器的目的。但是,如果和ROM相關的寄存器很多,那麼定義、記憶和管理那麼多偏移量不是很不方便嗎?到這裏,如果你對前面關於offsetof還有印象的話,我想你可能會作下面的優化:
#define ROMCON_ADDR(m) (((size_t)IO_BASE+\
(size_t)ROMCONOffset+\
(size_t)offsetof(ROMCON,m))
ROMCON_ROM_CONTROL tRomCtrl={0};
dword_t* pReg=(dword_t*)ROMCON_ADDR(ConfigA);
rom_read(pReg,(uint32_t)*(&tRomCtrl));
/*查看寄存器的VersaPortDisable位,如果沒有啓動就啓動它*/
if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)
{
tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;
rom_write(pReg,(uint32_t)*(&tRomCtrl));
}
7.實驗
#include "stdio.h"
#define OFFSET(s, m) ((size_t)&(((s *)0)->m))
#define OFFset(s, m) (&(((s *)0)->m))
typedef unsigned int size;
typedef struct node
{
int aa;
unsigned char bb;
unsigned int cc[5];
unsigned char dd[8];
unsigned long ee;
int ff;
char gg;
int hh;
}nn;
typedef struct node2
{
char aa;
unsigned char bb;
unsigned char cc[8];
int dd;
int ee;
int ff;
unsigned int gg[5];
unsigned long hh;
}mm;
int main(int argc, char * argv[])
{
printf("OFFSET of aa=%d\n", OFFSET(nn, aa));
printf("OFFSET of bb=%d\n", OFFSET(nn, bb));
printf("OFFSET of cc=%d\n", OFFSET(nn, cc));
printf("OFFset=%d\n", OFFset(nn, cc));
printf("OFFSET of dd=%d\n", OFFSET(nn, dd));
printf("OFFSET of ee=%d\n", OFFSET(nn, ee));
printf("OFFSET of ff=%d\n", OFFSET(nn, ff));
printf("OFFSET of gg=%d\n", OFFSET(nn, gg));
printf("OFFSET of hh=%d\n\n", OFFSET(nn, hh));
printf("OFFSET of aa=%d\n", OFFSET(mm, aa));
printf("OFFSET of bb=%d\n", OFFSET(mm, bb));
printf("OFFSET of cc=%d\n", OFFSET(mm, cc));
printf("OFFset=%d\n", OFFset(mm, cc));
printf("OFFSET of dd=%d\n", OFFSET(mm, dd));
printf("OFFSET of ee=%d\n", OFFSET(mm, ee));
printf("OFFSET of ff=%d\n", OFFSET(mm, ff));
printf("OFFSET of gg=%d\n", OFFSET(mm, gg));
printf("OFFSET of hh=%d\n\n", OFFSET(mm, hh));
return 0;
}
通過GCC編譯、運行後,答案簡略爲:
0 4 8(8) 28 36 40 44 48
0 1 2(2) 12 16 20 24 44
結論:
1.
在32位機器中,long(long int)型數據佔32bit,和short一樣。long long是爲了支持64位數據而產生的,表示64bit的數據。
在64位機器中,long型數據佔64bit,short佔32bit。
2.
編譯器要求對齊,造成了結構體“空洞”。所以相同類型的數據還是靠近吧。空洞問題也可以解釋爲什麼結構體不支持比較(支持==),“空洞”中的隨機數據會導致失敗。
附(摘錄):
1. 字節對齊(byte alignment):
現代計算機中內存空間都是按照byte劃分的,從理論上講對於任何類型的變量的訪問都可以從任何地址開始。但是實際情況是在訪問特定類型變量的時候經常在特定的內存地址訪問,這就需要各種類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的牌坊,這就是對齊。
2. 對齊原因:
(1)有些CPU訪問沒有對齊的變量時候會發生錯誤。比如Motorola 68000不允許將16位的字存儲到奇數地址中, 將一個16位的字寫到奇數地址將引發異常。
(2)不對數據進行對齊,會在存取效率上帶來損失。比如:有些CPU從偶數地址存儲int數據,讀取時需要一個機器週期,從偶數地址存儲,就需要2個機器週期。
3.對齊規則:
實際上, 對於c中的字節組織, 有這樣的對齊規則:
(1) 結構體變量的首地址能夠被其最寬基本類型成員的大小所整除;
(2) 結構體每個成員相對於結構首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節(internal adding);
(3) 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最末一個成員之後加上填充字節(trailing padding)。
不同CPU的對其規則可能不同, 請參考手冊。爲什麼會有上述的限制呢? 理解了內存組織, 就會清楚了。
CPU通過地址總線來存取內存中的數據,32位的CPU的地址總線寬度既爲32位置, 標爲A[0:31]。在一個總線週期內,CPU從內存讀/寫32位。 但是CPU只能在能夠被4整除的地址進行內存訪問,這是因爲: 32位CPU不使用地址總線的A1和A2(比如ARM,它的A[0:1]用於字節選擇, 用於邏輯控制, 而不和存儲器相連,存儲器連接到A[2:31])。訪問內存的最小單位是字節(byte), A0和A1不使用, 那麼對於地址來說, 最低兩位是無效的,所以它只能識別能被4整除的地址了。 在4字節中,通過A0和A1確定某一個字節。
4. 字節對齊對程序的影響:
(32bit、x86、gcc)(所佔字節數char:1, short:2, int:4, long:4, float:4, double:8)
struct A
{
int a;
char b;
short c;
};
struct B
{
char b;
int a;
short c;
};
sizeof(strcut A) == 8
sizeof(struct B) == 12
5.修改默認對齊:
(1)編程時,使用#pragma pack()。
#pragma pack (2) /*指定按2字節對齊*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
sizeof(strcut C) == 8 == 2(b) + 4(a) + 2(c)
(2)修改編譯器的默認對齊方式。
6.編程注意情況:
(1)爲了字節對齊,顯示聲明冗餘變量。當然,不冗餘聲明,編譯器也會自動對齊。
struct A
{
char a;
char reserved[3]; /*使用空間換時間*/
int b;
};
(2)帶來隱患,尤其是對代碼移植的影響。
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;
p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最後兩句代碼,從奇數邊界去訪問unsigned short型變量,顯然不符合對齊的規定。
在x86上,類似的操作只會影響效率,但是在MIPS或者sparc上,可能就是一個error,因爲它們要求必須字節對齊。