Skip to content

内存管理

4.1 内存空间分布

内存分为 静态区动态区 ,静态区在编译时分配内存,而动态区在程序运行时分配(称为栈)或手动使用malloc分配内存(称为堆)。

画板

4.1.1 代码区

代码段(Text Segment)的代码区存放 程序编译后的二进制机器码

测试程序:

C
#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;
}

输出结果:

Text Only
[FUNC] function call.
[INFO] 0x401202.
[INFO] 0xe5894855.
Segmentation fault (core dumped)

分析结果:

  • 代码区可以访问、读,但不能写入。

  • 为什么访问函数就是在访问代码段?

因为函数是逻辑指令的集合,编译后成为不可修改的机器码。因此,访问函数和修改函数地址下的数据,就可以测试读写代码段数据。

4.1.2 只读数据区

代码段(Text Segment)的只读数据区存放 字符常量和**const**修饰的常量

测试程序:

C
#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;
}

输出结果:

Text Only
[INFO] 0x619e18005169
[INFO] 0x619e18006026
[INFO] hello
Segmentation fault (core dumped)

分析结果:

  • 只读数据区的地址高于代码区。
  • 只读数据区不能写入。
  • 赋值字符串常量时,建议加上const修饰,在编译阶段将错误指出。

  • 代码区和只读数据区都归属于代码段。

通过Linux指令:size <可执行文件>可以查看各内存段的字节数,可见代码区和只读数据区都归属于代码段(Text Segment)。

4.1.3 全局数据段

全局数据段(BBS/DATA)存放 全局变量和静态变量(包括函数中的静态变量)。其中,未初始化的变量在 BBS 段,初始化的变量在 DATA 段。

测试程序:

C
#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;
}

输出结果:

Text Only
[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 函数释放这部分空间则会造成内存泄漏。

测试程序:

C
#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;
}

输出结果:

Text Only
[INFO] code area: 0x5e3d8ae77189
[INFO] read only area: 0x5e3d8ae78004
[INFO] data seg: 0x5e3d8ae7a010
[INFO] bbs seg: 0x5e3d8ae7a024
[INFO] heap: 10 0x5e3dafad46b0

4.1.5 栈

栈空间存放 局部变量和函数参数 ,在程序运行时分配,且 内存地址向下增长

测试程序:

C
#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;
}

输出结果:

Bash
# 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 个字节)。

测试程序:

C
#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*));
}

输出结果:

Text Only
指针类型大小对比:
char*: 8 字节
int*: 8 字节
long*: 8 字节
float*: 8 字节
double*: 8 字节
void*: 8 字节

4.2.2 空指针和野指针

空指针是指向**NULL**的指针变量,对空指针进行访问会出现段错误。

测试程序:

C
#include <stdio.h>

int main() 
{
    int *ptr = NULL;
    printf("[INFO] %p\n", ptr); 
    printf("[INFO] %d\n", *ptr);
    return 0;
}

输出结果:

Text Only
[INFO] (nil)
段错误 (核心已转储)

输出分析:

  • 在使用指针时,应当先判断其是否为空。

野指针是指向不明确地址或非法地址的指针变量,对工程有很大的危害。

测试程序:

C
#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;
}

输出结果:

Text Only
[INFO] 0x563083f292a0
[INFO] 100
[INFO] 0x563083f292a0
[INFO] 100

输出分析:

  • 虽然使用free释放了内存空间,但此时ptr1依旧指向这部分内存的地址,即ptr1 != NULL
  • ptr2申请的内存空间与ptr1释放的内存空间位同一片,因此ptr1ptr2指向地址一致,导致内存空间混乱。

4.2.3 指针访问内存

通过指针访问内存的方式:

  • *p
  • p->x
  • p[x]
  • *(p+x)

指针的地址递增规则:

  • (p+x) = 地址 + x * sizeof(p)

测试程序:

C
#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;
}

输出结果:

Text Only
[INFO] 120 120
[INFO] 10 10
[INFO] 86 86
[INFO] 30 30
[INFO] 10 1 9
[INFO] 10 1 9

输出分析:

  • 指针的地址递增会根据数据类型而变化。
  • 结构体使用地址递增时,需要统一数据类型。
  • 需要注意: 指针始终指向地址的低位 ,无论是大端还是小端均是如此。

4.2.4 Linux第一宏思想

利用 Linux 第一宏container_of的思想,实现利用结构体成员的地址找到结构体的地址。

测试程序:

C
#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;
}

输出结果:

Text Only
[INFO] 10 1 9
[INFO] ofset = 4
[INFO] 10 1 9

4.3 多级指针

多级指针也就是一个指针变量,实际工程中建议不要超过二级指针(**p)。

指针级别不影响存储大小,只影响间接访问深度。即无论几级指针,大小均为四个字节(32位系统)。

画板

C
// 对应上图的指针关系
int a = 0x12345678;
int *p1 = &a;
int **p2 = &p1;

4.3.1 地址传递

二级指针的用法:

C
#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;
}

输出结果:

Text Only
[INFO] hello world
[INFO] hello world
[INFO] hello world
[INFO] hello linux

两个函数的工作区别如下图(函数的参数传递实际是参数拷贝):

画板

4.3.2 无序变有序

使用多级指针将 物理无序 映射为 逻辑有序

C
#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;
}

输出结果:

Text Only
[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

将数组中地址无序的字符串常量,转而用有序的二级指针表示,具体逻辑如下图:

画板