王道C++班级参考资料
——
C语言部分卷3函数
节1函数和函数调用

最新版本V2.0
王道C++团队
COPYRIGHT ⓒ 2021-2024. 王道版权所有

概述

Gn!

几乎所有的编程语言都有函数(或方法)的概念,在C程序中,函数尤为重要,因为C程序就是函数的集合。

那么什么是函数呢?

简单地说,函数可以看作是一个程序的子程序,一台大型机器中的“微型执行单元”:

  1. 输入:当你启动一个函数时,你通常会传递给它一些数据,这些数据被称为“参数”或“输入参数”。

  2. 处理:函数一旦接收到这些输入,它会根据内部的指令和逻辑进行处理和运算。

  3. 输出:处理完成后,函数通常会提供一个结果,这就是我们所说的“返回值”

如下图所示:

编程语言中的函数

这实际上就是函数的三个基本特征:参数输入、功能执行以及输出返回值。

总之,函数是C语言编程中的基本组成单位。它是一个代码块,用于执行特定的任务,可以接收参数并返回一个值。

C语言中的函数分类

在C语言中,根据函数的来源,函数可以分为两大类:

  1. 标准库函数:这些函数属于C语言的标准库,例如 printf(), scanf(), sqrt() 等。为了调用这些函数,必须首先引入相应的头文件。

  2. 用户定义函数:由C程序员手动编写代码实现的函数。

但不论是来源于标准库还是由用户自定义,所有的函数在C语言中都遵循相同的基本原则和语法结构。

为什么使用函数?

Gn!

假设程序需要多次计算长方形的面积。面对这一需求,有两种选择:

  1. 每次都编写一段计算面积的代码。

  2. 定义一个函数并多次调用它。

显然,选择使用函数是更好的选择,具有以下显著优势:

  1. 代码复用:减少冗余,提高代码的可维护性。

  2. 模块化设计:函数使程序结构更清晰,将大任务分解为小任务,降低了解决问题的复杂度。

  3. 灵活应用:仅通过传递不同的参数,就能轻松地为多种情况使用同一功能。

这些优势使函数在编程中成为一个不可或缺的工具,尤其是C语言程序。

C语言中函数的使用

Gn!

下面,我们来一起看一下在C语言中如何使用一个函数。这涉及函数的定义、函数调用等概念。

函数的定义

Gn!

函数的定义在C语言中是一个专有名词,专指以下结构:

函数的定义语法

逐一解释每个部分:

  1. 返回值类型:

    1. 指明了函数完成执行后返回的数据的类型。

    2. 如果函数的返回值类型不为void,则一般需要在函数体当中使用return表示返回值。(若函数返回值不为void,就在函数体中给定一个明确的返回值是一个良好的编程习惯)

    3. 当函数不需要返回值时,使用void作为返回值类型。对于void类型的函数,可以省略return语句(也可使用return语句,但仅用于提前退出函数,不表示返回值)

    4. C语言不允许函数返回数组类型。

    5. 在C90标准中,如果省略了函数的返回值类型,编译器默认其为int类型。但从C99标准开始,这种做法已不再被支持。因此现代C编程中建议总是明确指定返回值类型。

  2. 函数名

    1. 标识符的一种,用于在其他地方引用或调用这个函数。

    2. 在C语言中,函数名是函数的唯一标识,同一个文件中不允许两个同名的函数定义。

  3. 形参列表:

    1. 形式参数(形参)组成的列表。形参的语法形式为:"数据类型 形参名",多个形参间用","分隔。

    2. 形参列表可以是空的,表示函数不需要任何外部输入。在C语言中,推荐使用void取代空形参,以明确地表示函数不接受任何参数。

    3. 形参列表中的数据类型,决定了调用函数时所需的实际参数的类型。例如,int a表明需要一个整数型参数。

    4. 形参列表中的形参名,决定了参数传入函数后,如何去使用此参数。

    5. 形参列表中的数据类型、形参个数、顺序都会影响此函数的调用,而形参名则不会影响函数调用。

  4. 函数体

    1. 包含在"{ }"内的代码语句

    2. 描述函数的具体操作和逻辑。

