从main函数参数,printf多参数来了解C语言可变参数函数


一般在C语言中,我们使用的main函数都是不带参数的,但实际上main函数是可以带参数的。

C语言规定main函数的参数只能有两个,习惯上这两个参数写为argc和argv。因此,main函数的函数头可写为:

main (argc,argv)

C语言还规定argc(第一个形参)必须是整型变量,argv(第二个形参)必须是指向字符串的指针数组。加上形参说明后,main函数的函数头应写为:

int main(int argc, char** argv)
或int main(int argc, char* argv[])

由于main函数不能被其它函数调用,因此不可能在程序内部取得实际值。那么,在何处把实参值赋予main函数的形参呢?实际上,main函数的参数值是从操作系统命令行上获得的。当我们要运行一个可执行文件时,在DOS提示符下键入文件名,再输入实际参数即可把这些实参传送到main的形参中去。

例如有命令行为:
C:>E24 BASIC foxpro FORTRAN
由于文件名E24本身也算一个参数,所以共有4个参数,因此argc取得的值为4。argv参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。 指针数组的长度即为参数个数。数组元素初值由系统自动赋予。其表示如图所示:

image-20210728214948116

main(int argc,char *argv){
    while(argc-->1)
        printf("%s\n",*++argv);
}

本例是显示命令行中输入的参数。如果上例的可执行文件名为e24.exe,存放在A驱动器的盘内。因此输入的命令行为:
C:>a:e24 BASIC foxpro FORTRAN
则运行结果为:
BASIC
foxpro
FORTRAN
该行共有4个参数,执行main时,argc的初值即为4。argv的4个元素分为4个字符串的首地址。执行while语句,每循环一次argv值减1,当argv等于1时停止循环,共循环三次,因此共可输出三个参数。在printf函数中,由于打印项*++argv是先加1再打印, 故第一次打印的是argv[1]所指的字符串BASIC。第二、三次循环分别打印后二个字符串。而参数e24是文件名,不必输出。

但是这带来了一个疑问:在C语言中是没有重载的,因此我们在声明定义函数时应该是要确定其有几个函数参数的。那为什么main函数可以不带参数,也可以带两个参数呢?

这个先按下不表,后面一起解答。


C语言中的printf函数应该是我们使用频率最高的库函数了吧?

我们在使用printf函数时,是可以根据需要选择输入参数的。

printf("%d",a);//两个参数
printf("%d %d",a ,b);//三个参数
printf("%d %d %d",a ,b ,c);//四个参数
……

看起来这个函数和我们之前用的函数时不一样的,之前我们用的函数都是必须确定函数参数,那为啥printf函数的参数是可变的呢?

看来这里面应该有一种机制来实现这种函数。先按下不表,我们来看看printf函数的源码实现

int printf
    (
    const char *  fmt,	/* format string to write */
    ...      		/* optional arguments to format string */
    )
    {
    va_list vaList;	/* traverses argument list */
    int nChars;

    va_start (vaList, fmt);
    nChars = fioFormatV (fmt, vaList, printbuf, 1);
    va_end (vaList);

    return (nChars);
    }
    
typedef struct {
	char *a0;	/* pointer to first homed integer argument */
	int offset;	/* byte offset of next parameter */
} va_list;

#define _INTSIZEOF(n)	( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)	( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)	( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)	( ap = (va_list)0 )

/*******************************************************************************
*
* printbuf - printf() support routine: print characters in a buffer
*/

LOCAL STATUS printbuf
    (
    char *buf,
    int nbytes,
    int fd
    )
    {
    return (write (fd, buf, nbytes) == nbytes ? OK : ERROR);
    }
    


C++中有函数重载这种方法,以供我们调用时要可以不确定实参的个数,其实 C 语言也可以,而且更高明!

我们在stdio.h 中可以看到 printf() 函数的原型:

int printf(char * format,…)

事实上,我们如果要写这样的函数也可以类似的写,那么在定义函数时用上这个符号“ … ” ,它叫占位符,喊它 “ 三个点 ” 也可以,只要你愿意!那么我可以这样定义我的函数:

fun(int a,…) { }

且听我介绍3 个小东东:

1、 va_list

2、 va_arg()

3、 va_start()

