内存管理
4.1 内存空间分布¶
内存分为 静态区 和 动态区 ,静态区在编译时分配内存,而动态区在程序运行时分配(称为栈)或手动使用malloc
分配内存(称为堆)。
4.1.1 代码区¶
代码段(Text Segment)的代码区存放 程序编译后的二进制机器码 。
测试程序:
#include <stdio.h>
void function(void)
{
printf("[FUNC] function call.\r\n");
}
// 声明一个函数指针,并指向function函数
void (*func_ptr)(void) = function;
int main(void)
{
// 访问函数
func_ptr();
int *ptr = (int *)function; // 将函数入口地址强制转换为int*指针
// 读
printf("[INFO] %p.\n", ptr);
printf("[INFO] 0x%x.\n", *(ptr));
// 写
*ptr = 0xFF; // 抛出错误
return 0;
}
输出结果:
分析结果:
- 代码区可以访问、读,但不能写入。
- 为什么访问函数就是在访问代码段?
因为函数是逻辑指令的集合,编译后成为不可修改的机器码。因此,访问函数和修改函数地址下的数据,就可以测试读写代码段数据。
4.1.2 只读数据区¶
代码段(Text Segment)的只读数据区存放 字符常量和**const**
修饰的常量 。
测试程序:
#include <stdio.h>
void function(void)
{
printf("[FUNC] function call.\r\n");
}
int main(void)
{
// 代码区地址
int *ptr = (int *)function;
printf("[INFO] %p\n", ptr);
// 字符串常量的地址
printf("[INFO] %p\n", "hello");
// 字符串常量
printf("[INFO] %s\n", "hello");
// 尝试修改字符串常量
char *p_str = "hallo"; // 替换为 const char *p_str = "hallo" 可在编译阶段指出错误
p_str[1] = 'e'; // 抛出错误
return 0;
}
输出结果:
分析结果:
- 只读数据区的地址高于代码区。
- 只读数据区不能写入。
- 赋值字符串常量时,建议加上
const
修饰,在编译阶段将错误指出。
- 代码区和只读数据区都归属于代码段。
通过Linux
指令:size <可执行文件>
可以查看各内存段的字节数,可见代码区和只读数据区都归属于代码段(Text Segment)。
4.1.3 全局数据段¶
全局数据段(BBS/DATA)存放 全局变量和静态变量(包括函数中的静态变量)。其中,未初始化的变量在 BBS 段,初始化的变量在 DATA 段。
测试程序:
#include <stdio.h>
int a = 10;
int b;
const char *str = "hello world";
void function(void)
{
static int e = 1;
static int f;
printf("[func] data reg: %p\n", &e);
printf("[func] bbs reg: %p\n", &f);
}
int main(void)
{
static int c = 2;
static int d;
// 代码区地址
int *ptr = (int *)function;
printf("[INFO] code area: %p\n", ptr);
// 字符串常量的地址
printf("[INFO] read only area: %p\n", str);
// data段
printf("[INFO] data seg: %p\n", &a);
printf("[INFO] data seg: %p\n", &c);
// bbs段地址
printf("[INFO] bbs seg: %p\n", &b);
printf("[INFO] bbs seg: %p\n", &d);
// 函数中的静态变量
function();
return 0;
}
输出结果:
[INFO] code area: 0x654d63fdb149
[INFO] read only area: 0x654d63fdc004
[INFO] data seg: 0x654d63fde010
[INFO] data seg: 0x654d63fde018
[INFO] bbs seg: 0x654d63fde02c
[INFO] bbs seg: 0x654d63fde034
[func] data reg: 0x654d63fde014
[func] bbs reg: 0x654d63fde030
分析结果:
- 静态变量(包括函数内)存放在全局数据段。
- TEXT段地址 < DATA段地址 < BBS段地址。
4.1.4 堆¶
堆空间由程序员自行分配(malloc)和释放(free),且 内存地址向上增长 。注意,函数中申请的堆空间不会在函数运行结束后释放,若不使用 free 函数释放这部分空间则会造成内存泄漏。
测试程序:
#include <stdio.h>
// malloc
#include <stdlib.h>
int a = 10;
int b;
const char *str = "hello world";
void function(void)
{
// 申请一块int类型大小的内存
int *heap_var = (int *)malloc(sizeof(int));
// 申请失败判断
if (heap_var == NULL) {
return;
}
// 使用申请的内存空间
*heap_var = 10;
printf("[INFO] heap: %d %p\n", *heap_var, heap_var);
// 释放内存
free(heap_var);
}
int main(void)
{
// 代码区地址
int *ptr = (int *)function;
printf("[INFO] code area: %p\n", ptr);
// 字符串常量的地址
printf("[INFO] read only area: %p\n", str);
// data段
printf("[INFO] data seg: %p\n", &a);
// bbs段地址
printf("[INFO] bbs seg: %p\n", &b);
// 函数
function();
return 0;
}
输出结果:
[INFO] code area: 0x5e3d8ae77189
[INFO] read only area: 0x5e3d8ae78004
[INFO] data seg: 0x5e3d8ae7a010
[INFO] bbs seg: 0x5e3d8ae7a024
[INFO] heap: 10 0x5e3dafad46b0
4.1.5 栈¶
栈空间存放 局部变量和函数参数 ,在程序运行时分配,且 内存地址向下增长 。
测试程序:
#include <stdio.h>
// malloc
#include <stdlib.h>
int a = 10;
int b;
const char *str = "hello world";
void function(void)
{
// 申请一块int类型大小的内存
int *heap_var = (int *)malloc(sizeof(int));
// 申请失败判断
if (heap_var == NULL) {
return;
}
// 使用申请的内存空间
*heap_var = 10;
printf("[INFO] heap: %d %p\n", *heap_var, heap_var);
// 释放内存
free(heap_var);
}
char *stack_func(void)
{
char *s = "hello world";
char buffer[] = "hello world";
return s;
// return buffer;
}
int main(void)
{
// 代码区地址
int *ptr = (int *)function;
printf("[INFO] code area: %p\n", ptr);
// 字符串常量的地址
printf("[INFO] read only area: %p\n", str);
// data段
printf("[INFO] data seg: %p\n", &a);
// bbs段地址
printf("[INFO] bbs seg: %p\n", &b);
// 堆使用函数
function();
// 栈函数
char *p = stack_func();
printf("[INFO] %s %p\n", p, p);
return 0;
}
输出结果:
# return s;
[INFO] code area: 0x638db03a91a9
[INFO] read only area: 0x638db03aa004
[INFO] data seg: 0x638db03ac010
[INFO] bbs seg: 0x638db03ac024
[INFO] heap: 10 0x638de0adc6b0
[INFO] hello world 0x638db03aa004
# return buffer;
stack.c: In function ‘stack_func’:
stack.c:31:16: warning: function returns address of local variable [-Wreturn-local-addr]
31 | return buffer;
|
[INFO] code area: 0x5ae8265431a9
[INFO] read only area: 0x5ae826544004
[INFO] data seg: 0x5ae826546010
[INFO] bbs seg: 0x5ae826546024
[INFO] heap: 10 0x5ae8291836b0
[INFO] (null) (nil)
- 为什么同样是局部变量,指针变量可以返回,而数组则无法返回?
1. 局部指针变量
s
指向字符串常量hello world
,而字符串常量存放在只读数据区,生命周期是整个程序执行期间。 2. 局部数变量buffer
是拷贝字符串常量hello world
,内存分配在栈空间。
4.2 指针¶
4.2.1 指针大小¶
指针是一个存储内存地址的变量, **指针的大小取决于系统的内存寻址能力 **(如 32 位系统下指针是 4 个字节,而 64 位系统下指针则是 8 个字节)。
测试程序:
#include <stdio.h>
int main() {
// 不同类型指针的大小对比
printf("指针类型大小对比:\n");
printf("char*: %ld 字节\n", sizeof(char*));
printf("int*: %ld 字节\n", sizeof(int*));
printf("long*: %ld 字节\n", sizeof(long*));
printf("float*: %ld 字节\n", sizeof(float*));
printf("double*: %ld 字节\n", sizeof(double*));
printf("void*: %ld 字节\n", sizeof(void*));
}
输出结果:
4.2.2 空指针和野指针¶
空指针是指向**NULL**
的指针变量,对空指针进行访问会出现段错误。
测试程序:
#include <stdio.h>
int main()
{
int *ptr = NULL;
printf("[INFO] %p\n", ptr);
printf("[INFO] %d\n", *ptr);
return 0;
}
输出结果:
输出分析:
- 在使用指针时,应当先判断其是否为空。
野指针是指向不明确地址或非法地址的指针变量,对工程有很大的危害。
测试程序:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *ptr1 = (int *)malloc(sizeof(int));
free(ptr1);
// ptr1 = NULL; // 建议释放完空间,将指针置为NULL
if (NULL != ptr1) {
*ptr1 = 100;
}
int *ptr2 = (int *)malloc(sizeof(int));
printf("[INFO] %p\n", ptr1);
printf("[INFO] %d\n", *ptr1);
printf("[INFO] %p\n", ptr2);
printf("[INFO] %d\n", *ptr2);
return 0;
}
输出结果:
输出分析:
- 虽然使用
free
释放了内存空间,但此时ptr1
依旧指向这部分内存的地址,即ptr1 != NULL
。 ptr2
申请的内存空间与ptr1
释放的内存空间位同一片,因此ptr1
和ptr2
指向地址一致,导致内存空间混乱。
4.2.3 指针访问内存¶
通过指针访问内存的方式:
*p
p->x
p[x]
*(p+x)
指针的地址递增规则:
(p+x) = 地址 + x * sizeof(p)
测试程序:
#include <stdio.h>
typedef struct {
int a;
int b;
char c;
} Struct_A;
int main(void)
{
int a = 0x12345678;
int b[] = { 10, 30, 20 };
char *p1 = (char *)&a;
int *p2 = b;
printf("[INFO] %d %d\n", *p1, p1[0]);
printf("[INFO] %d %d\n", *p2, p2[0]);
printf("[INFO] %d %d\n", *(p1+1), p1[1]);
printf("[INFO] %d %d\n", *(p2+1), p2[1]);
/* 结构体指针 */
Struct_A struct_a = {
.a = 10,
.b = 1,
.c = 9,
};
Struct_A *p3 = &struct_a;
/* 常规方式 */
printf("[INFO] %d %d %d\n", p3->a, p3->b, p3->c);
/* 非常规手法,需要对char进行类型转换 */
int *p4 = (int *)&struct_a;
printf("[INFO] %d %d %d\n", p4[0], p4[1], (char)p4[2]);
return 0;
}
输出结果:
输出分析:
- 指针的地址递增会根据数据类型而变化。
- 结构体使用地址递增时,需要统一数据类型。
- 需要注意: 指针始终指向地址的低位 ,无论是大端还是小端均是如此。
4.2.4 Linux第一宏思想¶
利用 Linux 第一宏container_of
的思想,实现利用结构体成员的地址找到结构体的地址。
测试程序:
#include <stdio.h>
typedef struct {
int a;
int b;
char c;
} Struct_A;
void find_struct(int *member)
{
// 虚拟结构体,获取偏移
unsigned long offset = (unsigned long)&((Struct_A *)0)->b;
printf("[INFO] ofset = %ld\n", offset);
// 将(int *)转为(char *)做指针单字节偏移计算
Struct_A *p = (Struct_A *)((char *)member - offset);
printf("[INFO] %d %d %d\n", p->a, p->b, p->c);
}
int main(void)
{
Struct_A struct_a = {
.a = 10,
.b = 1,
.c = 9,
};
Struct_A *p3 = &struct_a;
printf("[INFO] %d %d %d\n", p3->a, p3->b, p3->c);
find_struct(&(p3->b));
return 0;
}
输出结果:
4.3 多级指针¶
多级指针也就是一个指针变量,实际工程中建议不要超过二级指针(**p
)。
指针级别不影响存储大小,只影响间接访问深度。即无论几级指针,大小均为四个字节(32位系统)。
4.3.1 地址传递¶
二级指针的用法:
#include <stdio.h>
void func_1(char *p)
{
printf("[INFO] %s\n", p);
p = "hello linux";
}
void func_2(char **p)
{
printf("[INFO] %s\n", *p);
*p = "hello linux";
}
int main(void)
{
char *p1 = "hello world";
func_1(p1);
printf("[INFO] %s\n", p1);
func_2(&p1);
printf("[INFO] %s\n", p1);
return 0;
}
输出结果:
两个函数的工作区别如下图(函数的参数传递实际是参数拷贝):
4.3.2 无序变有序¶
使用多级指针将 物理无序 映射为 逻辑有序 :
#include <stdio.h>
int main(void)
{
char *arr[] = { "hello", "world", "linux" };
char **p = arr;
printf("[INFO] %s:%p\n", p[0], p[0]);
printf("[INFO] %s:%p\n", p[1], p[1]);
printf("[INFO] %s:%p\n", p[2], p[2]);
printf("[INFO] &p[0]:%p\n", &p[0]);
printf("[INFO] &p[1]:%p\n", &p[1]);
printf("[INFO] &p[2]:%p\n", &p[2]);
printf("[INFO] &p[0]:%p\n", p);
printf("[INFO] &p[1]:%p\n", p+1);
printf("[INFO] &p[2]:%p\n", p+2);
return 0;
}
输出结果:
[INFO] hello:0x558bb5b55004
[INFO] world:0x558bb5b5500a
[INFO] linux:0x558bb5b55010
[INFO] &p[0]:0x7ffe81c20aa0
[INFO] &p[1]:0x7ffe81c20aa8
[INFO] &p[2]:0x7ffe81c20ab0
[INFO] &p[0]:0x7ffe81c20aa0
[INFO] &p[1]:0x7ffe81c20aa8
[INFO] &p[2]:0x7ffe81c20ab0
将数组中地址无序的字符串常量,转而用有序的二级指针表示,具体逻辑如下图: