为什么存在动态内存分配
- 之前开辟空间大小是固定的
- 数组在创建时必须制定数组长度。
动态内存分配,解决上面的问题。可以让内存分配更灵活。
动态内存函数介绍
malloc
动态内存开辟。需要引用stdlib.h头文件。
void* malloc(size_t size);
malloc会返回一个指针指向开辟好的空间,当申请空间失败时,会返回一个NULL。
注意⚠️:使用这个函数,一定要检查是否申请成功,因为它可能申请失败。
12345678910111213141516171819202122232425262728 | int main(){ //向内存申请10个整型空间 int* p = (int*)malloc(10*sizeof(int)); if (p==NULL) //检查是否申请成功 { printf("%s\n",strerror(errno)); } else { //开辟成功,可以使用空间 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;} |
打印的结果是0 1 2 3 4 5 6 7 8 9
把申请10个int型的空间修改为int* p = (int*)malloc(INTMAX_MAX);时,返回Cannot allocate memory没有足够的空间。
如果申请的size空间为0,这种行为是标准未定义的,取决于编译器。(不要这样写)
free
对动态开辟的空间进行回收和释放。void free(void *memblock);
12345678910111213141516171819202122232425 | int main(){ //向内存申请10个整型空间 int* p = (int*)malloc(40); if (p==NULL) { printf("%s\n",strerror(errno)); }else { //开辟成功,可以使用空间 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 = NULL;//将p赋值为空指针 return 0;} |
free (p);将空间释放。但是p的值是没有改变的。
使用free释放空间后最好运行:p = NULL;
free的空间必须是动态开辟出来的,如果不是动态开辟出来的,这种行为是标准未定义的。
当程序结束时,即使没写free函数,操作系统也会将空间回收♻️。
calloc
开辟空间,并把元素改成0。void *calloc(size_t num,size_t size);size_t num是元素个数,size_t size是每个元素的大小(字节)。
开辟空间,用指针返回,空间不足时,返回空指针NULL。
1234567891011121314151617181920212223 | int main(){ int* p = calloc(10, sizeof(int)); if (p==NULL) { printf("%s\n",strerror(errno)); } else { int i = 0; for (i = 0; i<10; i++) { printf("%d ",*(p+i)); } } //释放空间,用free函数。 free(p); p = NULL; return 0;} |
打印的结果是0 0 0 0 0 0 0 0 0 0
calloc和malloc的区别
希望开辟的空间内容为0,使用calloc;
希望开辟的空间不要初始化,使用malloc;
realloc
realloc让动态开辟的空间更灵活。
可以调整动态开辟空间的内存大小。
void *realloc(void *memblock,size_t size);void *memblock是之前开辟好的动态空间,size_t size是新的大小(字节)。
返回一个指针指向新开辟的内存块,
realloc使用注意事项:
- 如果p指向的空间之后有足够的内存空间,直接追加内存,后返回p
- 如果p指向的空间之后没有足够的内存空间,则realloc会重新找一块新的内存区域,开辟一块新的满足需求的空间,并把原先内存中的拷贝至新内存空间,释放旧内存空间,最后返回新开辟的内存空间地址。
- 使用新变量来接受realloc的返回值,因为它可能申请失败,使用新变量来避免之前保存的内容丢失。
- 当动态申请的空间不使用的时候,要还给操作系统。使用free函数。
12345678910111213141516171819202122232425262728293031323334353637383940414243 | int main(){ int *p = (int*)malloc(20); if ( p == NULL ) { printf("%s\n",strerror(errno)); } else { int i = 0; for (i=0; i<5; i++) { *(p+i) = i; } } //上面是使用malloc开辟的空间,20字节 //20个字节不够了,想要40个字节,使用realloc调整 int* p2 = realloc(p, 40); if (p2 == NULL) { //追加失败 printf("%s\n",strerror(errno)); } else { //追加成功 p = p2; int i = 0; for (i=5; i<10; i++) { *(p+i) = i; } for (i=0; i<10; i++) { printf("%d ",*(p+i)); } } //释放内存 free(p); p = NULL; return 0;} |
常见的动态内存错误
- 对空指针(NULL)的解引用操作
1234567891011121314151617 | int main(){ int *p = (int*)malloc(40); //万一malloc失败了,p会被赋值为NULL //所以使用malloc返回后必需要进行判断! int i = 0; for (i=0; i<10; i++) { *(p+i) = i; } free(p); p = NULL; return 0;} |
- 对开辟的空间进行越界访问
12345678910111213141516171819202122 | int main(){ int *p = (int*)malloc(5 * sizeof(int)); if (p==NULL) { return 0; } else { int i = 0; for (i=0; i<10; i++)//只开辟了5个整型的元素,越界访问 { *(p+i) = i; } } //释放 free(p); p = NULL; return 0;} |
- 对非动态开辟的空间使用free
12345678910 | int main(){ int a = 10; int* p = &a; *p = 20; free(p); p = NULL; return 0;} |
- 使用free释放一块动态开辟内存的一部分
1234567891011121314151617181920 | int main(){ int* p = (int*)malloc(40); if (p == NULL) { return 0; } int i = 0; for (i=0; i<10; i++) { *p++ = i; } //回收空间 free(p);//p改变了,经过了10次++,不再是原先的开辟的空间 p = NULL; return 0;} |
- 对同一块动态内存多次释放
123456789101112131415161718 | int main(){ int* p = (int*)malloc(40); if (p==NULL) { return 0; } //使用 //释放 free(p); //p = NULL; //... free(p); return 0;} |
为避免这种情况,谁开辟,谁回收。
但只要第一是释放之后,把p置成了空指针,后面的释放就没有意义了,程序可以正常运行。
- 动态开辟的内存忘记释放(内存泄露)
123456789101112 | int main(){ while (1) { malloc(1); } return 0;} |
C/C++程序的内存开辟
C/C++程序内存分配有4个区域
- 栈区(stack):在执行函数时,函数内局部变量的存储单元在栈上创建,函数结束时,被释放。栈区主要存放运行函数分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放,程序员不释放会由OS回收。分配方式类似链表
- 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体的二进制代码
普通的局部变量是在栈区分配空间的,栈区的特点就是创建的变量出了作用域就销毁了。
被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直至程序结束才销毁。
柔性数组
结构中最后一个元素允许是未知大小的数组,这就是柔性数组成员
1234567891011 | struct S{ int n; int arr[];//未知大小的 //int arr[0];//未知大小的 - 和上面是一样的,只是写法不同};int main(){ struct S s;} |
上面结构体S中的整型arr数组是未知大小的,就是柔性数组成员,柔性的意思就是:数组大小是可以调整的。
柔性数组的大小
12345678910 | struct S{ int n; int arr[];//未知大小的};int main(){ struct S s; printf("%d\n",sizeof(s));} |
打印的结果是4。
包含柔性数组成员的结构体计算大小时,不包含柔性数组成员的大小。
柔性数组的使用
柔性数组:
1234567891011121314151617181920212223242526272829303132 | struct S//先创建一个包含柔性数组成员的结构体{ int n; int arr[];//未知大小的};int main(){ struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int)) ;//开辟了24个字节,强制转换为struct S*型。其中有4字节空间给n,另外20字节给arr数组。 ps->n = 100;//令n=100 int i = 0; for (i=0; i<5; i++) //给这个柔性数组中的5个成员赋值 { ps->arr[i] = i; } struct S* ptr = realloc(ps, 44);//想开辟另外20字节的空间给这个柔性数组 if (ptr != NULL) //判断空间是否申请成功 { ps = ptr; } for (i=5; i<10; i++) //给柔性数组中新开辟的5块arr数组赋值 { ps->arr[i] = i; } for (i=0; i<10; i++) //打印这个arr柔性数组 { printf("%d",ps->arr[i]); } //释放空间 free(ps); ps = NULL; return 0;} |
使用指针维护的结构体数组数组(和柔性数组实现的功能一致):
1234567891011121314151617181920212223242526272829303132333435363738 | struct S{ int n; int *arr;//指针 - 指向一块动态开辟的空间};int main(){ struct S *ps = (struct S*)malloc(sizeof(struct S));//开辟一块空间,包括n和*arr指针的空间,ps指针指向,强转为(struct S*)型 ps->arr = malloc(5*sizeof(int));//给*arr这个指针指向的地址开辟5块int大小的空间 int i = 0; for (i=0; i<5; i++) //给数组内的5个成员赋值 { ps->arr[i] = i; } for (i=0; i<5; i++) //打印数组内的五个成员 { printf("%d ",ps->arr[i]); } int *ptr = realloc(ps->arr, 10*sizeof(int));//调整arr数组的大小,先用ptr指针指向 if (ptr != NULL) //判断内存申请是否成功 { ps->arr = ptr; } for (i=5; i<10; i++) //给新创建的五个数组成员赋值 { ps->arr[i] = i; } for (i=0; i<10; i++) //打印所有数组成员 { printf("%d ",ps->arr[i]); } //释放内存 free(ps->arr);//先释放arr数组的内存,避免找不到它 ps->arr = NULL; free(ps);//后释放ps。 ps = NULL; return 0;} |
柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其他成员
- sizeof返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用
malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
优点:
- 方便内存释放
- 有利于访问速度