大学计算机专业 · 程序设计基础课程

C语言程序设计:
第一个 Hello World到独立完成小项目

本教程面向大学一年级学生,按基础语法 → 程序设计 → 数据结构 → 指针与内存 → 文件与工程 → 综合实践的路线展开。完成课程后,你将能够读懂、独立编写中等规模的 C 程序,理解指针、内存与编译原理,并能用 C 完成小型项目。

1972Dennis Ritchie 于贝尔实验室发明
编译型源码 → 编译 → 链接 → 可执行
系统级操作系统 / 嵌入式 / 编译器
C17 / C23当前主流标准的演进
↓ 向下滚动,按章节系统学习并完成课堂练习
00

学习指南:为什么学、怎么学、学到什么

在动手写代码之前,先建立全局认知 —— C 的本质、C 在计算机体系中的位置,以及一条循序渐进的学习路线。

00.1 为什么学习 C 语言

C 语言是一种通用、过程式、编译型的编程语言,由 Dennis Ritchie 于 1972 年在贝尔实验室为重写 UNIX 操作系统而设计。它具有以下特点:

  • 接近底层:可直接操作内存地址(指针)、位运算,几乎与汇编一一对应;
  • 可移植:同一份源码可在 Windows / Linux / macOS / 嵌入式平台编译运行;
  • 语法相对简洁:核心关键字仅约 30 个;
  • 高性能:生成的机器码效率与汇编相当,被广泛用于性能敏感场景。

C 语言的典型应用领域

  • 操作系统:Linux、macOS、Windows 内核与大量系统组件;
  • 嵌入式系统:单片机、IoT 设备、汽车电子;
  • 编译器与解释器:GCC、Clang、CPython、Redis 等;
  • 驱动程序数据库(SQLite、Redis)、高性能计算
  • 网络程序:Nginx、libuv、curl 等基础设施;
  • 教学语言:理解计算机系统底层的最佳载体。

一句话总结:C 是连接高级语言与计算机硬件的"枢纽语言",学会它能让你真正理解程序如何在机器上运行。

00.2 学习目标 & 路线

完成本课程后,你将能够:

  • 读懂中等复杂度的 C 程序;
  • 独立编写 200 ~ 500 行的小型 C 程序;
  • 熟练使用变量、表达式、控制结构、函数、数组与字符串;
  • 深入理解指针与内存模型(核心难点);
  • 使用结构体组织数据、动态分配与释放内存;
  • 读写文件、使用预处理与标准库;
  • 使用 gdb / VS Code 调试 C 程序;
  • 独立完成 1 ~ 2 个完整的小项目。

学习路线(推荐顺序)

基础语法第 03 章
变量与类型第 04 章
控制结构第 05 章
函数第 06 章
数组与字符串第 07 章
指针与内存第 08 章 ⭐
结构体第 09 章
文件 I/O第 10 章
标准库第 11 章
算法基础第 12 章
综合项目第 14 章
01

C语言入门:起源、特点与第一个程序

认识 C 的诞生、标准演进,以及"Hello, World!"背后的每一个零件。

1.1 什么是程序

程序 = 数据结构 + 算法,本质上是一组告诉计算机"做什么、按什么顺序做"的指令。C 程序的生命周期通常为:

源代码
.c 文件
预处理展开宏 / 头文件
编译→ 汇编
汇编→ .o 目标文件
链接→ 可执行文件
运行CPU 执行
源代码 hello.c 预处理 .i 编译 .s 汇编 汇编 .o 目标 链接 可执行 GCC: gcc hello.c -o hello (一键完成上述全部步骤)
C 程序从源代码到可执行的完整流程

1.2 C 语言发展简史与标准演进

C 诞生于 1970 年代,1989 年由 ANSI 标准化(ANSI C / C89),随后 ISO 跟进(C90)。其后主流标准演进:

1972C 诞生K&R C 1989ANSI CC89 / C90 1999C99// 注释、long long、<stdint.h> 2011C11多线程、泛型宏、_Static_assert 2017C17bug 修复,无新特性 2023C23新关键字、现代语法
C 语言标准演进时间轴

本教程以 C17 / C23 现代 C 视角组织内容,但兼顾 C99 兼容性(编译器默认模式)。

1.3 第一个 C 程序 —— Hello, World!

几乎所有 C 教材的第一个例子,让我们逐行拆解:

