分享
 
 
 

参数可变函数的实现(上)

王朝other·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

此文献给如我一般还在探索C语言之路的朋友们。

注:本文中测试程序的编译环境为win2000和VC6.0

缘起:

作为一个程序员,我没有写过参数可变的函数,我相信大部分朋友也没有涉及过,或者我的境界层次太低了。那么缘何我要去揭这一层面纱呢?因为好奇!

我是个思维具有极大惰性的人,曾经识得参数可变函数,也懒得去深究,但是它的三点(函数声明时参数列表中的“…”)却深刻的映入了我的记忆里,而且是带着若干个闪耀的问号。可是就在昨天,在拜读某君的高论时,它再一次出现了。我的资质真的是不太够,因为某君在谈到它时只是给出了<stdarg.h>中关于它的宏定义,我想大概在高手眼里,点这一下就神会了吧。可是他这么轻轻一点却使留在记忆里曾经的那几个问号无限的膨胀,以至于我这个又菜又懒的所谓程序员也萌生了莫大的好奇。

破题:

但凡所谓“实现”都是从没有到有的过程,但是我只是想去解惑它的实现,因为它原本就是好端端的正为成千上万的程序员们服务。

还是从我们熟悉的printf说起:

如果你是个C语言的程序员,无论你是初学者还是高高手,对于printf都不会陌生,甚至你已经用了无数次了。我已经说过我是个有极大惰性的人,所以每次用printf都是照本宣科,规规矩矩的按教科书上说的做,从来没有问过一个为什么,这就是所谓的“熟视无睹”吧。

其实,printf函数是一个典型的参数可变的函数。在保证它的第一个参数是字符串的条件下,你可以输任意数量任意合法类型的参数。只要你在第一个字符串参数中使用了对应的格式化字符串,你就可以输出正确的值。这难道不是件很有趣的事吗?那它是怎么做到的?

1,首先,怎么得到参数的值。对于一般的函数,我们可以通过参数对应在参数列表里的标识符来得到。但是参数可变函数那些可变的参数是没有参数标识符的,它只有“…”,所以通过标识符来得到是不可能的,我们只有另辟途径。

我们知道函数调用时都会分配栈空间,而函数调用机制中的栈结构如下图所示:

| ...... |

------------------

| 参数2 |

------------------

| 参数1 |

------------------

| 返回地址 |

------------------

|调用函数运行状态|

------------------

可见,参数是连续存储在栈里面的,那么也就是说,我们只要得到可变参数的前一个参数的地址,就可以通过指针访问到那些可变参数。但是怎么样得到可变参数的前一个参数的地址呢?不知道你注意到没有,参数可变函数在可变参数之前必有一个参数是固定的,并使用标识符,而且通常被声明为char*类型,printf函数也不例外。这样的话,我们就可以通过这个参数对应的标识符来得到地址,从而访问其他参数变得可能。我们可以写一个测试程序来试一下:

#include <stdio.h>

void va_test(char* fmt,...);//参数可变的函数声明

void main()

{

int a=1,c=55;

char b='b';

va_test("",a,b,c);//用四个参数做测试

}

void va_test(char* fmt,...) //参数可变的函数定义,注意第一个参数为char* fmt

{

char *p=NULL;

p=(char *)&fmt;//注意不是指向fmt,而是指向&fmt,并且强制转化为char *,以便一个一个字节访问

for(int i = 0;i<16;i++)//16是通过计算的值(参数个数*4个字节),只是为了测试,暂且将就一下

{

printf("%.4d ",*p);//输出p指针指向地址的值

p++;

}

}

编译运行的结果为

0056 0000 0066 0000 | 0001 0000 0000 0000 | 0098 0000 0000 0000 | 0055 0000 0000 0000

由运行结果可见,通过这样方式可以逐一获得可变参数的值。

至于为什么通常被声明为char*类型,我们慢慢看来。

2,怎样确定参数类型和数量

