第9章 函数

英文原版: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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容

  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,340评论 0 2
  • 原文链接:https://github.com/EasyKotlin 值就是函数,函数就是值。所有函数都消费函数,...
    JackChen1024阅读 5,965评论 1 17
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,180评论 0 3
  • 我有两位高中同学,原本他俩在苏州都有工厂,但因为环保不达标,这个工厂都关门了,而且随时有被拆迁的可能。但他俩并没有...
    蒋坤元阅读 4,262评论 45 150
  • 看了今天的分享,我先根据今天的文章进行一个总结。 首先老师对罗森塔尔效应进行了说明,包括神话的由来——皮格马利翁效...
    茄子ni阅读 887评论 1 1