函数

1. 函数的定义和调用

函数简单来说就是一连串的语句,这些语句被组合在一块,并取了一个名字。函数是C程序的构建块。每个函数本质上是一个自带声明和语句的小程序。所以可以利用函数把程序划分成小块,这样便于理解和修改程序。
例子:

double average(double a,double b)
{
    return (a+b)/2;
}
  • 位于函数开始处的单词double表示average函数的返回类型,也就是每次调用该函数时返回数据的类型。
  • 标识符a和b(即函数的形式参数)表示在调用average函数时需要提供的两个数。每一个形式参数都必须有类型(正像每个变量有类型一样),这里选择了double作为a和b的类型(这里看上去有些奇怪,但是单词double必须出现两次:一次为a,一次为b),函数的形式参数本质上是变量,其初始值在调用函数的时候才提供。
  • 每个函数都有一个带有花括号的执行部分,称之为函数体。因此,每一个函数体都是一个复合语句。average函数的函数体由一对花括号,以及其中的return语句组成。执行return语句会使函数“返回”调用它的地方,表达式(a+b)/ 2的值作为函数的返回值。

为了调用函数,需要写出函数名及跟随其后的实际参数列表。例如,average(x,y)是对average函数的调用。实际参数用来给函数提供信息。在此例中,函数average需要知道求哪两个数的平均值。调用average(x,y)的效果就是把变量x和变量y的值复制给形式参数a和b,然后执行average函数的函数体。实际参数不一定是变量,任何正确类型的表达式都可以,average(5.1,8.9)和average(x/2,y/3)都是合法的函数调用。

  • 其实也可以这样写,printf("Average: %g\n",average(x,y));这条语句产生如下效果:

    • 以变量x和变量y作为实际参数使用调用average函数。
    • 把x和y的值复制给a和b。
    • average函数执行自己的return语句,返回a和b的平均值。
    • printf函数显示出函数average的返回值(average函数的返回值成了函数printf的一个实际参数)。
  • Notice:

    • 我们没有保存average函数的返回值,程序显示这个值后就把它丢弃了。如果需要在稍后的程序中用到返回值,可以把这个返回值赋值给变量。
    • avg = average(x,y);
    • 这条语句调用了average函数,然后把它的返回值存储在变量avg中。
  • 不是每一个函数都返回一个值。例如,进行输出操作的函数可能不需要返回任何值。为了指示出不带返回值的函数,需要指出这类函数的返回类型是void。(void是一个没有值的类型)。

void printf_count(int n)
{
    printf("T minus %d and counting\n",n);
}
  • print_count有一个形式参数n,,参数的类型为int。此函数没有任何返回值,所以用void指明它的返回值类型,并且省略了return语句。既然print_count函数没有返回值,那么不能使用调用average函数的方法来调用。print_count函数的调用必须自成一个语句。

  • print_count(i);

  • 有些函数根本没有形式参数。

void print_pun(void)
{
    printf("To c,or not to c:that is the questiong.\n");
}
  • 圆括号中的单词void表明print_pun函数没有实际参数。(这里使用void作为占位符,表示这里没有任何东西)
  • 调用不带实际参数的函数时,只需要写出函数名,并且在后面加上一对圆括号: print_pun(); 即使没有实际参数也必须给出圆括号。

1.1 函数定义

【函数定义】 返回类型 函数名(形式参数) 复合语句

  • 函数的“返回类型”是函数返回值的类型。下列一些规则用来管理返回类型:

    • 函数不能返回数组,但关于返回类型没有其他限制。
    • 指定返回类型是void类型,说明函数没有返回值。
    • 如果省略返回类型,C89会假定函数返回值的类型是int类型,但在C99中这是不合法的。
  • 函数名后边有一串形式参数列表。需要在每个形式参数的前面说明其类型,形式参数间用逗号进行分割。如果函数没有形式参数,那么圆括号内应该出现void。注意:即使几个形式参数具有相同的数据类型,也必须分别说明每个形式参数的类型。接下来列举一个错误的例子:

    • double average(double a, b) / wrong /
  • 例子:

