C Primer Plus 第九章 大二第二学期 第一天学习

9.1.9 函数类型

**声明函数时必须声明函数的类型。带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为void类型。**如果没有声明函数的类型,旧版本的C编译器会假定函数的类型是int。这一惯例源于C的早期,那时的函数绝大多数都是int类型。然而C99标准不再支持int类型函数的这种假定设置。
**类型声明是函数定义的一部分。**要记住,函数类型指的是返回值的类型,不是函数参数的类型。(函数和函数参数的类型是不同的)

例如,下面的函数头定义了一个带两个int类型参 数的函数,
但是其返回值是double类型。 
double klink(int a, int b)

要正确的使用函数,程序在第一次使用函数之前必须知道函数的类型
方法之一是,把完整的函数定义放在第一次调用函数的前面,这种方法增加了函数的阅读难度。而且,要使用的函数可能在C库或其他文件中。
因此,

通常的做法是提前声明函数,把函数的信息告知编译器

在这里插入图片描述

在这里插入图片描述
注意在这两种情况之中,函数原型都声明在使用函数之前

ANSI C标准库中 ,函数<—系列<—有各自的头文件<----包含其他的内容和本系列所有函数的声明

例如, stdio.h 头文件包含了标准 I/O 库函数(如,printf()和scanf())的声明。math.h 583
头文件包含了各种数学函数的声明。例如,下面的声明: double sqrt(double);

告知编译器sqrt()函数有一个double类型的形参,而且返回double类型的 值。

不要混淆函数的声明和定义

函数声明告知编译器函数的类型,而函数定义则提供实际的代码
在程序中包含 math.h 头文件告知编译器:sqrt()返 回double类型,但是sqrt()函数的代码在另一个库函数的文件中。

9.2 ANSI C函数原型

int imin();
然而,以上函数声明并未给出imin()函数的参数个数和类型。因此,如果调用imin()时使用的参数个数不对或类型不匹配,编译器根本不会察觉出来。

/*用过去声明函数的方式声明了imax()函 数,然后错误地使用该函数。
把函数的参数去掉
*/

#include "stdio.h"

int imax();
int main(void)
{
    int evil1,evil2;
//    printf("Please Enter a pai of integer ( q to quit ): \n");
 //   while(scanf("%d%d",&evil1,&evil2) == 2)
    //{
        printf("The   maxinum of %d and %d is %d.\n",5,3,imax(3));
        printf("The   maxinum of %d and %d is %d.\n",5.3666,3.3666,imax(5.3666,3.3666));
      //  printf("Please Enter a pai of integer ( q to quit ): \n");
    //}
    return 0;
}
int imax( int m,int n )
{

    int max;
    if( m > n)
        max = m ;
    else
        max = n;
    return max;
}

在这里插入图片描述

之所以输出错误的结果,是因为他们运行的程序没有使用函数原型

到底是哪里出了问题?由于不同系统的内部机制不同,所以出现问题的 具体情况也不同。下面介绍的是使用P C和VA X的情况。

主调函数把它的参数储存在被称为栈的临时存储区,被调函数从栈中读取这些参数

对于该例,这两个过程并未相互协调。

主调函数根据函数调用中的实际参数决定传递的类型,而被调函数根据他的形式参数读取值。因此,函数调用IMAX(3)把一个整数放在栈中。

当imax()函数开始执行时,它从栈中读取两个 586
整数。而实际上栈中只存放了一个待读取的整数,所以读取的第 2 个值是当 时恰好在栈中的其他值

第2次使用imax()函数时,它传递的是float类型的值。这次把两个double 类型的值放在栈中(回忆一下,当float类型被作为参数传递时会被升级为 double类型)。在我们的系统中,两个double类型的值就是两个64位的值, 所以128位的数据被放在栈中。当imax()从栈中读取两个int类型的值时,它 从栈中读取前64位。在我们的系统中,每个int类型的变量占用32位。这些数 据对应两个整数,其中较大的是3886。

C语言在遇到float 类型的时候,会为了提高数据精度,把float 类型转换成一个double

9.2.2 ANSI的解决方案

针对参数不匹配的问题

ANSI C标准要求在函数声明时还要声明变量 的类型,即使用函数原型(function prototype)来声明函数的返回类型、参 数的数量和每个参数的类型。未标明 imax()函数有两个 int 类型的参数,可 以使用下面两种函数原型来声明: int imax(int, int); int imax(int a, int b);

第一种形式使用以逗号分隔的类型列表,第二种形式在类型后面添加了变量名。注意,

这里的变量名是假名,不必与函数定义的形式参数名一致

有了这些信息,编译器可以检查函数调用是否与函数原型匹配。参数的数量是否正确?参数的类型是否匹配?

以 imax()为例,如果两个参数都是数 字,但是类型不匹配,
编译器会把实际参数的类型转化为形式参数的类型
例如,imax(3.0, 5.0)会被转换成imax(3, 5)。

错误和警告的区别是:错误导致无法编译,而警告仍然允许编译。一些 编译器在进行类似的类型转换时不会通知用户,因为C标准中对此未作要 求。不过,许多编译器都允许用户选择警告级别来控制编译器在描述警告时 的详细程度。

在这里插入图片描述
在这里插入图片描述

9.2.3 无参数和未指定参数

假设有下面的函数原型:
void printf_name();
一个支持ANSI C的编译器会假定用户没有用函数原型来声明函数,它将不会检查参数。为了表明函数确实没有参数,应该在圆括号中使用void关键字:
void printf_name(void);
支持ANSI C的编译器解释为print_name()不接受任何参数。然后在调用 该函数时,编译器会检查以确保没有使用参数。