举一个简单的例子来说明,一个求和的函数:

函数的定义-示例代码
  1. 返回值类型:

    1. int类型

    2. 表示函数add的执行结果是一个整数型数据,并且当这个函数被调用时,它会返回这个整数值。

    3. 既然返回值类型不为void,那么在函数体中应当配合return关键字给定一个明确的返回值。

  2. 函数名:

    1. add

    2. 函数的标识符,当我们在其他地方要使用这个函数时,可以通过这个名字来调用它。

    3. 具有唯一性,此文件中定义了add函数,就不允许再次定义同名函数了。

  3. 形参列表:

    1. (int a, int b)

    2. 这个函数在调用时接受两个整数参数,当我们调用这个函数时,需要为这两个参数提供对应的实际值(也称为实参)。

    3. 若想要在函数体中使用这两个参数,只需使用a和b这两个标识符。

  4. 函数体:

    1. return a + b;

    2. 函数实现功能的核心部分,描述了具体的操作和逻辑。

    3. 在这个例子中,函数体非常简单:它只是返回两个参数a和b的和。

函数头

Gn!

一个函数的定义中,有函数体的概念,那么有身体就应该有头。函数头是一个经常被提及的概念,它包括函数定义当中除函数体外的部分:

比如:

函数的定义-示例代码

函数体是大括号包裹的部分,而函数头就是:

注意函数头本身既不包括大括号,也不用带分号。

函数调用

Gn!

当你定义一个函数后,这个函数仅仅是一个可以被调用的声明或者说是“模版”。实际上,只有当你调用这个函数时,函数内的代码才会被执行。

在C语言中,main函数是程序的入口点,所有的执行都从这里开始。因此如果你想在程序运行时调用其他函数,这些调用:

  1. 要么直接出现在main函数中,

  2. 要么位于main函数直接或间接调用的其他函数当中。

函数调用的语法如下:

在C语言中,函数调用语句可以看作一条表达式语句,它同样有主要作用和副作用:

  1. 主要作用。函数调用的主要作用一般就是返回值。虽然你可以选择不处理这个返回值,但最佳实践是接收、处理或使用此返回值。如果函数的返回类型为void,则这样的函数调用没有主要作用,因为它不返回任何值。

  2. 副作用。函数体当中进行的任何操作都可以看成是函数调用的副作用。比如键盘录入、屏幕打印等IO操作、修改全局变量等外部变量的赋值操作。函数调用并不一定就有副作用,比如上面的一个求和sum函数,调用它就没有任何副作用。

总之一个函数调用具有两大潜在效应:主要作用和副作用:

  1. 为了充分利用函数的主要作用,调用者应当主动接收并处理函数的返回值。

  2. 若一个函数调用后仅以分号“;”结尾,并未进一步处理其返回值,这意味着调用者关注该函数的副作用。

  3. void函数没有主要作用,调用它需要关注函数调用的副作用。

Rd!

例子1:

函数调用-示例代码1

当你调用add(3, 4)时:

  1. 主要作用:返回7这个值,你可以接收处理这个值。如果不处理,则函数调用失去了主要作用。

  2. 没有副作用。此函数体中没有做任何其他操作。

例子2:

函数调用-示例代码2
  1. 主要作用:返回从键盘输入的整数。

  2. 副作用:向屏幕输出提示信息,并从键盘读取用户输入。

函数的定义位置影响函数调用

Gn!

首先,我们来看两个简单的代码示例:

函数的声明-代码示例1

在Visual Studio中执行这段代码,能否成功运行?

函数的声明-代码示例2

这段代码在Visual Studio中可以成功执行吗?

答案是,代码2可以成功执行,而代码1会编译报错。

首先我们要明确C程序编译的特点:C编译器是从上到下逐行编译代码的。

当编译器遇到函数调用语句时,它会查找该函数的声明或定义。如果在调用处之前未找到函数的声明或定义,编译器将报错,因为它不知道这个函数的细节。

在代码示例1中,当编译器遇到"print_hello();"的调用时,由于print_hello函数的定义在main函数之后,编译器会报错。

而代码示例2为何能正常运行呢?

因为在C90标准中,如果编译器没有事先找到函数的声明或定义,它会默认此函数返回int类型。而示例2中的sum函数恰好就返回int类型,所以成功运行了。

示例1的print_hello函数返回值类型是void,所以编译器产生以下信息(因为编译器默认是int类型,实际却是void):

警告 C4013 “print_hello”未定义;假设外部返回 int 错误 C2371 “print_hello”: 重定义;不同的基类型

这种让编译器假定int返回值类型,然后成功运行代码的编程做法是不推荐的,因为:

  1. 一般读程序也是从上到下的,将函数定义放在调用之后会给读者带来困惑。

  2. (重要)从C99开始,编译器不再支持这种默认为int返回值类型的做法。

总之,应对针对C语言的这一特性,建议采用以下策略:

  1. 始终将函数定义放在其调用之前。

  2. 在函数调用前使用函数的声明语法,事先声明此函数。

函数的定义语法我们已经学习过了,下面我们来讲一下函数的声明。

函数的声明

Gn!

在C语言中,函数的声明为编译器提供了函数的基本信息,但不提供具体的实现细节。函数的声明实际上就是函数头加分号,在C语言中,函数的声明专指以下结构:

例如,在main函数的上面加上print_hello函数的声明:

这样上述代码示例1就可以正常运行,而不会报错了。

注意:

  1. 函数的声明和定义是完全不同的概念,大家要注意区分。函数声明告诉编译器有这样一个函数,但不提供具体实现;函数定义则提供实现。规范的C程序代码,应该在调用函数前,声明或定义该函数。

  2. 函数在定义时,形参列表必须带上形参名, 但函数在声明时可以不带形参名(但加上形参名也有利于理解函数)。

  3. 从语法上讲,一个函数可以在多处声明,但只能定义一次。

举例,函数的声明和定义可以按照以下格式书写:

函数的声明-示例代码
函数的定义-示例代码

函数设计的原则

Gn!

我们在设计一个程序中的函数时,最重要的有两个原则:

  1. "单一性原则"。一个函数的功能,应该越单一越好。

  2. "性能优化原则"。一个函数的性能,在可能的情况下越高效越好。

通俗地说,一个函数应该只做自己的一件事情,并且尽力做得好。

遵循单一原则,使得函数代码可读性更强,易于维护。而且单一功能的函数,也更容易被复用。

而性能优化就直接关乎用户体验和程序的运行成本,也是非常重要的。

课堂练习

Gn!

下面给两道练习题来讲解一下函数的使用。

海伦公式求三角形面积

Gn!

问题描述:

键盘录入三个边长(带小数),然后用海伦公式计算三角形的面积(如果它确实是一个三角形的话)

海伦公式求三角形面积:

给定三角形的三边长为 a, b, 和 c ,首先计算三角形的半周长p:

(1)p=a+b+c2

然后,使用海伦公式计算三角形的面积S:

(2)S=p(pa)(pb)(pc)

求平方根

Gn!

在C语言中,要求一个数的开平方根,你可以使用标准库函数sqrt(double)来实现。sqrt是"square root",意为开平方。

sqrt库函数用于计算一个非负数的平方根,你可以在<math.h>头文件中找到它。此函数的声明如下:

调用此函数,参数需要传入一个非负数double x,返回值是x的开平方根,返回值也是一个double类型的值。如果传入一个负数,那么函数的返回值是未定义的。

参考代码

Gn!

参考代码:

海伦公式计算三角形面积-参考代码

