【數據結構】31_老生常談的兩個宏(Linux)

Linux 內核中經常使用的兩個宏定義

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

#ifndef container_of
#define container_of(ptr, type, member) ({                  \
        const typeof(((type*)0)->member)* __mptr = (ptr);   \
        (type*)((char*)__mptr - offsetof(type, member));})
#endif

見招拆招 - 第一式:編譯器作了什麼?

offsetof 用於計算 TYPE 結構體中 MEMBER 成員的偏移位置
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif
  • 編譯器清楚的知道結構體成員的偏移位置
  • 經過結構體變量首地址與偏移量定位成員變量

編程實驗: offsetof 原理剖析

#include <stdio.h>

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

struct ST
{
    int i;  // 0
    int j;  // 4
    char c; // 8
};

void func(struct ST *pst)
{
    int *pi = &(pst->i);    // (unsigned int)pst + 0
    int *pj = &(pst->j);    // (unsigned int)pst + 4
    char *pc = &(pst->c);   // (unsigned int)pst + 8

    printf("pst = %p\n", pst);
    printf("pi  = %p\n", pi);
    printf("pj  = %p\n", pj);
    printf("pc  = %p\n", pc);
}

int main()
{
    struct ST s = {0};

    func(&s);

    printf("---------\n");

    func(NULL);

    printf("---------\n");

    printf("offset i : %d\n", offsetof(struct ST, i));
    printf("offset j : %d\n", offsetof(struct ST, j));
    printf("offset c : %d\n", offsetof(struct ST, c));

    return 0;
}

結論

計算成員變量與其結構體變量首地址的偏移量編程

見招拆招 - 第二式:({})

  • ({}) 是 GNU 編譯器的語法擴展
  • ({}) 與逗號表達時相似,結果爲最後一個語句的值
#include <stdio.h>

void method_1()
{
    int a = 0;
    int b = 0;

    int r = (
                a = 1,
                b = 2,
                a + b
            );

    printf("r = %d\n", r);
}

void method_2()
{
    int r = ({
                 int a = 1;
                 int b = 2;
                 a + b;
             });

    printf("r = %d\n", r);
}

int main()
{
    method_1();
    method_2();

    return 0;
}

輸出:安全

r = 3
r = 3

見招拆招- 第三式: typeof 是一個關鍵字嗎?

  • typeof 是 GUN C 編譯器的特有關鍵字
  • typeof 只在編譯期生效,用於獲得變量的類型
#include <stdio.h>

int main()
{
    int i = 100;
    typeof(i) j = i;
    const typeof(j) *p = &j;

    printf("sizeof(j) = %d\n", sizeof(j));
    printf("j = %d\n", j);
    printf("*p = %d\n", *p);

    return 0;
}

輸出:測試

sizeof(j) = 4
j = 100
*p = 100

見招拆招 - 第四式:最後的原理

image.png

數學關係:spa

pc = p + offset

==>指針

size_t offset = offsetof(struct ST, c);
struct ST *p = (struct ST*)((char*)pc - offset);

編程實驗:container_of 原理剖析

#include <stdio.h>

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

#ifndef container_of
#define container_of(ptr, type, member) ({                  \
        const typeof(((type*)0)->member)* __mptr = (ptr);   \
        (type*)((char*)__mptr - offsetof(type, member));})
#endif

struct ST
{
    int i;
    int j;
    char c;
};

int main()
{
    struct ST s = {0};
    char *pc = &s.c;

    struct ST *pst = container_of(pc, struct ST, c);

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

輸出:code

&s = 0061FEB8
pst = 0061FEB8

結論

根據成員變量地址推導結構體變量首地址blog


思考:

const typeof(((type*)0)->member)* __mptr = (ptr);
下面這一段代碼測試證實沒有這一行也能夠獲得正確的結果,那它到底有什麼做用呢?編譯器

#include <stdio.h>

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

#ifndef container_of
#define container_of(ptr, type, member) ({                  \
        const typeof(((type*)0)->member)* __mptr = (ptr);   \
        (type*)((char*)__mptr - offsetof(type, member));})
#endif

#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)ptr - offsetof(type, member)))
#endif

struct ST
{
    int i;
    int j;
    char c;
};

int main()
{
    struct ST s = {0};
    char *pc = &s.c;

    struct ST *pst = container_of_new(pc, struct ST, c);  // 注意!!!

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

輸出:數學

&s = 0061FEBC
pst = 0061FEBC
答疑 1: 爲了可以提供微弱但很重要的類型安全檢查

由於 continer_of 只能用宏來實現,但宏是由預處理器處理,僅進行簡單的文本替換,不會進行任何的類型檢查。這就有可能致使在編寫代碼時,因爲疏忽傳遞了錯誤的類型指針而編譯器不發出任何警告。it

測試:

int main()
{
    struct ST s = {0};

    int e = 0;
    int *pe = &e;

    struct ST *pst = container_of_new(pe, struct ST, c);

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

輸出:【編譯無錯誤,無警告,但運行結果錯誤】

&s = 0061FEBC
pst = 0061FEB0
int main()
{
    struct ST s = {0};

    int e = 0;
    int *pe = &e;

    struct ST *pst = container_of(pe, struct ST, c);

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

編譯輸出:

warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
         const typeof(((type*)0)->member)* __mptr = (ptr);   \
                                                    ^
答疑 2: 爲何使用 ({})

當定義指針變量時,一行代碼再也沒法完成 continer_of 的總體功能(逗號表達式中不能夠定義變量),因而使用了 GNU 的擴展語法({})分多行定義局部變量。

答疑 3: 此行代碼是怎樣獲取成員變量的類型的呢?

typeof(((type*)0)->member)

小結

  • 編譯器清楚的知道結構體成員變量的偏移位置
  • ({}) 與逗號表達式類型,結果爲最後一個語句的值
  • typeof 只在編譯期生效,用於獲得變量的類型
  • container_of 使用 ({}) 進行類型安全檢查

以上內容整理於狄泰軟件學院系列課程,請你們保護原創!

相關文章
相關標籤/搜索