目录
一.为什么会有动态内存分配?
我们已经学过的申请内存的方法有两种
1.int a = 0;//直接创建变量进行申请
2.char arr[20] = {0};//用数组申请内存
不过上述的两种方式有两个特点:
空间开辟大小都是固定的
数组在声明的必须指定数组长度,数组的大小一旦确定后,就无法改变
但是有一些程序它需要的空间是在程序运行时才能知道的,所以就加入了动态内存分配让程序员自己申请和释放空间,使得代码更加灵活
二.malloc、calloc、realloc、free函数
上述所有的函数都是存放在<stdlib.h>头文件中的
1.malloc
malloc函数就是用来动态内存分配的函数,其函数原型如下:
void* malloc(size_t num);
malloc只需要你传送一个 你想要开辟的空间的大小 作为参数
1.如果开辟成功,返回该空间的起始地址;如果开辟失败,则返回NULL,所以在使用malloc的时候一定要做检查
2.由于malloc函数的设计者也不知道你要开辟什么类型的空间,所以必须由你自己来设计返回值的类型,例如:
// 把这一块空间变成你想要的类型
int* ptr = (int*)malloc(sizeof(int)*10);
// 开辟10个int大小的空间(40字节)
//检查是否开辟失败
assert(ptr != NULL);
3.如果你向malloc传递的参数为0,那这是标准未定义的,由编译器决定
在使用动态内存开辟的空间时,最好检查该空间是否开辟失败
assert函数包含在<assert.h>头文件中,它需要接收一个表达式作为参数
2.calloc
calloc与malloc一样都是用来动态内存分配的函数,其函数原型如下:
void* calloc(size_t num,size_t size);
代码的意思是开辟num个大小为size的空间
只不过calloc与malloc唯一的区别就是calloc会把开辟的空间的每个字节都初始化为0
举个栗子:
int main()
{
//malloc开辟
int* ptr_i1 = (int*)malloc(sizeof(int) * 10);
//检查是否开辟失败
assert(ptr_i1 != NULL);
for (int k = 0; k < 10; k++)
{
printf("%d ", ptr_i1[k]);
}
//calloc开辟
int* ptr_i2 = (int*)calloc(10,sizeof(int));
//检查是否开辟失败
assert(ptr_i2 != NULL);
putchar('\n');
for (int k = 0; k < 10; k++)
{
printf("%d ", ptr_i2[k]);
}
free(ptr_i1);
ptr_i1 = NULL;
free(ptr_i2);
ptr_i2 = NULL;
return 0;
}
3.realloc
realloc是用来调整开辟后的空间的大小的,原型如下:
void* realloc(void* ptr,size_t size);
ptr是你要调整的那块空间的地址,size是重新调整后的大小
如果size的大小,小于原来空间的大小,则是减少原来动态开辟的空间;如果大于原来空间的大小,则是扩大原来动态开辟的空间
举个例子:
int main()
{
//malloc开辟40个字节的空间
int* ptr_i1 = (int*)malloc(sizeof(int) * 10);
//检查是否开辟失败
assert(ptr_i1 != NULL);
//代码...
//检查是否为空
assert(ptr_i1 != NULL);
//发现40个字节不够用了
//扩充40个字节,变为80个字节
int* ptr_i2 = (int*)realloc(ptr_i1, sizeof(int) * 20);
return 0;
}
而开辟大于原来空间的内存又分为两种情况:
1.如果向后扩充的空间没有被其他数据占用,这也是正常的情况
2.向后扩充的空间被其他数据所占用,从下图可以看到realloc扩充的空间与内存中其他数据所占用的空间重合了
4.free
free函数与前面的函数正好相反,它是用来释放动态内存开辟的空间的。
原型如下:
void free(void* ptr);
它只需要一个指针告诉该函数需要释放的空间在哪
1.如果给它传递NULL,那么free函数什么都不做
2.如果给free函数传递的参数不是动态内存开辟的,那么该函数的行为是未定义的
int main()
{
//开辟
int* p = (int*)malloc(sizeof(int) * 10);
//检查是否开辟失败
assert(p != NULL);
//释放
free(p);
//问题来了
//需要把指针p赋值为空指针码?
return 0;
}
回答:
最好是把p赋值为空指针,因为在free后,指针p指向的空间已经被释放了,所以这块空间已经没有办法使用了,但是p还保存着这块空间的地址,如果你后面对p进行检查并使用,那是不是就非法访问了?!但如果你给p赋值为NULL,那么后面对他进行检查和使用的时候就知道它是空指针了
所以应这样写
int main()
{
//开辟
int* p = (int*)malloc(sizeof(int) * 10);
//检查是否开辟失败
assert(p != NULL);
//释放
free(p);
p = NULL;
return 0;
}
我们知道动态内存管理开辟的空间也是需要进行释放的,一般有两种方法
一是程序结束后由操作系统释放(小程序可以,大程序不行)
二是程序员主动使用free函数进行释放
[链接]动态分配内存,不释放,程序退出后会被系统回收吗
三.常见错误
1.free释放非动态内存开辟的空间
int main()
{
int a = 0;
int* p = &a;
free(p);
return 0;
}
2.内存泄漏(动态开辟空间未释放)
int main()
{
int* ptr = (int*)malloc(40);
assert(ptr != NULL);
//代码...
return 0;
}
3.对动态内存多次释放
int main()
{
int* ptr = (int*)malloc(40);
assert(ptr != NULL);
free(ptr);
free(ptr);
ptr = NULL;
return 0;
}
4.对NULL进行解引用操作(对开辟失败的动态内存操作)
int main()
{
int* ptr = (int*)malloc(40);
//不进行检查
*ptr = 20;//如果malloc开辟失败就会返回NULL
return 0;
}
5.对部分动态内存进行释放
int main()
{
int* ptr = (int*)malloc(40);
assert(ptr != NULL);
for (int i = 0; i < 5; i++)
{
*ptr = i + 1;
printf("%d ", *ptr++);
}
//此时ptr指向第六块空间,已不再是指向整块起始位置
free(ptr);
ptr = NULL;
return 0;
}
6.对动态内存越界访问
int main()
{
int* ptr = (int*)malloc(20);
assert(ptr != NULL);
for (int i = 0; i < 10; i++)
{
ptr[i] = i + 1;
//i==5时就已经越界
printf("%d ", ptr[i]);
}
free(ptr);
ptr = NULL;
return 0;
}
四.柔性数组
在结构体中有这样一个东西,如果该结构体在最后一个成员前有大于等于1个成员(也就是该结构体内至少有两个元素),那么这最后一个成员就可以是一个不指定大小的数组,其名为柔性数组
柔性数组不占用内存空间,而数组名本身不占用空间,它只是一个偏移量,它代表了一个不可修改的地址常量
//柔性数组
struct stu
{
//必须含至少一个其他成员
char n;
//这就是柔性数组成员
int arr[];
};
int main()
{
// 结构成员的空间 为柔性数组开辟的空间
struct stu* ptr = (struct stu*)malloc(sizeof(struct stu) + 40);
assert(ptr != NULL);
ptr->n = 'A';
printf("%c\n", ptr->n);
for (int i = 0; i < 10; i++)
{
ptr->arr[i] = i;
printf("%d ", ptr->arr[i]);
}
free(ptr);
ptr = NULL;
return ;
还有一种方法可以模拟柔性数组
struct stu
{
char n;
//把数组换成指针
int* p;
};
int main()
{
struct stu* ptr = (struct stu*)malloc(sizeof(struct stu));
//这里的这个指针其实也可以换成,下面这种,直接给成员开辟空间
//struct stu s = { 0 };
//s.ptr = (int*)malloc(40);
assert(ptr != NULL);
ptr->n = 'A';
printf("%c\n", ptr->n);
ptr->p = (int*)malloc(sizeof(int)*10);
for (int i = 0; i < 10; i++)
{
ptr->p[i] = i;
printf("%d ", ptr->p[i]);
}
//这里顺序一定要对,要先释放成员,再释放指针变量
free(ptr->p);
free(ptr);
ptr->p = NULL;
ptr = NULL;
return 0;
}
但是使用柔性数组有下面这样几个函数:
1.方便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
2.这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)
五.C/C++内存区域划分
1.栈区(stack)
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元将自动释放。栈 内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限。栈区主要用来存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2.堆区(heap)
堆区内的空间一般由程序员分配释放,若程序员不释放,程序结束后则可能由操作系统回收。分配方式类似于链表
3.数据段(静态区)
用于存储全局变量和静态数据,程序结束后由操作系统回收
4.代码段
存放函数体(类成员函数和全局函数)的二进制代码