double average(double a,double b)
{
    double sum;

    sum = a + b;
    return sum/2;
}
  • 函数体内声明的变量专属于此函数,其他函数不能对这些变量进行检查或修改。在C89中,变量的声明必须出现在语句之前。在C99中,变量声明和语句可以混在一起,只要变量在一次使用之前进行声明就行。

  • 对于返回类型为void的函数(本书称之为“void函数”),其函数体可以只是一堆花括号:

    • void print_pun(void) { };
    • 函数开发过程中留下函数体是有意义的。因为没有时间完成函数,所以为它预留下空间,以后可以再回来编写它的函数体。

1.2 函数调用

函数调用由函数名和跟随其后的实际参数列表组成,其中实际参数列表用圆括号括起来:

average(x,y)
print_count(i)
print_pun()
  • 如果丢失圆括号,那么将无法进行函数调用
  • 如 print_pun; / Woring /
  • 这样的结果是合法的表达式语句,虽然看上去正确,但是不起任何作用。一般来说,一些编译器会发出一些警告。

void函数调用的后边始终跟着分号,使得该调用成为语句。

  • Print_count(i);
  • print_pun();

另外,非void函数调用会产生一个值,该值可以存储在变量中,进行测试,显示或者用于其他用途:

    avg = average(x,y);
    if(average(x,y) > 0)
    printf("Average is positive\n");
    printf("The average is %g\n",average(x,y));
  • 如果不需要非void函数返回的值,总是可以将其丢弃:

    • average(x,y);
    • average函数的调用就是一个表达式语句的例子:语句计算出值,但是不保存它。
  • 注意,main函数包含一个名为n的变量,而is_prime函数的形式参数也叫n。一般来说,在一个函数中可以声明与另一个函数中的变量同名的变量。这两个变量在内存中的地址不同,所以给其中一个变量赋新值不会影响另一个变量。

2. 函数声明

函数的定义总是放置在调用点的上面。事实上,C语言并没有要求函数的定义必须放置在调用点之前。以下有一个例子:

# include <stdio.h>

int main(void)
{
    double x,y,z;

    printf("Enter three number: ");
    scanf("%lf%lf%lf",&x,&y,&z);
    printf("Average of %g and %g: %g\n",x,y,average(x,y));
    printf("Average of %g and %g: %g\n",y,z,average(y,z));
    printf("Averagr of %g and %g: %g\n",x,z,average(x,z));

    return 0;
}

double average (double a,double b)
{
    return (a + b)/2;

}
  • 当遇到main函数中第一个average函数调用时,编译器没有任何关于average函数的信息:编译器不知道averag函数有多少形式参数,形式参数的类型是什么,也不知道average函数的返回值是什么类型。但是,编译器不会给出错消息,而是假设average函数返回int型的值。我们可以说编译器为该函数创建一个隐式声明。编译器无法检查传递给average的实参个数和实参类型,只能进行默认实参提升并期待最好的情况发生。当编译器在后面遇到average的定义时,它会发现函数的返回类型实际上是double而不是int,从而我们得到一条出错消息。
  • 为了避免定义前调用的问题,一种方法是使每个函数的定义都出现在其调用之前。可惜的是,有时候无法进行这样的安排;即使可以这样安排,程序也会因为函数定义的顺序不自然而难以阅读。
  • 幸运的是,C语言提供了一种更好的解决方法:在调用前声明每个函数。函数声明使得编译器可以先对函数进行概要浏览,而函数的完整定义以后再给出。函数声明类似于函数定义的第一行,不同之处是在结尾处有分号。
    • 【函数声明】 返回类型 函数名(形式参数);
    • 无需多言,函数的声明必须与函数的定义一致。

以下是为average函数添加了声明后程序的样子:

# include <stdio.h>
double average (double a,double b);

int main(void)
{
    double x,y,z;

    printf("Enter three number: ");
    scanf("%lf%lf%lf",&x,&y,&z);
    printf("Average of %g and %g: %g\n",x,y,average(x,y));
    printf("Average of %g and %g: %g\n",y,z,average(y,z));
    printf("Averagr of %g and %g: %g\n",x,z,average(x,z));

    return 0;
}