hello.c复制
#include <stdio.h>          // 引入标准输入输出头文件

int main(void)                // 主函数:程序入口
{
    printf("Hello, World!\n");  // 输出字符串到屏幕
    return 0;                  // 返回 0 表示正常退出
}

逐行解释

  • #include <stdio.h>预处理指令,告诉编译器把 stdio.h(标准 I/O 头文件)的内容"粘贴"进来;
  • int main(void):主函数,每个 C 程序必须有且仅有一个 main,是程序入口;
  • { }:复合语句块,函数体边界;
  • printf(...):标准库函数,把字符串打印到标准输出;
  • \n:转义字符,代表换行;
  • return 0;:返回 int 类型的退出码,0 表示正常;
  • ;:每条语句以分号结尾(复合语句的 { } 后不需要)。
编译运行:在 Linux/macOS 终端执行 gcc hello.c -o hello && ./hello,在 Windows + MinGW 下同理。

1.4 注释

C 提供两种注释方式:

comments.c复制
// 单行注释:以 // 开头,到行尾结束(C99 引入)

/*
 * 多行注释:以 /* 开头,*/ 结尾
 *  常用于函数说明、临时屏蔽代码块
 */

int main(void) {
    // printf("暂时不执行\n");
    return 0;
}
注意:多行注释 不能嵌套/* /* */ */ 这种写法会让编译器在第一个 */ 处认为注释结束,导致后面的 */ 报错。
02

C 语言开发环境:编译器、IDE 与项目结构

从一段文本到一个可运行的二进制,C 程序需要"翻译官"和"工具箱"。

2.1 主流编译器

编译器平台特点
GCCLinux/macOS/Windows (MinGW)GNU 出品,最常用的开源 C 编译器,本课程默认使用
ClangmacOS (默认) / Linux / WindowsApple 主力,错误提示友好,编译速度快
MSVCWindowsVisual Studio 自带,Windows 平台原生
TCC全平台Tiny C Compiler,几百 KB,编译极快,适合教学

2.2 第一个命令行编译运行

terminal复制
# 1. 用文本编辑器写一个 hello.c(见 1.3)
# 2. 编译:gcc 把 hello.c 翻译成可执行文件 hello
$ gcc hello.c -o hello

# 3. 运行:在当前目录下执行 ./hello
$ ./hello
Hello, World!

# 常用编译选项
$ gcc -Wall -Wextra -O2 hello.c -o hello    # 开启警告、优化
$ gcc -g    hello.c -o hello               # 生成调试信息(供 gdb 使用)
$ gcc -std=c17 hello.c -o hello            # 指定 C 标准版本

小贴士:Windows PowerShell 下用 .\hello 运行;Linux/macOS 下通常用 ./hello

2.3 推荐的开发环境组合

方案 A:Linux / macOS + VS Code(推荐)

  • 安装 GCC:Linux 发行版一般自带;macOS xcode-select --install
  • 安装 VS Code + 扩展:C/C++、Code Runner;
  • 写代码 → 终端 gcc ... && ./a.out
  • 调试:用 gdb 或 VS Code 的图形调试。

方案 B:Windows + MinGW + VS Code

  • 安装 MinGW-w64(提供 gcc);
  • C:\mingw64\bin 加入 PATH;
  • VS Code 同上;
  • 或在 Dev-C++ / Code::Blocks 一体化 IDE 中使用。

2.4 一个最小的 C 项目结构

tree复制
hello-c/
├── src/
│   └── main.c         // 程序入口与业务逻辑
├── include/
│   └── utils.h        // 自定义头文件(可选)
├── Makefile           // 或 CMakeLists.txt,自动化编译
└── README.md

随着代码量增长,会进一步引入多文件头文件保护Makefile 等工程化概念。

03

基本语法:字符集、标识符、关键字与输入输出

C 程序的"词汇表"——所有合法单词都由字符集构造,关键字是语言的保留字。

3.1 字符集与标识符

  • 字符集:C 源文件由 ASCII 字符组成——英文字母、数字、空白符、标点;
  • 标识符(identifier)用来命名变量、函数、类型等:
identifier.c复制
int student;            // 由字母开头的单词
int student_name;       // 下划线连接多个单词
int score1;             // 字母后接数字

// ❌ 错误:不能以数字开头
// int 1score;

// ❌ 错误:不能使用关键字
// int return;

