前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >C语言 | offsetof宏和container_of宏

C语言 | offsetof宏和container_of宏

作者头像
飞哥
发布于 2020-07-10 02:27:36
发布于 2020-07-10 02:27:36
2.2K00
代码可运行
举报
运行总次数:0
代码可运行

今天分享C语言中的两个宏,这两个宏包含了指针和结构体的知识,非常具有代表性。另外,这个题目曾经是大疆无人机的一道笔试题,可见,这两个宏对C语言基础还是有一定要求的。先说明一下,今天所有的例子都是以32位系统来说的。

废话不多说,今天要说的两个宏分别是offsetof和container_of,第一个宏是用来计算结构体中某个成员相对于结构体的偏移量,第二个宏是已知指向结构体某个成员的指针,来计算结构体的指针。来看一下它们的原型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#define container_of(ptr, type, member) ({            \
    const typeof(((type *)0)->member) * __mptr = (ptr); \
    (type *)((char *)__mptr - offsetof(type, member)); })

下面分别来介绍一下这两个宏。

一、offsetof

这个宏是用来计算结构体某个成员的偏移量的,所以我们先来定义一个简单的结构体类型,来说明。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct mystruct
{
    char a;         // 0
    int b;          // 4
    short c;        // 8
};

利用这个机构体类型定义一个结构体变量

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct mystruct  s;

在这个结构体当中,有三个成员变量,本来char类型是占一个字节,int类型是占4个字节,short类型是占2个字节,一共占7个字节,但是根据结构体的三个对齐原则,我们知道在这里,char占了4个字节,int占了4个字节,short占了4个字节(关于结构体对齐原则不是今天介绍的重点,所以不多介绍)。所以c实际上的偏移量是8,而不是5。这里因为结构体的成员很少,且类型不复杂,所以可以自己手动算出来,但是如果结构体更复杂一些,我们就不可能自己手动去算了,那有什么好的办法呢?

我们知道,C语言给我们提供了一个很好的方式去访问结构体成员,比如结构体变量我们可以用点.去访问,结构体指针我们可以用->去访问,这两种访问方式本质上是通过指针进行访问的,只不过这个过程是编译器帮我们处理了。

比如我们要给变量c赋值,我们可以用简单的方法:

s.c=12;

我们也可以用指针的方法:

short *p=(short*)((int)&s+8);

*p=12;

显然第二种方法要麻烦的多,并且要自己计算偏移量,还要知道变量类型,所以C语言帮我们考虑了这一点,使用简单的点的方式就行了。

既然C语言帮我们做了计算偏移量这件事情,那我们是不是可以反过来利用一下它,先通过点的方式访问变量,再对变量进行取地址运算,减去结构体首地址不就是变量的偏移量了吗?如果首地址是0的话就更好了,直接取地址之后就是偏移量了。

没错,这就是这个宏的思路。

(TYPE *)0:就是将地址0转化为TYPE类型的指针;

((TYPE *)0)->MEMBER通过结构体指针来访问结构体变量;

&((TYPE *)0)->MEMBER对结构体变量进行取地址运算;

((int) &((TYPE*)0)->MEMBER)最后将地址转化为整形,这一步其实可以省略,看你是需要返回整形还是直接返回地址。

我们可以做个简单的实验来验证这一点

二、container_of

上面介绍了offsetof宏的使用,相信不是那么难理解,那么这个宏就看起来复杂多了,但是,其实只要把思路理清楚了,也不是那么复杂。这个宏我在VC6.0编译器上编译的时候是会报错的,其中的typeof这个关键字它就不认识,因此没法做实验,但是在gcc编译器上是可以的,估计因为这个原因,使用的会更少一些,但是这没关系,重要的在于我们能够理解它的原理。

下面是我用这个宏在gcc上做的实验:

这个宏的作用是已知某个结构体成员变量的指针,反过来得到结构体的地址。其实有了上面的那个基础,这个会更简单一些。

既然有了指向结构体成员变量的指针,那么也就是说知道了这个变量的地址,如果我们又知道了这个变量的偏移量,那么利用这个变量的地址减去它的偏移量不就知道结构体地址了吗?没错,它的思路就是这么简单。

下面将这个宏拆分来理解:

((type *)0):将地址0转化为type类型的指针;

((type *)0)->member:通过指针访问member这个成员;

typeof(((type*)0)->member):获取member这个成员的数据类型;

typeof(((type*)0)->member) * __mptr:利用获取的这个类型来定义一个指针变量;

typeof(((type*)0)->member) * __mptr = (ptr);利用上面定义的指针来指向你已知的那个结构成员。

((char *)__mptr -offsetof(type, member));将指针转化为char类型,并且减去偏移量。这里要注意的就是这个偏移量是int类型的,上面说到计算偏移量时可以不强制转化为int型,但是这里做加减时就必须转化为int型了,因为char*类型不能和指针相加减,只能和数字相加减。

(type *)((char *)__mptr -offsetof(type, member));最后将获取的指针再转化为type类型。

可能前面几句可以理解,后面就糊涂了。其实也不难理解,我举个简单的例子。

int *p=&b; //p指向成员b;

