一、函数的概念
1. 核心定义
C 语言中的函数(又称 “子程序”)是一段完成特定独立任务的代码片段,具有固定语法格式和调用规则。类比数学中的y=kx+b(给定 x 可求唯一 y),C 语言函数接收输入(参数),经内部逻辑处理后输出结果(返回值),或仅执行特定操作(无返回值)。
2. 核心价值
- 模块化编程:将大型程序拆解为多个小函数,降低代码复杂度,便于维护和调试(如计算 “学生总成绩” 可拆分为 “输入成绩”“计算总分”“打印结果” 3 个函数)。
- 代码复用:同一功能的函数可在程序中多次调用,无需重复编写(如
printf函数可反复用于输出),提升开发效率。 - 可读性提升:函数名按功能命名(如
Add表示加法、is_leap_year表示判断闰年),使代码逻辑更清晰。
3. 函数分类
- 库函数:由 C 语言标准库(ANSI C 规定)提供,编译器厂商实现的现成函数,直接调用即可(如
printf、scanf、sqrt)。 - 自定义函数:开发者根据实际需求编写的函数,灵活性强,是编程核心(如实现两个数的乘法、数组排序等)。
//计算f的下标
char arr[]="abcdef"; //[a b c d e f \0]
//[0 1 2 3 4 5 ]
int right1= strlen(arr)-1;
int right2= sizeof(arr)/sizeof(arr[0])-2;
printf("%d\n",right1);
printf("%d\n",right2);
二、库函数
1. 基础特性
- 依赖标准库:C 语言仅规定语法规则,不提供库函数;库函数是编译器厂商按 ANSI 标准实现的通用功能集合。
- 需包含头文件:库函数的声明(函数名、参数、返回值)存储在对应头文件中,使用前必须通过
#include引入(如数学函数需#include <math.h>,输入输出函数需#include <stdio.h>)。
2. 常用库函数类别及头文件
| 功能类别 | 代表函数 | 对应头文件 |
|---|---|---|
| 输入输出 | printf、scanf | <stdio.h> |
| 数学运算 | sqrt(平方根)、pow(幂运算) | <math.h> |
| 字符串操作 | strlen(长度)、strcpy(拷贝) | <string.h> |
| 日期时间 | time(获取时间) | <time.h> |
3. 库函数使用步骤(以sqrt为例)
(1)查询函数文档
通过官方工具了解函数细节:
- 函数原型:
double sqrt(double x)(double为返回值类型,x为参数,需传入双精度浮点数)。 - 功能:计算
x的平方根(x≥0,否则返回 NaN)。 - 头文件:
<math.h>。
(2)编写代码并调用
#include <stdio.h> // 包含printf的头文件
#include <math.h> // 包含sqrt的头文件
int main() {
double d = 25.0; // 传入sqrt的参数(double类型)
double result = sqrt(d); // 调用库函数,接收返回值
printf("平方根:%lf\n", result); // 输出结果:5.000000
return 0;
}
(3)注意事项
- 必须匹配参数类型:若传入
int类型(如sqrt(25)),编译器会自动转换为double,但建议显式声明类型(如sqrt(25.0))。 - 不包含头文件的后果:编译器无法识别函数,可能报 “未定义标识符” 警告(如未包含
<math.h>时,sqrt可能被默认当作int类型函数,导致返回值错误)。
4. 库函数学习工具
- 官方文档:cppreference.com(权威、全面)。
- 辅助工具:cplusplus.com(示例清晰,适合初学者)。
三、自定义函数
1. 语法格式
ret_type fun_name(parameter_list) {
// 函数体:完成任务的代码逻辑
statement;
return return_value; // 与ret_type匹配(void类型可省略)
}
各部分说明:
ret_type(返回值类型):函数执行后返回结果的类型,如int(整型)、double(双精度浮点型);若无需返回值,用void(空类型)。fun_name(函数名):遵循标识符命名规则(字母、数字、下划线组成,首字符非数字),需 “见名知义”(如Sub表示减法、SortArray表示数组排序)。parameter_list(参数列表):函数接收的输入数据,格式为 “类型 1 变量名 1, 类型 2 变量名 2,...”;无参数时可写void(或省略)。函数体:用{}包裹的代码块,是函数的核心逻辑(如计算、循环、判断等)。return语句:返回结果给调用者,执行后函数立即结束(后续代码不执行);void类型函数可写return;(或省略)。
2. 自定义函数示例(3 个典型场景)
(1)有参数、有返回值(加法函数)
#include <stdio.h>
// 函数定义:计算两个int类型数的和
int Add(int x, int y) { // x、y为形参,接收实参的值
int z = x + y; // 函数体:计算逻辑
return z; // 返回结果z(类型与ret_type一致)
}
// 简化写法(省略临时变量z)
// int Add(int x, int y) { return x + y; }
int main() {
int a = 10, b = 20;
// 函数调用:将a、b作为实参传入,接收返回值
int sum = Add(a, b);
printf("和:%d\n", sum); // 输出:30
return 0;
}
(2)有参数、无返回值(数组置零函数)
#include <stdio.h>
// 函数定义:将数组所有元素置为0(无需返回值,用void)
void SetZero(int arr[], int sz) { // arr为数组参数,sz为数组长度
for (int i = 0; i < sz; i++) {
arr[i] = 0; // 函数体:修改数组元素
}
// 无return语句(或写return;)
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int sz = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
SetZero(arr, sz); // 函数调用:传入数组名和长度
// 打印数组(验证结果)
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]); // 输出:0 0 0 0 0
}
return 0;
}
(3)无参数、有返回值(获取随机数函数)
#include <stdio.h>
#include <time.h> // 包含time函数的头文件
// 函数定义:生成1-100的随机数(无参数)
int GetRandom() {
srand((unsigned int)time(NULL)); // 设置随机数种子
return rand() % 100 + 1; // 返回1-100的随机数
}
int main() {
int num = GetRandom(); // 函数调用:无实参传入
printf("随机数:%d\n", num); // 输出:1-100的随机数
return 0;
}
3. 自定义函数设计原则
- 单一职责:一个函数只完成一个核心任务(如 “计算加法” 和 “打印结果” 拆分两个函数)。
- 参数清晰:明确参数的类型、含义和个数,避免过多参数(超过 3 个可考虑用结构体封装)。
- 返回值明确:若函数有计算结果,必须返回;若仅执行操作(如修改数组),用
void。
四、形参和实参
1. 定义与区别
| 类别 | 定义 | 特点 | 示例(Add 函数) |
|---|---|---|---|
| 实参(实际参数) | 调用函数时传递给函数的真实数据 | 必须有确定值(变量、常量、表达式均可),占用独立内存 | Add(a, b)中的a、b(或Add(10, 20)中的10、20) |
| 形参(形式参数) | 函数定义时括号中的参数变量 | 仅在函数调用时实例化(申请内存),接收实参的值;函数执行完后销毁 | Add(int x, int y)中的x、y |
2. 核心关系:临时拷贝
- 形参是实参的临时拷贝:函数调用时,编译器为形参分配内存,将实参的值复制到形参中;形参和实参的内存地址完全不同,修改形参的值不会影响实参。
- 验证示例:
#include <stdio.h>
void Swap(int x, int y) { // x、y为形参
int temp = x;
x = y;
y = temp; 修改的是形参x、y的值
}
int main() {
int a = 10, b = 20;
Swap(a, b); // a、b为实参
printf("a=%d, b=%d\n", a, b); // 输出:a=10, b=20(实参未变)
return 0;
}
- 结论:上述代码无法实现交换功能,因为
Swap函数修改的是形参x、y(临时拷贝),实参a、b的内存未被修改(若需修改实参,需用指针传递)。
3. 注意事项
- 形参的类型必须与实参匹配:若实参是
int类型,形参不能是double类型(否则会发生隐式类型转换,可能导致数据丢失)。 - 形参仅在函数内部有效:函数执行结束后,形参的内存被释放,外部无法访问。
五、return 语句
1. 基本用法
- 带返回值:
return 表达式;(表达式结果需与函数返回类型匹配),示例:
int Max(int x, int y) {
if (x > y)
return x; // 返回x的值,函数结束
else
return y; // 返回y的值,函数结束
}
- 无返回值:
return;(仅用于void类型函数),示例:
void PrintHello() {
printf("Hello World!\n");
return; // 可选,函数执行到此处结束
printf("This line will not be executed\n"); // 不会执行
}
2. 关键规则
- 函数返回类型与 return 值类型需一致:若不一致,编译器会自动隐式转换(如
return 3.14用于int类型函数,会转换为3),可能导致错误,建议显式匹配。 - 分支结构中必须保证所有路径有返回值:若函数有返回类型(非
void),且包含if-else、switch等分支,需确保每个分支都有return语句,否则编译报错。
// 错误示例(缺少else分支的return)
int GetSign(int x) {
if (x > 0)
return 1;
else if (x == 0)
return 0;
// 若x<0,无return语句,编译报错
}
// 正确示例
int GetSign(int x) {
if (x > 0)
return 1;
else if (x == 0)
return 0;
else
return -1; // 补充else分支的return
}
- return 语句执行后函数立即结束:后续代码无论是否在循环、判断中,均不再执行。
六、数组做函数参数
1. 数组传参的本质
- 数组传参时,传递的是数组首元素的地址,而非整个数组的拷贝。
- 形参接收的是地址,因此形参操作的数组与实参数组是同一个数组(修改形参数组元素会直接修改实参数组)。
- 关键结论:形参无法通过
sizeof获取数组的真实长度(sizeof(arr)得到的是地址的大小,而非数组总字节数),因此必须单独传递数组长度作为参数。
2. 形参的合法写法
| 实参形式 | 形参合法写法 | 说明 |
|---|---|---|
一维数组int arr[] | int arr[] 或 int arr[10](10 可省略)或 int* arr(指针形式) | 一维数组形参的大小可省略,编译器会忽略括号中的数字 |
二维数组int arr[3][4] | int arr[][4](列数 4 不可省略)或 int (*arr)[4](指针形式) | 二维数组形参的行数可省略,但列数必须明确(否则编译器无法计算元素地址) |
3. 示例:数组打印与修改
#include <stdio.h>
// 形参:数组arr(接收首元素地址),长度sz(单独传递)
void PrintArray(int arr[], int sz) {
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]); // 操作实参数组
}
printf("\n");
}
void ModifyArray(int arr[], int sz) {
for (int i = 0; i < sz; i++) {
arr[i] *= 2; // 修改实参数组元素(翻倍)
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int sz = sizeof(arr) / sizeof(arr[0]); // 计算真实长度(5)
PrintArray(arr, sz); // 输出:1 2 3 4 5
ModifyArray(arr, sz); // 修改数组
PrintArray(arr, sz); // 输出:2 4 6 8 10(实参数组已被修改)
return 0;
}
4. 常见错误
- 错误 1:形参写
int arr[],但未传递长度,用sizeof(arr)/sizeof(arr[0])计算长度(结果错误)。 - 错误 2:二维数组形参写
int arr[][](省略列数),编译报错(需明确列数,如int arr[][4])。
七、嵌套调用和链式访问
1. 嵌套调用(函数调用函数)
- 定义:一个函数内部调用另一个函数,形成嵌套关系(类似乐高积木拼接),但函数不能嵌套定义(即不能在一个函数内部定义另一个函数)。
- 示例:计算某年某月的天数(调用闰年判断函数)
//计算某年某月有多少天
// 子函数1:判断是否为闰年(返回1=闰年,0=非闰年)
int is_leap_year(int y) {
return ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0));
}
// 子函数2:计算某月天数(调用is_leap_year函数)
int get_days(int y, int m) {
int days[] = {0,31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 0,1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10, 11, 12
if (m == 2 && is_leap_year(y)) // 嵌套调用is_leap_year
days[2] += 1; // 闰年2月29天
return days[m];
}
int year = 2024, month = 2;
int day_count = get_days(year, month); // 调用get_days函数
printf("%d年%d月有%d天\n", year, month, day_count); // 输出:2024年2月有29天
return 0;
- 调用关系:
main→get_days→is_leap_year(层层嵌套,执行完子函数后返回上一层)。
2. 链式访问(函数返回值作为另一个函数的参数)
- 定义:将一个函数的返回值直接作为另一个函数的参数,形成 “链条” 式调用。
- 示例 1:简化字符串长度打印(
strlen返回值作为printf参数)
#include <stdio.h>
#include <string.h>
int main() {
// 链式访问:strlen的返回值(字符串长度)作为printf的参数
printf("字符串长度:%d\n", strlen("hello world")); // 输出:11
return 0;
}
- 示例 2:多层链式访问(
printf嵌套调用)
#include <stdio.h>
int main() {
// printf返回值:打印的字符个数(如printf("43")返回2)
printf("%d\n", printf("%d", printf("%d", 43))); // 输出:4321
printf("%d \n", printf("%d ", printf("%d ", 43))); // 输出:43 3 2
return 0;
}
-
解析:
- 最内层
printf("%d", 43):打印 “43”(2 个字符),返回 2。 - 中层
printf("%d", 2):打印 “2”(1 个字符),返回 1。 - 最外层
printf("%d", 1):打印 “1”,最终输出 “4321”。
- 最内层
八、函数的声明和定义
1. 基本概念
- 函数定义:函数的完整实现(包含函数体),是函数的 “具体实现”,整个程序中只能定义一次。示例:
int Add(int x, int y) { return x + y; } - 函数声明:告知编译器函数的 “存在”(函数名、返回类型、参数类型),无需函数体,可声明多次(通常在头文件中)。示例:
int Add(int x, int y);(参数名可省略,如int Add(int, int);)
2. 声明的必要性:解决 “先调用后定义” 的问题
C 语言编译器按从上到下的顺序扫描代码,若函数调用在定义之前,编译器会因 “未识别函数” 报错(或警告)。
- 错误示例(先调用后定义):
- 编译警告:
warning C4013: "Add"未定义: 假设外部返回int(编译器无法确认函数类型,可能导致错误)。 - 解决方法:在调用前添加函数声明:
#include <stdio.h>
int main() {
int sum = Add(10, 20); // 调用Add函数(此时未定义)
printf("sum=%d\n", sum);
return 0;
}
// 函数定义在调用之后
int Add(int x, int y) {
return x + y;
}
#include <stdio.h>
// 函数声明(告知编译器Add函数的信息)
int Add(int x, int y); // 或int Add(int, int);
int main() {
int sum = Add(10, 20); // 编译器已识别函数,正常调用
printf("sum=%d\n", sum);
return 0;
}
// 函数定义
int Add(int x, int y) {
return x + y;
}
3. 多文件编程:声明与定义分离(企业级规范,方便多人协同)
当程序代码较多时,通常将函数声明放在头文件(.h) 中,函数定义放在源文件(.c) 中,实现模块化管理。
-
示例结构:
plaintext
项目文件夹/
├─ add.h (函数声明)
├─ add.c (函数定义)
└─ test.c (主函数,调用函数)
-
文件内容:
add.h(头文件,存放声明):
#pragma once // 防止头文件重复包含
// 函数声明
int Add(int x, int y);
-
add.c(源文件,存放定义):
#include "add.h" // 包含头文件(确保声明与定义一致)
// 函数定义
int Add(int x, int y) {
return x + y;
}
-
test.c(主文件,调用函数):
#include <stdio.h>
#include "add.h" // 包含头文件(获取函数声明)
int main() {
int sum = Add(10, 20);
printf("sum=%d\n", sum); // 输出:30
return 0;
}
-
核心优势:
- 代码分离:声明(接口)与实现分离,修改函数实现(
add.c)无需修改调用者(test.c)。 - 复用性:多个源文件可通过包含头文件(
#include "add.h")调用Add函数。 - 避免重复定义:头文件用
#pragma once(或 #ifndef 宏)防止重复包含,避免编译错误。
- 代码分离:声明(接口)与实现分离,修改函数实现(
九、static 和 extern 关键字(函数与变量的作用域控制)
1. 基础概念:作用域与生命周期
- 作用域:变量 / 函数的 “可用范围”(局部变量→局部范围,全局变量 / 函数→整个工程)。
- 生命周期:变量从 “创建(申请内存)” 到 “销毁(释放内存)” 的时间段(局部变量→进入作用域创建,出作用域销毁;全局变量→程序启动创建,程序结束销毁)。
2. static 关键字的 3 种用法
(1)修饰局部变量:延长生命周期
- 未修饰的局部变量:存储在栈区,生命周期为 “进入函数→退出函数”(每次调用重新初始化)。
- static 修饰的局部变量:存储在静态区,生命周期与程序一致(仅初始化一次,退出函数不销毁,下次调用沿用上次的值)。
- 示例:
#include <stdio.h>
void Test() {
static int i = 0; // static修饰局部变量,仅初始化一次
i++;
printf("i=%d ", i);
}
int main() {
for (int j = 0; j < 5; j++) {
Test(); // 调用5次Test函数
}
// 输出:i=1 i=2 i=3 i=4 i=5(i的值累加,未重新初始化)
return 0;
}
- 注意:static 不改变局部变量的作用域(仍仅在函数内部可用),仅改变生命周期。
(2)修饰全局变量:限制作用域
-
未修饰的全局变量:具有外部链接属性,可在整个工程的多个源文件中使用(需用
extern声明)。 -
static 修饰的全局变量:具有内部链接属性,仅在当前源文件中可用,其他源文件无法访问(即使
extern声明也报错)。 -
示例(多文件):
file1.c(定义 static 全局变量):
static int g_val = 100; // static修饰全局变量
-
file2.c(尝试访问 g_val):
#include <stdio.h>
extern int g_val; // 声明外部全局变量
int main() {
printf("%d\n", g_val); // 编译报错:无法解析的外部符号g_val
return 0;
}
- 用途:防止全局变量被其他文件意外修改,提高代码安全性。
(3)修饰函数:限制作用域
-
未修饰的函数:具有外部链接属性,可在整个工程的多个源文件中使用(需用
extern声明)。 -
static 修饰的函数:具有内部链接属性,仅在当前源文件中可用,其他源文件无法调用。
-
示例(多文件):
add.c(定义 static 函数):
static int Add(int x, int y) { // static修饰函数
return x + y;
}
-
test.c(尝试调用 Add 函数):
#include <stdio.h>
extern int Add(int x, int y); // 声明外部函数
int main() {
int sum = Add(10, 20); // 编译报错:无法解析的外部符号Add
printf("sum=%d\n", sum);
return 0;
}
- 用途:隐藏内部函数(仅在当前文件使用),避免函数名冲突(多个文件可定义同名 static 函数)。
3. extern 关键字:声明外部符号
-
作用:声明 “在其他源文件中定义的全局变量或函数”,告知编译器 “该符号存在,无需报错,链接时会找到”。
-
示例(多文件调用全局变量):
file1.c(定义全局变量):
int g_val = 100; // 未修饰的全局变量(外部链接属性)
-
file2.c(调用 g_val):
#include <stdio.h>
extern int g_val; // 声明外部全局变量(无需定义,仅告知编译器)
int main() {
printf("%d\n", g_val); // 输出:100(链接时找到file1.c中的g_val)
return 0;
}
- 注意:
extern仅用于 “声明”,不能用于 “定义”(如extern int g_val = 100;是错误的,会被当作定义,可能导致重复定义错误)。
十、常见问题与注意事项
- 函数名冲突:不同文件中的非 static 函数名不能重复(否则链接报错),建议按功能命名(如
Add_int、Add_double区分不同类型的加法函数)。 - 数组传参遗漏长度:形参无法获取数组真实长度,必须单独传递长度参数,否则循环可能越界。
- return 语句缺失:非 void 类型函数的所有分支必须有 return 语句,否则编译报错。
- 形参修改实参:形参是临时拷贝,修改形参无法影响实参,需修改实参需用指针传递(后续章节讲解)。
- 头文件重复包含:头文件需添加
#pragma once或#ifndef宏,防止重复包含导致的重复声明错误。