double average (double a,double b)
{
    return (a + b)/2;

}
  • 顺便说一句,函数原型不需要说明函数形式参数的名字,只要显示他们的类型即可。
    • double average(double, double);
    • 通常最好不要省略形参的名字,因为这些名字可以说明每个形参的目的,并且提醒程序员在函数调用实参的出现次序。当然,省略形参的名字也有一定的道理。
    • C99遵循这样的规则:在调用一个函数之前,必须先对其进行声明或定义。调用函数时,如果此前编译器未见到该函数的声明或定义,会导致出错。

3. 实际参数

复习一下形式参数和实际参数之间的差异。形式参数出现在函数定义中,它们以加名字来表示函数调用时需要提供的值;实际参数时出现在函数调用中的表达式。在形式参数和实际参数的差异不是很重要的时候,有时会用参数表示两个两者中的任意一个。

  • 在C语言中,实际参数是值传递的:调用函数时,计算出每个实际参数的值,并把它赋给相应的形式参数。在函数执行过程中,对形式参数的改变不会影响实际参数的值,这是因为形式参数中包含的是实际参数值的副本。从效果上来说,每个形式参数的行为好像是把变量初始化成与之匹配的实际参数的值。
  • 实际参数的值传递既有利也有弊 。因为形式参数的修改不会影响到相应的实际参数,所以可以把形式参数作为函数内的变量来使用,这样可以减少真正需要的变数数量。

家人们,思考下面这个例子:

    int power(int x,int n)
    {
        int i,result = 1;
        for(i = 1;i<= n;i++)
        result = result * x;

        return result;

    }
  • 此函数用来计算数x的n次幂,因为n是原始指数的副本,所以可以在函数体内修改它。

可惜的是,C语言对实际参数值传递的要求使它很难编写某些类型的函数。例如,假设我们需要一个函数,它把double型的值分解成小数部分和整数部分。因为函数无法返回两个数,所以可以尝试把两个变量船队给函数并修改它们:

    void decompose(double x,long int int_part,double frac_part)
    {
        int_part = (long)x;
        farc_part = x - int_part;

    }

    int main()
    {
        decompose(3.14159,i,d);

    }
  • 在调用开始时,程序把3.14159赋值给x,把i的值复制给int_part,而把d的值复制给了farc_part。然后decompose函数内的语句把3赋值给int_part,把.14159赋值给farc_part,接着函数返回。可以呀,变量d和i不会因为赋值给他们而受到影响,所以它们在函数调用前后的值完全是一样。

3.1 实际参数的转化

C语言允许在实际参数的类型与形式参数的类型不匹配的情况下进行函数调用。管理如何转化实际参数的规则与编译器是否调用前遇到函数的原型(或者函数的完整定义)有关。

  • 编译器在调用前遇到原型。就像使用赋值一样,每个实际参数的值被隐式地转换成相应形式参数的类型。例如,如果把int类型地实际参数传递给期望得到double类型数据的函数,那么实际参数会被自动转换成double类型。
  • 编译器在调用前没有遇到原型。编译器执行默认实参提升:
    1.把float类型的实际参数转换成double类型
    2.执行整值提升,即把char类型和short类型的实际参数转换成int类型。(C99实现了整数提升。)

观察下面这个例子

# include <stdio.h>
int main()
{
    double x = 3.0;
    printf("Square:%d\n",square(x));

    return 0;    
}

int Square(int n)
{
    return n*n;

}
  • 在调用square函数时,编译器没有遇到原型,所以他不知道square函数期望int类型的实际参数。因此,编译器在变量x上执行了没有效果的,默认实参提升。因此为square函数期望int类型的实参,却获得了double类型值,所以square函数将产生无效的结果。通过把自己square的实际参数强制转换为正确的类型,可以解决这个问题。
    • printf("Square:%d\n",square((int) x));
    • 当然更好的解决方案是在调用square前提供该函数的原型。在C99中,调用square之前不提供声明或定义是错误的。

