C语言可变参数与函数参数的内存对齐详解

C语言可变参数与函数参数的内存对齐详解

目录

什么是可变参数?

使用可变参数

函数参数的内存对齐

总结

什么是可变参数?

有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数。

C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数。

比如我们最常用的printf函数,它的函数声明是:int printf(const char *format, ...);该函数就是一个典型的应用可变参数的实例,后面那三个...就是说明该函数是可变参数函数。

使用可变参数

要使用可变函数,得引用一个头文件#include <stdarg.h>该文件提供了实现可变参数功能的函数和宏。
使用可变参数的步骤如下:

1.定义一个函数,最后一个参数为省略号...,省略号前面可以设置自定义参数(至少得有一个固定参数)。如
int getSum(int num, ...)//定义可变参数的函数

2.在函数中定义va_list类型的变量list,该类型在stdarg.h中已定义。

3.使用宏函数va_start来初始化变量list,该宏函数在stdarg.h中已定义。

4.使用宏函数va_arglist来访问参数列表中的每个项。

5.使用宏函数va_end来清理赋予list变量的内存。

宏的声明

/** \brief 初始化 ap 变量,它与 va_arg 和 va_end 宏是一起使用的。 *last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。 *这个宏必须在使用 va_arg 和 va_end 之前被调用。 * * \param ap -- 这是一个 va_list 类型的对象, * 它用来存储通过 va_arg 获取额外参数时所必需的信息。 * \param last_arg -- 最后一个传递给函数的已知的固定参数(省略号前面的那个参数)。 * \return 无 * */ void va_start(va_list ap, last_arg) /** \brief 检索函数参数列表中类型为 type 的下一个参数。它无法判断检索到的参数是否是传给函数的最后一个参数。 * * \param ap -- 这是一个 va_list 类型的对象,存储了有关额外参数和检索状态的信息。 * 该对象应在第一次调用 va_arg 之前通过调用 va_start 进行初始化。 * \param type -- 这是一个类型名称。该类型名称是作为扩展自该宏的表达式的类型来使用的。 * \return 该宏返回下一个额外的参数,是一个类型为 type 的表达式。 * */ type va_arg(va_list ap, type) /** \brief 该宏允许使用了 va_start 宏的带有可变参数的函数返回(释放内存)。如果在从函数返回之前没有调用 va_end,则结果为未定义。 * * \param ap -- 这是之前由同一函数中的 va_start 初始化的 va_list 对象。 * \return 无 * */ void va_end(va_list ap)

实例1一个可变参数的函数,求和

#include <stdio.h> #include <stdarg.h>//引用可变参数宏头文件 int getSum(int num, ...)//定义可变参数的函数 { int sum = 0; va_list list;//创建va_list类型的变量 va_start(list, num);//初始化可变参数list for(int i = 0; i < num; i++) { sum += va_arg(list, int);//访问参数列表中的每个项 } va_end(list);//释放内存 return sum; } int main() { printf("%d\n", getSum(4, 4, 5, 6, 7)); }

实例2输出字符串

#include <stdio.h> #include <stdarg.h>//引用可变参数宏头文件 void func(char *demo, ...) { char *pstr = NULL; va_list list; va_start(list, demo); while(1) { pstr = va_arg(list, char *); if(*pstr == '$')//以 '$' 代表结束 break; printf("%s\n", pstr); } va_end(list); } int main() { func("demo", "ABC", "123", "Hello Wolrd!", '$'); }

这里特别注意一下,宏va_arg无法判断检索到的参数是否是传给函数的最后一个参数,所以我们需要告诉该参数是不是最后一个参数,有2个方法,一是在使用一个函数参数来说明可变参数的数量,一是定义一个结束标志符。

可变参数的另外的一种使用方式

#include <stdio.h> int getSum(int num, ...) { int sum = 0; char *p = NULL; p = (char*)&num; p += 8; for(int i = 0; i < num; i++) { sum += *((int*)p); p += 8; } return sum; } int main() { int a = 1; int b = 2; int c = 3; printf("sum = %d\n", getSum(3, a, b, c)); } /* 输出结果 sum = 6; */

为什么这样也可以访问可变参数呢?为什么指针p要加8呢?
因为这与函数参数的入栈出栈及函数参数的内存对齐有关。

函数参数的内存对齐

首先我们来看函数void func(int a, int b, int c)各个参数在栈中的位置

c高地址
b
a低地址

函数参数的传递存储在栈中,从右至左压入栈中,压栈过程为递减;出栈过程为递增。

所以我们只需要知道a的地址,在a的地址上加上偏移量就可以访问b或者c了。