// ⚠️ 区分大小写:Student 和 student 是两个不同的标识符

3.2 关键字(C 语言保留字)

关键字是 C 语言预定义的有特殊含义的单词,不能用作标识符。常见关键字按功能分组:

类别关键字
数据类型char int float double void short long signed unsigned _Bool
控制语句if else switch case default while do for break continue return goto
存储与作用域auto static extern register
构造类型struct union enum typedef sizeof
常量与限定const volatile inline restrict
预处理#define #include #ifdef #ifndef #endif

3.3 常量与字面量

literal.c复制
// 1) 整数常量
int a = 100;            // 十进制
int b = 0x1F;           // 十六进制 (31)
int c = 0755;           // 八进制   (493)
long d = 1000L;         // long 类型
unsigned e = 42U;      // 无符号

// 2) 浮点常量
double pi  = 3.14159;
double avo = 6.02e23;   // 科学计数法

// 3) 字符常量:用单引号
char g = 'A';          // ASCII 65
char nl = '\n';        // 换行符

// 4) 字符串常量:用双引号
char msg[] = "Hello";   // 末尾自动加 '\0'

3.4 输入与输出:printf / scanf

io.c复制
#include <stdio.h>

int main(void)
{
    int    age;
    char  name[32];

    printf("请输入您的姓名和年龄:");
    scanf("%s %d", name, &age);          // 注意 &取地址

    printf("你好,%s!今年 %d 岁。\n", name, age);
    return 0;
}

常用格式控制符速记

占位符含义占位符含义
%d十进制整数 (int)%ldlong 整数
%f浮点数 (double)%c单个字符
%s字符串%x / %X十六进制整数
%u无符号整数%p指针地址
%lfdouble 读入%%输出一个 %
小心:scanf("%d", &x) 中必须传 地址&x),否则会段错误;scanf("%lf", &d) 中 double 必须用 %lf(读入时),而 printf 中 double 既可用 %f 也可用 %lf。

课堂练习 · 第03章

1下列哪个不是 C 语言的合法注释?
2用 scanf 读取一个 double 变量 d,正确的格式字符串是?
04

数据类型、变量与运算符

C 是一门强类型语言:每个变量都有明确类型,决定了占用内存大小与可执行的操作。

4.1 基本数据类型与大小

类型含义典型大小 (字节)取值范围(示意)
char字符 / 1 字节整数1-128 ~ 127
short短整型2-32768 ~ 32767
int整型4约 ±2.1 × 10⁹
long长整型4 或 8与平台相关
long long更长整型 (C99)8约 ±9.2 × 10¹⁸
float单精度浮点4约 ±3.4 × 10³⁸(6~7 位有效数字)
double双精度浮点8约 ±1.8 × 10³⁰⁸(15~16 位有效数字)
_Bool / bool布尔 (0/1)1<stdbool.h>

提示:实际大小可用 sizeof(int) 查询,需要固定宽度整数请用 <stdint.h>int32_tuint64_t 等。

4.2 变量与常量

variable.c复制
#include <stdio.h>

// 宏常量:预处理阶段做文本替换
#define PI       3.1415926
#define MAX_LEN  256

int main(void)
{
    // 变量:值可以改变
    int    age    = 18;
    float  score  = 95.5;
    char   grade  = 'A';

    // const 常量:运行时不可修改
    const int   MAX_SCORE = 100;

    age = 19;            // 重新赋值 ✔
    // MAX_SCORE = 200;     // ❌ 编译错误:不能修改 const

    printf("PI=%.4f, MAX_SCORE=%d\n", PI, MAX_SCORE);
    return 0;
}

4.3 类型转换

  • 自动转换(隐式):运算时低精度自动提升到高精度(如 int + double → double);
  • 强制转换(目标类型)表达式,例如 (int)3.7 得 3(截断小数部分);
cast.c复制
int    a = 10;
double b = (double)a / 3;     // 3.333...  (true division)
int    c = a / 3;             // 3          (integer division)

// ⚠️ 精度损失:double → int 时小数部分被截断(非四舍五入)
int d = (int)3.99;            // d == 3

4.4 运算符一览

类别运算符(部分)示例
算术+ - * / %a + b * c
关系> < >= <= == !=a == b
逻辑&& || !a > 0 && b > 0
赋值= += -= *= /= %= <<= >>= &= |= ^=a += 3
自增自减++ --i++ (后置), ++i (前置)
条件?:max = a > b ? a : b;
位运算& | ^ ~ << >>flags & 0x0F
逗号,for(i=0, j=0; ...)
陷阱:a == b 中间不能写成 a = b(赋值);== vs = 是 C 语言最常见的笔误之一。

