总言
C语言:动态内存管理介绍。
文章目录
- 总言
- 1、为什么存在动态内存管理
- 2、动态内存函数介绍
- 2.1、malloc、free
- 2.1.1、malloc函数
- 2.1.2、free函数
- 2.2、calloc、realloc
- 2.2.1、calloc函数
- 2.2.2、realloc函数
- 3、常见的动态内存错误
- 3.1、对NULL指针的解引用操作
- 3.2、对动态开辟空间的越界访问
- 3.3、对非动态开辟内存使用free释放
- 3.4、使用free释放一块动态开辟内存的一部分
- 3.5、对同一块动态内存多次释放
- 3.6、动态开辟内存忘记释放(内存泄漏)
- 4、习题演练
- 4.1、题一
- 4.2、题二·返回栈空间地址、返回栈空间变量
- 4.3、题三
- 4.4、题四
- 5、柔性数组
- 5.1、什么是柔性数组
- 5.2、柔性数组的特点和使用
- 5.2.1、柔性数组的特点
- 5.2.2、柔性数组的使用
- 5.3、柔性数组的优势
- 5.3.1、一个对比
- 5.3.2、柔性数组的优势
1、为什么存在动态内存管理
1)、内存开辟方式:
下示为常见变量开辟方式:
int n = 2;//全局变量:在静态区开辟四个字节
int main()
{
int val = 20;//局部变量:在栈空间上开辟四个字节
char arr[10] = { 0 };//局部变量:在栈空间上开辟10个字节的连续空间
return 0;
}
对于上述变量,可以知道的是: 这些变量开辟出的空间大小是固定的。即使是数组,为了在编译时确定其所需要的内存,也会固定数组长度。
这样就存在一个问题,有时候我们需要的内存空间大小只有在程序运行的时候才能知道,直接固定内存大小,会遇到匹配不当的现象,比如内存空间过大浪费,或内存空间过小不够。
因此才有了即将了解的动态内存开辟。
2)、内存空间区域分配简单引入:
2、动态内存函数介绍
2.1、malloc、free
2.1.1、malloc函数
1)、malloc函数介绍:
相关函数:malloc
说明:
1、这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
2、如果开辟成功,则返回一个指向开辟好空间的指针。如果开辟失败,则返回一个NULL
指针,因此malloc
的返回值一定要做检查。
3、返回值的类型是 void*
,所以malloc
函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
4、如果参数 size
为0
,malloc
的行为是标准是未定义的,取决于编译器。
2)、使用演示:
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(40);//注意此处40的单位是字节,类型为int*,那么指针每次能访问4字节空间。
if (ptr == NULL)//因为有申请失败的可能性,因此需要做检查。
{
perror("malloc");
return 1;
}
//申请到的动态内存空间的相关使用举例:
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
return 0;
}
2.1.2、free函数
1)、free函数介绍:
相关函数链接:free
说明:
1、函数free
是用来释放和回收态内存的。
2、如果参数 ptr
指向的空间不是动态开辟的,那free
函数的行为是未定义的。
3、如果参数 ptr
是NULL
指针,则函数什么事都不做。
int* np = NULL;
free(np);//合法的,free参数可为空
4、当我们不释放动态申请的内存空间时,如果程序结束,动态申请的内存由操作系统自动回收,如果程序不结束,动态内存是不会自动回收的,就会形成内存泄漏的问题。
2)、使用演示:
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(40);//注意此处40的单位是字节,类型为int*,那么指针每次能访问4字节空间。
int* p = ptr;
if (ptr == NULL)//因为有申请失败的可能性,因此需要做检查。
{
perror("malloc");
return 1;
}
//申请到的动态内存空间的相关使用举例:
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
//释放空间
free(p);
p = NULL;
return 0;
}
注意事项:
1、动态申请空间后,不需要时要记得使用free函数释放;
2、free(p);
实际释放的是p指针指向的那块动态内存空间,即将内存使用权收回,但实际上p仍旧指向该内存空间,ptr虽然释放了对应内存空间,但它还能指向该地址空间,即p成为了野指针,因此需要为其赋值为空。
3)、动态内存申请失败举例:
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(INT_MAX);//申请整型最大值这么多的内存空间
int* p = ptr;
if (ptr == NULL)//检查。
{
perror("malloc");
return 1;
}
//申请到的动态内存空间的相关使用举例:
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
//释放空间
free(p);
p = NULL;
return 0;
}
2.2、calloc、realloc
2.2.1、calloc函数
1)、calloc函数介绍:
相关函数链接:calloc
说明:
1、colloc
函数的功能是为 num
个大小为 size
的元素开辟一块空间,并且把空间的每个字节初始化为0
。
2、与函数 malloc
的区别只在于 calloc
会在返回地址之前把申请的空间的每个字节初始化为全0
。
2)、使用演示:
int main()
{
//等价空间大小:
//int* ptr = (int*)malloc(40);
//int* ptr = (int*)malloc(sizeof(int)*10);
int* ptr = (int*)calloc(10, sizeof(int));//区别在于初始化为零
if (ptr == NULL)//检查
{
perror("malloc");
return 1;
}
//使用举例:
int* p = ptr;
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放空间
free(ptr);
ptr = NULL;
return 0;
}
2.2.2、realloc函数
相关函数链接:realloc
说明:
1、有时为了合理设计内存空间,我们会对内存的大小做灵活的调整(申请的空间太小、太大)。那么 realloc
函数就可以做到对动态开辟内存大小的调整。
2、realloc函数的两个参数,ptr
是要调整的内存地址,size
是调整之后新大小。
3、返回值为调整之后的内存起始位置。根据不同情况,该函数会在调整原内存空间大小的基础上,将原来内存中的数据移动到新的空间。
4、关于扩容时新空间选择说明:
情形一: 当内存空间足够,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情形二: 原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
2)、使用演示:
int main()
{
//预备处理:
int* ptr = (int*)calloc(10, sizeof(int));
if (ptr == NULL)//检查
{
perror("malloc");
return 1;
}
printf("%p: ", ptr);
//使用举例:
int* p = ptr;
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//重新扩容:
int* tmp = (int*)realloc(ptr, 20 * sizeof(int));
if (tmp != NULL)//检查:防止出现扩容失败,直接赋值的话会导致原先ptr改变指向引起内存泄漏
{
ptr = tmp;
}
printf("\n%p: ", tmp);
//使用举例:
p = ptr;
for (int i = 0; i < 20; i++)
{
printf("%d ", p[i]);
}
//释放空间
free(ptr);
ptr = NULL;
return 0;
}
3、常见的动态内存错误
3.1、对NULL指针的解引用操作
int* p = (int*)malloc(1000);
for (int i = 0; i < 250; i++)
{
p[i] = i;//error
printf("%d ", *(p+i));//error
}
存在问题:若动态申请空间失败,返回空指针NULL,*NULL
是非法的。
解决方案:对动态申请后的相关返回值进行判空。
int* p = (int*)malloc(1000);
if (p == NULL)
{
//……
return 1;
}
for (int i = 0; i < 250; i++)
{
p[i] = i;
printf("%d ", *(p+i));
}
3.2、对动态开辟空间的越界访问
int* p = (int*)malloc(1000);
if (p == NULL)
{
//……
return 1;
}
for (int i = 0; i <=250; i++)//error
{
p[i] = i;
printf("%d ", p[i]);
}
存在问题:i <=250
实际越界。
解决方案:对内存边界主动检查。
int* p = (int*)malloc(1000);
if (p == NULL)
{
//……
return 1;
}
for (int i = 0; i <250; i++)
{
p[i] = i;
printf("%d ", p[i]);
}
3.3、对非动态开辟内存使用free释放
int a = 10;
int* p = &a;
free(p);
p = NULL;
3.4、使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
//释放空间
free(p);//error
p = NULL;
return 0;
}
存在问题:
3.5、对同一块动态内存多次释放
int main()
{
int* p = malloc(100);
if (p == NULL)
return 1;
free(p);
//....
free(p);//error
p = NULL;
return 0;
}
3.6、动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = malloc(100);
//……
}
int main()
{
test();
//.....
while (1)
{
;
}
return 0;
}
存在问题:test函数结束,栈销毁,局部变量p被销毁,没有释放动态空间,会导致内存泄露。
解决方案:动态开辟的空间一定要释放,并且正确释放。
void test()
{
int* p = malloc(100);
//……
free(p);
p = NULL;
}
int main()
{
test();
//.....
while (1)
{
;
}
return 0;
}
4、习题演练
4.1、题一
问:请问运行Test
函数会有什么样的结果?
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
问题分析:
void GetMemory(char* p)//问题一
{
p = (char*)malloc(100);//问题二
}
void Test(void)
{
char* str = NULL;
GetMemory(str);//问题一
strcpy(str, "hello world");//问题三
printf(str);
}
1、GetMemory(str);
、char* p
尽管str是指针,但指针变量仍旧属于变量的范畴,在函数传参时,对该指针变量而言这种写法属于值传递,除非说传递它的地址,即二级指指针(&str),这样才是址传递。
2、p = (char*)malloc(100);
因值传递,临时变量p出了函数被销毁,在GetMemory中申请到的动态空间由于没有指向它的指针,最终无法找到,也无法释放,形成内存泄漏。
3、strcpy(str, "hello world");
因上述问题导致str指针未做任何改变,仍旧是空指针,对空指针的解引用会导致程序崩溃。
延伸:printf(str);
,这种写法正确吗?
int main()
{
char* p = "Why We Sleep\n";//该指针指向字符首元素地址
printf("Why We Sleep\n");
char arr[] = "Why We Sleep\n";//数组名表示数组首元素地址
printf(arr);
//printf打印字符串,只需要传递首元素地址
return 0;
}
解决方案:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
4.2、题二·返回栈空间地址、返回栈空间变量
1)、基本例题说明:
问:请问运行Test
函数会有什么样的结果?
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
问题分析:在GetMemory函数中创建数组并将其地址返回给str指针。由于GetMemory函数结束后,其内部局部变量销毁,对应内存空间又还给系统本身,str得到的是一个没有访问权限的地址,则属于野指针。对其打印会出现未定义行为。
2)、返回栈空间地址讲解:
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
printf("%d\n", *p);
return 0;
}
第一次printf能打印成功有时属于侥幸,对应的空间中数据还没有被清理/删除/覆盖,当打印第二次时就能发现问题,这样因为相同空间被其它函数栈帧使用,故其中数据也不复从前。
需要注意,即使第一次输出结果正确,但不代表它没问题,本质上这种写法就是非法的。举例子:偷窃,虽然结果是没被抓/发现,但这一行为本身不符立定的道德规范。
需要区分:返回栈空间的地址是有问题的,但返回栈空间的变量是可行的。
int* test()//error:返回栈空间地址
{
int a = 10;//创建一个int变量
return &a;//返回的是该变量的地址:传址返回
}
int test()//right:返回栈空间变量
{
int a = 10;
int*p=&a;
return *p;
}
int main()
{
int n=test();
return 0;
}
延伸问题:
如下:此提错误之处是访问野指针。非返回栈空间地址。
int* f2(void)
{
int* ptr;//创建一个指针变量
*ptr = 10;//error
return ptr;//返回的是该指针变量,传值返回
}
4.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);
}
问题分析:
1、该代码能正常打印值。
2、但没有释放动态申请的空间。
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;
}
4.4、题四
问:请问运行Test
函数会有什么样的结果?
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;
}
问题分析:
1、瑕疵:没有进行判空操作,当malloc申请失败时,第一次字符拷贝传参可能存在问题。
2、问题:free释放动态内存空间,str仍然指向对应地址,只是该地址被系统收回,后续使用时str为野指针,虽能访问该内存空间,但其操作为非法访问。
正确写法应在释放内存后令str为空指针。此处的一种错误改法是把free(str)放到if循环之后,这样虽然没有野指针的问题,但其逻辑不能自洽(即先用了strcpy拷贝,再用if语句判断:既然已经投入使用又何必多此一举添个判空,既然判空为什么不在刚完成空间申请后立马做判空检查)
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;//修改
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
5、柔性数组
5.1、什么是柔性数组
C99 中,结构体最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
以下为结构体中,柔性数组的两种写法:
struct S1
{
int num;
double d;
int arr[];//柔性数组成员
};
struct S2
{
int num;
double d;
int arr[0];//柔性数组成员
//此处的0不是指数组元素,而是特指柔性数组
};
5.2、柔性数组的特点和使用
5.2.1、柔性数组的特点
struct S1
{
int num;
int arr[0];//柔性数组成员
};
1、结构中的柔性数组成员前面必须至少一个其他成员。
2、sizeof 返回的这种结构大小不包括柔性数组的内存。
演示如下:
sizeof计算结构体,不包含柔性数组的大小,正因此,为了保障结构体大小存在,柔性数组不能单独存在结构体中。
3、包含柔性数组成员的结构用malloc ()
函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct S1* ps1 = (struct S1*)malloc(sizeof(struct S1));//error
struct S1* ps2 = (struct S1*)malloc(sizeof(struct S1) + 40);
这种写法错误是因为我们只考虑了num
的空间,正确写法是要加上柔性数组需要的空间大小。
5.2.2、柔性数组的使用
int main()
{
struct S1* ps = (struct S1*)malloc(sizeof(struct S1)+40);
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->num = 100;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;//柔性数组成员:赋值
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);//柔性数组成员:打印
}
printf("\n");
//扩容
struct S1* ptr = (struct S1*)realloc(ps, sizeof(struct S1)+80);
if (ptr == NULL)
{
perror("realloc\n");
return 1;
}
else
{
ps = ptr;
}
for (i = 10; i < 20; i++)//为后面扩容的值初始化
{
ps->arr[i] = i;
}
for (i = 0; i < 20; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
//释放
free(ps);
ps = NULL;
return 0;
}
5.3、柔性数组的优势
5.3.1、一个对比
以下为两种结构体数组的写法:
使用柔性数组:
struct S1
{
int num;
int arr[0];//柔性数组成员
};
int main()
{
struct S1* ps = (struct S1*)malloc(sizeof(struct S1)+40);
if (ps == NULL)
{
perror("malloc");
return 1;
}
//使用
//……
//释放
free(ps);
ps = NULL;
return 0;
}
使用指针:
struct S2
{
int num;
int* arr;//指针
};
int main()
{
struct S2* ps = (struct S2*)malloc(sizeof(struct S2));
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->arr = (int*)malloc(sizeof(int) * 10);
if (ps->arr == NULL)
{
perror("malloc");
free(ps);
ps = NULL;
return 1;
}
//使用
//……
//释放
free(ps->arr);
free(ps);
return 0;
}
可以看到,上述两种写法都能达到相同效果,那么我们为什么不直接使用指针,而要再学习一个柔性数组呢?
以下为二者的区别:
对第一种,数组是结构体中的一员,内存空间相衔接。
对第二种,可取代柔性数组得到相同结果,但其内存空间是单独开辟的,存在不衔接的情况。
5.3.2、柔性数组的优势
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。