C Primer Plus 学习DAY1

26 阅读7分钟

C Primer Plus 核心笔记:第10章

核心目标:彻底理解 C 语言的内存管理、指针算术及存储模型。这是嵌入式、系统编程及高性能后端开发的基石。


一、 数组与指针 (第10章核心)

1. 数组名的“两副面孔”

  • 一般情况(退化) :数组名 arr 是数组首元素的地址

    • 类型:type * (常指针)
    • 效果:arr 等价于 &arr[0]
  • 特殊情况(不退化)

    1. sizeof(arr):返回整个数组占用的字节数。
    2. &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 语言中,定义一个变量其实是在定义它的三个属性:

  1. 存储期 (Duration) —— 活多久?

    • 静态存储期:程序一启动就存在,直到程序结束才销毁。(就像公司的老员工,一直都在)
    • 自动存储期:函数调用时分配,函数结束时销毁。(就像临时工,干完活就走)
    • 动态存储期malloc 申请,free 释放。(就像外包团队,按需雇佣,手动解散)
  2. 作用域 (Scope) —— 谁能看见?

    • 块作用域{} 里面。
    • 文件作用域:整个 .c 文件。
  3. 链接 (Linkage) —— 能否跨文件?

    • 无链接:只有当前代码块能用(局部变量)。
    • 内部链接:只有当前文件能用(被 static 关禁闭的全局变量)。
    • 外部链接:整个项目的所有文件都能用(普通的全局变量)。

二、 五种存储方案深度解析

方案名称关键字与位置存储区域特点与面试考点
1. 自动变量函数内 auto (可省略)栈 (Stack)默认状态。进函数进栈,出函数出栈。切记:不初始化时,值是随机垃圾值!
2. 寄存器变量函数内 registerCPU 寄存器请求编译器把它存到 CPU 里以提高速度。考点:不能对它取地址 &(因为它不在内存条上)。
3. 静态块作用域函数内 staticData/BSS“有记忆的局部变量” 。函数结束它不销毁,下次进函数它还记得上次的值。但外面的人看不见它。
4. 静态内部链接函数外 staticData/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;

运行结果分析:

  1. call_count 从 1 变到 2:说明它活着,记住了上次的值。
  2. temp_money 每次都是 100:说明它重生了,没有记忆。
  3. Global_Pool 持续减少:说明它是共享的。

四、 内存布局 (面试常画图)

理解变量在哪里,就能理解为什么有的会“爆栈”,有的会“泄漏”。

  1. Text (代码段) :存你的代码逻辑,只读
  2. Data (已初始化数据段) :存 int g = 10;static int s = 10;
  3. BSS (未初始化数据段) :存 int g;。系统会自动把这里清零
  4. Heap (堆)malloc 的地盘,由低地址向高地址增长。
  5. Stack (栈)auto 变量的地盘,由高地址向低地址增长。栈溢出 (Stack Overflow) 通常就是递归太深把这里撑爆了。

五、 求职实战:工程易错点与陷阱 (Job Hunter Special)

1. static 的初始化陷阱

 void func() {
     static int x = 10; // 这行代码在汇编层面只跑一次!
     x++;
     printf("%d", x);
 }
  • 误区:新手以为每次调用 funcx 都会变回 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 指向垃圾
 }

修正方案

  1. 使用 static: static char str[] = "Hello"; (变成了静态区,安全)
  2. 使用 malloc: char *str = malloc(10); (变成了堆内存,安全,但记得 free)
  3. 返回字符串常量: 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 慎重地共享全局状态。