第十九讲:动态内存分配
- 1.为什么要有动态内存分配
- 2.malloc和free
- 2.1malloc
- 2.1.1函数原型
- 2.1.2函数使用
- 2.2free
- 2.2.1函数原型
- 2.2.2函数使用
- 2.2.3函数使用注意事项
- 2.2.3.1注意点1
- 2.2.3.2注意点2
- 2.2.3.3注意点3
- 2.2.3.4注意点4
- 2.3malloc和free使用注意事项
- 2.3.1内存覆盖
- 2.3.2内存泄漏
- 3.calloc和realloc
- 3.1calloc函数
- 3.1.1函数原型
- 3.1.2函数使用
- 3.2realloc函数
- 3.2.1函数原型
- 3.2.2函数使用
- 4.常见的动态内存的错误
- 4.1对NULL指针的解引用操作
- 4.2对动态开辟空间的越界访问
- 4.3对非动态开辟的空间使用free释放
- 4.4使用free释放开辟空间的一部分
- 4.5对同一块动态空间多次释放
- 4.6动态开辟空间忘记释放
- 4.7总结
- 5.动态内存经典笔试题分析
- 5.1题目1:
- 5.2题目2:
- 5.3题目3:
- 5.4题目4:
- 6.柔性数组
- 6.1什么是柔性数组
- 6.2柔性数组的特点
- 6.3柔性数组的使用
- 6.4柔性数组的优势
- 7.总结C/C++程序中内存区域的划分
- 8.结构体补充
1.为什么要有动态内存分配
我们之前开辟内存时,使用的方法为:
int main()
{
int a = 10;
char arr[20];
return 0;
}
但是这种内存开辟模式存在着一些问题:
1.它所开辟的空间是固定的
2.数组开辟空间时,空间是确定的,无法在程序运行时改变数组开辟的空间
3.如果数组开辟的空间远大于所需要使用的空间,会造成严重的内存浪费
4.如果数组开辟的空间小于所需要的空间,程序可能会崩溃
C语言引出了动态内存开辟,让程序员自己开辟内存,比较灵活
2.malloc和free
它们都被包含在头文件<stdlib.h>或<malloc.h>中
2.1malloc
2.1.1函数原型
该函数是用于开辟内存块的函数,函数原型如下:
void *malloc( size_t size );
1.函数参数size:所要开辟的内存空间的字节数
2.函数返回值:是一个void*类型的指针①如果内存开辟失败,会返回NULL指针
②如果内存开辟成功,则返回一个指向开辟好的内存块的指针
③返回值的类型为void类型,若要返回非void类型的指针,则要对返回值进行强制类型转换
④如果size为0,malloc的行为是未定义的,取决于编译器
2.1.2函数使用
//malloc函数使用
#include <stdlib.h> //头文件的引用
int main()
{
//1.开辟空间
int *pa = (int *)malloc(10 * sizeof(int));
//开辟一个10个字节的空间,返回开头空间的指针,将他存储到pa中
//2.空间的使用
//2.1在使用之前首先要判断pa是否为空指针
if (pa == NULL)
{
perror("malloc");//如果为空指针,将错误写出
return 1;
}
else
{
//先赋值
for (int i = 0; i < 10; i++)
*(pa + i) = i + 1;
//后打印
for (int i = 0; i < 10; i++)
printf("%d ", pa[i]);//pa[i]和*(pa+i)等价
}
//3.空间使用完一定要释放空间
free(pa);
//4.并且要将pa置为空指针,否则pa会变成野指针
pa = NULL;
return 0;
}
需要注意的是,malloc函数在使用时开辟的空间是连续的
2.2free
我们看完了malloc函数,紧接着先说明free的使用,最后再讨论这两个函数在使用时的注意事项
2.2.1函数原型
该函数是释放动态内存开辟空间的函数
void free (void* ptr);
函数参数:指向动态内存分配的内存块的地址
函数返回值:函数没有返回值
2.2.2函数使用
在上述代码中已经写入了该函数的使用,所以这里不再进行阐述,不信你看:
2.2.3函数使用注意事项
2.2.3.1注意点1
如果ptr(函数参数)指向的内存空间不是通过动态内存开辟的,那么free函数的使用是未定义的,比如:
int main()
{
int a = 0;
int* pa = &a;//指向不是通过动态内存开辟的空间
free(pa);
pa = NULL;
return 0;
}
程序会崩溃
2.2.3.2注意点2
释放已释放的内存
int main()
{
int* pa = (int*)malloc(10 * sizeof(int));
if (pa == NULL)
{
perror("malloc");
return 1;
}
free(pa);
free(pa);//两次释放空间
return 0;
}
程序依然会崩溃,改进的方法如下·:
int main()
{
int* pa = (int*)malloc(10 * sizeof(int));
if (pa == NULL)
{
perror("malloc");
return 1;
}
free(pa);
pa = NULL;//改进点
free(pa);
return 0;
}
为什么会这样,我们来看注意点3
2.2.3.3注意点3
NULL指针的释放
如果ptr(函数参数)是NULL,那么free函数什么也不做
2.2.3.4注意点4
对动态开辟的空间进行释放,函数参数必须指向所开辟空间的首地址
int main()
{
int* pa = (int*)malloc(10 * sizeof(int));
if (pa == NULL)
{
perror("malloc");
return 1;
}
else
{
pa++;//如果我们在使用开辟的空间时,对开辟空间的位置进行了修改,那么下面在释放的时候程序会崩溃
}
free(pa);
pa = NULL;
return 0;
}
2.3malloc和free使用注意事项
2.3.1内存覆盖
内存覆盖是指计算机程序中,一块内存空间的数据被意外地写入了另一块内存区域,导致原始数据丢失
申请了内存空间之后,必须检查是否分配成功,也就是这一步:
if (pa == NULL)
{
perror("malloc");//如果为空指针,将错误写出
return 1;
}
如果不对pa进行检查,在后续使用pa时可能发生内存覆盖的问题,对NULL指针进行解引用是一个未定义的行为,对NULL指针进行修改可能导致程序崩溃
而且,在释放内存后,不要使用指向该内存的指针,因为这块内存已经不属于你,最好的方法是传入NULL指针
2.3.2内存泄漏
内存泄漏是指在计算机程序中,动态分配的内存在不使用后没有被正常释放,导致这部分内存无法被程序的其他部分使用或无法被操作系统回收的现象
通俗来说,内存泄漏来源于它所描述的现象:程序在动态分配空间后,由于某些原因,失去了对这块空间的控制,导致无法释放它,可类比于现实生活中的液体泄漏
如果开辟内存后不进行释放,就是内存泄漏
3.calloc和realloc
3.1calloc函数
3.1.1函数原型
void* calloc (size_t num, size_t size);
该函数是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0
函数参数:
1.num:指对象的数量
2.size:指每个对象的大小
函数返回值:
1.成功时,返回指向分配空间的指针
2.失败时,返回NULL
3.1.2函数使用
因为这个函数对申请的空间实现了初始化为0,所以如果我们对申请的空间要求初始化,可以使用这个函数,函数使用方法如下:
//calloc函数使用
#include <stdlib.h>
int main()
{
int* pa = calloc(10, sizeof(int));//创建一个10个整形大小的空间
//也要检查空指针
if (pa == NULL)
{
perror("calloc");
return 1;
}
else
{
for (int i = 0; i < 10; i++)
printf("%d ", pa[i]);//会打印10个0,因为有初始化
}
//释放空间
free(pa);
pa = NULL;
return 0;
}
3.2realloc函数
3.2.1函数原型
realloc是用于重新分配内存大小的函数
void* realloc (void* ptr, size_t size);
函数参数:
ptr:要调整的地址
size:调整之后的大小
函数返回值:函数返回调整之后内存的起始位置
3.2.2函数使用
函数在开辟空间时,无论是扩大或缩小,原来空间的内容是不变的,对于缩小,缩小的那一部分内容会被释放,但是缩减操作可能不会发生,这取决于编译器,对于扩张,存在着两种情况:
1.原有空间内存后有足够的空间
这样的话直接进行开辟就好了
2.原有空间后不足以进行内存开辟
这样的话会重新开辟一个所需大小的内存空间,并将原内存中的数据复制到现空间中,然后释放原内存空间
所以我们使用时需要注意!
int main()
{
int* pa = calloc(10, sizeof(int));//创建一个10个整形大小的空间
//也要检查空指针
if (pa == NULL)
{
perror("calloc");
return 1;
}
else
{
int *ppa = (int*)realloc(pa, 20);
if (ppa == NULL)//仍然需要进行判断
return;
else
{
pa = ppa;//不为空指针的话,可以将原指针指向开辟指针,这样就可以使用原指针变量了
ppa = NULL;
}
}
//释放空间
free(pa);
pa = NULL;
return 0;
}
4.常见的动态内存的错误
下面就观察常见的一些错误,可能在上边已经叙述过,但还是简单看一下,查漏补缺
4.1对NULL指针的解引用操作
int main()
{
int* p = (int*)malloc(INT_MAX / 4);//当开辟空间错误时,就会返回空指针
*p = 20;
//此时对空指针进行了解引用,最有可能会发生非法访问,导致程序崩溃,原因具体取决于编译器
//改进方法
if (p == NULL)
{
perror("malloc");
return 1;
}
free(p);
p = NULL;
return 0;
}
4.2对动态开辟空间的越界访问
越界访问会造成的问题很多:
1.非法访问,程序崩溃
2.数据损坏:可能覆盖原有内存
3.影响系统稳定性、安全性等,使程序变得不可预测
4.3对非动态开辟的空间使用free释放
这种行为是未定义的,会导致程序崩溃
4.4使用free释放开辟空间的一部分
之前已经谈到过,使用开辟空间指针时,如果对指针的位置进行了更改,就容易发生这种问题,这种情况是未定义的,也会导致程序崩溃
4.5对同一块动态空间多次释放
这种行为是未定义的,被称为双重释放,可能发生违法访问问题,会导致程序崩溃
4.6动态开辟空间忘记释放
会导致内存泄露的问题,这种行为是危险的,比如,对于一台不间断工作的机器,不断存在着内存泄漏的问题,内存一点点地被消耗,会导致程序不间断停止
4.7总结
对于动态开辟的内存:
1.指针必须要检查是否为空
2.要有一个指针指向内存起始地址,保证准确释放
3.开辟空间必须要释放,指针必须要置为空
5.动态内存经典笔试题分析
5.1题目1:
//Test会发生什么结果
void GetMemory(char* p)
{
p = (char*)malloc(100);
//传入的是str的地址,p指向str的地址
//所以对p进行动态内存开辟并不会反应到str上
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
//str为空指针,对空指针进行了非法访问
printf(str);
//在最后没有对开辟空间进行释放
}
int main()
{
Test();
return 0;
}
改进:
//Test会发生什么结果
void GetMemory(char** p)
{
*p = (char*)malloc(100);
//二级指针就可以实现str的内存开辟了
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);//传入的是一级指针的地址
//对空指针进行判断
if (str == NULL)
perror("malloc");
else
{
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
}
int main()
{
Test();
return 0;
}
5.2题目2:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
//返回了数组p的首元素地址,但是数组p的生命周期只在函数中,离开函数会被销毁
//所以p指向的那块地址是不能被访问的
printf(str);
}
int main()
{
Test();
return 0;
}
5.3题目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);
//上面的操作其实都对,美中不足的是没有检查空指针,没有释放开辟空间并将指针置为空
/*free(str);
str = NULL;*/
}
int main()
{
Test();
return 0;
}
5.4题目4:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
//内存空间的提前释放,使得开辟的空间不属于你,后续无法再使用了
//最好的方式是将指针置为NULL,这样就可以通过判断得出指针是否被提前释放了
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
6.柔性数组
C99中,结构体的最后一个元素允许是未知大小的数组,这就叫柔性数组成员
6.1什么是柔性数组
我们来看一下柔性数组的创建,就知道了什么是柔性数组
struct Book
{
int a;
char b;
char arr[];//结构体前仍然是正常的结构体成员,最后的数组可以不指定数组大小
//也可以写成char arr[0]
};
6.2柔性数组的特点
1.柔性数组成员前必须存在至少一个成员
2.sizeof结构体时不会包含柔性数组成员
3.包含柔性数组成员的结构用malloc进行动态内存分配,而且分配的大小必须大于结构体的大小,用于柔性数组的使用
比如:
struct Book
{
int a;
char b;
char arr[];//结构体前仍然是正常的结构体成员,最后的数组可以不指定数组大小
};
int main()
{
printf("%zd", sizeof(struct Book));
//结果为8,因为没有计算arr的大小
return 0;
}
6.3柔性数组的使用
struct Book
{
int a;
char b;
char arr[];
};
int main()
{
struct Book* pb = (struct Book*)malloc(sizeof(struct Book) + 10 * sizeof(int));
//使用malloc进行动态内存开辟时,包含两个空间:
//1.sizeof(struct Book),即结构体本身的大小,也就是不算上数组的大小
//2.10*sizeof(int),它是数组的大小
if (pb == NULL)
{
perror("malloc");
return 1;
}
else
{
for (int i = 0; i < 10; i++)
{
pb->arr[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", pb->arr[i]);
}
}
free(pb);
pb = NULL;
return 0;
}
6.4柔性数组的优势
那为什么要整出一个柔性数组呢,我们可以通过下面的代码实现结构体中数组成员的使用:
struct Book
{
int a;
char b;
int* pb;
};
int main()
{
struct Book* ppb = (struct Book*)malloc(sizeof(struct Book));
ppb->a = 4;
ppb->b = 'a';
ppb->pb = (int*)malloc(10 * sizeof(int));
if (ppb->pb == NULL)
{
perror("malloc");
return 1;
}
else
{
for (int i = 0; i < 10; i++)
{
ppb->pb[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", ppb->pb[i]);
}
}
//此时要释放两次
free(ppb->pb);
ppb->pb = NULL;
free(ppb);
ppb = NULL;
return 0;
}
这样也可以实现数组的创建,但是,它使用起来并不像柔性数组那样简便,而且,需要注意的是,使用柔性数组所创建的结构体分配的是一块连续的内存!,这样,它既有利于内存释放,也有利于访问速度
7.总结C/C++程序中内存区域的划分
这次只是简单地总结了一下,后续学习过程中可能还会有些许差别,但是现在理解这个已经足够了
8.结构体补充
这部分内容全都来源于一篇大佬的文章,链接如下:
链接: link
不幸的是,大佬已经离世,我在此缅怀大佬
我在此只写一下我自己想要列举出来的内容:
struct str {
int len;
char a;
char b;
char s[0];
};
struct foo {
struct str* a;
};
int main(int argc, char** argv) {
struct foo f = { 0 };
if (f.a->s) {
//访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实得到的是相对地址里的内容
//而访问结构体成员其实就是加成员的偏移量
printf("%x\n", &f.a->len);//表示&str+0x0
//结果为0
printf("%x\n", &f.a->b);//表示&str+0x5
//结果为5
printf("%x\n", f.a->s);//结果为6
}
return 0;
}