今天想看看C语言是怎么实现函数可变参数列表的,无意间一个小实验发现了C语言在gcc下编译时内存分配的策略(VC不知道怎么样),下面记录一下
现在有一段这样的小程序
#include <stdio.h>
#include <stdlib.h>
void myfunc(int a,char b,short c)
{
printf("%x\n%x\n%x",&a,&b,&c); //分三行输出a,b,c的地址
}
int main()
{
int a = 4;
char b = 'c';
short c = 2;
myfunc(a,b,c);
return 0;
}
首先,我们知道C语言中函数的参数是从后往前压入栈中的,也就是先把 参数 c 压栈,然后是b,最后是a
也就是说,c 的地址要小于b的地址,b 的地址要小于 a 的地址
那么现在我假设 c 的地址是 0x28fee8,能不能算出b 和 a的地址呢?
答案当然是可以的
我们按照各个参数的大小,可以推算出 b的地址应该是 0x28fee8 + 0x2 (这是short类型的大小) = 0x28feea , 响应的a的地址应该是 0x28feea + 0x1 = 0x28feeb
实际结果是不是这样?运行一下程序发现结果是这样的:
跟我们预想的结果不一样.....错误发生在哪里?
仔细观察可以发现,三个地址间隔都是4, 现在计算机是以字节为单位来记录地址的,地址相差4,也就是4个字节,也就是32位。
一般我们的系统的内存地址是按4个字节对齐的,这样访问效率会比较高,因为地址总线总是按照对齐后的地址来访问,至于内存对齐的概念,可以Google一下。
举个例子,如果我们想得到0x00000001开始的4字节内容,系统首先需要以0x00000000读4个字节,从中去后面的3个字节,然后再用0x00000004作为开始地址,获得下一个4字节,再从中取1个字节,最后组合两次取值。这样子的效率就很低了。但是,如果我们一开始就对齐到0x00000000,系统只要一次读取4个字节就可以,效率就高很多。
回到我们的问题:
其实我们的问题和上面的内存对齐并没有非常直接的关系,只是概念差不多而已,gcc 至少会给每个参数分配4个字节的大小,比如如果是char参数,那么相应地会得到一个4字节的内存块,哪怕它实际只用到了1个字节,如果是double,那就分配8个字节
有什么好处呢?至少可以用来实现可变参数,同时保证内存读取的速度,不过响应地耗费了内存,但是这只是在栈中分配内存,而不是进行网络传输或者文件IO读写,相比而言,CPU计算速度和内存读取速度的优先级要比这几个字节的节省的优先级要高许多。
C语言在头文件stdarg.h中定义了几个宏,详细可以百度百科 va_list
其中就有一个宏 _INTSIZEOF(n)
#define _INTSIZEOF(n) ( ( sizeof(n) + sizeof(int) - 1 ) & ~(sizeof(int) - 1) )
这个宏定义看起来很复杂,其实含义就是,保证 _INTSIZEOF(sometype) 算出来的 sometype的大小符合下面两种情况
- 如果 sizeof(sometype) < sizeof(int) ,与运算会使得最后的结果是 sizeof(int) ,读者不妨试试看,或者分析一下与运算表达式
- 如果sizeof(sometype) >= sizeof(int) , 结果就直接是 比sizeof(sometype)大的最接近的 sizeof(int)的倍数