一、函数概述
函数是实现一定功能的独立代码模块,使用函数需遵循先定义后使用的原则。
使用函数的优势
- 可提供功能给他人使用,也可使用他人提供的函数,减少代码量。
- 减少重复性代码。
- 实现结构化(模块化)程序设计思想,即通过多文件与函数将大型任务划分为相互独立的小型任务模块。
函数的作用
- 代码复用:避免重复编写相同功能的代码。
- 模块化设计:将复杂程序拆分成多个小功能模块,每个函数负责独立任务,使代码逻辑更清晰。
- 便于维护和调试:单个函数功能单一,出现问题易定位和修改,无需改动整个程序。
- 提高开发效率:便于多人协同开发,分工明确,编写不同函数后组合成完整程序。
重要说明
C 语言程序必须包含一个 main 函数,可包含零个或多个其他函数。
二、函数分类
按来源分
- 库函数:由 C 语言标准库实现并提供,如scanf()、printf()、fgets()、fputs()、strlen()等。
- 自定义函数:需程序员自行实现,开发中大部分函数为此类。
按参数分
- 无参函数:调用时无需传递参数,可有可无返回值,如show_all()。
- 有参函数:调用时需传递参数,常配套返回值使用,如printf("%d\n", 12)。
按返回值分
- 有返回值函数:执行后返回一个值,如if (scanf("%d", &num) != 1)。
- 无返回值函数(void):仅执行操作,不返回值。
从函数调用角度分
- 主调函数:主动调用其他函数的函数,main 函数只能作为主调函数。
- 被调函数:被其他函数调用的函数。需要注意的是,一个函数可能既是主调函数又是被调函数。
三、函数定义
语法
[返回类型] 函数名([形参列表]) // 函数头|函数首部 { 函数体语句 // 整个{}包裹的内容,{}不能省略 } { 函数体语句 // 整个{}包裹的内容,{}不能省略 } 函数体语句 // 整个{}包裹的内容,{}不能省略 } } |
函数头各部分说明
- 返回类型:函数返回值的类型。
- 函数名:遵循标识符命名规则,不能以数字开头,只能包含大小写字母、下划线、数字。建议采用小写 + 下划线,如show_all(),或小驼峰命名法,即第一个单词首字母小写,其他单词首字母大写,如showAll()。
- 形参列表:用于接收主调函数传递的数据,多个参数用逗号分隔,每个形参都需指明类型。
特殊说明
- 形参是主调函数向被调函数传递数据的载体,函数定义时系统不为形参申请内存,调用时才申请,函数返回时自动回收内存。
- C 语言中所有参数传递都是值传递,若要修改实参需传递指针,指针传递本质也是值传递。
- void类型表示空类型或无类型,无返回值时应明确使用。在 C 语言中,无返回值函数的return语句可省略。
- C89 标准允许省略函数返回类型标识符,默认返回 int;但 C99/C11 标准要求必须明确指定返回类型,不再支持默认 int 类型。
- 函数中return语句形式可为return(表达式)或return 表达式。
- 形参列表有多个参数时,即使类型相同,也需逐个说明,不能省略类型。若形参列表无参数,可不写或用void标识。
- C89 开始支持变长参数,需引入<stdarg.h>,语法为[返回类型] 函数名(参数列表, ...)。
四、形参和实参
形参(形式参数)
函数定义时指定的参数,用于接收数据。函数调用时系统为其申请内存,返回时自动回收。
实参(实际参数)
函数调用时主调函数传递给被调函数的具体数据,可是常量、变量、表达式、带有返回值的函数等。
关键特性
- 类型多样性:实参类型丰富,可根据需求选用常量、变量、表达式或带返回值的函数等。
- 类型转换:实参和形参类型不同时,按赋值规则进行类型转换,可能导致精度丢失。
- 单向值传递:C 语言采用单向值传递机制,实参仅将值赋给形参,形参值的改变不影响实参。
- 内存独立性:实参和形参在内存中占据不同空间,形参有独立内存地址。
五、函数的返回值
- 不需要返回值的函数可以没有return语句,返回类型为void时,return关键字可省略。
- 一个函数中可有多条return语句,但同一时刻只有一条被执行。
- 返回类型一般应与return语句返回的数据类型一致,不一致时需符合 C 语言隐式转换规则。
六、函数的调用
调用方式
- 函数语句:对于无返回值的函数直接调用,如test();对于有返回值的函数,一般在主调函数中接收返回值,如int res = max(2,4)。
- 函数表达式:如4 + max(2,4)、scanf("%d", &num) != 1、(c = getchar()) != '\0'。
- 函数参数:函数可作为实参,如printf("%d", (int)fabs(number)),但作为形参需使用函数指针。
调用条件
- 被调用函数必须已定义。
- 使用库函数需在文件开头用#include包含对应头文件。
- 使用自定义函数且其在主调函数后面时,需在主调函数中对被调函数进行声明。
七、函数的声明
函数调用若出现在定义之前,需进行函数声明。完整的函数使用包括函数声明、函数定义和函数调用。
定义
函数声明保留函数头,便于编译系统检查,可省略形参名称,但不能省略参数类型。同一文件中,函数声明需在所有函数定义最前面,也可抽取到对应的.h文件中。
注意事项
函数定义时参数列表要和声明完全对应,且必须保留形参名称,不能省略参数的数据类型、个数和名称。
声明方式
- 函数头加上分号,如int add(int a, int b);。
- 函数头加上分号,省略形参名称,如int add(int, int);。
八、变量和函数底层工作原理(扩展)
变量的底层执行机制
变量本质是内存中的存储空间,底层处理涉及编译期符号解析和运行时内存分配与访问。
- 编译阶段:编译器为每个变量创建符号表条目,记录相关信息,全局和静态变量分配到对应段并计算偏移量,局部变量记录在栈帧中的相对位置。
- 运行阶段:全局 / 静态变量在程序加载时映射到内存固定位置,局部变量在函数调用时通过栈帧分配地址,动态变量通过系统调用在堆中分配内存。
- 访问指令:CPU 通过地址计算得到内存地址,执行加载或存储指令访问变量。
函数的底层执行机制
函数执行是指令流的跳转与栈帧管理,涉及调用、栈帧创建、参数传递和返回值处理。
- 编译阶段:编译器将函数体编译为机器指令存储在代码段,记录函数名与起始地址,参数和返回值传递方式由调用约定决定。
- 调用步骤:包括参数入栈、保存返回地址、跳转至函数入口、创建栈帧、执行函数体、返回结果、恢复栈帧并返回等环节。
关键底层概念
- 内存分段:分为代码段、数据段、BSS 段、栈、堆,分别存储指令、全局变量、未初始化全局变量、局部变量和函数调用信息、动态内存。
- 栈帧:每个函数调用对应一个栈帧,包含参数、返回地址、局部变量,由基址指针和栈指针界定。
- 地址绑定:变量和函数地址在编译期、加载期或运行期确定。