函数详解
在结构化程序设计中,函数是将任务进行模块划分的基本单位。通过函数,可以把一个复杂的任务分解成若干个易于解决的小任务。充分体现结构化程序设计由粗到精,逐步细化的设计思想,一个大的程序一般应分成若干个程序模块,每个模块实现一个特定的功能,这些模块称之为子程序,在C语言中子程序用函数实现。

如上,我们设计一个图书管理系统,可以按照功能划分,分为多个模块,再将多个模块继续划分,直到划分成实现单一功能的模块。将这些模块一一封装成函数,再通过函数调用,即可实现该系统。
1. 函数的分类
函数分为两类:自定义函数,库函数。
1.1 库函数
C语言库提供给用户的,已经封装好的函数,使用时包含相应的头文件。
举个例子:
c语言库中的#include< ctype.h>头文件
用来确定包含于字符数据中的类型的函数,如是否是数字,是否是英文字母,是否是大写等等。
int main()
{
char ch;
ch = getchar();
if (iscntrl(ch)) // 是否是控制字符,是返回非0值,否返回0值
{
printf("this is a control character\n");
}
else if (isdigit(ch)) // 是否是数字型字符
{
printf("this is a digit character\n");
}
else if (islower(ch)) // 是否是小写字符
{
printf("this is a small character\n");
}
else if (isupper(ch)) // 是否是大写字符
{
printf("this is a capital character\n");
}
else
{
printf("this is a other character\n");
}
return 0;
}
上述代码,调用了一些判断字符的库函数。
1.2 自定义函数
编程者自己根据需求,将某个具有相对独立功能的程序封装成的函数。
返回值类型 函数名(形参列表){ 函数体 }
int add(int a, int b); // 函数声明
int main()
{
int a = 10, b = 20;
int sum = add(a, b); // 函数调用
}
int add(int a, int b) // 函数定义
{
return a + b;
}
注意:
- 在标准C语言中,函数可以嵌套使用,但不能嵌套定义。
- 若函数被定义在该函数调用的代码之后,在调用函数之前一定要声明函数。
- 函数声明:返回值类型 函数名(形参列表 / 形参类型列表);
- 函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型。函数原型的作用是告诉编译器与该函数有关的信息,让编译器知道函数的存在,以及存在形式,即使函数暂时没有定义,编译器也知道如何使用它。
- 有了函数声明,函数定义就可以在任何地方了,甚至是其他文件、静态链接库、动态链接库等。
2. 编译和链接
2.1 文件
- 文件:文件是一个外存的概念,文件只存在于外存(硬盘,U盘,网盘)中,文件由两部分构成,文件名和文件主体。
- 文件的分类:可执行文件和不可执行文件。
- 可执行文件:Windows中,扩展名为:.exe、.bat、.com 等文件都是可执行文件。可执行文件是由指令和数据构成的,在Linux中,是靠文件属性来判断是否可执行。
- 不可执行文件:只有数据构成。
- C/C++中,(.c)(.cpp) 源文件(文本); (.h)(.hpp) 头文件(文本);(.i)预编译文件(文本);(.s)汇编文件;(.o / .obj)二进制目标文件;(.exe)可执行文件(二进制文件)。
2.2 编译链接过程 (X86)

