• 动态内存管理


    1.为什么存在动态内存分配

    我们知道栈区(stack)是由编译器自动分配释放,存放函数的形式参数,局部变量的值等。 我们已经掌握的内存开辟方式有:

    int val = 20;//在栈空间上开辟四个字节
    char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
    
    • 1
    • 2

    但是上述的开辟空间的方式有两个特点:

    1. 空间开辟大小是固定的。

    2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

    但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态内存开辟了。

    首先我们要知道动态内存管理都是在堆区开辟空间,如下:

    image-20220806132505340

    2.动态内存函数的介绍

    2.1 malloc和free

    C语言提供了一个动态内存开辟的函数:

    void* malloc (size_t size);
    
    • 1

    功能:分配内存块,新分配的内存块的内容未初始化,仍保留不确定的值。

    参数:size——内存块的大小,以字节为单位,是无符号整数类型。

    返回值:开辟好空间的指针或NULL指针。

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

    1. 如果开辟成功,则返回一个指向开辟好空间的指针。

    2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

    3. 返回值的类型是 void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

    4. 如果参数 size为0,malloc的行为是标准未定义的,取决于编译器。

    • malloc的使用案例:
    #include
    #include
    #include
    int main()
    {
    	//动态内存开辟
    	int* p = (int*)malloc(40);//向内存申请一块40字节的空间,并对放回的指针类型转换
    	if (p == NULL)//如果p为空指针,打印错误信息,返回值,结束程序
    	{
    	   printf("%s\n", strerror(errno));
    	   return 1;//异常返回
    	}
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i;
    	}
    	for (i = 0; i < 10; i++)
    	{
    		printf("%d ", *(p + i));
    	}
        //没有free
        //并不是说内存空间就不回收了
        //当程序退出的时候,系统会自动回收内存空间的。
    	return 0;//正常返回
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这样就对开辟的内存空间进行赋值使用了,但是还有一些问题,因为向内存申请的空间没有进行释放。

    但是C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

    void free (void* ptr);
    
    • 1

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

    参数:ptr——指向先前使用或分配的内存块指针。

    返回值:没有

    1.如果参数 ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。

    image-20220729233743025

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

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

    • free的使用案例
    #include
    #include
    #include
    int main()
    {
    	//动态内存开辟
    	int* p = (int*)malloc(40);//向内存申请一块40字节的空间,并对放回的指针类型转换
    	if (p == NULL)//如果p为空指针,打印错误信息,返回值,结束程序
    	{
    	   printf("%s\n", strerror(errno));
    	   return 1;//异常返回
    	}
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i;
    	}
    	for (i = 0; i < 10; i++)
    	{
    		printf("%d ", *(p + i));
    	}
        free(p);//释放p所指向的动态内存
        p = NULL;//将p的值赋值为NULL
    	return 0;//正常返回
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    1.free的参数一定是动态开辟内存空间的那个起始位置的地址,否则会报错。

    2.在用free释放完动态开辟的内存之后,要对之前指向动态开辟空间的那个指针置为NULL,因为那块动态开辟的空间已经被操作系统回收 了,没有了访问的权限,所以要让p的值为NULL,避免野指针的问题。

    image-20220729230358498

    3.如果对动态内存开辟的空间没有释放掉,会出现一个内存泄漏的问题。

    image-20220729232223624

    2.2 calloc

    C语言还提供了一个函数叫 calloc, calloc函数也用来动态内存分配。原型如下:

    void* calloc (size_t num, size_t size);
    
    • 1

    功能:分配内存块,新分配的内存块的内容均初始化为0。

    参数:

    num——要分配的元素个数

    size——每个元素的大小,单位字节

    返回值:内存开辟成功时,返回指向函数分配的内存块的指针;内存开辟失败时,返回NULL指针。

    1. 函数的功能是为num个大小为 size的元素开辟一块空间,并且把空间的每个字节初始化为0。
    2. 与函数 malloc的区别只在于 calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
    • calloc的使用案例:
    //开辟10个整形的空间
    int main()
    {
        int* p = (int*)calloc(10, sizeof(int));
        if(p == NULL)
        {
            printf("%s\n",strerror(errno));
            return 1;
        }
        //打印
        int i = 0;
        for(i = 0; i < 10; i++)
        {
            printf("%d ",*(p + i));
        }
        //释放
        free(p);
        p = NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image-20220729235741604

    所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

    2.3 realloc

    realloc函数的出现让动态内存管理更加灵活。 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc函数就可以做到对动态开辟内存大小的调整。 函数原型如下:

    void* realloc (void* ptr, size_t size);
    
    • 1

    功能:重新分配内存块的大小

    参数:

    ptr——要调整的内存地址

    size——调整之后新大小

    返回值——调整之后的内存起始位置或NULL指针

    这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

    realloc在调整内存空间的是存在两种情况:

    情况1:原内存空间之后有足够大的空间

    image-20220730002927374

    情况2:原内存空间之后没有足够大的空间

    image-20220730002950096

    情况1

    当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

    情况2

    当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

    image-20220730003012462

    由于上述的两种情况,realloc函数的使用就要注意一些。举个例子:

    int main()
    {
    	int* p = (int*)malloc(40);
    	if (p == NULL)
    	{
    		perror("calloc");
    		return 1;
    	}
    	//使用 1 2 3 4 5 6 7 8 9 10
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i + 1;
    	}
    	//扩容
    	int* ptr = (int*)realloc(p, 80);
    	if (p != NULL)
    	{
    		p = ptr;
    	}
    	//打印 1 2 3 4 5 6 7 8 9 10
    	for (i = 0; i < 10; i++)
    	{
    		printf("%d ", *(p + i));
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    image-20220730103535076

    另外,realloc也能缩小空间,但是缩小空间不会出现原内存空间后空间不足的情况,因为它就在原来的内存空间范围内缩小。

    注意:如果你频繁的开辟malloc, 那么内存块与内存块之间会出现大量的内存碎片,如果不能很好地利用这些碎片化的空间,就会导致内存的利用率会下降,而且频繁的malloc也会导致效率的下降,堆区是操作系统管理的,malloc函数是调用操作系统的接口,然后去堆区上申请空间,申请空间是非常浪费时间的。所以我们可以使用内存池,内存池的概念是我们向内存申请了一块相对满足使用的空间,然后程序内部用内存池的方式维护这块空间,使用空间向内存池申请,用完后再还给内存池。

    补充:image-20220730110201542

    3.常见的动态内存错误

    3.1 对NULL指针的解引用操作

    image-20220730111143586

    编译器警告直接取消对NULL指针“p"的引用。在其他的编译器下运行可能会出现问题,所以我们在使用的时候一定要对动态开辟内存函数的返回值进行一个NULL指针的判断。

    3.2 对动态开辟空间的越界访问

    int main()
    {
        int* p = (int*)malloc(40);
        if(p == NULL)
        {
            printf("%s\n",strerror(errno));
            return 1;
        }
        //方式
        int i = 0;
        for(i = 0; i <= 10; i++)
        {
            p[i] = i;
        }
        free(p);
        p = NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    当*(p + 10) = 10时对动态开辟的空间进行了一个越界访问了,编译器直接报错.

    image-20220730111814491

    3.3 对非动态开辟内存使用free释放

    int main()
    {
        int a = 10;
        int* p = &a;
        free(p);
        p = NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    a是在栈区开辟的空间,不是在堆区开辟的空间。

    image-20220730112214070

    3.4 使用free释放一块动态开辟内存的一部分

    int main()
    {
    	int* p = (int*)malloc(40);
    	if (p == NULL)
    	{
    		printf("%s\n", strerror(errno));
    		return 1;
    	}
    	int i = 0;
    	for (i = 0; i < 5; i++)
    	{
    		*p = i;
    		p++;
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    p没有指向动态开辟内存的起始位置,编译器报错。

    image-20220730113335632

    3.5 对同一块动态内存多次释放

    int main()
    {
        int* p = (int*)malloc(40);
        free(p);
        //p = NULL;
        free(p);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    p已经释放过一次。

    image-20220730124749148

    3.6 动态开辟内存忘记释放(内存泄漏)

    int main()
    {
    	int* p = (int*)malloc(100);
    	int flag = 0;
    	scanf("%d", &flag);
    	if (flag == 5)
    	{
    		return;
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image-20220730130856790

    忘记释放不再使用的动态开辟的空间会造成内存泄漏。动态开辟的空间一定要释放,并且正确释放 。

    4.四个经典的笔试题

    4.1 题目1:

    void GetMemory(char *p)
    {
    	p = (char *)malloc(100);
    }
    void Test(void)
    {
        char *str = NULL;
        GetMemory(str);
        strcpy(str, "hello world");
        printf(str);
    }
    int main()
    {
        Test();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    请问运行Test 函数会有什么样的结果?

    先创建一个字符指针变量str,然后赋值为NULL。然后调用GetMemory函数 ,把实参str传递给GetMemory函数,因为形参是实参的一份临时拷贝,所以p里面存放的也是NULL。在调用函数时,申请了一块内存,p现在指向这块内存空间的地址,p是函数的形参,一旦出了这个函数,p就被销毁了,但是开辟的空间还在,这块空间就出现了内存泄漏的问题。所以str还是指向NULL,strcpy函数使用时对str(NULL)进行解引用,造成非法访问,程序会崩溃。

    修改版本:image-20220730170603620

    image-20220730171721316

    4.2 题目2:

    char *GetMemory(void)
    {
        char p[] = "hello world";
        return p;
    }
    void Test(void)
    {
        char *str = NULL;
        str = GetMemory();
        printf(str);
    }
    int main()
    {
        Test();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    请问运行Test 函数会有什么样的结果?

    先创建一个字符指针变量str,然后赋值为NULL。然后调用GetMemory函数 ,调用函数时,创建一个字符数组p,存入字符串。 然后返回p(数组名==首元素的地址),这个p是一个临时的,函数内部创建的数组。而函数调用结束后,数组开辟的空间已经被回收了。str接收p的值,str接收的是野指针。p所指向的空间已经还给操作系统了。这块空间未来会不会被使用,内容是否变化我们不得而知。所以printf(str)的值是不确定的。

    image-20220730181935309

    这题写的太差,不改了,偷懒。😁😁😁

    4.3 题目3:

    void GetMemory(char **p, int num)
    {
    	*p = (char *)malloc(num);
    }
    void Test(void)
    {
        char *str = NULL;
        GetMemory(&str, 100);
        strcpy(str, "hello");
        printf(str);
        //修改:free(str);
        //str = NULL;
    }
    int main()
    {
        Test();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    请问运行Test 函数会有什么样的结果?

    先创建一个字符指针变量str,然后赋值为NULL。调用GetMemory函数时传址调用,将申请的动态开辟内存空间的起始位置地址给了str,所以能够正常访问开辟的内存。这里唯一的问题是没有进行free会造成内存泄漏的问题。

    4.4 题目4:

    void Test(void)
    {
        char *str = (char *) malloc(100);
        strcpy(str, "hello");
        free(str);
        //修改:str = NULL;
    	if(str != NULL)
    	{
            strcpy(str, "world");
            printf(str);
    	}
    }
    int main()
    {
        Test();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    请问运行Test 函数会有什么样的结果?

    将申请的动态开辟内存空间的起始位置地址给了str,正常访问开辟的内存。free(str)释放了动态开辟的内存空间,这块空间被操作系统回收了。但是str存的仍然是这块空间的地址,如果强行把字符串放到str指向的空间中,就会造成非法访问。这里str已经是野指针了。

    5.C/C++程序的内存开辟

    C/C++程序内存分配的几个区域:

    image-20220730221013353

    1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。

    2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

    3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

    4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

    有了这幅图,我们就可以更好的理解在前面讲的static关键字修饰局部变量的例子了。

    实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

    但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁所以生命周期变长。

  • 相关阅读:
    一文掌握 Java8 Stream 中 Collectors 的 24 个操作
    数据存储(二)WebStorage 之 IndexedDB
    【数据库09】数据库系统体系结构
    Mac管理Ruby环境
    JackSon工具类
    React | 初学react(JSX创建虚拟DOM、创建组件的两种方法)
    基于Springboot+Vue的校园在线打印预约系统
    公司信息系统架构建设规划
    一文带你深入理解【Java基础】· Java集合(下)
    Netty实战(二)
  • 原文地址:https://blog.csdn.net/m0_64224788/article/details/126193625