第9章 函数

作者: 橡树人 | 来源:发表于2020-02-28 07:47 被阅读0次

英文原版:P183

在C语言中,什么是函数?

  • 函数是一连串被组合到一起、有名字的语句。
  • 每个函数本质上是一个小程序,自带声明和语句。
  • 在C语言中,一个函数不一定会有参数,也不一定会计算一个值。

在C语言中,函数有什么作用?

  • 函数是构建C程序的基础。
  • 使用函数可以将一个程序划分成更易于理解和修改的小块。
  • 使用函数可以避免编写被多次使用的代码,从而消除编程中的一些乏味。
  • 函数是可以复用的:取来自程序A中的函数,在程序B中使用。

本章的主要内容有:

  • 9.1节介绍如何定义和调用函数。
  • 9.2节介绍函数声明,以及函数声明跟函数定义的区别。
  • 9.3节介绍如何给函数传递参数。
  • 9.4节介绍return语句。
  • 9.5节介绍跟程序终止相关的问题。
  • 9.6节介绍递归。

9.1节 函数定义和函数调用

函数定义

返回值类型 函数名(形式参数)
{
  声明
  语句
}

针对返回值类型有如下几条规则:

  • 函数的返回值类型不能是数组;
  • 如果返回值类型是void,则表明该函数没有返回值;
  • 在C89中,如果一个函数省略了返回值类型,则假设函数的返回值类型为int;在C99中,省略函数的返回值类型是非法的。

针对形式参数有几点说明:

  • 每个形式参数的前面是参数类型;
  • 形式参数间用逗号分隔;
  • 如果函数没有任何参数,则括号里应该有void;
  • 即使多个形式参数有相同的类型,也必须对每个参数进行单独的声明。

针对函数体有几点说明:

  • 函数体可以包含声明和语句;
  • 函数体内声明的变量专属于此函数,不能被其他函数修改或者检查;
  • 对于返回值是void类型的函数,函数体可为空;

例1 错误的函数定义

double average(double a, b){
}

例2 有返回值且有形式参数的函数定义

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

例3 无返回值有形式参数的函数定义

void print_count(int n) {
  printf("T minus %d and counting\n", n);
}

例4 无返回值无形式参数的函数定义

void print_pun(void) {
  printf("To C, or not to C: that is the question.\n");
}

函数调用

格式:

函数名(参数列表)

例1 函数调用示例

average(x, y)
print_count(i)
print_pun()

例2 返回值是void的函数调用语句

print_count(i);
print_pun();

例3 保存一个非void函数的调用语句

avg = average(x, y);
if (average(x, y) > 0) {
  printf("Average is positive\n");
}
printf("The average is %g\n", average(x, y));

例4 丢弃非void函数返回值的调用语句

average(x,y);

程序示例1:计算平均值

源文件:average.c

#include <stdio.h>

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

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

    printf("Enter three numbers: ");
    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("Average of %g and %g: %g\n", z, x, average(z, x));

    return 0;
}

程序示例2:输出递减计数器

源文件:countdown.c

#include <stdio.h>

void print_count(int n){
  printf("T minus %d and counting\n", n);
}

int main(void) {
    int i;

    for(i=10; i>0; --i){
        print_count(i);
    }

    return 0;
}

程序示例3:打印双关语

源文件pun2.c

#include <stdio.h>

void print_pun(void)
{
  printf("To C, or not to C: that is the question\n");
}

int main(void)
{
    print_pun();

    return 0;
}

程序示例4:检测一个数是不是素数

源文件:prime.c

#include <stdio.h>
#include <stdbool.h>

bool is_prime(int n)
{
  int divisor;

  if (n < 1) {
    return false;
  }

  for (divisor = 2; divisor * divisor <= n; divisor++) {
    if (n % divisor == 0) {
        return false;
    }
  }

  return true;
}

int main(void)
{
    int n;

    printf("Enter a number: ");
    scanf("%d", &n);

    if (is_prime(n)) {
        printf("Prime\n");
    }else {
        printf("Not prime\n");
    }

    return 0;
}

9.2节 函数声明

函数声明格式:

返回值类型 函数名(形式参数列表);

规则:

  • 在函数调用前必须进行声明
  • 函数声明必须跟函数定义保持一致

注:
这种函数声明就是函数原型。

函数原型(函数声明)有什么作用?

  • 告诉编译器这是一个函数的简介,该函数的定义会在稍后给出;
  • 描述如何调用一个函数:要提供多少个实际参数,这些实际参数的类型是什么,返回值是什么类型等;
  • 函数原型不一定要列出形式参数的名字,但一般不建议这么做;