((char *)p-4) //p减去偏移量4,不就是结构体地址了吗,只不过这个是char*类型的指针,如果要将它还原成结构体,还得再强制类型转化一次。

(struct mystruct*) ((char *)p-4);

可能还是有些人不理解为什么要先转化成char*类型之后再减4,那么这就涉及到指针的加减问题了。

我们知道,在内存当中,是按字节为单位来编址的,可以想象为一个字节就是一个个的小格子,每个小格子都有一个编号,这个编号实际上就是地址。如果我们定义一个 int *p;那么,每次p加一或者减一都是移动四个字节,而如果定义一个 char *p,那么每次p加一或者减一都是移动一个字节,换句话说,p一次移动的字节数就是它指向的变量的类型所占的字节数。上面因为返回的偏移量是以字节为单位的,所以必须先转化为char*类型才能加减,加减完之后再转化为你需要的类型。

实际上,也不是一定要转化成char*类型的,我们可以将地址先转化为int类型的,加减完之后再转化为指针,这两种方式都是一样的。我们可以做一个小实验来证明这一点

我们可以发现,这两种方法都可以准确的还原结构体的地址。

代码清单:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <stdio.h>

struct mystruct
{
    char a;         // 0
    int b;          // 4
    short c;        // 8
    int d;          //c
};


#define offsetof(TYPE, MEMBER)   ((int) &((TYPE *)0)->MEMBER)


int main(void)
{
    struct mystruct  s;
    printf("s的地址为:%p\n",&s);
    int *p;
    p=&(s.d);
    printf("p的地址为:%p\n",p);
    printf("p的偏移量%d\n",offsetof(struct mystruct,d));   
    printf("方式一得到的s的地址为:%p\n", (struct mystruct *)  ( (char*)p-offsetof(struct mystruct,d)));
    printf("方式二得到的s的地址为:%p\n", (struct mystruct *)  ( (int)p-offsetof(struct mystruct,d)));

    return 0;
}

以上就是今天要分享的内容,实际上,这些内容所涉及到的东西是很多的,比如指针,结构体,强制类型转化等等。但是这些归根结底来说,是对内存和数据类型要有非常深刻的理解。必须要先搞清楚什么是内存,还有数据类型的含义到底是什么,变量是什么,变量和数据类型的关系,才能理解上面说的东西,否则的话只是表面懂了,稍微变化一下就不知道怎么办了。

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

本文分享自 电子技术研习社 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
剖析c语言结构体的高级用法(二)
昨天分享了结构体里面的一些常见用法(因为测试代码测试的有点晚,有些地方没有分享完。),今天我们来继续分享结构体里面的其他用法。
用户6280468
2022/03/21
5080
剖析c语言结构体的高级用法(二)
Linux内核中container_of宏的详细解释
  我们可以看到,结构体的地址和结构体第一个成员的地址是相同的。这也就是我们之前在拒绝造轮子!如何移植并使用Linux内核的通用链表(附完整代码实现)中提到的为什么在结构体中要把 struct list_head放在首位。
嵌入式与Linux那些事
2021/05/20
1.3K0
Linux内核中container_of宏的详细解释
container_of宏定义作用_宏内核
上一篇我们讲到内核链表和普通链表的区别,就有小伙伴追问:内核链表是怎么通过指针域来访问数据域的呢?这篇文章我们就来解答这个问题。
全栈程序员站长
2022/09/23
1.3K0
container_of宏定义分析
其作用是:已知某结构体的成员member和指向该成员的指针ptr(也就是member的地址),算出该结构体的起始地址。
知否知否应是绿肥红瘦
2025/02/19
900
container_of宏定义分析
container of()函数用法简介
在Linux 内核编程中,会经常见到一个宏函数container_of(ptr,type,member)。已知结构体type的成员member的地址ptr,求结结构体type的起始地址。
无刺鱼
2022/03/29
1.6K0
container of()函数用法简介
Linux内核第一宏
list_entry()有着内核第一宏的美称,它被设计用来通过结构体成员的指针来返回结构体的指针。现在就让我们通过一步步的分析,来揭开它的神秘面纱,感受内核第一宏设计的精妙之处。
Linux阅码场
2019/09/02
1.5K0
Linux内核第一宏
Linux内核中container_of函数详解
在Linux 内核中,container_of 函数使用非常广,例如 Linux内核链表 list_head、工作队列work_struct中 在Linux 内核中有一个大名鼎鼎的宏container
小小科
2018/05/04
2.2K0
Linux内核中container_of函数详解
offsetof()和container_of()函数
在linux 内核编程中,会经常见到一个宏函数container_of(ptr,type,member), 但是当你通过追踪源码时,像我们这样的一般人就会绝望了(这一堆都是什么呀?函数还可以这样定义???怎么还有0呢??? 哎,算了,还是放弃吧。。。)。这就是内核大佬们厉害的地方,随便两行代码就让我们怀疑人生,凡是都需要一个过程,慢慢来吧。
用户6280468
2022/04/18
3250
offsetof()和container_of()函数
contain_of宏定义
 Container_of在Linux内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址。 实现方式:   container_of(ptr, type, member) ;    其实它的语法很简单,只是一些指针的灵活应用,它分两步:     第一步,首先定义一个临时的数据类型(通过typeof( ((type *)0)->member )获得)与ptr相同的指针变量__mptr,然后用它来保存ptr的值。  
