CONTAINING_RECORD 宏

CONTAINING_RECORD 这样的一个宏,我看了它的定义,如下:
#define
CONTAINING_RECORD(address, type, field) ((type *)( (PCHAR)(address) -
(ULONG_PTR)(&((type*)0)->field)))

class
A
{
      char
c;
      int
a;
      short
b;
}

int a = 100;
int *pInt
= &a;
比如,我调用了
CONTAINING_RECORD(pInt,A,a);
完全展开来后如下:
(A*)((char*)pInt -
(unsigned long)(&((A*)0)->a))

为什么要用这个宏:
      
这个宏所做的操作其实就是把pInt与A结构中的相应的类型值进行一个位置配对。上面可以看到int
a定义在第二个数据中,可以想象,如果我们有a的地址,然后直接把a转化成A*的话,很明显,a的地址就变成了A*的首地址了,但问题是A的第一个元素是char型的,这样的话,pInt就无法对齐上结构中的a元素的位置了。所以要进行一个偏移量操作.

下面,我们下解析一下:
       
首先,红色的部分很容易理解,我们知道,如果有一个int *a;的指针,我们a - 1,其实相当于a -
sizeof(int),相于于把指针向右移了4个位置,把一个指针转化成一个char*型,这样,进行四则运算时就会按照我们正常的操作,(char*)(a -
1)就只是把指针移动了一个位置。
       
然后,看下蓝色的部分,首先,要明白,对0指针的取值操作并不会出错,只是不确定这个值返回的是什么值,当然,如果我们对这个值进行修改,这是很危险的。这里用的0位置指针是很特别的,相对于0的位置,0指针对->a的操作,返回的数值取地址值后再转化成unsigned
long值,其实得到的就是a相对于结构体A来说,偏移了多少个位置。0是起始地址,那么对于一个->操作,简单来理解,其实相当于0(结构体起始地址)+
sizeof(a前面的数据),当然,这里要考虑字节对齐的问题不过,编译器还是会帮我们把这些都完成。
      
最后,我们知道了pInt的结构体的首地址,知道了a的偏移地址,那么我们把pInt的地址值-偏移量,相当于把pInt倒退了偏移量个地址值,然后,我们再转换甩A*的话,相当于A*的起始地址已经是pInt的前面偏移量个地址,也就是a最前面的一个元素的地址值,对于A来说就是char
c的地址,这样,我们就得到了正确的起始地址,然后再转换成(A*)的话,我们的pInt就能和A*的a的地址对应上了.

=========================

转载

宏CONTAINING_RECORD的用处其实还是相当大的, 而且很是方便,
它的主要作用是:
    根据结构体中的某成员的指针来推算出该结构体的指针!
  下面从一个简单的例子开始说起:
  我们定义一个结构体,
同时类型化:


typedef struct{
int a;
int b;
int c;
}ss;

  这是一个很简单的结构体, 没什么特殊的,
稍微分析下该结构体:
    结构体的大小(字节):4+4+4=12字节
    成员a的偏移:0
    成员b的偏移:4
    成员c的偏移:8
  我们用ss来定义一个变量:
    ss
s =
{1,2,3};
  那么此时a,b,c的值分别为:a=1,b=2,c=3.
其实编译器在生成代码的时候其实是这样给成员变量赋值的:
  假定s的地址为:0x12000000,
则:
    *(int*)((char*)&s + 0) = 1;
    *(int*)((char*)&s + 4) =
2;
    *(int*)((char*)&s + 8) =
3;
  也就是说是在&s的地址基础上加上变量的偏移来确定成员的指针并赋值的, 所以:
    &s->a 将得到
0x12000000 + 0 = 0x12000000
    &s->b 将得到 0x12000000 + 4 =
0x12000004
    &s->c 将得到 0x12000000 + 8 =
0x12000008
  所以:    结构体的地址   +  成员变量的偏移
= 成员变量的地址
  移一下项:  成员变量的地址 -  成员变量的偏移
= 结构体的地址
  这就是我们想要的地址, 不就是做了个减法嘛
  首先,