课堂练习 · 第04章

3表达式 10 / 3(int 类型)的结果是?
4下列强制类型转换的结果是?(int)3.99
05

程序控制结构:顺序、选择、循环

从"会写语句"到"会写程序"的关键一步——让代码具备判断与重复执行的能力。

5.1 顺序结构:程序的默认执行方式

语句按书写顺序依次执行。最简单的程序:输入 → 处理 → 输出。

seq.c复制
#include <stdio.h>

int main(void) {
    int r;
    printf("请输入半径:");
    scanf("%d", &r);

    double area = 3.1415926 * r * r;
    printf("半径=%d, 面积=%.2f\n", r, area);
    return 0;
}

5.2 选择结构:if / switch

if 三种形式

if.c复制
if (score >= 90)
    printf("A\n");

if (score >= 60)
    printf("及格\n");
else
    printf("不及格\n");

if (score >= 85)
    printf("优秀\n");
else if (score >= 70)
    printf("良好\n");
else if (score >= 60)
    printf("及格\n");
else
    printf("不及格\n");

switch:多分支离散值

switch.c复制
switch (choice) {
case 1:
    printf("新建\n");
    break;
case 2:
    printf("打开\n");
    break;
case 3:
case 4:
    printf("保存 / 另存为\n");
    break;
default:
    printf("无效选项\n");
}
注意:每个 case 通常需要 break 跳出,否则会"贯穿"到下一个 case(少数情况是有意为之)。

5.3 循环结构:while / do-while / for

while:先判断后执行

while.c复制
int i = 0;
while (i < 5) {
    printf("%d\n", i);
    i++;
}

do-while:先执行后判断

dowhile.c复制
int n;
do {
    printf("输入正数:");
    scanf("%d", &n);
} while (n <= 0);

for:经典计数循环

for.c复制
for (int i = 0; i < 10; i++) {
    printf("%d ", i);
}
printf("\n");

循环控制

  • break立即跳出当前循环或 switch;
  • continue跳过本次剩余语句,进入下一轮;
  • goto label;:跳转到大一通常不推荐使用,容易破坏结构。

5.4 经典案例:九九乘法表

9x9.c复制
#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 9; i++) {
        for (int j = 1; j <= i; j++)
            printf("%dx%d=%-2d ", j, i, j*i);
        printf("\n");
    }
    return 0;
}

关键:循环嵌套体现"行 × 列"二维结构——外层控制行,内层控制列。

课堂练习 · 第05章

5下面代码的输出是?
for(int i=0;i<5;i++){ if(i==2) continue; printf("%d ",i); }
6关于 switch 语句,下列说法错误的是?
06

函数与程序模块化

函数是 C 程序的基本构件——把复杂问题拆成可复用、可独立测试的小单元。

6.1 函数定义与调用

func.c复制
#include <stdio.h>

// 函数定义:返回类型 + 名字 + 参数列表 + 函数体
int add(int a, int b) {
    return a + b;
}

// 没有返回值:void
void say_hello(const char* name) {
    printf("Hello, %s!\n", name);
}

int main(void) {
    say_hello("Tom");
    int s = add(3, 4);   // 函数调用
    printf("3+4=%d\n", s);
    return 0;
}

函数声明(原型)

如果函数的定义出现在调用之后,需要先声明(告诉编译器函数签名):

prototype.c复制
int add(int, int);    // 函数原型(参数名可省略)

int main(void) {
    printf("%d\n", add(1, 2));
    return 0;
}

int add(int a, int b) { return a + b; }

6.2 参数传递:值传递 vs 地址传递

C 的函数参数默认值传递:形参是实参的副本,函数内修改形参不影响实参。

swap.c复制
void swap_wrong(int a, int b) {
    int t = a; a = b; b = t;   // 仅交换副本,原变量不变
}

void swap_ok(int *a, int *b) {
    int t = *a; *a = *b; *b = t;  // 通过地址真正交换
}

记忆:想让函数修改外部变量,必须传地址(指针)。这是后续"指针"章节的核心铺垫。

6.3 作用域与存储类别

