C 语言入门教程
本教程面向零基础学习者,从最基础的概念讲起,循序渐进地掌握 C 语言编程。
目录
1. C 语言简介与历史
1.1 C 语言的诞生
C 语言诞生于 1972 年,由美国贝尔实验室的 Dennis Ritchie(丹尼斯·里奇)设计开发。最初目的是为了重写 Unix 操作系统——早期的 Unix 是用汇编语言编写的,移植性很差,Dennis Ritchie 希望用一种更高级、更可移植的语言来替代。
C 语言从早期的 B 语言(由 Ken Thompson 开发)演变而来,因此得名 "C"。在 B 语言基础上,C 语言增加了类型系统,使得编译器能进行更严格的检查。
1.2 为什么学 C 语言?
- 理解计算机底层:C 语言直接操作内存,学习它能帮助你理解计算机的工作原理。
- 操作系统和嵌入式开发:Linux 内核、Windows 底层、嵌入式系统(如单片机、物联网设备)大多用 C 编写。
- 性能极高:C 编译后的程序运行效率接近汇编语言,适合对性能要求极高的场景。
- 其他语言的基础:C++、Java、Python、Go 等现代语言都受到 C 的深刻影响,学会 C 有助于理解其他语言。
- 就业需求:系统开发、驱动开发、游戏引擎、高频交易等领域对 C 人才需求旺盛。
1.3 C 语言的标准演进
| 标准 | 年份 | 说明 |
|---|---|---|
| K&R C | 1978 | 最早的非正式标准,由 Kernighan 和 Ritchie 的著作定义 |
| ANSI C (C89) | 1989 | 第一个正式标准 |
| C99 | 1999 | 增加了变长数组、单行注释 //、bool 类型等 |
| C11 | 2011 | 增加了多线程支持、泛型选择等 |
| C17 | 2017 | 修正 C11 的缺陷,无重大新特性 |
| C23 | 2023 | 最新标准,增加了更多现代特性 |
本教程的内容基于 C99/C11 标准,这是目前最广泛使用的版本。
2. 开发环境搭建
2.1 编译器选择
C 语言是编译型语言,源代码需要通过编译器翻译成机器码才能运行。主流编译器有:
- GCC(GNU Compiler Collection):Linux 和 macOS 上最常用,开源免费。
- Clang:Apple 主推的编译器,错误提示友好,macOS 上默认安装。
- MSVC(Microsoft Visual C++):Windows 上 Visual Studio 自带。
2.2 各平台安装
Windows
推荐安装 MinGW-w64,获取 GCC 编译器:
- 访问 https://www.mingw-w64.org/ 下载安装包
- 安装完成后,将
bin目录添加到系统环境变量PATH中 - 打开命令提示符,输入
gcc --version验证安装
或者直接安装 Visual Studio Code + C/C++ 扩展 + MinGW,获得更好的编辑体验。
macOS
# 安装 Xcode Command Line Tools(包含 Clang 编译器)
xcode-select --install
# 验证
gcc --version
Linux(Ubuntu/Debian)
# 安装 GCC 和构建工具
sudo apt update
sudo apt install build-essential
# 验证
gcc --version
2.3 编辑器推荐
| 编辑器 | 说明 |
|---|---|
| VS Code | 轻量、插件丰富,推荐安装 C/C++ 扩展 |
| CLion | JetBrains 出品,功能强大的 C/C++ IDE(付费) |
| Vim/Neovim | 终端编辑器,适合进阶用户 |
| Dev-C++ | Windows 上的老牌 C/C++ IDE,适合初学者 |
2.4 编译运行流程
一个 C 程序从源代码到运行,经历以下步骤:
源代码(.c) → 预处理 → 编译 → 汇编 → 链接 → 可执行文件
使用 GCC 编译运行一个程序:
# 编译
gcc hello.c -o hello
# 运行(Linux/macOS)
./hello
# 运行(Windows)
hello.exe
3. 第一个 C 程序
3.1 Hello, World!
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
逐行解析:
#include <stdio.h>:预处理指令,将标准输入输出头文件包含进来。printf函数的声明就在这个文件里。int main(void):主函数,每个 C 程序必须有且只有一个main函数,它是程序的入口点。int表示返回整数,void表示不接受参数。printf("Hello, World!\n");:调用printf函数输出文字。\n是换行符。return 0;:返回 0 给操作系统,表示程序正常结束。
3.2 程序结构
一个 C 程序的基本结构:
#include <头文件> // 预处理指令(可有多个)
// 全局变量声明(可选)
// 函数声明(可选)
int main(void) {
// 你的代码
return 0;
}
// 其他函数定义(可选)
3.3 注释
// 这是单行注释(C99 起支持)
/* 这是多行注释
可以跨越多行
C89 就支持 */
注释是给人看的,编译器会完全忽略。养成写注释的好习惯非常重要。
4. 基本数据类型
C 语言是一种强类型语言,每个变量都有明确的类型。基本数据类型如下:
4.1 整数类型
| 类型 | 关键字 | 典型大小 | 取值范围(32位系统) |
|---|---|---|---|
| 短整型 | short |
2 字节 | -32,768 ~ 32,767 |
| 整型 | int |
4 字节 | -2,147,483,648 ~ 2,147,483,647 |
| 长整型 | long |
4 或 8 字节 | 平台相关 |
| 长长整型 | long long |
8 字节 | 约 ±9.2 × 10¹⁸ |
可以加 unsigned 前缀表示无符号(只存储非负数),范围翻倍:
unsigned int age = 25; // 0 ~ 4,294,967,295
unsigned short score = 100; // 0 ~ 65,535
4.2 浮点类型(小数)
| 类型 | 关键字 | 典型大小 | 精度 |
|---|---|---|---|
| 单精度 | float |
4 字节 | 约 6~7 位有效数字 |
| 双精度 | double |
8 字节 | 约 15~16 位有效数字 |
| 长双精度 | long double |
12 或 16 字节 | 更高精度 |
float pi = 3.14f; // float 字面量加 f 后缀
double e = 2.718281828; // double 是默认浮点类型
4.3 字符类型
char letter = 'A'; // 单个字符,占 1 字节
char digit = '9'; // 字符 '9',不是数字 9
char newline = '\n'; // 转义字符:换行
char 本质上是一个小整数,存储的是字符的 ASCII 码值。'A' 的 ASCII 值是 65,'a' 是 97,'0' 是 48。
4.4 布尔类型(C99)
#include <stdbool.h>
bool is_student = true; // true 或 false
bool is_passed = false;
4.5 使用 sizeof 查看类型大小
#include <stdio.h>
int main(void) {
printf("char: %zu 字节\n", sizeof(char));
printf("short: %zu 字节\n", sizeof(short));
printf("int: %zu 字节\n", sizeof(int));
printf("long: %zu 字节\n", sizeof(long));
printf("long long: %zu 字节\n", sizeof(long long));
printf("float: %zu 字节\n", sizeof(float));
printf("double: %zu 字节\n", sizeof(double));
return 0;
}
5. 变量与常量
5.1 变量
变量是存储数据的"容器",有名称、类型和值。
int age = 20; // 声明并初始化
float height; // 仅声明,值未定义(危险!)
height = 175.5f; // 赋值
int a = 1, b = 2, c = 3; // 同时声明多个
命名规则:
- 由字母、数字、下划线组成,不能以数字开头
- 区分大小写(
age和Age是不同变量) - 不能使用关键字(如
int、return、if) - 推荐使用有意义的名称(
student_count而非sc)
5.2 常量
// 方式1:#define 宏定义(预处理阶段替换)
#define PI 3.14159265
#define MAX_SIZE 100
// 方式2:const 关键字(推荐,有类型检查)
const int MONTHS = 12;
const double GRAVITY = 9.8;
为什么推荐 const? 因为 #define 是简单的文本替换,没有类型检查,调试时也看不到变量名。const 是真正的变量,编译器会进行类型检查。
5.3 输入与输出
#include <stdio.h>
int main(void) {
int age;
char name[50];
printf("请输入你的名字:");
scanf("%s", name); // 读取字符串(注意:name 不加 &)
printf("请输入你的年龄:");
scanf("%d", &age); // 读取整数(注意 & 取地址符)
printf("你好,%s!你今年 %d 岁。\n", name, age);
return 0;
}
常用格式说明符:
| 说明符 | 含义 | 示例 |
|---|---|---|
%d |
整数 | printf("%d", 42) → 42 |
%f |
浮点数 | printf("%f", 3.14) → 3.140000 |
%.2f |
保留两位小数 | printf("%.2f", 3.14) → 3.14 |
%c |
字符 | printf("%c", 'A') → A |
%s |
字符串 | printf("%s", "hello") → hello |
%zu |
sizeof 的返回值 |
printf("%zu", sizeof(int)) |
%p |
指针地址 | printf("%p", &age) |
%% |
输出百分号 | printf("100%%") → 100% |
6. 运算符
6.1 算术运算符
int a = 10, b = 3;
a + b // 13 加法
a - b // 7 减法
a * b // 30 乘法
a / b // 3 整数除法(截断小数)
a % b // 1 取余(模运算)
注意:两个整数相除,结果仍是整数(小数部分被截断)。要得到小数结果,至少一个操作数应为浮点类型:
float result = 10.0f / 3; // 3.333333
6.2 赋值运算符
int x = 10;
x += 5; // 等价于 x = x + 5; 结果 15
x -= 3; // 等价于 x = x - 3; 结果 12
x *= 2; // 等价于 x = x * 2; 结果 24
x /= 4; // 等价于 x = x / 4; 结果 6
x %= 5; // 等价于 x = x % 5; 结果 1
6.3 自增自减
int i = 5;
i++; // i 变为 6(后置:先使用再加 1)
++i; // i 变为 7(前置:先加 1 再使用)
i--; // i 变为 6
--i; // i 变为 5
6.4 关系运算符(比较)
// 结果为 1(真)或 0(假)
a == b // 等于(注意是双等号!)
a != b // 不等于
a > b // 大于
a < b // 小于
a >= b // 大于等于
a <= b // 小于等于
6.5 逻辑运算符
// 与(&&):两者都为真,结果为真
(5 > 3) && (10 > 7) // 1(真)
// 或(||):至少一个为真,结果为真
(5 > 3) || (3 > 10) // 1(真)
// 非(!):取反
!(5 > 3) // 0(假)
短路求值:&& 左边为假时,右边不会执行;|| 左边为真时,右边不会执行。这是一个重要特性。
7. 控制流语句
7.1 if-else 条件语句
#include <stdio.h>
int main(void) {
int score;
printf("请输入成绩:");
scanf("%d", &score);
if (score >= 90) {
printf("优秀!\n");
} else if (score >= 80) {
printf("良好\n");
} else if (score >= 60) {
printf("及格\n");
} else {
printf("不及格,需要加油!\n");
}
return 0;
}
7.2 switch 语句
当需要根据一个变量的多个不同值执行不同操作时,switch 比多个 if-else 更清晰:
#include <stdio.h>
int main(void) {
int day;
printf("请输入星期几(1-7):");
scanf("%d", &day);
switch (day) {
case 1: printf("星期一\n"); break;
case 2: printf("星期二\n"); break;
case 3: printf("星期三\n"); break;
case 4: printf("星期四\n"); break;
case 5: printf("星期五\n"); break;
case 6: printf("星期六\n"); break;
case 7: printf("星期日\n"); break;
default: printf("输入无效!\n"); break;
}
return 0;
}
注意:每个 case 后面的 break 不能省略,否则会"穿透"到下一个 case 继续执行。
7.3 while 循环
#include <stdio.h>
int main(void) {
// 计算 1 到 100 的和
int sum = 0;
int i = 1;
while (i <= 100) {
sum += i;
i++;
}
printf("1 + 2 + ... + 100 = %d\n", sum); // 5050
return 0;
}
7.4 do-while 循环
与 while 的区别:do-while 至少执行一次循环体(先执行,再判断条件)。
#include <stdio.h>
int main(void) {
int num;
do {
printf("请输入一个正数:");
scanf("%d", &num);
if (num <= 0) {
printf("输入无效,请重新输入!\n");
}
} while (num <= 0);
printf("你输入的正数是:%d\n", num);
return 0;
}
7.5 for 循环
for 循环是最常用的循环结构,适合已知循环次数的场景:
#include <stdio.h>
int main(void) {
// 打印九九乘法表
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
printf("%d×%d=%-4d", j, i, i * j);
}
printf("\n");
}
return 0;
}
输出:
1×1=1
1×2=2 2×2=4
1×3=3 2×3=6 3×3=9
...
7.6 break 和 continue
// break:立即跳出当前循环
for (int i = 0; i < 100; i++) {
if (i == 10) break; // i 等于 10 时退出循环
printf("%d ", i); // 输出 0 1 2 3 4 5 6 7 8 9
}
// continue:跳过本次循环,进入下一次
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) continue; // 跳过偶数
printf("%d ", i); // 输出 1 3 5 7 9
}
8. 函数
8.1 函数的定义与调用
函数是一段可重复使用的代码块。将代码组织成函数,能提高可读性和可维护性。
#include <stdio.h>
// 函数声明(原型)
int add(int a, int b);
void greet(char name[]);
int main(void) {
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
greet("小明");
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
void greet(char name[]) {
printf("你好,%s!欢迎学习 C 语言!\n", name);
}
8.2 函数的参数传递
C 语言的参数传递是值传递——函数接收的是参数的副本,修改副本不会影响原始变量:
void try_to_change(int x) {
x = 100; // 只修改了副本,main 中的 a 不受影响
}
int main(void) {
int a = 5;
try_to_change(a);
printf("a = %d\n", a); // 输出 5,不是 100
return 0;
}
如果需要在函数中修改外部变量,需要使用指针(后面会详细讲解)。
8.3 递归
函数可以调用自身,这叫做递归。经典例子——计算阶乘:
#include <stdio.h>
// 递归计算 n!
long long factorial(int n) {
if (n <= 1) return 1; // 基准条件(递归出口)
return n * factorial(n - 1); // 递归调用
}
int main(void) {
for (int i = 0; i <= 10; i++) {
printf("%d! = %lld\n", i, factorial(i));
}
return 0;
}
递归必须有基准条件(base case),否则会无限递归导致栈溢出。
9. 数组
9.1 一维数组
数组是一组相同类型数据的集合,存储在连续的内存空间中。
#include <stdio.h>
int main(void) {
// 声明并初始化
int scores[5] = {90, 85, 78, 92, 88};
// 也可以让编译器自动推断大小
int nums[] = {1, 2, 3, 4, 5}; // 大小为 5
// 访问元素(下标从 0 开始!)
printf("第一个元素:%d\n", scores[0]); // 90
printf("第三个元素:%d\n", scores[2]); // 78
// 遍历数组
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += scores[i];
}
printf("平均分:%.1f\n", sum / 5.0);
return 0;
}
重要:C 语言不会检查数组越界!访问 scores[10] 不会报错,但会读到垃圾数据甚至导致程序崩溃。
9.2 二维数组
二维数组可以理解为"数组的数组",常用于表示表格或矩阵:
#include <stdio.h>
int main(void) {
// 3 行 4 列的二维数组
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 遍历
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%4d", matrix[i][j]);
}
printf("\n");
}
return 0;
}
9.3 数组作为函数参数
// 计算数组平均值
float average(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return (float)sum / size;
}
int main(void) {
int data[] = {85, 90, 78, 92, 88};
int n = sizeof(data) / sizeof(data[0]); // 计算数组元素个数
printf("平均分:%.1f\n", average(data, n));
return 0;
}
10. 字符串
C 语言没有专门的字符串类型,字符串本质上是以 \0(空字符)结尾的字符数组。
10.1 字符串的声明
// 方式1:字符数组
char str1[] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 方式2:字符串字面量(推荐)
char str2[] = "Hello";
// 方式3:字符指针
const char *str3 = "Hello";
sizeof(str2) 是 6(包含 \0),而 strlen(str2) 是 5(不包含 \0)。
10.2 常用字符串函数
需要 #include <string.h>:
#include <stdio.h>
#include <string.h>
int main(void) {
char str[100] = "Hello";
char str2[] = "World";
// strlen:获取字符串长度
printf("长度:%zu\n", strlen(str)); // 5
// strcat:拼接字符串
strcat(str, " ");
strcat(str, str2);
printf("拼接后:%s\n", str); // "Hello World"
// strcpy:复制字符串
char dest[100];
strcpy(dest, str);
printf("复制:%s\n", dest);
// strcmp:比较字符串(返回 0 表示相等)
if (strcmp(str, dest) == 0) {
printf("两个字符串相等\n");
}
// strstr:查找子串
char *pos = strstr(str, "World");
if (pos != NULL) {
printf("找到了:%s\n", pos); // "World"
}
return 0;
}
10.3 安全提示
strcpy 和 strcat 不检查目标缓冲区大小,可能导致缓冲区溢出(安全漏洞)。推荐使用更安全的版本:
// 使用 strncpy 和 strncat 限制长度
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 确保以 \0 结尾
11. 指针
指针是 C 语言最强大也最难理解的概念。简单来说,指针是一个存储内存地址的变量。
11.1 什么是指针?
#include <stdio.h>
int main(void) {
int a = 42;
int *p = &a; // p 指向 a 的地址
printf("a 的值:%d\n", a); // 42
printf("a 的地址:%p\n", (void *)&a); // 例如 0x7ffd5e8a3b4c
printf("p 的值(即 a 的地址):%p\n", (void *)p);
printf("p 指向的值(*p):%d\n", *p); // 42(解引用)
// 通过指针修改 a 的值
*p = 100;
printf("修改后 a = %d\n", a); // 100
return 0;
}
关键概念:
&a:取变量a的地址*p:解引用,获取指针p指向的值int *p:声明一个指向int类型的指针
11.2 指针与函数
用指针可以在函数中修改外部变量(解决前面"值传递"的问题):
#include <stdio.h>
// 通过指针交换两个变量的值
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int x = 10, y = 20;
printf("交换前:x=%d, y=%d\n", x, y);
swap(&x, &y); // 传入地址
printf("交换后:x=%d, y=%d\n", x, y);
return 0;
}
11.3 指针与数组
数组名本质上是一个指向首元素的指针:
#include <stdio.h>
int main(void) {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // 等价于 int *p = &arr[0];
// 用指针遍历数组
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d, *(p+%d) = %d\n", i, arr[i], i, *(p + i));
}
// 指针算术
printf("p 指向:%d\n", *p); // 10
p++; // 移动到下一个元素
printf("p++ 后指向:%d\n", *p); // 20
return 0;
}
11.4 指针常见错误
// ❌ 野指针:未初始化的指针
int *p;
*p = 10; // 危险!p 指向未知地址
// ❌ 空指针解引用
int *q = NULL;
*q = 10; // 程序崩溃!
// ✅ 正确做法
int *r = NULL;
if (r != NULL) {
*r = 10; // 安全
}
12. 结构体
12.1 定义和使用
结构体允许你将不同类型的数据组合在一起,形成自定义的数据类型:
#include <stdio.h>
#include <string.h>
// 定义结构体类型
struct Student {
char name[50];
int age;
float score;
};
int main(void) {
// 声明并初始化
struct Student stu1;
strcpy(stu1.name, "张三");
stu1.age = 20;
stu1.score = 92.5;
// 也可以一次性初始化
struct Student stu2 = {"李四", 21, 88.0};
// 访问成员
printf("姓名:%s\n", stu1.name);
printf("年龄:%d\n", stu1.age);
printf("成绩:%.1f\n", stu1.score);
return 0;
}
12.2 typedef 简化
typedef struct {
char name[50];
int age;
float score;
} Student; // 现在可以直接用 Student 代替 struct Student
int main(void) {
Student s = {"王五", 19, 95.0};
printf("%s 的成绩是 %.1f\n", s.name, s.score);
return 0;
}
12.3 结构体指针
#include <stdio.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
void print_student(const Student *s) {
// 用 -> 运算符通过指针访问成员
printf("姓名:%s, 年龄:%d, 成绩:%.1f\n",
s->name, s->age, s->score);
}
int main(void) {
Student s = {"赵六", 22, 91.0};
print_student(&s); // 传入结构体的地址
return 0;
}
注意:s->name 等价于 (*s).name,箭头运算符 -> 是专门用于通过指针访问结构体成员的简写。
13. 文件操作
13.1 文件的打开与关闭
#include <stdio.h>
int main(void) {
// 打开文件
FILE *fp = fopen("data.txt", "w"); // "w" 表示写入模式
if (fp == NULL) {
printf("无法打开文件!\n");
return 1;
}
// 写入内容
fprintf(fp, "Hello, File!\n");
fprintf(fp, "这是第二行\n");
// 关闭文件
fclose(fp);
printf("写入完成!\n");
return 0;
}
文件打开模式:
| 模式 | 说明 |
|---|---|
"r" |
只读(文件必须存在) |
"w" |
只写(文件不存在则创建,存在则清空) |
"a" |
追加(在文件末尾写入) |
"r+" |
读写(文件必须存在) |
"w+" |
读写(文件不存在则创建,存在则清空) |
13.2 读取文件
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
printf("无法打开文件!\n");
return 1;
}
char line[256];
// 逐行读取
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
13.3 二进制文件读写
#include <stdio.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
int main(void) {
// 写入二进制文件
Student students[] = {
{"张三", 20, 92.5},
{"李四", 21, 88.0},
{"王五", 19, 95.0}
};
int count = 3;
FILE *fp = fopen("students.dat", "wb"); // "wb" 二进制写入
if (fp == NULL) return 1;
fwrite(&count, sizeof(int), 1, fp); // 写入学生数量
fwrite(students, sizeof(Student), count, fp); // 写入学生数据
fclose(fp);
// 从二进制文件读取
Student loaded[3];
int loaded_count;
fp = fopen("students.dat", "rb"); // "rb" 二进制读取
if (fp == NULL) return 1;
fread(&loaded_count, sizeof(int), 1, fp);
fread(loaded, sizeof(Student), loaded_count, fp);
fclose(fp);
// 打印读取的数据
for (int i = 0; i < loaded_count; i++) {
printf("%s, %d岁, %.1f分\n",
loaded[i].name, loaded[i].age, loaded[i].score);
}
return 0;
}
14. 动态内存管理
14.1 为什么需要动态内存?
数组的大小在编译时就确定了。如果需要运行时根据用户输入决定大小,就需要动态内存分配。
#include <stdio.h>
#include <stdlib.h> // malloc, free 的头文件
int main(void) {
int n;
printf("请输入学生人数:");
scanf("%d", &n);
// 动态分配 n 个 int 的空间
int *scores = (int *)malloc(n * sizeof(int));
if (scores == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 使用动态分配的内存
for (int i = 0; i < n; i++) {
printf("请输入第 %d 个学生的成绩:", i + 1);
scanf("%d", &scores[i]);
}
// 计算平均分
int sum = 0;
for (int i = 0; i < n; i++) {
sum += scores[i];
}
printf("平均分:%.1f\n", (float)sum / n);
// 释放内存(必须!)
free(scores);
scores = NULL; // 避免悬空指针
return 0;
}
14.2 相关函数
| 函数 | 说明 |
|---|---|
malloc(size) |
分配 size 字节的内存,不初始化 |
calloc(n, size) |
分配 n × size 字节,初始化为 0 |
realloc(ptr, size) |
重新调整已分配内存的大小 |
free(ptr) |
释放之前分配的内存 |
黄金规则:每一个 malloc 都必须有对应的 free,否则会造成内存泄漏。
15. 实战项目:学生管理系统
下面是一个综合运用前面所有知识点的实战项目——命令行版学生管理系统。
15.1 功能设计
- 添加学生信息
- 显示所有学生
- 按姓名查找学生
- 按成绩排序
- 删除学生
- 保存到文件 / 从文件加载
- 统计平均分、最高分、最低分
15.2 完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_NAME 50
#define MAX_STUDENTS 100
#define DATA_FILE "students.dat"
// ========== 结构体定义 ==========
typedef struct {
int id; // 学号
char name[MAX_NAME]; // 姓名
float score; // 成绩
} Student;
// ========== 全局变量 ==========
Student students[MAX_STUDENTS]; // 学生数组
int student_count = 0; // 当前学生数量
// ========== 函数声明 ==========
void show_menu(void);
void add_student(void);
void show_all(void);
void search_student(void);
void sort_by_score(void);
void delete_student(void);
void show_statistics(void);
void save_to_file(void);
void load_from_file(void);
void clear_input_buffer(void);
// ========== 主函数 ==========
int main(void) {
load_from_file(); // 启动时加载数据
int choice;
while (1) {
show_menu();
printf("请输入选项(0-7):");
scanf("%d", &choice);
clear_input_buffer();
switch (choice) {
case 1: add_student(); break;
case 2: show_all(); break;
case 3: search_student(); break;
case 4: sort_by_score(); break;
case 5: delete_student(); break;
case 6: show_statistics();break;
case 7: save_to_file(); break;
case 0:
save_to_file();
printf("数据已保存,再见!\n");
return 0;
default:
printf("无效选项,请重新输入!\n");
}
}
return 0;
}
// ========== 函数实现 ==========
void clear_input_buffer(void) {
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
void show_menu(void) {
printf("\n");
printf("╔══════════════════════════════╗\n");
printf("║ 学 生 管 理 系 统 ║\n");
printf("╠══════════════════════════════╣\n");
printf("║ 1. 添加学生 ║\n");
printf("║ 2. 显示所有学生 ║\n");
printf("║ 3. 查找学生 ║\n");
printf("║ 4. 按成绩排序 ║\n");
printf("║ 5. 删除学生 ║\n");
printf("║ 6. 统计信息 ║\n");
printf("║ 7. 保存数据 ║\n");
printf("║ 0. 保存并退出 ║\n");
printf("╚══════════════════════════════╝\n");
}
void add_student(void) {
if (student_count >= MAX_STUDENTS) {
printf("学生数量已达上限(%d)!\n", MAX_STUDENTS);
return;
}
Student *s = &students[student_count];
s->id = student_count + 1;
printf("请输入姓名:");
fgets(s->name, MAX_NAME, stdin);
// 去掉 fgets 读入的换行符
s->name[strcspn(s->name, "\n")] = '\0';
printf("请输入成绩:");
scanf("%f", &s->score);
clear_input_buffer();
student_count++;
printf("添加成功!学号:%d\n", s->id);
}
void show_all(void) {
if (student_count == 0) {
printf("暂无学生数据。\n");
return;
}
printf("\n%-6s %-15s %-8s\n", "学号", "姓名", "成绩");
printf("--------------------------------\n");
for (int i = 0; i < student_count; i++) {
printf("%-6d %-15s %-8.1f\n",
students[i].id,
students[i].name,
students[i].score);
}
printf("--------------------------------\n");
printf("共 %d 名学生\n", student_count);
}
void search_student(void) {
char keyword[MAX_NAME];
printf("请输入要查找的姓名:");
fgets(keyword, MAX_NAME, stdin);
keyword[strcspn(keyword, "\n")] = '\0';
int found = 0;
for (int i = 0; i < student_count; i++) {
if (strstr(students[i].name, keyword) != NULL) {
printf("找到:学号 %d, 姓名 %s, 成绩 %.1f\n",
students[i].id, students[i].name, students[i].score);
found++;
}
}
if (found == 0) {
printf("未找到匹配的学生。\n");
} else {
printf("共找到 %d 条记录。\n", found);
}
}
void sort_by_score(void) {
if (student_count < 2) {
printf("学生数量不足,无需排序。\n");
return;
}
// 冒泡排序(从高到低)
for (int i = 0; i < student_count - 1; i++) {
for (int j = 0; j < student_count - 1 - i; j++) {
if (students[j].score < students[j + 1].score) {
Student temp = students[j];
students[j] = students[j + 1];
students[j + 1] = temp;
}
}
}
printf("已按成绩从高到低排序!\n");
show_all();
}
void delete_student(void) {
int id;
printf("请输入要删除的学号:");
scanf("%d", &id);
clear_input_buffer();
int index = -1;
for (int i = 0; i < student_count; i++) {
if (students[i].id == id) {
index = i;
break;
}
}
if (index == -1) {
printf("未找到学号为 %d 的学生。\n", id);
return;
}
// 确认删除
printf("确定要删除 %s(学号 %d)吗?(y/n):",
students[index].name, students[index].id);
char confirm;
scanf("%c", &confirm);
clear_input_buffer();
if (confirm == 'y' || confirm == 'Y') {
// 将后面的元素前移
for (int i = index; i < student_count - 1; i++) {
students[i] = students[i + 1];
}
student_count--;
printf("删除成功!\n");
} else {
printf("已取消删除。\n");
}
}
void show_statistics(void) {
if (student_count == 0) {
printf("暂无数据。\n");
return;
}
float sum = 0, max_score = students[0].score, min_score = students[0].score;
int max_idx = 0, min_idx = 0;
for (int i = 0; i < student_count; i++) {
sum += students[i].score;
if (students[i].score > max_score) {
max_score = students[i].score;
max_idx = i;
}
if (students[i].score < min_score) {
min_score = students[i].score;
min_idx = i;
}
}
printf("\n===== 统计信息 =====\n");
printf("学生总数:%d\n", student_count);
printf("平均分:%.1f\n", sum / student_count);
printf("最高分:%.1f(%s)\n", max_score, students[max_idx].name);
printf("最低分:%.1f(%s)\n", min_score, students[min_idx].name);
// 分数段统计
int excellent = 0, good = 0, pass = 0, fail = 0;
for (int i = 0; i < student_count; i++) {
if (students[i].score >= 90) excellent++;
else if (students[i].score >= 80) good++;
else if (students[i].score >= 60) pass++;
else fail++;
}
printf("\n分数段统计:\n");
printf(" 优秀(90+):%d 人\n", excellent);
printf(" 良好(80-89):%d 人\n", good);
printf(" 及格(60-79):%d 人\n", pass);
printf(" 不及格(<60):%d 人\n", fail);
}
void save_to_file(void) {
FILE *fp = fopen(DATA_FILE, "wb");
if (fp == NULL) {
printf("保存失败:无法打开文件!\n");
return;
}
fwrite(&student_count, sizeof(int), 1, fp);
fwrite(students, sizeof(Student), student_count, fp);
fclose(fp);
printf("数据已保存到 %s(%d 条记录)\n", DATA_FILE, student_count);
}
void load_from_file(void) {
FILE *fp = fopen(DATA_FILE, "rb");
if (fp == NULL) {
printf("未找到数据文件,将创建新文件。\n");
return;
}
fread(&student_count, sizeof(int), 1, fp);
if (student_count > MAX_STUDENTS) student_count = MAX_STUDENTS;
fread(students, sizeof(Student), student_count, fp);
fclose(fp);
printf("已从文件加载 %d 条学生记录。\n", student_count);
}
15.3 编译和运行
# 编译
gcc student_manager.c -o student_manager
# 运行
./student_manager
15.4 项目知识点总结
这个项目综合运用了以下知识点:
- 结构体:定义
Student数据类型 - 数组:存储多个学生
- 函数:将功能模块化(添加、查找、排序等)
- 控制流:
switch处理菜单、for循环遍历、while维持主循环 - 文件操作:
fopen/fclose/fread/fwrite实现数据持久化 - 字符串处理:
fgets、strstr、strcspn处理用户输入 - 指针:函数参数传递、结构体指针访问
- 排序算法:冒泡排序
16. 学习建议与下一步
16.1 学习路线
基础阶段(你在这里)
- 数据类型、控制流、函数、数组、指针、结构体、文件操作
- 多写小程序练习,不要只看不写
进阶阶段
- 预处理器(
#define、#ifdef、宏函数) - 位运算(
&、|、^、~、<<、>>) - 联合体(
union)和枚举(enum) - 函数指针与回调
- 链表、栈、队列等数据结构的 C 实现
- 预处理器(
高级阶段
- 多文件编程与 Makefile
- 内存模型与编译原理基础
- 系统编程(进程、线程、网络编程)
- 阅读优秀开源项目源码(Redis、Nginx、Linux 内核模块)
16.2 推荐资源
| 资源 | 说明 |
|---|---|
| 《C Primer Plus》 | 经典入门教材,讲解详细 |
| 《C 程序设计语言》(K&R) | C 语言之父写的权威著作,适合有一定基础后阅读 |
| 《C 和指针》 | 深入理解指针的好书 |
| CS50(哈佛公开课) | 计算机科学入门,前半部分用 C 教学 |
| LeetCode | 用 C 刷算法题,锻炼编程能力 |
16.3 常见陷阱提醒
- 忘记
&:scanf("%d", age)应为scanf("%d", &age) - 数组越界:C 不检查越界,后果不可预测
- 内存泄漏:
malloc了就要free - 悬空指针:
free后将指针设为NULL ==和=混淆:if (a = 5)是赋值,永远为真!- 忘记
\0:字符数组作为字符串使用时,别忘了结尾的空字符 scanf残留换行:用getchar()或scanf(" %c", &ch)中的空格跳过
16.4 编码规范建议
- 使用有意义的变量名
- 每个函数只做一件事
- 函数不超过 50 行
- 始终检查
malloc和fopen的返回值 - 编译时开启所有警告:
gcc -Wall -Wextra -pedantic - 使用
const修饰不应被修改的参数
附录:ASCII 码表(常用部分)
| 字符 | ASCII 值 | 字符 | ASCII 值 |
|---|---|---|---|
'\0' (空字符) |
0 | ' ' (空格) |
32 |
'0' - '9' |
48-57 | 'A' - 'Z' |
65-90 |
'a' - 'z' |
97-122 | '\n' (换行) |
10 |
写在最后:学习编程没有捷径,唯一的秘诀就是多写代码。每学一个新概念,就写几个小程序来练习。遇到错误不要怕——编译器的错误提示是最好的老师。祝你学习愉快!