大家好,我是杂烩君。
上篇文章分享几个实用的代码片段(第二弹)我们分享了一段代码:
有位读者发问:(type * )0不是指向空地址吗?(type*)0->member不是访问非法内存了吗?为什么不会出错?
这篇文章我们就来解释这个问题。
GET_MEMBER_SIZE分析
// 获取结构体成员大小#define GET_MEMBER_SIZE(type, member) sizeof(((type*)0)->member)
虽然说这里用了 ((type*)0)->member ,看起来似乎有问题?访问非法地址0地址?
其实不是的,注意这里用到了 sizeof操作符 。在C语言中,sizeof() 是一种内存容量度量函数,其字节数的计算是在 编译阶段 进行的。
C语言源程序经过编译器进行词法分析、语法分析等过程生成中间语言(object后缀的文件)编译期间会生成一个字符表和静态分配空间(如new static 全局变量)它们所需的内存空间可以计算出来放在链接库后的可执行文件中(虚拟内存即磁盘),在运行时将放在可执行文件中的偏移量加载到内存的堆中同时将局部变量加载到栈中。
所有内存的开辟只有程序运行的时候才会在物理内存中开辟,即sizeof(((type*)0)->member)的操作不是等到程序运行期间计算的,而是在编译阶段就计算了,所以GET_MEMBER_SIZE宏定义并没有访问非法内存的操作。
进一步的,我们看看上面那个代码实例中,结构体成员的字节数是不是在编译阶段计算出的,编译出汇编文件:
gcc -S member_size.c -o member_size.s
这个汇编文件我们可能不全看懂所有指令,但大概知道如下三个指令的意思我们就大概可以知道这段汇编代码的意思了。
- leaq:加载有效地址指令,即将有效地址复制到寄存器中。
- movl:数据传送指令。
- call指令: 将当前的 IP 或 CS和IP 压入栈中, 转移(jmp)。
可以看到,从上到下,依次会把立即数1、1、2、4、3、12放到esi寄存器中。
为什么是这些立即数?
我们编译运行一下我们的程序:
可以看到,正好就是我们需要求的结构体各成员的大小及结构体的大小,所以GET_MEMBER_SIZE(type, member)是在编译阶段起作用的。
其实,GET_MEMBER_SIZE宏定义中的0只是看做一个随意给的地址,方便求成员的大小,如果写为0容易引起误解,不妨可以写为一个任意值,比如修改为100,也是可以计算出各结构体成员的大小的。
最后,如果 ((type*)0)->member 在其它地方使用,会出现什么问题呢?自然就是这位读者所理解的:操作非法地址。会引起段错误。比如添加如下一行代码:
编译运行:
段错误的定位方法可查阅往期文章:分享一种你可能不知道的bug定位方法、嵌入式段错误的3种调试方法汇总!
GET_MEMBER_OFFSET分析
// 获取结构体成员偏移量#define GET_MEMBER_OFFSET(type, member) ((size_t)(&(((type*)0)->member)))
该宏返回的是以0地址为基准的地址,也不涉及访问0地址的操作。
以上就是本次的分享。
期待你的三连支持!
私信回复【嵌入式书籍】,可获取博主精心整理的嵌入式电子书一份