5.3 函数调用及执行过程

createh53周前 (12-05)技术教程20

5.3 函数调用及执行过程

上一节讲解了函数的定义方法和函数的一般设计原则。本讲将介绍如何使用自定义函数,以及在调用函数时的参数传递方法和函数的执行过程。

定义好的函数仅是一段不会被执行的代码,静静地呆着,在执行前不会产生任何作用。函数要起作用的前提是被其它函数调用,并且被执行。前面我们使用过了printf、scanf、getchar、sqrt等函数,这些函数都是别人写好的库函数,在程序中对这些函数的使用称为函数调用

正确调用函数需要知道函数的功能是什么、给函数“喂进”什么数据、函数完成后会返回什么结果?这是函数调用的三要素。如对scanf和printf的调用如下:

        scanf("%d%d%d",&x,&y,&z);
        printf("x=%d,y=%d,z=%d\n",x,y,z);

两个函数的调用都是由函数名、一对括号和一系列参数构成的,而且参数之间全部用逗号“,”号分隔,即:

       函数名( 实际参数列表 )

“函数名”代表了函数的代码段,“实际参数列表”表示要“喂给”函数的数据,要“喂给”函数多少个数据,每个数据的类型是什么?取决于函数定义时的形式参数的个数及类型。如果函数定义时没有形式参数,则调用时括号内也不要给任何数据。我们发现scanf和printf函数在调用时实际参数个数可多可少,且参数类型也是可变的,这是由于scanf、printf函数使用了可变形式参数的原因,这种情况相当复杂,学习者可自行查阅可变形式参数的定义方法。实际参数可以常量、变量或任何合法的表达式,在实际参数传递给形式参数之前要从右至左先完成计算和传递,被调用函数的实际参数要与其定义时的形式参数个数相同,对应参数的数据类型要相容。

函数分为“函数型”函数和“过程型”函数。前者具有返回值,后者没有返回值,其调用方法也略有不同。“函数型”函数既可以用作表达式中的运算量也可以用作语句,如:

       函数名( 实际参数列表 )

       函数名( 实际参数列表 );

用作语句时,只要在函数调用之后加上分号即可,前面的scanf和printf函数就是按语句使用的例子。

过程型”函数指的是函数类型为void的函数,这种函数没有返回值,因此不能作为运算量用在表达式中,只能作为语句的调用,即:

函数名(实际参数列表);

两个函数A、B发生关联的方法是函数调用。如果在函数A中调用了函数B,如:

A( )
{
    ...
   B( );
   ...
}

我们称函数A为主调用函数,函数B则称为被调用函数。这种调用关系表明了函数A的完成要依赖于B的执行结果。

接下来,看一下如何进行函数调用。函数必须先定义后使用,也就是被调用函数必须在主调用函数之前定义才能被调用。所以函数定义时要注意定义的先后顺序。先来看一个例子。

例1:求sum=3!+5!+7!+11!

分析题目,我们如果能写出求阶乘的函数fact,就可以调用函数fact四次累加后求出结果,如:

sum=fact(3)+fact(5)+fact(7)+fact(11);

可见,解题的关键是写出阶乘函数。让我们用IPO分析方法来进行函数的分析和设计。

(1)确定输入:函数要求某个整数的阶乘,因此只用一个输入,如n,即只需要定义一个形式参数。

(2)确定输出:函数要返回n!,所以,也只有一个输出一个变量f,其值表示n!。为了可以求更大的n的阶乘,变量f可用双精度实型(double)或长长整型(long long)表示。

(3)确定处理:可设变量f的初值为1,只需要把1至n的每个整数i依次乘到f中,即重复做f=f*i就能得到n!并放在f中,代码为:

      f=1;
      for(i=1;i<=n;i++)
           f=f*i;

根据(1)(2)可写出函数框架:

double fact(int n)
{
  函数体
  return f;
}

把(3)中处理的代码替换“函数体”并完善变量定义,求阶乘的函数定义如下:

double fact(int n)
{
double f;
int i;
 f=1;
for(i=1;i<=n;i++)
     f=f*i;
return f;
}

有了求阶乘函数后,主函数可调用完成指定的功能:

int main( )
{
double sum;
sum=fact(3)+fact(5)+fact(7)+fact(9);
printf("sum=%.0f\n",sum);
return 0;
 }

本例只涉及到两个函数,fact是被调用函数,main是主调用函数。所以fact应该在main之前定义,因此,完整的程序如下:

#include<stdio.h>
double fact(int n)
{
....
}
int main( )
{ ...
      sum=fact(3)+fact(5)+fact(7)+fact(9);
      ...     
}

如果不按被调用函数、主调用函数的顺序定义函数,即把被调用函数fact定义在主调用函数之后,会发生什么?

#include<stdio.h>
int main( )
{     ...
      sum=fact(3)+fact(5)+fact(7)+fact(9);
      ...     
}
double fact(int n)
{
....
}

以上程序将不能通过编译处理,因为在main中调用fact之前还未对fact函数进行定义,打破了“先定义后使用”的规则。解决之道是在调用函数前要对该函数进行函数声明,表明该函数在程序中是有定义的,可以被调用。