一些函数接受许多参数。例如对于printf(),第一个参数是字符串,但是其余参数的类型和数量都不固定

*对于这种情况, ANSI C允许使用部分原型。例如,对于printf()可以使用下面的原型:
int printf(const char , …); 这种原型表明,第1个参数是一个字符串(第11章中将详细介绍),可 能还有其他未指定的参数。 C库通过stdarg.h头文件提供了一个定义这类(形参数量不固定的)函数 的标准方法。第16章中详细介绍相关内容

9.2.4 函数原型的优点

函数原型是C语言的一个强有力的工具,它让编译器捕获在使用函数时可能出现的许多错误或疏漏

如果编译器没有发现这些问题,就很难觉察出 来。是否必须使用函数原型?不一定。你也可以使用旧式的函数声明(即不 用声明任何形参),但是这样做的弊大于利。

有一种方法可以省略函数原型却保留函数原型的优点。
首先要明白,之 所以使用函数原型,是为了让编译器在第1次执行到该函数之前就知道如何 使用它。因此,把整个函数定义放在第1次调用该函数之前,也有相同的效 果。此时,函数定义也相当于函数原型。对于较小的函数,这种用法很普 遍:
在这里插入图片描述

9.3 递归

C允许函数调用它自己,这种调用过程称为递归

结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。
可以使用循环的地方通常都可以使用递归,递归方案更简洁,但效率却没有循环高

9.3.1 演示递归

程序清单 9.6 中的 main() 函数调用 up_and_down()函数,这次调用称为“第1级递归”。然后 up_and_down()调用自己,这次调用称为“第2级递归”。接着第2级递归调用 第3级递归,以此类推。该程序示例共有4级递归。

为了进一步深入研究递归 时发生了什么,

程序不仅显示了变量n的值,还显示了储存n的内存地址 &n

(。本章稍后会详细讨论&运算符,printf()函数使用%p转换说明打印地 址,如果你的系统不支持这种格式,请使用%u或%lu代替%p)。

&Y 取出这个变量所存的内存的位置

/*递归演示*/
#include <stdio.h>


int main()
{
    up_and_down(1);
    return 0;
}

void up_and_down(int n)
{
    printf("  level %d : n location %p\n",n,&n);
    if(n<4)

    up_and_down(n+1);
    printf("  Level %d : n location %p\n",n,&n);

}

我们来仔细分析程序中的递归是如何工作的。首先,main()调用了带参 数1的up_and_down()函数,执行结果是up_and_down()中的形式参数n的值是 1,所以打印语句#1打印Level 1。然后,由于n小于4,up_and_down()(第1 593
级)调用实际参数为n + 1(或2)的up_and_down()(第2级)。于是第2级调 用中的n的值是2,打印语句#1打印Level 2。与此类似,下面两次调用打印的 分别是Level 3和Level 4。 当执行到第4级时,n的值是4,所以if测试条件为假。up_and_down()函 数不再调用自己。第4级调用接着执行打印语句#2,即打印LEVEL 4,因为n 的值是4。此时,第4级调用结束,控制被传回它的主调函数(即第3级调 用)。在第3级调用中,执行的最后一条语句是调用if语句中的第4级调用。 被调函数(第4级调用)把控制返回在这个位置,因此,第3级调用继续执行 后面的代码,打印语句#2打印LEVEL 3。然后第3级调用结束,控制被传回 第2级调用,接着打印LEVEL2,以此类推。

注意:每级递归的变量n都属于本级递归私有 这从程序输出的地址值可以看出(当然,不同的系统表示的地址格式不同,这里关键要注意, Level 1和LEVEL1的地址相同,Level 2和LEVEL2的地址相同,等等)

也就是说:地址值相同,那么他们就属于同一级递归
被调函数完成后要返回主调函数

对此 代码,附上自己的理解:函数中一共有四行语句,前三行执行到level结束,为1234,之后跳出循环,直接执行第四条语句为Level4,被调函数执行完后会返回主调函数,4返回3,以此类推,会有接下来的输出321,就会有程序输出的结果

9.3.2 递归的基本原理

第一,每级函数调用都有自己的变量。也就是说,第一级的n和第二级的n不同,所以程序创建了4个单独的变量,每个变量名都是n,但是他们的值各不相同。当程序最终返回up_and_down()的第一级调用时,最初的n仍然是它的初值1
在这里插入图片描述
图9.4 递归中的变量

第二,每次函数调用都会返回一次
当函数执行完毕后,控制权将被传回上一级递归。程序必须按顺序逐级返回递归
从某up_and_down()返回上一级的up_and_down(),不能跳级回到main()中的第一级调用

第三,递归函数中位于递归调用之前的语句 均按被调函数的顺序执行。 例如:程序清单9.6中的打印语句#1位于递归调用之前,它按照递归的顺序:1234,执行了四次

第四,递归函数中位于递归调用之后的语句,均按被调函数相反的方向执行,4321 递归调用的这种特性在解决涉及相反顺序的编程问题时很有用。

第五,虽然每级递归都有自己的变量,但是并没有拷贝函数的代码 程序按顺序执行函数中的代码,而递归调用就相当于又从头开始执行函数的代码。除了为每次递归调用创建变量外,递归调用非常类似于一个循环语句。实际上,递归有时可用循环来代替,循环有时也能用递归来代替。
最后,递归函数必须包含能让递归调用停止的语句。通常,递归函数都使用if或其他等价的测试条件在函数形参等于某特定值时终止递归。为此,每次递归调用的形参都要使用不同的值。

例如,程序清单9.6中的 up_and_down(n)调用up_and_down(n+1)。最终,实际参数等于4时,if的测试 条件(n < 4)为假

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值