写在前面:这篇文章是我学C语言这几年,把核心知识重新梳理了一遍。说实话,C语言是我学的第一门编程语言,当年在大学课堂上被指针虐得死去活来,考试前一天还在图书馆死磕
**p和*p的区别。C语言的指针、内存管理、位运算这些东西,才是真正决定你能不能写好底层代码的关键。这篇文章把C语言的核心知识体系整理了出来,面试常问的、工作中真正用到的、初学者最容易踩坑的地方,我都尽量讲透了。全文大概一万多字,建议先收藏再看,当字典用也行。
文章目录
-
- 一、C语言到底学什么?先给你一张全景图
- 二、基础语法:万丈高楼平地起
-
- [2.1 数据类型:C语言的骨架](#2.1 数据类型:C语言的骨架)
- [2.2 运算符:那些容易踩的坑](#2.2 运算符:那些容易踩的坑)
- [2.3 sizeof不是函数](#2.3 sizeof不是函数)
- 三、指针与内存:C语言的灵魂
-
- [3.1 指针基础:地址与解引用](#3.1 指针基础:地址与解引用)
- [3.2 指针的级别:多级指针](#3.2 指针的级别:多级指针)
- [3.3 指针与数组:形同神不同](#3.3 指针与数组:形同神不同)
- [3.4 函数指针:C语言的高阶特性](#3.4 函数指针:C语言的高阶特性)
- 四、动态内存管理:malloc与free的艺术
-
- [4.1 为什么需要动态内存](#4.1 为什么需要动态内存)
- [4.2 malloc、calloc、realloc的区别](#4.2 malloc、calloc、realloc的区别)
- [4.3 内存泄漏:C程序的头号杀手](#4.3 内存泄漏:C程序的头号杀手)
- 五、数组与字符串:C语言的基石
-
- [5.1 字符数组 vs 字符串](#5.1 字符数组 vs 字符串)
- [5.2 常用字符串函数](#5.2 常用字符串函数)
- 六、结构体、联合体与枚举
-
- [6.1 struct:C语言的面向对象](#6.1 struct:C语言的面向对象)
- [6.2 内存对齐:struct的大小不是简单相加](#6.2 内存对齐:struct的大小不是简单相加)
- [6.3 union:共享内存](#6.3 union:共享内存)
- [6.4 enum与typedef](#6.4 enum与typedef)
- 七、文件IO:与外部世界交互
-
- [7.1 文件读写基础](#7.1 文件读写基础)
- [7.2 二进制文件读写](#7.2 二进制文件读写)
- 八、预处理器与多文件编译
-
- [8.1 宏定义:编译时的文本替换](#8.1 宏定义:编译时的文本替换)
- [8.2 头文件与多文件编译](#8.2 头文件与多文件编译)
- 九、面试高频考点汇总
- 十、C语言学习路线与资源推荐
-
- [10.1 推荐学习路线](#10.1 推荐学习路线)
- [10.2 推荐学习资源](#10.2 推荐学习资源)
- 写在最后
- 参考资料

一、C语言到底学什么?先给你一张全景图
很多人问:"C语言要学到什么程度才算合格?"
这个问题其实没有标准答案,取决于你的方向。做嵌入式和底层开发,C语言就是你的吃饭工具;做后端开发,C语言是理解操作系统和内存管理的基石。
不管哪个方向,C语言的核心知识可以分成这六大板块:
#mermaid-svg-l2i6nbhwLLwQrPIf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l2i6nbhwLLwQrPIf .error-icon{fill:#552222;}#mermaid-svg-l2i6nbhwLLwQrPIf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l2i6nbhwLLwQrPIf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l2i6nbhwLLwQrPIf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l2i6nbhwLLwQrPIf .marker.cross{stroke:#333333;}#mermaid-svg-l2i6nbhwLLwQrPIf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l2i6nbhwLLwQrPIf p{margin:0;}#mermaid-svg-l2i6nbhwLLwQrPIf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-l2i6nbhwLLwQrPIf .cluster-label text{fill:#333;}#mermaid-svg-l2i6nbhwLLwQrPIf .cluster-label span{color:#333;}#mermaid-svg-l2i6nbhwLLwQrPIf .cluster-label span p{background-color:transparent;}#mermaid-svg-l2i6nbhwLLwQrPIf .label text,#mermaid-svg-l2i6nbhwLLwQrPIf span{fill:#333;color:#333;}#mermaid-svg-l2i6nbhwLLwQrPIf .node rect,#mermaid-svg-l2i6nbhwLLwQrPIf .node circle,#mermaid-svg-l2i6nbhwLLwQrPIf .node ellipse,#mermaid-svg-l2i6nbhwLLwQrPIf .node polygon,#mermaid-svg-l2i6nbhwLLwQrPIf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-l2i6nbhwLLwQrPIf .rough-node .label text,#mermaid-svg-l2i6nbhwLLwQrPIf .node .label text,#mermaid-svg-l2i6nbhwLLwQrPIf .image-shape .label,#mermaid-svg-l2i6nbhwLLwQrPIf .icon-shape .label{text-anchor:middle;}#mermaid-svg-l2i6nbhwLLwQrPIf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-l2i6nbhwLLwQrPIf .rough-node .label,#mermaid-svg-l2i6nbhwLLwQrPIf .node .label,#mermaid-svg-l2i6nbhwLLwQrPIf .image-shape .label,#mermaid-svg-l2i6nbhwLLwQrPIf .icon-shape .label{text-align:center;}#mermaid-svg-l2i6nbhwLLwQrPIf .node.clickable{cursor:pointer;}#mermaid-svg-l2i6nbhwLLwQrPIf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-l2i6nbhwLLwQrPIf .arrowheadPath{fill:#333333;}#mermaid-svg-l2i6nbhwLLwQrPIf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-l2i6nbhwLLwQrPIf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-l2i6nbhwLLwQrPIf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l2i6nbhwLLwQrPIf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-l2i6nbhwLLwQrPIf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l2i6nbhwLLwQrPIf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-l2i6nbhwLLwQrPIf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-l2i6nbhwLLwQrPIf .cluster text{fill:#333;}#mermaid-svg-l2i6nbhwLLwQrPIf .cluster span{color:#333;}#mermaid-svg-l2i6nbhwLLwQrPIf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-l2i6nbhwLLwQrPIf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-l2i6nbhwLLwQrPIf rect.text{fill:none;stroke-width:0;}#mermaid-svg-l2i6nbhwLLwQrPIf .icon-shape,#mermaid-svg-l2i6nbhwLLwQrPIf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l2i6nbhwLLwQrPIf .icon-shape p,#mermaid-svg-l2i6nbhwLLwQrPIf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-l2i6nbhwLLwQrPIf .icon-shape rect,#mermaid-svg-l2i6nbhwLLwQrPIf .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l2i6nbhwLLwQrPIf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-l2i6nbhwLLwQrPIf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-l2i6nbhwLLwQrPIf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} C语言核心知识体系
基础语法
指针与内存
函数与程序结构
数组与字符串
结构体与联合体
文件IO与预处理器
数据类型与变量
运算符与表达式
流程控制
指针基础
指针与数组
动态内存管理
函数定义与调用
递归
作用域与生命周期
一维/二维数组
字符串操作
数组与指针
struct结构体
union联合体
enum枚举与typedef
文件读写
宏定义与条件编译
头文件与多文件编译
下面我们一个一个来。
二、基础语法:万丈高楼平地起
2.1 数据类型:C语言的骨架
C语言的数据类型比Java少得多,但也正因如此,你需要更深入地理解它们在内存中的表示。
| 数据类型 | 字节数(32位/64位) | 取值范围 | 典型用途 |
|---|---|---|---|
char |
1 / 1 | -128 ~ 127 或 0 ~ 255 | 字符、小整数 |
short |
2 / 2 | -32768 ~ 32767 | 节省内存的整数 |
int |
4 / 4 | ±21亿 | 通用整数(最常用) |
long |
4 / 8 | 平台相关 | 大整数、时间戳 |
long long |
8 / 8 | 非常大 | 超大整数 |
float |
4 / 4 | 精度6-7位 | 不推荐用于精确计算 |
double |
8 / 8 | 精度15-16位 | 科学计算 |
void |
0 / 0 | 无 | 无类型指针、不返回值 |
踩坑提醒 :C语言中
char到底是有符号还是无符号,由编译器决定 。GCC默认有符号,但某些嵌入式编译器默认无符号。跨平台代码一定要用signed char或unsigned char明确指定。
c
#include <stdio.h>
#include <limits.h>
int main() {
// 坑1:整数溢出(C语言不会自动抛异常!)
int max = INT_MAX;
printf("INT_MAX = %d\n", max); // 2147483647
printf("INT_MAX + 1 = %d\n", max + 1); // -2147483648 溢出!
// 坑2:char的符号问题
char c = 200; // 200 > 127,有符号char会变成负数
printf("char 200 = %d\n", c); // -56(有符号)或 200(无符号)
// 坑3:浮点数精度
float f = 0.1f;
printf("0.1f == 0.1 ? %s\n", f == 0.1 ? "true" : "false"); // false!
// float和double的精度不同,比较永远不相等
return 0;
}
运行结果:
INT_MAX = 2147483647
INT_MAX + 1 = -2147483648
char 200 = -56
0.1f == 0.1 ? false
2.2 运算符:那些容易踩的坑
C语言的运算符有几十个, precedence(优先级)规则复杂得让人头疼。
c
#include <stdio.h>
int main() {
int a = 5;
// 坑1:i++ 和 ++i 的区别
int i = 5;
int j = i++; // j=5, i=6(先用后加)
int k = ++i; // k=7, i=7(先加后用)
printf("j=%d, k=%d, i=%d\n", j, k, i); // j=5, k=7, i=7
// 坑2:运算符优先级(经典面试题)
int result = a << 2 + 1; // 等价于 a << (2+1),不是 (a<<2)+1
printf("a << 2 + 1 = %d\n", result); // 5 << 3 = 40
// 坑3:% 和负数
printf("-7 %% 3 = %d\n", -7 % 3); // -1(C99标准,结果与被除数同号)
printf("7 %% -3 = %d\n", 7 % -3); // 1
// 坑4:逻辑与短路
int x = 0, y = 0;
if (x++ && y++) {
// x++ 为0(假),短路,y++不执行
}
printf("x=%d, y=%d\n", x, y); // x=1, y=0
return 0;
}
2.3 sizeof不是函数
很多人以为sizeof是一个函数,其实它是编译器运算符,在编译时就确定了结果。
c
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
// sizeof在编译时确定,不运行任何代码
printf("sizeof(int) = %zu\n", sizeof(int)); // 4
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 20(5个int)
printf("sizeof(arr)/sizeof(arr[0]) = %zu\n",
sizeof(arr)/sizeof(arr[0])); // 5(数组元素个数)
// 指针退化:数组传参后变成指针
int *p = arr;
printf("sizeof(p) = %zu\n", sizeof(p)); // 8(64位指针)
printf("sizeof(*p) = %zu\n", sizeof(*p)); // 4(一个int)
return 0;
}
经验之谈 :
sizeof是C语言中最容易被误解的运算符之一。面试官经常问"数组作为函数参数时,sizeof能得到数组大小吗?"答案是不能,因为数组传参会退化为指针。
三、指针与内存:C语言的灵魂
指针是C语言最核心、最强大、也最容易出问题的特性。搞懂指针,C语言就懂了一大半。
3.1 指针基础:地址与解引用
c
#include <stdio.h>
int main() {
int a = 42;
int *p = &a; // p指向a的地址
printf("a的值: %d\n", a); // 42
printf("a的地址: %p\n", &a); // 0x7ffd...
printf("p的值: %p\n", p); // 0x7ffd...(和&a一样)
printf("*p的值: %d\n", *p); // 42(解引用,通过地址取值)
// 通过指针修改值
*p = 100;
printf("修改后 a = %d\n", a); // 100
return 0;
}
核心概念:
&a:取地址运算符,获取变量a的内存地址*p:解引用运算符,获取指针p指向地址上的值p:指针变量本身,存储的是一个地址
3.2 指针的级别:多级指针
c
#include <stdio.h>
int main() {
int a = 42;
int *p = &a; // 一级指针
int **pp = &p; // 二级指针(指向指针的指针)
int ***ppp = &pp; // 三级指针
printf("a = %d\n", ***ppp); // 42(三级解引用)
// 实际应用:在函数中修改指针
// 二级指针最常见的用途就是"让函数修改调用者的指针"
return 0;
}
面试高频:面试官最爱问"为什么要用二级指针?"答案是为了在函数内部修改调用者的指针。比如动态内存分配函数,你需要把分配好的地址传回给调用者,就必须传指针的地址(二级指针)。
3.3 指针与数组:形同神不同
c
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 数组名就是首元素地址
// 指针算术
printf("*(p+0) = %d\n", *(p + 0)); // 10
printf("*(p+1) = %d\n", *(p + 1)); // 20
printf("*(p+2) = %d\n", *(p + 2)); // 30
printf("p[3] = %d\n", p[3]); // 40(p[i]等价于*(p+i))
// 数组指针 vs 指针数组
int *ptr_arr[5]; // 指针数组:5个指针,每个指向一个int
int (*arr_ptr)[5]; // 数组指针:1个指针,指向一个有5个int的数组
printf("sizeof(ptr_arr) = %zu\n", sizeof(ptr_arr)); // 40(5个指针)
printf("sizeof(arr_ptr) = %zu\n", sizeof(arr_ptr)); // 8(1个指针)
return 0;
}
指针数组 vs 数组指针------这是C语言最容易搞混的两个概念:
| 名称 | 声明 | 含义 | 类比 |
|---|---|---|---|
| 指针数组 | int *arr[5] |
数组中每个元素是一个指针 | 5个人,每人拿着一张地图 |
| 数组指针 | int (*arr)[5] |
一个指针,指向整个数组 | 1个人,拿着一张5个房间的地图 |
3.4 函数指针:C语言的高阶特性
c
#include <stdio.h>
// 普通函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
// 函数指针类型
typedef int (*OpFunc)(int, int);
// 回调函数模式
int calculate(int a, int b, OpFunc op) {
return op(a, b);
}
int main() {
// 函数指针数组(跳表模式)
OpFunc ops[] = {add, sub, mul};
printf("3 + 4 = %d\n", calculate(3, 4, ops[0])); // 7
printf("3 - 4 = %d\n", calculate(3, 4, ops[1])); // -1
printf("3 * 4 = %d\n", calculate(3, 4, ops[2])); // 12
return 0;
}
实战场景:函数指针在嵌入式开发中用得非常多。比如STM32的HAL库,中断处理、回调机制、状态机实现都依赖函数指针。Linux内核中的驱动框架也大量使用函数指针实现多态。
四、动态内存管理:malloc与free的艺术
4.1 为什么需要动态内存
c
#include <stdio.h>
#include <stdlib.h>
int main() {
// 静态分配:编译时确定大小
int arr[100]; // 固定100个元素,可能浪费或不够
// 动态分配:运行时确定大小
int n;
printf("请输入元素个数: ");
scanf("%d", &n);
int *dynamic_arr = (int *)malloc(n * sizeof(int));
if (dynamic_arr == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 使用动态数组
for (int i = 0; i < n; i++) {
dynamic_arr[i] = i * 10;
}
// 一定要释放!
free(dynamic_arr);
dynamic_arr = NULL; // 防止野指针
return 0;
}
4.2 malloc、calloc、realloc的区别
| 函数 | 功能 | 特点 | 使用场景 |
|---|---|---|---|
malloc(size) |
分配size字节 | 不初始化内存(垃圾值) | 通用分配 |
calloc(n, size) |
分配n个size字节 | 初始化为0 | 需要零初始化的数组 |
realloc(ptr, size) |
调整已分配内存大小 | 保留原数据,可能移动位置 | 动态扩容 |
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// malloc:不初始化
int *p1 = (int *)malloc(5 * sizeof(int));
printf("malloc: ");
for (int i = 0; i < 5; i++) printf("%d ", p1[i]); // 垃圾值
// calloc:初始化为0
int *p2 = (int *)calloc(5, sizeof(int));
printf("\ncalloc: ");
for (int i = 0; i < 5; i++) printf("%d ", p2[i]); // 全是0
// realloc:扩容
int *p3 = (int *)malloc(3 * sizeof(int));
p3[0] = 1; p3[1] = 2; p3[2] = 3;
p3 = (int *)realloc(p3, 6 * sizeof(int)); // 扩大到6个
printf("\nrealloc: ");
for (int i = 0; i < 6; i++) printf("%d ", p3[i]); // 1 2 3 0 0 0
free(p1); free(p2); free(p3);
return 0;
}
4.3 内存泄漏:C程序的头号杀手
c
#include <stdlib.h>
// 典型的内存泄漏场景
void memory_leak_example() {
int *p = (int *)malloc(100 * sizeof(int));
// ... 使用p ...
// 忘记free(p)!每次调用这个函数都会泄漏400字节
}
// 正确做法
void correct_example() {
int *p = (int *)malloc(100 * sizeof(int));
if (p == NULL) return; // 检查分配是否成功
// ... 使用p ...
free(p); // 释放
p = NULL; // 置空,防止野指针
}
踩坑提醒 :我见过一个线上服务因为内存泄漏,运行3天后OOM被kill。排查了半天,最后发现是一个工具函数里malloc了内存但在某个错误分支上忘记free。从那以后我养成了一个习惯------每次写malloc就立刻写对应的free,哪怕中间隔了100行代码。
五、数组与字符串:C语言的基石
5.1 字符数组 vs 字符串
C语言没有真正的"字符串类型",字符串本质上是以\0结尾的字符数组。
c
#include <stdio.h>
#include <string.h>
int main() {
// 方式1:字符数组(可以修改)
char str1[] = "hello"; // 编译器自动在末尾加'\0'
str1[0] = 'H'; // 可以修改
printf("%s\n", str1); // Hello
// 方式2:字符指针(指向字符串常量,不可修改!)
char *str2 = "hello";
// str2[0] = 'H'; // 未定义行为!可能段错误
// 因为"hello"是字符串常量,存储在只读数据段
// 方式3:sizeof vs strlen
char str3[] = "hello";
printf("sizeof(str3) = %zu\n", sizeof(str3)); // 6(包含'\0')
printf("strlen(str3) = %zu\n", strlen(str3)); // 5(不包含'\0')
return 0;
}
踩坑提醒 :
char *str = "hello"中的"hello"是字符串常量,存储在只读内存区域。试图修改它会导致未定义行为(Undefined Behavior),在Linux上通常是段错误(Segmentation Fault)。这个坑我见过太多人踩了。
5.2 常用字符串函数
c
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[50];
// strcpy:复制(不安全,可能越界)
strcpy(dest, src);
printf("strcpy: %s\n", dest);
// strncpy:安全复制(指定最大长度)
strncpy(dest, src, 5);
dest[5] = '\0'; // 手动添加'\0'!
printf("strncpy: %s\n", dest); // Hello
// strcat:拼接(不安全)
strcat(dest, ", C Language");
printf("strcat: %s\n", dest);
// strcmp:比较
printf("strcmp(\"abc\", \"abd\") = %d\n", strcmp("abc", "abd")); // 负数
printf("strcmp(\"abc\", \"abc\") = %d\n", strcmp("abc", "abc")); // 0
printf("strcmp(\"abd\", \"abc\") = %d\n", strcmp("abd", "abc")); // 正数
// sprintf:格式化到字符串
char buf[100];
sprintf(buf, "name=%s, age=%d, score=%.1f", "Tom", 20, 95.5);
printf("sprintf: %s\n", buf);
// strtok:分割字符串
char sentence[] = "C,language,is,powerful";
char *token = strtok(sentence, ",");
while (token != NULL) {
printf("token: %s\n", token);
token = strtok(NULL, ",");
}
return 0;
}
运行结果:
strcpy: Hello, World!
strncpy: Hello
strcat: Hello, C Language
strcmp("abc", "abd") = -1
strcmp("abc", "abc") = 0
strcmp("abd", "abc") = 1
sprintf: name=Tom, age=20, score=95.5
token: C
token: language
token: is
token: powerful
六、结构体、联合体与枚举
6.1 struct:C语言的面向对象
C语言没有class,但struct可以实现类似的功能。
c
#include <stdio.h>
#include <string.h>
// 定义结构体
typedef struct {
char name[50];
int age;
float score;
} Student;
// 结构体包含指针(动态内存)
typedef struct {
char *name;
int *grades;
int grade_count;
} StudentDynamic;
int main() {
// 基本使用
Student s1 = {"Alice", 20, 95.5f};
Student s2;
strcpy(s2.name, "Bob");
s2.age = 21;
s2.score = 88.0f;
printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", s1.name, s1.age, s1.score);
// 结构体指针
Student *p = &s1;
printf("通过指针: %s, %d\n", p->name, p->age); // -> 等价于 (*p).name
// 结构体数组
Student class1[3] = {
{"Alice", 20, 95.5},
{"Bob", 21, 88.0},
{"Charlie", 19, 92.3}
};
for (int i = 0; i < 3; i++) {
printf("%s: %.1f\n", class1[i].name, class1[i].score);
}
return 0;
}
6.2 内存对齐:struct的大小不是简单相加
c
#include <stdio.h>
// 默认对齐
struct A {
char c; // 1字节
// 3字节填充
int i; // 4字节
}; // 总共8字节
struct B {
int i; // 4字节
char c; // 1字节
// 3字节填充
}; // 总共8字节
struct C {
char c; // 1字节
char c2; // 1字节
// 2字节填充
int i; // 4字节
}; // 总共8字节
struct D {
char c; // 1字节
// 7字节填充
double d; // 8字节
}; // 总共16字节
int main() {
printf("sizeof(A) = %zu\n", sizeof(struct A)); // 8
printf("sizeof(B) = %zu\n", sizeof(struct B)); // 8
printf("sizeof(C) = %zu\n", sizeof(struct C)); // 8
printf("sizeof(D) = %zu\n", sizeof(struct D)); // 16
// 优化:按大小排序成员,减少填充
struct Optimized {
double d; // 8字节
int i; // 4字节
char c; // 1字节
char c2; // 1字节
};
printf("sizeof(Optimized) = %zu\n", sizeof(struct Optimized)); // 16 → 16
return 0;
}
面试高频:内存对齐是C语言面试的高频考点。面试官会给你一个struct,让你算sizeof的结果。记住规则:每个成员的偏移量必须是自身大小的整数倍,整个struct的大小必须是最大成员大小的整数倍。
6.3 union:共享内存
c
#include <stdio.h>
typedef union {
int i;
float f;
char bytes[4];
} Data;
int main() {
Data d;
d.i = 0x12345678;
// 所有成员共享同一块内存
printf("int: 0x%x\n", d.i);
printf("float: %f\n", d.f);
printf("bytes: %02x %02x %02x %02x\n",
d.bytes[0], d.bytes[1], d.bytes[2], d.bytes[3]);
// 实际应用:判断大小端
if (d.bytes[0] == 0x78) {
printf("小端模式(Little Endian)\n");
} else {
printf("大端模式(Big Endian)\n");
}
return 0;
}
6.4 enum与typedef
c
#include <stdio.h>
// 枚举:定义一组命名常量
typedef enum {
STATUS_OK = 0,
STATUS_ERROR = -1,
STATUS_PENDING = 1,
STATUS_TIMEOUT = 2
} StatusCode;
// 函数指针类型
typedef int (*CompareFunc)(const void *, const void *);
int main() {
StatusCode status = STATUS_OK;
switch (status) {
case STATUS_OK:
printf("操作成功\n");
break;
case STATUS_ERROR:
printf("操作失败\n");
break;
case STATUS_PENDING:
printf("等待中\n");
break;
default:
printf("未知状态: %d\n", status);
}
return 0;
}
七、文件IO:与外部世界交互
7.1 文件读写基础
c
#include <stdio.h>
#include <stdlib.h>
int main() {
// 写入文件
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
fprintf(fp, "Hello, C Language!\n");
fprintf(fp, "这是一个文件写入示例\n");
fclose(fp);
// 读取文件
fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
7.2 二进制文件读写
c
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int id;
char name[50];
float score;
} Record;
int main() {
// 写入二进制文件
FILE *fp = fopen("data.bin", "wb");
if (!fp) return 1;
Record records[] = {
{1, "Alice", 95.5f},
{2, "Bob", 88.0f},
{3, "Charlie", 92.3f}
};
fwrite(records, sizeof(Record), 3, fp);
fclose(fp);
// 读取二进制文件
fp = fopen("data.bin", "rb");
if (!fp) return 1;
Record r;
while (fread(&r, sizeof(Record), 1, fp) == 1) {
printf("ID: %d, Name: %s, Score: %.1f\n", r.id, r.name, r.score);
}
fclose(fp);
return 0;
}
八、预处理器与多文件编译
8.1 宏定义:编译时的文本替换
c
#include <stdio.h>
// 普通宏
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 带do-while(0)的多语句宏(防止if/else问题)
#define SWAP(a, b) do { \
(a) ^= (b); \
(b) ^= (a); \
(a) ^= (b); \
} while(0)
// 条件编译
#define DEBUG
int main() {
printf("PI = %f\n", PI);
printf("MAX(3, 5) = %d\n", MAX(3, 5));
int x = 10, y = 20;
SWAP(x, y);
printf("SWAP后: x=%d, y=%d\n", x, y);
// 条件编译
#ifdef DEBUG
printf("调试模式:x=%d, y=%d\n", x, y);
#endif
// 宏的陷阱
int a = 1;
int result = MAX(a++, 10); // a被递增两次!
printf("result=%d, a=%d\n", result, a); // result=10, a=3
return 0;
}
踩坑提醒 :带副作用的参数传给宏是C语言的经典陷阱。
MAX(a++, 10)会展开成((a++) > (10) ? (a++) : (10)),a被递增了两次。所以宏的参数不要有副作用 (如++、--、函数调用)。
8.2 头文件与多文件编译
c
// math_utils.h
#ifndef MATH_UTILS_H // 头文件保护(防止重复包含)
#define MATH_UTILS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
// math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
// main.c
#include <stdio.h>
#include "math_utils.h"
int main() {
printf("3 + 4 = %d\n", add(3, 4));
printf("3 * 4 = %d\n", multiply(3, 4));
return 0;
}
// 编译命令:
// gcc -o program main.c math_utils.c
九、面试高频考点汇总
下面是C语言面试中出现频率最高的知识点。
Q1:指针和数组的区别?
A:数组名在大多数情况下会退化为指向首元素的指针,但有两个关键区别:
sizeof不同:sizeof(arr)是整个数组的大小,sizeof(p)是指针的大小- 数组名是常量(不能
arr++),指针是变量(可以p++) - 数组在定义时分配连续内存,指针可以指向任意地址
Q2:malloc和new的区别?
A :malloc是C标准库函数,只分配内存不初始化;new是C++运算符,分配内存并调用构造函数。malloc返回void*需要强转,new返回正确类型。malloc失败返回NULL,new失败抛异常。配套释放分别是free和delete。
Q3:什么是野指针?如何避免?
A:野指针是指向已释放内存或未初始化内存的指针。避免方法:
- 指针声明时初始化为NULL
- free后立即置NULL
- 不返回局部变量的地址
- 使用前检查是否为NULL
Q4:strcpy和strncpy的区别?
A :strcpy不检查目标缓冲区大小,可能缓冲区溢出;strncpy指定最大复制长度,更安全。但strncpy不会自动添加\0,如果源字符串长度>=n,需要手动添加。推荐使用strlcpy(非标准但更安全)或snprintf。
Q5:大端和小端的区别?如何判断?
A :大端(Big Endian)高位字节存储在低地址,小端(Little Endian)低位字节存储在低地址。判断方法:用一个int存0x12345678,取其首字节,如果是0x78就是小端,0x12就是大端。x86架构是小端,ARM可以配置。
Q6:static关键字的作用?
A :static有三个作用:
- 局部静态变量:函数内声明,只初始化一次,生命周期贯穿程序运行
- 文件作用域:修饰全局变量或函数,限制在本文件内可见(内部链接)
- C++类静态成员:属于类而非对象
十、C语言学习路线与资源推荐
10.1 推荐学习路线
第1周:基础语法(变量、运算符、流程控制、函数)
第2周:数组与字符串(一维/二维数组、字符串函数)
第3周:指针(指针基础、指针与数组、函数指针)
第4周:内存管理(malloc/free、堆栈区别、内存泄漏)
第5周:结构体(struct、union、enum、内存对齐)
第6周:文件IO(文本文件、二进制文件、预处理器)
第7周:数据结构(链表、栈、队列、排序算法)
第8周:综合项目(学生管理系统/简易数据库/网络聊天室)
10.2 推荐学习资源
| 资源 | 说明 |
|---|---|
| 《C Primer Plus》 | 入门首选,讲解详细,适合零基础 |
| 《C专家编程》 | 进阶必读,深入理解C语言的底层机制 |
| 《C陷阱与缺陷》 | 避坑指南,专门讲C语言中容易出错的地方 |
| LeetCode | 用C刷题,巩固数据结构和算法 |
写在最后
C语言是计算机科学教育的基石。不管你以后做Java、Python还是Go,理解C语言的指针、内存管理、底层IO,都能让你对计算机有更深的理解。
我见过太多人,Java框架用得很溜,但不知道malloc和free是怎么工作的,不知道一个对象在内存中到底占多少字节,不知道为什么数组下标越界不会报错但程序行为诡异。C语言教会你的不是语法,而是对计算机底层的理解。
这篇文章涵盖了C语言的核心知识点,但学编程最重要的是动手实践。建议你今天就把这篇文章里的代码都编译运行一遍,遇到编译错误就查,遇到段错误就用gdb调试,这比看十篇文章都有用。
互动话题 :你在学C语言的过程中,哪个知识点让你印象最深或者踩过最深的坑?是指针的**p和(*p)搞混了,还是malloc忘记free导致内存泄漏?欢迎在评论区分享你的经历,大家一起交流!
如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持!后续我会持续更新C语言进阶系列文章,包括数据结构实现、Linux系统编程、嵌入式开发实战等。
本文为C语言全面教学系列的第一篇,后续文章正在撰写中,关注我不迷路 👇