指针动态内存分配
前言
在正常情况下,声明一个数组时type arrayName [arraySize]
:
1 | int ch[10] = {0}; //就是将是个成员全部初始化为0 |
还有一种情况就是arraySize是const类型或者#define,因为在规范里定义了,声明数组大小的时候必须是常量表达式:
1 |
|
至于为什么不能是变量,放到最后再说。
正文
malloc
这就不得不提c语言的malloc函数了void* malloc(size_t size)
1 | //malloc 是向计算机申请一片内存使用,所以参数为type*typesize |
当然,malloc如果没有申请到内存就会返回0,指针得到的结果也就为0
calloc
void * calloc(size_t count,size_t size)
差别到不是太大,就是两个形参,一个是个数,一个是类型大小。
1 | int *p = (int*)calloc(10,4); |
同样的,如果没有成功分配到内存,返回也是0,好处是calloc分配完内存后会进行初始化,而malloc则没有这么贴心
realloc
re可能就会联想到重新,realloc就是重新分配内存void* realloc(voir* _Block, size_t_size)
先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。
——摘自百度百科
1 | int main(){ |
这里可以看到经过扩容之后的内存,起始位置发生了改变,但是原本p[0]上的1还是存在
1 | int *p = (int *)malloc(sizeof(int) * 10); |
然后改成缩小,并且用一个指针先记录p原先的地址,在realloc前后各打印一次
可以看到经过realloc之后指针p的首地址发生了改变。符号百度的情况
但是上述两种都是基于x64编译的,试了下x86编译的,发现不论是扩大还是缩小,内存首地址居然没变化
起初还以为是vs2022做了什么优化,又用了clion试了一下:
发现好像是malloc的不够大,超过好几倍的时候才会申请新的空间拖拽,不然就是首地址往后或者前操作。
回到vs2022上,将p = (int*)realloc(p, 200);
修改成200。
能够看到p的地址果然发生改变了。
由大缩小后首地址不变。
x64下同理:
那么malloc就符合之前的描述:当内存足够时,realloc的内存=原本内存+剩余内存,当内存不够时,realloc会申请新的内存并将原来的内存拷贝到新内存里,旧的将被free掉。
free
void free(void* _Block)
,_Block就是要释放的地址。
1 | int *p = (int*)malloc(10*sizeof(int)); |
但是连续free一块内存空间是会出点毛病的,具体要看编译器怎么处理了。
new
数据类型 *变量名 = new 数据类型:int *p = new int;
数据类型 *变量名 = new 数据类型[arraySize]: int *p = new int[5];
老规矩分配失败返回0
1 | int *p = new int; |
其实底层还是malloc那套,我们随便打个断点反汇编进去,一步步看:
所以万变不离其宗,封装再好,也能看到底层,我们也能模仿造轮子。
其次就是之前说过的,动态分配的内存,都需要我们手动释放,malloc那些用free,而new 对应的时delete
1 | delete p; |
所以数组需要加上[]消除,delete []p2
可以看到程序没有报错和警告。
风险
野指针
前面提到过的悬挂指针问题:
1 | int main(){ |
当内存空间free掉之后,指针指向的内存生命周期到了,没有了。下次程序使用的时候就有可能产生不可预料的错误。
1 | int main(){ |
会报错是好事,能及时止损。
还有一种情况就是你的指针备份过了:
1 | int main(){ |
这玩意也挺狠,利用备份的指针重写,都不知道写到哪去了。
重复释放
1 | free(p); |
编译器会给出警告,虽然你释放过了这片区域,但是难免不保证被被人申请到,当别人申请到了之后,你又再来一下释放。。。这不是一种耍流氓行为吗,所以不可取。
内存碎片
当程序频繁申请和释放内存,就会导致产生内存碎片
虽然释放掉的内存没有被占用,但是无法规避我们后面需要多大的空间。那么这些零散的内存就被称为内存碎片。
当然new和delete会尽量规避风险,有能力自己额外注意。
混用
malloc申请的内存用delete释放
new分配的内存用free释放
底层new就是通过malloc实现的,那么delete也八九不离十是free为原型的
但是,new毕竟是c++的产物,所以不建议混用,以免养成不好习惯或不必要的麻烦。
memcpy
如果想要将数组复制到new出来的内存上时,最常用就是通过循环赋值
1 | int main(){ |
那么c++也有定义好的函数void * memcpy(void* _Dst, const void *_Src, size_t_size);
也就是将src的内存数据复制到dst上,关键是还要指定长度size
1 | int main(){ |
如果不想全复制,那也就是对长度做手脚,修改一下就行。往小了还行,往大了赋值只会越界,后面的内存不归你使用,所以不要傻乎乎玩这么大
memset
在经过memcpy之后,如果只复制几个,后面的元素又没有初始化过。就是一堆乱值。
memset就是将指定内存区域按每个字节的值都设置起来void *memset(void *_Dst,int val,size_t_size);
1 | int *p = new int[5]; |
for循环打印查看结果:
然后就是坏事,因为memset是按照每个字节分配内存。一个字节的范围0-255,十六进制也就0xff。
如果val超过了255,可以看看会发生什么。
val = 0xff的情况下:
十进制可能不够明显,我们转换成十六进制输出std::cout << std::hex << p[i] << std::endl;
val = 0x123456时:
所以建议初始化的时候,val设置为0或者0x00,就不要整别的了
结语
那么动态内存分配就到此先,凡是需要人去手动构造的都需要格外谨慎。