• 【C语言进阶】动态内存管理


    大家好好我是沐曦希💕

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

    前面我们已经掌握的内存开辟方式有:

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

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

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

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

    那么此时要使用动态内存了。

    2.动态内存函数的介绍

    2.1 malloc函数

    malloc的头文件是stdlib.h
    C语言提供了一个动态内存开辟的函数:

    void* malloc (size_t size);
    
    • 1

    在这里插入图片描述
    这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

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

    例如:

    #include
    #include
    #include
    #include
    int main()
    {
    	int* p = (int*)malloc(40);
    	if (NULL == p)
    	{
    		printf("%s\n", strerror(errno));
    		return 1;
    	}
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i + 1;
    	}
    	for (i = 0; i < 10; i++)
    	{
    		printf("%d ", *(p + i));
    	}
    	//没有free并不是说内存空间就不回收了
    	//当程序退出的时候,系统会自动回收内存空间的
    	//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

    在这里插入图片描述

    2.2 free函数

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

    void free (void* ptr);
    
    • 1

    在这里插入图片描述
    free函数用来释放动态开辟的内存。

    1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
    2.如果参数 ptr 是NULL指针,则函数什么事都不做。
    malloc和free都声明在 stdlib.h 头文件中。

    例如:

    #include
    #include
    int main()
    {
    	//代码1
    	int num = 0;
    	scanf("%d", &num);
    	int arr[num];//C99标准的变长数组
    	//代码2
    	int* ptr = NULL;
    	ptr = (int*)malloc(num * sizeof(int));
    	if (NULL != ptr)//判断ptr指针是否为空
    	{
    		int i = 0;
    		for (i = 0; i < num; i++)
    		{
    			*(ptr + i) = 0;
    		}
    	}
    	free(ptr);//释放ptr所指向的动态内存
    	ptr = 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

    在这里插入图片描述
    在这里插入图片描述
    由上图可知,即使ptr所指向的空间被释放了,ptr还是指向该空间,所以应该加上代码:ptr = NULL;

    2.3 calloc函数

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

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

    在这里插入图片描述

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

    例如:

    #include
    #include
    #include
    int main()
    {
    	int* p = (int*)calloc(10, sizeof(int));
    	if (NULL == p)
    	{
    		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

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

    2.4 realloc函数

    1.realloc函数的出现让动态内存管理更加灵活。
    2.有时会发现过去申请的空间太小了或者申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。

    realloc 函数就可以做到对动态开辟内存大小的调整。

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

    在这里插入图片描述

    1.ptr 是要调整的内存地址
    2.size 调整之后新大小
    3.返回值为调整之后的内存起始位置。
    4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
    realloc在调整内存空间的是存在两种情况:
    第一种情况:原有空间之后有足够大的空间,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
    第二种情况:原有空间之后没有足够大的空间,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

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

    #include
    #include
    #include
    #include
    int main()
    {
    	//开辟40个字节空间
    	int* p = (int*)malloc(40);
    	if (NULL == p)
    	{
    		printf("%s\n", strerror(errno));
    		return 1;
    	}
    	//使用
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i + 1;
    	}
    	//扩容
    	int* ptr = (int*)realloc(p, 80);//追加40个字节,80是新大小
    	if (ptr != NULL)
    	{
    		p = ptr;
    		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
    • 30
    • 31
    • 32
    • 33

    在这里插入图片描述

    realloc(NULL,40);//与malloc(40);等价
    
    • 1

    3.常见的动态内存错误

    3.1 对NULL指针的解引用操作

    #include
    #include
    #include
    void test()
    {
    	int* p = (int*)malloc(INT_MAX / 4);
    	*p = 20;//如果p的值是NULL,就会有问题
    	free(p);//p变成野指针
    	//free(p);后面应该加上p=NULL;
    }
    int main()
    {
    	test();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    正确代码:

    #include
    #include
    #include
    void test()
    {
    	int* p = (int*)malloc(INT_MAX / 4);
    	if (NULL == p)
    	{
    		return 1;
    	}
    	*p = 20;
    	free(p);
    	p = 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

    INT_MAX是int类型的最大整数,头文件为limits.h

    #define INT_MAX       2147483647
    
    • 1

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

    #include
    #include
    void test()
    {
    	int i = 0;
    	int* p = (int*)malloc(10 * sizeof(int));
    	if (NULL == p)
    	{
    		exit(EXIT_FAILURE);
    	}
    	for (i = 0; i <= 10; i++)//应该改为i<10
    	{
    		*(p + i) = i;//当i是10的时候越界访问
    	}
    	free(p);
    	p = 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

    EXIT_FAILURE是C语言头文件库中定义的一个符号常量,在vc++6.0下头文件stdlib.h中定义如下: #define EXIT_FAILURE 1 可以作为exit()的参数来使用,表示没有成功地执行一个程序。

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

    #include
    #include
    void test()
    {
    	int a = 10;
    	int* p = &a;
    	//...
    	free(p);
    }
    int main()
    {
    	test();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    free只能释放动态内存的释放和回收的。
    在这里插入图片描述

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

    #include
    #include
    #include
    void test()
    {
    	int* p = (int*)malloc(100);
    	for (int i = 0; i < 10; i++)
    	{
    		*p = i;
    		p++;
    	}
    	free(p);//p不再指向动态内存的起始位置
    	p = 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

    此时会造成内存泄露,没有进行判断p是否为NULL,对空指针解引用会报错的。
    正确代码:

    #include
    #include
    void test()
    {
    	int* p = (int*)malloc(100);
    	if (NULL = P)
    	{
    		return;
    	}
    	for (int i = 0; i < 10; i++)
    	{
    		*(p + i) = i;
    	}
    	free(p);//p不再指向动态内存的起始位置
    	p = 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

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

    #include
    #include
    void test()
    {
    	int* p = (int*)malloc(100);
    	//...
    	free(p);
    	//...
    	free(p);//重复释放
    }
    int main()
    {
    	test();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    对同一块内存多次释放:可以在第一次释放后面加上p=NULL来改正。
    正确代码:

    #include
    #include
    void test()
    {
    	int* p = (int*)malloc(100);
    	if(NULL = P)
    	{
    		return;
    	}
    	//...
    	free(p);
    	p = NULL;
    	//...
    	free(p);//重复释放
    }
    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

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

    #include
    #include
    void test()
    {
    	int* p = (int*)malloc(100);
    	if (NULL != p)
    	{
    		*p = 20;//忘记了释放开辟的动态内存
    	}
    }
    int main()
    {
    	test();
    	while (1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    free(p);
    p = NULL;
    
    • 1
    • 2

    4.几个经典的笔试题

    4.1 笔试题一

    void GetMemory(char* p)
    {
    	p = (char*)malloc(100);
    }
    void Test(void)
    {
    	char* str = NULL;
    	GetMemory(str);
    	strcpy(str, "hello world");
    	printf(str);//与printf("%s\n",str);等价
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请问运行Test 函数会有什么样的结果?
    str指针变量作为实参,p指针变量作为形参,传值调用不会改变实参的值,即str一样是NULL,对空指针解引用,程序会崩溃。而且指针变量P在出GetMemory后,指针变量p会被销毁,而所申请的动态内存没有被释放会造成内存泄漏。
    所以Test函数不会有打印结果。
    在这里插入图片描述
    正确代码:

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

    在这里插入图片描述

    4.2 笔试题二

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

    请问运行Test 函数会有什么样的结果?
    这是返回栈空间的地址的问题,p在出GetMemory后,p数组的内存空间会被回收,所以strb变成野指针,对野指针解引用是很危险的行为,无法预判打印什么。
    在这里插入图片描述

    4.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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请问运行Test 函数会有什么样的结果?
    没有释放所开辟的动态内存,造成内存泄漏。没有进行判断是否开辟成功。
    Tset函数运行的结果是会打印hello
    正确代码:

    #include
    #include
    void GetMemory(char** p, int num)
    {
    	*p = (char*)malloc(num);
    }
    void Test(void)
    {
    	char* str = NULL;
    	GetMemory(&str, 100);
    	if (NULL == str)
    	{
    		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

    4.4 笔试题四

    void Test(void)
    {
    	char* str = (char*)malloc(100);
    	strcpy(str, "hello");
    	free(str);
    	if (str != NULL)
    	{
    		strcpy(str, "world");
    		printf(str);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请问运行Test 函数会有什么样的结果?
    str是被释放后是野指针,非法访问,会出问题。
    但是Test函数运行结果还是会打印world。
    在这里插入图片描述
    应该在free(str);后面加上str = NULL;

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

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

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

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

    实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
    但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

    6.柔性数组

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

    例如:

    typedef struct S
    {
    	int i;
    	int arr[0];//柔性数组成员
    }ty_S;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    有些编译器会报错无法编译可以改成:

    typedef struct S
    {
    	int i;
    	int arr[];//柔性数组成员
    }ty_S;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    6.1 柔性数组的特点

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

    #include
    typedef struct S
    {
    	int i;
    	int arr[];//柔性数组成员
    }ty_S;
    int main()
    {
    	printf("%d\n", sizeof(ty_S));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    6.2 柔性数组

    #include
    #include
    typedef struct S
    {
    	int i;
    	int arr[];
    }type_a;
    int main()
    {
    	//代码1
    	int i = 0;
    	type_a* p = (type_a*)malloc(sizeof(type_a) + 10 * sizeof(int));
    	if (p == NULL)
    	{
    		perror("p");
    		return 1;
    	}
    	//业务处理
    	p->i = 100;
    	for (i = 0; i < 10; i++)
    	{
    		p->arr[i] = i;
    	}
    	for (i = 0; i < 10; i++)
    	{
    		printf("%d ", p->arr[i]);
    	}
    	//增容
    	struct S* ptr = (struct S*)realloc(p, sizeof(struct S) + 80);
    	if (ptr == NULL)
    	{
    		perror("ptr");
    		return 1;
    	}
    	p = ptr;
    	ptr = NULL;
    	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
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    在这里插入图片描述
    这样柔性数组成员a,相当于获得了10个整型元素的连续空间,并且增容到了20个整型元素的连续空间。

    6.2.1 perror

    void perror ( const char * str );
    
    • 1

    在这里插入图片描述

    #include
    int main()
    {
    	int* p = (int*)malloc(sizeof(int) * 1000000000000000000);
    	if (p == NULL)
    	{
    		perror("p");
    		return 1;
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    6.3 柔性数组的优势

    上述的 type_a 结构也可以设计为:

    //代码2
    #include
    #include
    typedef struct st_type
    {
    	int i;
    	int* p_a;
    }type_a;
    int main()
    {
    	type_a* p = (type_a*)malloc(sizeof(type_a));
    	if (p == NULL)
    	{
    		perror("p");
    		return 1;
    	}
    	p->i = 100;
    	p->p_a = (int*)malloc(p->i * sizeof(int));
    	if (p->p_a == NULL)
    	{
    		perror("p->p_a");
    		return 1;
    	}
    	//业务处理
    	int i = 0;
    	for (i = 0; i < 100; i++)
    	{
    		p->p_a[i] = i;
    	}
    	//释放空间
    	free(p->p_a);
    	p->p_a = NULL;
    	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
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    在这里插入图片描述
    上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处

    1.方便内存释放

    如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
    用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
    不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
    了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

    2.这样有利于访问速度.

    连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实觉得也没多高了,反正跑不了要用做偏移量的加法来寻址)

    7.写在最后

    那么动态内存管理到这里就结束啦。既然学到了动态内存,那么再过两章,小沐的C语言零基础学习专栏也差不多该结束,以后补充的C语言大概率是根据小沐学的《C语言深度解剖》来围绕啦。C语言后就是C++啦!学海无涯啊!
    在这里插入图片描述

  • 相关阅读:
    CopyOnWriteArrayList 源码详解
    Unity中Shader中UI材质去色功能实现
    【HTML】筑基篇
    用公众号给女朋友推送早安问候(恋爱值+++++)
    AC8015笔记
    qml介绍
    B46 - STM32太阳能充电智能心率监测骑行仪
    若依框架使用mars3d的环境配置,地球构建
    2.13每日一题(根号下的定积分及去绝对值的定积分)
    【C语言】文件相关函数详解
  • 原文地址:https://blog.csdn.net/m0_68931081/article/details/125747937