第一章:C语言基础回顾——指针的本质与内存模型
说实话,很多学C/C++的朋友,最后都卡在指针上。我见过太多人,写了几年代码,一提到指针还是含糊其辞。其实指针没那么玄乎,它就是一个数字——一个存放内存地址的数字。你想想看,内存就像一条长长的街道,每个门牌号就是一个地址。指针就是那个写着门牌号的小纸条。
1.1 指针的本质:内存地址的化身
指针变量存的是什么?是另一个变量的地址。就这么简单。我刚开始学的时候,总把指针和它指向的值搞混。后来我养成一个习惯:看到 int *p,我就默念「p是一个存放int类型变量地址的变量」。
核心理解:指针的大小在32位系统上是4字节,在64位系统上是8字节。不管它指向的是char、int还是结构体,指针本身的大小只跟系统位数有关。
int a = 42;
int *p = &a; // p里存的是a的地址
printf("%p\n", p); // 打印地址
printf("%d\n", *p); // 解引用,得到42
我在项目中遇到过一个问题:有人把指针和数组名混用,结果在64位系统上出了bug。说白了,数组名是常量指针,不能自增自减。而指针变量可以。这个区别,坑过不少人。
1.2 内存模型:你的程序住在哪里
一个C程序跑起来后,内存会分成几个区域。我习惯把它们想象成一座公寓楼:
| 区域 | 存放内容 | 特点 |
|---|---|---|
| 栈区 | 局部变量、函数参数 | 自动分配释放,大小有限(通常几MB) |
| 堆区 | malloc/new出来的东西 | 手动分配释放,大小取决于内存 |
| 全局区 | 全局变量、static变量 | 程序启动分配,结束释放 |
| 代码区 | 函数指令 | 只读,不可修改 |
注意:栈空间很宝贵。我曾经在嵌入式项目里,因为递归调用太深,直接把栈撑爆了,系统当场死机。从那以后,我对栈大小特别敏感。
堆区虽然大,但管理不好就会内存泄漏。我建议:谁分配,谁释放。写malloc的时候,顺手把free写上,再填充中间代码。这个习惯救过我很多次。
1.3 数组与指针的纠缠
数组和指针,表面上很像,骨子里不同。数组名是地址常量,不能修改。指针是变量,可以随便指。但它们在很多场景下可以互换使用——这就是C语言的灵活之处,也是混乱之源。
int arr[5] = {1,2,3,4,5};
int *p = arr; // 数组名就是首元素地址
// 下面两种写法等价
printf("%d\n", arr[2]);
printf("%d\n", *(p + 2));
你想想看,为什么 arr[2] 和 *(arr+2) 一样?因为编译器把 arr[2] 翻译成了 *(arr+2)。嗯,这里要注意:sizeof(arr) 是整个数组的大小,而 sizeof(p) 只是指针的大小。这个区别,面试常考,实际开发也容易踩坑。
个人经验:当数组作为函数参数传递时,它会退化为指针。所以函数内部用 sizeof 拿不到数组长度。我一般会额外传一个长度参数,或者用宏定义固定长度。
1.4 函数调用栈与栈帧
每次调用函数,系统都会在栈上分配一块内存,叫栈帧。栈帧里放着:局部变量、参数、返回地址。函数返回时,栈帧自动销毁。这就是为什么局部变量不能返回地址——函数一结束,栈帧就没了,你拿到的地址指向了一块被回收的内存。
int* bad_function() {
int local = 100;
return &local; // 危险!返回了局部变量的地址
}
我曾经在代码审查里看到有人这么写,当时就指出:这个指针指向的栈空间,在函数返回后可能被其他调用覆盖。果然,程序在特定条件下会输出随机值。这就是典型的「悬空指针」问题。
函数调用栈的布局,从高地址到低地址大致是:参数、返回地址、旧基址、局部变量。理解这个顺序,对调试栈溢出、理解递归都很有帮助。
避坑指南:我曾经在递归函数里定义了一个大数组(比如1024字节),递归深度一深,栈直接爆了。后来我改成用堆分配,问题解决。记住:递归深度大时,别在栈上放大家伙。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到指针、数组、栈帧之间的关系。
本章小结
指针就是地址,数组名是常量指针,栈帧是函数调用的临时空间。这三个概念,是C语言的基石。我建议你亲手写几个小例子:打印指针大小、观察数组退化、模拟栈帧布局。光看是不行的,得动手。
嗯,这一章就到这里。记住:指针不可怕,可怕的是你以为自己懂了,实际上还差得远。多写代码,多调试,慢慢就熟了。
公众号:蓝海资料掘金营,微信deep3321