C Primer Plus 核心笔记:第10章
核心目标:彻底理解 C 语言的内存管理、指针算术及存储模型。这是嵌入式、系统编程及高性能后端开发的基石。
一、 数组与指针 (第10章核心)
1. 数组名的“两副面孔”
-
一般情况(退化) :数组名
arr是数组首元素的地址。- 类型:
type *(常指针) - 效果:
arr等价于&arr[0]
- 类型:
-
特殊情况(不退化) :
sizeof(arr):返回整个数组占用的字节数。&arr:返回整个数组的地址(类型是数组指针type (*)[N])。
2. 指针运算与解引用
-
步长(Stride) :
p + n实际增加的物理地址字节数 =n * sizeof(指针指向的类型)。 -
万能公式:
ar[i]永远等价于*(ar + i)。- 面试趣题:
2[ar]是合法的吗?是的,因为它会被展开为*(2 + ar),等价于ar[2]。
- 面试趣题:
-
多维数组指针:
- 对于
int ar[2][3]: ar指向第0行(类型int (*)[3])。ar[0]指向第0行第0个元素(类型int *)。
- 对于
3. 函数传参的真相
-
传值还是传址? C 语言只有传值。当数组做参数时,传递的是首地址的值。
-
声明陷阱:
void func(int arr[]) { printf("%zu", sizeof(arr)); // 永远输出指针大小 (4或8),而不是数组总大小! } -
等价写法:
void func(int *ar, int n);(推荐,最直观)void func(int ar[], int n);void func(int n, int m, int ar[n][m]);(C99 变长数组 VLA)
二、 深入理解指针算术与 const
1. 指针步长规律
-
地址增量:指针自增
p++增加的字节数等于sizeof(*p)。 -
维度跨越:对于二维数组
int a[2][3]:a + 1:跳过一行(3个int,12字节)。a[0] + 1:跳过一个元素(1个int,4字节)。
2. const 修饰符全解 (面试高频)
口诀: const 往左看,修饰谁谁不变;如果在最左边,这就修饰它右边的类型。
| 语法 | 术语 | 限制操作 | 助记 |
|---|---|---|---|
const int * p | 指向常量的指针 | ❌ *p = 10 | 只能看,不能改内容 |
int const * p | 指向常量的指针 | ❌ *p = 10 | 同上 |
int * const p | 常量指针 | ❌ p = &b | 只能指这,不能指别处 |
const int * const p | 双重锁定 | ❌ 都不能改 | 地址和内容都锁死 |
3. 指针兼容性
- 安全原则:
const指针可以指向非const数据(权限缩小,安全)。 - 危险操作:普通指针指向
const数据(权限放大,报 Warning/Error)。
C Primer Plus 核心笔记:第12章 存储类别、链接与内存管理
核心目标:理解变量的“生老病死”(存储期)和“社交范围”(作用域/链接)。这是理解 C 语言模块化编程和避免内存错误的基石。
一、 三大核心维度(面试必考概念)
在 C 语言中,定义一个变量其实是在定义它的三个属性:
-
存储期 (Duration) —— 活多久?
- 静态存储期:程序一启动就存在,直到程序结束才销毁。(就像公司的老员工,一直都在)
- 自动存储期:函数调用时分配,函数结束时销毁。(就像临时工,干完活就走)
- 动态存储期:
malloc申请,free释放。(就像外包团队,按需雇佣,手动解散)
-
作用域 (Scope) —— 谁能看见?
- 块作用域:
{}里面。 - 文件作用域:整个
.c文件。
- 块作用域:
-
链接 (Linkage) —— 能否跨文件?
- 无链接:只有当前代码块能用(局部变量)。
- 内部链接:只有当前文件能用(被
static关禁闭的全局变量)。 - 外部链接:整个项目的所有文件都能用(普通的全局变量)。
二、 五种存储方案深度解析
| 方案名称 | 关键字与位置 | 存储区域 | 特点与面试考点 |
|---|---|---|---|
| 1. 自动变量 | 函数内 auto (可省略) | 栈 (Stack) | 默认状态。进函数进栈,出函数出栈。切记:不初始化时,值是随机垃圾值! |
| 2. 寄存器变量 | 函数内 register | CPU 寄存器 | 请求编译器把它存到 CPU 里以提高速度。考点:不能对它取地址 &(因为它不在内存条上)。 |
| 3. 静态块作用域 | 函数内 static | Data/BSS | “有记忆的局部变量” 。函数结束它不销毁,下次进函数它还记得上次的值。但外面的人看不见它。 |
| 4. 静态内部链接 | 函数外 static | Data/BSS | “私有全局变量” 。只有本文件能用。用于模块封装,防止污染全局命名空间。 |
| 5. 静态外部链接 | 函数外 (无关键字) | Data/BSS | “公共全局变量” 。全项目通用。其他文件使用前需用 extern 声明。 |
三、 综合代码实例:五种方案同台竞技
这段代码模拟了一个小型银行账户系统,展示了这 5 种变量的行为差异。
#include <stdio.h>
/* -------------------------------------------------------
【方案 5】静态外部链接 (全局变量)
作用:整个项目可见。
场景:比如银行的总资金池。
------------------------------------------------------- */
int Global_Pool = 1000;
/* -------------------------------------------------------
【方案 4】静态内部链接 (文件私有全局变量)
作用:只在当前文件可见,避免和其他文件冲突。
场景:本文件的操作日志计数器。
------------------------------------------------------- */
static int File_Log_Count = 0;
void transaction() {
/* ---------------------------------------------------
【方案 3】静态块作用域 (局部静态变量)
作用:函数结束不销毁,保留记忆。
场景:统计这个函数被调用了多少次。
--------------------------------------------------- */
static int call_count = 0; // 只在程序启动时初始化一次!
call_count++;
/* ---------------------------------------------------
【方案 1】自动变量 (普通局部变量)
作用:每次进来都重新创建。
场景:临时的交易额。
--------------------------------------------------- */
int temp_money = 100; // 每次调用都重置为 100
/* ---------------------------------------------------
【方案 2】寄存器变量
作用:频繁使用,请求极速访问。
场景:循环计数器。
--------------------------------------------------- */
register int i;
for(i = 0; i < 1000; i++); // 模拟耗时计算
// 修改外部变量
Global_Pool -= temp_money;
运行结果分析:
call_count从 1 变到 2:说明它活着,记住了上次的值。temp_money每次都是 100:说明它重生了,没有记忆。Global_Pool持续减少:说明它是共享的。
四、 内存布局 (面试常画图)
理解变量在哪里,就能理解为什么有的会“爆栈”,有的会“泄漏”。
- Text (代码段) :存你的代码逻辑,只读。
- Data (已初始化数据段) :存
int g = 10;或static int s = 10;。 - BSS (未初始化数据段) :存
int g;。系统会自动把这里清零。 - Heap (堆) :
malloc的地盘,由低地址向高地址增长。 - Stack (栈) :
auto变量的地盘,由高地址向低地址增长。栈溢出 (Stack Overflow) 通常就是递归太深把这里撑爆了。
五、 求职实战:工程易错点与陷阱 (Job Hunter Special)
1. static 的初始化陷阱
void func() {
static int x = 10; // 这行代码在汇编层面只跑一次!
x++;
printf("%d", x);
}
- 误区:新手以为每次调用
func,x都会变回 10。 - 真相:
x = 10是编译时决定的。程序运行时,这行代码会被直接跳过。
2. 局部变量遮蔽 (Shadowing)
int x = 100; // 全局变量
void func() {
int x = 5; // 局部变量 x "遮蔽" 了全局的 x
printf("%d", x); // 输出 5
}
- 工程规范:尽量避免局部变量和全局变量重名,这非常容易导致 Bug。
3. extern 的声明 vs 定义
这是多文件编译报错的重灾区。
- 定义 (Definition) :
int g_val = 10;(真正在内存里开房子,只能写一次) - 声明 (Declaration) :
extern int g_val;(告诉编译器去别处找这个名字,可以写多次) - 错误示范:在头文件
.h里写int g_val = 10;。如果这个头文件被两个.c包含,连接器会报错“重复定义 (Multiple Definition)”。
4. 永远不要返回 auto 变量的地址 (再强调一遍)
这是 C 语言最严重的内存错误之一。
char* get_str() {
char str[] = "Hello"; // str 在栈上
return str; // ❌ 函数结束,栈内存释放,str 指向垃圾
}
修正方案:
- 使用
static:static char str[] = "Hello";(变成了静态区,安全) - 使用
malloc:char *str = malloc(10);(变成了堆内存,安全,但记得 free) - 返回字符串常量:
return "Hello";(位于只读常量区,安全)
5. free 之后的悬空指针
int *p = malloc(sizeof(int));
free(p);
// 此时 p 依然保存着那个地址,但那个房子已经归还给系统了。
*p = 10; // ❌ 非法访问 (Use After Free),可能导致极其隐蔽的崩溃
工程习惯:free(p); p = NULL;
💡 总结建议
- 小项目:
auto走天下,偶尔用malloc。 - 大项目/嵌入式:大量使用
static(内部链接) 来隐藏模块细节;小心使用malloc(防止碎片和泄漏);用extern慎重地共享全局状态。