目录
一、前言
二、动态内存的简单介绍
什么是动态内存分配
为什么要使用动态内存分配
三、动态内存函数的介绍和拓展
malloc()函数
free()函数
calloc()函数
realloc()函数
四、常见动态内存分配的错误
五、共勉
一、前言
在学习动态内存分配时,感觉这些动态分配没什么用,也就没在意跳过去了,直到碰到数据结构和一些需要动态数组的题目时才知道,动态内存分配的重要性。这次专门花了一早上的时间来学习了动态内存分配,并将它分享出来,希望对大家有帮助哦!!!!
二、动态内存的简单介绍
什么是动态内存分配
知识点1:
目前在我们平时写代码的过程中接触最多的就是在栈空间上开辟连续的空间:
// 在栈上开辟4个字节int val = 20; // 在栈空间上开辟10个字节的连续空间char arr[10] = {0};
此时我们需要注意在栈空间上的开辟空间的特点:
▶空间开辟的大小是固定的。
▶数组在声明时必须指定数组的长度,在编译时会开辟并分配其所需要的内存空间。知识点2:
所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
为什么要使用动态内存分配
知识点1:
之前学习了数组,数组的元素储存在内存中连续位置。在声明数组时必须要指定数组的元素个数,即数组空间大小在声明时已经确定了。但是需存放的元素个数常常在运行时才能知道(取决于输入的数据)。此时应用栈去申请内存空间的缺点就出现了:
▶1. 当输入元素个数大于数组声明的元素个数时会带来意想不到错误
▶2. 当输入元素个数小于数组声明的元素个数时会带来内存空间的浪费
▶3.数组大小不能动态调整
总结:有时我们需要的空间大小在程序运行的时候才能知道,这时在数组编译时开辟空间的方式就不能满足了,这时我们就需要动态内存开辟来解决问题。
三、动态内存函数的介绍和拓展
malloc()函数
知识点1:
malloc()函数的头文件:#includemalloc()函数的声明:
函数声明的解释:
▶ size_t 表示无符号类型 size:表示申请分配的内存大小,单位为字节▶返回值:申请成功返回该空间起始地址,申请失败返回NULL指针,因为不知道申请的空间要存放什么类型数据所以返回void*类型
malloc()函数的功能介绍
malloc是C语言提供的一个动态内存开辟的函数,该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。具体情况如下:
▶如果开辟成功,则返回一个指向开辟好空间的指针。
▶如果开辟失败,则返回一个NULL指针。
▶如果开辟失败,则返回一个NULL指针。
▶如果size为 0(开辟0个字节),malloc的行为是标准未定义的,结果将取决于编译器。
注意:
1. malloc返回值有可能是NULL指针,使用前需要检查
2. malloc申请的空间并没有被初始化代码举例说明:
用malloc()来创建一个 数组。可以在程序运行时使用malloc()请求一个存储块,另外还需要一个指针来存放该块在内存中的位置。
int * ptd;ptd = (int * ) malloc (30 * sizeof(int));if (ptd == NULL) //空间申请失败则退出{return -1; }
代码解释:
▶这段代码请求30个 int类型 值的空间,并且把ptd指向该空间所在位置。
▶注意: ptd是作为指向一个 int类型值 的指针声明
的,而不是
指向30个 int类型 值的数据块的指针。
free()函数
知识点1:
free()函数的头文件:#includefree()函数的声明:
函数声明的解释:
ptr : 指向先前用malloc、calloc或realloc分配的内存块的指针
free()函数的功能:
释放申请的动态内存分配的空间(即malloc、calloc、realloc函数申请的空间)具体情况:
▶如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的。
▶如果参数ptr是NULL指针,那么free将不会执行任何动作。
注意事项:
▶使用完之后一定要记得使用free函数释放所开辟的内存空间。▶使用指针指向动态开辟的内存,使用完并free之后一定要记得将其置为空指针,防止越界访问。
原因:free()函数只会释放ptr指向空间的值,但ptr本身不会被置空。
代码举例:
#include #include int main(){//申请10个int类型大小空间,10 * sizeof(int)相对于sizeof(40)更具有移植性//由于malloc返回值为void*类型,所以强制类型转换为int*类型int n; // 开辟空间数举例为 10printf("请输入想开辟的 int 数组大小\n");scanf("%d", &n);int* p = (int*)malloc(n * sizeof(int));if (p == NULL) {exit(EXIT_FAILURE); //p 现在指向有 n 个元素的数组}int i = 0;for (i = 0; i < 10; i++) //打印这10个元素{printf("%d ", *(p + i));}printf("\n");for (i = 0; i < 10; i++) //对数组元素赋值{p[i] = i;// 此时可以将 指针 p 看作数组名}for (i = 0; i < 10; i++) //打印这10个元素{printf("%d ", *(p + i));}printf("\n");free(p); //释放p所指向动态内存分配的空间p = NULL;//将p置为NULL指针,防止访问一个已释放的空间return 0;}
运行结果:
请输入想开辟的 int 数组大小:10-842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -8421504510 1 2 3 4 5 6 7 8 9
代码解释:
▶ 开始 指针p 指向 int 类型 开辟的40个字节空间,并没有赋值,所以输出的值都是随机的。
▶exit()函数。该函数的原型在 stdlib.h 中,用来在
内存分配失败时结束程序
。值EXIT_FAILURE也在这个头文件中定义。标准库提供了两个保证能够在所有操作系统 下工作的返回值:
EXIT SUCCESS(或者,等同于0)指示程序正常终止;
EXIT_FAILURE指示程序异常终止。
知识点2:内存泄露
大家是否有跟我一样的想法,目前我们知道了动态内存的开辟函数 malloc() ,之后便出现了释放函数 free(),我有点好奇,在我开辟函数之后,我不去释放,你能把我怎样呢?抱着这样的想法,我做了一个实验:
#include#includevoid gobble (double ar[], int n);int main(){ double glad[2000];int i; for(i = 0; i<100000000; i++){gobble(glad, 2000);}}void gobble(double ar[], int n){double *temp = (double *) malloc(n*sizeof(double)) ;//free(temp);/*忘记使用*///我就皮,我就用}
代码解释:
▶ 第一次调用gobble()时,它创建了指针temp,并使用malloc()为之分配16000字节的内存(设double是8个字节)。
假定我们如暗示的那样没有使用free()。
当函数终止时,指针temp作为一个自动变量消失了。
但它所指向的16000个字节的内存仍旧存在。
我们无法访问这些内存,因为地址不见了。
由于没有调用free(),不可以再使用它了。▶第二次调用gobble(),它又创建了一个temp,再次使用malloc()分配16000个字节的内存。
第一个16000字节的块已不可用,因此malloc()不得不再找一个l6000字节的块。
当函数终止时,这个内存块也无法访问,不可再利用。▶但循环执行了1000次,因此在循环最终结束时,已经有1600万字节的内存从内存池中移走。事实上,在到达这一步前,程序很可能已经内存溢出了。
▶这类问题被称为
内存泄漏(memory leak)
,可以通过在函数末尾处调用free()
防止该问题出现。
calloc()函数
知识点1:
calloc()函数的头文件:#includecalloc()函数的声明:
解释函数声明:
▶num:元素个数▶size: 元素大小
▶返回值:申请成功返回该空间起始地址,申请失败返回NULL指针,因为不知道申请的空间要存放什么类型数据所以返回void*类型。
calloc()函数的功能:
calloc函数与malloc函数功能一样,区别主要在于calloc会对分配的空间初始化为0,另外它们请求内存大小的方式不同。验证malloc()函数与calloc()函数的区别:
malloc()函数:#include #include int main(){// mallocint* p = (int*)malloc(40); // 开辟40个空间if (p == NULL) {exit(EXIT_FAILURE); //p 现在指向有 10 个元素的数组}int i = 0;for (i = 0; i < 10; i++)printf("%d ", *(p + i));free(p);p = NULL; return 0;}
运行结果:
// 随机的 10 个值
calloc()函数:
#include #include int main(){// callocint* p = (int*)calloc(10, sizeof(int)); // 开辟10个大小为int的空间,40 if (p == NULL) {exit(EXIT_FAILURE); //p 现在指向有 10 个元素的数组}int i = 0;for (i = 0; i < 10; i++)printf("%d ", *(p + i));free(p);p = NULL; return 0;}
运行结果:
0 0 0 0 0 0 0 0 0 0
总结:
说明calloc会对内存进行初始化,把空间的每个字节初始化为 0 。如果我们对于申请的内存空间的内容,要求其初始化,我们就可以使用calloc函数来轻松实现。
realloc()函数
知识点1:
realloc()函数的头文件:#includerealloc()函数的声明:
声明的解释:
▶ptr:指向先前用malloc、calloc或realloc分配的内存块的指针
▶size:动态内存空间新大小,单位为字节
▶返回值:返回调整后空间的起始地址,调整失败返回NULL指针
realloc()函数的功能:
让动态内存管理更加灵活。用于重新调整之前调用malloc或calloc所分配的ptr所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。realloc()函数调整内存的 4 中情况:(看图解)
▶ 情况一:在原有的基础上扩大空间原有空间之后有足够大的空间。
▶ 情况二:原有空间之后没有足够大的空间。
原空间之后没有足够多的空间满足扩展需求,在内存上另外寻找一个适合大小的来连续空间来使用,并将原空间数据先复制过来然后释放空间。这样函数返回的是一个新空间的内存地 址
▶ 情况三:缩小原有的空间
原空间尾部的部分空间被释放,剩余空间数据依旧保留。
▶ 情况四:
代码演示:
realloc()调整函数大小:#include #include int main(){int* p = (int*)calloc(10, sizeof(int));if (p == NULL){exit(EXIT_FAILURE); //p 现在指向有 10 个元素的数组}// 使用int i = 0;for (i = 0; i < 10; i++) {*(p + i) = 5;}// 此时,这里需要p指向的空间更大,需要20个int的空间// realloc 调整空间p = (int*)realloc(p, 20 * sizeof(int)); // 调整为20个int的大小的空间// 释放free(p);p = NULL;}
❗ 疑问: 刚才提到的第四种情况,如果realloc找不到合适的空间,就会返回空指针。我们想让它增容,他却存在返回空指针的危险,这怎么行?
代码优化:
#include #include int main() {int* p = (int*)calloc(10, sizeof(int));if (p == NULL){exit(EXIT_FAILURE); //p 现在指向有 n 个元素的数组}// 使用int i = 0;for (i = 0; i < 10; i++) {*(p + i)= 5;}// 此时,这里需要 p 指向的空间更大,需要 20 个int的空间// realloc 调整空间int* ptmp = (int*)realloc(p, 20*sizeof(int));// 如果ptmp不等于空指针,再把p交付给它if (ptmp != NULL) {p = ptmp;} // 释放free(p);p = NULL;return 0;}
代码演示全过程;
#include #include #include #include int main(){int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){exit(EXIT_FAILURE); //p 现在指向有 10 个元素的数组}int i = 0;for (i = 0; i < 10; i++) //对数组元素赋值{*(p + i) = i;}int* ptr = (int *)realloc(p, 20 * sizeof(int)); //对动态内存大小进行调整if (ptr == NULL) //调整失败并不影响原本p指向空间{printf("空间调整失败\n");}else{p = ptr; //调整成功,p指向调整后空间起始地址ptr = NULL;for (i = 10; i < 20; i++) //对数组元素赋值{*(p + i) = i;}for (i = 0; i < 20; i++) //打印数组元素{printf("%d ", *(p + i));}}free(p); //释放p所指向动态内存分配的空间p = NULL;//将p置为NULL指针,防止访问一个已释放的空间return 0;}
运行结果:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
知识点2:
有趣的是,其实你可以把 realloc 当 malloc 用:
// 在要调整的内存地址部分,传入NULL:int* p = (int*)realloc(NULL, 40); // 这里功能类似于malloc,就是直接在堆区开辟40个字节
四、常见动态内存分配的错误
知识点1:使用 free 释放一块动态开辟内存的一部分
错误代码演示:
#include #include int main(){int* p = (int *)malloc(10*sizeof(int)); if (p == NULL){exit(EXIT_FAILURE); //p 现在指向有 10 个元素的数组}int i = 0;for (i = 0; i < 5; i++) {*p++ = i; // p指向的空间被改变了} free(p);p = NULL;return 0;}
错误原因分析:
此时free(p)出现了大问题,释放的是后面的空间。不能从一块动态开辟的内存空间的某一部分释放,必须从头开始释放。
代码更新:
#include #include int main(){int* p = (int *)malloc(10 * sizeof(int));if (p == NULL){exit(EXIT_FAILURE); //p 现在指向有 10 个元素的数组}int* ptd = p;//提前将 p 指针与 ptd 指针进行替换int i = 0;for (i = 0; i < 5; i++) {*ptd++ = i; // ptd指向的空间被改变了}free(p);p = NULL;return 0;}
知识点2:
问题: 下列代码存在什么问题?请指出问题并做出相应的修改。#include #include #include void GetMemory(char *p) {p = (char*)malloc(100);} void Test() {char *str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);} int main() {Test();return 0;}
参考答案:str 传给 GetMemory 函数时为值传递,所以 GetMemory 函数的形参 p 是 str 的一份临时拷贝。在 GetMemory 函数内部动态开辟的内存空间的地址存放在了 p 中,并不会影响 str。所以当 GetMemory 函数返回之后, str 仍然是 NULL,导致 strcpy 拷贝失败。其次,随着 GetMemory 函数的返回,形参 p 随即销毁并且没有及时的使用 free 释放,从而导致动态开辟的100个字节存在内存泄露问题。根据经验,程序会出现卡死的问题。
修改代码:
#include #include #include // ↓ 修改返回类型为char*char* GetMemory(char *p) {p = (char*)malloc(100);return p; // 将p带回来} void Test() {char *str = NULL;str = GetMemory(str); // 用str接收,此时str指向刚才开辟的空间strcpy(str, "hello world"); // 此时copy就没有问题了printf(str);// 用完之后记得free,就可以解决内存泄露问题free(str);str = NULL; // 还要将str置为空指针} int main() {Test(); return 0;}
运行结果:
hello world
五、共勉
以下就是我对动态内存分配的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对数据结构——-顺序表的理解,请持续关注我哦!!!!!