3.3 数组型实际参数

数组经常被用作实际参数。当形式参数是一维数组时,可以不用说明数组的长度:

int f(int a[])
{
    ……
}
  • 实际参数可以是元素类型正确的任何一维数组。只有一个问题:f函数如何知道数组是多长呢?可惜的是,C语言没有为函数提供任何简便的方法来确定传递给它的数组的长度;如果函数需要,我们必须把长度作为额外的参数提供出来。

下面的函数说明了一维数组型实际参数的用法。当给出具有int类型值的数组a时,sum_array函数返回数组a中元素的和。因为sum_array函数需要知道数组a的长度,所以必须把长度作为第二个参数提供出来。

    int sum_array(int a[],int n)
    {
        int i,sum = 0;
        for(i = 0;i < n; i++)
        sum += a[i];

        return sum;

    }
  • sum_array函数的原型有下列形式:
    • int sum_array(int a[],int n)
  • 通常情况下,如果愿意的话,则可以省略形式参数的名字:
    • int sum_array(int [],int);
  • 在调用sum_array函数时,第一个参数是数组的名字,第二个参数是这个数组的长度。例如:
#define LEN 100

int main(void)
{
    int b[LEN],total;
    ……
    total = sum_array(b,LEN);
    ……
}
  • 注意,在把数组名传递给函数时,不要在数组名的后边放置方括号:
    • total = sum_array(b[],LEN); //Wrong
  • 一个关于数组型实际参数的重要论点:函数无法检测传入的数组长度的正确性。我们可以利用这一点来告诉函数,数组的长度比实际情况小。假设,虽然数组b有100个元素,但实际仅存储了50个数。通过书写下列语句可以对数组的前50个元素进行求和:
    • total = sum_array(b,50);
    • sum_array函数将忽略另外50个元素。
  • 注意不要告诉函数,数组型实际参数比实际情况大:
    • total = sum_array(b,150); //Wrong
    • 在这个例子中,sum_array函数将超出数组的末尾,从而导致未定义的行为。

-一个关于数组型实际参数另一个重要论点:函数可以改变数组型形式参数的元素,并且改变会在相应的实际参数中体现出来。例如,下面的函数通过在每个数组元素中存储0来修改数组:

    void store_zeros(int a[],int n)
    {
        int i;

        for(i = 0;i < n;i++ )
            a[i] = 0;
    }
  • 函数调用: store_zeros(b,100); 会在数组b的前100个元素中存储0。数组型实际参数可以修改,这似乎与C语言中的实际参数的值传递相矛盾。事实上这并不矛盾,但现在没法解释。如果想知道请继续关注后续章节笔记。

  • Notice:如果形式参数是多维数组,声明参数时只能省略第一维的长度。例如,如果修改sum_array函数使得a是一个二维数组,我们可以不指出行的数量,但是必须指定列的数量:

#define LEN 10
int sum_two_dimensional_array(int a[][LEN],int n)
{
    int i,j,sum = 0;
    for(i = 0;i < n;i++)
        for(j = 0;j < LEN; j++)
            sum += a[i][j];

    return sum;

}
  • 不能传递具有任意列数的多维数组是很讨厌的。幸运的是,我们经常可以通过使用指针数组解决这种困难。C99中的变长数组形式参数则提供了一种更好的解决方案。

3.4 变长数组形式参数

  • C99增加了几个与数组型参数相关的特性。第一个是变长数组,这一特性允许我们用非常量表达式指定数组的长度。变长数组也可以作为参数。
  • 考虑本节前面提到过的函数sum_array,这里给出它的定义,省略了函数体的部分:
    int sum_array(int a[],int n)
    {
        ……

    }
  • 这样的定义使得n和数组a的长度之间没有直接的联系。尽管函数体会将n看作数组a的长度,但是数组的实际长度有可能比n大(也可能比n小,这种情况下函数不能正确地运行)。
    • 如果使用变长数组形式参数,我们可以显式地说明数组a的长度就是n:
    int sum_array(int n,int a[n])
    {
        ……

    }
  • 第一个参数n的值确定了第二个参数a的长度。注意,这里交换了形式参数的顺寻,使用变长数组形式参数时参数的顺序很重要。

  • 下面的sum_array函数定义是非法的:

    int sum_array(int a[n],int n)
    {
        ……

    }
