Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

内存管理 / Memory Management

1. C++ 内存管理总览:先看完整地图 / C++ Memory Management Map

2. 程序内存布局:内存不是只有堆和栈 / Program Memory Layout

3. 栈与函数调用:局部变量为什么会消失 / Stack and Function Calls

4. 堆与动态分配:new 到底做了什么 / Heap and Dynamic Allocation

5. 对象生命周期:地址还在不代表对象还活着 / Object Lifetime

6. RAII 与所有权:让对象负责清理 / RAII and Ownership Design

7. 智能指针与所有权关系:谁拥有,谁只是看 / Smart Pointers and Ownership Graphs

8. 容器、迭代器与内存失效 / Containers, Iterators, and Invalidation

9. allocator 与 PMR:把分配策略抽出来 / Allocators and PMR

10. 内存错误与 Sanitizer:把崩溃变成证据 / Memory Errors and Sanitizers

本页目录

栈与函数调用:局部变量为什么会消失 / Stack and Function Calls ​

栈不是“放局部变量的地方”这么一句话就够了。

它更像函数调用的工作区。理解这点,才能理解局部变量、返回、递归、悬垂指针和栈溢出。

1. 函数调用需要保存现场 ​

当一个函数调用另一个函数时,程序必须记住:

text
调用完以后回到哪里
当前函数还有哪些局部状态
被调用函数需要哪些参数和临时空间
1
2
3

例如:

cpp
int square(int x) {
    return x * x;
}

int sum_square(int a, int b) {
    return square(a) + square(b);
}

int main() {
    return sum_square(3, 4);
}
1
2
3
4
5
6
7
8
9
10
11

调用过程可以简化成:

text
main()
  |
  v
sum_square(3, 4)
  |
  +-- square(3)
  |
  +-- square(4)
1
2
3
4
5
6
7
8

运行时通常会有一叠调用帧:

text
when square(3) is running

+------------------+
| frame: square    |
| x = 3            |
+------------------+
| frame: sum_square|
| a = 3, b = 4     |
+------------------+
| frame: main      |
+------------------+
1
2
3
4
5
6
7
8
9
10
11

square(3) 返回后,它自己的调用帧就撤掉。

这就是“栈”这个模型最核心的地方:

text
后调用的函数先返回。
函数返回时,它的本次工作区随之失效。
1
2

2. 局部自动对象跟随作用域结束 ​

看代码:

cpp
void f() {
    int x = 42;
}
1
2
3

x 是局部自动对象。

“自动”不是说它是自动变量这个老术语,而是强调:

text
进入作用域时自动创建
离开作用域时自动销毁
1
2

更具体地说:

text
进入 f()
  |
  v
为 x 准备存储
  |
  v
x 的生命周期开始
  |
  v
离开 f()
  |
  v
x 的生命周期结束
1
2
3
4
5
6
7
8
9
10
11
12
13

对 int 这种类型,生命周期结束看起来没有明显动作。

对 std::string 就不同:

cpp
#include <string>

void f() {
    std::string s = "hello";
}
1
2
3
4
5

离开函数时,s 的析构函数会执行。

析构函数会释放它拥有的字符资源。

3. 返回局部变量地址为什么危险 ​

经典错误:

cpp
int* bad() {
    int value = 42;
    return &value;
}
1
2
3
4

过程是:

text
bad() 被调用
  |
  v
value 在 bad() 的调用帧里创建
  |
  v
return &value
  |
  v
bad() 返回,调用帧撤掉
  |
  v
返回出去的地址还在
  |
  v
但地址上没有活着的 int 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

图像化:

text
inside bad()

+------------------+
| frame: bad       |
| value = 42 <---+ |
+----------------| +
                |
return pointer -+

after bad() returns

+------------------+
| frame: caller    |
+------------------+

old frame of bad is gone
pointer still stores an address, but object lifetime ended
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这就是悬垂指针。

