一、函数的本质:什么是 C 语言中的函数?
数学中的函数我们并不陌生,比如一次函数y=kx+b,给定 x 就能得到对应的 y 值。C 语言中的函数(也称为子程序)延续了这一核心思想 ------一段完成特定任务的独立代码块,通过接收输入(参数)、执行逻辑、返回结果(可选),实现功能的封装与复用。
C 语言程序的本质的是函数的组合:一个复杂的任务可以拆解为多个小型函数,每个函数负责单一功能,最终通过函数间的配合完成整体需求。这种设计的优势在于:
- 代码复用:避免重复编写相同逻辑,提升开发效率;
- 结构清晰:每个函数职责明确,便于维护和调试;
- 模块化开发:支持多人协作,各自负责不同函数模块。
C 语言中的函数主要分为两类:
- 库函数:编译器厂商根据 ANSI C 标准实现的现成函数(如
printf、scanf),直接调用即可; - 自定义函数:开发者根据需求编写的函数,灵活性更高,是编程的核心重点。
二、库函数:站在巨人的肩膀上编程
库函数是 C 语言标准库提供的现成工具,覆盖了数学计算、字符串处理、输入输出等常见场景。使用库函数无需关心底层实现,只需掌握调用方法,就能大幅提升开发效率。
2.1 库函数的使用前提
库函数的声明都包含在对应的头文件中,因此使用前必须通过#include引入头文件,否则可能导致编译错误。例如:
- 输入输出函数(
printf、scanf)需要引入<stdio.h>; - 数学函数(
sqrt、pow)需要引入<math.h>; - 字符串函数(
strlen、strcpy)需要引入<string.h>。
2.2 库函数的调用示例:计算平方根
以sqrt函数(计算平方根)为例,讲解库函数的完整使用流程:
- 查看函数文档:明确函数原型、参数、返回值和头文件;
- 函数原型:
double sqrt(double x); - 功能:返回 x 的平方根(x 需为非负数);
- 头文件:
<math.h>。
- 函数原型:
- 编写代码:
cpp
#include <stdio.h>
#include <math.h>
int main() {
double d = 16.0;
double result = sqrt(d); // 调用sqrt函数
printf("平方根:%lf\n", result); // 输出结果:4.000000
return 0;
}
2.3 库函数的学习工具
无需死记硬背所有库函数,学会使用以下工具即可按需查询:
- C/C++ 官方文档:https://zh.cppreference.com/w/c/header;
- cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/。
三、自定义函数:打造专属功能模块
库函数虽方便,但无法满足所有个性化需求,自定义函数才是 C 语言编程的核心。自定义函数的设计自由度极高,可根据实际需求灵活定义功能、参数和返回值。
3.1 自定义函数的语法结构
自定义函数的语法与库函数一致,核心由 4 部分组成:
cpp
ret_type fun_name(parameter_list) {
// 函数体:实现功能的代码逻辑
}
ret_type:返回值类型,指定函数执行后的返回结果类型(如int、double);若无需返回值,用void表示;fun_name:函数名,需遵循标识符命名规则,建议根据功能命名(如Add表示加法,isLeapYear表示判断闰年);parameter_list:参数列表,即函数的输入(原材料),可无参数(写void)或多个参数(需指定类型和名称);- 函数体:用
{}包裹的代码块,是函数功能的核心实现。
3.2 自定义函数示例:实现两数相加
需求:编写一个函数,接收两个整型参数,返回它们的和。
cpp
#include <stdio.h>
// 自定义加法函数
int Add(int x, int y) {
return x + y; // 直接返回表达式结果,简化代码
}
int main() {
int a = 0, b = 0;
printf("请输入两个整数:");
scanf("%d %d", &a, &b);
int sum = Add(a, b); // 调用自定义函数
printf("和为:%d\n", sum);
return 0;
}
四、形参和实参:函数的输入传递机制
在函数调用过程中,参数分为实际参数(实参) 和形式参数(形参),两者既相关又独立。
在函数调用过程中,参数分为实际参数(实参) 和形式参数(形参),两者既相关又独立。
4.1 概念区分
- 实参:调用函数时传递的具体值(如上述代码中
Add(a, b)的a和b),是真实存在的数据,占用内存空间; - 形参:函数定义时括号中的参数(如
Add(int x, int y)的x和y),仅在函数被调用时才分配内存,用于接收实参的值,函数执行结束后立即释放。
4.2 核心关系:形参是实参的临时拷贝
关键结论:形参和实参占用不同的内存空间,形参只是实参的一份临时拷贝。
这意味着:函数内部修改形参的值,不会影响实参。例如:
cpp
#include <stdio.h>
void Swap(int x, int y) {
int temp = x;
x = y;
y = temp; 交换的是形参x和y的值
}
int main() {
int a = 3, b = 5;
printf("交换前:a=%d, b=%d\n", a, b); // 输出:3 5
Swap(a, b);
printf("交换后:a=%d, b=%d\n", a, b); // 输出:3 5(实参未变)
return 0;
}
五、return 语句:函数的返回机制
return语句是函数返回结果的核心,其使用有明确的规则和注意事项:
- 返回值与函数类型匹配:
return后的值类型需与函数声明的ret_type一致,若不一致,系统会自动隐式转换(可能导致精度丢失或错误); - 终止函数执行:
return语句执行后,函数立即结束,后续代码不再执行
cpp
int Add(int x, int y) {
return x + y;
printf("这行代码不会执行"); // unreachable code
}
六、数组做函数参数:批量数据的传递
当需要向函数传递批量数据时,数组是常用选择,但数组传参有特殊规则,需重点掌握。
6.1 数组传参的核心规则
- 形参可写为数组形式(
int arr[]),也可写为指针形式(int* arr),本质是传递数组首元素的地址; - 一维数组形参的大小可省略(如
int arr[]),二维数组形参的行可省略,但列不能省略(如int arr[][3]); - 函数内部操作的是原数组:数组传参时,形参并未创建新数组,而是直接操作实参数组的内存空间,修改形参数组会影响原数组。
6.2 示例:数组初始化与打印
需求:编写两个函数,分别将数组所有元素置为 - 1,以及打印数组内容。
cpp
#include <stdio.h>
// 将数组所有元素置为-1
void SetArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] = -1; // 操作原数组
}
}
// 打印数组
void PrintArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
printf("修改前:");
PrintArray(arr, size); // 输出:1 2 3 4 5
SetArray(arr, size);
printf("修改后:");
PrintArray(arr, size); // 输出:-1 -1 -1 -1 -1
return 0;
}
七、函数的调用技巧:嵌套调用与链式访问
函数的强大之处不仅在于独立功能的实现,更在于函数间的灵活配合,主要体现为嵌套调用和链式访问。
7.1 嵌套调用:函数间的层级协作
嵌套调用指一个函数内部调用另一个函数,如同乐高积木的拼接,是构建复杂程序的核心方式。
示例:计算某年某月的天数(需判断是否为闰年)
cpp
#include <stdio.h>
// 判断是否为闰年:是返回1,否返回0
int IsLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 计算某月天数:调用IsLeapYear函数
int GetDays(int year, int month) {
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[month];
if (IsLeapYear(year) && month == 2) {
day += 1; // 闰年2月多1天
}
return day;
}
int main() {
int year = 0, month = 0;
printf("请输入年和月(如2024 2):");
scanf("%d %d", &year, &month);
int result = GetDays(year, month);
printf("%d年%d月有%d天\n", year, month, result);
return 0;
}
7.2 链式访问:函数返回值的连续使用
链式访问指将一个函数的返回值作为另一个函数的参数,如同链条一样串联函数调用。
示例 2:多层链式访问(趣味题)
cpp
#include <stdio.h>
int main() {
// printf返回值为打印的字符个数
printf("%d", printf("%d", printf("%d", 43))); // 输出:4321
return 0;
}
解析:
- 最内层
printf("%d", 43):打印43(2 个字符),返回 2; - 中间层
printf("%d", 2):打印2(1 个字符),返回 1; - 最外层
printf("%d", 1):打印1,最终输出4321。
八、函数的声明与定义:多文件编程基础
在实际开发中,代码量较大时需拆分到多个文件,这就需要理解函数的声明与定义的区别,以及static和extern关键字的作用。
8.1 声明与定义的区别
- 函数定义:完整的函数实现(包含函数体),用于明确函数的功能逻辑,一个函数只能定义一次;
- 函数声明:告诉编译器函数的名称、返回类型和参数列表(无需函数体),用于让编译器识别函数,可声明多次。
8.2 单文件场景:声明的作用
若函数定义在调用之后,需先声明函数,否则编译器会报错。例如:
cpp
#include <stdio.h>
// 函数声明(参数名可省略,仅需类型)
int IsLeapYear(int);
int main() {
int year = 2024;
if (IsLeapYear(year)) { // 调用前已声明,编译器可识别
printf("%d是闰年\n", year);
}
return 0;
}
// 函数定义(在调用之后)
int IsLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
8.3 多文件场景:头文件与源文件分离
企业开发中,通常将函数声明放在.h头文件中,函数实现放在.c源文件中,结构如下:
add.h(头文件):函数声明;add.c(源文件):函数定义;test.c(主文件):调用函数。
示例:
add.h(声明)
cpp
#ifndef ADD_H
#define ADD_H
int Add(int x, int y); // 函数声明
#endif
add.c(定义):
cpp
#include "add.h"
int Add(int x, int y) {
return x + y; // 函数实现
}
test.c(调用):
cpp
#include <stdio.h>
#include "add.h" // 包含头文件,获取函数声明
int main() {
int sum = Add(3, 5);
printf("sum = %d\n", sum); // 输出:8
return 0;
}
8.4 static 与 extern:控制函数 / 变量的作用域
extern:用于声明外部符号(函数或全局变量),允许在当前文件中使用其他文件定义的符号;static:用于限制符号的作用域,使其仅在当前文件中可用。
(1)static 修饰局部变量
改变局部变量的生命周期:局部变量默认存储在栈区,函数执行结束后销毁;被static修饰后存储在静态区,生命周期与程序一致(程序结束后才销毁),但作用域仍为函数内部。
示例:
cpp
#include <stdio.h>
void Test() {
static int i = 0; // static修饰局部变量
i++;
printf("%d ", i);
}
int main() {
for (int j = 0; j < 5; j++) {
Test(); // 输出:1 2 3 4 5(i的值累加)
}
return 0;
}
(2)static 修饰全局变量 / 函数
限制作用域:全局变量和函数默认具有外部链接属性(可被其他文件使用),被static修饰后变为内部链接属性(仅当前文件可用)。
示例:若add.c中定义static int g_val = 10,则test.c中无法通过extern int g_val使用该变量;同理,static int Add(...)定义的函数,也无法被其他文件调用。
九、总结
函数是 C 语言的核心模块化工具,掌握以下关键点,就能灵活运用函数编写高效、清晰的代码:
- 理解函数的本质:封装功能、实现复用;
- 库函数:学会查询文档,按需引入头文件;
- 自定义函数:掌握语法结构,根据需求设计参数和返回值;
- 形参实参:明确 "临时拷贝" 特性,避免踩坑;
- 数组传参:本质是传递首元素地址,需手动传递数组长度;
- 调用技巧:嵌套调用实现复杂逻辑,链式访问简化代码;
- 声明与定义:支持多文件编程,
static和extern控制作用域。