温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

C之函数参数(三十九)

发布时间:2020-06-19 11:27:21 来源:网络 阅读:716 作者:上帝之子521 栏目:编程语言

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

C之函数参数(三十九)

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

#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。我们来看看编译结果是否如我们所分析的那样,编译结果如下

C之函数参数(三十九)

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

C之函数参数(三十九)

        那么我们看到在 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;我么来看看结果是这样吗?

C之函数参数(三十九)

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

C之函数参数(三十九)

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

C之函数参数(三十九)

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

C之函数参数(三十九)

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

C之函数参数(三十九)

        我们看到它是执行完 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 行的这样的定义可行吗?来看看编译结果

C之函数参数(三十九)

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

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


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

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI