首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C語言技巧:通過成員地址反推結構體首地址(container_of 宏原理)

     編輯:關於C語言

問題背景

給定結構體某個成員的地址,如何計算該結構體實例的起始地址?這是 Linux 內核中著名的 container_of 宏的核心應用場景,廣泛用於鏈表、設備驅動、對象模型等子系統。

核心原理:偏移量計算

關鍵宏定義

#define STRUCT_OFFSET(struct_name, member) \
    ((unsigned long)&((struct struct_name *)0)->member)

原理圖解

假設結構體定義:
struct stru_addr {
    int a;      // 偏移 0
    char b;     // 偏移 4
    int d;      // 偏移 8 (對齊)
    char c;     // 偏移 12
};

虛擬地址 0 強轉為結構體指針:
(struct stru_addr *)0
    │
    ├─> a @ 偏移 0
    ├─> b @ 偏移 4
    ├─> d @ 偏移 8
    └─> c @ 偏移 12  ← &((struct stru_addr*)0)->c == 12

因為基址為 0,成員地址 等於 其相對偏移量。此技巧利用編譯器常量折疊,零開銷獲取編譯期常量偏移。

完整可運行示例

#include <stdio.h>
#include <stddef.h>  // offsetof 宏標準定義

#define STRUCT_OFFSET(struct_name, member) \
    ((unsigned long)&((struct struct_name *)0)->member)

struct stru_addr {
    int a;
    char b;
    int d;
    char c;
};

int main(void) {
    struct stru_addr s;

    printf("s 起始地址: %p\n", (void *)&s);
    printf("s.a 地址:   %p\n", (void *)&s.a);
    printf("s.c 地址:   %p\n", (void *)&s.c);

    // 方法一:自定義宏
    unsigned long offset = STRUCT_OFFSET(stru_addr, c);
    printf("c 偏移量(自定義): %lu\n", offset);
    printf("反推起始地址:     %p\n", (void *)((char *)&s.c - offset));

    // 方法二:標準庫 offsetof (C89 起標準化,推薦使用)
    printf("c 偏移量(offsetof): %zu\n", offsetof(struct stru_addr, c));
    printf("反推起始地址:       %p\n", (void *)((char *)&s.c - offsetof(struct stru_addr, c)));

    return 0;
}

運行結果

s 起始地址: 0x7ffd12345670
s.a 地址:   0x7ffd12345670
s.c 地址:   0x7ffd1234567c
c 偏移量(自定義): 12
反推起始地址:     0x7ffd12345670
c 偏移量(offsetof): 12
反推起始地址:       0x7ffd12345670

Linux 內核 container_of 宏實現

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

用法示例

struct list_head {
    struct list_head *next, *prev;
};

struct my_struct {
    int data;
    struct list_head list;  // 嵌入鏈表節點
};

// 已知 list 節點地址,獲取包含結構體指針
struct list_head *pos = ...;
struct my_struct *p = container_of(pos, struct my_struct, list);

關鍵技術點總結

技術點說明
零地址強轉利用編譯器計算成員偏移,無運行時開銷
對齊自動處理編譯器按 ABI 規則插入填充字節,偏移量已含對齊
offsetof 宏標準 C 庫提供,定義於 <stddef.h>,優先使用
指針算術用 char* 按字節偏移,避免類型大小影響
typeof (GNU)內核宏用 typeof 保持類型安全,標準 C 用 void* 轉換

應用場景

  • 內核鏈表:list_head 嵌入業務結構體,遍歷時反推宿主對象
  • 異構容器:通用節點掛載不同類型對象
  • 設備驅動:cdev、platform_device 等嵌入模型
  • C 語言模擬繼承:基類結構體嵌入派生類首部

注意事項

  • 成員必須是直接成員,不能是嵌套結構體的成員(需層層反推)
  • 結構體不能包含虛函數表指針(C++ 多態),否則佈局不兼容
  • 嚴格別名規則下,char* 字節訪問是合法的
  • 現代 C 建議直接用 offsetof,可讀性更好且標準化

這個技巧展示了 C 語言「指針即地址、結構體即內存佈局」的底層本質,是系統級編程的核心內功之一。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved