V2.0
王道C++班级参考资料<br />——<br />C语言部分卷3函数<br/>节1函数和函数调用<br/><br/>最新版本V2.0
<br>王道C++团队<br/>COPYRIGHT ⓒ 2021-2024. 王道版权所有概述为什么使用函数?C语言中函数的使用函数的定义函数头函数调用函数的定义位置影响函数调用函数的声明函数设计的原则课堂练习海伦公式求三角形面积求平方根参考代码求素数exit退出函数THE END
Gn!
几乎所有的编程语言都有函数(或方法)的概念,在C程序中,函数尤为重要,因为C程序就是函数的集合。
那么什么是函数呢?
简单地说,函数可以看作是一个程序的子程序,一台大型机器中的“微型执行单元”:
输入:当你启动一个函数时,你通常会传递给它一些数据,这些数据被称为“参数”或“输入参数”。
处理:函数一旦接收到这些输入,它会根据内部的指令和逻辑进行处理和运算。
输出:处理完成后,函数通常会提供一个结果,这就是我们所说的“返回值”。
如下图所示:
这实际上就是函数的三个基本特征:参数输入、功能执行以及输出返回值。
总之,函数是C语言编程中的基本组成单位。它是一个代码块,用于执行特定的任务,可以接收参数并返回一个值。
C语言中的函数分类
在C语言中,根据函数的来源,函数可以分为两大类:
标准库函数:这些函数属于C语言的标准库,例如 printf(), scanf(), sqrt() 等。为了调用这些函数,必须首先引入相应的头文件。
用户定义函数:由C程序员手动编写代码实现的函数。
但不论是来源于标准库还是由用户自定义,所有的函数在C语言中都遵循相同的基本原则和语法结构。
Gn!
假设程序需要多次计算长方形的面积。面对这一需求,有两种选择:
每次都编写一段计算面积的代码。
定义一个函数并多次调用它。
显然,选择使用函数是更好的选择,具有以下显著优势:
代码复用:减少冗余,提高代码的可维护性。
模块化设计:函数使程序结构更清晰,将大任务分解为小任务,降低了解决问题的复杂度。
灵活应用:仅通过传递不同的参数,就能轻松地为多种情况使用同一功能。
这些优势使函数在编程中成为一个不可或缺的工具,尤其是C语言程序。
Gn!
下面,我们来一起看一下在C语言中如何使用一个函数。这涉及函数的定义、函数调用等概念。
Gn!
函数的定义在C语言中是一个专有名词,专指以下结构:
函数的定义语法
31返回值类型 函数名(形参列表){
2// 函数体
3}
逐一解释每个部分:
返回值类型:
指明了函数完成执行后返回的数据的类型。
如果函数的返回值类型不为void,则一般需要在函数体当中使用return表示返回值。(若函数返回值不为void,就在函数体中给定一个明确的返回值是一个良好的编程习惯)
当函数不需要返回值时,使用void作为返回值类型。对于void类型的函数,可以省略return语句(也可使用return语句,但仅用于提前退出函数,不表示返回值)
C语言不允许函数返回数组类型。
在C90标准中,如果省略了函数的返回值类型,编译器默认其为int类型。但从C99标准开始,这种做法已不再被支持。因此现代C编程中建议总是明确指定返回值类型。
函数名:
标识符的一种,用于在其他地方引用或调用这个函数。
在C语言中,函数名是函数的唯一标识,同一个文件中不允许两个同名的函数定义。
形参列表:
形式参数(形参)组成的列表。形参的语法形式为:"数据类型 形参名",多个形参间用","分隔。
形参列表可以是空的,表示函数不需要任何外部输入。在C语言中,推荐使用void取代空形参,以明确地表示函数不接受任何参数。
形参列表中的数据类型,决定了调用函数时所需的实际参数的类型。例如,int a表明需要一个整数型参数。
形参列表中的形参名,决定了参数传入函数后,如何去使用此参数。
形参列表中的数据类型、形参个数、顺序都会影响此函数的调用,而形参名则不会影响函数调用。
函数体:
包含在"{ }"内的代码语句
描述函数的具体操作和逻辑。
举一个简单的例子来说明,一个求和的函数:
函数的定义-示例代码
31int add(int a, int b) {
2return a + b;
3}
返回值类型:
int类型
表示函数add的执行结果是一个整数型数据,并且当这个函数被调用时,它会返回这个整数值。
既然返回值类型不为void,那么在函数体中应当配合return关键字给定一个明确的返回值。
函数名:
add
函数的标识符,当我们在其他地方要使用这个函数时,可以通过这个名字来调用它。
具有唯一性,此文件中定义了add函数,就不允许再次定义同名函数了。
形参列表:
(int a, int b)
这个函数在调用时接受两个整数参数,当我们调用这个函数时,需要为这两个参数提供对应的实际值(也称为实参)。
若想要在函数体中使用这两个参数,只需使用a和b这两个标识符。
函数体:
return a + b;
函数实现功能的核心部分,描述了具体的操作和逻辑。
在这个例子中,函数体非常简单:它只是返回两个参数a和b的和。
Gn!
一个函数的定义中,有函数体的概念,那么有身体就应该有头。函数头是一个经常被提及的概念,它包括函数定义当中除函数体外的部分:
11返回值类型 函数名(形参列表)
比如:
函数的定义-示例代码
31int add(int a, int b){
2return a + b;
3}
函数体是大括号包裹的部分,而函数头就是:
11int add(int a, int b)
注意函数头本身既不包括大括号,也不用带分号。
Gn!
当你定义一个函数后,这个函数仅仅是一个可以被调用的声明或者说是“模版”。实际上,只有当你调用这个函数时,函数内的代码才会被执行。
在C语言中,main函数是程序的入口点,所有的执行都从这里开始。因此如果你想在程序运行时调用其他函数,这些调用:
要么直接出现在main函数中,
要么位于main函数直接或间接调用的其他函数当中。
函数调用的语法如下:
xxxxxxxxxx
1函数名(实际参数列表);
在C语言中,函数调用语句可以看作一条表达式语句,它同样有主要作用和副作用:
主要作用。函数调用的主要作用一般就是返回值。虽然你可以选择不处理这个返回值,但最佳实践是接收、处理或使用此返回值。如果函数的返回类型为void,则这样的函数调用没有主要作用,因为它不返回任何值。
副作用。函数体当中进行的任何操作都可以看成是函数调用的副作用。比如键盘录入、屏幕打印等IO操作、修改全局变量等外部变量的赋值操作。函数调用并不一定就有副作用,比如上面的一个求和
sum函数
,调用它就没有任何副作用。总之一个函数调用具有两大潜在效应:主要作用和副作用:
为了充分利用函数的主要作用,调用者应当主动接收并处理函数的返回值。
若一个函数调用后仅以分号“;”结尾,并未进一步处理其返回值,这意味着调用者关注该函数的副作用。
void函数没有主要作用,调用它需要关注函数调用的副作用。
Rd!
例子1:
函数调用-示例代码1
31int add(int a, int b) {
2return a + b;
3}
当你调用add(3, 4)时:
主要作用:返回7这个值,你可以接收处理这个值。如果不处理,则函数调用失去了主要作用。
没有副作用。此函数体中没有做任何其他操作。
例子2:
函数调用-示例代码2
x1int getInput(void) {
2int input;
3printf("Please enter an integer: ");
4scanf("%d", &input);
5return input;
6}
主要作用:返回从键盘输入的整数。
副作用:向屏幕输出提示信息,并从键盘读取用户输入。
Gn!
首先,我们来看两个简单的代码示例:
函数的声明-代码示例1
xxxxxxxxxx
912int main(void) {
3print_hello();
4return 0;
5}
6
7void print_hello(void) {
8printf("hello world!\n");
9}
在Visual Studio中执行这段代码,能否成功运行?
函数的声明-代码示例2
xxxxxxxxxx
912int main(void) {
3printf("%d", sum(1, 1));
4return 0;
5}
6
7int sum(int a, int b) {
8return a + b;
9}
这段代码在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返回值类型,然后成功运行代码的编程做法是不推荐的,因为:
一般读程序也是从上到下的,将函数定义放在调用之后会给读者带来困惑。
(重要)从C99开始,编译器不再支持这种默认为int返回值类型的做法。
总之,应对针对C语言的这一特性,建议采用以下策略:
始终将函数定义放在其调用之前。
在函数调用前使用函数的声明语法,事先声明此函数。
函数的定义语法我们已经学习过了,下面我们来讲一下函数的声明。
Gn!
在C语言中,函数的声明为编译器提供了函数的基本信息,但不提供具体的实现细节。函数的声明实际上就是函数头加分号,在C语言中,函数的声明专指以下结构:
xxxxxxxxxx
11返回值类型 函数名(形参列表);
例如,在main函数的上面加上print_hello函数的声明:
xxxxxxxxxx
11void print_hello(void);
这样上述代码示例1就可以正常运行,而不会报错了。
注意:
函数的声明和定义是完全不同的概念,大家要注意区分。函数声明告诉编译器有这样一个函数,但不提供具体实现;函数定义则提供实现。规范的C程序代码,应该在调用函数前,声明或定义该函数。
函数在定义时,形参列表必须带上形参名, 但函数在声明时可以不带形参名(但加上形参名也有利于理解函数)。
从语法上讲,一个函数可以在多处声明,但只能定义一次。
举例,函数的声明和定义可以按照以下格式书写:
函数的声明-示例代码
xxxxxxxxxx
81/*
2函数声明,告诉编译器有这样一个函数。从该行开始往下,编译器就认识这个函数了。
3但函数声明并不包括实现,要想真正调用此函数,还需要手动实现该函数
4也就是需要完成该函数的定义
5*/
6// 下面两条声明是同一个函数,但即便这样也没关系,函数声明可以有多个
7double divide(double, double); // 这样声明函数更简洁
8double divide(double dividend, double divisor); // 这样声明函数更容易理解参数的含义,此函数表示dividend / divisor
函数的定义-示例代码
xxxxxxxxxx
41// 这是函数的定义,它提供了函数的实现。函数的定义必须带上形参名
2double divide(double dividend, double divisor) {
3return dividend / divisor;
4}
Gn!
我们在设计一个程序中的函数时,最重要的有两个原则:
"单一性原则"。一个函数的功能,应该越单一越好。
"性能优化原则"。一个函数的性能,在可能的情况下越高效越好。
通俗地说,一个函数应该只做自己的一件事情,并且尽力做得好。
遵循单一原则,使得函数代码可读性更强,易于维护。而且单一功能的函数,也更容易被复用。
而性能优化就直接关乎用户体验和程序的运行成本,也是非常重要的。
Gn!
下面给两道练习题来讲解一下函数的使用。
Gn!
问题描述:
键盘录入三个边长(带小数),然后用海伦公式计算三角形的面积(如果它确实是一个三角形的话)
海伦公式求三角形面积:
给定三角形的三边长为 a, b, 和 c ,首先计算三角形的半周长p:
然后,使用海伦公式计算三角形的面积S:
Gn!
在C语言中,要求一个数的开平方根,你可以使用标准库函数sqrt(double)来实现。sqrt是"square root",意为开平方。
sqrt库函数用于计算一个非负数的平方根,你可以在<math.h>头文件中找到它。此函数的声明如下:
xxxxxxxxxx
11double sqrt(double x);
调用此函数,参数需要传入一个非负数double x,返回值是x的开平方根,返回值也是一个double类型的值。如果传入一个负数,那么函数的返回值是未定义的。
Gn!
参考代码:
海伦公式计算三角形面积-参考代码
431234
5// 函数声明
6int is_triangle(double a, double b, double c);
7double calculate_area(double a, double b, double c);
8
9int main(void) {
10double a, b, c;
11
12// 输入三角形的三边
13printf("请输入三角形的三个边长:\n");
14scanf("%lf %lf %lf", &a, &b, &c);
15
16// 判断是否能构成三角形
17if (is_triangle(a, b, c)) {
18printf("这三个边长可以构成三角形。\n");
19// 计算面积
20float area = calculate_area(a, b, c);
21printf("三角形的面积为: %.2f\n", area);
22}
23else {
24printf("这三个边长不能构成三角形。\n");
25}
26
27return 0;
28}
29
30// 判断是否能构成三角形
31int is_triangle(double a, double b, double c) {
32if (a + b > c && a + c > b && b + c > a) {
33return 1;
34}
35return 0;
36}
37
38// 计算三角形面积
39double calculate_area(double a, double b, double c) {
40float s = (a + b + c) / 2; // 半周长
41// 开平方,调用sqrt函数
42return sqrt(s * (s - a) * (s - b) * (s - c));
43}
这个C程序的设计正好可以作为示例来解释单一职责原则,在这个程序中,我们定义了两个主要的函数:is_triangle和calculate_area。
is_triangle函数:仅有一个职责,那就是检查给定的三个数值是否可以构成一个三角形。它不涉及任何计算面积或其他操作,其职责非常明确。
calculate_area函数:同样只有一个职责,就是根据给定的三边长计算三角形的面积。它没有其他额外的功能,例如判断这三条边能否构成三角形。
这两个函数都没有做诸如:数据键盘录入,控制台打印提示信息等操作,这也体现了单一职责原则。
通过这种方式,每个函数都只做它应该做的事情,使得代码清晰、模块化和易于维护:
当你需要修改检查三角形的规则或面积的计算方式时,你可以直接修改相关的函数,而不会影响到其他部分。
只要是检查三个边长是否能构成三角形的场景,都可以复用函数is_triangle
只要是求三角形面积的场景,都可以复用函数is_triangle
Gn!
补充:
上述问题提到了一个开平方根的数学概念,这不由得使很多同学想到了另一个常见数学概念:求幂运算。
首先,在日常的计算机文本编辑中,我们常常用
a ^ b
来表示a的b次方幂运算。但显然在C语言不能直接这么写,因为^
是位运算符中的按位异或运算符。那么在C语言代码中如何实现幂运算呢?
这同样也需要使用一个<math.h>标准数学库中的库函数:
11double pow(double x, double y);
它就表示计算
x ^ y
幂运算,需要注意的是它的参数和返回值类型都是double。
Gn!
问题描述:
键盘录入一个正整数,请判断它是否是一个素数,然后控制台输出对应的结果。
素数是一个大于1的自然数,它仅能被1和自身整除。
参考代码:
求素数问题-参考代码
xxxxxxxxxx
12345
6bool is_prime(int num) {
7// 如果num是1甚至比1还小,那它一定不是素数
8if (num <= 1) {
9return false;
10}
11// 将num从2开始除,一直除到该数的平方根
12for (int i = 2; i <= sqrt(num); i++) {
13if (num % i == 0) {
14// 此过程中只要有一个除尽了,那么就不是素数
15return false;
16}
17return true;
18}
19}
20
21int main(void) {
22int num;
23printf("请输入一个整数: ");
24scanf("%d", &num);
25
26if (is_prime(num)) {
27printf("%d是一个素数!\n", num);
28}
29else {
30printf("%d不是一个素数!\n", num);
31}
32
33return 0;
34}
is_prime函数的定义就很符合我们提到的两个原则:
单一职责原则:代码中的 is_prime 函数遵循了这一原则。该函数仅做一件事情:判断一个给定的数是否为素数。它没有涉及输入/输出或其他非相关任务。
性能优化:在 is_prime 函数中,我们在判断一个数是否为素数时,只循环除到了 sqrt(n) 而不是 n。这是基于数学原理的:如果 n 不是素数(即它可以被整除),那么它必然有一个小于它平方根的因子。因此,只要循环到 sqrt(n) 就足够了,这大大减少了需要检查的范围,从而提高了性能。
Gn!
我们都知道,一个C源文件代码通过编译和链接的过程,生成了一个可执行的程序。这个可执行程序的入口就是C语言的main函数,它是由操作系统来进行调用的,main 函数的返回值类型为 int,它表示程序结束时返回给操作系统的状态码:
如果程序正常终止,main 函数应该返回 0;
如果程序异常终止,那么 main 函数应该返回非 0 的值。
所以一个C进程的结束,可以通过结束返回main函数来完成。
在这里,我们就来学习另一种结束C进程的方式,它比使用return结束返回main函数更灵活,可以使用在程序的任何位置结束程序。即:
声明在<stdlib.h>头文件中的
exit
函数。传递给
exit
函数的参数和main
函数的return返回值具有相同的含义,传递0表示程序正常结束,非0表示程序异常终止。正常结束程序时,我们可以这样写:
xxxxxxxxxx
11exit(0);
因为 0 是一个魔法数字,所以 C 语言允许使用
EXIT_SUCCESS
来替代:xxxxxxxxxx
11exit(EXIT_SUCCESS);
如果异常退出,可以这样写:
xxxxxxxxxx
11exit(EXIT_FAILURE);
EXIT_SUCCESS
和EXIT_FAILURE
都是定义在 <stdlib.h> 中的宏常量,它们的值是由实现决定的,通常分别为 0 和 1。
return
语句和exit
函数之间的差异是:不管哪个函数调用
exit
函数都会导致程序终止,return
语句仅当在main
函数中执行时才会导致程序的终止。Tips:某些程序员仅使用 exit 函数终止程序,这样做的好处是方便以后定位程序的全部退出点。