- 编译器在遇到int a[n]时显示此前它没有见过n。
  • 对于新版本的sum_array函数,其函数原型有好几种写法。一种写法是使其看起来跟函数定义一样:

    • int sum_array(int n ,int a[n]);
  • 另一种写法是用 *取代数组长度:

    • *int sum_array(int n,int a[]);**
    • 使用 *的理由如下。函数声明时,形式参数的名字是可选的。如果第一个参数定义被忽略了,那么就没有办法说明数组a的长度是n,而星号的使用则为我们提供了一个线索——数组长度与形式参数列表中前面的参数相关:
    • *int sum_array( int, int [ ])**
  • 另外,方括号为空也是合法的。在声明数组参数时我们经常这么做:

    • int sum_array( int n, int a[ ] );
    • int sum_array( int ,int [n]);
  • 但是让括号为空不是一个很好的选择,因为这样并没有说明n和a之间的关系。

  • 一般来说,变长数组形式参数的长度可以是任意表达式。例如,假设我们要编写一个函数来连接两个数组a和b,要求先复制a的元素,再复制b的元素,把结果写入第三个数组c:

    int concatenate(int m,int n,int a[m],int b[n],int c[m+n])
    {
        ……

    }
  • 数组C的长度是a和b的长度之和。这里用于指定数组C长度的表达式只用到了另外两个参数:但一般来说,该表达式可以使用函数外部的变量,甚至可以调用其它函数。
  • 到目前为止,我们所举的例子都是一维变长数组形式参数,变长数组的好处还体现不够充分。一维变长数组形式参数通过指定数组参数的长度使得函数的声明和定义更具描述性。但是,由于没有进行额外的错误检测,数组参数仍然有可能太长或太短。
  • 如果变长数组参数是多维的,则更加实用。之前,我们尝试过写一个函数来实现二维数组中元素相加。原始的函数要求数组的列数固定。如果使用变长数组形式参数,则可以推广到任意列数的情况:
    int concatenate(int m,int n,int a[m+n])
    {
        int i,j,sum = 0;

        for(i = 0;i < m;i++)
            for(j = 0;j < n;j++)
            {
                sum += a[i][j];

            }

        return sum;
    }
  • 这个函数的原型可以是以下几种:
    1.int sum_two_dimensional_array(int m,int n,int a[m][n]);
    2.int sum_two_dimensional_array(int m,int n,int a[][]);
    3.int sum_two_dimensional_array(int m,int n,int a[][n]);
    4.int sum_two_dimensional_array(int m,int n,int a[][*]);

4. 复合字面量

  • 再来看看sum_array函数。当调用sum_array函数时,第一次参数通常是(用于求和的数组的名字。例如可以这样调用sum_array:)
    • int b[ ] = {3,0,3,4,1};
    • total = sum_array(b,5);
  • 这样做唯一问题是需要把b作为一个变量声明,并在调用前进行初始化。如果b不作他用,这样做其实有点浪费。
  • 在C99中,可以使用符合字面量来避免该问题,复合字面量是通过指定其包含的元素而创建的没有名字的数组。下面调用sum_array函数,第一个参数就是一个复合字面量:
    • total = sum_array( (int [ ]) {3, 0, 3, 4, 1},5)
  • 在这个例子中,复合字面量的格式如下:先在一对圆括号内给定类型名,随后是一个 初始化器,用来指定初始值。因此,可以在复合字面量的初始化器中指示器一样,而且同样可以不提供完全的初始化(未初始化的元素默认被初始化为0)。
  • 函数内部创建的复合字面量可以包含任意的表达式,不限于常量。例如:
    • *taotal = sum_array( (int [ ]) {2乘i, i + j, k },5)**
    • 其中i,j,k都是变量。复合字面量的这一大特性极大的增加了其实用性。
    • 复合字面量为左值,所以其元素的值可以改变。如果要求其值为“只读”,可以在类型前加上const,如(const int [ ] ){ 5 , 4 };