声明的格式和定义基本一样,不同的是:(1)函数的声明中的形式参数名可以省略,只保留参数类型即可;(2)函数声明可以与变量定义可以放在一起。如以下几种函数声明方法都是合法的:

(1)double fact(int n);
(2)double fact(int);
(3)double sum,fact(int);

方法(1)最简单,只是把函数定义的头部直接复制后粘贴在声明位置再加分号;方法(2)则省略了形式参数名,只保留每个形式参数数据类型名;方法(3)是把函数声明和变量定义混在一起。在main调用fact之前,先对fact函数进行声明后就可以正常调用了,main程序如下:

int main( )
{
double sum,fact(int);     //声明fact函数
sum=fact(3)+fact(5)+fact(7)+fact(9);
      ...     
}
double fact(int n)
{
....
}

我们再看一个函数设计和使用的例子。

例2:写一个用格里戈里公式求π的近似值的函数,要求最后一项的误差的绝对值小于指定的eps值。


先按自己喜欢给函数起一个名字如getPi,然后确定函数的输入和输出。根据题目的意思,输出的结果是由最后一项误差eps决定,因此函数形参只需要一个双精度实数eps。函数执行完后只需要返回一个结果,即PI的近似值,所以函数框架定义为:

double getPi(double eps)
{
   double pi;
   函数体
   return pi;
}

用前一章求PI的关键代码(见4.3)去替换“函数体”部分,进一步完善变量的定义,函数变为:

double getPi(double eps)
{
  double pi,item;
  int d,flag;
  item=1;d=1;flag=1;   //可以写为;item=d=flag=1;
  while( fabs(item)>=eps )
  {
    sum=sum+item;
    d=d+2;
    flag=-flag;
    item=flag*1.0/d;
  }
  pi=4*sum;
  return pi;
 }

我们可以多次调用getPi输出不同误差下的π的值,如:

int main( )
{
double Pi;
Pi=getPi(0.001);   
printf("Pi=%f\n",Pi);
Pi=getPi(0.0001);
printf("Pi=%f\n",Pi);
Pi=getPi(0.00001);
printf("Pi=%f\n",Pi);
return 0;
}

主调用函数和被调用函数的执行过程是怎样的呢?接下来该看一下函数的调用和执行过程了。

以主函数main调用函数getPi求π的近似值为例,过程是这样的:(1)先要保存调用时main中各种变量的值及当前的内存地址(设为A),以便函数执行完后能返回到正确的位置,接着转向getPi的程序段执行;(2)为被调用函数的形式参数自动分配存储空间,同时计算实际参数的值后分别给对应的形式参数赋值,然后开始执行被调用函数体中的语句,直到碰到return语句或到函数结束括号“}”;(3)由return把计算结果返回给主调用函数,无条件转向主调用函数调用点的位置(A),在主调用函数中把函数的计算结果赋给指定的变量Pi,然后继续执行函数调用后面的语句,此时函数名就像一个变量一样被使用。第二次和第三次调用的过程都是一样的。执行顺序示意图如下;


上图,getPi程序段被重复写了三次,只是为了观察调用过程方便而已,实际上是同一段程序代码。

再来看一个复杂的函数调用实例。当函数A调用函数B,函数B又调用函数C,函数C又用函数D,代码执行顺序是什么呢?


程序在执行函数A时,先执行语句1,然后转到B,执行语句3,转到C,接着执行语句5,转到D,执行语句7,转到E,在E 中执行语句9和语句10,然后返回D,执行语句8,返回C,执行语句6,返回B,执行语句4,返回A,执行语句2,结束函数A的调用并返回主调用函数。语句的执行顺序为:语句1->语句3->语句5->语句7->语句9->语句10->语句8->语句6->语句4->语句2。可见语句2是要把后面所函数执行完后才能执行到。

本节讲了函数的调用,以及在调用过程中实际参数与形式参数的传递方法,分析了函数的调用和执行过程。正确理解函数的调用和执行过程是阅读和编写程序的关键。本节就讲到这里,下次再见!

相关文章

C语言函数递归调用理解

函数除了在其他地方被调用之外,也可以自己调用自己(好家伙,套娃是吧),这种玩法我们称为递归。#include <stdio.h> void test(){ printf("...

mybatis调用流程

一、什么是MyBatis MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。可以对配置和原生Map使用简单的...

从零开始学习C语言丨函数的定义、调用和参数

国庆回归的第一篇文章,《从零开始学C语言》系列的第十篇。在前面的文章中,我们其实有见过函数,就是C语言程序有且仅有一个的主函数 main()。因此,可以说C语言程序中至少都会有一个函数。那么,接下来就...

CPU眼里的:函数调用 | 返回

“为什么有人说C/C++语言的函数返回,是最高效、脆弱的设计,让我们用CPU的视角一探究竟”01提出问题请问当函数执行完毕后,函数怎么知道:自己应该返回到哪里?它是否有走错路的可能?为什么有人说函数返...