身在井隅,心向星光
眼里有诗,自在远方
目录
动态内存的简单介绍
动态内存的优势
可以控制内存的大小
可以多次利用这部分空间
动态内存函数malloc、free
malloc开辟函数
free释放函数
动态内存函数calloc、realloc
calloc开辟函数
realloc调整函数
动态内存在运用中常出现的错误
1. 对NULL指针的解引用操作:
2.对动态开辟空间的越界访问
3.对非动态开辟内存使用free释放
4.使用free释放一块动态开辟内存的一部分
5.对同⼀块动态内存多次释放
6.动态开辟内存忘记释放(内存泄漏)
柔性数组
柔性数组的定义:
柔性数组的特点:
柔性数组的运用 :
我们平时写程序内存不够用是不是手动去扩容
这样程序小了浪费空间,程序大了空间不够用
不启用程序时,空间还得不到很好的释放
不用担心我们今天就来解决这类问题
我们今天讲解动态内存的管理
我们先来介绍一下,为什么会有动态内存?
我们学过的两种的内存开辟方法主要是静态内存开辟如下
要么用变量开辟一个空间,要么用数组开辟一个连续的空间
int a = 10; //在栈空间上开辟四个字节
int a[4] = {0} //在栈空间上开辟十六个字节
如果我们所需的空间大小只有在程序运行的时候才会知道,那么之前的数组编译开辟内存的方法就得不到满足,于是我们引进了动态内存
动态内存的优势在于:
可以控制内存的大小
我们可以自己申请和释放内存空间,大了可以调小,小了可以调大,这样程序就比较灵活
可以多次利用这部分空间
动态内存分配的空间,可以通过利用完,就可以 free 这块申请的空间,当再次用动态内存申请空间时,就可以再次利用这块空间,这样也能在一定程度上,可以节省一定的空间
void* malloc (size_t size);
这个函数向内存申请⼀块连续可用的空间,并返回指向这块空间的指针
malloc所涉及的头文件是#include
- size -- 内存块的大小,以字节为单位
- 该函数如果开辟成功,则返回一个指针 ,指向已分配大小的内存。如果请求失败,则返回 NULL,因此malloc的返回值⼀定要做检查
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己强制类型转换决定 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
我们的malloc函数申请的空间是在内存的堆区:
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
//开辟一个10个整形字节的空间并用指针p来接收
if (p == NULL) //判断开辟成功与否
{
perror("malloc");//报错信息函数,反应malloc的错误
return 1;
}
//这样我们就可以使用这10个整形字节的空间了
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
如果开辟的空间巨大,malloc给不了那么大的空间则返回空指针:
现在如果这空间我们不想用了,我们可不可以还回去?
就像借钱一样,不能只借不还,我们可以用free函数来释放空间
void free (void* ptr);
free函数的作用就是释放动态开辟的内存
free所涉及的头文件是#include
- ptr -- 指针指向一个要释放内存的内存块的起始地址,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的
- 如果传递的参数是一个空指针,则不会执行任何动作
- 因为ptr把内存释放后依然指向原来空间的起始地址,会变为野指针,为了避免出现这种情况,所以我们要手动将ptr置为空指针
拿我们刚刚写的代码举例:
这样写的代码才是完美的
#include
#include
int main()
{
int* p = (int*)malloc(10* sizeof(int)); //开辟空间
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++) //打印
{
printf("%d ", *(p + i));
}
free(p); //释放空间
p = NULL; //手动置空
return 0;
}
malloc、calloc、realloc 申请的空间如果不主动释放,出了作用域是不会销毁的
释放的方式:
1.free主动释放2.直到程序结束,才由操作系统回收
注意:为了避免发生内存泄漏(一个内存空间没有释放掉不能被其他内容访问,内存泄露堆积后果很严重,无论多少内存,迟早会被占光)我们应该在不使用这块空间的时候及时释放掉
malloc注意:
要有一点要注意的是malloc函数的初始化并不是0
我把刚刚的赋值循环给撤回了,让我们看看malloc的初始化值是多少
#include
#include
int main()
{
int* p = (int*)malloc(10* sizeof(int)); //开辟空间
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++) //打印
{
printf("%d ", *(p + i));
}
free(p); //释放空间
p = NULL; //手动置空
return 0;
}
我们用十六进制的形式显现出来的就是cd这样一排
因为malloc分配的空间是未初始化的,所以在运行的时候未免出现什么异常,最好初始化一下
void *calloc(size_t ptr, size_t size)
calloc所涉及的头文件是#include
分配所需的内存空间,并返回一个指向它的指针
malloc 和 calloc 之间的不同点是:
malloc 不会设置内存为零,而 calloc 会设置分配的内存为零
- ptr -- 要被分配的元素个数
- size -- 元素的大小
#include
#include
int main()
{
int* p = (int*)calloc(10,sizeof(int)); //开辟空间
if (p == NULL)
{
perror("calloc");
return 1;
}
for (int i = 0; i < 10; i++) //打印
{
printf("%d ", *(p + i));
}
free(p); //释放空间
p = NULL; //手动置空
return 0;
}
callo与malloc相比的好处就是:将内存初始化为0,在一些情况下可以做到简化代码
realloc所涉及的头文件是#include
void *realloc(void *ptr, size_t size)
作用:尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小
- ptr -- 指针指向一个要重新分配内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针
- size -- 内存块的新的大小,以字节为单位。如果大小为 0,且 ptr 指向一个已存在的内存块,则 ptr 所指向的内存块会被释放,并返回一个空指针
realloc在调整失败后直接返回NULL
realloc在调整内存空间成功 扩容 时是存在两种情况:◦ 情况1:原有空间之后有足够大的空间◦ 情况2:原有空间之后没有足够大的空间
情况1:在已经开辟好的空间后边,没有足够的空间,直接进行空间的扩大在这种情况下,realloc函数会在内存的堆区重新找一个空间(满足新的空间的大小需求的),同时会把旧的数据拷贝到新的新空间,然后释放旧的空间,同时返回新的空间的起始地址
情况2:在已经开辟好的空间后边,有足够的空间,直接进行扩大扩大空间后,直接返回旧的空间的起始地址
(切记不能直接使用malloc、calloc、 realloc的指针接收,如果realloc一旦开辟失败之前开辟的空间也就找不到了 ) 为了保险起见而是用临时指针ptr来接收并判断是否扩容成功
#include
#include
int main()
{
int* p = (int*)malloc(10* sizeof(int)); //开辟空间
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 20; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 20; i++) //打印
{
printf("%d ", *(p + i));
}
//空间不够,想要扩大空间,80个字节
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr != NULL) //如果扩容成功,就将ptr赋值给p
{
p = ptr;
}
else
{
perror("realloc");
return 1;
}
free(p); //释放空间
p = NULL; //手动置空
return 0;
}
当然realloc除了扩容还可以缩容:
realloc可以随意调整动态内存空间的大小
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int)); //开辟空间
if (p == NULL)
{
perror("malloc");
return 1;
}
//空间太大,想要缩小空间,20个字节
int* ptr = (int*)realloc(p, 5* sizeof(int));
if (ptr != NULL) //如果缩容成功,就将ptr赋值给p
{
p = ptr;
}
else
{
perror("realloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 5; i++) //打印
{
printf("%d ", *(p + i));
}
free(p); //释放空间
p = NULL; //手动置空
return 0;
}
realloc除了调整函数空间之外还可以实现跟malloc一样的功能:
#include
#include
int main()
{
int* p = (int*)realloc(NULL, 10 * sizeof(int));
if (p == NULL)
{
perror("realloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
将realloc的指向的空间置为NULL,realloc就可以直接开辟动态内存空间
1. 对NULL指针的解引用操作:
错误代码描述:
int* p = (int*)malloc(max*sizeof(int)); *p = 10; free(p);
因为如果开辟的内存太大,malloc会返回NULL,这个时候对一个NULL指针进行解引用操作编译器就会报错
修正后:
int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } else { *p = 20; } free(p); p = NULL;
我们只要判断p是否为空,就能避免发生这类情况
2.对动态开辟空间的越界访问
错误代码描述:
int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } for (int i = 0; i <= 10; i++) { *(p + i) = i; } free(p); p = NULL;
我开辟10个整形类型的空间,却要访问11个元素,这样照成了越界访问
修正后:
int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } for (int i = 0; i < 10; i++) { *(p + i) = i; } free(p); p = NULL;
只要我们以后写代码小心注意一点,就可以避免这个情况
3.对非动态开辟内存使用free释放
错误代码描述:
int a = 10; int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } int *p = &a; //p指向的空间不再是堆区上的空间 free(p); p = NULL;
将静态栈区的a地址赋值给p,p所指向的空间就不再是堆区,而free只能释放堆区的空间所以导致程序崩溃
注意:
不能对栈区的空间进行free释放,否则会报错
4.使用free释放一块动态开辟内存的一部分
错误代码描述:
int *p = (int *)malloc(10*(sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } p++; free(p); p = NULL;
p++导致p不再指向动态内存的 起始位置 ,free函数在释放的时候必须是从 起始位置 开始释放,所以导致程序崩溃
注意:
不要随意改变指向动态内存空间指针的位置
5.对同⼀块动态内存多次释放
错误代码描述:
int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } free(p); free(p); p = NULL;
多次释放同一块空间导致程序崩溃
修正后:
int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } free(p); p = NULL; free(p); p = NULL;
把p重新定义成空指针就可以再次释放空间,不过没任何实际意义。注意不要多次释放同一块空间
6.动态开辟内存忘记释放(内存泄漏)
void test() { int *p = (int *)malloc(10*sizeof(int)); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }
malloc将10个整形的字节的空间赋值给p,但是没有释放空间。p是个局部变量出了函数定义域就销毁了,那么这10个整形的字节就找不到,得不到释放(除非程序结束)。那么这10个整形的字节的空间是不是等于内存泄漏。内存泄漏会导致程序内存占用越来越多,最终导致崩坏
忘记释放不再使用的动态开辟的空间会造成内存泄漏
修改后:void test() { int *p = (int *)malloc(10*sizeof(int)); if(NULL != p) { *p = 20; } free(p); p = NULL; } int main() { test(); while(1); }
所以大家在开辟内存后记得释放内存哦
结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员
struct Stu
{
int num;
int arr[];//柔性数组的成员
};
//或者
struct Stu
{
int num;
int arr[0];//柔性数组的成员
};
• 结构中的柔性数组成员前面必须至少⼀个其他成员• sizeof 返回的这种结构大小不包括柔性数组的内存• 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小以适应柔性数组的预期大小
我们来看一下面这段代码的结构体大小是多少呢?
#include
struct Stu
{
int num;
int arr[];
}S;
int main()
{
printf("%d\n", sizeof(S));
return 0;
}
我们发现结构体的大小是4,是一个整形成员的大小,所以柔性数组是不占用结构体空间的
我们来看以下代码:
我们在结构体4个字节的情况下,给结构体多开辟了10个整形的字节,那么这10个整形的字节给谁用了呢?
答案当然是我们的-柔性数组
#include
#include
typedef struct
{
int num;
int arr[];
}Stu;
int main()
{
int i = 0;
Stu* p = (Stu*)malloc(sizeof(Stu) + 10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
p->num = 100;
for (i = 0; i < 10; i++)
{
p->arr[i] = i;
}
printf("%d\n", p->num);
for (i = 0; i < 10; i++)
{
printf("%d ", p->arr[i]);
}
free(p);
p = NULL;
return 0;
}
像这种用柔性数组扩容结构体,用直接我们的内存函数也可以做到:
我们这样对arr指向的空间进行扩容,使用完后将arr指向的空间释放掉
#include
#include
typedef struct
{
int num;
int* arr;
}Stu;
int main()
{
Stu* p = (Stu*)malloc(sizeof(Stu));
p->num = 100;
if (p == NULL)
{
perror("malloc");
return 1;
}
p->arr = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc-2");
return 1;
}
printf("%d\n", p->num);
for (int i = 0; i < 15; i++)
{
p->arr[i] = i;
}
for (int i = 0; i < 15; i++)
{
printf("%d ", p->arr[i]);
}
int* ptr = (int*)realloc(p->arr, 15 * sizeof(int));
//如果空间不够用,进行内存扩容
if (ptr == NULL)
{
perror("realloc");
}
else
{
p = ptr;
}
for (int i = 10; i < 15; i++)
{
p->arr[i] = i;
}
for (int i = 10; i < 15; i++)
{
printf("%d ", p->arr[i]);
}
free(p->arr); //释放的是指向arr的空间
p->arr = NULL;
free(p); //释放的是p的空间
p = NULL;
return 0;
}
上述 代码1 和 代码2 可以完成同样的功能,但是柔性数组的实现有两个好处:
方便内存释放:柔性数组扩容只需要一次释放,而使用第二种要进行多次释放
有利于访问速度的提高:连续的内存有益于提高访问速度,也有益于减少 内存碎片 (会让程序内存利用率不高)
本期的动态内存就讲到这里
希望这些知识能让你对动态内存有更深的理解
我们下期再会~