8 学习函数
1. 本质与入口:程序从 main 函数启动,函数是构建程序功能的基本单元,实现“从无到有”的功能拆分。
2. 设计意义:降低耦合性(功能模块独立,关联少)、提升复用性(代码可重复调用) 。 void prtchar(); 是声明(告知编译器存在此函数,未定义实现),区别于函数定义(含具体逻辑)。
1. 定义限制:函数不可嵌套定义(函数内部不能再定义新函数)。
2. 完整结构:
返回值类型 函数名(形式参数列表)
{
语句块(实现逻辑)
}
1)返回值类型:可是基本类型( int / char 等)、指针,但不能是数组类型(数组名退化为指针传递,实际用指针替代)。
2)形式参数(形参):定义函数时的参数占位符,调用时由实参传递值;形参名可自定义,需明确每个参数的类型(如 int a, int b ,不能简写 int a,b ,语法需严谨)。
3. 调用规则:
1)调用前需确保“可见”:要么函数在调用处之前定义,要么通过 extern 声明(多文件场景)。
2)实参、形参匹配:数量一致、类型兼容(支持隐式转换,如 double 转 int 会截断小数;但类型差异大时(如 指针 转 int )需显式强转,否则编译报错 );实参、形参名称可相同(作用域不同,互不影响 )。
示例(含调用逻辑):
// 函数定义(加法逻辑)
int add(int a, int b)
{
int ret = a + b;
return ret;
}
// 主调函数(main)
int main(void)
{
int i = 10, j = 20;
int sum = add(i, j); // 实参i/j传值给形参a/b
printf("结果:%d\n", sum);
printf("直接调用:%d\n", add(11, 22));
return 0;
}
4. return 语句细节:
1)立即终止函数,将结果“带回”调用处;若函数无返回值( void 类型),可省略 return (或写 return; 显式结束 )。
2)返回值类型需与函数声明/定义的类型兼容:若函数定义时未明确返回值类型(旧语法兼容),默认按 int 处理;但现代 C 标准建议显式声明,避免歧义 。
3)禁用场景:函数调用表达式(如 add(1,2) )不能直接作为形参传递(需先计算值再传参 )。
5. 特殊函数类型(void):
1. 无返回值,常用于纯逻辑执行(如打印、修改全局状态 )。
示例:
void sayHi(void)
{
printf("Hello!\n");
}
int main(void)
{
sayHi(); // 调用无参函数,括号不能省略
return 0;
}
1. 值传递:
1)本质:形参是实参的“副本”(独立内存空间),函数内修改形参不影响实参 。
示例(验证值传递特性 ):
void modify(int n)
{
n += 10; // 修改形参,实参不受影响
}
int main(void)
{
int num = 5;
modify(num);
printf("实参值:%d\n", num); // 输出5
return 0;
}
2. 指针传参(地址传递):
1)本质:传递变量的内存地址,函数内通过指针操作可直接修改实参的值(突破值传递的“只读限制” )。
示例(修改实参 ):
void modify(int *p)
{
*p += 10; // 通过指针修改实参内存
}
int main(void)
{
int num = 5;
modify(&num); // 传递地址
printf("实参值:%d\n", num); // 输出15
return 0;
}
3. 数组传参(退化为指针):
1)一维数组:形参写为 数组名[] 或 指针 形式(本质是传递数组首元素地址 );因数组退化为指针,无法直接用 sizeof 获取原数组长度,需手动传长度参数 。
示例(修改数组元素 ):
void updateArray(int arr[], int len)
{
for (int i = 0; i < len; i++)
{
arr[i] += 10; // 通过指针操作修改数组元素
}
}
int main(void)
{
int arr[] = {1,2,3,4,5};
int len = sizeof(arr) / sizeof(arr[0]);
updateArray(arr, len);
// 遍历验证
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]); // 输出11 12 13 14 15
}
return 0;
}
2)字符数组(字符串):依赖 '\0' 判断结束,无需显式传长度;
示例:
void printStr(char str[])
{
printf("字符串:%s\n", str); // 自动识别'\0'
}
int main(void)
{
char str[] = "Hello";
printStr(str); // 输出Hello
return 0;
}
3)二维数组:需指定列数(或第二维长度 ),形参写为 int arr[][N] ( N 为列数,不可省略 );函数内通过 sizeof(arr[0])/sizeof(arr[0][0]) 计算列数(基于首行数组 ),需手动传行数即元素个数 int rows = sizeof(a) / sizeof(a[0]); 。
示例(打印二维数组 ):
void print2DArray(int arr[][3], int rows)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main(void)
{
int arr[2][3] = {{1,2,3}, {4,5,6}};
int rows = sizeof(a) / sizeof(a[0]);
print2DArray(arr, 2);
return 0;
}
4)传参顺序与副作用:
多参数传递时,实际入栈顺序是“从右到左”(编译器实现细节 );避免对同一变量多次操作(如 incr(++i, i++) ),因顺序依赖编译器,结果不确定(代码不健壮 )。
1. 核心逻辑:函数直接/间接调用自身,需明确“递归条件”(避免无限递归 )。
2. 优缺点:
1)优点:简化复杂问题(如汉诺塔、分治场景 )。
2)缺点:效率低(频繁入栈出栈)、占用栈空间大(可能导致栈溢出,程序崩溃 );递归深度有限,需合理设计终止条件 。
示例(累加求和 ):
int sum(int n)
{
if (n == 1)
{ // 递归终止条件
return 1;
}
return sum(n - 1) + n; // 递归调用自身
}
int main(void)
{
printf("1-5累加和:%d\n", sum(5)); // 输出15
return 0;
}
3)经典案例(汉诺塔 ):
规则:将 n 个盘子从 A 柱移到 C 柱,借助 B 柱,每次只能移一个,大盘不能压小盘。
递归逻辑:
1. 把 n-1 个盘子从 A → B (借助 C )。
2. 把第 n 个盘子从 A → C 。
3. 把 n-1 个盘子从 B → C (借助 A )。
代码实现:
// 打印移动步骤
void move(char from, char to)
{
printf("%c -> %c\n", from, to);
}
void hanoi(int n, char A, char B, char C)
{
if (n == 1)
{
move(A, C); // 终止条件:直接移动
return;
}
hanoi(n - 1, A, C, B); // 1. 移n-1个到B
move(A, C); // 2. 移第n个到C
hanoi(n - 1, B, A, C); // 3. 移n-1个到C
}
int main(void)
{
hanoi(3, 'A', 'B', 'C'); // 3个盘子的汉诺塔
return 0;
}
移动步数规律: 2^n - 1 ( n 为盘子数量 )。
1. 分类核心:按作用域(哪里能访问)和存储周期(存在多久)划分,影响变量的“可见性”和“生存期” 。
2. 局部变量:
1)定义:在 {} 代码块、函数形参中声明的变量,作用域仅限当前代码块/函数(局部可见 )。
2)存储:默认在栈区( auto 类型,空间自动分配/销毁 );若用 static 修饰(静态局部变量 ),则存储在静态区,生存期与程序一致(值会保留,下次调用延续 )。
静态局部变量示例:
void count()
{
static int num = 0; // 静态局部变量,存储在静态区
num++;
printf("调用次数:%d\n", num);
}
int main(void)
{
count(); // 输出1
count(); // 输出2(值保留)
return 0;
}
3. 全局变量:
1)定义:在函数外声明的变量,作用域默认全局可见(整个程序均可访问 );存储在静态区,未初始化时自动赋 0 (数值类型)或空(指针/数组 )。
2)最佳实践:尽量减少全局变量(增加耦合性,易引发 Bug ),优先用函数传参替代 。
4. 可见性规则:
1)标识符需先定义,后使用(编译器按顺序解析 )。
2)同一作用域(如同一函数内)禁止同名变量(编译器无法区分 )。
3)不同作用域(无包含关系):同名变量互不影响(作用域隔离 )。
4)不同作用域(有包含关系,如函数内与全局 ):内层作用域的变量会“屏蔽”外层同名变量(就近原则,外层变量在此作用域不可见 )。
1. 栈区(stack):
1)存储:局部变量、函数参数、返回地址等;自动分配/销毁(函数调用时入栈,返回时出栈 )。
2)特点:空间小(几 MB )、速度快,遵循FILO(先进后出) 。
2. 堆区(heap):
1)存储:动态分配的内存(如 malloc / free 管理的空间 );需手动申请、释放,否则内存泄漏 。
2)特点:空间大(按需分配 )、灵活,但操作复杂(需管理指针 )。
3. 静态区(全局区):
1)存储:全局变量、静态变量( static 修饰 );程序运行期间一直存在,程序结束后由系统回收 。
4. 字符串常量区:
1)存储:字符串常量(如 "Hello" );只读(修改会触发未定义行为 ),程序结束后释放 。
5. 代码区:
1)存储:程序指令(函数逻辑、操作码等 );只读,由编译器加载到内存 。
1. extern 声明(跨文件共享):
多文件编程时,若 main.c 需调用 func.c 的函数/变量,需通过 extern 声明“告知编译器存在” 。规范流程(头文件配合 ):
1)func.c :定义函数/变量(实现逻辑 )。
// func.c
int add(int a, int b)
{
return a + b;
}
2)func.h :声明函数/变量(头文件只做声明,不写实现 ),并通过头文件保护(防止重复包含 )。
// func.h
#ifndef FUNC_H
#define FUNC_H
extern int add(int a, int b); // 函数声明
#endif
3)main.c :通过 #include "func.h" 引入声明,直接调用 。
// main.c
#include "func.h"
#include
int main(void)
{
printf("结果:%d\n", add(10, 20)); // 调用func.c的函数
return 0;
}
编译命令(GCC 示例 ):
gcc -o app main.c func.c
./app # 运行程序
2. static 修饰(限制作用域):
1)静态全局变量( static int g_var; ):作用域仅限当前文件(其他文件无法访问 ),避免跨文件变量冲突 。
2)静态函数( static int func(); ):作用域仅限当前文件,不对外暴露,增强代码封装性 。