变量类型作用域生命周期说明
局部变量所在块 { } 内块结束时销毁函数内定义的变量
全局变量从定义处到文件末程序整个运行期函数外定义,默认初值 0
static 局部所在块内整个程序运行期值在调用之间保留
static 全局当前 .c 文件整个程序运行期不暴露给其他文件
extern整个程序(多文件)整个程序运行期声明在其他文件定义的全局变量

6.4 递归函数:自己调用自己

递归 = 终止条件 + 递归步骤。三个经典例子:

factorial.c复制
long factorial(int n) {
    if (n <= 1) return 1;      // 终止条件
    return n * factorial(n - 1); // 递归步骤
}
// factorial(5) = 5*4*3*2*1 = 120
递归风险:递归太深会栈溢出。Fibonacci 的朴素递归就是反例 —— 应改用迭代或记忆化。
07

数组与字符串

数组 = 同类型元素的连续存储。字符串 = 以 '\0' 结尾的字符数组。

7.1 一维数组

array1d.c复制
int a[5];                       // 声明:5 个 int,未初始化
int b[5] = {1, 2, 3, 4, 5}; // 全部初始化
int c[5] = {0};                // 全为 0(部分初始化其余补 0)
int d[]  = {10, 20, 30};      // 自动推导长度 = 3

a[0] = 100;                  // 通过下标访问(下标从 0 开始)
printf("%d\n", a[4]);          // 最后一个元素
printf("size=%zu\n", sizeof a); // 20 (5×4)
数组越界:C 不会检查下标越界,访问 a[5]a[-1] 是未定义行为 —— 后果难以预料。

7.2 二维与多维数组

array2d.c复制
int matrix[3][4] = {              // 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");
}

7.3 字符串与常用函数

C 没有真正的"字符串变量",字符串是'\0' 结尾的 char 数组

str.c复制
char s1[] = "Hello";              // 自动加 \0,实际长度 6
char s2[10] = {'H','i','\0'}; // 手动构造

printf("%zu\n", strlen(s1));    // 5  (不计 \0)
strcpy(s2, "Hi");                 // 复制(不安全,建议用 strncpy)
strcat(s1, ", world!");          // 拼接
printf("%d\n", strcmp("abc", "abd")); // <0

// 字符分类与转换(ctype.h)
printf("%d %c\n", isalpha('A'), toupper('b')); // 非零 B

常用字符串函数速查(<string.h> / <ctype.h>)

函数作用
strlen(s)返回字符串长度(不含 \0)
strcpy(dst, src) / strncpy字符串复制
strcat(dst, src) / strncat字符串拼接
strcmp(a, b) / strncmp字典序比较
strchr(s, c) / strrchr查找字符
strstr(s, sub)查找子串
isalpha / isdigit / isspace字符分类
toupper / tolower大小写转换

指针与内存:整个 C 课程的核心难点

指针 = 存储"内存地址"的变量。掌握 *&,你就掌握了 C 的灵魂。

8.1 什么是指针

每个变量都占用一段内存,地址就是这段内存的"门牌号"。指针变量保存的就是这种地址。

ptr.c复制
int  a = 10;       // 整型变量 a,值为 10
int *p = &a;        // p 是指针,指向 a 的地址

printf("a      = %d\n",   a);     // 10
printf("&a     = %p\n",   &a);   // a 的地址
printf("p      = %p\n",   p);     // 同上(p 保存的就是 &a)
printf("*p     = %d\n",   *p);    // 通过 p 间接访问:10

*p = 99;                       // 通过指针修改 a
printf("a 现在是 %d\n", a);     // 99
a 值: 10 地址: 0x7ffd 10 0x7ffd · · · 4c p 0x..4c p 指向 a
指针 p 保存了 a 的地址,*p 即"通过地址找值"

8.2 指针运算与数组

数组名 a 在大多数表达式中会退化为指向首元素的指针,因此 a[i] 等价于 *(a+i)

ptrarr.c复制
int a[5] = {10, 20, 30, 40, 50};
int *p = a;           // 等价于 int *p = &a[0];

printf("%d\n", *(p + 2));   // 30  (p 移动 2 个 int 长度)
printf("%d\n", p[2]);        // 30  (下标写法)

p++;                    // p 现在指向 a[1]
printf("%d\n", *p);         // 20
注意:指针加减的单位是"它指向类型的大小":int *p; p+1 实际地址加 4 字节(典型 int 大小)。

