从可变参数列表解析到cpu内存对齐问题

可变参数列表,简单的理解一下就是该函数可以接受1个以上的任意多个参数(参数个数不确定)

刚开始我听到这个概念就觉得好神奇啊,从来没听过这么神奇的函数!

果然,打脸总是分分钟就会来的。啥叫没听说过,我朋友告诉我,学习c语言我写的第一个程序就用到了可变参数列表的概念

printf("hello world!");

没错,printf()函数就是一个典型的例子,让我们深度剖析一下。。。


对printf的定义是这样的,他的形参有一个这样的东西不知道大家有没有注意到:...(三个点)--> 这就表示该函数的参数列表是可变的,顾名思义就是传的参数可以是任意多个。

那既然是任意为什么还要有这个参数:const char *format --> 上面已经提到过了“1个以上任意多个参数”,那你要不写这个参数,我要说任意为0呢,这不就没参数了嘛。printf不就疯掉了,这是让我打印什么。。。

好啦,正经一点。。。


我们平时使用printf打印时这样写的

int a = 10;
char b = 'A';
float c = 0.0;
printf("%d %c %f\n", a, b, c);

我写过一篇关于栈帧的博客,我们知道,形参实例化是从右往左进行的,最接近被调用函数的参数(也就是地址最低的参数)其实是最左边的参数 --> 那么printf找到参数*format之后继续寻找上面的参数,可参数的个数是任意的,它怎么知道什么时候就把参数找完了呢?找到参数之后他存放了多大,又应该怎么取它的内容呢??

其实是这样的,printf里面有一些这样的符号%d、%c、%f、%s、%u......这些内容就可以告诉你传进了几个参数,每个参数又存放在多大空间中

类似于上面的例子,*format指向“%d %c %f\n”--> 即后面还有三个参数,第一个是整型它放在format上面的四个字节的空间中、第二个是字符型它放在整型上面的一个字节的空间中、第三个是浮点型它放在了字符型上面的四个字节的空间中(我简要画一下临时变量在栈中的分布)


这样就可以通过第一个参数确定其他所有参数的个数以及类型!!!

因此,可变参数能够被使用,需要确定两个信息:每个传入参数的类型信息;一共传入的参数个数

对上面栈的画法以及形参实例化(所有和栈有关的),有不明白的,戳这里:

https://blog.csdn.net/God_bless_TYY/article/details/80233081


接下来通过一个程序了解一下可变参数列表的概念

实现一个函数可以求任意个参数的平均值

#include <stdio.h>
#include <windows.h>

int average(int n, ...)
{
	va_list arg;
	int i = 0;
	int sum = 0;
	va_start(arg, n);
	for (i = 0; i < n; i++)
	{
		sum += va_arg(arg, int);
	}
	va_end(arg);
	return sum / n;
}

int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	int avg1 = average(2, a, c);
	int avg2 = average(3, a, b, c);
	printf("avg1 = %d\n", avg1);
	printf("avg2 = %d\n", avg2);
	system("pause");
	return 0;
}

以下是系统提供的宏定义:

1)va_list --> 用来声明一个va_list类型的变量arg,用于访问参数列表的不确定部分

typedef char * va_list;

2)va_start --> 对变量arg进行初始化。第一个参数是需要初始化的对象,第二个参数是省略号前最后一个有名字的参数。执行结束后可以使arg指向可变参数部分的第一个参数

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

含义是:先取出已知参数v的地址,再将该地址上移至第一个可变参数的位置

不知道大家有没有注意到这个东西:_INTSIZEOF(v),在此可先简单的理解为sizeof(v)。至于具体的区别,我放在下面的扩充内容里研究

3)va_arg --> 取出可变参数的值。第一个参数是变量arg,第二个参数是arg指向参数的类型。执行结束后可以取出该参数,并且将变量arg移至下一个可变参数的位置

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

含义是:先将ap移至需取出参数的下一个可变参数的位置,再取出需取出的参数

4)va_end --> 对变量arg置空

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

从上面的例子可以看出,对可变参数我们也是有限制的:

1)可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的。但是,你想一开始就访问参数列表中间的参数,那是不可能的!

2)参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用va_start。

3)这些宏是无法直接判断实际存在参数的数量。

4)这些宏无法判断每个参数的类型。

5)如果在va_arg中指定了错误的类型,那么其后果是不可预测的。


扩充部分:_INTSIZEOF(n) --> 内存对齐

_INTSIZEOF(n)它的作用就是实现内存对齐问题,它也是系统提供的一个宏定义,是这么写的:

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

注意:cpu每次读取内存是以整型的整数倍进行读取的,也就是说一次读取4字节(或者8字节、或者16字节、......)的内容。这就需要引入内存对齐的概念,当有些数据它所占的空间不足4字节的整数倍时,那就把它放大为4字节的整数倍(向上取整),这样做的目的是为了提高cpu读取内存的效率。

换句话说,当我只有一个字节的数据时,cpu一次还是访问四个字节的空间。因此我在上面画的printf的栈图,应该进行小小的修改


看!虽然临时变量b是个字符型它的值只占了一个字节,但仍然将它放在了四个字节的空间中,因为cpu走一步就是四字节的整数倍


了解了内存对齐这个概念之后,我们再来看看这个宏是怎么实现内存对齐的:

现在有x,y都是正整数,要把x向上取整为y的整数倍,我现将x写成ky+b的形式,其中0<=b<=y-1

这样的话,当b为0时,f(x) = ky,否则f(x) = (k+1)y

求整数倍,当b = 0时比较简单,x/y就可以求得k。但b不为0呢,如何通过x,y求得(k+1)?

直接用x/y得到的总是k,k+1是得不到的

那么现在只能考虑增大分子了:(x+m) / y = (ky+b+m) / y,这个m可以使得当b=0时结果为k,否则结果为k+1

这个问题就等价于,m要小于y,并且要足够大,大到能让最小的余数b=1加上它之后不再小于y

m<y && (m+1)>y --> m=y-1


现在回头来看宏定义的式子,(sizeof(n) + sizeof(int) - 1)这个部分就实现了x+m

~(sizeof(int) - 1)其实就是对3按位取反:1111  1111  1111  1100

这两个部分按位与的结果就实现了将sizeof(n)向上取整为sizeof(int)的整数倍

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值