成员变量的地址是我们知道的. 
  其次, 我们需要得到成员变量的偏移(假定为成员b的偏移).
    怎么办呢?
我们可以这样:
      &s->b - (unsigned long)&s, 这样就可以得到成员b的偏移了, 但是, 但是,
&s 是我们需要的, 显然暂时是个未知数, 既然这样...
    那, 我们再做一次减法吧(非正确的C语言表达式, 不过结果没问题,
这里只是显得清楚点):
      &(s-s)->b - (unsinged
long)&(s-s), 
    其中, 为保证类型一致, 需要:
      s-s =
(ss*)0
      (unsigned long)&(s-s) = (unsigned long)(ss*)0 = 0,
直接省略该部分就可以了
    那么, 化简得到: &((ss*)0)->b - (unsigned long)0
    最简结果:
&((ss*)0)->b, 这就是b的偏移
  哈哈, 很简单吧, 0指针的妙用, 总共做了两次减法而已
  其中,
我们需要知道ss结构体的原型, ss结构体中的某个成员变量b(其实无论哪个都一样, 只是要和前面提供指针的那个变量要一致)

  总结下,
我们需要提供:结构体中某个成员变量的地址, 该结构体的原型,
该结构体中的某个成员变量(与前面要是同一个变量)

  最终的CONTAINING_RECORD的定义为:


#define CONTAINING_RECORD(addr,type,field) ((type*)((unsigned char*)addr - (unsigned long)&((type*)0)->field))
  addr: 结构体中某个成员变量的地址
  type: 结构体的原型
  field: 结构体的某个成员(与前面相同)

  好了, 所有的结论都出来了, 这是一个万能公式, 不管成员变量是哪一个结果都正确,
这是相对于知道第一个变量的地址而言的:
    如果知道的是第一个成员的地址(pa = &s->a)的话,
这是最简单的情况了:
    直接强制类型转换就可以了: (ss*)pa 即可, 此时 &((type*)0)->field
这部分恰好为0
    所以结果直接就是((type*)addr)了, 最简单的情况. 也是我们最容易想到的一种情况,
比如把链表元素放在结构体的最开始 
  
  到这里这个CONTAINING_RECORD宏就已经说完了
  现在,
我们在使用LIST_ENTRY等双向链表时, 不管把该链表放在结构体的哪个地方,
都可以在遍历链表时通过CONTAINING_RECORD宏来准确得到整个结构体的地址了
记得移除链表中的某个元素的时候,
要free整个结构体的地址才行哦, WDK提供的操作函数只是把该链表元素脱离整个链表

  把addr转换为 unsigned
char*的原因是在指针计算时的计算单位为1, 也就是说 (unsigned char*)addr+1 = addr+1,
不转换的话肯定是错误的
  把&((type*)0)->field转换为(unsigned
long)4个字节宽的同时是要保证表达式不是由两个指针的算术操作构成的, 因为C语言标准未定义那样的运算

时间: 2024-10-10 06:33:07

CONTAINING_RECORD 宏的相关文章

我对CONTAINING_RECORD宏的详细解释

宏CONTAINING_RECORD的用处其实还是相当大的, 而且很是方便, 它的主要作用是: 根据结构体中的某成员的指针来推算出该结构体的指针! 下面从一个简单的例子开始说起: 我们定义一个结构体, 同时类型化: typedef struct{ int a; int b; int c; }ss; 这是一个很简单的结构体, 没什么特殊的, 稍微分析下该结构体: 结构体的大小(字节):4+4+4=12字节 成员a的偏移:0 成员b的偏移:4 成员c的偏移:8 我们用ss来定义一个变量: ss s

宏CONTAINING_RECORD学习

宏CONTAINING_RECORD的用处其实还是相当大的, 而且很是方便, 它的主要作用是: 根据结构体中的某成员的地址来推算出该结构体整体的地址! 下面从一个简单的例子开始说起: 我们定义一个结构体, 同时类型化: typedef struct{ int a; int b; int c; }ss; 这是一个很简单的结构体, 没什么特殊的, 稍微分析下该结构体(假设在32位平台上): 结构体的大小(字节):4+4+4=12字节 成员a的偏移:0 成员b的偏移:4 成员c的偏移:8 我们用ss来

