首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >吊打面试官:从 szieof 对齐到Linux kernel 宏函数container_of

吊打面试官:从 szieof 对齐到Linux kernel 宏函数container_of

作者头像
早起的鸟儿有虫吃
发布2025-11-20 15:49:57
发布2025-11-20 15:49:57
890
举报

1. 什么是offsetof

1.1 作用

offsetof是 C/C++ 中的一个宏,用于获取结构体/类中成员的字节偏移量。

代码语言:javascript
复制

#define offsetof(type, member) /* 编译器实现 */
[1] https://en.cppreference.com/w/cpp/types/offsetof.html

返回指定成员在结构体/类中的字节偏移量

满足每个成员的对齐要

1.2 使用限制

If type is not a PODT ype(until C++11)standard-layout type(since C++11), the result of offsetof is undefined

在 C++ 中,POD(Plain Old Data) 类型指的是与 C 语言中的结构体和基础类型行为类似的类型。

POD 类型具有非常简单的内存模型,可以在没有额外复杂语义的情况下被安全地拷贝、传递、初始化等。它的内存布局在不同的编译器和平台上是可预见的,符合 C 语言的内存模型

类型要求:必须是标准布局类型(standard-layout type)

代码语言:javascript
复制
//判断是否 pod 或者标准 布局
https://en.cppreference.com/w/cpp/types/is_standard_layout.html
https://en.cppreference.com/w/cpp/types/is_pod.html
#include <type_traits>
 
struct A { int m; };
static_assert(std::is_standard_layout_v<A>  true);
 
class B: public A { int m; };
static_assert(std::is_standard_layout_v<B>  false);
 
struct C { virtual void foo(); };
static_assert(std::is_standard_layout_v<C> == false);
 
int main()
{}

POD 类型的规则细节

不能有自定义的构造函数、析构函数或拷贝构造函数。

不能有虚函数或虚基类。

成员变量的访问控制符(publicprivateprotected)不能混合使用,即成员变量的访问权限必须一致。

不能有带有初始值的非静态数据成员。

POD 类型的意义和用途

POD 类型的概念非常重要,特别是在需要与 C 语言兼容的场合。它确保类型在内存中的布局是可预见的,可以用于:

与 C 语言互操作:POD 类型与 C 语言的结构体兼容,可以用于跨语言调用。

二进制序列化:POD 类型的内存布局非常简单,可以直接进行二进制的序列化和反序列化。

高效的拷贝和赋值:POD 类型可以进行位拷贝,不需要调用复杂的构造函数或析构函数,因此拷贝操作效率很高。

1.3 举例

代码语言:javascript
复制
#include <cstddef>
#include <iostream>

struct S 
{
    char m0;     // 偏移量 0
    double m1;   // 偏移量 8(可能有填充)
    short m2;    // 偏移量 16
    char m3;     // 偏移量 18
//private:
   //  private: int z; // warning: 'S' is a non-standard-layout type
};

int main() {
    std::cout << "offset of char m0 = " << offsetof(S, m0) << '\n'
              << "offset of double m1 = " << offsetof(S, m1) << '\n'
              << "offset of short m2 = " << offsetof(S, m2) << '\n'
              << "offset of char m3 = " << offsetof(S, m3) << '\n';
}

输出示例

代码语言:javascript
复制
offset of char m0 = 0offset of double m1 = 8offset of short m2 = 16offset of char m3 = 18

1.4 疑问 为什么是 24 而不是成员大小之和(1 +8 +2 +1 =12)

步骤分析

1

成员各自大小

m0 :类型 char → 大小 1 字节

m1 :类型 double → 大小 8 字节

m2 :类型 short → 大小 2 字节

m3 :类型 char → 大小 1 字节 成员大小加起来 = 1 + 8 + 2 + 1 = 12 字节

2

对齐要求 & 中间填充

double 通常要求 8 字节对齐。因为 m0 放在偏移 0(占 1 字节),下一个成员 m1 若直接放在偏移 1,会违反其 8 字节对齐要求。

因此编译器会插入中间填充字节,将 m1 安放在偏移 8。也就是在偏移 1–7 处插入 7 字节填充。

接下来,m1 占偏移 8–15(共 8 字节)。

然后 m2:要求 2 字节对齐,下一个可用偏移是 16 → 符合对齐;占 16–17 (2 字节)

然后 m3:要求 1 字节对齐,下一个可用偏移是 18 → 放在 18,大小 1 字节

3

尾部填充(尾对齐)

整个结构体的对齐要求通常是其 “最大成员对齐要求” 的值,即这里为 8 字节(因为有一个 double)。

最后一个成员结束在偏移 18 +1 = 19 字节位置。若结构体没有尾部填充,那么数组中第二个元素可能从偏移 19 开始,这将不能保证双精度类型在其自身元素内对齐。

