前言
在使用c语言时,我们经常需要创建一个(些)空间来存放数据,常见的开辟空间方式有2种,即创建一个变量来存放数据 int a = 10; 或是创建一个数组来存放数据int arr[10]; ,但这两种方式在开辟空间时也有一定的局限性,只要一创建,这个空间的大小就已经被指定,不能再根据需要改变其大小。
同时,在计算机中内存是十分宝贵的,若全是静态内存的话,运行较小的程序倒是没什么问题,但若是运行较大的程序时就可能出现程序还没运行完,内存就已经被全部占用完了,所以,为了避免这类问题的发生,我们可以进行动态分配内存。
动态内存函数
动态内存函数,顾名思义,就是来管理动态内存的函数;
这些函数包括
开辟动态内存的函数(malloc,calloc),
修改所开辟的动态内存大小的函数(realloc),
释放动态内存的函数(free)。
malloc函数
当需要开辟一块动态内存的时候,就可以使用malloc函数;
由图可知,malloc函数的功能为,向内存申请空间,若是申请成功则返回空间开头位置的地址;且所开辟的空间不进行初始化 - 保留初始的不确定的值。
该函数的参数为 size_t size ,即需要开辟空间的大小,单位为字节。
该函数的返回类型为void* ,确保所开辟的空间在使用过程中没有类型的限制。
若是空间开辟失败则返回一个空指针NULL。
既然返回的是void*类型的指针,该如何运用呢?
int*p = (int*)malloc(20);
若是利用malloc函数来开辟一块空间,在使用时需要将它强制类型转换为需要使用的类型并赋给相应类型的指针;
同时,为了避免开辟动态空间失败导致的对空指针NULL非法解引用,应在开辟之后对开辟的空间进行判断,判断其是否为空指针;
正确的使用方法:
int main() { int* p = (int*)malloc(20); if (p == NULL) { return 1; } return 0; }
开辟动态空间后使用对应类型的指针对返回的地址进行接收,再将接收的地址进行判断,若是为空指针即开辟空间失败,提前退出程序。
也可以使用strerror函数将错误信息打印出来。
动态内存的使用
动态空间开辟后可以对该空间进行使用,例如同上代码,现在需要在开辟的空间内存放1-5五个整形。
int main() { //创建 int* p = (int*)malloc(20); if (p == NULL) { return 1; } //使用 for (int i = 0; i < 5; i++) { *(p + i) = i + 1; } for (int i = 0; i < 5; i++) { printf("%d ", p[i]); } return 0; }
free函数
在一块开辟的动态空间,我们可以对这块空间进行使用,当然,若是这块空间已经用完的话,应将这块空间及时的进行释放;
若是开辟并已经使用完的内存并没有对其及时释放,这块内存则会一直被占用,直至程序运行结束。
int main() { //创建 int* p = (int*)malloc(20); if (p == NULL) { return 1; } //使用 for (int i = 0; i < 5; i++) { *(p + i) = i + 1; } for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); } //释放 free(p); //对指针进行置空 p = NULL; return 0; }
同时,为了避免指针所指向的空间被提前释放而造成的野指针,应及时将指针置为空指针。
calloc函数
calloc函数与malloc函数类似,都是向内存申请一块空间,但较为不同的是,calloc函数所申请的空间更类似于一个数组,也为一个连续的空间,且在申请空间后,calloc函数会将所申请的空间初始化为0;
calloc函数与malloc函数与之不同的是参数不同以及在开辟空间后calloc函数会将所开辟的空间初始化为0;
calloc函数的参数共有两个:
- size_t num
无符号类型的需要开辟空间的个数。 - size_t size
每个空间的大小是多少(单位字节)。
这就是为什么相比之下calloc函数开辟动态内存空间更类似于创建一个数组。
int main() { //开辟 int*p= (int*)calloc(10,sizeof(int)); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //使用 //释放 free(p); //置空 p = NULL; return 0; }
calloc函数在开辟内存过程中也存在着失败的风险,所以应在开辟后判断函数的返回值是不是为空指针NULL,若是为空指针则开辟失败。
同时,calloc函数与malloc函数在使用时相同,已经用完的动态内存应及时进行释放,且指向已经释放空间的那个指针也将及时的置空,避免出现内存泄漏与出现野指针的问题。
realloc函数
动态内存的方便不仅在于随用随开辟,不用则释放;同时也在于开辟的动态内存可以根据使用需求进行适应的大小调整;
若是在使用过程中觉得开辟的内存不够使用需要扩大内存时则需要使用realloc函数来调整开辟的动态内存大小。
由图可知,realloc函数内共有两个参数:
- void*ptr
需要改变的内存块的起始位置地址。 - size_t size
需要将内存块改变的大小。
且该函数返回类型也为void*。
该函数若是修改成功时,则会返回这个新空间的起始地址。
失败则会返回一个空指针。
int main() { //创建 int* p = (int*)malloc(20); if (p == NULL) { return 1; } //使用 for (int i = 0; i < 5; i++) { *(p + i) = i + 1; } printf("realloc之前:"); for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); } //realloc扩大 p = (int*)realloc(p, 40); //释放 free(p); //对指针进行置空 p = NULL; return 0; }
上面代码在使用realloc函数中有一定问题,若是以上面的代码编写程序,会出现什么问题呢?
- 未判断函数返回值是否为空指针(修改大小是否成功):
realloc函数与malloc函数或calloc函数相同,也存在着失败的风险,故应在修改内存后判断函数的返回值是否为空指针,若是为空指针则代表在扩大空间的过程中出现了错误。 - 直接使用指向malloc函数开辟空间的指针接收:
直接使用原指针将改动后的空间地址进行接收,若是空间开辟失败,返回空指针,将会使原空间内的数据丢失,同时也将造成内存泄漏。
正确的使用方法√:
int main() { //创建 int* p = (int*)malloc(20); if (p == NULL) { return 1; } //使用 for (int i = 0; i < 5; i++) { *(p + i) = i + 1; } printf("realloc之前:"); for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); } //realloc扩大 int*ptr = (int*)realloc(p, 40); if (ptr != NULL) { p = ptr; } else { printf("%s\n", strerror(errno)); return 1; } //释放 free(p); //对指针进行置空 p = NULL; return 0; }
使用一个临时的指针接收函数返回的地址,并判断空间是否开辟(修改)成功(判断临时指针是否未空指针),若是成功,则使用原指针来接收该临时指针所指向的地址,否则提前退出程序或者使用原数据不再进行增容。
realloc函数的原理
在使用realloc函数时,应该明白该函数在扩容过程中,扩容成功会有两种情况:
若是在内存中,原动态开辟空间后的内存足够用来扩容这块空间时,将会直接在该空间后的内存进行扩容。
同时函数会返回原指针的地址。
若是原动态开辟空间后的内存不足进行扩容,realloc函数则会再其他位置重新再申请一块空间;
并将原空间内的数据拷贝至新的空间,并对原来的内存进行释放;
同时函数将返回新空间的地址;
注意事项
- 在使用动态内存函数开辟空间时,应注意在使用时要判断开辟空间是否成功,若是不成功则可能导致程序崩溃。
- 使用calloc、malloc、realloc函数开辟的动态内存应在用完后及时进行释放,如果不释放,在程序结束之后操作系统也会将这块空间进行回收;若是程序未结束,空间也没有释放,则会出现内存泄露的现象。
- 释放了内存后应及时将指向这块空间的指针置为空指针,确保指针不会在空间释放后变为野指针。
- 动态内存不能被部分释放。
- free函数不能用来释放非动态开辟内存,若是free函数释放非动态开辟内存,程序将崩溃。