目录
1. 为什么存在动态内存分配
2. 动态内存函数的介绍
2.1 malloc和free
2.2 calloc
2.3 realloc
3. 常见的动态内存错误(极易造成程序卡死)
3.1 对NULL指针的解引用操作
3.2 对动态内存开辟空间的越界访问
3.3 对非动态开辟内存使用free释放
3.4 使用free释放一块动态开辟内存的一部分
3.5 对同一块动态内存多次释放
3.6 动态开辟内存忘记释放(内存泄漏)
4. 几个经典的笔试题
4.1 题目1
4.2 题目2
4.3 题目3
4.4 题目4
5. C/C++程序的内存开辟
6. 柔性数组
6.1 柔性数组的特点
6.2 柔性数组的使用
6.3 柔性数组的优势
1. 为什么存在动态内存分配
已经掌握的内存开辟方式有:
int a = 0;int arr[10] = {0};
创建变量,在内存中申请空间如需要4字节空间:inta=0;需要40字节空间:intarr[10]={0};变量创建好之后,其大小是不能被改的,a就是4个字节,arr就是40个字节,这样就会导致问题——如实现通讯录时创建存储数据的空间时空间是一下子开辟好的,朋友少则多余空间浪费,朋友多空间不够,不够灵活这里就需要动态内存分配,动态内存分配就允许我们向内存申请空间想要多大就多大的空间,大则缩小,不够则增加,此时内存空间就会动态调配所以叫动态内存分配。
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态存开辟了。
总结:
像数组、结构体所开辟的空间一旦开辟好其大小就是固定的,无法调整就不够灵活,开辟的空间过大就会浪费,太小就不够,所以在C语言中就给程序员独立提供了一种能力——动态内存分配,在内存的堆区进行动态内存分配。
2. 动态内存函数的介绍
动态内存分配所涉及的内存在哪里开辟呢?
在内存中有栈区,堆区,静态区,在栈区中可以放局部变量,函数的形式参数,临时变量(数据)的空间都是在栈区开辟的;静态变量和全局变量是在静态区开辟的,堆区是用来动态内存分配的,就是在堆区申请的空间觉得小了可以大,觉得大了可以小。
即动态内存分配所申请的空间都在堆区中,而动态内存分配所涉及到的函数是malloc,calloc,realloc,free。
怎么进行动态内存分配呢?
接下来介绍这4个函数:
2.1 malloc和free
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
——函数是用来申请size_t个字节的内存空间,单位是字节,返回值:返回一个指向那块空间起始位置的指针;如果开辟空间失败(比如没有足够空间去开辟)就返回空指针。
malloc()函数的参数是无符号整形,返回类型是void*,对于内存分配来说,malloc不知道开辟的是整形空间还是字符空间还是结构体空间,即不知道所开辟的空间用来存放什么的,只知道大小:多少字节,所以就返回不了具体某种类型(如int*或char*或double*等),所以函数的返回类型是void*。
1、开辟空间:(向内存申请空间的方法)
#include int main(){//开辟10个整型的空间://int arr[10];void* p = malloc(40);//10个整型40个字节return 0;}
开辟40个整形的空间就是:
malloc(40)接收它的返回值就是void*=malloc(40);但这样写不合理:把申请空间的起始地址放在p指针变量中,而p是void*的指针,这个指针没法用。因为想要的是开辟10个整形的空间,则这40个空间未来存放整形,所以最好用整形指针来维护,解引用访问一个整形,+1就跳过一个整形,这样方便合理,所以希望函数的返回值用int*指针接收,而函数本身返回的是void*非要放到int*指针中则需要强制类型转换;即对malloc()函数的返回值进行强制类型转换,转换为我们想要的合适的类型。
所以开辟空间的基本方法:
当我们在堆区申请一块连续空间时,这块连续的空间的起始地址就被void*指针返回来,然后就被放到一个指针变量中,这里是p指针变量,p作为int*的指针就能够很好的维护这块空间;
如果空间开辟成功返回那块空间起始地址;
如果空间开辟失败返回的是空指针(空间不能使用)——报错提示——用报错函数strerror(),括号里写错误码:ereno,看一下开辟空间失败的原因,当申请足够大的空间就会开辟失败。
注意使用errno需要包含头文件;使用strerror()函数需要包含头文件;使用malloc()函数需要包含头文件
#include #include #include #include int main(){int* p = (int*)malloc(4000000000);//开辟40亿空间//如果空间开辟失败——判断原因if (NULL == p){printf("%s\n", strerror(errno));//运行结果:Not enough spacereturn 0;}//如果空间开辟成功——使用空间return 0;}
总结malloc()函数的使用:
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好的空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
2、使用开辟的空间——把空间还给操作系统(空间是向操作系统申请的),则归还的方式——free()函数。
malloc()函数——向内存申请空间
free()函数——释放申请的内存空间
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
参数是void*的指针,该指针指向由malloc,calloc,realloc开辟的的空间(内存块)。
#include #include #include #include int main(){int* p = (int*)malloc(40);if (NULL == p){printf("%s\n", strerror(errno));return 0;}free(p);return 0;}
在调试时发现指针变量p存储的地址在free前后并没有发生变化。
free()函数的作用:
假设向内存申请了空间的起始地址是0x0012ff40,申请的地址放到p中了,即p存储的值就是0x0012ff40,使用完这块空间后要释放该块空间,free(p)的意思是把该块空间还给操作系统,即这块空间已经没有使用权限了,但是p里面保存的还是这个地址,则这个地址就是野指针,即当释放这块空间后p就是野指针,这就很危险——p还能找到这块空间,而这块空间已经还给操作系统了(并不是在内存中已经没有这块空间了)如果其他程序员不小心拿起p去访问p所指向的这块空间,这就形成了非法访问内存。
所以建议在释放空间后把p置成空指针,p为空指针就不能使用它了。
free(p);p = NULL;
3、怎么使用这块空间呢?
这块空间是连续的,把它当成数组存储值即可。p存储的是这块空间的起始地址,p+i找到的是下标为i元素的地址,*(p+i)就是下标为i元素的内容。
#include #include #include #include int main(){//开辟10个整型的空间int* p = (int*)malloc(40);//对返回值判断if (NULL == p){printf("%s\n", strerror(errno));return 0;}//不是空指针就使用://使用 //初始化这块空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;} //打印值for (i = 0; i < 10; i++){printf("%d ", p[i]);}//运行结果:0 1 2 3 4 5 6 7 8 9//释放free(p);//置成空指针p = NULL;return 0;}
打印值,p是首元素地址与数组名没有区别,所以写成p[i]也可以。
这里使用了这块内存空间,通过p该了这块空间,所以不应该加const修饰p(指向对象)或*p(指向对象的内容)。
这块空间的使用与数组很相似。
这里不强制类型转换在有些编译器下不会报错,但是尽量对返回值进行强制类型转换——标椎写法。
当用malloc()函数开辟0字节的空间,这种做法没有任何意义,只是个地址,指向的空间不能被访问,因为指向的空间大小时0。
#include int main(){ int* p = (int*)malloc(0);//用malloc创建了0字节的空间,没意义 free(p); p = NULL; return 0;}//相当于创建了一个野指针,确实得到了一个地址,但是指针指向的空间不是自己的,不能使用。
总结free()函数的使用:
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做(执行)。什么都不会释放,因为空指针没有指向任何有效的空间。但free(NULL)是正确的语法。
malloc和free都声明在头文件中。
2.2 calloc
C语言还提供了一个函数calloc(), calloc()函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
参数num指的是元素的个数;size是指每个元素的大小。返回值是指向空间的起始地址。
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
即是:
malloc()函数——开辟(不初始化)
calloc()函数——开辟+初始化为全0
还有一个区别是二者的参数不同。
如果空间开辟成功返回那块空间起始地址;
如果空间开辟失败返回的是空指针(空间不能使用)——报错提示——用报错函数strerror(),括号里写错误码:ereno,看一下开辟空间失败的原因,当申请足够大的空间就会开辟失败。
这与malloc()函数的返回值一致。
#include #include #include #include int main(){//开辟10个整型的空间int* p = (int*)calloc(10,sizeof(int));//对返回值判断if (NULL == p){printf("%s\n", strerror(errno));return 0;}//不是空指针就使用://使用初始化这块空间int i = 0;//for (i = 0; i < 10; i++)//{//*(p + i) = i;//}//打印值//这里不初始化直接打印这块空间的内容for (i = 0; i < 10; i++){printf("%d ", p[i]);}//运行结果:0 0 0 0 0 0 0 0 0 0//释放free(p);//置成空指针p = NULL;return 0;}
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
malloc()函数相较于calloc()函数效率更高,因为malloc()函数没有初始化直接就返回地址了;calloc()函数需要初始化后才能返回地址。
malloc()函数和calloc()和函数都只是申请,realloc()函数是重新开辟,可以用来调整内存大小。
注意:函数初始化后会使掌控感更好,能知道更多的信息。
2.3 realloc
realloc()函数的功能:
1、可以开辟空间
2、也可以调整空间
即realloc()函数既能自己单独开辟空间,也能帮助其他函数申请的空间调整其大小。
realloc函数的出现让动态内存管理更加灵活。
有时我们会发现之前申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);
ptr——是要调整的内存地址,是malloc()函数,calloc()函数,realloc()函数开辟空间的起始地址,如果ptr是空指针就相当于调了一个malloc()函数,就和malloc()函数的功能一样。
size——调整之后新大小。
返回值为开辟好或调整好之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
#include #include #include #include int main(){//开辟10个整型的空间int* p = (int*)calloc(10,sizeof(int));//对返回值判断if (NULL == p){printf("%s\n", strerror(errno));return 0;}//不是空指针就使用://使用int i = 0;//这里不初始化直接打印这块空间的内容for (i = 0; i < 10; i++){printf("%d ", p[i]);}//运行结果:0 0 0 0 0 0 0 0 0 0//开辟的空间不够,需要增加容量——80(增加一倍)//返回的是重新调整为80个字节之后的地址,p接受其返回值,//再由函数本身返回的void*指针强制类型转换为int*类型的指针p = (int*)realloc(p, 80);//但是这样写代码是有风险的//释放free(p);//置成空指针p = NULL;return 0;}
为什么这么写是有风险的?
p = (int*)realloc(p, 80);
分析:
对p指针指向的空间采用realloc来调整,realloc()函数增加到80个字节,即扩大一倍的情况:
(realloc在调整内存空间时存在两种情况)
情况1:原有空间之后有足够大的空间:
当calloc后面有足够空间,这在calloc后面增加40个字节,则这种情况依然返回的是原来p的地址,还是由p来维护这块空间的。
扩展方法是:扩展内存是直接在原有内存之后直接追加的空间,原来空间的数据不发生变化。
情况2:原有空间之后没有足够大的空间:
calloc后面的空间不够增加40个字节,如果强行在calloc后面增加40字节空间,则就会把其他已经被使用的内存块给覆盖掉,这种方式不合理。realloc()函数发现calloc后面不够自己增容时,realloc就会在内存中重新找一块新的空间,这块空间的大小是原来空间的2倍,找的是80字节的空间同时把旧的空间(calloc函数申请的里面全是0)的所有0拿下来,则新找的空间的前40个字节也改成0,(即把原来的内容复制下来),这种情况返回的值是新找的空间(80字节)的起始地址。(和旧地址不同,返回的是新地址,旧地址的空间不需要我们亲自用free去释放,realloc()函数把旧空间的数据拷贝下来的同时直接就把这块旧地址空间释放了)。
把新找的空间的起始地址还赋值给指针变量p,现在p就指向新空间,p认为前40个字节还是它的0就以为这块空间并没有发生过任何变化,同时会释放原来的40字节的空间,因为这块空间已经没有用了,已经重新开辟了增加到自己想要的了,所以会把这块空间还给内存,还给操作系统。
即是在原有空间之后没有足够多的空间时,扩展方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况,所以realloc返回的值有可能和原来的值一样,有可能和原来的内容不一样。realloc函数的使用就要注意一些。
还有一种情况:realloc会失败
如果让realloc增容,但realloc调整空间失败,已经没有足够的空间增容——返回空指针。
当返回空指针的时候,本来p还是指向40个字节的空间,让realloc增容但是增容失败,调整空间失败返回空指针,空指针就放在p里了,这就会出问题(没有增容变大反而返回NULL放进p中)会让p找不到原来的40个字节的空间。所以不能直接把realloc的返回值放到p里,一般都是再单独创建一个指针如ptr,当realloc返回之后再判断其返回值是否是空指针,当realloc返回值不是空指针时,就把ptr交给p,让p来维护。
只有realloc返回值不是空指针的时候才说明增容成功,增容成功再把返回值交给p。
增容好之后又可以继续使用
所以对:
p = (int*)realloc(p, 80);
此代码更改为:
int* ptr = (int*)realloc(p, 80);if (NULL != ptr){p = ptr;}
增容前和增容后都用p指针来维护内存空间便于保持前后统一。
#include #include #include #include int main(){//开辟10个整型的空间int* p = (int*)calloc(10,sizeof(int));//对返回值判断if (NULL == p){printf("%s\n", strerror(errno));return 0;}//不是空指针就使用://使用int i = 0;//这里不初始化直接打印这块空间的内容for (i = 0; i < 10; i++){printf("%d ", p[i]);}//运行结果:0 0 0 0 0 0 0 0 0 0//需要增容/*p = (int*)realloc(p, 80);*///这样写代码是有风险的int* ptr = (int*)realloc(p, 60);if (NULL != ptr){p = ptr;}//继续使用://释放free(p);//置成空指针p = NULL;return 0;}
验证p是否是原来的地址:
p和ptr的值不同,说明是情况2
p的值和ptr的值一模一样,说明是情况1
注意:不需要free(ptr),ptr的值已经赋给p了,ptr和p指向同一块空间,free(ptr)则就是free(p)是一个意思,最多把ptr置成空指针,p还指向那块空间。
如果没有后面的free(p),则程序结束的时候才由操作系统释放。程序运行起来死掉之后刚刚申请的这些堆空间也会还给操作系统的,但是这种方式不好,因为运行起来后程序在走,前面申请的空间不用也不释放,则别人也用不上就会造成浪费,这种情况是内存泄漏,所以开辟好的空间不用就释放掉。所以一般情况下开辟和释放会成对出现,并且释放有机会执行,如果代码在释放之前就已经结束了,程序没结束的时候return返回跳出free了,也会造成内存泄漏的问题。
有时候开辟好的空间不能直接释放掉,若后面的人要继续用就不用释放掉,留给后面的人去使用,否则释放掉后面的人都不能使用了,根据自己需求。
realloc()函数在开辟空间的时候,如果后面的空间不够就会在堆区空间上另找一个合适大小的连续空间开辟,一定是在新空间成功开辟好之后才把原来的空间释放掉,因为还要拷贝原来空间存储的值到新空间上,如果新空间开辟失败则肯定不会释放原来的空间,这个原来的空间还能继续使用。
而像局部变量、函数里的形式参数等在栈区的临时变量就不需要free,因为这些变量在栈区开辟的,栈区里开辟的变量是自动创建自动回收的,只有通过malloc、realloc、calloc所开辟的空间才需要去free。
malloc()函数、ralloc()函数和realloc()函数的头文件都是。
realloc()函数的另一种写法(用法):
//开辟10个整型的空间int* p = (int*)calloc(10,sizeof(int));
realloc()函数的第一个参数是NULL时,此时realloc()函数的功能和malloc()函数的功能是一样的,就会开辟40个字节的空间:
#include #include #include #include int main(){//开辟10个整型的空间int* p = (int*)calloc(NULL, 40);//对返回值判断if (NULL == p){printf("%s\n", strerror(errno));return 0;}//不是空指针就使用://使用int i = 0;//这里不初始化直接打印这块空间的内容for (i = 0; i < 10; i++){printf("%d ", p[i]);}//运行结果:0 0 0 0 0 0 0 0 0 0/*p = (int*)realloc(p, 80);*///这样写代码是有风险的int* ptr = (int*)realloc(p, 60);if (NULL != ptr){p = ptr;}//继续使用://释放free(p);//置成空指针p = NULL;return 0;}
如果传的第一个参数不是空指针,它的功能就是调整空间。
总结:
malloc()函数申请空间的大小是由我们来指定的,它向内存申请的是一块连续可用的空间,并且这块空间不会被初始化,它会返回那块空间的起始地址。申请可能会失败,一旦失败返回的就是空指针。
free:当在动态内存中申请的空间不想要的时候就可以用free释放掉,free可以释放掉malloc、calloc所开辟的空间,还可以释放掉realloc调整后的空间或realloc所申请的空间。
calloc()函数与malloc()函数相似,calloc()函数的参数指定了元素个数和一个元素的大小来开辟一块连续的空间,这块空间是经过初始化之后才把地址返回来的,calloc()函数会把这块空间中每个字节初始化为0。
realloc()函数大部分是用来进行内存调整的,空间过大就可以用ralloc()函数改小,如果开辟的空间小了就可以用realloc()函数增大。若扩容(调整空间)成功返回的是新的起始空间的地址,而新的起始空间的地址有可能和原来地址一样(原有空间后面的空间足够大,能够用),也有可能不一样(原有空间后面的空间不够大就会重新找一块空间)。如果开辟空间失败会返回空指针。
3. 常见的动态内存错误(极易造成程序卡死)
3.1 对NULL指针的解引用操作
空指针NULL是0,0地址处空间是不能访问的, 对NULL指针的解引用操作本身是非法的,那怎么还存在对空指针的解引用操作呢?
——是有可能存在的。
#include #include #include int main(){//开辟空间//若在堆区开辟的空间过大,是没法申请的/*malloc(INT_MAX)*/;//整型最大值(转到定义是21亿)//向内存中申请空间的时候假设这块空间是按照整型的方式访问:int* p = (int*)malloc(INT_MAX);//返回值用p接收//若使用刚开辟的这块空间的前10个整型;int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;//p+i访问的就是前10个整型,是每个整型的地址,*(p + i)就拿到了前10个整型}//这样会出问题!//由于malloc开辟的空间过大就导致不会开辟出这么大的空间就会返回空指针,即p就是NULL//*(p + i) = i;就是对空指针进行+-操作访问,因为p是空指针,当p加上i即加上几个数字也就//是加上几个偏移量然后解引用,这时候访问的空间依然是无法正常使用return 0;}//代码崩溃
当p是空指针时,再去访问它就会出现内存访问出错的问题。
正确的写法:
对malloc()函数的返回值进行判断,是空指针就提前终止程序让其没办法访问空指针。
#include #include #include int main(){int* p = (int*)malloc(INT_MAX);if (p == NULL){return 0;}int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}return 0;}
也可以拿assert进行断言。
所以对malloc、calloc、ralloc函数的返回值的检测是非常有必要的。需要先检测其返回值是否是空指针然后再去使用。
3.2 对动态内存开辟空间的越界访问
即使是动态内存开辟的空间也是有限的,不是想要多大就有多大。
我们向内存申请了多大空间,本身就有多大空间的访问权限。
#include #include #include #include int main(){//向内存中申请10个字符的空间:char* p = (char*)malloc(10 * sizeof(char));//检测返回值if (p == NULL){//是空指针//——则空间开辟失败,检测失败的原因//调用strerror()函数传错误码来提供错误信息printf("%s\n",strerror(errno));return 0;} //开辟成功//使用int i = 0;for (i = 0; i <= 10; i++){//访问这块内存*(p + i) = 'a'+i;} //不想用这块空间//则释放这块空间free(p);//释放p所指向的空间//释放完p所指向的空间之后, //因为p里面存的还是那个空间的地址//则:p = NULL;return 0;}
这里内存访问崩溃!
原因——越界。
因为只开辟了10字符的空间访问的时候却访问11个字符,开辟多大空间就使用多大空间,即使是动态内存开辟的空间它也不会自己去增大,需要我们程序员去申请,需要写realloc()函数进行调整才可以,不是它自己就能调整的。
正确的写法:
#include #include #include #include int main(){//申请空间:char* p = (char*)malloc(10 * sizeof(char));//检测返回值if (p == NULL){printf("%s\n", strerror(errno));return 0;}//使用int i = 0;/*for (i = 0; i <= 10; i++)*/for (i = 0; i < 10; i++){*(p + i) = 'a' + i;}//释放free(p);p = NULL;return 0;}
3.3 对非动态开辟内存使用free释放
对任何内存都可以用指针来维护。
#include int main(){int* p = (int*)malloc(40);//检测返回值……free(p);p = NULL;return 0;}
整个逻辑:
这里用malloc()函数开辟了40个字节的空间,可以用p指针进行维护,不想用这块空间就free(p),然后再把p置成空指针;
即这里对于一块动态开辟空间的维护就是用一个指针(地址)来维护的。
但是:
#include int main(){int a = 10;int* p = &a;//写代码……//……free(p);p = NULL;return 0;}
这种写法是错误的!
因为p所指向的空间a的4个字节,绝对不是动态开辟出来的,a是函数中的局部变量,是在栈上开辟的,不能free,这种空间不能调用free去释放的,若p所指向的空间是动态开辟出来的则可以使用free。栈区里的空间是一些局部变量,是自动创建自动销毁的,进入它的作用域它创建,出了它的作用域它就自动销毁,不需要程序员维护,是它们自己创建自己维护,即局部变量的创建、维护和销毁都是自动的,按照代码的执行顺序和逻辑的,程序员想主动释放也释放不了。
注意内存中的栈区和数据结构中的栈是不能划等号的。内存中的栈区的维护形式像栈,放数据的时候像压栈的效果。
即不能对非动态内存开辟的空间进行free;
free只能释放malloc、calloc、realloc这三个函数开辟的空间。
3.4 使用free释放一块动态开辟内存的一部分
#include #include #include #include int main(){int* p = (int*)malloc(40);//用malloc开辟40个字节的空间//检测返回值if (p == NULL){//空间开辟失败//打印错误信息printf("%s\n", strerror(errno));return 0;//开辟失败就不再继续向下执行代码}//使用这块内存//把这块空间的前5个整型初始化为1,2,3,4,5int i = 0;for (i = 0; i < 5; i++){//1、*p = i + 1;p++;//或://2、*p++ = i + 1;//这两种写法都是错误的!}//释放free(p);p = NULL;return 0;}
因为在使用内存时,p已经指向了第6个整型的地址,不再是空间的起始地址,不再指向空间的起始位置,那么free(p)简单理解就是把从第6个整型后面的空间释放掉——这是不可行的!
当把p改变后以后也找不到这块空间的起始位置,(除非分析代码把改变多少再改回来)即再也找不到这块空间就有可能导致内存泄漏。
动态开辟的空间在释放的时候不能只释放其中一部分,除非是调整后变小。
动态开辟的空间在释放的时候是整块空间释放。
所以在访问动态开辟的空间的时候,必须先保留空间的起始位置,然后可以用其他的指针指向这块空间来访问,让指针变动等,否则想释放这块空间的时候就有可能找不到它,这是非常危险的。
开辟0字节空间(在内存中就会随便返回一个地址)没有意义且可能导致问题——因为如两次mallco就有可能返回同一个地址。
realloc缩小空间时就把原来空间后面的内容都剔除掉了,因为只要把后面的空间放小就可以了。
综上,尽量不要让维护动态内存的指针的值(存储的地址)随便发生变化,否则就找不到维护的这块空间了。
正确的写法:
#include #include #include #include int main(){int* p = (int*)malloc(40);//用mallco开辟40个字节的空间//检测返回值if (p == NULL){//空间开辟失败//打印错误信息printf("%s\n", strerror(errno));return 0;//开辟失败就不再继续向下执行代码}//使用这块内存//把这块空间的前5个整型初始化为1,2,3,4,5int i = 0;for (i = 0; i < 5; i++){*(p + i) = i + 1;}//释放free(p);p = NULL;return 0;}
3.5 对同一块动态内存多次释放
#include #include #include #include int main(){int* p = (int*)malloc(40);if (p == NULL){printf("%s\n", strerror(errno));return 0;}int i = 0;for (i = 0; i < 5; i++){*(p + i) = i + 1;}//释放free(p);//……free(p);return 0;}
这里释放两次是不正确的——程序卡死!
正确的写法:
#include #include #include #include int main(){int* p = (int*)malloc(40);if (p == NULL){printf("%s\n", strerror(errno));return 0;}int i = 0;for (i = 0; i < 5; i++){*(p + i) = i + 1;}//释放free(p);p = NULL;//……free(p);return 0;}//运行成功
空指针时没有指向有效空间的,若p是空指针,free(p)是不会发生什么的。
所以在释放空间的时候,释放后就一定要把指针置成空指针,这样即使进行第二次释放也不会发生什么问题。但是如果释放完后不赋值为空指针,后面不小心再释放的时候就会产生问题——一块空间释放过之后就不能再进行释放了。
3.6 动态开辟内存忘记释放(内存泄漏)
#include void test(){//动态内存开辟:int* p = (int*)malloc(100);if (p == NULL){return 0;}//使用//…… //忘记释放}int main(){test();return 0;}
忘记释放——这种情况就会出现内存泄漏的问题,在其他地方如主函数中想释放也释放不了。
地址在p中存放,而p是局部变量,出了它所在的作用域(代码块)就不在了,即出了test()函数后就不在了,所以在主函数中想释放也释放不了。
#include int* test(){//动态内存开辟:int* p = (int*)malloc(100);if (p == NULL){return 0;}//使用//……//忘记释放return p;}int main(){int* ptr = test();//用指针来接收返回来的指针free(ptr);return 0;}
这种写法也可以,即把值可以返回去在主函数中释放,但是最终不能不释放,即必须要释放空间!
不返回这块空间的地址,不在函数内部释放这块内存,也不在函数外部释放就会出现内存泄漏的问题——一旦出了它的作用域就找不到这块空间了,就释放不了了。
避免内存泄漏的问题:
谁申请的空间谁去释放,或者空间还要想给别人使用,则在交接时交代谁申请的空间还未释放,最终再释放。
malloc和free要成对使用,只出现其一代码则肯定有问题。
无论在哪(包括主函数)申请开辟的空间最终都要释放!不释放就有可能导致问题。
#include int main(){int* p = (int*)malloc(100); //判断(检测)if (p == NULL){return 0;}//使用return 0;}
如果整个程序结束,动态申请的空间就会被释放也会回收。只要程序的生命周期结束操作系统就会主动回收申请的动态内存开辟的空间,但是主函数内部如果存在死循环(程序不会结束)也会造成内存的泄漏。
#include int main(){int* p = (int*)malloc(100);if (NULL == p){return 0;}//使用while (1);return 0;}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放 。
总结——动态内存开辟的常见错误:
- 1、对开辟空间的函数的返回值进行检测;
- 2、动态开辟的空间也有自己的使用范围;
- 3、不能对非动态开辟的内存释放;
- 4、不能释放动态开辟的内存的一部分;
- 5、不能多次释放同一块动态开辟的内存;
- 6、不能忘记释放动态开辟的内存。
4. 几个经典的笔试题
4.1 题目1
请问运行Test 函数会有什么样的结果?
#include #include #include void GetMemory(char* p){p = (char*)malloc(100);}void Test(void){char* str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);}int main(){Test();return 0;}
运行结果——没有输出,程序挂掉(程序崩溃死掉)
分析:
str中放的值是NULL,把指针变量str本身作为参数传过去,是直传,p是str的临时拷贝,即p中的值是NULL,malloc()函数在内存的堆区申请了100字节的空间,假设malloc返回的起始地址是0x0012ff80,则p中放的值就是0x0012ff80,即p指向了这块空间,其实这里应该对p的合法性进行判断,检测其返回值是否是空指针,但是当前代码不会有太大的影响,即使可能存在潜在的问题。出GetMemory()函数后,p是形参会被销毁,即p变量所占的4或8字节的空间就还给操作系统了,但是malloc()函数开辟的100字节的空间没有释放还属于程序,(这100个字节的空间的地址是放在p中的)p被销毁后原本由p指向的这块空间就会找不到——内存泄漏。strcpy()函数中,把“hello world”拷贝放到str指向的空间中去——写法有问题。因为在传参的过程中,把str里的内容拷贝放到p里面之后,p是得到了0x0012ff80这个值但是str并没有变,p的改变是不会影响到str的,则当GetMeory()函数返回之后str中的值依然是空指针,strcpy()把“hello world”拷贝放到str指向的空间中就会造成程序的崩溃!strcpy()函数会把空指针所指向的空间的内容进行覆盖,把“hello world”放进去这就形成了非法访问内存!
注意:
strcpy(str,"hello world");
这种写法是正确的。strcpy()函数的第二个参数是指针,是源,因为一个常量字符串作为参数时传递的不是整个字符串,传的是首字符的地址即h的地址。“hello world”是源字符串,指向的是h,str指向的是目标空间。
str是NULL,程序崩溃后,接下来的打印代码就不能执行。
printf(str);
这种代码写法也对,因为用printf()函数打印字符串时会写成:
printf("hello world\n")
这里向printf传的是“hello world”字符串,而实际上传的是首字符h的地址。printf()函数需要的是地址,就可以从h的地址打印“hello world”这个字符串。(这个地址指向的字符串整个打印出来。)
也见过:
char* p = "hello world";
这里不是把”hello world”这个字符串放到p中,是把首字符h的地址放到p中。
表达式“hello world”的结果是h的地址。
则就可以写成:
char* p = "hello world";printf(p);
因为p放的是h的地址,p指向“hello world”,printf(p)打印的就是“hello world”。
更改代码:
——因为p和str是两个完全不相干的变量,把地址放到p中不会影响到str,所以需要用GetMemory()函数把申请的100字节的空间起始地址给str——则传str的地址:
#include #include #include void GetMemory(char** p)//即p存的就是str的地址{//p找到str则就是*p*p = (char*)malloc(100);//开辟的100字节就是放到str中}void Test(void){char* str = NULL;//因为str是char*的指针,所以&str就是char**GetMemory(&str);//传str的地址//此时str就指向了100字节的空间strcpy(str, "hello world");//此时这里就是把"hello world"放到100字节的空间中去了printf(str);//释放:free(str);//释放str所指向的空间str = NULL;}int main(){Test();return 0;}//hello world
或:
#include #include #include char* GetMemory(char* p){p = (char*)malloc(100);return p;//返回p}void Test(void){char* str = NULL;str = GetMemory(str);strcpy(str, "hello world");printf(str);free(str);str = NULL;}int main(){Test();return 0;}//hello world
这里p能返回去吗?
返回值返回时,其实是把p的值放到寄存器如eax中了,p销毁就销毁,而寄存器不会销毁(寄存器是硬件上的东西),当函数调用完返回去的是寄存器中的值(即通过寄存器把值带回去)——知识点:函数调用过程解析——函数栈帧的创建和销毁_Bug梨哥的博客-CSDN博客——内功修炼之法https://blog.csdn.net/m0_60624580/article/details/127111665
代码实现的效果:
这里GetMemory()函数是获取一块内存,获取的内存用str维护,就是GetMemory()函数中申请的100字节的空间的起始地址放到str中,由str维护。GetMemory()函数之后str指向了一块空间,所以才能strcpy把“hello world”拷贝到str中去。
总结:
动态申请的空间需要程序员主动去free释放,否则在程序死掉的时候这块空间才回收。
常量字符串作为表达式时其结果是首字符的地址。
4.2 题目2
请问运行Test 函数会有什么样的结果?
#include char* GetMemory(void){char p[] = "hello world";return p;}void Test(void){char* str = NULL;str = GetMemory();printf(str);}int main(){Test();return 0;}
运行结果:烫烫烫烫烫烫烫烫8
报警告:返回局部变量或临时变量的地址: p
分析:
str接收GetMemory()函数的返回值,p数组是在GetMemory()函数中临时创建的,是局部的数组,出GetMemory()函数p数组就会被销毁,在还没被销毁时把p的地址返回去,则str指针就指向p数组,但是p数组所占的空间已经被回收了,即str所指向的空间已经没有使用权限了,而str指针还记住p数组的地址,所以str就是野指针,所以打印的结果就是随机值——这种问题统一称为返回栈空间地址的问题。(注意是栈空间地址!不是栈空间)
所以代码错误原因:返回栈区某个变量的地址。
局部变量是在栈上存放的,出作用域就会被销毁,所以不要把它的地址返回去;地址返回去之后一旦记录起来(记录也没有意义)就使指针变为野指针。
更改代码:
#include char* GetMemory(void){static char p[] = "hello world";return p;}void Test(void){char* str = NULL;str = GetMemory();printf(str);}int main(){Test();return 0;}//hello world
static修饰局部变量则会使局部变量出它的作用域不销毁,即p数组这块空间还在,此时可以访问这块空间。
注意:返回栈区某个变量本身——可行!
#include int test(){int a = 10;return a;//这里返回的是栈区的变量本身,是值,并没有返回它的地址}int main(){int m = test();printf("%d\n", m);return 0;}//成功运行:10
这个代码是可行的,这里是先把a的值放到寄存器中了,出test()函数后,a被销毁,m得到的值是寄存器中的值。
但是:返回栈区某个变量的地址——错误!
#include int* test(){int a = 10;return &a;}int main(){int* m = test();printf("%d\n", *m);return 0;}//10,但是报警告——返回局部变量或临时变量的地址: a
这是典型的返回栈空间地址的问题。
分析:
能打印出a的原因:
m存的是a的地址,a变量的空间虽然销毁了,但是a变量的值没有发生变化如果依然通过m指针去访问a就有可能打印出a的值,这并不意味着a这块空间能被使用,若是a的值被修改了就打印不出原来的a的值了。如:
#include int* test(){int a = 10;return &a;}int main(){int* m = test();printf("hehe\n");printf("%d\n", *m);return 0;}//5
在test()函数调用后加一次printf函数调用,这次printf函数的调用就会把上一次函数调用的函数栈帧覆盖掉。
解释:
每一次函数调用都会创建栈帧。
先调用main()函数,则先为main()函数创建栈帧(开辟一块空间);
test()函数调用的时候就会在内存中开辟空间,调用完test()函数,则test()函数所申请的整个栈帧空间就会被回收,则a所占的4个字节的空间就没有使用权限了,但这块空间还有可能没被修改依然在这个位置;当调用printf()函数时又会在内存中申请空间,则有可能就把之前test函数()还回去的空间给printf()函数,printf()函数就会把这块空间内容重新进行修改,则就有可能把a所占的4个字节空间覆盖掉,这时再通过存的地址去访问a所占的空间时值就发生变化了。 如果没有printf()函数调用,a这块空间就可能没被覆盖所以还能打印出10,但并不代表代码是正确的——返回栈空间地址的问题。
4.3 题目3
请问运行Test 函数会有什么样的结果?
#include #include #include void GetMemory(char** p, int num){*p = (char*)malloc(num);}void Test(void){char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);}int main(){Test();return 0;}
运行结果:hello
存在问题——没有释放,内存泄漏
注意:*p是str,所以str就指向了100字节的空间。
更改代码:
#include #include #include void GetMemory(char** p, int num){*p = (char*)malloc(num);}void Test(void){char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);//忘记释放了free(str);str = NULL;}int main(){Test();return 0;}
4.4 题目4
#include #include #include void Test(void){char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);}}int main(){Test();return 0;}
运行结果:world,报两处警告。
注意free(str)动作不会让str本身置成空的,所以str依然保存所指向空间的地址,则str确实不是空指针,strcpy(str, “world”)是把world拷贝放到str所指向的空间中去,但是str所指向的空间已经free还给操作系统了,不能再使用这块空间了,则此时拷贝就是非法访问内存。
这里考察的不是早释放和晚释放的问题,而是free释放完之后应该主动置为空指针(这样也会使下面的判断更有意义),所以更改代码:
#include #include #include void Test(void){char* str = (char*)malloc(100);strcpy(str, "hello");free(str);str = NULL;if (str != NULL){strcpy(str, "world");printf(str);}}int main(){Test();return 0;}
5. C/C++程序的内存开辟
视角:在程序中看内存的划分——目的是知道写的变量在哪个区域:
分析:
右边表格中其实是虚拟地址空间或进程地址空间,这里先理解为内存。
图中用常量字符串初始化字符数组和字符指针,这种常量字符串是放在代码段中的。
C/C++程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。即在栈区上创建的变量在作用域内自动申请出作用域自动销毁。栈内存分配运算内置于处理器的指令集中,效率很高,但是栈空间的分配内存容量有限,当栈区空间使用时会栈溢出。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):涉及malloc、calloc、realloc空间的开辟和维护都在堆区。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
3. 数据段(静态区)(static):存放全局变量、静态数据(静态变量)。程序结束后由系统释放,所以静态区中的数据存放的时间比较久,与程序的生命周期是一样的。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。代码编译成二进制指令之后,这些二进制指令是存在代码段的,常量字符串也是存在代码段的,这块数据是只读的不能被修改。
注意:栈是向高地址向低地址增长的,即先使用高地址的栈空间;而堆是向上增长的。
回顾:
static关键字修饰局部变量:
普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁,所以生命周期变长。
6. 柔性数组
柔性数组(flexible array)确实是存在的,它是结构体中的一个成员,就叫做柔型数组成员。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。(如果编译器不支持C99标准语法就不能使用柔性数组)
#include struct S1{int n;int arr[0];//数组大小未知,是柔性数组成员};//编译器支持这两者之一的语法,肯定有一个是支持的struct S2{int n;int arr[];//没有指定大小,是柔性数组成员};int main(){return 0;}
结构中的最后一个成员是数组,这个数组没有指定大小,此时这个数组就被称为柔性数组成员。
6.1 柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员,这样才保证结构体大小不是0,才会让它成为一个柔性数组成员,为它开辟空间。
sizeof 返回的这种结构大小不包括柔性数组的内存,只计算柔性数组成员前面所占空间大小。
包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小——正确使用柔性数组。
#include struct S{int n;int arr[0];};int main(){printf("%d\n", sizeof(struct S));//4return 0;}
即在计算包含柔性数组成员的结构体大小时,柔性数组的大小是不计算的。
6.2 柔性数组的使用
#include #include struct S{int n;int arr[];};int main(){//申请空间/*struct S s;*///创建错误//包含柔性数组成员的结构空间的开辟:struct S* p = (struct S*)malloc(sizeof(struct S)+40);//sizeof(struct S)的空间是给n的,40个字节是开辟给arr的//使用空间p->n = 100;//访问arr,40个字节当成10个整型int i = 0;for (i = 0; i arr[i] = i;}//进行其他的使用//增容struct S* ptr = (struct S*)realloc(p, sizeof(struct S) + 80);//增加一倍if (ptr == NULL){return 0;}else{p = ptr;}//释放free(p);p = NULL;return 0;}
——这是柔性数组实现的方式。
即柔性数组通过realloc的方式增大或缩小,柔性数组成员具有变长变短的可能性。
realloc是对malloc开辟的空间进行调整的,空间调整好后再去访问内部成员。
让arr数组是可变的,是不是没有必要使用柔型数组也能实现?
则让数组变大变小,可以用指针,使指针指向的空间是动态内存开辟的。
#include #include struct S{int n;int* arr;};int main(){//ps所指向的空间有n,有arrstruct S* ps = (struct S*)malloc(sizeof(struct S));ps->n = 100;ps->arr = (int*)malloc(40);//让arr是整型数组则强制类型转换为整型指针//使用//增容//释放//不能先释放ps,若先释放ps就找不到arr了free(ps->arr);ps->arr = NULL;//把ps指向的arr指针置成NULL/*free(ps->n);ps->n = NULL;*/free(ps);ps = NULL;return 0;}
代码的基本内存布局:
——这里是用指针指向了一块动态开辟的空间实现的。
方案1比方案2从设计上来说更优一些,为什么?
6.3 柔性数组的优势
因为:
方案1——对于包含柔性数组成员的结构体,只要一次malloc就把arr和n都开辟出来了,也是一次free。方案2——malloc两次,free两次。当malloc和free的使用次数过多就容易出错。
如果频繁多次在动态内存开辟,空间和空间之间势必会留下空隙,在内存中就会形成很多的缝隙,这些缝隙空间如果以后没机会利用就叫内存碎片,开辟越多,内存碎片越多,内存碎片利用率如果不够高则内存的利用率就不够高。方案1中只用一次malloc,内存碎片的概率就降低一些。
即方案2的缺点:
1、开辟的释放次数多,容易出错;
2、容易形成内存碎片。
两次malloc()开辟的空间就需要两次free。
官方解释:
:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。提高访问速度在于:程序的局部性——如果程序当前在访问一个内存块中的数据,则接下来80%的可能是在访问它周围的数据,这时把周围的数据加载到寄存器中,寄存器中命中数据的概率就会高一些;而在内存中是这开辟一块空间,那开辟一快空间,数据不够连续就有可能把某些数据未加载到寄存器中,下次去寄存器中拿数据就可能没有命中即相对降低了访问速度。
这里只是简单举例,结构体中可能有多个成员。
对于申请的空间用完之后,不用了也不还(别人也用不上)就会造成内存泄漏。程序只要死掉空间就会被操作系统回收。
总结——柔性数组的优势:
柔性数组成员的内存是少次(单次)连续开辟的,开辟和释放较简单,内存碎片较少,根据程序局部原理便于访问数据,相对提高了访问速度。
扩展阅读对柔性数组的了解:
C语言结构体里的成员数组和指针(C语言的一个隐晦角落——关于零数组)_yang_yulei的博客-CSDN博客_结构体中的数组成员C语言结构体里的成员数组和指针(关于零数组)【转自酷壳网:http://coolshell.cn/articles/11377.html 作者:陈皓】单看这文章的标题,你可能会觉得好像没什么意思。你先别下这个结论,相信这篇文章会对你理解C语言有帮助。这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接。微博截图如下。我觉得好多人对这段代码的理解还不https://blog.csdn.net/yang_yulei/article/details/23395315
寄个知识小卡片:
strerror()函数的头文件是
errno这个值的头文件是
strerror()函数是把错误码转换为对应的错误信息,这个错误信息是字符串,所以是字符串相关的函数,所以包含的头文件是