1. 为什么存在动态内存分配
内存的使用方式
- 创建变量(开辟一块独立的空间)
- 局部变量,存在栈区。
- 全局变量,放静态区。
- 创建数组(开辟一块连续的空间)
- 局部数组-栈区。
- 全局数组-静态区。
但是上述开辟空间的方式有两个特点:
- 空间开辟的大小是固定的
- 数组在定义的时候,必须指定数组的长度,它所需要的内存在编译时分配。
假设我有一个50个坑位的数组int arr[50] = {0};
,如果只用存30个东西进去,剩下20个位置明显就浪费掉了,如果有60则不够存。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态内存开辟了,不是所有编译器都支持用变量改变数组元素个数
动态内存分配的好处就是要一块空间就去开辟一块空间,不会造成浪费或坑位不够
2. 动态内存函数
引用头文件<stdlib.h>
2.1 malloc
内存开辟函数
void* malloc (size_t size)
;
申请一块连续可用
的存储空间,并返回一个指向这个空间的指针
- 如果开辟成功,则返回一个指向开辟好的空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
举个栗子:
向内存申请10个整型的空间
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int* p = (int*)malloc(10*sizeof(int));//返回的是void*,要强转
if(NULL == p)//如果开辟空间失败
{
//打印错误原因
printf("%s\n",strerror(errno));
}
else
{
//可以正常使用空间
int i;
for(i = 0;i < 10;i++)
{
*(p + i) = i;
printf("%d ",*(p + i));
}
putchar('\n');
}
return 0;
}
0 1 2 3 4 5 6 7 8 9
开辟失败的情况
int* p = (int*)malloc(1000000000*sizeof(int));
if(NULL == p)//开辟空间失败
{
printf("%s\n",strerror(errno));
}
程序执行结果Cannot allocate memory
2.2 free
释放动态开辟的空间
void free (void* ptr);
有借有还,再借不难,free函数是用来释放动态开辟的内存。
当malloc申请的空间不再使用的时候,就应该free给操作系统
- 要释放哪块空间,就把哪块空间的地址交给free就完事了。
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
- 在释放内存空间后,所指向的指针要致空,不再使用它。
把p指向的空间还回去
代码如果还要继续写,但是这块空间不再使用时,自己借的空间应该自己主动free掉,而不是等着return 0帮你还。
注意
:此时只是把p指向的那块空间还了回去,但是p还记得之前手里拿的地址,"不法分子"利用p还是能找到那块空间,所以主动点,动动发财的小手p = NULL
2.3 calloc
开辟一块空间,并将元素初始化成0。
void* calloc (size_t num, size_t size);
- void* calloc (元素个数, 每个元素的大小);
- 为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的唯一区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
- 开辟成功,返回地址,开辟失败,返回NULL
举个栗子:
开辟一个40个字节的空间
malloc(10 * sizeof(int));
calloc(10 , sizeof(int));
calloc开辟空间的同时全部初始化为0
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int* p = (int*)calloc(10,sizeof(int));
if(NULL == p)
{
printf("%s",strerror(errno));
}
else
{
int i;
for(i = 0;i < 10;i++)
{
printf("%d ",*(p + i));
}
putchar('\n');
}
free(p);//用完才释放
p = NULL;
return 0;
}
0 0 0 0 0 0 0 0 0 0
和mallloc的区别也就只有malloc不初始化,calloc全部初始化为0而已。
2.4 realloc
调整动态开辟内存空间的大小
有时候觉得开辟的空间不够了,要增加一些,有时候又多了要减少一点,这时候就要用到realloc函数来调整开辟的空间。
void* realloc (void* ptr, size_t size);
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
- 如果追加空间失败,会返回NULL。
举个栗子
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
int i;
int* p = (int*)malloc(20);
if(NULL == p)
{
printf("%s\n",strerror(errno));
}
else
{
printf("调整前:");
for(i = 0;i < 5;i++)
{
*(p + i) = i;
printf("%d ",*(p + i));
}
putchar('\n');
}
int* pp = (int*)realloc(p,40);//改p指向的空间为40字节
if(NULL != pp)//追加空间如果失败会返回NULL,如果用p来接收,会导致原来p指向的20个字节的空间也找不到了
{ //所以先用一个新的指针变量接收,来判断是不是NULL
p = pp;
}
printf("调整后:");
for(i = 5;i < 10;i++)
{
*(p + i) = i;
}
for(i = 0;i < 10;i++)
{
printf("%d ",*(p + i));
}
putchar('\n');
free(p);
p = NULL;
return 0;
}
调整前:0 1 2 3 4
调整后:0 1 2 3 4 5 6 7 8 9
realloc在调整内存空间时存在两种情况:
- 情况1:原有空间之后有足够大的空间,扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化,
返回旧地址
。 - 情况2:原有空间之后没有足够大的空间,没有足够的空间时总不能去抢别人的地盘吧,realloc会重新找一个内存区域开辟一块符合要求的空间,
返回新地址,拷贝旧数据,free旧空间
。
不管情况1还是2,用完动态开辟的内存之后都应该主动free掉
利用realloc开辟动态空间
void * realloc (NULL, size_t size);
举个栗子:
int* p = (int*)realloc(NULL,40);//效果等于malloc(40)
3. 常见动态内存错误
3.1 对NULL指针的解引用操作
万一开辟空间失败了,返回来个NULL,对接收NULL的指针进行解引用就会导致程序崩溃。
int* p = (int*)malloc(40);
//万一malloc开辟空间失败了,p就被赋值为NULL
int i;
for(i = 0;i < 10;i++)
{
//这里就会变成对NULL指针的解引用
*(p + i) = i;
}
free(p);
p = NULL;
所以在使用动态开辟的空间之前,一定要先判断指针是否为NULL。
3.2 对动态开辟空间的越界访问
int* p = (int*)malloc(5*sizeof(int));
if(NULL == p)
{
printf("%s\n",strerror(errno));
}
else
{
int i;
for(i = 0;i < 10;i++)//这块空间只有20个字节,却要使用40个字节
{
*(p + i) = i;
printf("%d ",*(p + i));
}
putchar('\n');
}
free(p);
p = NULL;
程序直接崩溃
3.3 对非动态开辟内存使用free释放
int a = 10;
int* pa= &a;
free(pa);
pa = NULL;
程序照样崩溃
3.4 使用free释放一块动态开辟内存的一部分
int* p = (int*)malloc(40);
if(NULL == p)
{
return 0;
}
else
{
int i;
for(i = 0;i < 10;i++)
{
*p++ = i;
}
}
//回收空间
free(p);
p = NULL;
直接崩溃
7fff475e7000-7fff475e9000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
已放弃(吐核)
因为p不再指向动态内存的起始地址
3.5 对同一块动态内存多次释放
int* p = (int*)malloc(40);
if(NULL == p)
{
return 0;
}
else
{
int i;
for(i = 0;i < 10;i++)
{
*(p + i) = i;
}
}
free(p);
//....代码写着写着忘记p已经释放过一次了,于是
free(p);
程序依然崩溃
7fff475e7000-7fff475e9000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
已放弃(吐核)
解决方法:在第二次释放之前,将p转换为空指针,再释放,就没有问题了,
所以free掉一块空间之后立刻将指针置为NULL是很有必要的。
3.6 动态开辟内存忘记释放(内存泄漏)
只借不还,不停的开辟开辟压根不带还的
while(1)
{
malloc(1);//不停的开辟一个字节的空间
}
已杀死
疯狂的开辟内存然后被计算机自动干掉了
4. 小试牛刀
4.1 题目1
运行Test后会发生什么?
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf("%s\n",str);
}
1. 运行代码时会出现崩溃的情况。
2. 程序存在内存泄露。
改正方法1
void GetMemory(char **p)
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf("%s\n",str);
free(str);
str = NULL;
}
hello world
改正方法2
让p这个临时变量,在临死之前把它手里记住的地址交给str
char* GetMemory(char *p)
{
p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory(str);
strcpy(str, "hello world");
printf("%s\n",str);
free(str);
str = NULL;
}
别忘了在开辟空间之后,str还是有可能为NULL,记得判断
4.2 题目2
问题出在哪?
#include <stdio.h>
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf("%s\n",str);
}
int main()
{
Test();
return 0;
}
非法访问内存,输出了个随机值
警告:函数返回局部变量的地址 [-Wreturn-local-addr]
return p;
^
��P�
因为数组p是局部变量,放在栈区,出了p所在的函数,p的内容会被销毁,这块空间还回去了,再拿着p的地址找回去,找到的就不知道是啥了。栈空间上的地址不要随便返回
解决方法1:
加个static
,延长局部变量的生命周期,把p放到静态区,让p出了函数之后不被销毁。
static char p[] = "hello world";
解决方法2
让hello world变成一个常量字符串,放在只读数据区
char* p = "hello world";
4.3 题目3
Test运行之后发生什么?
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;
}
可以正常打印,但是str用完之后没有主动free,存在内存泄露
4.4 题目4
运行Test后会发生什么?
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);//free释放str指向的空间之后,并不会把str置为NULL
if(str != NULL)//那么这个判断就没有意义了
{
strcpy(str, "world");//还将world拷到原来的那块空间属于非法访问
printf(str);
}
}
打印出world
但是,存着hello的那块空间已经被free掉,这块空间不再属于str,但是str任然记得这块空间的地址,将world拷到str属于非法访问内存,(酒店退房之后,但是还记得房间号,硬要用这个房间,非法访问了)
实际考察free掉str后,str成了个野指针,并不会变成空指针,那么if里的判断条件就没意义了
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if(str != NULL)//将str置为NULL后,这个判断才有意义
{
strcpy(str, "world");//还将world拷到原来的那块空间属于非法访问
printf(str);
}
}
5. 柔性数组
C99 中,结构体中的最后一个成员允许是未知大小的数组,这就叫做『柔性数组』成员。
举个栗子:
typedef struct student
{
int a;
int b[];//未知大小的-柔性数组成员-数组的大小是可以调整的
}stu;
编译是能通过的
有些编译器可能无法编译,可以改成
typedef struct student
{
int a;
int b[0];//未知大小的-柔性数组成员-数组的大小是可以调整的
}stu;
5.1 柔性数组的特点
- 结构体中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。
举个栗子
typedef struct student
{
int a;
int b[0];//柔型数组成员
}stu;
int main()
{
stu s;
printf("s = %d\n",sizeof(s));
return 0;
}
s = 4
可以看到,结构体的大小并没有包含柔性数组的大小.
5.2 柔性数组的使用
给柔性数组成员开辟5个整型的空间:
typedef struct student
{
int a;
int b[];
}stu;
int main()
{
stu* ps = (stu*)malloc(sizeof(stu) + 5 * sizeof(int));
return 0;
}
因为结构体大小是4字节,给结构体追加的20个字节就是给柔性数组开辟的字节。
此时总开辟空间大小就是(结构体大小+给柔性数组单独开辟的空间大小)
代码1
typedef struct student
{
int a;
int b[0];
}stu;
int main()
{
stu* ps = (stu*)malloc(sizeof(stu) + 5 * sizeof(int));//给柔性数组开辟了5个整型的空间
ps -> a = 100;
int i;
for(i = 0;i < 5;i++)
{
ps -> b[i] = i;//将前5个元素赋值0 1 2 3 4
}
stu* pa = realloc(ps,44);//将柔性数组的大小调整为10个整型
if(NULL != pa)
{
ps = pa;
}
for(i = 5;i < 10;i++)
{
ps -> b[i]= i;
}
for(i = 0;i < 10;i++)
{
printf("%d ",ps -> b[i]);
}
printf("\na = %d\n",ps -> a);
free(ps);
ps = NULL;
return 0;
}
0 1 2 3 4 5 6 7 8 9
a = 100
5.3 柔性数组的优势
上面的代码1也可以改成这个样子
代码2
typedef struct student
{
int a;
int* b;
}stu;
int main()
{
stu* ps = (stu*)malloc(sizeof(stu));
ps -> b = malloc(5 * sizeof(int));
ps -> a = 100;
int i;
for(i = 0;i < 5;i++)
{
ps -> b[i] = i;
printf("%d ",ps -> b[i]);
}
putchar('\n');
//调整大小
int* ptr = realloc(ps -> b,10*sizeof(int));
if(NULL != ptr)
{
ps -> b = ptr;
}
for(i = 5;i < 10;i++)
{
ps -> b[i] = i;
}
for(i = 0;i < 10;i++)
{
printf("%d ",ps -> b[i]);
}
printf("\na = %d\n",ps -> a);
free(ps -> b);
ps -> b = NULL;
free(ps);
ps = NULL;
return 0;
}
0 1 2 3 4
0 1 2 3 4 5 6 7 8 9
a = 100
代码1和代码2可以完成同样的功能,代码1(柔性数组)相对于方法2的优势。
- 优势1:方便内存释放
- 代码2中使用了两次malloc,所以就得进行两次free。
- malloc用的越多,free也就用的越多。
- free少了会导致内存泄露,free的先后顺序错了,也会导致出问题。
- 优势2 有利于访问速度.
- 连续的内存有益于提高访问速度,也有益于减少内存碎片。
- 多次使用malloc会导致到处开辟空间,内存碎片多了后其他东西可能就不够位置放了。
- 这里挖一个坑,那里挖一个坑,挤的别人只能在夹缝里里找空间存放东西