5.return语句

非void的函数必须使用return语句来指定将要返回的值。return语句有如下格式:
【return语句】 return 表达式;

  • 该表达式经常只有常量或变量:

    • return 0;
    • return status;
  • 但他也有可能是更加复杂的表达式。例如,在return语句的表达式中看到条件运算符是很平常的:

    • return n >= 0 ? n : 0;
  • 如果return语句中表达式的类型和函数的返回类型不匹配,那么系统会把表达式的类型隐式地转换成返回类型。例如,如果声明函数返回int的值,但是return语句包含double类型表达式,那么系统会把表达式的值转化为int类型。

  • 如果没有给出表达式,return语句可以出现在返回类型void的函数中:

    • return;
  • return语句不是必须的,因为在执行完最后一条语句后函数将自动返回。

  • 如果非void函数到达了函数体的末尾(也就是说没有执行return语句),那么如果程序试图使用函数的返回值,其行为是未定义的。

6. 程序终止

既然main是函数,那么它必须要有返回类型。正常情况下,main函数的返回类型是int类型,因此我们目前见到的main函数都是这样定义的:

  • int main(void) { …… }
  • 省略main函数参数列表中的void是合法的,但是最好显式地表明main函数没有没有参数。

main函数返回的是状态码,在某些操作系统中程序终止可以检测到状态码。如果程序正常终止,main函数应该返回0;为了表示异常终止,main函数应该返回非0的值(实际上,这一返回值也可以用于其他目的。)即使不打算使用状态码,确保每个C程序都返回状态码也是一个很好的实践,因为以后运行程序的人可能需要测试状态码。

  • exit函数
  • 在main函数中执行return语句是终止程序的一种方法,另一种方法是调用exit函数,此函数属于头。传递给exit函数的实际参数和main函数的返回值具有相同的含义:两者都说明程序终止时的状态。为了表示正常终止,传递0:
    • exit(0);
  • 因为0有点模糊,所以C语言允许用EXIT_SUCCESS来代替(效果是相同的):
    • exit(EXIT_SUCCESS); //终止成功
      • exit(EXIT_FAILURE); //终止异常
  • EXIT_SUCCESS和EXIT_FAILURE都是定义在中的宏。通常是0和1。

作为终止程序的方法,return语句和exit函数关系亲密。事实上,main函数中的语句 return 表达式 ; 等价于 exit(表达式);
return语句和exit函数之间的差异是,不管是那个函数调用exit函数都会导致程序终止,return语句仅当由main函数调用时才会导致程序终止。一些程序员只使用exit函数,以便更容易定位程序中的全部退出点

7. 递归

如果程序调用它本身,那么此函数就是递归的。
接下来是递归的另一个示例

int power(int x,int n)
{
    if (n == 0)
    {
        if(n == 0)
            return 1;
        else
        return x * power(x,n-1);
    }
}
  • 调用power(5,3)将按照如下方式进行

    1. power(5,3)发现3不等于0,所以power(5,3)调用
    2. power(5,2),此函数发现2不等于0,power(5,1)调用
    3. power(5,0),此函数发现0等于0,所以返回1,从而导致
    4. power(5,1)返回5x1=5,从而导致
    5. power(5,2)返回5x5=25,从而导致
    6. power(5,3)返回5x25=125
  • 一旦调用,fact函数和power函数就会仔细地测试“终止条件”。调用fact函数时,他会立刻检查参数是否小于等于1;调用power函数时,它先检查第二个参数是否等于0。为了防止无限递归,所有的递归函数都需要某些类型的终止条件。

分类: C

易碎

易碎

我看到的今夜的星空,是几万年前的光,我眼中的你是此时的你!

0 条评论

发表回复

Avatar placeholder

您的邮箱地址不会被公开。 必填项已用 * 标注

网站ICP备案皖ICP备2024045222号-1