8.3 动态内存:malloc / free

当数据大小在编译期未知时,使用动态分配内存:

malloc.c复制
#include <stdlib.h>

int main(void) {
    int n;
    scanf("%d", &n);

    int *p = malloc(sizeof(int) * n);  // 申请 n 个 int
    if (p == NULL) return 1;           // 必须检查失败

    for (int i = 0; i < n; i++)
        p[i] = i * i;                          // 像普通数组一样用

    free(p);                                  // 用完必须释放
    p = NULL;                             // 避免悬空指针
    return 0;
}
函数作用
malloc(size)申请 size 字节,未初始化,返回 void*
calloc(n, size)申请 n×size 字节并清零
realloc(p, new)调整已分配内存大小
free(p)释放内存(p 必须来自 malloc/calloc/realloc)

8.4 常见内存错误(务必警惕)

错误类型说明防范
野指针未初始化的指针声明时即初始化:int *p = NULL;
悬空指针指向已 free 的内存free 后立即 p = NULL;
NULL 解引用对 NULL 指针使用 *p解引用前检查
内存泄漏malloc 后忘了 free谁申请谁释放;
工具:valgrind / ASan
越界访问数组 / 缓冲区下标越界始终检查下标范围
重复释放对同一指针 free 两次free 后置 NULL
调试利器:Linux 下 valgrind --leak-check=full ./a.out;GCC/Clang 编译加 -fsanitize=address 可捕获大多数内存错误。

8.5 函数指针 & 多级指针(拓展)

funcptr.c复制
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main(void) {
    int (*fp)(int, int) = add;   // 函数指针
    printf("%d\n", fp(3, 2));   // 5  (像函数一样调用)
    fp = sub;
    printf("%d\n", fp(3, 2));   // 1
}

int **pp;   // 二级指针:指向"指向 int 的指针"

课堂练习 · 第08章(核心)

7已知 int a=5; int *p=&a;,表达式 *p 的含义是?
8关于动态内存,下列说法错误的是?
9对于 int a[5]; int *p = a;,哪个表达式等同于 a[3]
09

结构体、枚举与自定义类型

把相关数据"打包"成一个新的复合类型 —— 让代码更具表达力。

9.1 结构体(struct)

struct.c复制
#include <string.h>
#include <stdio.h>

// 定义结构体类型
struct Student {
    char  name[32];
    int   age;
    float score;
};

int main(void) {
    struct Student s1 = {"Tom", 18, 95.5};
    strcpy(s1.name, "Jerry");        // 修改成员
    s1.age = 19;

    struct Student *p = &s1;
    printf("%s %d %.1f\n", p->name, p->age, p->score);
}

访问成员的两种写法:s.age(变量)或 p->age(指针)。

9.2 typedef —— 给类型起别名

typedef.c复制
typedef struct {
    int   id;
    char  name[32];
} Student;                  // 现在 Student 是一个类型名

Student s = {1, "Alice"};  // 不再写 struct Student

9.3 枚举 enum / 共用体 union(拓展)

enum_union.c复制
enum Color { RED, GREEN, BLUE };   // 默认 0,1,2
enum Color c = GREEN;

union Data {                     // 共用一块内存,按最大成员解释
    int   i;
    float f;
    char  c[4];
};
10

文件 I/O 与预处理

让程序与磁盘交互;用宏和条件编译写出可移植、可维护的代码。

10.1 文件读写:文本与二进制

file.c复制
#include <stdio.h>

int main(void) {
    // 写文本
    FILE *fp = fopen("out.txt", "w");
    if (!fp) return 1;
    fprintf(fp, "Hello, %d\n", 42);
    fclose(fp);

    // 读文本
    char buf[128];
    fp = fopen("out.txt", "r");
    fgets(buf, sizeof buf, fp);   // 读一行
    fclose(fp);
    return 0;
}

文件打开模式速查

模式含义
"r" / "w" / "a"读 / 写(清空)/ 追加,文本模式
"rb" / "wb" / "ab"同上,二进制模式
"r+" / "w+" / "a+"读写

10.2 预处理:宏与条件编译

preproc.c复制
#define PI       3.14159
#define SQR(x)  ((x)*(x))            // 函数式宏(注意括号)
#define MAX(a,b) ((a)>(b)?(a):(b))

#ifdef DEBUG
printf("[debug] x=%d\n", x);   // 仅在 DEBUG 定义时编译
#endif

