C语言入门教程

教程简介

零基础C语言编程入门完整教程,涵盖环境搭建、基本语法、数据类型、控制流、函数、数组、指针、结构体、文件操作等核心概念,配有学生管理系统实战项目,适合编程初学者。

C 语言入门教程

本教程面向零基础学习者,从最基础的概念讲起,循序渐进地掌握 C 语言编程。


目录

  1. C 语言简介与历史
  2. 开发环境搭建
  3. 第一个 C 程序
  4. 基本数据类型
  5. 变量与常量
  6. 运算符
  7. 控制流语句
  8. 函数
  9. 数组
  10. 字符串
  11. 指针
  12. 结构体
  13. 文件操作
  14. 动态内存管理
  15. 实战项目:学生管理系统
  16. 学习建议与下一步

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 编译器:

  1. 访问 https://www.mingw-w64.org/ 下载安装包
  2. 安装完成后,将 bin 目录添加到系统环境变量 PATH
  3. 打开命令提示符,输入 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; // 同时声明多个

命名规则

  • 由字母、数字、下划线组成,不能以数字开头
  • 区分大小写(ageAge 是不同变量)
  • 不能使用关键字(如 intreturnif
  • 推荐使用有意义的名称(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 安全提示

strcpystrcat 不检查目标缓冲区大小,可能导致缓冲区溢出(安全漏洞)。推荐使用更安全的版本:

// 使用 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 实现数据持久化
  • 字符串处理fgetsstrstrstrcspn 处理用户输入
  • 指针:函数参数传递、结构体指针访问
  • 排序算法:冒泡排序

16. 学习建议与下一步

16.1 学习路线

  1. 基础阶段(你在这里)

    • 数据类型、控制流、函数、数组、指针、结构体、文件操作
    • 多写小程序练习,不要只看不写
  2. 进阶阶段

    • 预处理器(#define#ifdef、宏函数)
    • 位运算(&|^~<<>>
    • 联合体(union)和枚举(enum
    • 函数指针与回调
    • 链表、栈、队列等数据结构的 C 实现
  3. 高级阶段

    • 多文件编程与 Makefile
    • 内存模型与编译原理基础
    • 系统编程(进程、线程、网络编程)
    • 阅读优秀开源项目源码(Redis、Nginx、Linux 内核模块)

16.2 推荐资源

资源 说明
《C Primer Plus》 经典入门教材,讲解详细
《C 程序设计语言》(K&R) C 语言之父写的权威著作,适合有一定基础后阅读
《C 和指针》 深入理解指针的好书
CS50(哈佛公开课) 计算机科学入门,前半部分用 C 教学
LeetCode 用 C 刷算法题,锻炼编程能力

16.3 常见陷阱提醒

  1. 忘记 &scanf("%d", age) 应为 scanf("%d", &age)
  2. 数组越界:C 不检查越界,后果不可预测
  3. 内存泄漏malloc 了就要 free
  4. 悬空指针free 后将指针设为 NULL
  5. === 混淆if (a = 5) 是赋值,永远为真!
  6. 忘记 \0:字符数组作为字符串使用时,别忘了结尾的空字符
  7. scanf 残留换行:用 getchar()scanf(" %c", &ch) 中的空格跳过

16.4 编码规范建议

  • 使用有意义的变量名
  • 每个函数只做一件事
  • 函数不超过 50 行
  • 始终检查 mallocfopen 的返回值
  • 编译时开启所有警告: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

写在最后:学习编程没有捷径,唯一的秘诀就是多写代码。每学一个新概念,就写几个小程序来练习。遇到错误不要怕——编译器的错误提示是最好的老师。祝你学习愉快!

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录