函数参数的求值顺序依赖于编译器的实现,我们来看看下面代码的输出是什么?为什么呢?
#include <stdio.h> int func(int i, int j) { printf("i = %d, j = %d\n", i, j); return 0; } int main() { int k = 1; func(k++, k++); printf("%d\n", k); return 0; }
我们理论上分析,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> int main() { int k = 2; int a = 1; k = k++ + k++; printf("k = %d\n", k); if( a-- && a ) { printf("a = %d\n", a); } return 0; }
我们看到第 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> float average(int array[], int size) { int i = 0; float avr = 0; for(i=0; i<size; i++) { avr += array[i]; } return avr / size; } int main() { int array[] = {1, 2, 3, 4, 5}; printf("%f\n", average(array, 5)); return 0; }
我们利用一个数组就完成这个功能,那么我们还得去定义一个数组。有什么办法可以使我们不用定义数组就可以完成这个功能呢?答案就是我们可以利用可变参数的函数来实现这个功能。在 C 语言中可以定义参数可变的函数,参数可变函数的实现依赖于 stdarg.h 头文件。我们得介绍几个概念:a> va_list -- 参数集合;b> va_arg -- 取具体参数值;c> va_start -- 标识参数访问的开始;d> va_end -- 标识参数访问的结束;
下来我们来看看可变参数版的程序是怎样实现的,代码如下
#include <stdio.h> #include <stdarg.h> float average(int n, ...) { va_list args; int i = 0; float sum = 0; va_start(args, n); for(i=0; i<n; i++) { sum += va_arg(args, int); } va_end(args); return sum / n; } int main() { printf("%f\n", average(5, 1, 2, 3, 4, 5)); printf("%f\n", average(4, 1, 2, 3, 4)); return 0; }
我们在第 6 行定义了 args 参数,在第 10 行开始,14 行进行参数的相加,在 17 行结束。我们来看看第24, 25 行的这样的定义可行吗?来看看编译结果
结果已经正确实现了,这样是不是很方便呢?我们可以随时定义它的大小和内容。那么可变参数也有限制:a> 可变参数必须从头到尾按照顺序逐个访问;b> 参数列表中至少要存在一个确定的命名参数;c> 可变参数函数无法确定实际存在的参数的数量,同样也无法确定参数的实际类型,只能我们手动指定;注意:va_arg 中指定了错误的类型,那么结果是不可预测的!
通过对函数参数的学习,总结如下:1、函数的参数在栈上分配空间;2、函数的实参并没有固定的计算次序;3.顺序点是 C 语言中变量修改的最晚时机;4、调用约定指定了函数参数的入栈顺序以及栈的清理方式;5、可变参数的函数提供了一种函数设计技巧,提供了一种更方便的函数调用方式;6、可变参数必须顺序的访问,无法直接访问中间的参数值。
欢迎大家一起来学习 C 语言,可以加我QQ:243343083。
原文:http://blog.51cto.com/12810168/2111046