C语言可变参数列表的用法与深度剖析

目录
  • 前言
  • 一、可变参数列表是什么?
  • 二、怎么用可变参数列表
  • 三、对于宏的深度剖析
    • 隐式类型转换
  • 对两个函数的重新认知
  • 总结

前言

可变参数列表,使用起来像是数组,学习过函数栈帧的话可以发现实际上他也就是在栈区定义的一块空间当中连续访问,不过他不支持直接在中间部分访问。

声明: 以下所有测试都是在x86,vs2013下完成的。

一、可变参数列表是什么?

在我们初始C语言的第一节课的时候我们就已经接触了可变参数列表,在printf的过程当中我们通常可以传递大量要打印的参数,但是我们却不知道他是如何做到的,今天就带大家剖析它。

二、怎么用可变参数列表

首先我们要引入windows.h的头文件

然后我们先要介绍以下几个宏。在这里我们先简述它的功能,在后面会有详细的讲解,这里是为了方便大家入门。

typedef char* va_list;  //类型的重定义

#define _ADDRESSOF(v) (&(v))//一个取地址的宏。

1._ADDRESSOF:取传入变量的地址。

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

2._INTSIZEOF:该宏功能是让n的类型往4的倍数上取整。

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

下面一段代码进行解释:

#pragma pack(1)//设置默认对其数为1
struct A
{
	char ch[11];
};

int main()
{
	printf("int : %d\n", _INTSIZEOF(int));
	printf("double: %d\n", _INTSIZEOF(double));
	printf("short: %d\n", _INTSIZEOF(short));
	printf("float: %d\n", _INTSIZEOF(float));
	printf("long long int: %d\n", _INTSIZEOF(long long int));
	printf("struct A:%d\n", _INTSIZEOF(struct A));
	return 0;
}

结果:

3.__crt_va_start_a:取变量v的地址强转为char*然后向指向v类型对其数后,即找到第一个可变参数列表当中的变量!

#define __crt_va_start_a(ap, v) \
((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))

4.__crt_va_arg:将ap提前指向下一个要访问的位置,并且返回当前访问的内容。 注意+=后ap指向下一个要访问的地址,但是返回的内容是当前的。