“悬垂”可以理解成:它还挂着一个地址,但这个地址不再对应合法对象。

引用也一样:

cpp
int& bad_ref() {
    int value = 42;
    return value;
}
1
2
3
4

引用不是魔法保护罩。

它也不能让局部对象延长生命周期到函数外。

4. 作用域和生命周期相关但不完全相同 ​

大多数局部变量离开作用域时生命周期结束。

但不要把两个词完全画等号。

作用域是名字的可见范围。

生命周期是对象是否活着。

cpp
int main() {
    int* p = nullptr;

    {
        int x = 1;
        p = &x;
    }

    // x 这个名字不可见
    // x 对象生命周期也结束
    // p 仍保存旧地址
}
1
2
3
4
5
6
7
8
9
10
11
12

这里有三件事:

text
x 这个名字不能用了
x 这个对象也不活了
p 这个指针变量还活着
1
2
3

错误经常发生在第三件事:保存旧地址的对象还活着,但它指向的对象不活了。

5. 递归和栈空间 ​

递归函数每调用一层,就需要一层调用帧。

cpp
int factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}
1
2
3
4
5
6

调用 factorial(4) 时:

text
factorial(4)
  |
  v
factorial(3)
  |
  v
factorial(2)
  |
  v
factorial(1)
1
2
3
4
5
6
7
8
9
10

栈上可能像这样:

text
+------------------+
| factorial n = 1  |
+------------------+
| factorial n = 2  |
+------------------+
| factorial n = 3  |
+------------------+
| factorial n = 4  |
+------------------+
| main             |
+------------------+
1
2
3
4
5
6
7
8
9
10
11

如果递归太深,就可能栈溢出。

栈溢出不是 C++ 语法错误。

它是运行时栈空间不够了。

6. 大对象不要随便放栈上 ​

看代码:

cpp
void f() {
    int values[10'000'000]{};
}
1
2
3

这个数组很大。

如果放在栈帧中,可能直接导致栈溢出。

更合适的方式通常是:

cpp
#include <vector>

void f() {
    std::vector<int> values(10'000'000);
}
1
2
3
4
5

这里 values 对象本体仍然是局部对象。

但大量元素存储通常来自堆。

它离开作用域时,vector 析构函数会释放元素缓冲区。

7. 栈对象也可以安全返回 ​

不要误解为“局部变量都不能返回”。

不能返回的是局部对象的地址或引用。

返回值本身通常是安全的:

cpp
#include <string>

std::string make_name() {
    std::string s = "Alice";
    return s;
}
1
2
3
4
5
6

这里返回的是一个 std::string 值。

编译器可以做返回值优化,也可以移动构造。

调用者得到的是自己的对象。

危险的是这种:

cpp
const std::string& bad() {
    std::string s = "Alice";
    return s;
}
1
2
3
4

返回引用不会复制出新对象。

它只是让外面引用一个即将消失的对象。

8. 本篇总结 ​

栈的核心模型是:

text
函数调用
  |
  v
创建调用帧
  |
  v
局部自动对象在调用帧中存在
  |
  v
函数返回
  |
  v
调用帧撤销,局部对象生命周期结束
1
2
3
4
5
6
7
8
9
10
11
12
13

判断栈相关错误时问:

text
这个地址来自哪个函数的局部对象?
那个函数是否已经返回?
这个引用是否指向已经离开作用域的对象?
这个局部数组是否太大?
递归深度是否可能耗尽栈?
1
2
3
4
5

下一篇讲堆与动态分配。堆解决的是“对象需要更灵活地存在”,但它也带来“谁负责释放”的问题。

最后更新于:

Pager
上一篇2. 程序内存布局:内存不是只有堆和栈 / Program Memory Layout
下一篇4. 堆与动态分配:new 到底做了什么 / Heap and Dynamic Allocation

持续记录,持续成长

Copyright © Tidenflow