写 C 或 C++ 时,你有没有好奇过:每次调用函数,形参怎么就自动拿到实参的值了?局部变量又为啥一退出函数就“消失”了?背后没玄机,全是栈在干活。
栈不是堆,是后进先出的“快递柜”
想象你家楼下的智能快递柜——最上面那格放的总是最后塞进去的包裹。栈也一样:函数一进来,系统就在栈顶划一块地,把参数、返回地址、局部变量全压进去;函数一结束,“啪”一声弹出整块区域,连带所有变量一起清空。干净利落,不拖泥带水。
一个真实的小例子
看这段代码:
void calc(int a, int b) {
int sum = a + b;
int flag = 1;
printf("sum=%d, flag=%d\n", sum, flag);
}
int main() {
calc(3, 5);
return 0;
}当 calc(3, 5) 被调用时,栈上大概长这样(从高地址到低地址):
- 返回地址(main 中下一条指令的位置)
- 实参
b=5 - 实参
a=3 - 局部变量
sum(刚算出是 8) - 局部变量
flag=1
注意:参数和局部变量都在同一块栈帧里,只是顺序和生命周期不同。参数在前,是“别人给的”;局部变量在后,是“自己生的”。但它们都靠栈指针(SP)和帧指针(FP)精准定位,谁也不越界。
为什么不能返回局部变量的地址?
新手常踩这个坑:
int* bad_func() {
int x = 42;
return &x; // 危险!
}函数返回后,这块栈内存立刻被标记为“可回收”。下次调用别的函数,可能就直接覆盖掉 x 原来的位置。你拿着那个地址去读,读到的可能是垃圾数据,也可能是刚写进去的另一个变量——结果飘忽不定,调试起来头发掉一地。
栈传递,其实不传“值”本身?
严格说,栈上传的不是“变量”,而是“值的副本”。比如传一个结构体:
struct Point { int x; int y; };
void move(struct Point p) { ... }
// 调用 move({10, 20});
// 整个 struct 的 8 字节(假设 int 是 4 字节)会被原样拷贝进栈所以改 p.x 不会影响原来的结构体——因为压根就没传引用,更没传指针,就是完完整整搬了一份过去。这也是为啥大结构体传参慢,编译器有时会悄悄改成传指针,但那是优化层面的事了,语义上你写的还是“值传递”。
搞懂栈怎么托住局部变量,debug 时看汇编、查栈帧、分析段错误,心里就有底了。它不炫技,但天天在你代码底下稳稳托着——就像自行车链条,看不见,一断就蹬不动。