C语言——函数进阶与编程核心知识:递归、数组传参、作用域、存储类别与宏定义)
一、函数回顾与递归1.1 函数的核心思想函数的核心思想是自上而下逐步拆解将大问题拆成小问题小问题拆成更小问题更小的问题往往对应一个简单、独立的功能函数模型输入 — 处理 — 输出1.2 递归的概念递归函数自己调用自己。直接递归c void func1() { func1(); // 自己调用自己}间接递归c void func2() { func1(); } void func1() { func2(); }1.3 递归的两个关键要素递推关系问题 n 和问题 n-1 之间的关系递归结束条件什么时候停止递归1.4 递归求和c // 递归求和1 2 ... n int sum(int n) { if (n 1) // 递归结束条件 { return 1; } else // 递推关系sum(n) sum(n-1) n { return sum(n - 1) n; } }调用过程text sum(5) |--- sum(4) 5 |--- sum(3) 4 |--- sum(2) 3 |--- sum(1) 2 |--- 11.5 递归求阶乘c // 递归求阶乘n! n * (n-1) * ... * 1 int fact(int n) { if (n 1) { return 1; } else { return fact(n - 1) * n; } }1.6 斐波那契数列c // 斐波那契数列1, 1, 2, 3, 5, 8, 13, ... int fibo(int n) { if (n 1 || n 2) // 结束条件 { return 1; } else // 递推关系fibo(n) fibo(n-1) fibo(n-2) { return fibo(n - 1) fibo(n - 2); } }1.7 汉诺塔汉诺塔是递归的经典案例。思路将 n-1 个盘子从 A 借助 C 移到 B将剩下的最大盘子从 A 移到 C将 n-1 个盘子从 B 借助 A 移到 Cc // 汉诺塔递归函数 // n 个盘子从 A 借助 B 移动到 C void hanoi(int n, char A, char B, char C) { if (n 1) // 结束条件只剩一个盘子 { printf(%c - %c\n, A, C); } else { hanoi(n - 1, A, C, B); // 将 n-1 个盘子从 A 移到 B printf(%c - %c\n, A, C); // 将最大的盘子从 A 移到 C hanoi(n - 1, B, A, C); // 将 n-1 个盘子从 B 移到 C } }调用过程以hanoi(3, A, B, C)为例text hanoi(3, A, B, C) |--- hanoi(2, A, C, B) | |--- hanoi(1, A, B, C) - A - C | |--- A - B | |--- hanoi(1, C, A, B) - C - B |--- A - C |--- hanoi(2, B, A, C) |--- hanoi(1, B, C, A) - B - A |--- B - C |--- hanoi(1, A, B, C) - A - C递归深度不宜过深否则可能栈空间不够用导致段错误。二、数组作为函数参数2.1 数组元素 vs 整个数组数组元素a[0]就是普通变量可以像普通变量一样传参。整个数组a是数组名代表首元素地址需要传数组名和长度。2.2 整型一维数组作为函数参数形参c void printArray(int a[], int len); // 形式上数组形式 void printArray(int *a, int len); // 本质上指针变量实参c int arr[5] {1, 2, 3, 4, 5}; printArray(arr, 5); // 传数组名和长度重点形参中的数组长度会被忽略必须额外传长度。数组传参是地址传递函数内修改数组会影响原数组。示例c // 找出整型数组中的最大值 int maxOfArray(int a[], int len) { int max a[0]; for (int i 1; i len; i) { if (a[i] max) { max a[i]; } } return max; }2.3 字符型一维数组作为函数参数形参c void printString(char s[]); // 不需要长度字符串有 \0 结束标志 void printString(char *s);实参c char str[] hello; printString(str);示例c // 求字符串长度 int Strlen(char s[]) { int i 0; while (s[i] ! \0) { i; } return i; } // 字符串拷贝 void Strcpy(char dest[], char src[]) { int i 0; while (src[i] ! \0) { dest[i] src[i]; i; } dest[i] \0; // 手动添加结束标志 } // 字符串拼接 void Strcat(char dest[], char src[]) { int i 0; while (dest[i] ! \0) // 找到 dest 的结束位置 { i; } int j 0; while (src[j] ! \0) // 从结束位置开始拷贝 { dest[i] src[j]; i; j; } dest[i] \0; } // 字符串比较 int Strcmp(char s1[], char s2[]) { int i 0; while (s1[i] s2[i] s1[i] ! \0 s2[i] ! \0) { i; } return s1[i] - s2[i]; // 返回停止位置上两个字符的差值 }2.4 二维整型数组作为函数参数形参c void printArray(int a[3][4], int row); // 形式上数组形式 void printArray(int (*a)[4], int row); // 本质上指针实参c int arr[3][4] { ... }; printArray(arr, 3);重点二维数组形参中列数不能省略。行数可以省略但通常额外传行数参数。示例c // 二维数组累计求和 int sumOfArray(int a[][4], int row) { int sum 0; for (int i 0; i row; i) { for (int j 0; j 4; j) { sum a[i][j]; } } return sum; }2.5 二维字符型数组作为函数参数二维字符型数组用来存放多个字符串。写法与二维整型数组一致c void printStrings(char s[][10], int row); // 列数不能省略 示例 c // 找出最大的字符串 void maxOfStr(char s[][10], int row, char result[]) { int maxIdx 0; for (int i 1; i row; i) { if (Strcmp(s[i], s[maxIdx]) 0) { maxIdx i; } } Strcpy(result, s[maxIdx]); }2.6 数组传参总结数组类型形参实参是否需要长度整型一维数组int a[]或int *a数组名需要字符型一维数组char s[]或char *s数组名不需要二维整型数组int a[][4]或int (*a)[4]数组名需要行数二维字符型数组char s[][10]数组名需要行数三、标识符的作用域与可见性3.1 作用域作用域标识符发挥作用的区域。C 语言中一个{}就是一个作用域。局部作用域在{}内定义的变量是局部变量。全局作用域不在任何{}内定义的变量是全局变量。3.2 可见性规则先定义后使用。同一作用域中不能有同名标识符。不同作用域中同名标识符互不影响。如果作用域存在嵌套关系内层同名标识符会屏蔽外层就近原则。示例c int a 10; // 全局变量 int main() { int a 20; // 局部变量屏蔽全局 a printf(%d\n, a); // 输出 20 return 0; }四、存储类别关键字4.1 autocauto int a 10; // 表示变量空间开在栈上auto是默认的存储类别一般可以省略。局部变量默认就是auto。4.2 staticcstatic int a 10; // 表示将变量空间开在静态区静态变量的特点只会被初始化一次。不能用变量初始化。静态局部变量的值具有继承性。示例c void func() { static int count 0; // 只初始化一次 count; printf(%d\n, count); } // 多次调用 funccount 会持续累加static修饰全局变量将作用域限定为本文件其他文件无法引用。static修饰函数作用相同限定函数只能在本文件使用。4.3 registercregister int a 10; // 建议将变量存放到寄存器中register是建议性关键字。被register修饰的变量不能取地址运算因为寄存器没有内存地址。4.4 externcextern int a; // 声明变量 a 在别的文件中定义extern表示变量存储在别的文件中使用时到别的文件中寻找。extern扩展了变量的作用域。五、程序内存布局五个区text[栈] // 局部变量、函数调用信息自动申请自动释放 [堆] // 手动申请手动释放 [全局区/静态区] // 全局变量、静态变量 [字符串常量区] // hello [代码区] // 代码程序 代码 数据。5.1 栈先进后出FILO局部变量都放在栈上自动申请自动释放Linux 默认 8M可修改5.2 堆特别大手动申请手动释放32 位系统寻址范围 4G操作系统约占 1G5.3 存储体系text寄存器 cache 内存 硬盘从上到下容量越来越大价格越来越便宜速度越来越慢。六、预处理命令与宏定义6.1 预处理命令概述C 提供的预处理功能主要有三种宏定义文件包含条件编译所有预处理命令都以#开头。6.2 编译过程text 预处理gcc -E hello.c -o hello.i // 处理预处理命令生成纯 C 代码 编译 gcc -S hello.i -o hello.s // 语法词法分析生成汇编代码 汇编 gcc -c hello.s -o hello.o // 翻译成机器代码 链接 gcc hello.o -o hello // 链接第三方代码生成可执行程序6.3 宏定义c#define 宏名 宏值作用用宏名代替一串文本信息。注意宏名本质是标识符建议大写。宏工作是文本原样替换。字符串中的宏名不会被替换。用途符号常量做到一改全改提高可读性。示例c#define ARRAY_SIZE 10 int array[ARRAY_SIZE];练习计算圆的周长和面积c #include stdio.h #define PI 3.14159 int main() { double r; printf(请输入半径); scanf(%lf, r); double L 2 * PI * r; double S PI * r * r; printf(周长 %.2f\n, L); printf(面积 %.2f\n, S); return 0; }6.4 带参宏c#define 宏名(参数表) 宏值示例c#define MUL(a, b) ((a) * (b))带参宏与函数的本质区别函数的参数有类型。带参宏的参数没有类型只是文本。函数在程序运行时使用。带参宏在预处理阶段就处理完了。带参宏副作用c#define MUL(a, b) a * b MUL(1 2, 3 4) // 展开为 1 2 * 3 4结果错误 #define MUL(a, b) ((a) * (b)) MUL(1 2, 3 4) // 展开为 ((12) * (34))结果正确建议能加括号都加括号。练习求两个数的最大值宏c#define MAX(a, b) ((a) (b) ? (a) : (b))6.5 取消宏c#undef 宏名取消宏的作用域。6.6 文件包含c#include 文件名 // 到系统标准路径下寻找 #include 文件名 // 先到当前位置寻找找不到再到系统路径寻找说明include执行后用文件中的内容替换该命令所在位置。系统头文件用自己写的用。6.7 条件编译1#ifdefc #ifdef 标识符 程序段1 #else 程序段2 #endif语义如果标识符定义了选择程序段1否则选择程序段2。2#ifndefc #ifndef 标识符 程序段1 #else 程序段2 #endif语义如果没有定义标识符选择程序段1否则选择程序段2。3#ifc #if 表达式 程序段1 #else 程序段2 #endif常见用途c#if 0 // 这段代码不会被编译 #endif6.8 宏与预处理易错点宏名建议大写与变量区分。宏是文本替换不是函数调用。带参宏参数没有类型只是文本。带参宏能加括号就加括号避免副作用。宏定义以换行作为结束符。字符串中的宏名不会被替换。#include是文本替换不是链接。条件编译用于多文件编程和调试版本控制。七、多文件编程7.1 基本结构text.c 文件 --- 实现功能的具体代码函数 .h 文件 --- 头文件放宏、类型说明、函数声明注意变量的定义不能放头文件。7.2 头文件写法c #ifndef 头文件名_H // 要大写前后加下划线 #define 头文件名_H // 宏定义 // 类型说明 // 函数声明 #endif7.3 编译所有.c文件一起编译bashgcc main.c isLeapYear.c getAllDays.c -o main7.4 易错点头文件一定要写防止重复包含的宏。变量的定义不能放头文件只能放声明。函数声明放头文件函数定义放 .c 文件。static修饰的函数和全局变量只能在本文件使用。八、易错点总结递归必须有结束条件否则会无限递归导致栈溢出。递归深度不宜过深否则栈空间不够用。数组传参是地址传递函数内修改会影响原数组。形参中的数组长度会被忽略必须额外传长度。二维数组形参列数不能省略。字符数组传参不需要长度但必须保证有\0。同一作用域不能有同名标识符。内层作用域会屏蔽外层同名标识符。static局部变量只初始化一次值有继承性。register变量不能取地址。带参宏的参数没有类型只是文本替换。带参宏能加括号就加括号避免副作用。宏定义以换行作为结束符。字符串中的宏名不会被替换。