头文件保护(避免重复包含)

utils.h复制
#ifndef UTILS_H
#define UTILS_H

// 函数声明
int add(int, int);
void greet(const char*);

#endif
C23 简化:#pragma once 也能起到头文件保护的作用,被现代编译器广泛支持。
11

C 标准库速查:按头文件分类

C 标准库就是 C 语言的"自带工具箱"——按头文件使用,常用的不到 10 个。

头文件常用内容典型函数
<stdio.h>标准 I/Oprintf, scanf, fopen, fclose, fgets, fputs, fprintf, fscanf, fread, fwrite
<stdlib.h>通用工具malloc, calloc, realloc, free, rand, srand, exit, atoi, qsort, bsearch
<string.h>字符串与内存strlen, strcpy, strncpy, strcat, strcmp, strchr, strstr, memcpy, memset
<math.h>数学函数sqrt, pow, sin, cos, log, log10, fabs, ceil, floor
<time.h>时间日期time, clock, difftime, mktime, strftime
<ctype.h>字符分类isalpha, isdigit, isspace, islower, isupper, tolower, toupper
<stdbool.h>布尔类型 (C99)bool, true, false
<stdint.h>固定宽度整数int8_t, int16_t, int32_t, int64_t, uint8_t ...
<limits.h>类型极值INT_MAX, INT_MIN, CHAR_BIT ...
<float.h>浮点极值DBL_MAX, FLT_EPSILON ...
12

算法基础:查找、排序与递归

掌握 5 ~ 8 个基础算法,足以应对大一编程作业与面试入门。

12.1 顺序查找 & 二分查找

顺序查找 O(n)

linear.c复制
int linear_search(int a[], int n, int key) {
    for (int i = 0; i < n; i++)
        if (a[i] == key) return i;
    return -1;
}

二分查找 O(log n)(要求数组有序)

binary.c复制
int binary_search(int a[], int n, int key) {
    int lo = 0, hi = n - 1;
    while (lo <= hi) {
        int mid = (lo + hi) / 2;
        if (a[mid] == key)   return mid;
        else if (a[mid] < key) lo = mid + 1;
        else                 hi = mid - 1;
    }
    return -1;
}

12.2 基础排序:冒泡 / 选择 / 插入

bubble.c复制
// 冒泡排序:相邻元素两两比较交换,每轮把最大元素"冒"到末尾
void bubble_sort(int a[], int n) {
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (a[j] > a[j + 1]) {
                int t = a[j]; a[j] = a[j+1]; a[j+1] = t;
            }
}

12.3 经典递归:阶乘 / Fibonacci / 汉诺塔

fib.c复制
// 朴素递归 Fibonacci(教学示例,效率极低)
int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

// 迭代版:O(n) 时间 O(1) 空间(推荐)
long fib_iter(int n) {
    if (n < 2) return n;
    long a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        long c = a + b;
        a = b; b = c;
    }
    return b;
}
13

调试与编程规范

写出能跑的代码只是开始 —— 写出易调试、可维护的代码才是工程能力。

13.1 常见错误类型

阶段典型错误示例
编译错误语法 / 类型错误缺分号、未声明标识符、类型不匹配
链接错误找不到符号undefined reference to 'foo'
运行错误程序崩溃段错误(Segmentation fault)、除零、非法内存访问
逻辑错误结果不对边界条件 off-by-one、算法错误、== 写成 =

13.2 调试工具与方法

  • printf 调试:在关键位置打印变量值,最朴素但有效;
  • gdb / lldb:交互式调试器,支持断点、单步、查看变量、查看调用栈
  • VS Code 调试:图形界面下断点 + 变量窗口;
  • valgrind:检测内存泄漏与越界;
  • AddressSanitizer:编译加 -fsanitize=address,运行时自动报警;
  • 静态分析gcc -Wall -Wextraclang-tidy
gdb session复制
# 1. 编译带调试信息
$ gcc -g hello.c -o hello

# 2. 启动 gdb
$ gdb ./hello

(gdb) break main         # 设置断点
(gdb) run                # 运行
(gdb) next               # 单步(不进入函数)
(gdb) step               # 单步(进入函数)
(gdb) print x            # 查看变量
(gdb) continue           # 继续执行
(gdb) quit               # 退出

