前言

在正常情况下,声明一个数组时type arrayName [arraySize]

1
2
int ch[10] = {0}; //就是将是个成员全部初始化为0
int ch2[] = {1,2,3,4,5}; //没指定大小,但是有初始化,那么数组的大小就会根据初始化成员个数而定

还有一种情况就是arraySize是const类型或者#define,因为在规范里定义了,声明数组大小的时候必须是常量表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
#define N 5

int main(){
int ch[N] = { 0 }; //合法,N是常量
const int a = 5;
int ch2[a] = { 0 }; //合法,a是常量

int b = 10;
int ch3[b] = { 0 }; //不合法,b是一个变量

return 0;
}

至于为什么不能是变量,放到最后再说。


正文

malloc

这就不得不提c语言的malloc函数了void* malloc(size_t size)

1
2
3
4
5
6
7
8
9
10
11
//malloc 是向计算机申请一片内存使用,所以参数为type*typesize
malloc(sizeof(int)*10); //sizeof(type)更加直观,当然熟记类型大小也问题不大,就是怕在32位和64位下有区别,建议保守使用sizeof获取变量类型大小
//然后就是如何使用这片内存,学过指针,所以我们会将地址抛给指针
int *p = malloc(sizeof(int)*10); //这里会提示类型不符合,因为malloc原型是void*,所以需要强制转换
int *p = (int*)malloc(sizeof(int)*10);

p[0] = 1;
p[1] = 2;
p[2] = 1*2;
//都是合法的,[前提是不超过分配的内存大小],这个在指针那里就说过了为什么合法

当然,malloc如果没有申请到内存就会返回0,指针得到的结果也就为0


calloc

void * calloc(size_t count,size_t size)
差别到不是太大,就是两个形参,一个是个数,一个是类型大小。

1
2
3
4
int *p = (int*)calloc(10,4);
//或者
int *p = (int*)calloc(10,sizeof(int));
//当然10可以用变量代替

同样的,如果没有成功分配到内存,返回也是0,好处是calloc分配完内存后会进行初始化,而malloc则没有这么贴心


realloc

re可能就会联想到重新,realloc就是重新分配内存
void* realloc(voir* _Block, size_t_size)

先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。
——摘自百度百科

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(){
int *p = (int *)malloc(sizeof(int) * 10);
p[0] = 1;

std::cout << p << std::endl;
std::cout << p[0] << std::endl;

p = (int*)realloc(p, 20);

std::cout << p << std::endl;
std::cout << p[0] << std::endl;

return 0;
}

这里可以看到经过扩容之后的内存,起始位置发生了改变,但是原本p[0]上的1还是存在

1
2
3
4
5
6
7
8
9
10
11
12
int *p = (int *)malloc(sizeof(int) * 10);
p[0] = 1;
int *p2 = p;

std::cout << p << std::endl;
std::cout << p[0] << std::endl;

p = (int*)realloc(p, 5);

std::cout << p << std::endl;
std::cout << p2 << std::endl;
std::cout << p[0] << std::endl;

然后改成缩小,并且用一个指针先记录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
2
3
4
5
6
int *p = (int*)malloc(10*sizeof(int));
free(p);
//free完之后,原本的指针会变成悬挂指针也叫野指针,因为它现在没法用了
//常见情况下就是要么指针没有初始化,要么指针的对象生命周期到了,也就是局部变量离开作用域后失效
//还有一种就是free完之后,尽量将指针置空或者置零。
p = NULL; //或者=0,以后处理的工作大了也可以定义一个宏或者函数去处理这种事件

但是连续free一块内存空间是会出点毛病的,具体要看编译器怎么处理了。


new

数据类型 *变量名 = new 数据类型:int *p = new int;
数据类型 *变量名 = new 数据类型[arraySize]: int *p = new int[5];

老规矩分配失败返回0

1
2
3
int *p = new int;
int *p2 = new int[5];

其实底层还是malloc那套,我们随便打个断点反汇编进去,一步步看:

