• 梦开始的地方—— C语言动态内存管理(malloc+calloc+realloc+free)


    动态内存管理

    1.为什么需要动态内存分配

    创建变量或者数组就是我们常见的内存开辟方式

    int num = 100;//开辟4个字节的空间
    int arr[10] = {0};//开辟40个字节的连续空间
    
    • 1
    • 2

    上面开辟空间的方式有两个特点:

    1. 开辟的空间大小是固定的,也就是不能修改的。
    2. 数组在声明的时候,必须知道数组长度,它所需要的内存在编译时就已经分配好了。

    但是很多时候,我们对于空间的需求上面的两种情况是满足不了的,有的时候我们需要的内存大小要程序运行之后才能知道,或者说有时候数组大小空间不够了,那么数组编译时开辟的内存空间的方式就不可行了,这个时候就需要动态内存开辟了。

    注意:动态开辟的内存是在堆上的,而我们使用的局部变量和函数的形参是在栈上开辟空间。

    2. 动态内存函数

    malloc&free

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

    mallo函数是C语言提供的动态内存开辟的函数

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

    void* malloc (size_t size);
    
    • 1
    • 如果空间开辟成功,则返回一个指向开辟好空间起始地址的指针
    • 如果开辟失败,则返回一个NULL指针,所以该函数的返回值一定要做判断
    • 该函数的返回值是void*,所以malloc函数并不知道开辟空间的类型,要使用者来决定这块空间用来存放什么
    • 如果size参数给0,这是C语言标准未定义的,取决于编译器的实现
    • 开辟的是size个字节的空间

    C语言还提供了一个和malloc函数成对出现的函数free,用来释放动态开辟的的内存空间

    void free (void* ptr);
    
    • 1
    • 如果参数ptr指向的空间不是动态开辟的,那么对于free函数来说这是为定义的
    • 如果参数ptr是NULL,则函数什么事都不做

    代码示例

    #include 
    #include 
    #include 
    int main()
    {
    	int arr[10] = { 0 };//在栈区申请了40个字节的空间
    	int* ptr = (int*)malloc(40);//在堆区申请了40个字节的空间
    	if (ptr == NULL)
    	{
            
            //strerror会返回错误信息
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    	else
    	{
    		int i = 0;
    		for (i = 0; i < 10; i++)
    		{
    			*(ptr + i) = 0;
    		}
    	}
    	//初始化为0
    	
    	free(ptr);//释放空间
    	ptr = NULL;//指向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
    • 我们这里定义的arr数组是存放在栈区的
    • 而动态开辟的空间是在堆区申请的空间
    • 动态开辟内存的时候一定记得判断是否开辟成功
    • malloc开辟的空间如果不收到调用free函数释放,在程序结束的时候会由系统帮我们自动释放。
    • 但如果程序是在服务器上一直运行,老是申请内存又不释放,就会造成内存泄露。就可能导致服务器出问题,甚至宕机。所以当我们申请的内存不在使用时,就一定要记得释放。
    • 当内存释放后,也要记得把指针指向NULL,让这个指针失忆。虽然内存空间被释放了,但指针ptr还记得那块空间的地址是非常危险的一件事情,可能会出现误操作。所以内存释放后记得把指针指向空

    calloc

    C语言还提供了一个动态内存分配的函数calloc

    void* calloc (size_t num, size_t size);
    
    • 1
    • 该函数的作用是开辟num个大小为size的元素开辟一块连续的内存空间,并把空间的每一个字节初始化为0
    • 该函数和malloc的区别只在于calloc会在返回指针之前,把申请空间的每个字节初始化为全0

    代码示例

    #include 
    #include 
    #include 
    int main()
    {
    	int* ptr = calloc(1,sizeof(int));//在堆区申请了4个字节的空间
    	if (ptr == NULL)
    	{
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    	else
    	{
    
    	}
    	
    	free(ptr);//释放空间
    	ptr = NULL;//指向NULL
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    如果我们对申请的内存空间的内容要求初始化,那么可以使用calloc函数

    realloc

    realloc函数则更加灵活,有的时候我们会发现申请的空间太小了有时候又会绝对申请的空间太大了。我们就会对内存的大小进行跳转,而realloc函数就可以对动态开辟内存进行调整。

    void* realloc (void* ptr, size_t size);
    
    • 1
    • ptr是要调整内存的起始地址
    • size是调整后的大小
    • 返回值就是调整后内存的起始地址
    • 这个函数在调整内存空间大小的基础上,还有可能将原理内存中的数据移动到新的空间
    • 也就是说realloc对内存大小的调整存在两种情况

    在这里插入图片描述

    • 情况1:要调整的内存就直接在原有的内存之后直接追加空间,原来空间的数据不发生变化。
    • 情况2:原有空间之后没有足够多的空间时,扩展的方法就是:在堆空间上另外找一个合适大小的连续空间来使用。这样函数返回的就是一个新的内存地址。

    所以在使用realloc函数的时候就需要注意

    调整空间大小时需要新建一个指针变量来接收返回值,避免内存空间开辟失败。从而导致原理的空间丢失。

    #include 
    #include 
    #include 
    int main()
    {
    	int* ptr = (int*)malloc(10);
    	if (ptr == NULL)
    	{
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    	else
    	{
    
    	}
    	//调整空间
    	int* p = realloc(ptr, 40);//调整成40个字节大小
    	if (p == NULL)
    	{
    		printf("调整内存空间失败 %s", strerror(errno));
    	}
    	else
    	{
    		ptr = p;
    		p = NULL;
    	}
    	free(ptr);//释放空间
    	ptr = NULL;//指向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
    • 30
    • 31

    3. 常见的动态内存错误

    对NULL解引用

    当开辟内存过大时,ptr是有可能为NULL指针的,当为NULL的时候,*ptr非法访问内存

    #include 
    #include 
    int main()
    {
    	int* ptr = (int*)malloc(INT_MAX);
    	*ptr = 100;
    
    	free(ptr);//释放空间
    	ptr = NULL;//指向NULL
    
    
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

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

    越界访问也是会出现问题的

    #include 
    #include 
    #include 
    int main()
    {
    	int* ptr = (int*)malloc(12);
    	if (ptr == NULL)
    	{
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    	else
    	{
    		int i = 0;
    		for (i = 0; i < 10; i++)
    		{
    			//越界访问
    			*(ptr + i) = 100;
    		}
    	}
    
    	free(ptr);//释放空间
    	ptr = NULL;//指向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

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

    使用free是否非动态开辟的内存空间,也是错误的。

    #include 
    #include 
    #include 
    int main()
    {
    	int* ptr = (int*)malloc(12);
    	int arr[10] = { 0 };
    	if (ptr == NULL)
    	{
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    	
    
    	free(arr);//释放非动态开辟的空间
    	ptr = NULL;//指向NULL
    
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    #include 
    #include 
    #include 
    int main()
    {
    	int* ptr = (int*)malloc(12);
    	if (ptr == NULL)
    	{
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    	else
    	{
    		//此时ptr已经不指向动态开辟内存空间的起始位置了
    		ptr+=2;
    	}
    	
    
    	free(ptr);//释放空间
    	ptr = NULL;//指向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

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

    多次释放同一块内存空间也是会有问题的

    #include 
    #include 
    #include 
    int main()
    {
    	int* ptr = (int*)malloc(12);
    	if (ptr == NULL)
    	{
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    	else
    	{
    		//第一次释放
    		free(ptr);
    	}
    	
    
    	free(ptr);//再次释放
    	ptr = NULL;//指向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

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

    只申请不释放,会导致内存泄露,导致那些内存无法被使用。

    #include 
    #include 
    #include 
    void test()
    {
    	int* ptr = (int*)malloc(12);
    	if (ptr == NULL)
    	{
    		printf("开辟内存失败 %s", strerror(errno));
    	}
    }
    int main()
    {
    	
    	while (1)
    	{
    		test();
    	}
    
    
    	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

    4. 经典错误题目

    题目1

    这是一道非常经典的题目,这段代码存在这一下这个问题

    • malloc动态开辟的空间未进行释放会造成内存泄露
    • 传递给GetMemory函数的是形参变量str,形参是实参的一份临时拷贝。所以p只是一个局部变量,对它的修改并不会影响str
    • 当str为NULL时,strcpy函数就会出问题,如果str为NULL,printf打印时也会出现问题
    #include 
    #include 
    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
    • 17
    • 18

    这段代码应该这么修改

    • 取出str的地址,拿二级指针接收,解引用就能修改str了。
    • 使用完动态开辟的内存后,要进行释放。
    • 不要一维函数销毁了内存就会自动释放,函数是在栈上开辟空间,而malloc函数是从堆上申请空间
    #include 
    #include 
    #include 
    void GetMemory(char** p)
    {
        *p = (char*)malloc(100);
    }
    void Test(void)
    {
        char* str = NULL;
        GetMemory(&str);
        strcpy(str, "hello world");
        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
    • 19
    • 20
    • 21

    题目2

    这段代码打印的是啥也不确定

    • 因为p是一个局部的变量,当GetMemory函数调用结束了,p数组的声明周期也结束了。
    • p所指向的这一块空间就会被释放,归还给操作系统。
    • 当p作为返回值返回,str接收了这个返回值。
    • 虽然str指向的是p数组首元素地址,当拿块空间已经被释放,归还给操作系统了。那么此时这块空间就可能被其它程序使用
    • 所以虽然str指向首元素地址,但打印出来的可以不一定是"hello world"
    #include 
    #include 
    #include 
    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
    • 17
    • 18
    • 19

    题目3

    这道题目典型的内存泄露问题,没有对动态开辟的的内存进行释放

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

    稍微修改一下就好了

    #include 
    #include 
    #include 
    void GetMemory(char** p, int num)
    {
        *p = (char*)malloc(num);
    }
    void Test(void)
    {
        char* str = NULL;
        GetMemory(&str, 100);
        if (str == NULL)
        {
            return;
        }
        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
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    题目4

    这道题目最大的问题就是对释放后的动态开辟内存再次使用

    • 虽然free把开辟的内存释放了,但str是依旧指向那一块空间的起始地址的
    • 再使用strcpy进行字符串拷贝,这就属于非法内存访问了。
    • 所以free完后,记得把指针置为NULL
    #include 
    #include 
    #include 
    
    void Test(void)
    {
        char* str = (char*)malloc(100);
        strcpy(str, "hello");
        free(str);
        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
    • 18
    • 19
    • 20
    • 21

    这段代码写的非常古怪,但还是可以修改的。

    #include 
    #include 
    #include 
    
    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
    • 18
    • 19
    • 20
    • 21
    • 22

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

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

    • 栈区:在执行函数时,函数局部变量的存储单元都可以在栈上创建,当函数执行完毕后这些存储单元会被自动释放。栈区分配运算内置于处理器的指令集中,效率很高,但是分配的内存空间是有限的。栈区主要存放的是局部变量、函数参数(形参)、返回数据、返回地址。
    • 堆区:一般由程序员来释放,程序结束时可能由操作系统来帮程序员释放。像malloc就是在堆上开辟空间。
    • 静态区:存放全局变量、静态数据,程序结束时由系统释放
    • 代码段:存放函数体(类成员和全局函数)的二进制代码

    在这里插入图片描述


  • 相关阅读:
    Himall商城字符串帮助类移除前导/后导字符串
    非零基础自学Java (老师:韩顺平) 第6章 数组、排序和查找 6.9 排序的介绍 && 6.10 冒泡排序 && 6.11 查找
    开发 Java 用小而美的框架,Solon v1.9.4 发布
    pycharm联合Anaconda
    计算机网络概述
    【python 】pygame制作简单的游戏移动操作
    Web APIs:事件高级--DOM事件流及DOM事件流程代码验证
    Docker 创建网络
    ES6新增循环对象的四种方法(通俗易懂)
    协程(一)——什么是协程
  • 原文地址:https://blog.csdn.net/weixin_53946852/article/details/127958232