233333
2018/03/07
1.1K0
offset宏定义_vba offset 用法
函数作用:计算结构体成员的偏移,有些自有代码里也会手写这样的代码,实际上这个函数是标准实现的。实际上如果我们浏览 ANSI C 编译器的标头文件,将在 stddef.h 中遇到这样奇怪的宏。这个红具有可怕的声明。此外,如果您查阅编译器手册,您会发现一个无益的解释,上面写着如下:
全栈程序员站长
2022/09/23
6080
【Linux API 揭秘】container_of函数详解
container_of可以说是内核中使用最为频繁的一个函数了,简单来说,它的主要作用就是根据我们结构体中的已知的成员变量的地址,来寻求该结构体的首地址,直接看图,更容易理解。
董哥聊技术
2023/12/15
4190
【Linux API 揭秘】container_of函数详解
【C语言指南】offsetof宏的介绍 及其实现
MY_offsetof宏会返回结构体S中成员m的偏移量,这个偏移量是从结构体的起始地址到成员m的地址之间的距离(以字节为单位)。
倔强的石头
2024/12/06
1390
【C语言指南】offsetof宏的介绍 及其实现
C指针的这些使用技巧,掌握后立刻提升一个Level
半个月前写的那篇关于指针最底层原理的文章,得到了很多朋友的认可(链接: C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻),特别是对刚学习C语言的小伙伴来说,很容易就从根本上理解指针到底是什么、怎么用,这也让我坚信一句话;用心写出的文章,一定会被读者感受到!在写这篇文章的时候,我列了一个提纲,写到后面的时候,发现已经超过一万字了,但是提纲上还有最后一个主题没有写。如果继续写下去,文章体积就太大了,于是就留下了一个尾巴。
IOT物联网小镇
2021/05/13
5100
C指针的这些使用技巧,掌握后立刻提升一个Level
002 Linux内核中双向链表的经典实现
本文对双向链表进行探讨,介绍的内容是Linux内核中双向链表的经典实现和用法。其中,也会涉及到Linux内核中非常常用的两个经典宏定义offsetof和container_of。内容包括: 1.Linux中的两个经典宏定义 2.Linux中双向链表的经典实现
范蠡
2019/07/10
1.8K0
002 Linux内核中双向链表的经典实现
赵晨雨: 从微观角度来看linux内核设计
最近总结出来学习内核有两个大的角度,一种就是从宏观角度来看,总的来说就是顺着抽象,管理,操作来看,这种角度更多的是内核中应用层面的内容,用来理解内核中是怎么运转起来的。第二种就是从内核的最细节部分出发,深入到一个个具体的宏,看看内核设计者在细节部分有着怎么样的巧妙之处,这样也有助于我们夯实C语言基础,也可以学习到GNU C的用法。
Linux阅码场
2019/10/08
7920
赵晨雨: 从微观角度来看linux内核设计
【典藏】大佬们都在用的结构体进阶小技巧
今天跟大家分享一首华晨宇的《我管你》,个人觉得这首歌表达了一种年轻人的热血感,每次听都让自己非常来劲。最近工作挺忙的,写文章或许已经成为了一种兴趣和爱好了吧,也希望每次作者的唠叨都能带给各位小伙伴一些小小的收获。
C语言与CPP编程
2020/12/02
3130
【典藏】大佬们都在用的结构体进阶小技巧
offsetof宏的模拟实现
格式:offsetof(type, member) 头文件:<stddef.h> 这个宏有两个参数:
怠惰的未禾
2023/04/27
3020
offsetof宏的模拟实现
【C语言题解】1、写一个宏来计算结构体中某成员相对于首地址的偏移量;2、写一个宏来交换一个整数二进制的奇偶位
它接受两个参数:一个结构体类型和一个该类型中的成员名称,并返回该成员在结构体中的字节偏移量。
用户11162265
2024/06/14
1450
【C语言题解】1、写一个宏来计算结构体中某成员相对于首地址的偏移量;2、写一个宏来交换一个整数二进制的奇偶位
面试大全 | C语言高级部分总结
内存物理看是有很多个 Bank(就是行列阵式的存储芯片),每一个 Bank 的列就是位宽 ,每一行就是 Words,则存储单元数量=行数(words)×列数(位宽)×Bank的数量;通常也用 M×W 的方式来表示芯片的容量(或者说是芯片的规格/组织结构)。
C语言与CPP编程
2021/03/10
2K0
面试大全 | C语言高级部分总结
工作当中非常实用的Linux内核链表
在上期文章中,已经给大家分享过offsetof()和container_of两个宏函数,这两个宏函数在Linux内核链表里面有大量的应用,对于我们平时工作写代码有很大的帮助。下面是Linux内核链表的内容分享。
用户6280468
2022/06/09
1.1K0
工作当中非常实用的Linux内核链表
相关推荐
剖析c语言结构体的高级用法(二)
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文