动态内存管理
为什么要有动态内存分配
我们已经掌握的内存开辟⽅式有:
int val = 20; //在栈空间上开辟四个字节
char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间
但上述的开辟空间的⽅式有两个特点:
• 空间开辟⼤⼩是固定的。
• 数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整
一旦申请好空间 大小便无法调整了
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知道,那数组的编译时开辟空间的⽅式就不能满⾜了。
C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,这样就⽐较灵活了。
但灵活的同时 也会带来一些问题
动态内存管理的4个函数malloc free calloc realloc
接下来我们来一一学习
malloc:动态内存开辟函数
在使用malloc时 我们需包含一个头文件#include<stdlib.h>
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
如果开辟成功,则返回⼀个指向开辟好空间的指针。
如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。
在释放内存后 我们最好让指针arr置为空 原因我们放在下一节知识点内讲解
我们的指针arr指向分配空间的起始地址
free
C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:
free函数⽤来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那么free函数的⾏为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头⽂件中。
传递给free函数的是要释放的内存空间的起始地址(所以如果我们写arr++ 再释放空间 这里就会出现问题了)
free只是把开辟的空间的使用权限还给了操作系统 此时我们的指向该空间的指针变成了野指针 所以在释放空间后 要将指针置空
举个例⼦
calloc
C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。原型如下:
函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
举个例⼦:
这里我们就知道了 当我们想给所分配空间的数赋值就用calloc 不想赋初值就可以用malloc
realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使⽤内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。
函数原型如下:
ptr 是要调整的内存地址
size 调整之后新⼤⼩
返回值为调整之后的内存起始位置。
这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有⾜够⼤的空间
情况2:原有空间之后没有⾜够⼤的空间
情况2:
1.在堆区的内存中找一个新的空间 并且新的空间大小要求满足
2.会将原来空间的数据拷贝一份到新的空间
3.释放旧的空间
4.返回新的内存空间的起始地址
如果调整失败了的话 返回的是NULL
情况1
当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
情况2
当是情况2的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。
由于上述的两种情况,realloc函数的使⽤就要注意⼀些
代码1--情况1:如果原有空间之后有足够的空间,可以直接将realloc的返回值赋给ptr。
这行代码试图将ptr指向的内存块大小从100字节增加到1000字节。如果原有内存块之后有足够的连续空间,realloc就会扩展原有内存块的大小并返回指向它的指针。如果成功,ptr将指向新的更大的内存块。
代码2--情况2:如果原有空间之后没有足够的空间,realloc可能会移动内存块到另一个有足够空间的位置。因此,在调用realloc时,不应该直接覆盖原来的指针ptr,而应该先将realloc的返回值保存在一个临时指针(如p)中。
这段代码首先定义了一个临时指针p,并将realloc的返回值赋给它。如果realloc返回NULL,则释放操作失败,程序返回1。否则,将p(也就是realloc返回的新指针)赋给ptr,这样ptr就指向了新的内存块。
常⻅的动态内存的错误
对NULL指针的解引⽤操作
要判断malloc的返回值是否为NULL
当然assert也是可以的 assert(p)
对动态开辟空间的越界访问
对⾮动态开辟内存使⽤free释放
使⽤free释放⼀块动态开辟内存的⼀部分
p不再指向所分配的连续空间的起始地址
这里free就会出错
对同⼀块动态内存多次释放
为了避免这一问题 我们要养成在释放空间后 令指针置空
置空后 再释放不会有其他影响
动态开辟内存忘记释放(内存泄漏)
在test函数内 p是个局部变量 p指向分配空间的地址 当函数调用结束 p这个局部变量不再存在 但它所指向的内存块并没有得到释放 这时候就发生了内存泄露 最终导致这块内存不能被再次使用 且随着时间的推移 可能会消耗掉系统所有的可用内存
在函数内开辟空间 在函数调用结束后 找不到分配的空间 而这时候我们又不释放空间 我们再想使用这块空间 便无法使用
所以牢记 在不使用这块空间时 记得释放掉该空间
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
尽量要做到
1.谁(函数...)申请的空间谁释放
2.如果不能释放 要告诉使用的人 记得释放(不释放掉原先空间 便无法使用)
再给大家看一个牛马代码
动态内存经典笔试题分析
题⽬1:
现在我们对这段代码进行分析
在GetMemory这个函数 其接受一个字符指针 p 作为参数,并试图为它分配100字节的内存。但是,这里有一个重要的问题:在C语言中,函数参数是按值传递的,这意味着 p 是一个局部变量,它只是原始指针的一个副本。p和str并不指向同一地址,所以当在函数内部修改 p 时(如 p = (char*)malloc(100);),它只改变了这个副本(只对p进行了某某操作,str并不会改变),而不是原始的指针。因此,当 GetMemory 函数返回时,原始的 str 指针在 Test 函数中仍然是 NULL
在 Test 函数中,首先定义了一个字符指针 str 并初始化为 NULL。然后调用 GetMemory 函数,试图为 str 分配内存。但由于前面提到的 GetMemory 函数的问题,str 仍然是 NULL。接下来,strcpy(str, "hello world"); 将尝试将字符串 "hello world" 复制到 str 指向的位置,但因为 str 是 NULL,这将导致运行时错误(对NULL的解引用操作会导致程序崩溃)
printf(str); 也会引发问题,因为 printf 函数的正确用法是 printf("%s", str);。即使 str 不是 NULL,直接传递 str 给 printf 也不是标准做法
这段代码的主要问题是 GetMemory 函数无法正确地分配内存给传入的指针。要修复这个问题,你可以使用指针的指针(传址调用)来修改原始的指针。
例如:
当然我们还有另一种改法:
题目2:
要修复这个问题,你有几个选择:
1.使用静态数组:将局部数组改为静态数组,这样它的生命周期就会是整个程序的执行期间。但这种方法有其局限性,比如每次调用GetMemory都会返回同一个数组的地址,且静态变量在多次函数调用之间会保持其值。
2.动态分配内存:使用malloc或calloc在堆上分配内存,并返回这块内存的地址。这样,返回的地址在函数调用结束后仍然有效,直到显式地调用free来释放它。
记得在使用完通过malloc分配的内存后调用free来释放它,避免内存泄漏。
3.使用字符串字面量:直接返回字符串字面量的地址。字符串字面量存储在程序的只读数据段中,它们的生命周期是整个程序的执行期间。
注意,虽然这种方法简单且有效,但返回的指针是指向只读存储区的,所以你不应该试图修改通过这个方法返回的字符串的内容。
在任何情况下,都应该避免返回局部变量的地址,因为这通常会导致程序出现错误或崩溃。
看到这 爱提问的猴子就开口了 在一个函数内部 return n和return &n会有什么影响
看到这 猴子说 这答案不是都为10吗 那这俩的作用一样的
但其实不然 我们return &n中 n为一个局部变量 在函数调用结束 局部变量n的内存便还给操作系统了 此时局部变量n的内存可能会被释放或覆盖
不信 我给大家看一下
这里我们就看到输出的值不在是10了 说明局部变量n的内存被释放或覆盖了
大家如果想了解这一内容 我把这一知识点放在本章结尾 以供大家参考
题目3:
这段代码和我们之前修改的很相像 但我们还需要空间分配和 记得释放
即free(str); str=NULL;
题目4:
在动态内存管理讲完之后 再给大家补充一个新的知识点 我当时也是第一次见
柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构体中的最后⼀个成员允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。
例如:
上面两种方式是就不同的编译器来说
因为有些编译器支持第一种写法 有些支持第二种写法
柔性数组的特点:
结构体中的柔性数组成员前⾯必须⾄少⼀个其他成员。
sizeof 返回的这种结构体⼤⼩不包括柔性数组的内存。
包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。
例如:
这里我们就可以知道 假如柔性数组成员前面没有其他成员 sizeof返回的这种结构体大小就为0了 所以结构体中的柔性数组成员前⾯必须⾄少⼀个其他成员。
这个结构体ps(通过malloc)所得到的空间为4+20=24个字节
我们可以通过realloc来改变我们分配空间的大小 就不久很柔性了吗 收放自如
柔性数组 我们不就是让该数组可大可小吗
我们也可以有其他的方式来进行该操作
柔性数组的优势
当然这两种方式相比 还是柔性数组更好一点 因为柔性数组只需要一次malloc 一次free 不容易出错
而且malloc空间的次数越多 内存碎片(空间与空间的间隙)越多 内存的利用率就越低
柔性数组中分配的空间是连续的 连续的空间有益于提高访问速度 也有益于减少内存碎片 而第二种方式是先给结构体分配空间 再给结构体成员arr分配空间
所以柔性数组方便内存的释放 有利于访问速度
总结C/C++中程序内存区域划分
栈区:存放局部变量 函数参数
堆区:动态内存申请
数据段也就是静态区
C/C++程序内存分配的⼏个区域:
1.栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
《函数栈帧的创建和销毁》
2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收 。分配⽅式类似于链表。
3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。
在C语言中,return n 和 return &n 之间的区别涉及到返回值的数据类型和所指向的内存区域。
return n:
当你在函数中返回 n 时,你实际上返回的是变量 n 的值。这里的 n 必须是一个具有确定值的表达式,并且它的类型必须与函数的返回类型相匹配或兼容。
如果 n 是一个基本数据类型(如 int, float, char 等),那么返回的是这个值的副本。这意味着函数外部的调用者获得的是这个值的一个拷贝,而不是原始变量本身。
如果 n 是一个复杂的数据类型(如结构体或联合),那么返回的可能是一个这些类型的副本,但这取决于具体的实现和上下文。对于大型的数据结构,通常建议通过指针或引用传递,以避免不必要的复制开销。
需要注意的是,如果 n 是一个局部变量(定义在函数内部的变量),在函数返回后,这个局部变量占用的内存可能会被释放或覆盖,所以返回它的值通常没有问题,但尝试返回它的地址(如 &n)则是不安全的。
return &n:
当你返回 &n 时,你返回的是变量 n 的地址(即指向 n 的指针)。这意味着调用者现在拥有一个指向 n 的指针,可以通过这个指针来访问或修改 n 的值。
如果 n 是一个局部变量,那么返回它的地址是不安全的,因为当函数返回后,局部变量 n 的内存可能会被释放或覆盖。如果调用者尝试通过这个指针来访问或修改 n 的值,可能会导致未定义的行为,包括程序崩溃或数据损坏。
如果 n 是一个全局变量或静态变量,那么返回它的地址通常是安全的,因为这些变量的生命周期贯穿整个程序的执行过程。
返回指针时,函数的返回类型应该是一个指针类型,例如 int*、float* 或自定义数据类型的指针。
总结:
return n 返回的是变量 n 的值,而 return &n 返回的是变量 n 的地址。
返回局部变量的值通常是安全的,但返回局部变量的地址通常是不安全的。
函数的返回类型应该与返回值的类型相匹配或兼容。
在实际编程中,要谨慎处理指针和地址,确保不会发生悬挂指针(dangling pointer)或野指针(wild pointer)等问题,这些问题可能导致程序的不稳定或难以调试的错误。