那应该加上多少偏移量呢?

#include <stdio.h> void func(int a, int b, int c) { printf("a = %p\n", &a); printf("b = %p\n", &b); printf("c = %p\n", &c); } int main() { int a,b,c; func(a, b, c); } /* 输出结果 a = 000000000061FDF0 b = 000000000061FDF8 c = 000000000061FE00 */

通过上例,发现它们之间相差8,为什么是8呢?

因为我是在Window64位上运行的,故需要按照8字节对齐。

综上,函数参数的传递存储在栈中,从右至左压入栈中,压栈过程为递减,出栈过程为递增;并且需要进行内存对齐,Window64位为8字节对齐,32位为4字节对齐。

下面是我做的一些实验,更改了函数参数类型。

短整型

#include <stdio.h> void func(char a, short b, long long c) { printf("a = %p\n", &a); printf("b = %p\n", &b); printf("c = %p\n", &c); } int main() { char a = 1; short b = 2; long long c = 3; func(a, b, c); } /* 输出结果 a = 000000000061FDF0 b = 000000000061FDF8 c = 000000000061FE00 */

浮点型

#include <stdio.h> void func(double a, double b, double c) { printf("a = %p\n", &a); printf("b = %p\n", &b); printf("c = %p\n", &c); } int main() { double a = 1; double b = 2; double c = 3; func(a, b, c); } /* 输出结果 a = 000000000061FDF0 b = 000000000061FDF8 c = 000000000061FE00 */

结构体1

#include <stdio.h> typedef struct { char c[7]; }str_t; void func(str_t a, str_t b, str_t c) { printf("a = %p\n", &a); printf("b = %p\n", &b); printf("c = %p\n", &c); } int main() { str_t a; str_t b; str_t c; func(a, b, c); } /* 输出结果 a = 000000000061FDF0 b = 000000000061FDE0 c = 000000000061FDD0 */

结构体2

#include <stdio.h> typedef struct { char c[4]; }str_t; void func(str_t a, str_t b, str_t c) { printf("a = %p\n", &a); printf("b = %p\n", &b); printf("c = %p\n", &c); } int main() { str_t a; str_t b; str_t c; func(a, b, c); } /* 输出结果 a = 000000000061FDF0 b = 000000000061FDF8 c = 000000000061FE00 */

结构体1出问题了,具体没搞明白,欢迎大佬指导。

建议可变参数使用引用头文件stdarg.h得方式较好。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注易知道(ezd.cc)的更多内容! 

推荐阅读

    excel怎么用乘法函数

    excel怎么用乘法函数,乘法,函数,哪个,excel乘法函数怎么用?1、首先用鼠标选中要计算的单元格。2、然后选中单元格后点击左上方工具栏的fx公

    opporeno8参数配置及价格

    opporeno8参数配置及价格,面部,亿元,Oppo的荣誉2020年1月4日,接近屏幕关闭传感器是否支持双卡:支持oppor11splus什么时候上市的Oppo R11S P

    excel中乘法函数是什么?

    excel中乘法函数是什么?,乘法,函数,什么,打开表格,在C1单元格中输入“=A1*B1”乘法公式。以此类推到多个单元。1、A1*B1=C1的Excel乘法公式

    魅蓝note6性能参数有哪些

    魅蓝note6性能参数有哪些,摄像头,蓝牙,魅蓝note6性能参数有哪些魅力蓝色Note6最好拍照。电池寿命更长。蓝色Note6使用高通 snapdragon 625

    标准差excel用什么函数?

    标准差excel用什么函数?,函数,标准,什么,在数据单元格的下方输入l标准差公式函数公式“=STDEVPA(C2:C6)”。按下回车,求出标准公差值。详细

    设置总账参数|用友u8设置总账参数

    设置总账参数|用友u8设置总账参数,,1. 用友u8设置总账参数1、首先要点开数据权限控制设置;2、选择想要设置控制的单据;3、打开后看到左上角

    探探语言设置|探探怎么设置语言

    探探语言设置|探探怎么设置语言,,1. 探探怎么设置语言打开探探软件,然后就有消息提示的红点,点开就行了!其实这些软件都是挺简单的操作的,都是

    csgo参数设置|csgo怎么保存

    csgo参数设置|csgo怎么保存,,csgo怎么保存第一步下载csgo的官方版本。然后再下载一个5e对战平台,PS:5e的账号和csgo的账号不是一个账号。第

    移动apn设置|移动apn设置参数

    移动apn设置|移动apn设置参数,,移动apn设置参数1、打开手机系统设置界面应用,点击页面中的“移动网络”设置选项。2、进入移动网络设置页面