我们所写的文件是不可执行文件,当我们启动编译器后,编译器会将我们源文件经过预编译、编译、链接,最后形成.exe可执行文件,我们执行这个文件时,文件会被加载到内存中执行。
3. 可见性(作用域)和生命周期
3.1 作用域(可见性) 针对于编译链接过程
作用域:标识符能够被识别的范围,只有在作用域内标识符才能被使用。
- 函数中定义的标识符,包括形参和函数体中定义的局部变量,作用域都在该函数内,也称为函数域。
- 文件作用域也叫全局作用域,定义在所有的函数之外的标识符,具有文件作用域,作用域从定义到整个源文件结束。文件中定义的全局变量和函数都具有文件作用域。
3.2 生命周期 针对于程序执行过程
生命周期:标识符从程序开始运行时被创建,具有存储空间,到程序运行结束时消亡,释放存储空间的时间段。
-
局部变量生命周期:函数被调用,分配存储空间,到函数执行结束,释放存储空间。存储在 .stack区。
-
全局变量的生命周期:从程序开始执行,到执行结束。存储在 .data区。
-
动态生命周期:由特定的函数调用或运算符来创建或释放,如调用 malloc() 为变量分配内存空间时变量的生命周期开始,到调用 free() 释放空间或程序结束时,生命周期结束。具有动态生命周期。存储在 .heap 区。
int mian() { { static int a=10; //作用域在这个块作用域中,生命周期是整个文件。 a+=1; } return 0; }
4. 函数调用分析

当函数调用时,系统会给该函数分配栈帧,用来存放该函数中定义的局部变量以及形参,当函数调用完毕,退出函数时,这块栈帧会被回收。函数的返回值会被放到一个临时空间中。局部变量生命周期结束。
当函数调用时,实参传递给形参时,是从参数列表从右向左依次传递的。
4.1 总结函数调用机制
局部变量占用的内存是在程序执行过程中动态建立和释放的。这种动态是通过栈由系统自动管理进行的。当任何一个函数调用发生时,系统都要做以下工作:
- 建立栈空间。
- 为被调用函数中的局部变量分配空间,完成参数传递。
- 保护现场:主调函数运行状态和返回地址入栈。
- 执行被调函数的函数体。
- 被调函数执行结束,释放被调函数中局部变量占用的栈空间。
- 回复现场:取主函数运行状态及返回地址,释放栈空间。
- 继续主调函数的后续语句。
5. 传值调用与传址调用
5.1 传值调用
void Swap_i(int a, int b) // 经典交换函数
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 10, y = 20;
Swap_i(x, y);
return 0;
}
在此,我们顺便讨论一下,形参与实参:
-
实参:实际参数,就是函数在调用时,我们传递给函数的值,如上述代码中,x,y都是实参。
-
形参:函数被调用时,会跟据传递的实参,进行拷贝,形参就是实参的临时拷贝。在调用函数结束后,形参的生命周期结束。上述代码中,a,b是形参。
-
实参和形参在数量上、类型上、顺序上,必须一致。

该函数只能将形参a,b的值进行交换,函数调用完毕,栈帧回收,a,b也随之消失。无法实现x,y的值交换。
5.2 传址调用
void Swap(int* ap, int* bp)
{
int tmp = *ap;
*ap = *bp;
*bp = tmp;
}
int main()
{
int x = 10, y = 20;
Swap(&x, &y);
return 0;
}

通过指针,交换了x,y的值,函数调用完毕,栈帧回收,ap,bp随之消失。
6. 利用函数实现模块化程序设计
函数就是功能,每个函数用来实现一个特定的功能,函数的名字反应其代表的功能。
在设计一个较大的程序时,往往把它们分成若干个程序模块(模块可以是一个函数,也可能是一个.c文件),每个模块包含一个或多个函数,每个函数实现一个特定的功能。一个C程序可有一个主函数和若干个其他函数构成,由主函数调用其他函数,其他函数也可以相互调用。
有些功能可能往往需要在不同地方多次实现,因此通过调用封装的函数来实现做到事半功倍的效果。
6.1 函数设计的基本原则
- 封装:把代码封装进去,对调用者来说的话能隐藏我们的实现功能。
a)调用者(外界)对函数的影响——仅限于入口参数。
b)函数对调用者的影响——函数的返回值,指针参数。 - 检查函数的入口参数的有效合法,检查函数是否调用成功。(对于参数进行断言/if判断,特别时对指针判空)
- 函数的规模要小。不超过80行。
- 函数功能单一,只实现特定功能。
- 函数接口定义清晰,利于调用者使用(函数名,返回值,形参)。
6.2 函数的编写步骤
- 需求分析:明确我们要处理什么问题(完成什么功能)。即函数功能。确定我们处理问题所需要的资源。即函数参数列表信息。
- 算法设计(如何做一件事):根据所需的功能和拥有的资源,理清思路,确定具体步骤(算法)。
- 起函数名(见名知意),确定形参,确定返回值。
- 编写函数。
以上是编写函数的步骤,也是编写程序的步骤,理清步骤,才能做到编码行如流水,一气呵成。
6.3 函数设计的要求
- 可复用性:任何一个提供服务的模块都有可能在其他程序中复用。由于通常很难预测模块的未来使用,因此最好将模块设计成可复用的。
- 可维护性(最重要):将程序模块化后,程序中的错误通常只影响一个模块的实现。因此更容易找到并修正错误,在修正错误之后,重建程序只需要重新编译模块链接程序实现。更广泛的说,为了提高性能或将程序移植到另一个平台上,我们甚至可以替换整个模块的实现。
- 可读性:设计好程序逻辑。函数名、变量名要见名知意,参数设计不要过多,必要时写注释或文档。
- 健壮性:检查参数合法性、输入值的合法性,检查函数的返回值(明确返回值的意思)。
- 高内聚性:模块中的元素应该彼此紧密相关,我们可以认为它们是为了同一目标相互合作的,高内聚性会使模块更易于使用。
- 低耦合性:模块之间应该相互独立,低耦合性可以使程序更便于修改,并方便模块复用。
6.4 C语言函数接口
函数的调用者和其实现者之间存在一个协议,在函数调用之前,调用者要为实现者提供某些条件,在函数返回时,实现者完成调用者需要的功能。
函数接口通过函数名、参数和返回值来描述这个协议,只要函数名和参数名命名合理,参数和返回值的类型定义的准确,调用者仅通过函数接口就知道函数的用法,当函数接口不能表达全部语义时,文档就起了补充作用。
. 低耦合性:模块之间应该相互独立,低耦合性可以使程序更便于修改,并方便模块复用。
6.4 C语言函数接口
函数的调用者和其实现者之间存在一个协议,在函数调用之前,调用者要为实现者提供某些条件,在函数返回时,实现者完成调用者需要的功能。
函数接口通过函数名、参数和返回值来描述这个协议,只要函数名和参数名命名合理,参数和返回值的类型定义的准确,调用者仅通过函数接口就知道函数的用法,当函数接口不能表达全部语义时,文档就起了补充作用。
第六部分属于软件工程的内容,这部分内容有助于我们规范实现模块化设计,编写出高效率、简洁易读的代码。
1247

被折叠的 条评论
为什么被折叠?



