• 【C语言进阶】动态内存管理及柔性数组


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

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

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

    1. 空间开辟大小是固定的。
    2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

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

    下面我们来讲述三种我们常见的动态内存开辟函数

    2.malloc和free

    2.1 malloc函数功能介绍

    void* malloc (size_t size);

    函数功能
    这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
    如果开辟成功,则返回一个指向开辟好空间的指针。
    如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
    返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
    来决定。如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

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

    返回值
    void*
    成功时,指向函数分配的内存块的指针。
    这个指针的类型总是 ,它可以被强制转换为所需的数据指针类型,以便被解引用。
    如果函数未能分配请求的内存块,则返回空指针。

    2.2 free函数的功能介绍

    函数free,专门是用来做动态内存的释放和回收的

    void free (void* ptr);

    函数功能
    free函数用来释放动态开辟的内存。
    如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
    如果参数 ptr 是NULL指针,则函数什么事都不做。

    参数
    指向以前使用 或 分配的内存块的指针

    返回值
    没有

    2.3 代码演示

    malloc开辟空间是在堆区开辟空间

    #include
    #include
    int main()
    {
    	int* p= (int*)malloc(40);//我们要开辟的空间是整形的,所以我们把它强制转换成int型,这个可以根据自己的需求来
    	int* ptr = p;//保留指向malloc开辟空间的地址
    
    	if (ptr == NULL)//判断ptr是否是空指针
    	{
    		printf("%s\n", strerror(errno));
    		//把错误码记录到错误码的变量中,然后再编译,打印错误信息
    	}
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*ptr = i;
    		 ptr++;
    	}
    	free(p);//释放开辟的空间
    	p = NULL;//p置为空指针,让p不再是野指针
    	return 0;
    }
    '
    运行

    看完这段代码可能会有许多疑问,为啥要保留指向malloc开辟的空间?
    当我们用malloc申请空间后,我们要对申请的空间进行释放,如果不释放内存,内存可能就会越占越少以至于不够,所以我们需要free来释放空间,free的参数是刚开始指向空间的起始地址,也就是p指针,如果是参数是ptr,就不能释放开辟的空间,因为ptr++已经改变了ptr原来的地址,所以我们要保留原来的指针对它进行释放。为啥要把p置为NULL呢?,如果我们不把p置为NULL,p里面还是存放着原来的地址,但是p所指向的空间已经被释放,这样p就会变成一个野指针,如果继续访问,就会非法访问,为了防止这样我们就会把它置为NULL.

    3.calloc

    3.1calloc函数的功能介绍

    void* calloc (size_t num, size_t size);

    函数功能
    函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
    函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

    参数
    num:要分配的元素数。
    size:每个元素的大小,单位字节。

    返回值
    成功时,指向函数分配的内存块的指针。
    这个指针的类型总是 ,它可以被强制转换为所需的数据指针类型,以便被解引用。
    如果函数未能分配请求的内存块,则返回空指针。

    3.2代码演示

    #include
    #include
    int main()
    {
    	int* p = (int*)calloc(10, sizeof(int));
    	//开辟10个元素的空间,每个元素的大小为int型
    	if (p == NULL)
    	{
    		printf("%s\n", strerror(errno));
    	}
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i;
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    '
    运行

    开辟空间初始化为0
    在这里插入图片描述

    4.realloc

    4.1realloc函数功能介绍

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

    函数功能
    更改 所指向的内存块的大小
    该函数可能会将内存块移动到新位置(该位置的地址由函数返回)。

    参数
    ptr :是要调整的内存地址
    如果ptr为空指针(NULL),这是类似于malloc的功能
    size:内存块的新大小,以字节为单位。
    是无符号整数类型

    返回值

    指向重新分配的内存块的指针,该块可能与原来位置相同,也可以是新位置。
    这个指针的类型是,它可以被强制转换为所需的数据指针类型,以便被解引用

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

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

    图解
    在这里插入图片描述

    4.3代码演示

    #include
    #include
    int main()
    {
    	int* p = (int*)malloc(40);
    	if (p == NULL)
    		return 1;
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i;
    	}
    	for (int i = 0; i < 10; i++)
    	{
    		printf("%d ", *(p + i));
    	}
    	//增加空间
    	int* ptr = (int*)realloc(p, 80);
    	//如果开辟失败,返回的是NULL,要重新设置一个指针
    	//如果满足条件,把ptr赋给p指针
    	if (ptr != NULL)
    	{
    		p = ptr;
    		ptr = NULL;
    	}
    	for (i = 10; i < 20; i++)
    	{
    		*(p + i) = i;
    	}
    	for (int i = 0; i < 20; i++)//打印元素
    	{
    		printf("%d ", *(p + i));
    	}
    	//释放空间
    	free(p);
    	p = NULL;
    	return 0;
    }
    

    在这里插入图片描述

    5.常见的动态内存错误

    5.1对NULL指针的解引用

    void test()
    {
     int *p = (int *)malloc(INT_MAX/4);
     *p = 20;//如果开辟空间失败,p的值是NULL,对p进行解引用就会有问题
     free(p);
    }
    

    所以我门要对p进行判断是否为空指针

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

    #include
    int main()
    {
    	int* p = (int*)malloc(20);
    	if (p == NULL)
    		return 1;
    	int i = 0;
    	for (i = 0; i < 20; i++)
    	{
    		printf("%d ", *(p + i) = i);
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    

    这里我们原本是开辟了20个字节,也就是5个整形的大小,但是在循环时,往后打印了20个整数,超出了我们的访问权限,也就是越界访问,原本只能访问5个整形的大小,你直接访问了20个整形的大小,会造成程序报错,这个是跟数组在内存中连续存放一致

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

    #include
    int main()
    {
    	int num = 10;//非动态内存开辟空间
    	int* p = &num;
    	free(p);
    	p = NULL;
    	return 0;
    }
    

    num是非动态内存开辟空间,不能使用free对它进行空间释放。free的使用是与malloc calloc realloc配合进行使用

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

    #include
    int main()
    {
    	int* p = (int*)malloc(40);
    	if (p == NULL)
    		return 0;
    	for (int i = 0; i < 5; i++)
    	{
    		*p = i;
    		p++;
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    

    在free释放的时候,p指向的不再是动态内存空间的起始位置。如果要释放malloc开辟的空间,我们要从开辟空间的起始位置开始进行释放,如果p不是空间的起始位置,我们就不能释放空间或是释放空间的一部分。

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

    #include
    int main()
    {
    	int* p = (int*)malloc(40);
    	if (p == NULL)
    		return 0;
    	for (int i = 0; i < 5; i++)
        {
    		*(p + i) = i;
    	}
    	free(p);
    	p = NULL;
    	free(p);//重复释放空间
    	return 0;
    }
    '
    运行

    对一次开辟空间只需要free一次,一次动态空间开辟对应一次free释放空间。

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

    #include
    int* get_memory()
    {
    	int* p = (int*)malloc(40);
    	return p;
    }
    int main()
    {
    	int* ptr = get_memory();
    	//使用
    
    	return 0;
    }
    

    函数会返回动态开辟空间的地址,我们要对空间进行释放,否则会导致内存泄漏,上述代码中,malloc开辟空间,free没有释放空间。

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

    C/C++内存分配区域
    在这里插入图片描述
    C/C++程序内存分配的几个区域:

    1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
      束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
      分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
      回地址等。
    2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
      配方式类似于链表。
    3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
    4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

    7.柔性数组

    也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
    C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

    代码演示

    //表示1
    #include
    struct s
    {
    	int a;
    	char ch;
    	int arr[0];//柔性数组成员
    };
    
    //表示2
    struct s
    {
    	int a;
    	char ch;
    	int arr[];//柔性数组成员
    };
    

    上述有两种柔性数组的表示方法,有时候其中一种表示,编译器无法编译,就需要表示另一种方法

    7.1柔性数组的特点

    1.结构中的柔性数组成员前面必须至少一个其他成员。
    2.sizeof 返回的这种结构大小不包括柔性数组的内存。
    3.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
    小,以适应柔性数组的预期大小。

    7.2柔性数组的使用

    代码1

    #include
    #include
    struct S
    {
    	int n;
    	float s;
    	int arr[];//柔性数组成员
    };
    int main()
    {
    	//为结构体成员开辟空间,同时为柔性数组预留空间
    	struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
    	if (ps == NULL)
    		return 1;
    
    	ps->n = 100;
    	ps->s = 6.5;
    	int i = 0;
    	for (i = 0; i < 4; i++)
    	{
    		scanf("%d",&(ps->arr[i]));
    	}
    
    	printf("%d %f\n", ps->n, ps->s);
    
    	for (i = 0; i < 4; i++)
    	{
    		printf("%d ", ps->arr[i]);
    	}
    	//增加柔性数组的空间 ,从16个字节增加到40个字节
    	struct S*ptr=(struct S*)realloc(ps, sizeof(struct S) + sizeof(int) * 10);
    	if (ptr == NULL)
    	{
    		return 1;
    	}
    	else 
    	{
    		ps = ptr;
    	}
    	//然后在对新开辟的空间进行使用
    	
    	//释放空间
    	free(ps);
    	ps = NULL;
    	return 0;
    }
    '
    运行

    在对结构体开辟空间时,.sizeof 返回的这种结构大小不包括柔性数组的内存,也就是没有对柔性数组开辟空间,所以我们要在堆中为结构体开辟空间时,要另外给柔型数组预留一块空间,当我们想增加柔性数组的空间,我们就可以通过realloc来为柔性数组增加空间,这样我们就可以控制它的内存空间,使它可大可小,这也体现出它的柔性。

    同样我们也可以设计一下结构体,一样可以实现上面的功能。
    代码2

    #include
    #inlucde<stdlib.h>
    struct S
    {
    	int n;
    	float s;
    	int *arr;//最后一个成员是指针
    };
    
    int main()
    {
    	//结构体开辟空间
    	struct S* ps = (struct S*)malloc(sizeof(struct S));
    	if (ps == NULL)
    		return 1;
    	ps->n = 100;
    	ps->s = 5.5f;
    
    	//为结构体中的arr开辟空间
    	int* ptr = (int*)malloc(4 * sizeof(int));
    	if (ptr == NULL)
    	{
    		return 1;
    	}
    	else
    	{
    		ps->arr = ptr;
    	}
    	int i = 0;
    	for (i = 0; i < 4; i++)
    	{
    		scanf("%d", &(ps->arr[i]));
    	}
    
    	printf("%d %f\n", ps->n, ps->s);
    	for (i = 0; i < 4; i++)
    	{
    		printf("%d ", ps->arr[i]);
    	}
         //释放空间
    	free(ps);
    	ps = NULL;
    	free(ps->arr);
    	ps->arr = NULL;
    	return 0;
    }
    

    这里第一个malloc为结构体中的三个成员都开辟了空间,所以这里我们不用另外开辟出一块空间预留出来给第三个成员,第二个malloc是对arr进行增加内存空间,所以他们内存空间不是连续的,而柔性数组的所在的结构体空间是连续的。

    7.3柔性数组的优势

    从上面的两个代码,可以看出代码1的好处
    1.方便内存释放

    从上述的代码1中,结构体的内存及其成员的内存是一次性分配好了,所以只需要返回一个结构体指针,只用一次free就可以把所有的内存给释放了。
    而代码2中,第一个malloc对结构体做了第一次内存分配,第二次malloc又对结构体中的arr做了一次内存分配,所以我们到最后要free两次,才能把所有的内存给释放了

    2.有利于访问速度

    代码1中结构体的内存是连续存放的,而代码2中的结构体不是连续存放,这样就会导致内存之间的间隙,从而降低访问速度。

    7.4图解演示

    下面两个分析,可以让你更加理解两个代码中的结构体是如何分配空间的。
    在这里插入图片描述

  • 相关阅读:
    Cloud Native=Cloud+Native 理解云原生
    Wireshark学习 与 TCP/IP协议分析
    Mybatis框架复习
    PHICOMM(斐讯)N1盒子 - recovery模式救砖卡登录页LOGO卡1%卡4%卡26%
    ssh基于WEB的农产品销售管理系统
    Java常用类方法总结
    50-51 - C++对象模型分析
    14 MySQL-视图
    第2部分:物联网模式在行动
    QT GUI编程常用控件学习
  • 原文地址:https://blog.csdn.net/m0_71659028/article/details/127029936