相信你們在c語言程序開發的過程必定都使用過結構體,那麼不知你對結構體中成員變量偏移這塊是如何理解的?本文將和你們一塊兒分享下,本人最近關於c語言中結構體偏移的一些思考和總結。html
另外這篇博文還能夠幫你更好的理解這個問題c語言中兩種宏定義的區別,關於這個思考有哪些方面的意義,細心的你可能發現本文所屬的類別爲linux內核設計與實現,而並不是 GNU C語言編程,可能有些同窗會有些許好奇。不過不用着急,若是對本篇博文意義感興趣的同窗,可繼續關注後續的博文,會有進一步的闡述。node
咱們先來定義一下需求:linux
已知結構體類型定義以下:程序員
struct node_t{ char a; int b; int c; };
且結構體1Byte對齊算法
#pragma pack(1)
求:編程
結構體struct node_t中成員變量c的偏移。函數
注:這裏的偏移量指的是相對於結構體起始位置的偏移量。spa
看到這個問題的時候,我相信不一樣的人腦中浮現的解決方法可能會有所差別,下面咱們分析如下幾種可能的解法:.net
若是你對c語言的庫函數比較熟悉的話,那麼你第一個想到的確定是offsetof函數(其實只是個宏而已,先姑且這樣叫着吧),咱們man 3 offsetof查看函數原型以下:設計
#include <stddef.h> size_t offsetof(type, member);
有了上述的庫函數,咱們用一行代碼就能夠搞定:
offsetof(struct node_t, c);
固然這並不是本文探討的重點,請繼續閱讀。
當咱們對c語言的庫函數不熟悉的時候,此時也不要着急,咱們依然可使用咱們本身的方法來解決問題。
最直接的思路是:【結構體成員變量c的地址】 減去 【結構體起始地址】
咱們先來定義一個結構體變量node:
struct node_t node;
接着來計算成員變量c的偏移量:
(unsigned long)(&(node.c)) - (unsigned long)(&node)
&(node.c)爲結構體成員變量c的地址,並強制轉化爲unsigned long;
&node爲結構體的起始地址,也強制轉化爲unsigned long;
最後咱們將上述兩值相減,獲得成員變量c的偏移量;
按照方法2的思路咱們在不借助庫函數的狀況下,依然能夠獲得成員變量c的偏移量。但做爲程序員,咱們應該善於思考,是否是能夠針對上面的代碼作一些改進,使咱們的代碼變得更簡潔一些?在作具體的改進以前,咱們應該分析方法2存在哪些方面的問題。
相信不用我多說,細心的你必定已經察覺到,方法2中最主要的一個問題是咱們自定義了一個結構體變量node,雖然題目中並未限制咱們能夠自定義變量,但當咱們遇到比較嚴且題目中不容許自定義變量的時候,此時咱們就要思考新的解決方法。
在探討新的解決方法以前,咱們先來探討一個有關偏移的小問題:
小問題
這是一道簡單的幾何問題,假設在座標軸上由A點移動到B點,如何計算B相對於A的偏移?這個問題對於咱們來講是很是的簡單,可能大部分人都會脫口而出並獲得答案爲B-A。
那麼這個答案是否徹底準確呢?比較嚴謹的你以爲顯然不是,緣由在於,當A爲座標原點即A=0的時候,上述答案B-A就直接簡化爲B了。
這個小小的簡單的問題,對於咱們來講有什麼啓示呢?
咱們結合方法2的思路和上述的小問題,是否是很快就獲得了下面的關聯:
(unsigned long)(&(node.c)) - (unsigned long)(&node)
和
B - A
咱們小問題的思路是當A爲座標原點的時候,B-A就簡化爲B了,那麼對應到咱們的方法2,當node的內存地址爲0即(&node==0)的時候,上面的代碼可簡化爲:
(unsigned long)(&(node.c))
因爲node內存地址==0了,因此
node.c //結構體node中成員變量c
咱們就可使用另一種方式來表達了,以下:
((struct node_t *)0)->c
上述代碼應該比較好理解,因爲咱們知道結構體的內存地址編號爲0,因此咱們就能夠直接經過內存地址的方式來訪問該結構體的成員變量,相應的代碼的含義就是 獲取內存地址編號爲0的結構體struct node_t的成員變量c。
注:此處只是利用了編譯器的特性來計算結構體偏移,並未對內存地址0有任何操做,有些同窗對此可能還有些疑問,詳細的瞭解該問題可參考關於c語言結構體成員變量訪問方式的一點思考。
此時,咱們的偏移求法就消除了struct node_t node這個自定義變量,直接一行代碼解決,:
(unsigned long)(&(((struct node_t *)0)->c))
上述的代碼相對於方法2是否是更簡潔了一些。
這裏咱們將上面的代碼功能定義爲一個宏,該宏的做用是用來計算某結構體內成員變量的偏移(後面的示例會使用該宏):
#define OFFSET_OF(type, member) (unsigned long)(&(((type *)0)->member))
使用上面的宏,就能夠直接獲得成員變量c在結構體struct node_t中的偏移爲:
OFFSET_OF(struct node_t, c)
和示例1同樣,咱們先定義需求以下:
已知結構體類型定義以下:
struct node_t{ char a; int b; int c; };
int *p_c,該指針指向struct node_t x的成員變量c
結構體1Byte對齊
#pragma pack(1)
求:
結構體x的成員變量b的值?
拿到這個問題的時候,咱們先作一下簡單的分析,題目的意思是根據一個指向某結構體成員變量的指針,如何求該結構體的另一個成員變量的值。
那麼可能的幾種解法有:
因爲咱們知道結構體是1Byte對齊的,因此這道題最簡單的解法是:
*(int *)((unsigned long)p_c - sizeof(int))
上述代碼很簡單,成員變量c的地址減去sizeof(int)從而獲得成員變量b的地址,而後再強制轉換爲int *,最後再取值最終獲得成員變量b的值;
方法1的代碼雖然簡單,但擴展性不夠好。咱們但願經過p_c直接獲得指向該結構體的指針p_node,而後經過p_node訪問該結構體的任意成員變量了。
由此咱們獲得計算結構體起始地址p_node的思路爲:
【成員變量c的地址p_c】減去【c在結構體中的偏移】
由示例1,咱們獲得結構體struct node_t中成員變量c的偏移爲:
(unsigned long)&(((struct node_t *)0)->c)
因此咱們獲得結構體的起始地址指針p_node爲:
(struct node_t *)((unsigned long)p_c - (unsigned long)(&((struct node_t *)0)->c))
咱們也能夠直接使用示例1中定義的OFFSET_OF宏,則上面的代碼變爲:
(struct node_t *)((unsigned long)p_c - OFFSET_OF(struct node_t, c))
最後咱們就可使用下面的代碼來獲取成員變量a,b的值:
p_node->a p_node->b
咱們一樣將上述代碼的功能定義爲以下宏:
#define STRUCT_ENTRY(ptr, type, member) (type *)((unsigned long)(ptr)-OFFSET_OF(type, member))
該宏的功能是經過結構體任意成員變量的指針來得到指向該結構體的指針。
咱們使用上面的宏來修改以前的代碼以下:
STRUCT_ENTRY(p_c, struct node_t, c)
p_c爲指向結構體struct node_t成員變量c的指針;
struct node_t結構體類型;
c爲p_c指向的成員變量;
注:
上述示例中關於地址運算的一些說明:
int a = 10; int * p_a = &a;
設p_a == 0x95734104;
如下爲編譯器計算的相關結果:
p_a + 10 == p_a + sizeof(int)*10 =0x95734104 + 4*10 = 0x95734144
(unsigned long)p_a + 10 == 0x95734104+10 = 0x95734114
(char *)p_a + 10 == 0x95734104 + sizeof(char)*10 = 0x95734114
從上述三種狀況,相信你應該能體會到我所要表達的意思了。(注:後續某博文將從編譯器的角度對該問題進行詳細的闡述)
本文經過幾個示例描述了c語言結構體有關偏移的一些有意思的事情,但願可以對你有所幫助。爲何會有上述思考,相信有些同窗已經看出一些端倪,這也正是後續博文將要描述的主題。
如文中有錯誤之處,歡迎指出。
[1] http://isis.poly.edu/kulesh/stuff/src/klist/
[2] https://www.kernel.org/doc/Documentation/CodingStyle
[3] http://www.kroah.com/log/linux/container_of.html
若是您對算法或編程感興趣,歡迎掃描下方二維碼並關注公衆號「算法與編程之美」,和您一塊兒探索算法和編程的神祕之處,給您不同的解題分析思路。