通过上述的方式,我们首先解决了取得可变参数值的问题,但是对于一个参数,值很重要,其类型同样举足轻重,而对于一个函数来讲参数个数也非常重要,否则就会产生了一系列的麻烦来。通过访问存储参数的栈空间,我们并不能得到关于类型的任何信息和参数个数的任何信息。我想你应该想到了——使用char *参数。Printf函数就是这样实现的,它把后面的可变参数类型都放到了char *指向的字符数组里,并通过%来标识以便与其它的字符相区别,从而确定了参数类型也确定了参数个数。其实,用何种方式来到达这样的效果取决于函数的实现。比如说,定义一个函数,预知它的可变参数类型都是int,那么固定参数完全可以用int类型来替换char*类型,因为只要得到参数个数就可以了。

3,言归正传

我想到了这里,大概的轮廓已经呈现出来了。本来想就此作罢的(我的惰性使然),但是一想到如果不具实用性便可能是一堆废物,枉费我打了这么些字,决定还是继续下去。

我是比较抵制用那些不明所以的宏定义的,所以在上面的阐述里一点都没有涉及定义在<stdarg.h>的va(variable-argument)宏。事实上,当时让我产生极大疑惑和好奇的正是这几个宏定义。但是现在我们不得不要去和这些宏定义打打交道,毕竟我们在讨生计的时候还得用上他们,这也是我曰之为“言归正传”的理由。

好了,我们来看一下那些宏定义。

打开<stdarg.h>文件,找一下va_*的宏定义,发现不单单只有一组,但是在各组定义前都会有宏编译。宏编译指示的是不同硬件平台和编译器下用怎样的va宏定义。比较一下,不同之处主要在偏移量的计算上。我们还是拿个典型又熟悉的——X86的相关宏定义:

1)typedef char * va_list;

2)#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

3)#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )

4)#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

5)#define va_end(ap) ( ap = (va_list)0 )

我们逐一看来:

第一个我想不必说了,类型定义罢了。第二个是颇有些来头的,我们也不得不搞懂它,因为后面的两个关键的宏定义都用到了。不知道你够不够细心,有没有发现在上面的测试程序中,第二个可变参数明明是char类型,可是在输出结果中占了4个byte。难道所有的参数都会占4个byte的空间?那如果是double类型的参数,且不是会丢失数据!如果你不嫌麻烦的话,再去做个测试吧,在上面的测试程序中用一个double类型(长度为8byte)和一个long double类型(长度为10byte)做可变参数。发现什么?double类型占了8byte,而long double占了12byte。好像都是4的整数倍哦。不得不引出另一个概念了“对齐(alignment)”,所谓对齐,对Intel80x86机器来说就是要求每个变量的地址都是sizeof(int)的倍数。原来我们搞错了,char类型的参数只占了1byte,但是它后面的参数因为对齐的关系只能跳过3byte存储,而那3byte也就浪费掉了。那为什么要对齐?因为在对齐方式下,CPU 的运行效率要快得多(举个例子吧,要说明的是下面的例子是我从网上摘录下来的,不记得出处了。

示例:如下图,当一个long 型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU 存取这个数只需访问一次内存,而当一个long 型数(如图中的long2)在内存中的位置跨越了字边界时,CPU 存取这个数就需要多次访问内存,如i960cx 访问这样的数需读内存三次(一个BYTE、一个SHORT、一个BYTE,由CPU 的微代码执行,对软件透明),所以对齐方式下CPU 的运行效率明显快多了。

1 8 16 24 32

------- ------- ------- ---------

| long1 | long1 | long1 | long1 |

------- ------- ------- ---------

| | | | long2 |

------- ------- ------- ---------

| long2 | long2 | long2 | |

------- ------- ------- ---------

| ....)。好像扯得有点远来,但是有助于对_INTSIZEOF(n)的理解。位操作对于我来说是玄的东东。单个位运算还应付得来,而这样一个表达式摆在面前就晕了。怎么办?菜鸟自有菜的办法。(待续)

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有