这个C程序的设计正好可以作为示例来解释单一职责原则,在这个程序中,我们定义了两个主要的函数:is_triangle和calculate_area。

  1. is_triangle函数:仅有一个职责,那就是检查给定的三个数值是否可以构成一个三角形。它不涉及任何计算面积或其他操作,其职责非常明确。

  2. calculate_area函数:同样只有一个职责,就是根据给定的三边长计算三角形的面积。它没有其他额外的功能,例如判断这三条边能否构成三角形。

  3. 这两个函数都没有做诸如:数据键盘录入,控制台打印提示信息等操作,这也体现了单一职责原则。

通过这种方式,每个函数都只做它应该做的事情,使得代码清晰、模块化和易于维护:

  1. 当你需要修改检查三角形的规则或面积的计算方式时,你可以直接修改相关的函数,而不会影响到其他部分。

  2. 只要是检查三个边长是否能构成三角形的场景,都可以复用函数is_triangle

  3. 只要是求三角形面积的场景,都可以复用函数is_triangle

Gn!

补充:

上述问题提到了一个开平方根的数学概念,这不由得使很多同学想到了另一个常见数学概念:求幂运算。

首先,在日常的计算机文本编辑中,我们常常用a ^ b来表示a的b次方幂运算。但显然在C语言不能直接这么写,因为^是位运算符中的按位异或运算符。

那么在C语言代码中如何实现幂运算呢?

这同样也需要使用一个<math.h>标准数学库中的库函数:

它就表示计算x ^ y幂运算,需要注意的是它的参数和返回值类型都是double。

求素数

Gn!

问题描述:

键盘录入一个正整数,请判断它是否是一个素数,然后控制台输出对应的结果。

素数是一个大于1的自然数,它仅能被1和自身整除。

参考代码:

求素数问题-参考代码

is_prime函数的定义就很符合我们提到的两个原则:

  1. 单一职责原则:代码中的 is_prime 函数遵循了这一原则。该函数仅做一件事情:判断一个给定的数是否为素数。它没有涉及输入/输出或其他非相关任务。

  2. 性能优化:在 is_prime 函数中,我们在判断一个数是否为素数时,只循环除到了 sqrt(n) 而不是 n。这是基于数学原理的:如果 n 不是素数(即它可以被整除),那么它必然有一个小于它平方根的因子。因此,只要循环到 sqrt(n) 就足够了,这大大减少了需要检查的范围,从而提高了性能。

exit退出函数

Gn!

我们都知道,一个C源文件代码通过编译和链接的过程,生成了一个可执行的程序。这个可执行程序的入口就是C语言的main函数,它是由操作系统来进行调用的,main 函数的返回值类型为 int,它表示程序结束时返回给操作系统的状态码:

  1. 如果程序正常终止,main 函数应该返回 0;

  2. 如果程序异常终止,那么 main 函数应该返回非 0 的值。

所以一个C进程的结束,可以通过结束返回main函数来完成。

在这里,我们就来学习另一种结束C进程的方式,它比使用return结束返回main函数更灵活,可以使用在程序的任何位置结束程序。即:

声明在<stdlib.h>头文件中的exit函数。

传递给exit函数的参数和main函数的return返回值具有相同的含义,传递0表示程序正常结束,非0表示程序异常终止。

正常结束程序时,我们可以这样写:

因为 0 是一个魔法数字,所以 C 语言允许使用EXIT_SUCCESS来替代:

如果异常退出,可以这样写:

EXIT_SUCCESSEXIT_FAILURE 都是定义在 <stdlib.h> 中的宏常量,它们的值是由实现决定的,通常分别为 0 和 1。

return语句和exit函数之间的差异是:

不管哪个函数调用exit函数都会导致程序终止,return语句仅当在main函数中执行时才会导致程序的终止。

Tips:某些程序员仅使用 exit 函数终止程序,这样做的好处是方便以后定位程序的全部退出点。

THE END