Pwn 基础之C/C++(六):栈、堆、生命周期与动态内存
本篇目标:理解程序常见内存区域,正确使用
malloc、calloc、realloc和free。预计阅读时间:20~25 分钟。
1. 程序的常见内存区域
概念上可以粗略理解为:
高地址
+------------------+
| Stack |
+------------------+
| Memory Mapping |
+------------------+
| Heap |
+------------------+
| BSS |
+------------------+
| Data |
+------------------+
| Read-only Data |
+------------------+
| Text |
+------------------+
低地址
真实布局受操作系统、加载器、编译器、ABI 和随机化机制影响。
2. Text 与只读数据
Text 区域主要保存编译后的机器指令。
字符串字面量等数据通常位于只读数据区域:
const char *message = "hello";
指针变量和字符串字面量本身可能位于不同区域。
3. Data 与 BSS
有非零初始值的全局或静态变量通常与 Data 区域相关:
int global_value = 100;
static int static_value = 200;
未显式初始化或初始化为零的全局、静态变量通常与 BSS 区域相关:
int global_value;
static int static_value;
程序开始运行时,它们会被初始化为零。
4. 栈
普通局部变量和函数调用环境经常与栈有关:
void function(void)
{
int number = 100;
char text[32];
}
函数返回后,这些局部对象的生命周期结束。
但“局部变量一定在栈上”不是绝对规则。优化后,变量可能只存在于寄存器,也可能被完全消除。
5. 堆
堆用于运行时动态申请内存,适合:
- 编译时不知道元素数量;
- 数据需要跨越当前函数继续存在;
- 需要动态创建和销毁对象。
C 常用:
malloc calloc realloc free
6. 作用域与生命周期
作用域回答:
在源代码什么位置可以使用这个名称?
生命周期回答:
对象从什么时候开始存在,到什么时候结束?
int *create_number(void)
{
int *pointer = malloc(sizeof(*pointer));
if (pointer != NULL) {
*pointer = 100;
}
return pointer;
}
函数返回后,局部变量 pointer 消失,但它指向的动态对象仍存在,直到调用者释放它。
7. malloc
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *pointer = malloc(sizeof(*pointer));
if (pointer == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
*pointer = 100;
printf("%d\n", *pointer);
free(pointer);
pointer = NULL;
return 0;
}
malloc 接收字节数,成功时返回动态区域起始地址,失败时返回 NULL。
sizeof(*pointer) 能跟随指针类型变化,减少重复维护类型名称。
8. 动态数组
size_t count = 10;
int *numbers = malloc(count * sizeof(*numbers));
if (numbers == NULL) {
return 1;
}
for (size_t i = 0; i < count; i++) {
numbers[i] = (int)i;
}
free(numbers);
numbers = NULL;
实际代码还应考虑乘法是否超过 size_t 可表示范围,尤其是长度来自外部数据时。
9. calloc
int *numbers = calloc(count, sizeof(*numbers));
它接收元素数量和元素大小,并将申请区域初始化为全零字节。
全零字节适合初始化整数零,但不能脱离具体类型简单理解为所有类型的通用默认值。
10. realloc
int *temporary = realloc(numbers, new_count * sizeof(*numbers));
if (temporary != NULL) {
numbers = temporary;
}
realloc 可能:
- 在原位置调整;
- 分配新区域并复制旧内容;
- 返回不同地址;
- 失败并返回
NULL。
使用临时指针,调整失败时仍能保留原地址。
11. free
free(pointer);
pointer = NULL;
free 结束动态对象对应存储的可用生命周期,但不会自动修改指针变量。
free(NULL);
是允许的。
只应释放由匹配的动态分配函数返回、且尚未释放的地址。
12. 所有权与借用
动态内存管理的核心问题是:
谁负责释放?
可以建立约定:
创建者拥有对象
拥有者负责释放
转移所有权必须明确
借用者不得擅自释放
例如:
char *create_message(void);
如果返回动态字符串,接口应说明调用者负责 free。
const char *get_program_name(void);
如果返回程序内部已有字符串,调用者只借用,不应释放。
13. 动态数组扩容模型
int grow_array(int **array, size_t *capacity)
{
size_t new_capacity = (*capacity == 0) ? 4 : *capacity * 2;
int *temporary = realloc(*array, new_capacity * sizeof(**array));
if (temporary == NULL) {
return 0;
}
*array = temporary;
*capacity = new_capacity;
return 1;
}
这里同时练习:
- 动态内存;
- 二级指针;
- 容量管理;
- 失败处理;
- 所有权不变的扩容操作。
本篇练习
- 动态申请
n个整数并计算平均值。 - 使用
calloc创建全零数组。 - 编写可扩容整数列表。
- 画出指针变量与动态对象的生命周期。
- 实现学生信息动态数组,退出前释放全部资源。
本篇小结
- 程序包含代码、静态数据、栈、堆等区域;
- 作用域与生命周期是两个不同概念;
- 动态对象不会因创建函数返回而自动释放;
- 每次分配都要检查失败;
- 每块动态资源都应有明确所有者;
malloc/free、calloc/free和realloc必须按规则配合。
下一篇
下一篇学习输入输出、类型转换以及函数指针。

说些什么吧!