因此,为了保证每个 S 在数组中起始位置也满足按 8 字节对齐,编译器会将结构体整体尺寸向上填充到 8 的倍数。下一个 ≥19 的 8 的倍数是 24。

所以在偏移 19-23 之间插入尾部填充 5 字节,使得 sizeof(S) = 24 字节

1.5 更多举例对齐用法

示例 1

代码语言:javascript
复制
struct A {
    char  c;   // 1 字节
    int   i;   // 4 字节
    char  d;   // 1 字节
};

偏移地址      内容       说明
----------------------------------
0            c          char 占 1 字节
1            padding    填充 3 字节,使下一个成员 i 按 4 字节对齐
2            padding
3            padding
----------------------------------
4~7          i          int 占 4 字节
----------------------------------
8            d          char 占 1 字节
9            padding    填充,为保证整体结构体大小是 4 的倍数
10           padding
11           padding
----------------------------------
sizeof(A) = 12 字节

分析

成员大小之和 = 1 + 4 + 1 = 6 字节。

假设 int 要 4 字节对齐。于是布局可能是:

c 放在偏移 0(占 1 字节)

插入填充 3 字节,使下一个 i 从偏移 4 开始

i 占偏移 4-7(4 字节)

接着 d 放在偏移 8(1 字节)

然后尾部可能填充,使整个结构体大小为 12

最终你可能发现 sizeof(A) == 12

示例 2

代码语言:javascript
复制
struct B {
    double d;  // 8 字节
    char   c;  // 1 字节
    short  s;  // 2 字节
};

分析

代码语言:javascript
复制

代码:github.com:watchpoints/master-cpp.git

#include <stdio.h>

#include <stddef.h>

struct B 
{

	double d;

	char c;

	short s;

};



int main()
{

	printf("Sizeof(struct B) = %zu\n", sizeof(struct B));

	printf("Offset of d: %zu\n", offsetof(struct B, d));

	printf("Offset of c: %zu\n", offsetof(struct B, c));

	printf("Offset of s: %zu\n", offsetof(struct B, s));

	return 0;

}

/*

Sizeof(struct B) = 16

Offset of d: 0

Offset of c: 8

Offset of s: 10

*/

2. Linux kernel 宏函数container_of

2.1 用法和使用场景

container_of 是kernel代码中利用C语言特性做的一个精妙设计。

它的作用顾名思义,就是已知某个变量,通过这个变量找到包含它的结构体变量。

作用:通过结构体的某个成员变量地址找到该结构体的首地址

代码语言:javascript
复制
/**  
 * container_of - cast a member of a structure out to the containing structure  
 * @ptr:    the pointer to the member.  
 * @type:   the type of the container struct this is embedded in.  
 * @member: the name of the member within the struct.  
 *  
 */
#define container_of(ptr, type, member) ({          \  
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \  
    (type *)( (char *)__mptr - offsetof(type,member) );})

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/container_of.h?h=v5.16.3

参数

含义

示例说明

ptr

指向结构体成员的指针

比如,一个指向链表节点 list_head *node的指针。

type

该成员所属的整个结构体的类型

比如,包含该链表节点的自定义结构体 struct my_data。

member

该成员在结构体内部的名字

比如,链表节点在 struct my_data中定义的字段名 list。

例子:

container_of 宏最经典的应用是在 Linux 内核的链表实现中

内核实现了一种侵入式链表,链表节点被嵌入到业务数据结构中

代码语言:javascript
复制
#include <linux/container_of.h>
//定义宿主结构体(其中可以包含任意个成员,且成员可以是任意排列顺序)
struct demo {
    char *name;
    struct list_head list; // 链表节点嵌入其中
};

//从list获取name
void demo_list_to_name(struct list_head *lst)
{
    struct demo *dm = container_of(lst, strcut demo, list);
    printk(KERN_INFO "%s\n", dm->name);
}

//通过给子函数传递demo结构体中的list的指针,找到demo中的name变量
void demo_main(void)
{
    struct demo dm;
    dm.name = "Demo";
    demo_list_to_name(&dm->list);
}

参考

1

https://en.cppreference.com/w/cpp/types/offsetof.html

2

https://stackoverflow.com/questions/119123/why-isnt-sizeof-for-a-struct-equal-to-the-sum-of-sizeof-of-each-member

3

https://zhuanlan.zhihu.com/p/463588640

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-11-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 什么是offsetof
    • 1.1 作用
    • 1.2 使用限制
      • POD 类型的意义和用途
    • 1.3 举例
    • 1.4 疑问 为什么是 24 而不是成员大小之和(1 +8 +2 +1 =12)
      • 步骤分析
    • 1.5 更多举例对齐用法
      • 示例 1
      • 示例 2
  • 2. Linux kernel 宏函数container_of
    • 2.1 用法和使用场景
    • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档