1.为什么要有动态内存分配
我们现如今已经掌握的内存开辟方式有
int main()
{
int a = 0;
int arr[30] = { 0 };
return 0;
}
这两种方式,但是这种开辟空间的方式有两个特点:
1.空间开辟大小是固定的
2.数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小就不可更改了
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,这样的话数组编译时开辟的空间可能就会不够用
于是C语言就引入了动态内存开辟,让我们自己可以申请和释放空间,提高了空间利用的灵活性
2.malloc和free
2.1malloc
C语言提供了一个动态内存开辟的函数
头文件是stdble
void* malloc(size_t size);
-malloc函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
-如果开辟成功,则返回一个指向开辟好的内存的指针
-如果开辟失败,则返回一个空指针(NULL)
-返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,需要我们根据需要进行转换
-如果参数size为0,malloc的行为是标准是未定义的,取决于编译器
#include<stdlib.h>
int main()
{
int * p = (int *)malloc(10 * sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
if (p == NULL)
{
perror("malloc");
return 1;
}
return 0;
}
上述是关于用malloc创建10个整型空间
malloc申请的空间与数组空间的区别:
1.动态内存的大小是可以调整的
2.存放的位置不一样
2.2free
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的
头文件:stdlib.h
函数原型:
void free (void* ptr);
代码应用实例:
free(p);//p变成了野指针
//p指向的空间不属于当前程序
//但是还是可以找到原来的空间(空有地址无法使用)
p = NULL;
//为了更彻底,把p这个野指针用空指针拴住
free函数用来释放动态开辟的内存
如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的
例子:
int main()
{
int arr[20] = { 0 };
free(arr);
return 0;
}
-如果参数ptr是空指针,那么函数什么都不做
例子:
malloc和free一般都是一块使用
3.calloc和realloc
3.1calloc
C语言提供了一个函数叫calloc,calloc函数也用来动态内存分配
头文件:stdlib.h
函数模型:
void* calloc (size_t num, size_t size);
函数的功能是为num大小为size的元素开辟一块空间,并且把空间中每个字节初始化为0
与函数malloc的区别只在于calloc会返回地址之前把申请的空间的每个字节初始化为0
calloc函数的应用例子:
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
if (p == NULL)
{
return 1;
}
return 0;
}
运行结果为
2.2realloc
realloc函数使得动态内存管理更加灵活
有时候我们会发现申请的内存空间太小了,有时候又会觉得太大了,这时候我们就需要realloc函数对内存空间进行灵活的调整
头文件:stdlib.h
函数模型:
void* realloc (void* ptr, size_t size);
-ptr是要调整的内存
-size是调整之后的新大小
-返回值为调整之后的内存起始位置
-这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
-realloc在调整内存空间的是存在两种情况:
1.原有空间之后有足够大的空间:
直接在旧空间之后开辟空间,返回原来的空间地址
2.原有空间之后没有足够大的空间:
realloc函数直接在内存的堆区找一块新的满足大小的空间,并且将旧的数据拷贝到新的空间,然后释放旧的空间,返回新的地址
realloc函数使用的例子
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
if (p == NULL)
{
perror("calloc");
return 1;
}
//如果空间不够
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
else
{
p = ptr;
}
free(p);
p = NULL;
return 0;
}
tips:
realloc不仅可以调整内存空间,还可以想malloc和calloc一样创建动态内存空间
int main()
{
int* p = (int*)realloc(NULL, 40);//==malloc(40);
return 0;
}
4.常见的动态内存的错误
4.1对NULL指针的解引用操作
不注意p是否为空指针的话就会出现上述代码中的错误
加上判断的话编译器就不会报错了
4.2对动态开辟空间的越界访问
4.3对非动态开辟内存使用free释放
//对非动态空间进行释放
int main()
{
int arr[3] = { 0 };
int* p = arr;
free(p);
p = NULL;
return 0;
}
运行结果:
4.4使用free释放一部分动态内存空间
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i;
p++;//这里p的地址移动了,不再是原来的p
}
free(p);
p = NULL;
return 0;
}
运行结果:
4.5对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 40/*这里越界了*/; i++)
{
p[i] = i;
}
free(p);
//......
free(p);//多次释放并且第一次没有将p赋为NULL
p = NULL;
return 0;
}
运行结果:
但如果第一次内存释放已经将p变成空指针NULL,那么多次释放也没事
因为p是空指针,所以函数free什么也不做
4.6动态开辟内存忘记释放(内存泄漏)
void test()
{
int flag = 1;
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return;
}
//使用......
if (flag == 0)
return;
free(p);//看似进行了释放
//但是前面的条件有一个符合就会提前结束函数,便会漏掉释放内存
p = NULL;
}
int main()
{
test();//如果提前释放,那么就会发生内存泄漏
//malloc创建的动态内存空间无法释放,也无法被找到
//无法被找到是因为p只在test函数中使用,test函数也没有返回地址
return 0;
}
总的来说,动态内存管理是一把双刃剑,既能提供灵活的内存管理方式,又会具有一定的风险
5.动态内存题目练习
5.1
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
运行结果:程序会崩溃
解析:(注释)
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);//无返回值,创建malloc动态内存后也没有释放,也没有地址
//造成内存泄露
strcpy(str, "hello world");
//这里对str这个NULL空指针进行了解引用
//所以程序会崩溃
printf(str);
}
int main()
{
Test();
return 0;
}
正确代码:
或者不用二级指针,只是将函数GetMemory加个返回值,返回malloc动态空间的地址
5.2
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
运行结果:
解析:(注释)
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();//str的值变成了GM函数中p的地址
printf(str);//printf可以通过str找到原来char p[] 的空间地址
//但是没有访问权限,char p []只在GM函数中有效
//所以str相当于野指针,无法访问,所以打印出来了这些
}
int main()
{
Test();
return 0;
}
str的使用是个返回栈空间的问题
5.3
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;
}
运行结果:
看似没有问题,hello也可以打印出来,但是遗漏了一个重要的问题
malloc这个动态内存没有进行释放,使得内存泄漏
改正:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
5.4
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
运行结果:
看似没问题,但是str使用了未初始化的内存,意思是str是个野指针,这里是野指针的非法访问,代码是错误的
改正:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)//本意应该是为检测str是否转化为NULL
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
6.柔性数组
C99中,结构体中的最后一个元素允许是未知大小的数组
两种写法:(有些编译器只能由其中一个写法)
struct S
{
int a;
float b;
char c;
int arr[];//未知数组
};
struct S2
{
int a;
float b;
int arr[0];
};
6.1柔性数组的特点
1.-结构中的柔性数组成员前面必须至少一个其他成员
2.-sizeof返回的这种结构大小不包括柔性数组的内存
3.-包括柔性数组成员的结构用malloc()函数进行内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
struct S
{
int n;
int arr[];
};
int main()
{
struct S s;
//这个只创建了有int的结构体,不包含柔性数组的内存空间
struct S* pf = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
//这才是创建有柔性数组的结构体的代码
return 0;
}
应用:
struct S
{
int n;
int arr[];
};
int main()
{
struct S* pf = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
if (pf == NULL)
{
perror("malloc");
return 1;
}
pf->n = 10;
int i = 0;
for (i = 0; i < 10; i++)
{
pf->arr[i] = i;
printf("%d\n", pf->arr[i]);
}
return 0;
}
运行结果:
由于是malloc创建的动态内存空间,所以可以用realloc来对内存进行调整
struct S
{
int n;
int arr[];
};
int main()
{
struct S* pf = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
if (pf == NULL)
{
perror("malloc");
return 1;
}
pf->n = 10;
int i = 0;
for (i = 0; i < 10; i++)
{
pf->arr[i] = i;
printf("%d\n", pf->arr[i]);
}
//调整空间
struct S* ptr = (struct S*)realloc(pf, (sizeof(struct S) + 40 * sizeof(int)));
if (ptr != NULL)
{
pf = ptr;
ptr = NULL;
}
else
{
return 1;
}
//使用
//释放空间
free(pf);
pf = NULL;
return 0;
}
6.2柔性数组的优势
还有一种类型也可以有柔性数组的作用
就是将柔性数组变成指向malloc创建动态内存的指针
struct S
{
int n;
int* arr;
};
int main()
{
struct S* s = (struct S*)malloc(sizeof(struct S));
int* pl = (int*)malloc(20 * sizeof(int));
if (pl == NULL)
{
perror("malloc");
return 1;
}
else
{
s->arr = pl;
}
//use
//......
//调整内存
int* ptr = (int*)realloc(pl, 40 * sizeof(int));
if (ptr != NULL)
{
pl = ptr;
}
else
{
perror("realloc");
return 1;
}
//use......
//ending
free(s->arr);
free(pl);
pl = NULL;
return 0;
}
两者对比我们可以发现
柔性数组既方便内存的释放(释放一次即可),并且有利于提高访问速度