C语言中可变参宏的一种实现方式

  1. 好久不写了,有点手生了都。
  2. 下面主要分析可变参数宏的一种实现。
  3. 因为C语言的标准库是可平台(处理器)有关的,所以本历程不能保证所有的处理器可用。
  4. 本人在裸机ARM的处理器上实现过类似printf函数,适合ARM平台使用。

  5. 下面带代码来自linux内核的stdarg.h头文件
  6. typedef char *va_list;  
  7.   
  8. ​#define _AUPBND (sizeof (acpi_native_int) - 1)  
  9.   
  10. ​#define _ADNBND (sizeof (acpi_native_int) - 1)  
  11.   
  12. ​#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))  
  13.   
  14. ​#define va_arg(ap, T) (*((T*) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))  
  15.   
  16. ​#define va_end(ap) (void) 0  
  17.   
  18. ​#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

下面主要对上面几行代码分析和自己实现。

  1. typedef char *va_list;  

该行主要就是定义一个char类型的指针。(为什么是char不是其它类型呢?)

答:主要原因是char类型的指针进行加法运算,每次增加是以1个字节为基础,int*的类型每次加1实际是增加4个字节。所以char类型的指针更灵活操作。

  1. ​#define _AUPBND (sizeof (acpi_native_int) - 1)  
  2. ​#define _ADNBND (sizeof (acpi_native_int) - 1)  

这两行是平台相关的

其主要为下面两种实现方式

typedef s32 acpi_native_int        若采用这种宏定义,表明int 类型是用32位表示,也表示当前内存是4字节对齐

typedef s64 acpi_native_int        若采用这种宏定义,表明int 类型是用64位表示,也表示当前内存是8字节对齐

本文以4字节对齐来进行讲解。

  1. ​#define _AUPBND (sizeof (acpi_native_int) - 1)  
  2. ​#define _ADNBND (sizeof (acpi_native_int) - 1)  

带入上式后即可得到该宏的表示

  1. ​#define _AUPBND 3
  2. ​#define _ADNBND 3

  1. ​#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))  
继续带入上式得到

  1. ​#define _bnd(X, bnd) (((sizeof (X)) + 3) & (~(3)))  

3用2进制表示为 11b ,即低两位是1

sizeof (X)通常得到的是一个类型的字节数,通常为1,4,8....

把1,4,8带入上式可以发现其结果为

#define _bnd(char,3)    ==>  (1+3)&(~3)  ==> 4

#define _bnd(int,3)     ==>  (4+3)&(~3)  ==> 4

#define _bnd(double,3)  ==>  (4+3)&(~3)  ==> 8

........

可以发现它都是4的整数倍,同时满足传入类型无论是否为4(sizeof(int)),经过#defeine _bnd后都是4字节向上对齐

其作用也是参数传递过过程中对齐传递(比如char类型,传递参数时若入栈是4字节对齐压栈的)

接下来说一下C语言中函数传参当参数个数小于4个(也可能其他,和编译器有关),会通过寄存器传过去比如r0,r1,r2,r3

进入函数内部后,这几个寄存器还是要压栈(因为函数中要使用寄存器)。

其压栈顺序和参数传入顺序相反比如:下面xxx函数是先对参数c压栈后对参数b,最后压栈参数a.

void xxx(int a,int b,int c)

因为ARM平台默认是满减栈,所以参数c存在高地址,参数a存在低地址。

当参数个数超过4个(也可能是其他)时,其他参数也是由后往前压栈,前4个仍然使用寄存器传入后压栈。

假设有五个参数,void xxx(int a,int b,int c,int d,int e),其最终在内存中的排布如下


若是知道a的地址,知道每个参数的类型,则可以通过指针得到每一个参数的值。

接下来引入可变参函数,以printf为例

下面是printf函数的原型

int printf(const char *format, ...);

在printf函数中,知道了第一个参数的地址(通常第一个参数都为指针类型),则可以通过%d,%s,%x,%f之类得到参数类型,进而通过指针运算找到对应参数。

下面这个printf的使用为例子来分析

printf ("   %s .%d..%lf ", p_char,int_i,double_d);

下面根据上面的分析,画出,四个参数在内存中的分布图。


接下来主要分析可变参数中用到的宏(即如何通过指针运算找到真确地址)

  1. ​#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

va_start(ap, A)有两个参数,一个是函数传入的第一个参数(比如printf中的format),必须有这个参数,才能进行后面的数据索引

ap为用户自定义的一个参数通常用系统给定的va_list来定义   表示为va_list ap    ==>   char * ap

该宏为初始化ap,同时得到下一个参数的地址。

带入p参数

  1. ​#define va_start(ap, format) (void) ((ap) = (((char *) &(format)) + (_bnd (format,3))))

因为format为一个指针类型,32位平台为4个字节,所以后半部分的表达式经过对齐运算后_bnd (format,3)为4

即    ap = (void)((char*)&(format) + 4) 恰好format指针为四个字节,接下来ap指向p_char的字符指针类型。

而取出字符串的方法也很简单,使用下面这个宏即可实现。

  1. ​#define va_arg(ap, T) (*((T*) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) 

这个宏有两个参数,一个是字符指针类型ap,另一个为T类型,即要在ap这个起始地址取出的数据的类型。

因为第一个参数中的%s以及提示第二个参数为字符指针类型。

则最简单的方法就是  把char*类型的指针ap转换为 T类型的指针ap,然后进行引用操作

转换为T类型的指针可以这样操作(T*)ap 

然后取出这个值就很简单了*((T*)ap 

va_arg这个宏还要满足取值的值返回,同时ap指向下一个参数的地址,所以一条语句满足两个结果就稍微麻烦一些。

最简单的方法是使用逗号表达式,一条语句满足实现两种功能,并返回逗号前面的值

比下面这种实现:

  1. ​#define va_arg(ap, T) (*((T*) (ap)), ap += (_bnd (T, _AUPBND)))

逗号前面的是取值,逗号后面的是让ap指向下一个参数的地址(其实就是va_start

而内核使用了一种比较花哨的写法

先让ap 加上下一个参数的偏移量,然后又减去这个偏移量。然后得到对应值。

  1. ​#define va_arg(ap, T) (*((T*) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) 

标红的为让ap指向下一个参数的地址。但后面减去偏移量的值不用ap保存接收,而是直接通过类型转换,得到值。

起始和用逗号表达式的效果一致。但唯一对使用者来说看起来不是那么容易理解,但可能对库本身的实现的那类人来说,应该和逗号表达式的看起来 难度是一样的。



猜你喜欢

转载自blog.csdn.net/qq_16777851/article/details/79979084
今日推荐