我们上节博文讲了函数的意义,那么我们今天来讲下函数参数。函数参数在本质上与局部变量相同在栈上分配空间,函数参数的初始值是函数调用时的实参值。用下图来实际说明

函数参数的求值顺序依赖于编译器的实现,我们来看看下面代码的输出是什么?为什么呢?

#include<stdio.h>intfunc(inti,intj){printf("i=%d,j=%d\n",i,j);return0;}intmain(){intk=1;func(k++,k++);printf("%d\n",k);return0;}

我们理论上分析,func 函数先进行 k++,那么 i 就对应为 1,再次进行 k++,对应于 j 为 2。那么第 14 行应打印 i = 1, j = 2,。这时 k 为 3,所以第 16 行打印的值应为 3。我们来看看编译结果是否如我们所分析的那样,编译结果如下

我们看到 i 和 j 和我们所分析的正好相反,那么这是怎么回事呢?原来在gcc 编译器中,函数参数的实现是从右向左进行操作的,并非是我们所想的从左向右进行计算的。我们再在 BCC 编译器中进行编译,看看结果是怎样?

那么我们看到在 BCC 编译器中也是这样实现的。函数参数的操作是从右向左的,在现代的编译器中,基本上是按照从右向左的顺序进行函数参数的操作的。在一些古老的编译器中,也有从左向右的实现,这个的实现就依赖于具体的编译器的实现了。

下来我们来讲一个 C 语言中的知识点:顺序点!那么在程序中存在一定的顺序点,顺序点是指执行过程中修改变量值的最晚时刻,在程序到达顺序点的时候,之前所做的一切操作必须完成。那么 C 语言中的顺序点都在那些时刻呢?a> 每个完整表达式结束时,即分号处;b> &&,||,?: 以及逗号表达式的每个参数计算之后;c> 函数调用时所有实参求值完成后(即进入函数之前);

下面我们以代码为例进行分析,代码如下

#include<stdio.h>intmain(){intk=2;inta=1;k=k+++k++;printf("k=%d\n",k);if(a--&&a){printf("a=%d\n",a);}return0;}

我们看到第 8 行的进行两次 k++ 的相加,我们分析结果应该为 5;第 12 行的 a-- 执行完之后 a 为 0,但是此时它和 a 相与之后条件仍然为真,所以 第14行应该打印出 a = 0;我么来看看结果是这样吗?

那么我们看到我们分析的第一个是正确的,但是 a = 0 并没有打印出来,我们再来看看 BCC 编译器是多少

我们看到竟然 k = 6,a = 0 依然没有打印出来。我们再来看看 VS 编译器

我们进到反汇编看看它是怎么执行的


我们看到它是这样执行的,先是进行相加操作,这时的++操作被悬挂起来,程序看到;才意识到到了顺序点了,所以执行完那两次++操作,所以最后 k 的值为6。我们再来看看第14行怎么执行的

我们看到它是执行完 a-- 后看到 && 操作便意识到顺序点到了,便返回了。那么这时 a 的值已经变为 0 了,此时 if 语句条件为假,所以不会执行到它里面的打印语句。

下来我们再来看看参数入栈的顺序,函数参数的计算次序是依赖编译器实现的。那么函数参数的入栈次序是如何确定的呢?这块就涉及到里一个概念:调用约定。当函数调用发生时:a> 参数会传递给被调用的函数;b> 而返回值会被返回给函数调用者;调用约定描述参数如何传递到栈中以及栈的维护方式,参数传递顺序,调用栈清理。

调用约定是预定义的可理解为调用协议,调用约定通常用于库调用和库开发的时候。我们来看看一些常用的操作:a>从右到左依次入栈:__stfcall, __cdecl, __thiscall;b> 从左到右依次入栈:__pascall, __fastcall;那么我们一般的 C 程序开发遵循的就是上面的 __cdecl 这种方式的。

那么我们如果要编写一个计算平均数的函数,我们肯定首先想到的是下面这种

#include<stdio.h>floataverage(intarray[],intsize){inti=0;floatavr=0;for(i=0;i<size;i++){avr+=array[i];}returnavr/size;}intmain(){intarray[]={1,2,3,4,5};printf("%f\n",average(array,5));return0;}

我们利用一个数组就完成这个功能,那么我们还得去定义一个数组。有什么办法可以使我们不用定义数组就可以完成这个功能呢?答案就是我们可以利用可变参数的函数来实现这个功能。在 C 语言中可以定义参数可变的函数,参数可变函数的实现依赖于 stdarg.h 头文件。我们得介绍几个概念:a> va_list -- 参数集合;b> va_arg -- 取具体参数值;c> va_start -- 标识参数访问的开始;d> va_end -- 标识参数访问的结束;

下来我们来看看可变参数版的程序是怎样实现的,代码如下

#include<stdio.h>#include<stdarg.h>floataverage(intn,...){va_listargs;inti=0;floatsum=0;va_start(args,n);for(i=0;i<n;i++){sum+=va_arg(args,int);}va_end(args);returnsum/n;}intmain(){printf("%f\n",average(5,1,2,3,4,5));printf("%f\n",average(4,1,2,3,4));return0;}

我们在第 6 行定义了 args 参数,在第 10 行开始,14 行进行参数的相加,在 17 行结束。我们来看看第24, 25 行的这样的定义可行吗?来看看编译结果

结果已经正确实现了,这样是不是很方便呢?我们可以随时定义它的大小和内容。那么可变参数也有限制:a> 可变参数必须从头到尾按照顺序逐个访问;b> 参数列表中至少要存在一个确定的命名参数;c> 可变参数函数无法确定实际存在的参数的数量,同样也无法确定参数的实际类型,只能我们手动指定;注意:va_arg 中指定了错误的类型,那么结果是不可预测的!

通过对函数参数的学习,总结如下:1、函数的参数在栈上分配空间;2、函数的实参并没有固定的计算次序;3.顺序点是 C 语言中变量修改的最晚时机;4、调用约定指定了函数参数的入栈顺序以及栈的清理方式;5、可变参数的函数提供了一种函数设计技巧,提供了一种更方便的函数调用方式;6、可变参数必须顺序的访问,无法直接访问中间的参数值。


欢迎大家一起来学习 C 语言,可以加我QQ:243343083。