在学习这3 个小东东之前,我们先回忆一下, C 语言是怎么操作文件时,是怎么样处理内存中的数据的呢?学习文件操作时,我们提到了“流”的概念,我们用指针指向数据所在的内存地址,再一个一个的操作。

学习指针时,我们知道有函数指针这个东东,不是指针函数而是函数打针哦!(呵呵,我的同学如果还记得就当复习一下,不要嫌我啰嗦_ )。我们记得程序在执行时,会将函数存储到内存中去。现在深入的讲一点点,存储函数时,参数传递的过程是怎样实现的呢?所谓的形式参数(局部变量)实质上又是什么呢?把这些问题连起来想想,想通了,你的思维势如破竹!

在调用函数时,程序同样会把实参传入,在函数存储区保存起来,如果有很多参数,将一起保存起来。

这时候就要用到va_list 了,这是个类型定义,我们可以把它理解成一个指针,它指向第一个参数的地址。

如果,我们这样定义: va_list pp ;

则pp 就是这样一种变量,它是指向所有参数中的第一个参数的。它不同于一般的指针变量,它是个复合变量,什么是复合变量啊?结构体类型的嘛,呵呵。如果 a 是第一个参数,能不能写成 pp=a 呢?

假设我定义了char d[]=“ruixin”,e[]=“gelin”; 我要把 e 的值赋给 d ,能不能写成 d=e 呢?得用 strcpy() ,是吧!呵呵,一样的道理,这儿我们也用一个函数来实现,它就是 va_start();

如果这样写:va_start(pp,a);

那么pp 就指向第一个参数 a 了,并且可得到 a 的类型 int 。

这时候如果有下一个参数,就需要使pp 指向下一个参数,并且得到它的类型。同样需要使用函数来实现,这个函数是: va_arg()

可以这样写:va_arg(pp, 类型 ) ,这样 pp 就指向一个参数,并且可以得到那个参数的类型了。

注意!类型非常重要,学过指针的都应该清楚,指针的类型如果弄错的话,位置正确,取出来的数可能也是乱七八糟的。

下面我们看一个简单的例子:

#include <stdio.h>
#include<stdarg.h>
void fun(int a,...)
{
	va_list pp;
	int n=1;//使用 n 计量参数个数
	va_start(pp,a);
	do {
	printf("第 %d 个参数 =%d/n",n++,s);
	a=va_arg(pp,int);//使 pp 指向下一个参数,将下一个参数的值赋给变量 a
	}
	while (a!=0);//直到参数为 0 时停止循环
}
main()
{
	fun(20,40,60,80,0);
} 

注意!

一定要有上面两个文件包含命令,因为程序中用到的那3个小东东都在那个文件里。其实真正意义上应该说那是函数,实质上那不过是两个宏,呵呵。

VA_LIST 是在C语言中解决变参问题的一组宏