所以万变不离其宗,封装再好,也能看到底层,我们也能模仿造轮子。

其次就是之前说过的,动态分配的内存,都需要我们手动释放,malloc那些用free,而new 对应的时delete

1
2
delete p;
delete p2; //p2是分配了五个int类型的内存,所以这里会产生警告,不然你只是delete掉p2首地址

所以数组需要加上[]消除,delete []p2

可以看到程序没有报错和警告。


风险

野指针

前面提到过的悬挂指针问题:

1
2
3
4
5
6
7
8
9
10
int main(){
int *p = (int *)malloc(sizeof(int) * 10);
p[0] = 250;

free(p);

p[2] = 99; //编译器不一定会报错

return 0;
}

当内存空间free掉之后,指针指向的内存生命周期到了,没有了。下次程序使用的时候就有可能产生不可预料的错误。

1
2
3
4
5
6
7
8
int main(){
int *p = new int[10];
p[0] = 250;
delete[]p;
p[0] = 300; //这段程序就会报错

return 0;
}

会报错是好事,能及时止损。

还有一种情况就是你的指针备份过了:

1
2
3
4
5
6
7
int main(){
int *p = new int[10];
int *p2 = p;
p[0] = 250;
delete[]p;
p2[0] = 300; //编译通过了
}

这玩意也挺狠,利用备份的指针重写,都不知道写到哪去了。


重复释放

1
2
3
4
5
6
free(p);
free(p);

//或者
delete p;
delete p;

编译器会给出警告,虽然你释放过了这片区域,但是难免不保证被被人申请到,当别人申请到了之后,你又再来一下释放。。。这不是一种耍流氓行为吗,所以不可取。


内存碎片

当程序频繁申请和释放内存,就会导致产生内存碎片

虽然释放掉的内存没有被占用,但是无法规避我们后面需要多大的空间。那么这些零散的内存就被称为内存碎片。
当然new和delete会尽量规避风险,有能力自己额外注意。


混用

malloc申请的内存用delete释放
new分配的内存用free释放

底层new就是通过malloc实现的,那么delete也八九不离十是free为原型的
但是,new毕竟是c++的产物,所以不建议混用,以免养成不好习惯或不必要的麻烦。


memcpy

如果想要将数组复制到new出来的内存上时,最常用就是通过循环赋值

1
2
3
4
5
6
7
8
int main(){
int ch[5] = { 1,2,3,4,5 };
int *p = new int[5];

for (int i = 0; i < 5; i++){
p[i] = ch[i];
}
}

那么c++也有定义好的函数
void * memcpy(void* _Dst, const void *_Src, size_t_size);
也就是将src的内存数据复制到dst上,关键是还要指定长度size

1
2
3
4
5
6
int main(){
int ch[5] = { 1,2,3,4,5 };
int *p = new int[5];

memcpy(p, ch, sizeof(int) * 5);
}

如果不想全复制,那也就是对长度做手脚,修改一下就行。往小了还行,往大了赋值只会越界,后面的内存不归你使用,所以不要傻乎乎玩这么大


memset

在经过memcpy之后,如果只复制几个,后面的元素又没有初始化过。就是一堆乱值。
memset就是将指定内存区域按每个字节的值都设置起来
void *memset(void *_Dst,int val,size_t_size);

1
2
int *p = new int[5];
memset(p,0,sizeof(int)*5);

for循环打印查看结果:

然后就是坏事,因为memset是按照每个字节分配内存。一个字节的范围0-255,十六进制也就0xff。
如果val超过了255,可以看看会发生什么。

val = 0xff的情况下:
十进制可能不够明显,我们转换成十六进制输出std::cout << std::hex << p[i] << std::endl;

两个十六进制数一个字节

val = 0x123456时:

所以建议初始化的时候,val设置为0或者0x00,就不要整别的了


结语

那么动态内存分配就到此先,凡是需要人去手动构造的都需要格外谨慎。