C 语言程序设计
一、C 语言概述
1.1 C 语言简介
C 语言由 Dennis Ritchie 于 1972 年在贝尔实验室开发,用于重写 UNIX 操作系统。它是面向过程的编程语言,兼具高级语言的易用性和低级语言的底层操作能力。
1.2 C 语言特点
|
特点 |
说明 |
|---|---|
|
高效 |
编译为机器码直接执行,运行效率极高 |
|
可移植 |
标准 C 代码可在不同平台编译运行 |
|
灵活 |
支持指针,可直接操作内存 |
|
结构化 |
函数式编程,模块化组织代码 |
|
丰富运算符 |
提供位运算、自增自减等底层操作 |
1.3 第一个 C 程序
#include <stdio.h> // 预处理器指令:引入标准输入输出头文件
int main() { // 主函数:程序入口
printf("Hello, World!\n"); // 输出到控制台
return 0; // 返回 0 表示程序正常结束
}
1.4 C 程序编译执行流程
源文件(.c) → 预处理 → 编译 → 汇编 → 链接 → 可执行文件(.exe)
gcc hello.c -o hello # 编译
./hello # 运行
二、数据类型、运算符与表达式
2.1 基本数据类型
|
类型 |
关键字 |
字节数 |
取值范围 |
格式符 |
|---|---|---|---|---|
|
整型 |
|
4 |
-2,147,483,648 ~ 2,147,483,647 |
|
|
短整型 |
|
2 |
-32,768 ~ 32,767 |
|
|
长整型 |
|
4/8 |
平台相关 |
|
|
长长整型 |
|
8 |
-2⁶³ ~ 2⁶³-1 |
|
|
无符号整型 |
|
4 |
0 ~ 4,294,967,295 |
|
|
单精度浮点 |
|
4 |
±3.4×10⁻³⁸ ~ ±3.4×10³⁸ |
|
|
双精度浮点 |
|
8 |
±1.7×10⁻³⁰⁸ ~ ±1.7×10³⁰⁸ |
|
|
字符型 |
|
1 |
-128 ~ 127 或 0~255 |
|
|
布尔型 |
|
1 |
0(false) / 1(true) |
|
#include <stdio.h>
int main() {
int age = 25;
float score = 92.5;
double pi = 3.14159265358979;
char grade = 'A';
printf("年龄: %d\n", age);
printf("分数: %.1f\n", score);
printf("圆周率: %.10f\n", pi);
printf("等级: %c\n", grade);
// sizeof 查看占用字节数
printf("int: %zu 字节\n", sizeof(int));
printf("double: %zu 字节\n", sizeof(double));
printf("char: %zu 字节\n", sizeof(char));
return 0;
}
2.2 常量
// 方式一:const 常量
const int MAX = 100;
const float PI = 3.14159;
// 方式二:#define 宏常量(预处理阶段替换,无类型检查)
#define MAX 100
#define PI 3.14159
#define NEWLINE '\n'
const vs #define:
|
|
const |
#define |
|---|---|---|
|
本质 |
变量,有类型 |
文本替换,无类型 |
|
内存 |
占内存 |
不占内存 |
|
调试 |
可调试 |
不可调试(已被替换) |
|
推荐 |
✅ 有类型安全 |
仅用于条件编译和宏 |
2.3 类型转换
// 自动转换(隐式):小类型 → 大类型
int a = 10;
double b = a; // int → double,安全
// 强制转换(显式):(目标类型) 表达式
double x = 3.14;
int y = (int)x; // double → int,截断小数部分,y = 3
// 注意:整数除法
int m = 5, n = 2;
double result1 = m / n; // 结果 2.0(先整数除法得 2,再转 double)
double result2 = (double)m / n; // 结果 2.5(先转 double,再除法)
2.4 运算符
算术运算符
|
运算符 |
含义 |
示例 |
|---|---|---|
|
|
加 |
|
|
|
减 |
|
|
|
乘 |
|
|
|
除 |
|
|
|
取余 |
|
|
|
自增 |
|
|
|
自减 |
|
关系运算符
|
运算符 |
含义 |
|---|---|
|
|
等于 |
|
|
不等于 |
|
|
大于 |
|
|
小于 |
|
|
大于等于 |
|
|
小于等于 |
逻辑运算符
|
运算符 |
含义 |
短路特性 |
|---|---|---|
|
|
逻辑与 |
左边为假则不执行右边 |
|
|
逻辑或 |
左边为真则不执行右边 |
|
|
逻辑非 |
单目运算 |
位运算符(C 语言特色)
|
运算符 |
含义 |
示例 |
|---|---|---|
|
|
按位与 |
|
|
|
按位或 |
|
|
|
按位异或 |
|
|
|
按位取反 |
|
|
|
左移 |
|
|
|
右移 |
|
int a = 5, b = 3; // 5=0101, 3=0011
printf("%d\n", a & b); // 0001 = 1
printf("%d\n", a | b); // 0111 = 7
printf("%d\n", a ^ b); // 0110 = 6
printf("%d\n", a << 1); // 5*2 = 10
printf("%d\n", a >> 1); // 5/2 = 2
运算符优先级(从高到低)
() [] -> . 最高
! ~ ++ -- + - * & (类型) sizeof
* / %
+ -
<< >>
< <= > >=
== !=
&
^
|
&&
||
?: 条件运算符
= += -= *= /= %= &= ... 赋值运算符(最低)
记不住就用括号!括号是最清晰的优先级声明。
三、顺序结构:输入与输出
3.1 printf — 格式化输出
int age = 20;
double score = 88.5;
char name[] = "张三";
// 基本输出
printf("年龄: %d\n", age);
printf("分数: %.1f\n", score);
printf("姓名: %s\n", name);
// 多变量输出
printf("%s 今年 %d 岁,成绩 %.1f 分\n", name, age, score);
// 格式控制详解
printf("%d\n", 42); // 整型
printf("%5d\n", 42); // 右对齐,占 5 位: 42
printf("%-5d\n", 42); // 左对齐,占 5 位:42
printf("%05d\n", 42); // 用 0 填充:00042
printf("%f\n", 3.14); // 浮点:3.140000
printf("%.2f\n", 3.14); // 保留 2 位小数:3.14
printf("%8.2f\n", 3.14); // 占 8 位,2 位小数
printf("%c\n", 'A'); // 字符
printf("%s\n", "hello"); // 字符串
printf("%p\n", &age); // 指针地址
printf("%x\n", 255); // 十六进制:ff
printf("%o\n", 10); // 八进制:12
3.2 scanf — 格式化输入
int age;
float score;
char name[50];
// 基本输入
printf("请输入年龄: ");
scanf("%d", &age); // ⚠️ 变量前必须加 &(取地址)
printf("请输入分数: ");
scanf("%f", &score);
printf("请输入姓名: ");
scanf("%s", name); // 数组名本身就是地址,不加 &
// ⚠️ scanf("%s") 遇到空格就停止,不能读带空格的字符串
printf("你输入了: %d 岁, %.1f 分, 姓名 %s\n", age, score, name);
scanf 注意事项:
// 问题:连续输入时缓冲区残留
int a;
char c;
scanf("%d", &a); // 输入 10 回车,缓冲区留了 \n
scanf("%c", &c); // c 读到了 \n,不是你想要的字符
// 解决:用 getchar() 吃掉换行符,或在 %c 前加空格
scanf("%d", &a);
getchar(); // 吃掉 \n
scanf("%c", &c);
// 或:scanf(" %c", &c); // %c 前面的空格会跳过空白字符
3.3 字符输入输出
char ch;
// 读一个字符
ch = getchar(); // 从标准输入读一个字符
putchar(ch); // 输出一个字符
// 读一行(含空格)
char str[100];
fgets(str, sizeof(str), stdin); // 安全地读一行,包含空格
puts(str); // 输出字符串并换行
四、选择结构
4.1 if-else
int score = 85;
// 单分支
if (score >= 60) {
printf("及格\n");
}
// 双分支
if (score >= 60) {
printf("及格\n");
} else {
printf("不及格\n");
}
// 多分支
if (score >= 90) {
printf("优秀\n");
} else if (score >= 80) {
printf("良好\n");
} else if (score >= 60) {
printf("及格\n");
} else {
printf("不及格\n");
}
// 条件表达式(三元运算符)
char* result = (score >= 60) ? "及格" : "不及格";
int max = (a > b) ? a : b;
4.2 switch-case
int day = 3;
switch (day) {
case 1:
printf("星期一\n");
break; // ⚠️ 忘记 break 会导致"穿透"
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 6:
case 7: // 多个 case 共享一个执行体
printf("周末\n");
break;
default:
printf("无效日期\n");
}
五、循环结构
5.1 while 循环
// 先判断,后执行(可能一次都不执行)
int i = 1;
while (i <= 10) {
printf("%d ", i);
i++;
}
// 死循环
while (1) {
// 需要 break 退出
}
5.2 do-while 循环
// 先执行一次,后判断(至少执行一次)
int i = 1;
do {
printf("%d ", i);
i++;
} while (i <= 10);
5.3 for 循环
// for(初始化; 条件; 更新)
for (int i = 1; i <= 10; i++) {
printf("%d ", i);
}
// 嵌套循环:打印乘法口诀表
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
printf("%d×%d=%-2d ", j, i, i * j);
}
printf("\n");
}
5.4 break 与 continue
// break:跳出整个循环
for (int i = 1; i <= 10; i++) {
if (i == 5) break; // 到 5 就退出循环
printf("%d ", i); // 输出:1 2 3 4
}
// continue:跳过本次循环,进入下一次
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue; // 跳过偶数
printf("%d ", i); // 输出:1 3 5 7 9
}
六、数组
6.1 一维数组
// 定义并初始化
int arr[5] = {10, 20, 30, 40, 50};
int arr2[5] = {1, 2}; // 等价 {1, 2, 0, 0, 0}
int arr3[] = {1, 2, 3, 4, 5}; // 自动推断长度
// 访问元素(下标从 0 开始)
printf("%d\n", arr[0]); // 10(第一个)
printf("%d\n", arr[4]); // 50(最后一个)
// 遍历数组
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// 数组名是首元素地址
printf("arr = %p\n", arr); // 首地址
printf("&arr[0] = %p\n", &arr[0]); // 与上面相同
数组的经典算法:
// 1. 求最大值
int arr[] = {23, 45, 12, 67, 34, 89, 21};
int n = sizeof(arr) / sizeof(arr[0]);
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) max = arr[i];
}
printf("最大值: %d\n", max);
// 2. 冒泡排序
int a[] = {64, 34, 25, 12, 22, 11, 90};
n = sizeof(a) / sizeof(a[0]);
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
// 3. 二分查找(数组必须有序)
int binarySearch(int arr[], int n, int target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1; // 未找到
}
6.2 二维数组
// 定义并初始化
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 访问
printf("%d\n", matrix[1][2]); // 第 2 行第 3 列 = 7
// 遍历
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%-3d ", matrix[i][j]);
}
printf("\n");
}
// 二维数组在内存中是按行存储的(行优先)
// matrix[i][j] 的地址 = 首地址 + (i * 列数 + j) * sizeof(元素类型)
七、字符串
7.1 字符串的本质
C 语言没有专门的字符串类型,字符串是以 \0(空字符)结尾的字符数组。
char str1[] = "hello"; // 等价 {'h','e','l','l','o','\0'},占 6 字节
char str2[6] = "hello"; // 显式指定长度
char str3[10] = "hello"; // 后面自动补 \0
// '\0' 是字符串结束标志
// 忘记给 \0 留空间 → 数组越界 → 未定义行为
7.2 字符串输入输出
char name[50];
// 方式一:scanf(遇空格停止)
scanf("%s", name);
// 方式二:gets(危险,不检查越界,C11 已移除)
// gets(name); // ❌ 不要用
// 方式三:fgets(推荐,安全)
fgets(name, sizeof(name), stdin); // 会保留末尾的 \n
// 方式四:puts 输出
puts(name); // 自动追加换行
printf("%s\n", name);
7.3 常用字符串函数(string.h)
#include <string.h>
char s1[100] = "hello";
char s2[] = "world";
char dest[100];
// 长度
size_t len = strlen(s1); // 5(不包含 \0)
printf("长度: %zu\n", len);
// 复制
strcpy(dest, s1); // dest = "hello"
strncpy(dest, s2, 2); // 复制前 2 个字符
// 拼接
strcat(s1, " "); // s1 = "hello "
strcat(s1, s2); // s1 = "hello world"
strncat(s1, s2, 3); // 拼接前 3 个字符
// 比较
int cmp = strcmp(s1, s2); // 0=相等, 负=s1<s2, 正=s1>s2
strncmp(s1, s2, 3); // 比较前 3 个字符
// 查找字符
char* p = strchr(s1, 'l'); // 返回第一次出现 'l' 的位置
p = strrchr(s1, 'l'); // 最后一次出现 'l' 的位置
// 查找子串
p = strstr("hello world", "wo"); // 返回 "world" 的起始地址
// 转大小写(非标准,但常见)
#include <ctype.h>
char ch = toupper('a'); // 'A'
ch = tolower('Z'); // 'z'
7.4 字符串转换
#include <stdlib.h>
// 字符串 → 数字
int num = atoi("12345"); // 12345(atoi 无错误检测)
long lnum = atol("123456789");
double dnum = atof("3.14");
// 推荐使用 strtol / strtod(有错误检测)
char* end;
long n = strtol("12345abc", &end, 10); // n=12345, end 指向 "abc"
if (*end != '\0') {
printf("转换失败: 非数字字符\n");
}
7.5 手动实现字符串函数
// 手写 strlen
size_t my_strlen(const char* s) {
size_t len = 0;
while (*s++) len++;
return len;
}
// 手写 strcpy
char* my_strcpy(char* dest, const char* src) {
char* p = dest;
while ((*dest++ = *src++));
return p;
}
// 手写 strcmp
int my_strcmp(const char* s1, const char* s2) {
while (*s1 && *s1 == *s2) {
s1++; s2++;
}
return *(unsigned char*)s1 - *(unsigned char*)s2;
}
// 手写 strcat
char* my_strcat(char* dest, const char* src) {
char* p = dest;
while (*p) p++; // 移到 dest 末尾
while ((*p++ = *src++)); // 复制 src
return dest;
}
八、函数
8.1 函数定义与调用
// 函数声明(告诉编译器函数存在)
int add(int a, int b);
// 函数定义
int add(int a, int b) {
return a + b;
}
// 函数调用
int result = add(3, 5);
8.2 参数传递
// ⚠️ C 语言只有值传递(传值),没有引用传递
// 实参的值被复制给形参,修改形参不影响实参
void swap_wrong(int a, int b) { // ❌ 错误:不能交换
int temp = a;
a = b;
b = temp;
}
void swap_correct(int* a, int* b) { // ✅ 正确:通过指针
int temp = *a;
*a = *b;
*b = temp;
}
int x = 10, y = 20;
swap_correct(&x, &y); // 传地址,x=20, y=10
8.3 数组作为函数参数
// 数组作为参数时,退化为指针(丢失长度信息)
// 以下三种写法等价:
void printArray(int arr[], int n);
void printArray(int arr[10], int n); // 10 被忽略
void printArray(int* arr, int n);
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 所以传数组时必须同时传长度
int a[] = {1, 2, 3, 4, 5};
int n = sizeof(a) / sizeof(a[0]);
printArray(a, n);
8.4 递归
// 阶乘
int factorial(int n) {
if (n <= 1) return 1; // 基线条件
return n * factorial(n - 1); // 递归条件
}
// 斐波那契数列
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 汉诺塔
void hanoi(int n, char from, char to, char aux) {
if (n == 1) {
printf("将第 1 个盘子从 %c 移到 %c\n", from, to);
return;
}
hanoi(n - 1, from, aux, to);
printf("将第 %d 个盘子从 %c 移到 %c\n", n, from, to);
hanoi(n - 1, aux, to, from);
}
8.5 变量的作用域与生命周期
|
类型 |
作用域 |
生命周期 |
存储位置 |
默认值 |
|---|---|---|---|---|
|
局部变量 |
函数/块内部 |
函数调用期间 |
栈 |
随机值 |
|
全局变量 |
整个文件 |
程序运行期间 |
静态区 |
0 |
|
static 局部 |
函数/块内部 |
程序运行期间(只初始化一次) |
静态区 |
0 |
|
static 全局 |
当前文件 |
程序运行期间 |
静态区 |
0 |
int global = 100; // 全局变量
void func() {
static int count = 0; // 静态局部变量:只初始化一次,值在调用之间保持
count++;
printf("调用第 %d 次\n", count);
int local = 10; // 局部变量:每次调用重新分配
}
九、指针(C 语言的灵魂)
9.1 什么是指针
指针就是存储内存地址的变量。
int a = 100;
int* p = &a; // p 是指针变量,存储 a 的地址
// & 是取地址运算符
// * 在定义时表示"这是一个指针"
printf("a 的值: %d\n", a); // 100
printf("a 的地址: %p\n", &a); // 0x7fff5fbff83c(示例)
printf("p 的值: %p\n", p); // 与 &a 相同
printf("p 指向的值: %d\n", *p); // 100(解引用:读取 p 指向的值)
9.2 指针的核心操作
int a = 10, b = 20;
int* p = &a;
*p = 30; // 通过指针修改 a 的值,a 现在是 30
p = &b; // 让 p 指向 b
*p = 50; // b 现在是 50
// 指针输出
printf("%d\n", *p); // 解引用:输出 p 指向的值
// 多级指针
int** pp = &p; // 指向指针的指针
printf("%d\n", **pp); // 50(两级解引用)
9.3 指针与数组
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // 数组名是首元素地址,等价于 int* p = &arr[0];
// 以下四者等价
printf("%d\n", arr[2]); // 30
printf("%d\n", *(arr + 2)); // 30
printf("%d\n", p[2]); // 30
printf("%d\n", *(p + 2)); // 30
// 遍历数组
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i)); // 指针偏移
}
// 指针相减:两个指针之间的元素个数
int* p1 = &arr[1]; // 指向 20
int* p2 = &arr[4]; // 指向 50
printf("相差: %ld\n", p2 - p1); // 3
9.4 指针与字符串
// 字符串常量(指向只读内存)
char* str = "hello"; // str 指向字符串常量区,不可修改
// str[0] = 'H'; // ❌ 未定义行为!可能崩溃
// 字符数组(可修改)
char str2[] = "hello"; // 在栈上分配,可修改
str2[0] = 'H'; // ✅ OK
// 遍历字符串
char* s = "hello";
while (*s) {
printf("%c", *s);
s++; // 指针后移
}
9.5 指针数组与数组指针
// 指针数组:数组里存的是指针
int a = 1, b = 2, c = 3;
int* ptrArr[3] = {&a, &b, &c}; // ptrArr 是数组,元素类型是 int*
printf("%d\n", *ptrArr[0]); // 1
// 指针数组常见应用:字符串数组
char* colors[] = {"red", "green", "blue", "yellow"};
for (int i = 0; i < 4; i++) {
printf("%s\n", colors[i]);
}
// 数组指针:指向数组的指针
int matrix[3][4];
int (*p)[4] = matrix; // p 指向一个包含 4 个 int 的数组
// p + 1 跳过一整行(4 × sizeof(int) 字节)
9.6 函数指针
// 定义函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
// 函数指针:指向函数的指针
int (*funcPtr)(int, int); // funcPtr 是指向"返回int、参数两个int"的函数指针
funcPtr = add;
printf("%d\n", funcPtr(3, 5)); // 8
funcPtr = sub;
printf("%d\n", funcPtr(10, 3)); // 7
// 应用:回调函数
void operate(int x, int y, int (*op)(int, int)) {
printf("结果: %d\n", op(x, y));
}
operate(10, 5, add); // 结果: 15
operate(10, 5, sub); // 结果: 5
9.7 动态内存分配
#include <stdlib.h>
// malloc — 分配内存(不初始化,内容是随机值)
int* p = (int*)malloc(10 * sizeof(int)); // 分配 10 个 int 的空间
if (p == NULL) {
printf("内存分配失败\n");
return 1;
}
for (int i = 0; i < 10; i++) {
p[i] = i * 10;
}
// calloc — 分配并初始化为 0
int* q = (int*)calloc(10, sizeof(int));
// realloc — 调整已分配内存的大小
p = (int*)realloc(p, 20 * sizeof(int)); // 扩大到 20 个 int
// free — 释放内存(⚠️ 必须释放,否则内存泄漏)
free(p);
free(q);
p = NULL; // 防止野指针
q = NULL;
9.8 野指针与空指针
// 野指针:指向未知/已释放内存的指针
int* p; // 未初始化,野指针
// *p = 10; // ❌ 危险!不知道会写到哪
// 正确的初始化方式
int* p1 = NULL; // 空指针
int a = 10;
int* p2 = &a; // 指向已知变量
// 使用前检查
if (p1 != NULL) {
*p1 = 100;
}
// free 之后置 NULL
int* p3 = (int*)malloc(sizeof(int));
free(p3);
p3 = NULL; // 防止被再次误用
十、结构体
10.1 结构体定义与使用
// 定义结构体类型
struct Student {
int id;
char name[50];
int age;
double score;
}; // ⚠️ 分号不能少
// 创建结构体变量
struct Student s1 = {1, "张三", 20, 88.5};
struct Student s2;
s2.id = 2;
strcpy(s2.name, "李四");
s2.age = 21;
s2.score = 92.0;
// 访问成员
printf("姓名: %s, 分数: %.1f\n", s1.name, s1.score);
// typedef 简化类型名
typedef struct Student Student;
Student s3 = {3, "王五", 19, 85.0};
10.2 结构体指针
Student s = {1, "张三", 20, 88.5};
Student* p = &s;
// 通过指针访问成员:-> 运算符
printf("姓名: %s\n", p->name); // 等价于 (*p).name
printf("年龄: %d\n", p->age);
p->score = 95.0; // 通过指针修改
// 结构体数组与指针
Student class[3] = {
{1, "张三", 20, 88},
{2, "李四", 21, 92},
{3, "王五", 19, 85}
};
Student* ptr = class; // 指向数组首元素
for (int i = 0; i < 3; i++) {
printf("%s: %.1f\n", (ptr + i)->name, ptr[i].score);
}
10.3 链表(结构体 + 动态内存)
// 单链表节点
typedef struct Node {
int data;
struct Node* next; // 指向下一个节点
} Node;
// 创建节点
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 头插法
Node* insertHead(Node* head, int data) {
Node* newNode = createNode(data);
newNode->next = head;
return newNode;
}
// 尾插法
Node* insertTail(Node* head, int data) {
Node* newNode = createNode(data);
if (head == NULL) return newNode;
Node* p = head;
while (p->next) p = p->next;
p->next = newNode;
return head;
}
// 遍历链表
void printList(Node* head) {
Node* p = head;
while (p) {
printf("%d -> ", p->data);
p = p->next;
}
printf("NULL\n");
}
// 释放链表
void freeList(Node* head) {
while (head) {
Node* temp = head;
head = head->next;
free(temp);
}
}
十一、共用体与枚举
11.1 共用体(Union)
共用体的所有成员共享同一块内存空间,大小等于最大成员的大小。
union Data {
int i;
float f;
char str[20];
};
union Data d;
d.i = 10;
printf("%d\n", d.i); // 10
d.f = 3.14; // 覆盖了之前的 i
printf("%f\n", d.f); // 3.14
printf("%d\n", d.i); // 无意义的值(已被 f 覆盖)
printf("union 大小: %zu\n", sizeof(union Data)); // 20(最大成员 str[20])
11.2 枚举(Enum)
// 默认值从 0 开始
enum Color { RED, GREEN, BLUE };
// RED=0, GREEN=1, BLUE=2
// 自定义值
enum Weekday { MON = 1, TUE, WED, THU, FRI, SAT, SUN };
// MON=1, TUE=2, ..., SUN=7
// 使用枚举
enum Color c = RED;
switch (c) {
case RED: printf("红色\n"); break;
case GREEN: printf("绿色\n"); break;
case BLUE: printf("蓝色\n"); break;
}
十二、文件操作
12.1 文件指针与打开模式
#include <stdio.h>
// FILE* 是文件指针类型
FILE* fp;
// 打开模式
// "r" — 只读(文件必须存在)
// "w" — 只写(文件不存在则创建,存在则清空)
// "a" — 追加(文件不存在则创建,存在则在末尾追加)
// "r+" — 读写(文件必须存在)
// "w+" — 读写(创建或清空)
// "a+" — 读+追加
// "rb"/"wb" — 二进制模式
fp = fopen("test.txt", "w");
if (fp == NULL) {
printf("文件打开失败\n");
return 1;
}
12.2 读写单个字符
// 写一个字符
fputc('A', fp);
fputc('\n', fp);
// 读一个字符
char ch = fgetc(fp);
while (ch != EOF) {
putchar(ch);
ch = fgetc(fp);
}
// 判断文件结束
if (feof(fp)) {
printf("文件已读完\n");
}
12.3 读写字符串
// 写字符串
fputs("Hello, World!\n", fp);
fputs("第二行内容\n", fp);
// 读字符串(读一行,最多 n-1 个字符)
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line); // fgets 保留 \n,不需要额外加 \n
}
12.4 格式化读写
// 写格式化数据
fprintf(fp, "姓名: %s, 年龄: %d, 分数: %.1f\n", "张三", 20, 88.5);
// 读格式化数据
char name[50];
int age;
float score;
fscanf(fp, "姓名: %s, 年龄: %d, 分数: %f\n", name, &age, &score);
// 注意:fscanf 对格式要求严格,实际项目更推荐 fgets + sscanf
char buffer[256];
if (fgets(buffer, sizeof(buffer), fp)) {
sscanf(buffer, "姓名: %s, 年龄: %d", name, &age);
}
12.5 二进制读写
// 二进制写入
int arr[] = {10, 20, 30, 40, 50};
fwrite(arr, sizeof(int), 5, fp);
// 二进制读取
int buffer[5];
fread(buffer, sizeof(int), 5, fp);
// 结构体的二进制读写
typedef struct {
int id;
char name[20];
int age;
} Student;
Student s1 = {1, "张三", 20};
fwrite(&s1, sizeof(Student), 1, fp);
Student s2;
fread(&s2, sizeof(Student), 1, fp);
printf("ID: %d, 姓名: %s, 年龄: %d\n", s2.id, s2.name, s2.age);
12.6 文件定位
// 获取当前读写位置
long pos = ftell(fp);
printf("当前位置: %ld\n", pos);
// 移动到文件开头
rewind(fp); // 等价于 fseek(fp, 0, SEEK_SET);
// 移动到指定位置
fseek(fp, 0, SEEK_SET); // 文件开头
fseek(fp, 0, SEEK_END); // 文件末尾
fseek(fp, 10, SEEK_CUR); // 从当前位置向后移 10 字节
// 获取文件大小
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
printf("文件大小: %ld 字节\n", size);
12.7 关闭文件与错误处理
// 关闭文件(⚠️ 必须关闭)
fclose(fp);
// 检查错误
if (ferror(fp)) {
perror("文件操作错误"); // 打印 errno 对应的错误信息
clearerr(fp); // 清除错误标志
}
// 缓冲区刷新(确保数据写入磁盘)
fflush(fp);
12.8 实战:学生管理系统(文件版)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char name[50];
int age;
double score;
} Student;
void addStudent() {
FILE* fp = fopen("students.dat", "ab"); // 二进制追加
if (!fp) { printf("文件打开失败\n"); return; }
Student s;
printf("输入学号 姓名 年龄 分数: ");
scanf("%d %s %d %lf", &s.id, s.name, &s.age, &s.score);
fwrite(&s, sizeof(Student), 1, fp);
fclose(fp);
printf("添加成功\n");
}
void listStudents() {
FILE* fp = fopen("students.dat", "rb");
if (!fp) { printf("暂无学生数据\n"); return; }
Student s;
printf("学号\t姓名\t年龄\t分数\n");
while (fread(&s, sizeof(Student), 1, fp) == 1) {
printf("%d\t%s\t%d\t%.1f\n", s.id, s.name, s.age, s.score);
}
fclose(fp);
}
void searchStudent(int id) {
FILE* fp = fopen("students.dat", "rb");
if (!fp) { printf("暂无学生数据\n"); return; }
Student s;
int found = 0;
while (fread(&s, sizeof(Student), 1, fp) == 1) {
if (s.id == id) {
printf("学号: %d, 姓名: %s, 年龄: %d, 分数: %.1f\n",
s.id, s.name, s.age, s.score);
found = 1;
break;
}
}
if (!found) printf("未找到该学生\n");
fclose(fp);
}
十三、预处理指令
13.1 宏定义
// 无参宏
#define PI 3.14159
#define MAX 100
// 带参宏(⚠️ 宏只是文本替换,注意括号陷阱)
#define SQUARE(x) ((x) * (x)) // 正确:加括号
// #define SQUARE(x) x * x // 错误:SQUARE(1+2) → 1+2*1+2 = 5
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// # 和 ## 运算符
#define STR(x) #x // 转为字符串
#define CONCAT(a, b) a##b // 拼接
printf("%s\n", STR(hello)); // "hello"
int xy = 100;
printf("%d\n", CONCAT(x, y)); // 100
13.2 条件编译
// #ifdef / #ifndef — 是否定义了某个宏
#define DEBUG
#ifdef DEBUG
printf("调试信息: x = %d\n", x);
#endif
// #if — 表达式条件
#if defined(WIN32)
// Windows 特定代码
#elif defined(__linux__)
// Linux 特定代码
#else
// 其他平台
#endif
// 防止头文件重复包含
#ifndef _MY_HEADER_H_
#define _MY_HEADER_H_
// ... 头文件内容 ...
#endif
// 或使用 #pragma once(更简洁,但非标准)
#pragma once
13.3 #include
#include <stdio.h> // 从系统目录搜索
#include "myheader.h" // 先从当前目录搜索,再搜索系统目录
十四、多文件编程
14.1 头文件(.h)与源文件(.c)
项目结构:
├── main.c # 主程序
├── student.h # 学生模块头文件(声明)
├── student.c # 学生模块源文件(实现)
├── utils.h # 工具函数头文件
└── utils.c # 工具函数源文件
// ============ student.h ============
#ifndef _STUDENT_H_
#define _STUDENT_H_
typedef struct {
int id;
char name[50];
int age;
} Student;
// 函数声明(只声明,不实现)
void printStudent(const Student* s);
void inputStudent(Student* s);
#endif
// ============ student.c ============
#include <stdio.h>
#include "student.h"
void printStudent(const Student* s) {
printf("ID: %d, 姓名: %s, 年龄: %d\n", s->id, s->name, s->age);
}
void inputStudent(Student* s) {
printf("输入学号 姓名 年龄: ");
scanf("%d %s %d", &s->id, s->name, &s->age);
}
// ============ main.c ============
#include "student.h"
int main() {
Student s;
inputStudent(&s);
printStudent(&s);
return 0;
}
14.2 extern 关键字
// ============ global.c ============
int globalVar = 100; // 定义全局变量
// ============ main.c ============
extern int globalVar; // 声明:告诉编译器变量在其他文件定义
printf("%d\n", globalVar);
14.3 static 限制作用域
// static 函数:仅在本文件可见(类似 private)
static void helper() {
printf("这个函数只能在当前 .c 文件中使用\n");
}
// static 全局变量:仅本文件可见
static int counter = 0;
十五、常用标准库函数详解
15.1 <stdio.h> — 标准输入输出
printf — 格式化输出
int printf(const char* format, ...);
// 返回:成功输出的字符数,失败返回负数
int n = printf("Hello %s, you are %d years old\n", "张三", 20);
printf("输出了 %d 个字符\n", n);
// 全部格式符速查
printf("%d\n", 42); // %d/%i: 有符号十进制整数
printf("%u\n", 42); // %u: 无符号十进制整数
printf("%x\n", 255); // %x: 十六进制小写 → ff
printf("%X\n", 255); // %X: 十六进制大写 → FF
printf("%o\n", 10); // %o: 八进制 → 12
printf("%f\n", 3.14); // %f: 浮点数(默认6位小数)
printf("%e\n", 3.14); // %e: 科学计数法 → 3.140000e+00
printf("%g\n", 3.14); // %g: 自动选 %f 或 %e(较短的那个)
printf("%c\n", 'A'); // %c: 字符
printf("%s\n", "hello"); // %s: 字符串
printf("%p\n", &a); // %p: 指针地址
printf("%%\n"); // %%: 输出百分号本身
// 修饰符
printf("%10d\n", 42); // 最少占 10 位,右对齐
printf("%-10d\n", 42); // 左对齐
printf("%010d\n", 42); // 用 0 填充 → 0000000042
printf("%.2f\n", 3.14159); // 保留 2 位小数 → 3.14
printf("%*d\n", 10, 42); // 动态指定宽度 → 占 10 位
scanf — 格式化输入
int scanf(const char* format, ...);
// 返回:成功匹配并赋值的输入项数,失败返回 EOF
int age;
char name[50];
int n = scanf("%d %s", &age, name); // n=2 表示成功读入两个
// ⚠️ 经典陷阱:%c 会读取缓冲区残留的换行符
scanf("%d", &age); // 输入 20 回车,缓冲区留 \n
// scanf("%c", &ch); // ch 会读到 \n!
scanf(" %c", &ch); // ✅ %c 前面加空格,跳过空白字符
// %s 遇到空白字符停止,不能读带空格的字符串
// 读带空格的行使用 fgets
sprintf / sscanf — 字符串格式化
// sprintf: 把格式化内容写入字符串
char buffer[256];
sprintf(buffer, "姓名: %s, 年龄: %d, 分数: %.1f", "张三", 20, 88.5);
printf("%s\n", buffer);
// snprintf: 安全版本,指定最大写入长度(防止缓冲区溢出)
snprintf(buffer, sizeof(buffer), "姓名: %s", name);
// sscanf: 从字符串中按格式解析数据
char input[] = "张三 20 88.5";
char name[50];
int age;
float score;
sscanf(input, "%s %d %f", name, &age, &score);
// 返回成功解析的项数
fopen / fclose — 文件打开与关闭
FILE* fopen(const char* filename, const char* mode);
int fclose(FILE* stream);
// 模式:
// "r" 只读,文件必须存在
// "w" 只写,创建或清空
// "a" 追加,创建或不覆盖
// "r+" 读写,文件必须存在
// "w+" 读写,创建或清空
// "a+" 读+追加
// 加 "b" 为二进制模式:"rb" "wb" "ab" "r+b"
FILE* fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("打开文件失败"); // 打印系统错误原因
return 1;
}
// ... 操作文件 ...
fclose(fp); // ⚠️ 必须关闭!
fgets / fputs — 行读写
// fgets: 读一行(保留 \n,最多 n-1 个字符,安全)
char* fgets(char* str, int n, FILE* stream);
// 返回 str(成功)或 NULL(文件结束/错误)
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
// line 包含 \n,如不需要可去掉
line[strcspn(line, "\n")] = '\0'; // 去掉末尾换行
printf("读到: %s\n", line);
}
// fputs: 写一个字符串(不会自动加 \n)
int fputs(const char* str, FILE* stream);
fputs("Hello\n", fp);
fread / fwrite — 二进制块读写
// fwrite: 写数据块
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
// 参数: 数据地址, 每个元素大小, 元素个数, 文件指针
// 返回: 实际写入的元素个数
int arr[] = {10, 20, 30, 40, 50};
size_t written = fwrite(arr, sizeof(int), 5, fp);
// written == 5 表示全部写入成功
// fread: 读数据块
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
// 返回: 实际读取的元素个数
int buffer[5];
size_t read = fread(buffer, sizeof(int), 5, fp);
// read 可能小于 5(文件不够长或出错)
// ⚠️ 判断 fread 是否成功应该检查返回值,不要用 feof
feof / ferror / clearerr — 状态判断
// feof: 文件是否读到末尾
if (feof(fp)) printf("文件读完了\n");
// ferror: 是否发生读写错误
if (ferror(fp)) printf("读写出错\n");
// clearerr: 清除错误标志和 EOF 标志
clearerr(fp);
fseek / ftell / rewind — 文件定位
// fseek: 移动文件位置指针
int fseek(FILE* stream, long offset, int origin);
// origin: SEEK_SET(文件头) SEEK_CUR(当前位置) SEEK_END(文件尾)
fseek(fp, 0, SEEK_SET); // 回到文件开头
fseek(fp, 100, SEEK_SET); // 跳到第 100 字节
fseek(fp, 0, SEEK_END); // 跳到文件末尾
fseek(fp, -10, SEEK_CUR); // 回退 10 字节
// ftell: 获取当前文件位置(距文件开头的字节数)
long pos = ftell(fp);
// rewind: 回到文件开头(等价于 fseek(fp, 0, SEEK_SET))
rewind(fp);
// 获取文件大小
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
printf("文件大小: %ld 字节\n", size);
remove / rename — 文件操作
// 删除文件
if (remove("old.txt") == 0) {
printf("文件已删除\n");
}
// 重命名文件
if (rename("old.txt", "new.txt") == 0) {
printf("重命名成功\n");
}
fflush — 刷新缓冲区
// 将输出缓冲区的内容强制写入文件/终端
fflush(fp); // 刷新指定文件
fflush(stdout); // 刷新标准输出(确保 printf 内容立即显示)
fflush(NULL); // 刷新所有打开的输出流
tmpfile / tmpnam — 临时文件
// 创建临时文件(关闭时自动删除)
FILE* tmp = tmpfile(); // wb+ 模式
fprintf(tmp, "临时数据\n");
// fclose(tmp) 时文件自动删除
// 生成不重复的临时文件名
char name[L_tmpnam];
tmpnam(name);
15.2 <string.h> — 字符串与内存操作
strlen — 字符串长度
size_t strlen(const char* str);
// 返回 \0 之前的字符数(不包含 \0)
printf("%zu\n", strlen("hello")); // 5
printf("%zu\n", strlen("")); // 0
// ⚠️ strlen 从给定地址开始一直数到 \0,如果字符串没有 \0 → 越界!
strcpy / strncpy — 字符串复制
// strcpy: 把 src 复制到 dest(包括 \0)
char* strcpy(char* dest, const char* src);
char dest[50];
strcpy(dest, "hello");
// ⚠️ dest 必须足够大,否则缓冲区溢出!
// strncpy: 安全版,最多复制 n 个字符
char* strncpy(char* dest, const char* src, size_t n);
strncpy(dest, src, sizeof(dest) - 1); // 预留 \0 的位置
dest[sizeof(dest) - 1] = '\0'; // 手动确保 \0
// ⚠️ 如果 src 长度 >= n,dest 不会自动加 \0!
strcat / strncat — 字符串拼接
// strcat: 把 src 追加到 dest 末尾(覆盖 dest 的 \0,最后加 \0)
char* strcat(char* dest, const char* src);
char s[50] = "Hello ";
strcat(s, "World"); // s = "Hello World"
// ⚠️ dest 必须有足够空间!
// strncat: 安全版,最多追加 n 个字符(会自动加 \0)
char* strncat(char* dest, const char* src, size_t n);
char s[20] = "Hello ";
strncat(s, "World!!!", 5); // 只追加 "World" 5 个字符
strcmp / strncmp — 字符串比较
// strcmp: 逐字符比较(按 ASCII 码)
int strcmp(const char* s1, const char* s2);
// 返回值: 0(s1==s2) <0(s1<s2) >0(s1>s2)
strcmp("abc", "abc"); // 0
strcmp("abc", "abd"); // <0 ('c' < 'd')
strcmp("abc", "ABC"); // >0 ('a' > 'A') 小写字母 > 大写字母
// strncmp: 只比较前 n 个字符
strncmp("abc123", "abc456", 3); // 0(前 3 个相同)
// ⚠️ 不能直接用 == 比较字符串!
// if (s1 == s2) 是比较指针地址,不是比较内容
strchr / strrchr — 字符查找
// strchr: 从左向右查找字符第一次出现的位置
char* strchr(const char* str, int c);
char* p = strchr("hello", 'l'); // 指向第一个 'l'
printf("位置: %ld\n", p - str); // 2
if (strchr("hello", 'x') == NULL) printf("未找到\n");
// strrchr: 从右向左查找字符最后一次出现的位置
char* p = strrchr("hello", 'l'); // 指向最后一个 'l'
printf("位置: %ld\n", p - str); // 3
strstr — 子串查找
// strstr: 在 haystack 中查找 needle 子串首次出现的位置
char* strstr(const char* haystack, const char* needle);
char* p = strstr("hello world", "world");
if (p) {
printf("找到: %s\n", p); // "world"
printf("位置: %ld\n", p - haystack); // 6
}
// 常见用法:判断字符串是否包含某子串
if (strstr(url, "https://")) {
printf("是 HTTPS 链接\n");
}
strtok — 字符串分割
// strtok: 按分隔符切割字符串(⚠️ 会修改原字符串!)
char* strtok(char* str, const char* delimiters);
// 第一次调用传 str,后续调用传 NULL 继续切割同一个字符串
char s[] = "张三,20,北京,13800000001"; // ⚠️ 必须是可修改的 char 数组
char* token = strtok(s, ",");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, ",");
}
// 输出: 张三 20 北京 13800000001
// ⚠️ strtok 不是线程安全的(用静态变量保存状态),多线程用 strtok_r
strspn / strcspn — 字符集匹配
// strspn: 开头连续匹配字符集的最大长度
strspn("123abc", "0123456789"); // 3(开头 3 个是数字)
// strcspn: 开头连续不匹配字符集的最大长度
strcspn("hello world", " "); // 5(第一个空格前有 5 个字符)
// 常见用法:去掉字符串末尾的换行
line[strcspn(line, "\n")] = '\0';
strerror — 错误码转字符串
#include <string.h>
#include <errno.h>
FILE* fp = fopen("notexist.txt", "r");
if (fp == NULL) {
printf("错误: %s\n", strerror(errno)); // "No such file or directory"
}
// perror 等价效果,直接打印到 stderr
memcpy — 内存复制
// memcpy: 从 src 复制 n 个字节到 dest
void* memcpy(void* dest, const void* src, size_t n);
// ⚠️ src 和 dest 不能重叠(重叠用 memmove)
// ⚠️ 不检查 \0,严格复制 n 个字节
int src[5] = {1, 2, 3, 4, 5};
int dest[5];
memcpy(dest, src, sizeof(src)); // 复制整个数组
// 复制结构体
struct Student s1 = {1, "张三", 20}, s2;
memcpy(&s2, &s1, sizeof(struct Student));
memmove — 安全内存复制(支持重叠)
// memmove: 和 memcpy 类似,但支持 src 和 dest 重叠
void* memmove(void* dest, const void* src, size_t n);
// 原理:先判断重叠方向,选择从前或从后复制
char s[] = "abcdefg";
memmove(s + 2, s, 4); // s = "ababcdg"
// 如果 src 和 dest 可能重叠,优先用 memmove
memset — 内存填充
// memset: 将 n 个字节都设置为 c
void* memset(void* str, int c, size_t n);
// 将数组全部清零
int arr[100];
memset(arr, 0, sizeof(arr));
// 将字符数组初始化为特定字符
char s[50];
memset(s, '-', sizeof(s) - 1);
s[49] = '\0'; // "- - - - ..."
// ⚠️ memset 按字节赋值!非 char 类型小心:
int arr2[10];
memset(arr2, 0, sizeof(arr2)); // 每个字节都是 0,int 也为 0 → ✅
memset(arr2, 1, sizeof(arr2)); // 每个字节都是 1,int = 0x01010101 → ❌ 不是 1!
memcmp — 内存比较
// memcmp: 逐字节比较 n 个字节
int memcmp(const void* s1, const void* s2, size_t n);
// 返回值同 strcmp
int a[] = {1, 2, 3}, b[] = {1, 2, 4};
if (memcmp(a, b, sizeof(a)) < 0) {
printf("a < b\n");
}
15.3 <stdlib.h> — 通用工具
malloc / calloc / realloc / free — 动态内存
// malloc: 分配 size 字节(不初始化,内容是随机值)
void* malloc(size_t size);
int* p = (int*)malloc(100 * sizeof(int)); // 100 个 int
// calloc: 分配 count × size 字节并初始化为 0
void* calloc(size_t count, size_t size);
int* q = (int*)calloc(100, sizeof(int)); // 100 个 int,全是 0
// realloc: 调整已分配内存的大小
void* realloc(void* ptr, size_t new_size);
// 可能原地扩展,也可能移到新地址并复制旧数据
int* tmp = (int*)realloc(p, 200 * sizeof(int));
if (tmp != NULL) p = tmp; // ⚠️ 不要直接 p = realloc(p, ...)
// free: 释放动态分配的内存
void free(void* ptr);
free(p);
p = NULL; // 防止野指针
atoi / atol / atof — 字符串转数字
// atoi: 字符串 → int(无错误检测,转换失败返回 0)
int a = atoi("12345"); // 12345
int b = atoi(" -42"); // -42(自动跳过前导空白)
int c = atoi("42abc"); // 42(遇到非数字停止)
int d = atoi("abc"); // 0(无法转换)
// atol: 字符串 → long
// atof: 字符串 → double
double x = atof("3.14159"); // 3.14159
strtol / strtod — 字符串转数字(带错误检测,推荐)
// strtol: 字符串 → long,带错误检测
long strtol(const char* str, char** endptr, int base);
// base: 进制(2-36),0 表示自动检测(0x 开头为 16 进制,0 开头为 8 进制)
char* end;
long n = strtol("12345abc", &end, 10);
printf("数字: %ld\n", n); // 12345
printf("剩余: %s\n", end); // "abc"(第一个非数字字符的位置)
if (*end != '\0') {
printf("警告: 存在非数字字符\n");
}
// strtod: 字符串 → double,同理
char* end2;
double x = strtod("3.14abc", &end2);
rand / srand — 随机数
// rand: 生成伪随机数(0 到 RAND_MAX,通常 32767)
int r = rand();
int dice = rand() % 6 + 1; // 1-6
// srand: 设定随机种子(同样的种子 → 同样的随机序列)
srand(time(NULL)); // 用当前时间做种子(每次运行不同)
srand(42); // 固定种子(可复现的随机)
// 生成指定范围的随机数
int n = rand() % (max - min + 1) + min; // [min, max]
exit / atexit / abort — 程序终止
// exit: 正常终止程序,会执行 atexit 注册的函数 + 刷新缓冲区
exit(0); // 0 表示成功
exit(EXIT_SUCCESS);
exit(EXIT_FAILURE);
// atexit: 注册程序退出时执行的清理函数
void cleanup() {
printf("程序退出,执行清理...\n");
}
atexit(cleanup); // 在 main 中注册,exit 时调用
// abort: 异常终止程序(不执行 atexit 函数,可能生成 core dump)
abort();
// 通常用于不可恢复的错误
system — 执行系统命令
int ret = system("ls -la"); // Linux 下执行命令
int ret = system("dir"); // Windows 下执行命令
int ret = system("mkdir test"); // 创建目录
// 返回命令退出状态,失败返回 -1
qsort — 快速排序
void qsort(void* base, size_t count, size_t size,
int (*compar)(const void*, const void*));
// 比较函数:返回负数(a<b) / 0(a==b) / 正数(a>b)
int cmpInt(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int arr[] = {34, 12, 56, 78, 23, 9, 45};
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), cmpInt);
// 结构体排序:按分数降序
typedef struct { char name[50]; int score; } Stud;
int cmpScore(const void* a, const void* b) {
return ((Stud*)b)->score - ((Stud*)a)->score; // 降序
}
Stud class[5] = {{"张三",88}, {"李四",92}, {"王五",76}};
qsort(class, 3, sizeof(Stud), cmpScore);
bsearch — 二分查找
void* bsearch(const void* key, const void* base, size_t count,
size_t size, int (*compar)(const void*, const void*));
// ⚠️ 数组必须已排序!
int arr[] = {9, 12, 23, 34, 45, 56, 78};
int n = 7, key = 34;
int* found = (int*)bsearch(&key, arr, n, sizeof(int), cmpInt);
if (found) printf("找到: %d\n", *found);
15.4 <math.h> — 数学函数
#include <math.h>
// 编译时需要链接数学库:gcc prog.c -lm
// ===== 基本运算 =====
double sqrt(double x); // 平方根: sqrt(16) = 4.0
double pow(double x, double y); // x 的 y 次幂: pow(2, 10) = 1024.0
double fabs(double x); // 绝对值: fabs(-3.14) = 3.14
double fmod(double x, double y); // 浮点取余: fmod(7.5, 2.0) = 1.5
// ===== 三角函数(参数为弧度) =====
double sin(double x); // 正弦
double cos(double x); // 余弦
double tan(double x); // 正切
double asin(double x); // 反正弦
double acos(double x); // 反余弦
double atan(double x); // 反正切
double atan2(double y, double x); // 反正切(y/x),能判断象限
// ===== 指数与对数 =====
double exp(double x); // e 的 x 次幂: exp(1) = e ≈ 2.718
double log(double x); // 自然对数 ln: log(e) = 1.0
double log10(double x); // 以 10 为底: log10(100) = 2.0
// ===== 取整 =====
double ceil(double x); // 向上取整: ceil(3.14) = 4.0, ceil(-3.14) = -3.0
double floor(double x); // 向下取整: floor(3.14) = 3.0, floor(-3.14) = -4.0
double round(double x); // 四舍五入: round(3.6) = 4.0, round(3.4) = 3.0
double trunc(double x); // 向 0 取整: trunc(3.9) = 3.0, trunc(-3.9) = -3.0
// ===== 实用示例 =====
double radius = 5.0;
double area = M_PI * pow(radius, 2); // 圆面积
double hypotenuse = sqrt(pow(3, 2) + pow(4, 2)); // 斜边:5.0
// 把角度转为弧度
double radians = 30.0 * M_PI / 180.0;
printf("sin(30°) = %f\n", sin(radians)); // 0.5
15.5 <ctype.h> — 字符处理
#include <ctype.h>
// ===== 字符分类(返回非 0 = 真,0 = 假) =====
isalpha(c); // 是否字母(a-z, A-Z)
isdigit(c); // 是否数字(0-9)
isalnum(c); // 是否字母或数字
isspace(c); // 是否空白字符(空格、\t、\n、\r、\f、\v)
isupper(c); // 是否大写字母
islower(c); // 是否小写字母
isxdigit(c); // 是否十六进制数字(0-9, a-f, A-F)
ispunct(c); // 是否标点符号
isprint(c); // 是否可打印字符(含空格)
isgraph(c); // 是否可打印字符(不含空格)
iscntrl(c); // 是否控制字符
// ===== 字符转换 =====
char ch = tolower('A'); // 'a'
ch = toupper('a'); // 'A'
// ===== 实战:统计字符串中各类型字符数 =====
char* s = "Hello, 2024!";
int letters = 0, digits = 0, spaces = 0;
for (int i = 0; s[i]; i++) {
if (isalpha(s[i])) letters++;
else if (isdigit(s[i])) digits++;
else if (isspace(s[i])) spaces++;
}
printf("字母:%d 数字:%d 空格:%d\n", letters, digits, spaces);
// 字母:5 数字:4 空格:1
15.6 <time.h> — 时间处理
#include <time.h>
// time: 获取当前时间戳(从 1970-01-01 00:00:00 UTC 起的秒数)
time_t now = time(NULL);
printf("Unix 时间戳: %ld\n", now);
// ctime: 时间戳 → 可读字符串
printf("当前时间: %s", ctime(&now)); // 自带 \n
// difftime: 计算两个时间之差(秒)
time_t start = time(NULL);
// ... 执行一些操作 ...
time_t end = time(NULL);
printf("耗时: %.0f 秒\n", difftime(end, start));
// clock: 程序运行时钟(更精确,单位:clock_t 滴答数)
clock_t c1 = clock();
// ... 操作 ...
clock_t c2 = clock();
double cpu_time = (double)(c2 - c1) / CLOCKS_PER_SEC;
printf("CPU 时间: %f 秒\n", cpu_time);
// ===== 时间格式化 =====
time_t t = time(NULL);
struct tm* local = localtime(&t); // 转为本地时间
printf("%d-%02d-%02d %02d:%02d:%02d\n",
local->tm_year + 1900, // 年:从 1900 起算
local->tm_mon + 1, // 月:0-11
local->tm_mday, // 日:1-31
local->tm_hour, // 时:0-23
local->tm_min, // 分:0-59
local->tm_sec); // 秒:0-59
// strftime: 自定义格式化
char buf[100];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local);
printf("%s\n", buf); // "2026-05-20 14:30:00"
// 常用格式符:
// %Y: 四位年份 %y: 两位年份 %m: 月份(01-12)
// %d: 日期(01-31) %H: 时(00-23) %M: 分(00-59) %S: 秒(00-59)
// %A: 星期全称 %a: 星期缩写 %B: 月份全称 %b: 月份缩写
15.7 <stdlib.h> 补充 — 环境变量与进程
// getenv: 获取环境变量的值
char* home = getenv("HOME"); // Linux
char* home = getenv("USERPROFILE"); // Windows
if (home) printf("Home: %s\n", home);
// system: 执行 shell 命令
int status = system("ping -c 1 baidu.com");
// abs / labs: 整数绝对值(在 <stdlib.h> 中)
int a = abs(-42); // 42
long b = labs(-123456789L); // 123456789
15.8 <assert.h> — 调试断言
#include <assert.h>
// assert: 表达式为假时终止程序并打印诊断信息
int divide(int a, int b) {
assert(b != 0); // b 为 0 时程序终止
return a / b;
}
// 在 release 版本中禁用断言:编译时加 -DNDEBUG
// gcc -DNDEBUG prog.c → 所有 assert 被忽略
15.9 <limits.h> 与 <float.h> — 类型边界
#include <limits.h>
#include <float.h>
// 整型范围
printf("INT_MAX = %d\n", INT_MAX); // 2147483647
printf("INT_MIN = %d\n", INT_MIN); // -2147483648
printf("UINT_MAX = %u\n", UINT_MAX); // 4294967295
printf("LLONG_MAX = %lld\n", LLONG_MAX);
// 浮点范围
printf("float 范围: %e ~ %e\n", FLT_MIN, FLT_MAX);
printf("double 范围: %e ~ %e\n", DBL_MIN, DBL_MAX);
printf("float 精度: %d 位十进制\n", FLT_DIG); // 6
printf("double 精度: %d 位十进制\n", DBL_DIG); // 15
15.10 <stdarg.h> — 可变参数
#include <stdarg.h>
// 实现一个求和的变参函数
int sum(int count, ...) {
va_list args;
va_start(args, count); // 初始化,count 是最后一个固定参数
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // 每次取出一个 int
}
va_end(args); // 清理
return total;
}
int result = sum(5, 10, 20, 30, 40, 50); // 150
15.11 <setjmp.h> — 非局部跳转
#include <setjmp.h>
jmp_buf env;
void func() {
printf("func 中发生错误,跳回去\n");
longjmp(env, 1); // 跳回 setjmp 处,返回值为 1
}
int main() {
if (setjmp(env) == 0) {
printf("正常执行\n");
func(); // 这里调用
} else {
printf("从 longjmp 跳回来的\n");
}
return 0;
}
// 输出:
// 正常执行
// func 中发生错误,跳回去
// 从 longjmp 跳回来的
15.12 函数手册速查表
|
头文件 |
函数 |
功能 |
返回值 |
|---|---|---|---|
|
|
|
格式化输出 |
字符数 |
|
|
|
格式化输入 |
成功匹配数 |
|
|
|
格式化写入字符串 |
字符数 |
|
|
|
从字符串格式化读取 |
成功匹配数 |
|
|
|
打开文件 |
FILE* / NULL |
|
|
|
关闭文件 |
0 / EOF |
|
|
|
读一行 |
char* / NULL |
|
|
|
读数据块 |
读取的块数 |
|
|
|
写数据块 |
写入的块数 |
|
|
|
文件定位 |
0 / 非 0 |
|
|
|
获取文件位置 |
字节偏移 / -1L |
|
|
|
字符串长度 |
字符数 |
|
|
|
字符串复制 |
dest 指针 |
|
|
|
字符串比较 |
0 / <0 / >0 |
|
|
|
字符串拼接 |
dest 指针 |
|
|
|
查找字符 |
位置指针 / NULL |
|
|
|
查找子串 |
位置指针 / NULL |
|
|
|
字符串分割 |
token 指针 / NULL |
|
|
|
内存复制 |
dest 指针 |
|
|
|
内存填充 |
str 指针 |
|
|
|
内存比较 |
0 / <0 / >0 |
|
|
|
安全内存复制 |
dest 指针 |
|
|
|
分配内存 |
指针 / NULL |
|
|
|
释放内存 |
无 |
|
|
|
字符串→int |
转换结果 |
|
|
|
字符串→long(安全) |
转换结果 |
|
|
|
生成随机数 |
0 ~ RAND_MAX |
|
|
|
快速排序 |
无 |
|
|
|
二分查找 |
指针 / NULL |
|
|
|
终止程序 |
无返回 |
|
|
|
平方根 |
double |
|
|
|
幂运算 |
double |
|
|
|
向上取整 |
double |
|
|
|
向下取整 |
double |
|
|
|
是否字母 |
非 0 / 0 |
|
|
|
是否数字 |
非 0 / 0 |
|
|
|
获取时间戳 |
time_t |
|
|
|
格式化时间 |
写入字符数 |
|
|
|
断言 |
无(假则终止) |
十六、C 语言知识体系总结
C 语言知识体系
│
├── 基础语法
│ ├── 数据类型(int/float/double/char)
│ ├── 常量与变量
│ ├── 运算符与表达式
│ └── 类型转换
│
├── 程序结构
│ ├── 顺序结构(输入输出)
│ ├── 选择结构(if-else/switch)
│ └── 循环结构(while/do-while/for)
│
├── 数组
│ ├── 一维数组
│ ├── 二维数组
│ └── 字符数组 = 字符串
│
├── 函数
│ ├── 定义与调用
│ ├── 参数传递(值传递)
│ ├── 递归
│ └── 作用域与生命周期
│
├── 指针 ★★★(核心难点)
│ ├── 指针与变量
│ ├── 指针与数组
│ ├── 指针与字符串
│ ├── 指针与函数
│ ├── 多级指针
│ ├── 指针数组 vs 数组指针
│ ├── 函数指针
│ └── 动态内存分配
│
├── 构造类型
│ ├── 结构体(struct)
│ ├── 共用体(union)
│ └── 枚举(enum)
│
├── 链表(结构体 + 指针综合应用)
│
├── 文件操作
│ ├── 文本文件读写
│ └── 二进制文件读写
│
├── 预处理
│ ├── 宏定义(#define)
│ ├── 条件编译(#ifdef)
│ └── 文件包含(#include)
│
└── 多文件编程
├── 头文件与源文件分离
├── extern 外部声明
└── static 文件内作用域