13.3 编程规范建议

  • 命名:变量小写、函数驼峰或下划线、常量全大写;避免单字母(除循环计数 i/j/k);
  • 缩进:统一 4 空格或 1 Tab;
  • 注释:解释"为什么"而不是"是什么";公开 API 必须有注释;
  • 函数长度:尽量不超过一屏(≈ 50 行);
  • 模块化:一个函数做一件事;高内聚低耦合;
  • 错误处理:每个系统调用、malloc、文件操作都检查返回值;
  • 避免魔数:用 #defineconst 给数值起名。
14

综合项目:从练习到作品

把零散语法整合成完整的小项目 —— 这是从"会写代码"到"能交付代码"的关键一跳。

项目一:猜数字游戏 🎲

难度 ★☆☆ · 涉及:循环 / 条件 / 随机数 / 函数

程序随机生成 1 ~ 100 的整数,玩家循环输入猜测,程序给出"大了 / 小了"提示,直到猜中。

guess.c复制
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    srand((unsigned)time(NULL));
    int target = rand() % 100 + 1;
    int guess, tries = 0;

    do {
        printf("猜一个 1-100 的数:");
        scanf("%d", &guess);
        tries++;
        if (guess > target)      printf("大了!\n");
        else if (guess < target) printf("小了!\n");
    } while (guess != target);

    printf("恭喜!用了 %d 次猜中。\n", tries);
    return 0;
}

项目二:简易计算器 🧮

难度 ★★☆ · 涉及:switch / 函数 / 函数指针

支持 + - × ÷ 与连续运算,尝试把每个运算封装成函数,再用函数指针数组调度。

项目三:通讯录管理系统 📇

难度 ★★★ · 涉及:结构体 / 数组 / 文件 / 字符串 / 排序

每个联系人含姓名、电话、邮箱;支持增删改查、按姓名排序、保存到文件、从文件加载。

项目四:学生成绩管理系统 📊

难度 ★★★★ · 综合运用

结构体数组存储多名学生;统计平均分、最高分、按成绩排序;用文件持久化。

项目五:图书管理系统 📚

难度 ★★★★★ · 完整工程

多文件工程(main.c + book.c/h + io.c/h);借阅 / 归还状态机;链表或数组两种实现可选。

综合项目学习建议
  • 从最小可用版本开始(MVP),再逐步加功能;
  • 每完成一个功能就跑一次测试,不要攒到最后;
  • git 做版本管理,每次功能提交一次;
  • 遇到 bug 先 复现,再定位,再修复;
  • 完成后写一份 README:功能列表、编译运行方法、已知问题。

C 语言速查表(Cheat Sheet)

最后一块"索引"——写代码时随手翻一翻,胜过每次去搜。

关键字(共 32 个 · C17)

autobreakcasecharconstcontinuedefaultdodoubleelseenumexternfloatforgotoifinlineintlongregisterrestrictreturnshortsignedsizeofstaticstructswitchtypedefunionunsignedvoidvolatilewhile

运算符优先级(从高到低,前几档最常用)

优先级运算符结合性
1 (最高)() [] -> .
2! ~ ++ -- + - * & (type) sizeof
3* / %
4+ -
5<< >>
6< <= > >=
7== !=
8&
9^
10|
11&&
12||
13?:
14= += -= ...
15 (最低),

常用转义字符

转义含义转义含义
\n换行\t制表符 (Tab)
\r回车\\反斜杠
\'单引号\"双引号
\0空字符\xhh十六进制字节
\ooo八进制字节\?问号(避免被识别为三字母词)

常见错误自查清单

  • if (a = b) — 应写 if (a == b)
  • scanf("%d", x) — 应写 scanf("%d", &x)
  • char s[5] = "hello"; — "hello" 需要 6 字节(含 \0
  • while (n = 0) — 赋值为 0,循环立即结束;改成 n == 0
  • int *p; *p = 10; — p 指向未初始化地址;应 p = &x;
  • ❌ 函数忘了 return,返回值未定义
  • ❌ 头文件忘记保护,导致重复定义
@media print{ #topnav,#progress,.back-top{display:none!important} .hero{padding:20px 0!important;background:none!important;color:#000!important} .hero h1{color:#000!important} .hero .sub,.hero .hero-chips{color:#333!important} section{break-inside:avoid;box-shadow:none!important;border:1px solid #ccc!important} pre{background:#f5f5f5!important;color:#000!important} }