#define __crt_va_arg(ap, t)   \
      (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

5.__crt_va_end:将ap置成NULL。

#define __crt_va_end(ap)\
 ((void)(ap = (va_list)0))

紧接着我们看一下以下几个定义。

#define va_start __crt_va_start
#define va_arg   __crt_va_arg
#define va_end   __crt_va_end

测试:找一组不存放在数组当中的最大的一个数据返回。

int Find_Max(int num, ...)
{
	//定义一个char* 的变量arg
	va_list arg;
	//将arg指向第一个可变参数
	va_start(arg, num);
	//将max置成第一个可变参数,然后arg指向下一个可变参数
	int max = va_arg(arg, int);
	//循环num-1次,访问完剩下的可变参,找到最大的赋值给max
	for (int i = 1; i < num; ++i)
	{
		int r;
		if (max < (r = va_arg(arg, int)))
		{
			max = r;
		}
	}
	//将arg指针变量置成NULL,避免野指针
	va_end(arg);
	return max;
}
int main()
{
	int ret = Find_Max(5, 0x1, 0x2, 0x3, 0x4, 0x5);
	printf("ret :%d\n", ret);
	return 0;
}

结果:

三、对于宏的深度剖析

虽然在Linux下的进程地址空间是由高到低排列的,但是由于vs下的内存是从低字节到高字节的,我们的栈会和linux下画的不太一样,但是都是朝着低地址方向扩展的。这是方便大家理解。

Linux的进程地址空间示意图:

代码栈帧示意图:

隐式类型转换

举个栗子,当我们执行下面的代码,当我们以char类型传参,但函数体依旧以int的步长获取,此时会出错吗?

#include<stdio.h>
#include<windows.h>
int Find_Max(int num, ...)
{
	//定义一个char* 的变量arg
	va_list arg;
	//将arg指向第一个可变参数
	va_start(arg, num);
	//将max置成第一个可变参数,然后arg指向下一个可变参数
	int max = va_arg(arg, int);
	//循环num-1次,访问完剩下的可变参,找到最大的赋值给max
	for (int i = 1; i < num; ++i)
	{
		int r;
		if (max < (r = va_arg(arg, int)))
		{
			max = r;
		}
	}
	//将arg指针变量置成NULL,避免野指针
	va_end(arg);
	return max;
}

int main()
{
	char a = '1'; //ascii值: 49
	char b = '2'; //ascii值: 50
	char c = '3'; //ascii值: 51
	char d = '4'; //ascii值: 52
	char e = '5'; //ascii值: 53
	int ret = Find_Max(5, a, b, c, d, e);
	//int ret = Find_Max(5, 0x1, 0x2, 0x3, 0x4, 0x5);
	printf("ret :%d\n", ret);
	system("pause");
}

答案:

不会的,由于压栈的时候是通过寄存器传参的,32位下的寄存器就是4个字节。

压栈时的汇编:其中第一条不是mov,而是movsx,即汇编语言数据传送指令MOV的变体。带符号扩展,并传送。也就是整形提升。

同理:用float传参,用double字长走,也是没有问题的。

总结

所以我们习惯在函数体内部(Find_Max)用int/double为长度走,而传参的时候我们可以用char/short/float等等类型。

注意:

64位下的定义和32位下差异是很大的。

为什么按照4字节对齐:

先前讲到在短整型,在压栈的过程中会发生整形提升,那么从栈帧中要拿到对应的数据也要按照对应的方法提取。

_INTSIZEOF的数学理解:

_INTSIZEOF(n)的意思:计算一个最小数字x,满足 x>=n && x%4==0,n表示sizeof(n)的值。即该类型的大小要满足往n的整数倍对齐,且最小不能小于n。

以4字节对齐为栗子:

n%4 == 0,则 ret = n;

n %4 != 0 , 则 ret = (n+ 4 - 1)/4 *4;

(n+ 4 - 1)/4 -->假设 n为1到4,那么(n + 4 - 1)/4的结果都是1,再乘上对其数4就是以4对齐的最小对齐数了。就能将这4个数值范围最小对齐倍数控制在同一个值。

我们观察(n+ 4 -1)/4 *4,/4实际上就是将二进制序列往右移,*4就是把二进制序列往左移动,这一来一回实际上就是把最低两位置成0,那么我们还可以简化成:
(n+ 4 -1) & ~3 ,也就是源码当中的定义了!!

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

对两个函数的重新认知

对printf的理解:

在上述的例子中,宏是无法判断实际存在参数的数量,以及实际参数的类型的,那么在printf当中,必定有能够确定参数数量以及辨别参数类型的方法,实际上也就是%c,%d,%lf,其中%的数量除了%%外的%的数量实际上就能让我们得知参数的数量,而%c,%d,实际上也就说明了对应的类型。

对exec系列的理解:

进程控制,当时讲述了实际上只有一个系统调用execve,其他函数exec函数最终都是要调用execve函数,那么是如何实现从参数l到v这个过程的呢?

答案:

实际上访问到null为止,传参的数量用一个count一直计数就能拿到,而类型毫无疑问就是char*,我们可以用strlen去计算要走多长。(不过两个char数组通常会间隔多8个字节)

总结

到此这篇关于C语言可变参数列表的文章就介绍到这了,更多相关C语言可变参数列表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言中编写可变参数函数

    通过stdarg.h头文件为函数提供了定义可变参数列表的能力.声明一个可变参数的函数类似: void f1(int n,...); 其中n表示参数列表个数,而用省略号来表示未知参数列表.stdarg.h中提供了一个va_list类型,用于存放参数.一个大概的使用过程类似: void f1(int n,...) { va_list ap; va_start(ap,n); //初始化参数列表 double first=va_arg(ap,double); //取第一个参数 int second=va

  • C语言中可变参数的使用方法示例

    前言 在C语言程序编写中我们使用最多的函数一定包括printf以及很多类似的变形体.这个函数包含在C库函数中,定义为 int printf( const char* format, ...); 除了一个格式化字符串之外还可以输入多个可变参量,如: printf("%d",i); printf("%s",s); printf("the number is %d ,string is:%s", i, s); 格式化字符串的判断本章暂且不论,下面分析一

  • C语言可变参数函数详解示例

    先看代码 复制代码 代码如下: printf("hello,world!");其参数个数为1个.printf("a=%d,b=%s,c=%c",a,b,c);其参数个数为4个. 如何编写可变参数函数呢?我们首先来看看printf函数原型是如何定义的.在linux下,输入man 3 printf,可以看到prinf函数原型如下: 复制代码 代码如下: SYNOPSIS#include <stdio.h>int printf(const char *form

  • c语言可变参数实现示例

    这段代码展示了如何不使用<stdarg.h>中的va_list.va_start.va_end宏来实现自定义可变参数以及如何改变默认的%d.%f.%s等格式字符. 复制代码 代码如下: #include <stdio.h>#include <stdlib.h> // itoa() and ltoa()#include <string.h> // strcat() and strlen() // echo("$i, $s, $l, $c",

  • C语言可变参数列表的用法与深度剖析

    目录 前言 一.可变参数列表是什么? 二.怎么用可变参数列表 三.对于宏的深度剖析 隐式类型转换 对两个函数的重新认知 总结 前言 可变参数列表,使用起来像是数组,学习过函数栈帧的话可以发现实际上他也就是在栈区定义的一块空间当中连续访问,不过他不支持直接在中间部分访问. 声明: 以下所有测试都是在x86,vs2013下完成的. 一.可变参数列表是什么? 在我们初始C语言的第一节课的时候我们就已经接触了可变参数列表,在printf的过程当中我们通常可以传递大量要打印的参数,但是我们却不知道他是如何

  • C语言进阶可变参数列表

    可变参数 可变参数是C语言提供的一种参数可变的机制,咱希望函数带有可变数量的参数,而不是预定义数量的参数.它允许咱定义一个函数,能根据具体的需求接受可变数量的参数,比如这种: int Max(int num,...) { va_list arg; va_start(arg,num); int max = va_arg(arg,int); for(int i = 1;i<num;i++) { int sid = va_arg(arg,int); } if(sid > max) { max = s

  • C语言可变参数函数详解

    目录 C语言可变参数函数 总结 C语言可变参数函数 C 语言允许定义参数数量可变的函数,这称为可变参数函数(variadic function).这种函数需要固定数量的强制参数(mandatory argument),后面是数量可变的可选参数(optional argument). 这种函数必须至少有一个强制参数.可选参数的类型可以变化.可选参数的数量由强制参数的值决定,或由用来定义可选参数列表的特殊值决定. C 语言中最常用的可变参数函数例子是 printf()和 scanf().这两个函数都

  • C语言可变参数与内存管理超详细讲解

    目录 概述 动态分配内存 重新调整内存的大小和释放内存 概述 有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数.C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数.下面的实例演示了这种函数的定义. int func(int, ... ) { . . . } int main() { func(2, 2, 3); func(3, 2, 3, 4); } 请注意,函数func()最后一个参数写成省略号,即三个点号(...)

  • Java可变参数列表详解

    Java可变参数列表详解 1.接受的传入参数情况: 如public void test(String ...args){...} 1)不使用参数,如test() 2)使用一个或多个参数,如test("1"); test("1","2"); 3)使用数组 test(new String[]{"1","2"}); 2.方法内部访问参数: 在test方法内部,我们可以像使用数组的访问方式一样来访问参数args.如

  • c语言基于stdarg.h的可变参数函数的用法

    C语言编程中有时会遇到一些参数个数可变的函数,本文详细讲解了可变参数函数的实现原理,分享给大家 在开始学习C语言的函数的时候,我们就知道函数的参数个数应该是在函数声明的时候就指定的,这一点我们没有任何疑问.但是不知道大家有没有注意到我们的printf()函数,他的函数参数理论上并不是确定的,而是随着匹配字符串中的格式控制符的个数控制的.其实当时也没有注意到这一点,到是最近,偶然间看到了 <嗨翻C语言> 这本书,这里就详细讲解了这种可变参数函数的实现原理,今天考试间隙就顺带学习了一下,其实就是一

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

    目录 什么是可变参数? 使用可变参数 函数参数的内存对齐 总结 什么是可变参数? 有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数. C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数. 比如我们最常用的printf函数,它的函数声明是:int printf(const char *format, ...);该函数就是一个典型的应用可变参数的实例,后面那三个...就是说明该函数是可变参数函数. 使用可变参数 要使用可变

  • Scala可变参数列表,命名参数和参数缺省详解

    重复参数 Scala在定义函数时允许指定最后一个参数可以重复(变长参数),从而允许函数调用者使用变长参数列表来调用该函数,Scala中使用"*"来指明该参数为重复参数.例如: scala> def echo (args: String *) = | for (arg <- args) println(arg) echo: (args: String*)Unit scala> echo() scala> echo ("One") One sca

随机推荐