【C语言】第十二章-动态内存管理

第十二章 动态内存管理

为什么会出现动态内存分配

  这个问题要结合数组来进行讨论。在C99之前的标准中,C语言中数组的定义要求必须给定常量的大小才能定义数组,也就是说数组变量的内存空间在编译时就已成定局,无法更改。那我们如果想要申请一块内存空间,空间大小在运行时才进行确定该怎么做呢?这就要用到动态的内存分配了,这里的空间大小的确定是在运行时才进行确定的。

动态内存分配函数

malloc和free

  在内存的动态分配中,首当其冲最为重要的两个函数就是mallocfree函数,它们挑起了动态内存分配的大梁。
  malloc函数负责向计算机申请一块连续的内存空间,并且返回指向该块内存的指针。

        void* malloc(size_t size);

  但是值得注意的是malloc所返回的指针的类型是void*型,所以在实际使用中我们往往要进行牵制类型转换将其转换为我们想要的指针类型。关于这个函数还有以下几点说明。

  1、如果开辟空间成功则返回一个指向该空间的指针。

  2、如果开辟空间失败则返回NULL

  3、如果参数为0则是未定义行为。

  4、返回类型为void*所以编译器并不知道所开辟的空间的类型,由使用者决定。

  既然开辟了空间,并且这个空间编译器不会帮我们自动将其释放那么就必然要求我们在使用完毕后将其释放,不浪费内存空间不造成泄露,因此就出现了free函数。

        void free (void* ptr);

  1、如果参数ptr的不是动态开辟的则行为是未定义的。

  2、如果参数ptr == NULL则函数什么都不做。

  mallocfree函数都在stdlib.h头文件中。

  以下举一个动态分配内存并且释放的例子。

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
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
printf("请输入要分配的数组的内存空间的大小:");
scanf("%d", &n);
int* array = (int*)malloc(sizeof(int) * n);
printf("分配成功!\n");
printf("请给数组赋值:");
for(int i = 0; i < n; i++)
{
scanf("%d", &array[i]);
}
for(int i = 0; i < n; i++)
{
printf("%d\t", array[i]);
}
printf("\n");
free(array);
array = NULL;
printf("数组已经释放!\n");
}


[misaki@localhost test]$ ./Main
请输入要分配的数组的内存空间的大小:8
分配成功!
请给数组赋值:1
2
3
4
5
6
7
8
1 2 3 4 5 6 7 8
数组已经释放!

  在以上这个例子中我们动态地在程序运行时对一个数组进行了空间的分配,并且让用户对数组进行了赋值,之后打印,数组使用完毕后我们又用free函数将其空间释放。这是一个很简单但是很实用的动态内存分配的例子。在这个例子中要尤为记住的一点是我们在进行动态的内存分配后一定不能忘记在使用完毕后将内存空间释放,并且将指针赋值为NULL,这一点是十分关键的,否则将造成内存泄漏和野指针,对程序造成很大的影响。

  往往内存泄漏不是我们忘记free而是不经意间造成的,以下就是一个典型的内存泄漏的例子。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int));
p = (int*)malloc(sizeof(int));
free(p);
p = NULL;
}

  这个例子中我们明明进行了释放却也造成了内存泄漏,这是因为我们申请了两次内存空间,但是用同一个指针来接收,只释放了一次,因此造成了内存的泄漏。因此我们在实际使用中牵扯动态内存分配的时候都一定要小心又小心。

calloc

  C语言还提供了一个函数进行动态内存分配。这个函数与malloc函数很相似,但是有一点不同的是这个函数会自动进行初始化。但是初始化有时候也不尽然全是好事,当我们要申请一个特别大的空间时,初始化会浪费很多很多的时间。

      
void* calloc(size_t num, size_t size);

  calloc的功能是分配num个大小为size的内存空间,并将内存空间初始化为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
#include <stdio.h>  
#include <stdlib.h>
int main()
{
int n = 0;
printf("请输入数组长度:");
scanf("%d", &n);
int* array = (int*)calloc(n, sizeof(int));
for(int i = 0; i < n; i++)
{
printf("%d\n", array[i]);
}
free(array);
}


[misaki@localhost test]$ ./Main
请输入数组长度:8
0
0
0
0
0
0
0
0

  在以上这个例子中我们看到calloc确实可以进行动态内存分配并且进行了初始化,但是这也是又两面性的。因此也要谨慎使用。

realloc

  当我们将内存空间进行动态分配后我们如果想要扩大我们分配的内存该怎么做呢?我们可以分配一块新的内存然后将原来内存中的数据再放到新的内存中。但是在C语言标准库中为了方便已经给我们准备了这么一个扩容函数realloc

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

  ptr是要调整大小的内存空间,size是调整后的大小。至于计算机是怎么进行扩容的呢?这里要分两种情况。

  1、如果目标内存空间后面有足够大小的空间则直接将后面的空间归入目标空间中即可。

  2、如果目标空间后面没有足够大小的空间则在内存中重新寻找一片足够大小的空间进行开辟并且将原来的数据放入到新的空间中。

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
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
printf("请输入数组的长度:");
scanf("%d", &n);
int* array = (int*)malloc(sizeof(int) * n);
printf("请输入目标扩容的大小:");
scanf("%d", &n);
array = (int*)realloc(array, n * sizeof(int));
printf("扩容成功!\n");
printf("请给数组赋值:");
for(int i = 0; i < n; i++)
{
scanf("%d", &array[i]);
}
printf("打印数组:");
for(int i = 0; i < n; i++)
{
printf("%d\n", array[i]);
}
}


[misaki@localhost test]$ ./Main
请输入数组的长度:5
请输入目标扩容的大小:8
扩容成功!
请给数组赋值:1
2
3
4
5
6
7
8
打印数组:1
2
3
4
5
6
7
8

  以上这个例子我们成功对已经分配的内存空间进行了扩容。

常见动态内存错误

  1、对NULL指针的解引用操作。

  2、对动态开辟空间越界访问。

  3、对非动态内存使用free释放。

  4、释放一块动态开辟内存的一部分。

  5、对同一块内存多次释放。

  6、动态开辟内存忘记释放。

  以上的错误都是十分常见的,因此我们在对内存进行操作的时候一定要万分小心。

C/C++程序内存开辟

  在这里将详细介绍下计算机内存中的几个区域。

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

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

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

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

  通常我们定义的变量都存在栈区,动态分配内存时变量的空间都是在堆区上进行分配的,堆区有着更大更充足的空间。由此一来我们对动态内存分配就有了更深的了解。

柔性数组

  柔性数组是在C99中最新的语法,其允许在结构体中最后一个成员是一个位置大小的数组,这就是柔性数组

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
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
int main()
{
type_a* p = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 10);//为结构体分配内存空间
p->i = 0;
for(int i = 0 ;i < 10; i++)
{
p->a[i] = 0;
}
for(int i = 0; i < 10; i++)
{
printf("%d\n", p->a[i]);
}
free(p);
}


[misaki@localhost test]$ ./Main
0
0
0
0
0
0
0
0
0
0

  在以上这个例子中我们使用了柔性数组,在使用柔性数组的时候我们要对结构体进行动态内存分配,并且分配的空间要大于结构体除柔性数组外的空间。

  柔性数组的特点:

  1、结构体中柔性数组前面必须至少一个其他成员。

  2、sizeof测量结构体大小不包括柔性数组的大小。

  3包含柔性数组的结构体用malloc进行内存动态分配,并且分配的内存大于结构体大小以适应 预期大小。

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助