C语言动态内存管理

119 阅读7分钟

动态内存管理

动态内存分配

C程序内存区域划分

C程序将数据存储在不同的内存区域, 内存被划分为栈区,堆区,数据段(静态区),代码段

  • 栈区: 存储函数的局部变量,局部变量在进入函数时开辟栈空间,函数结束时自动释放.
    栈内存分配在指令集中有定义,效率很高,但是分配的内存容量有限

  • 堆区: 动态内存是在堆区上分配的,程序员操作,不手动回收会一直等到程序结束系统才回收

  • 数据段(静态区): 全局变量,加上static关键字的静态变量,直到程序结束才销毁

  • 代码段: 存放函数体的二进制代码,常量字符串也存储在此处

#include <stdio.h>
#include <stdlib.h>

int e = 50;
int f = 60;

int main()
{
    printf("局部变量\n");
    int a = 10;
    int b = 20;
    printf("%p\n",&a);
    printf("%p\n",&b);
    printf("静态变量\n");
    static int c = 30;
    static int d = 40;
    printf("%p\n", &c);
    printf("%p\n", &d);
    printf("全局变量\n");
    printf("%p\n", &e);
    printf("%p\n", &f);
    printf("动态内存开辟\n");
    int *p1 = (int*)malloc(sizeof(int));
    int *p2 = (int*)malloc(sizeof(int));
    printf("%p\n", &p1);
    printf("%p\n", &p2);
    printf("常量字符串\n");
    printf("%p\n","hello world");
    printf("%p\n","welcome");
    return 0;
}

输出结果: 
局部变量
0x16d8ae9c8
0x16d8ae9c4
静态变量
0x102558008
0x10255800c
全局变量
0x102558000
0x102558004
动态内存开辟
0x16d8ae9b8
0x16d8ae9b0
常量字符串
0x102553f93
0x102553f9f

从此段程序的输出结果可以看出, 位于同一个内存区域的变量地址相近, 而位于不同内存区域的变量地址相差较远

而且我们仔细观察之后还能发现, 栈和堆的内存地址较接近,数据段和代码段的内存地址较接近,与上图的位置关系大致相符

动态内存分配的作用

栈区开辟空间只有普通变量和定义数组两种方式, 其长度都是固定的, 但是有许多的场景是要求我们根据外界输入来决定数组/开辟空间的大小,编写程序时是不知道的

所以我们在此场景下便要使用堆区的内存,在堆区上动态开辟内存空间

注意: 与栈空间初始为随机值不同,堆区上的内存的初始值是0

动态内存函数

动态内存开辟函数malloc
void* malloc (size_t size);//单位为字节 结合sizeof(类型)使用

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

如果开辟成功,则返回一个指向开辟好空间的指针。 (void* 类型)

如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。 (如果不进行检查,解引用时会有严重错误)

返回值的类型是 void* ,需要强制转化指针类型

size为0时,程序错误

动态内存释放/回收函数free
void free (void* ptr);

free函数用来释放动态开辟的内存。

如果参数 ptr 指向的空间不是动态开辟的,程序错误。

如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc和free都声明在 stdlib.h 头文件中。

指定类型开辟内存calloc
void* calloc (size_t num, size_t size);//要开辟几个空间 每个空间大小是多少个字节

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。(适用于对申请的内存空间初始化的场景)

调整动态内存realloc
void* realloc (void* ptr, size_t size);//单位:字节

可以对已分配的空间重新调整,通过size参数指定调整后的字节数为多少,返回调整后的空间地址

内部实现时会有两种情况:

  1. 如果原本的空间后面的内存足够用于调整, 是直接在现有内存后面扩展
  2. 如果原本的空间后面不足以用于调整, 则在堆空间上另找一块区域作为调整后空间, 返回新的内存地址

截屏2024-08-15 17.46.55.png

接受之后需要判断 是否申请失败, 如果是申请失败应该马上报错 否则如果将p指针直接指向ptr可能会造成内存泄露,一直无法释放p原来指向的内存空间

动态内存分配错误

  1. 开辟失败,返回NULL,但还是解引用了(不常见)
  2. 动态开辟空间越界访问(注意)
  3. 对非动态开辟内存使用free函数(不常见)
  4. 使用free函数只释放一部分内存(比如p+2)(不常见)
  5. 忘记释放内存--》内存泄露(特别注意!!!)
void test()
{
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    while(1);
}

函数出来之后p变量被销毁 但对应的内存空间未被释放 导致之后再也没有指针指向这段空间 无法进行回收 导致内存泄露

void GetMemory(char *p)
{
	p = (char *)malloc(100);
}
void Test(void)
{
  char *str = NULL;
  GetMemory(str);
  strcpy(str, "hello world");
  printf(str);
}

这段程序会出错, str还是NULL, p是形参 是str的临时拷贝 只是p指向了这段空间 并不是str指向这段空间, 函数结束后p会销毁, 会造成内存泄露

char *GetMemory(void)
{
  char p[] = "hello world";
  return p;
}
void Test(void)
{
  char *str = NULL;
  str = GetMemory();
  printf(str);
}

字符数组在函数的栈空间上创建, 随着函数的结束销毁, 所以str为野指针, 指向一个系统的内存空间, 程序会出现错误

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}

传递过去的是指针的地址,所以可以正常的将p返回回来,还是指向函数内开辟的堆内存,正常打印hello,但是忘记对开辟的堆空间进行释放

void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}

过早对开辟的内存回收, str变成了野指针

柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组, 这就是柔性数组成员

typedef struct st_type
{
    int i;
    int a[0];//柔性数组成员
}type_a;

前面最少有一个其他成员,sizeof返回结构的大小不包括柔性数组,如果用malloc进行内存分配,分配的大小要大于结构的大小,适应柔性数组的预期大小

int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++)
{
	p->a[i] = i;
}
free(p);

变相实现了变长数组,此处长度为100

也可以用malloc进行100个int元素的内存分配

但是用柔性数组的方法只用malloc一次,而第二种方法需要malloc两次(第一次结构,第二次数组,free需要多一次,风险高)

另外,malloc两次内存是不连续的,连续的内存访问速度更高,减少内存碎片(内存利用率更高)

详情可见: coolshell.cn/articles/11…