例1 计算平均数
average2.c

#include <stdio.h>

double average(double a, double b);

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

    printf("Enter three numbers: ");
    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("Average of %g and %g: %g\n", z, x, average(z, x));

    return 0;
}

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

9.3节 实际参数

区分形参和实参

  • 形参出现在函数定义里;
  • 实参出现在函数调用里;

在C语言中,形参有什么性质?

形参跟变量类似,形参的初始值是相匹配的实参的值。

在C语言中,实参有什么性质?

按值传递:当函数被调用时,会对每个参数求值,并把每个参数的值拷贝给相应的形参。
由于形参包含的是实参值的拷贝值,所以在程序执行过程中对形参的修改,不会影响实际参数。

按值传递有什么优势?

  • 减少真正需要的变量数量

例1 利用按值传递的性质来减少真正需要的变量数量
源程序:

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

  return result;
}

减少变量后:

int power(int x, int n)
{
  int result = 1;
  while(n--){
    result = result * x;
  }

  return result;
}

按值传递有什么缺点?

  • 无法编写某些类型的函数,比如现在需要一个能将double类型值分解成整数部分和小数部分。由于一个函数不能返回两个值,则尝试使用如下方式来实现:
void decompose(double x, long int_part, double frac_part)
{
  int_part = (long)x;
  frac_part = x - int_part;
}

但是由于按值传递的性质,不起作用。此时,需要利用指针的性质来实现,修改函数的参数类型为:

void decompose(double x, long *int_part, double *frac_part)
{
  *int_part = (long)x;
  *frac_part = x - *int_part;
}

实参的类型转换规则

C语言允许函数调用时的实参的类型跟形参的类型不匹配。

转换规则:

  • 如果编译器在函数调用前碰到过函数原型,则实参的值就隐式地转换成形参的值,比如int实参值传递给double形参,则会自动将int转换成double;
  • 否则,编译器会执行默认的类型提升规则:float实参值转换成double;char类型实参值和short类型实参值转换成int;

数组作为函数参数

一维数组作为函数参数的约定:

  • 需要额外提供数组的长度也作为函数参数。
  • 函数是没有办法来检查是否传递了正确的数组长度,所以务必要保证传递的数组长度值要小于等于实际值;如果超过实际值,会出现不确定的行为
  • 函数是允许修改数组参数元素的值的,且该修改会反映在相应实参数组中。

例1 1维数组作为函数参数
函数声明:

int sum_array(int a[], int n);

函数定义:

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

函数调用

#define LEN 100

int main(void)
{
  int b[LEN], total
  ...
  total = sum_array(b, LEN);//注意,这里不能写成sum_array(b[], LEN)
  ...
}

例2 修改数组参数的元素值,会修改对应实参数组的元素值
函数定义:

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值。

多维数组作为函数参数相关规则:

  • 当参数声明时,只有一维数组的长度可以省略,其余维的长度必须是确定的。
  • 由于前述声明的约束,使该声明方法,我们是不能传递具有任意列数的多维数组的;一般来说有两种解决办法:变长数组作为数组参数,或者指针数组作为参数等。

例3 多维数组作为函数参数

#define LEN 10

int sum_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];
    }
  }
}

可变长度数组作为函数参数

  • 一维可变数组形参:通过指定数组参数的长度使得函数的声明和定义更具体。
  • 多维可变数组形参:可传递任意列数。

例1 可变长度数组作为函数参数
函数原型

int sum_array(int n, int a[n]);
int sum_array(int n, int a[*]);

函数定义

//注意:对于可变数组来说,这里的n跟a[n]的顺序是不能交换的
int sum_array(int n, int a[n])
{
  ...
}

例2 使用多维可变数组形参来可传递任意列数
函数声明:

int sum_dimensional_array(int n, int m, int a[n][m]);

函数定义

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

在实参中使用复合字面量

什么是复合字面量?
一个没有名字的数组,它的元素需要简单地列出。

格式:

(类型 []){实参列表}

例1 使用复合字面量来进行函数调用

//(int [5]){3, 0, 3, 4, 1}
total = sum_array((int [5]){3, 0, 3, 4, 1}, 5);
//在复合字面量里,可不用指出数组长度:(int []){3, 0, 3, 4, 1}
total = sum_array((int []){3, 0, 3, 4, 1}, 5);
//复合字面量可包含指定初始化式:(int [10]){8, 6}
total = sum_array((int [10]){8, 6}, 10);
//复合字面量可包含任意表达式:(int []){2*i, i + j, j * k}
total = sum_array((int []){2*i, i + j, j * k}, 3);
//只读复合字面量:(const int []){5, 4}
total = sum_array((const int []){5, 4}, 2);

在数组参数的声明中使用static

例1 使用static来声明一维数组

int sum_array(int a[static 3], int n)
{
    ...
}

解释:

  • int a[static 3]表明数组a的长度至少是3;
  • 这里static对程序的行为没有任何影响;
  • 这里使用static的效果是允许C编译器来更快速地生成访问数组的指令;

注:
如果一个数组参数有多维,则static只能使用在第一个维。

9.4节 return语句

格式:

return 表达式;

解释:

  • 表达式通常是常数或者变量,也可以是复杂的表达式。
  • 当return语句里的表达式的值跟函数定义的返回值类型不匹配时,则表达式的值会被隐式地转换成函数定义的返回值类型。
  • 如果一个非void函数执行到函数体的结尾处也没有遇到return语句,而某个程序又要使用该函数的返回值,则该程序的行为是没有定义的。

例1 常见的返回语句

return 0;
return status;
return n>=0 ? n:0;

例2 在void函数里的返回语句

return ;

9.5节 程序终止

有两种方式来终止一个C程序:

  • return语句;
  • 调用包含在头文件<stdlib.h>中的exit函数;

return语句退出程序

通过main函数里的return语句来终止程序。

main函数必须有返回值,且返回值类型为int。
没有返回值的main函数是非法的。

main函数的返回值是一个状态码,可用该状态码来测试程序何时终止。

  • 如果程序正常终止,则main函数应该返回0;
  • 如果main函数返回非0值,则表明程序非正常退出;

注:
main函数有参数吗?

  • main函数有时会有两个参数,比如argc、argv等;
  • 如果main函数没有参数,也要显示地表明;

例1 正常退出程序

//如果main函数没有参数,也要显式地表明
int main(void)
{
    ...
    
    return 0;
}

exit函数终止程序

例1 正常退出程序

exit(0);//或者exit(EXIT_SUCCESS);

例2 非正常退出程序

exit(EXIT_FAILURE);

return语句退出程序跟exit语句之间的关系

相同点:

return 表达式;

等价于

exit(表达式);

不同之处:

  • 无论哪个函数调用exit,都会导致程序终止;
  • 只有在main函数里出现return语句,才会导致程序终止;

9.6节 递归

C语言允许递归,但不经常使用递归。

为了避免无限递归,所有的递归都必须有中止条件。

如果一个函数自己调用自己,则称这个函数是递归的。

例1 计算一个数的阶乘

int fact(int n)
{
  if (n <= 1)
  {
    return 1;
  }
  else 
  {
    return n * frac(n-1);
  }
}

例2 计算一个数的幂次方

int power(int x, int n)
{
  if (n == 0) 
  {
    return 1;
  }
  else
  {
    return x * power(x, n-1);
  }
}

程序示例:快速排序

qsort.c

#include <stdio.h>

#define N 10

void quicksort(int a[], int low, int high);
int split(int a[], int low, int high);

int main(void)
{
    int a[N], i;

    printf("Enter %d numbers to be sorted: ", N);
    for(i=0;i<N;i++)
    {
        scanf("%d", &a[i]);
    }
    quicksort(a, 0, N-1);

    printf("In sorted order: ");
    for (i = 0; i < N; ++i)
    {
        printf("%d ", a[i]);
    }
    printf("\n");

    return 0;

}


void quicksort(int a[], int low, int high)
{
    int middle;

    if (low >= high) 
    {
        return ;
    }
    middle = split(a, low, high);
    quicksort(a, low, middle -1);
    quicksort(a, middle+1, high);
}

int split(int a[], int low, int high)
{
    int part_element = a[low];

    for(;;) 
    {
        // 从右边开始向左扫描,找第一个被part_ment小的元素
        // 将其放置到part_ment的左边
        while(low < high && part_element <= a[high])
        {
            high--;
        }
        if(low >= high)
        {
            break;
        }
        a[low++] = a[high];
        // 从左边开始向右边扫描,找第一个被part_ment大的元素
        // 将其放置到part_ment的右边
        while(low < high && a[low]<=part_element)
        {
            low++;
        }
        if (low >= high)
        {
            break;
        }
        a[high--]=a[low];
    }

    //将part_ment放置到适当的位置
    a[high] = part_element;
    return high;
}

相关文章

网友评论

    本文标题:第9章 函数

    本文链接:https://www.haomeiwen.com/subject/nwfkhhtx.html