VA_LIST的用法:
(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针
(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。
(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型。
(4)最后用VA_END宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用VA_ARG获取各个参数。

VA_LIST在编译器中的处理:
1)在运行VA_START(ap,v)以后,ap指向第一个可变参数在堆栈的地址。
(2)VA_ARG()取得类型t的可变参数值,在这步操作中首先apt = sizeof(t类型),让ap指向下一个参数的地址。然后返回ap-sizeof(t类型)的t类型指针,这正是第一个可变参数在堆栈里的地址。然后用取得这个地址的内容。
(3)VA_END(),X86平台定义为ap = ((char*)0),使ap不再指向堆栈,而是跟NULL一样,有些直接定义为((void*)0),这样编译器不会为VA_END产生代码,例如gcc在Linux的X86平台就是这样定义的。

要注意的是:由于参数的地址用于VA_START宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。

使用VA_LIST应该注意的问题:
(1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
(2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。
小结:可变参数的函数原理其实很简单,而 VA系列是以宏定义来定义的,实现跟堆栈相关。我们写一个可变函数的C函数时,有利也有弊,所以在不必要的 场合,我们无需用到可变参数,如果在C++里,我们应该利用C++多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。


标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个“其他参数”。这种函数被称作“具有变长度参数表的函数”,或简称为“变参数函数”。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件<stdarg.h>,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。
一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。
假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):
int sum(int n, …)
我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。
为了能在变参数函数里取得并处理不定个数的“其他参数”,头文件<stdarg.h>提供了一套机制。这里提供了一个特殊类型va_list。在每个变参数函数的函数体里必须定义一个va_list类型的局部变量,它将成为访问由三个圆点所代表的实际参数的媒介。下面假设函数sum里所用的va_list类型的变量的名字是vap。在能够用vap访问实际参数之前,必须首先用“函数”a_start做这个变量初始化。函数va_start的类型特征可以大致描述为:
va_start(va_list vap, 最后一个普通参数)
实际上va_start通常并不是函数,而是用宏定义实现的一种功能。在函数sum里对vap初始化的语句应当写为:
va_start(vap, n);
在完成这个初始化之后,我们就可以通过另一个宏va_arg访问函数调用的各个实际参数了。宏va_arg的类型特征可以大致地描述为:
类型 va_arg(va_list vap, 类型名)
在调用宏va_arg时必须提供有关实参的实际类型,这一类型也将成为这个宏调用的返回值类型。对va_arg的调用不仅返回了一个实际参数的值(“当前”实际参数的值),同时还完成了某种更新操作,使对这个宏va_arg的下次调用能得到下一个实际参数。对于我们的例子,其中对宏va_arg的一次调用应当写为:
v = va_arg(vap, int);
这里假定v是一个有定义的int类型变量。
在变参数函数的定义里,函数退出之前必须做一次结束动作。这个动作通过对局部的va_list变量调用宏va_end完成。这个宏的类型特征大致是:
void va_end(va_list vap);
下面是函数sum的完整定义,从中可以看到各有关部分的写法:
int sum(int n, …) {
va_list vap;
int i, s = 0;
va_start(vap, n);
for (i = 0; i < n; i++) s += va_arg(vap, int);
va_end(vap);
return s;
}
这里首先定义了va_list变量vap,而后对它初始化。循环中通过va_arg取得顺序的各个实参的值,并将它们加入总和。最后调用va_end结束。
下面是调用这个函数的几个例子:
k = sum(3, 5+8, 7, 264);
m = sum(4, k, k
(k-15), 27, (k*k)/30);
在编写和使用具有可变数目参数的函数时,有几个问题值得注意。首先,虽然在上面描述了头文件所提供的几个宏的“类型特征”,实际上这仅仅是为了说明问题。因为实际上我们没办法写出来有关的类型,系统在预处理时进行宏展开,编译时即使发现错误,也无法提供关于这些宏调用的错误信息。所以,在使用这些宏的时候必须特别注意类型的正确性,系统通常无法自动识别和处理其中的类型转换问题。
第二:调用va_arg将更新被操作的va_list变量(如在上例的vap),使下次调用可以得到下一个参数。在执行这个操作时,va_arg并不知道实际有几个参数,也不知道参数的实际类型,它只是按给定的类型完成工作。因此,写程序的人应在变参数函数的定义里注意控制对实际参数的处理过程。上例通过参数n提供了参数个数的信息,就是为了控制循环。标准库函数printf根据格式串中的转换描述的数目确定实际参数的个数。如果这方面信息有误,函数执行中就可能出现严重问题。编译程序无法检查这里的数据一致性问题,需要写程序的人自己负责。在前面章节里,我们一直强调对printf等函数调用时,要注意格式串与其他参数个数之间一致性,其原因就在这里。
第三:编译系统无法对变参数函数中由三个圆点代表的那些实际参数做类型检查,因为函数的头部没有给出这些参数的类型信息。因此编译处理中既不会生成必要的类型转换,也不会提供类型错误信息。考虑标准库函数printf,在调用这个函数时,不但实际参数个数可能变化,各参数的类型也可能不同,因此不可能有统一方式来描述它们的类型。对于这种参数,C语言的处理方式就是不做类型检查,要求写程序的人保证函数调用的正确性。
假设我们写出下面的函数调用:
k = sum(6, 2.4, 4, 5.72, 6, 2);
编译程序不会发现这里参数类型不对,需要做类型转换,所有实参都将直接传给函数。函数里也会按照内部定义的方式把参数都当作整数使用。编译程序也不会发现参数个数与6不符。这一调用的结果完全由编译程序和执行环境决定,得到的结果肯定不会是正确的。
可以定义以va_list作为参数的函数,这里就不举例子了。
放一篇好文printf 函数实现的深入剖析

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小熊coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值