前言
在平常的编程中,经常需要用到一种数据结构:结构体,当结构体中的成员足够的多,且成员的数据类型又足够统一的情况下,外部函数需要操作其中的一个成员,有两种较为直接的方法:
1、取整个结构体的数据,对需要的数据进行更新,再更新整个结构体
2、针对该参数专门编写独立的接口供外部使用
这两种方法在日常使用过程中接触的比较多,方便快捷,但当遇到以下情况时,就变得不那么通用了。
假设结构体中有100个char型的data,从data1到data100,如果我们一次只需操作其中的一个同类型参数,而对整个结构体数据进行更新,第一种方法无疑做了很多没必要的工作。
如果操作具体的参数不是本函数内部决定,而是受外部的变化影响,data1-100都有可能涉及,但直至调用时,才知道具体参数是什么,针对这100个数据专门编写特定的接口一一对应,无疑是繁琐的。
此时偏移量的优势就出现了,你无需知道参数名称是什么,具体参数是哪个,只需要知道偏移量是多少,然后负责将结构体中指定偏移量的数据进行更改即可,而计算的工作完全可以交给调用函数去实现,使得模块间的耦合性降低,模块的功能更单一,更专一。
typedef unsigned int uint32;
#define OFFSETOF(type,field) ((uint32)&((type*)0)->field)
语句 | 注释 |
---|---|
(type*)0 | 把地址为0的指针指向type类型的结构体 |
((type*)0)->field | 取结构体变量 |
&((type*)0)->field | 取结构体变量地址 |
((uint32)&((type*)0)->field) | 将地址转换成uin32型 |
测试代码
接下来的这段代码中,我们定义两个结构体UTEST_T及UTEST1_T,这两个结构体的成员及成员对应的数据类型都是一致的,唯一不同的仅仅是结构体成员在结构体中的位置,通过计算及打印成员在结构体中的偏移量,观察结构体在内存中的存放规律,来更好的理解偏移量在实际使用中的直观感受。
#include<stdio.h>
typedef unsigned int uint32;
#define OFFSETOF(type,field) ((uint32)&((type*)0)->field)
#define CONTAINER_OF(ptr, type, member) \
((type *)((char *)(ptr) - OFFSETOF(type,member)))
typedef struct UTEST_
{
char a;
char b;
short c;
int d;
}UTEST_T;
typedef struct UTEST1_
{
char a;
int b;
char c;
short d;
}UTEST1_T;
int main()
{
UTEST_T test = {0};
printf("UTEST --- a:%d b:%d c:%d d:%d size:%d \n", OFFSETOF(UTEST_T,a), OFFSETOF(UTEST_T, b),OFFSETOF(UTEST_T, c),
OFFSETOF(UTEST_T, d),sizeof(UTEST_T));
printf("UTEST1--- a:%d b:%d c:%d d:%d size:%d\n", OFFSETOF(UTEST1_T,a), OFFSETOF(UTEST1_T, b),OFFSETOF(UTEST1_T, c),
OFFSETOF(UTEST1_T, d),sizeof(UTEST1_T));
printf("struct add:%d \r\nstruct.a add:%d\r\n",&test,&test.a);
return 0;
}
输出结果:UTEST — a:0 b:1 c:2 d:4 size:8
UTEST1 — a:0 b:4 c:8 d:10 size:12
struct add:1638208
struct.a add:1638208
对于UTEST_T(以下单位均为字节):
1、a的偏移量为0,因为a的地址和结构体的首地址是一样的;
2、b的偏移量为1,因为a的大小为1;
3、c的偏移量为2,因为a的大小为1,b的大小为1,1+1=2;
4、d的偏移量为4,因为a的大小为1,b的大小为1,c的大小为2;
对于UTEST1_T(以下单位均为字节):
1、a的偏移量为0,a的地址减去结构体首地址 == 偏移量;
2、b的偏移量为4,字节对齐,a的数据占1个字节,字节对齐补充了3个字节;
3、c的偏移量为8,因为a和b各占了4个字节,4+4=8;
4、d的偏移量为10,c和d共用4个字节,c使用第9/10个字节,d使用第11/12个字节
可见,在平时编程中要保持良好的代码风格和习惯,定义数据时尽量4字节对齐的去考虑,而不是想到什么定义什么,不够4字节的可以考虑定义保留位去凑满,也方面后续功能添加时代码变更的灵活性。
扩展
在平时使用结构体当中,会遇到一种情况,如果结构体的内容比较简洁单一,直接操作具体成员方便快捷,但当结构体中嵌套其余结构体,比如嵌套了一个双向链表用于排序,此时我们只知道链表指针地址的情况下,如何获取整个结构体的信息,这时候可以运用上面的知识,通过计算偏移量获取结构体的首地址,但需要做一点小小的改变,原型是Linux内核编程中的一个宏函数CONTAINER_OF(ptr, type, member),上面的代码中也贴了出来。
#if 1
#define CONTAINER_OF(ptr, type, member) \
((type *)((char *)(ptr) - OFFSETOF(type,member)))
#endif
#if 0
#define CONTAINER_OF(ptr, type, member) ({ \
const __typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member));})
#endif
贴张图方便理解
可以看到该宏仅仅多了一个指针ptr,而其余的变化并不大,我们先分步理解,其实它并没有那么难消化,细嚼慢咽慢慢来,OFFSETOF已经知道了嘛,获取某个参数的偏移量,重点是前面多了(char *)(ptr),ptr就是一个普通的指针,由于地址计算是以字节为单位,所以强制转换成char *型的指针,比如该指针指向b,地址为1638208,test_t中b的偏移量为4,那么1638208 - 4 = 1638204,即为 test_t的首地址,就这么简单。
至于这一句:const __typeof(((type *)0)->member) *__mptr = (ptr); 这个就精妙了朋友们,typeof是gun c的编译环境下才能使用的,作用是获取某个变量的数据类型,这里定义一个指向类型与ptr指向的数据类型相同,地址为0的指针,简单的说就是拐弯抹角定义一个和ptr一样的指针,只是该指针地址为0,然后用该指针指向ptr,如果此时数据类型出错的话,那么编译器就会产生警告,这一句对整体的宏函数没有什么影响,注释掉之后可以像下面测试代码中去使用,只是当你传参错误后,有这一句编译器会警告,防止你在代码中埋雷。
测试代码
#include<stdio.h>
#define OFFSETOF(type, member) ((unsigned int) &((type *)0)->member)
#define CONTAINER_OF(ptr, type, member) \
((type *)((char *)(ptr) - OFFSETOF(type,member)))
typedef struct test_
{
int a;
int b;
}test_t;
typedef struct test1_
{
test_t data1;
int data2;
}test1_t;
int main()
{
int *p = NULL;
test1_t test1 = {0};
test1_t *ptTest1 = NULL;
test1.data1.b = 1;
test1.data2 = 2;
p = &test1.data1.b;
ptTest1 = CONTAINER_OF(p,test_t,b);
printf("ptTest1->data2 : %d\r\n",ptTest1->data2);
return 0;
}
输出结果:ptTest1->data2 : 2