C语言程序设计
将一个问题逐步细化分解为若干子问题,用函数实现子问题
优点:
自顶向下,逐步求精:
解决问题 → 分解为子问题1, 子问题2, ... → 每个子问题用函数实现
"One function, one task." — 每个函数只做一件事
int rand(void);
<stdlib.h>
do { magic = GetNum(); // 产生随机数 GuessNum(magic); // 猜数 printf("Play again? (Y/N) "); scanf(" %c", &cmd); } while (cmd == 'y' || cmd == 'Y');
GetNum()
GuessNum()
main
int GetNum(void) { int x; printf("A magic number between 1 and %d has been chosen.\n", MAX_NUMBER); x = rand(); x = x % MAX_NUMBER + 1; return x; }
rand()
RAND_MAX
x % MAX_NUMBER + 1
MAX_NUMBER
#include <stdio.h> #include <stdlib.h> #include <time.h> #define MAX_NUMBER 100 int GetNum(void); void GuessNum(int magic); int main(void) { char command; int magic; srand((unsigned)time(NULL)); // 初始化随机种子 do { magic = GetNum(); GuessNum(magic); printf("Play again? (Y/N) "); scanf(" %c", &command); } while (command == 'y' || command == 'Y'); return 0; }
线性同余法(最常用的伪随机数生成算法):
a0=seed,an=(A⋅an−1+B) mod M,n≥1a_0 = \text{seed}, \quad a_n = (A \cdot a_{n-1} + B) \bmod M, \quad n \geq 1 a0=seed,an=(A⋅an−1+B)modM,n≥1
seed
srand((unsigned)time(NULL));
time(NULL)
<time.h>
不调用 srand 时,默认种子为 1——每次运行产生相同的序列
srand
思路:在单位正方形内随机投点,落在内切圆内的比例 ≈ π/4\pi/4π/4
#include <stdio.h> #include <stdlib.h> int main(void) { int n, hits = 0; printf("投点次数:"); scanf("%d", &n); for (int i = 0; i < n; i++) { double x = (double)rand() / RAND_MAX; // [0, 1) double y = (double)rand() / RAND_MAX; if (x * x + y * y <= 1.0) hits++; } printf("π ≈ %f\n", 4.0 * hits / n); return 0; }
使用自定义函数的三个步骤:
语法:
返回类型 函数名(参数列表) { 声明部分; 语句部分; }
示例:
int max(int x, int y) { // 返回两个整数中的较大值 return x > y ? x : y; }
void
int GetNum(void)
void GuessNum(int magic)
return
return; // void 函数中,提前返回 return 表达式; // 有返回值的函数
int abs_val(int x) { if (x >= 0) return x; // 两个 return return -x; }
语法:返回类型 函数名(参数类型表);
返回类型 函数名(参数类型表);
int max(int x, int y); // 完整原型 int max(int, int); // 省略参数名(也可以)
为什么要声明?
int main(void) { int result = max(3, 5); // 此时编译器还不知道 max 的存在 return 0; } int max(int x, int y) { // 定义在 main 之后 return x > y ? x : y; } // 编译报错:implicit declaration of function 'max'
解决:在调用之前给出声明(或把定义放在调用之前)
调用语法:函数名(实参列表);
函数名(实参列表);
putchar(c); // 库函数调用 c = getchar(); printf("%f", sqrt(x)); // 嵌套调用 GuessNum(GetNum()); // 函数作实参
C 语言的参数传递——值传递(Pass by Value):
void swap_wrong(int a, int b) { int temp = a; a = b; b = temp; // 只交换了副本! }
要在函数内修改实参,必须传地址(指针,第8章详述):
void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; // 通过指针操作原变量 }
C 语言未规定实参的求值顺序——不同编译器可能不同
int i = 3; printf("%d %d\n", i++, i++); // 未定义行为! // 可能输出 "3 4" 或 "4 3",取决于编译器
规则:不要在同一个函数调用中,对同一变量使用有副作用的表达式
传地址调用(提前预告):
int x; scanf("%d", &x); // 传递变量地址,scanf 通过地址修改 x
存储类型决定变量的:
四个存储类别关键字:auto、extern、static、register
auto
extern
static
register
auto int a; // 等价于 int a;(auto 可省略)
{}
extern int seed; // 引用性声明:告诉编译器 seed 在别处定义 int seed = 0; // 定义性声明:分配内存并初始化
注意:全局变量虽然方便,但应尽量少用——降低模块独立性
static int count = 0; // 静态变量
两种用法:
示例——统计函数调用次数:
void counter(void) { static int count = 0; // 只初始化一次 count++; printf("called %d times\n", count); }
register int i; // 建议编译器将变量存入寄存器
&i
递归:函数直接或间接调用自身
递归三要素:
void prn_int(int n) { if (n > 0) { printf("%d ", n); prn_int(n - 1); // 递归调用,规模缩小 } printf("%d ", n); // 回溯时输出 } // prn_int(4) 输出: 4 3 2 1 0 1 2 3 4
n!={1n=0 或 1n×(n−1)!n≥2n! = \begin{cases} 1 & n = 0 \text{ 或 } 1 \\ n \times (n-1)! & n \geq 2 \end{cases} n!={1n×(n−1)!n=0 或 1n≥2
long fact(int n) { if (n == 0 || n == 1) return 1; // 终止条件 else return n * fact(n - 1); // 递归步骤 }
执行过程:fact(4)
fact(4)
fact(4) → 4 * fact(3) → 4 * 3 * fact(2) → 4 * 3 * 2 * fact(1) → 4 * 3 * 2 * 1 → 4 * 3 * 2 → 4 * 6 → 24
long factorial_iter(int n) { long result = 1; for (int i = 2; i <= n; i++) { result *= i; } return result; }
F(n)={1n=1 或 2F(n−1)+F(n−2)n≥3F(n) = \begin{cases} 1 & n = 1 \text{ 或 } 2 \\ F(n-1) + F(n-2) & n \geq 3 \end{cases} F(n)={1F(n−1)+F(n−2)n=1 或 2n≥3
朴素递归(大量重复计算,效率极低):
long long fibo(int n) { if (n == 1 || n == 2) return 1; return fibo(n - 1) + fibo(n - 2); // fibo(5) 会重复计算 fibo(3) 两次 }
时间复杂度:O(2n)O(2^n)O(2n)——不可接受
用数组保存已计算的结果,避免重复计算:
long long fibo_memo(int n) { static long long memo[100] = {0, 1, 1}; // static 只初始化一次 if (memo[n] != 0) return memo[n]; // 已计算过,直接返回 memo[n] = fibo_memo(n - 1) + fibo_memo(n - 2); return memo[n]; }
时间复杂度:O(n)O(n)O(n)
long long fibo_iter(int n) { if (n <= 2) return 1; long long a = 1, b = 1, c; for (int i = 3; i <= n; i++) { c = a + b; a = b; b = c; } return b; }
时间复杂度 O(n)O(n)O(n),空间复杂度 O(1)O(1)O(1)——最优方案
尾递归:递归调用是函数的最后一步操作
long long fibo_tail(int n, long long first, long long second) { if (n <= 1) return first; return fibo_tail(n - 1, second, first + second); } // 调用方式:fibo_tail(10, 1, 1)
-O2
将 nnn 个盘子从 A 借助 B 移到 C:
递归策略:
void hanoi(int n, char from, char via, char to) { if (n == 1) { printf("%c --> %c\n", from, to); return; } hanoi(n - 1, from, to, via); // 步骤1 printf("%c --> %c\n", from, to); // 步骤2 hanoi(n - 1, via, from, to); // 步骤3 }
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
移动次数:2n−12^n - 12n−1,即 n=3n=3n=3 时需 7 步
int str_len(const char s[]) { if (s[0] == '\0') return 0; return 1 + str_len(s + 1); }
递归练习:
逆序输出整数:reverse(123) → 输出 321
reverse(123)
321
void reverse_print(int n) { if (n < 10) { printf("%d", n); return; } printf("%d", n % 10); reverse_print(n / 10); }
判断回文字符串:is_palindrome("Level") → 返回 1
is_palindrome("Level")
1
bool is_palindrome(const char s[], int left, int right) { if (left >= right) return true; if (s[left] != s[right]) return false; return is_palindrome(s, left + 1, right - 1); }
将正整数 MMM 划分为一系列正整数之和(按字典序不增排列)
示例:M=6M=6M=6
6 5+1 4+2, 4+1+1 3+3, 3+2+1, 3+1+1+1 2+2+2, 2+2+1+1, 2+1+1+1+1 1+1+1+1+1+1
void split(int n, int min, int a[], int cur) { if (n == 0) { // 输出 a[0] ~ a[cur-1] return; } for (int i = (n < min ? n : min); i >= 1; i--) { a[cur] = i; split(n - i, i, a, cur + 1); } }
在 8×88 \times 88×8 棋盘上放置 8 个皇后,使其互不攻击
#include <stdbool.h> int queen[8]; // queen[i] = 第i行皇后所在的列 bool safe(int row, int col) { for (int i = 0; i < row; i++) { if (queen[i] == col || queen[i] - i == col - row || queen[i] + i == col + row) return false; } return true; } void solve(int row) { if (row == 8) { /* 输出一个解 */ return; } for (int col = 0; col < 8; col++) { if (safe(row, col)) { queen[row] = col; solve(row + 1); } } }
大型程序通常拆分为多个源文件:
project/ ├── main.c // 主程序 ├── utils.c // 工具函数实现 ├── utils.h // 工具函数声明(头文件) └── Makefile // 构建脚本
编译:
gcc -Wall -std=c11 -o prog main.c utils.c
头文件规范:
// utils.h #ifndef UTILS_H // 防止重复包含 #define UTILS_H int max(int x, int y); void swap(int *a, int *b); #endif
.h
.c
#ifndef
下一章:第6章 编译预处理