EDKII CR宏:根据成员指针获取父结构体变量指针

核心提示: 1. CR宏 (Containing Record):根据成员指针获取父结构体变量指针 2. 0 指针的妙用. 在EDKII 的代码中有不少关于CR宏的使用,如 时钟中断处理函数CoreTimerTick. VOID EFIAPI CoreTimerTick ( IN UINT64 Duration ) { IEVENT *Event; ... if (!IsListEmpty (&mEfiTimerList)) { Event = CR (mEfiTimerList.Forward

宏定义中的#,##,...,do{}while(0),__VA_ARGS__

宏定义中的#,## 1.在一个预处理器宏中的参数前面使用一个#,预处理器会把这个参数转换为一个字符数组 #define syslog(a) fprintf(stderr,"Warning: " #a"\n"); 2.简单的说,"## "是一种分隔连接方式,它的作用是先分隔,然后进行强制连接 举列 -- 试比较下述几个宏定义的区别 #define A1(name, type)  type name_##type##_type 或 #define A

Word中 简单宏的使用

 (注意:打开文档时按住 Shift 键可以阻止 AutoOpen 宏运行) 1:Word中能够自动运行的默认宏代码名称及触发条件如下 -------------------------------------------------------- 1.名称:AutoExec 条件:启动Word或加载全局模板 2.名称:AutoNew 条件:每次生成新文档时 3.名称:AutoOpen 条件:每次打开一个已有文档时 4.名称:AutoClose 条件:每次关闭文档时 5.名称:AutoExit

常用的预定义的宏

常用的预定义的宏 常用的预定义的宏有:__LINE__ 当前源程序行的行号,用十进制整数常量表示 __FILE__ 当前源文件的名称,用字符串常量表示 __DATE__ 编译时的日期,用"MM dd yyyy"形式的字符串常量表示 __TIME__ 编译时的时间,用"hh:mm:ss"形式的字符串常量表示 __STDC__ 当且只当编译器遵循ISO标准时,它的值是十进制常量1 __STDC__VERSION__ 如果编译器遵循C99,则这个宏的值是199901L,其

笔记3:预处理器-(2)宏定义

#define指令称为宏定义指令,通常用#define指令来定义一个宏用来代表其他东西的一个名字(如常量表达式等).通常来说预处理器会通过将宏的名字和它的定义存储在一起来响应#define指令.当这个宏在后面的程序中使用到时,预处理器会"扩展"宏,将宏替换为其定义值. 简单的宏 简单的宏的定义格式: #define 标识符 替换列表 如: #define DTE_LEN 80 #define TRUE 1 #define FALSE 0 #define PI 3.1415926 #de

BOOST_AUTO宏

在boost中,有个非常不错的宏BOOST_AUTO(),它的作用是自动给var定义类型,适合function()函数返回的值的类型. 1 int function() 2 { 3 return 10; 4 } 5 main() 6 { 7 BOOST_AUTO(var, function()); 8 } 上面的作用类适于: int function() { return 10; } main() { int var = function(); }

宏------进阶

宏定义的黑魔法 - 宏菜鸟起飞手册 宏定义在C系开发中可以说占有举足轻重的作用.底层框架自不必说,为了编译优化和方便,以及跨平台能力,宏被大量使用,可以说底层开发离开define将寸步难行.而在更高层级进行开发时,我们会将更多的重心放在业务逻辑上,似乎对宏的使用和依赖并不多.但是使用宏定义的好处是不言自明的,在节省工作量的同时,代码可读性大大增加.如果想成为一个能写出漂亮优雅代码的开发者,宏定义绝对是必不可少的技能(虽然宏本身可能并不漂亮优雅XD).但是因为宏定义对于很多人来说,并不像业务逻辑那