????????导言:对于内存开辟来说,我们可以通过创建数组来开辟一片连续的内存。但是我们发现这并不能很好的满足要求,比如说通讯录中你有100个联系人需要存储,但你固定的开辟1000大小的数组,那就会造成内存浪费,因此,为了能更好的利用内存,我们也应该学会动态内存的管理。
目录
1 动态内存分配 VS 静态内存开辟
2 动态内存函数的介绍
2.1 malloc 和 free
2.2 malloc 和 free 的使用
2.3 calloc 函数
2.4 realloc 函数
2.5 小结
3 常见的动态内存错误
3.1?对NULL指针的解引用操作
3.2 对动态开辟空间的越界访问
3.3?对非动态开辟内存使用free释放
3.4?使用free释放一块动态开辟内存的一部分or忘记释放
3.5?对同一块动态内存多次释放
4 几个经典的面试题
4.1 题目1
4.2 题目2
4.3 题目3
5 柔性数组
5.1 柔性数组的概念
5.2 柔性数组的特点
5.3 柔性数组的使用
5.4 柔性数组的优势
1 动态内存分配 VS 静态内存开辟
我们已经掌握的内存开辟方式有:申请栈区开辟
int a = 20;
//在栈空间上开辟四个字节
char arr[10] = {0};
//在栈空间上开辟10个字节的连续空间
对于上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定 的
2.数组在申明的时候,必须指定 数组的长度,它所需要的内存在编译时分配
综上:有时候可能需要在程序运行的时候才知道所需的空间,那这种空间的开辟方式就不能满足了
2 动态内存函数的介绍
所需要的头文件:#include<stdlib.h>
2.1 malloc 和 free
malloc 函数:
void* malloc (size_t size);
功能介绍:向内存的堆区申请开辟一块连续可用的空间。
函数的参数:?想要申请的空间大小【单位:字节】
函数的返回值:?返回的是指向这块空间起始地址的指针,指针类型为void*(泛型指针)
如果malloc?开辟空间成功,则返回一个指向开辟好空间起始地址的指针
如果malloc 开辟空间失败,则返回一个NULL指针
free 函数:
void free (void* ptr);
功能介绍:释放动态开辟的内存。
?有了以上的了解,我们可以得出以下结论:
????????我们知道,以前我们定义的局部变量、函数、数组所需要的空间都是在栈上开辟的,但切记malloc申请的内存是在堆上开辟的。栈上的可用资源远远小于堆上的,所以malloc的专长是用来提供大内存需求,而且方便管理。malloc 开辟成功时返回类型为void* ,是一个泛型指针,可根据需要强制转换成想要的类型。malloc 开辟失败时返回值为NULL ,所以我们需要对返回值做出检查,以防非法访问内存 的现象出现。
????????注意:如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
????????同理对于free函数,如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
int main()
{
int a = 10;
int* p = &a;
free(p); //不允许释放静态开辟的内存
p = NULL;
return 0;
}
????????如果参数 ptr 是NULL指针,则函数什么事都不做。若没有对开辟的空间进行及时的释放,则也会造成内存泄漏 的问题,那这块空间就会一直占用着内存,浪费空间。但值得注意的是在一段代码中,没有free函数并不意味着内存空间就不回收了。而是当程序退出的时候,系统会自动回收内存空间。因此我们也知道,堆区的空间不是无限制开辟的,是有限的。
????????关于栈区和堆区,详见下面内存布局的理解:
1. 栈区(stack):
- 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
- 栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 栈区主要存放运行函数而分配的
局部变量 、函数参数 、返回数据 、返回地址 等。
2. 堆区(heap):
- 一般由程序员分配释放; 若程序员不释放,程序结束时可能由OS回收 。
- 分配方式类似于链表
3. 数据段(静态区)(static):
4. 代码段:
?综上:?对于动态开辟的空间,使用完后记得及时释放。
2.2 malloc 和 free 的使用
示例如下:
int main()
{
int arr[10] = { 0 }; //静态开辟写法
int* p = (int*)malloc(40); //动态内存开辟写法
if (p == NULL) //判断p指针是否为空
{
printf("%s\n",strerror(errno));
return 1; //习惯返回值为1代表异常
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ",*(p + i));
}
free(p); //释放p所指向的动态内存
p = NULL; //是否有必要?
return 0;
}
????????由上图很容易知道,如果只是用了free函数,那么该指针所指向的空间被释放后,该指针仍然存在,很容易造成非法访问内存。我们应该主动 的将这个指针置为NULL ,以防止非法访问内存。
小技巧:调试窗口中想查看指针的成员,可以输入 p,10
2.3 calloc 函数
void* calloc (size_t num, size_t size);
功能介绍:与malloc()函数的区别只在于, calloc()函数会在返回地址之前将所申请的内存空间中的每个字节都初始化为0。
参数上,第一个参数是你想要开辟的元素的个数,第二个参数是每个元素对应的字节大小。?
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if (p == NULL)
return 1;
int i = 0;
for (i = 0; i < 10; i++)
printf("%d ", *(p + 1));
free(p);
p = NULL;
return 0;
}
2.4 realloc 函数
void* realloc (void* ptr, size_t size);
功能介绍:realloc 是动态内存管理的灵魂之处,因为它可以真正实现动态内存的实现。
参数介绍:ptr是要调整的内存地址,size为调整之后的新大小;返回值为调整之后的内存起始位置;这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间;
工作原理:
?
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
//1 2 3 4 5 6 7 8 9 10
int i = 0;
for (i = 0; i < 10; i++)
*(p + i) = i + 1;
//扩容
//p = realloc(ptr, 80); //第一种方法(不推荐)可能会申请空间失败造成内存泄漏,把原指针置空。
int* ptr = (int*)realloc(p, 80); //第二种方法扩展
if (p != NULL)
p = ptr;
//使用
for (i = 0; i < 10; i++)
printf("%d ", *(p + i));
free(p);
p = NULL;
return 0;
}
?拓展名词:内存池(略)
2.5 小结
1.malloc()和calloc()函数用法一样, 唯一的区别是calloc()会对所申请内存的每个字节初始化为0
2.malloc(), calloc(), realloc()申请的内存不再使用时 ,一定要用free()释放 ,否则会造成内存泄漏
3.p = realloc(ptr, size)函数返回值不为空时, 释放内存时不需写free(ptr) ,只需写free(p)
4.realloc(NULL,40); ?//等价于malloc(40);
3 常见的动态内存错误
3.1?对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4); //INT_MAX是整型最大值
*p = 20; //如果p的值是NULL,就会有问题
free(p); //应对动态内存开辟后的返回值作判断。
}
3.2 对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
3.3?对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p); //非动态开辟的空间不能使用free
}
3.4?使用free释放一块动态开辟内存的一部分or忘记释放
void test()
{
int *p = (int *)malloc(40);
if(NULL == p)
{
return 1;
}
for(i=0; i<10; i++)
{
*p = i;
p++;
}
free(p);//p不再指向动态内存的起始位置
}
3.5?对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
4 几个经典的面试题
4.1 题目1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
请问运行Test 函数会有什么样的结果?
?为避免这种错误,可以:
-
//在传参的时候传&str,然后形参部分用二级指针接收即可
void GetMemory(char **p)
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str); //str存放的是动态开辟的100字节的地址
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
-
//更改返回值
char* GetMemory()
{
char *p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
4.2 题目2
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
请问运行Test 函数会有什么样的结果?
????????以上程序有如下错误:
GetMemory 函数内部创建的数组是在栈区 上创建的出了函数,结束函数时,p 数组的空间就还给操作系统,返回的地址是没有意义的,str就是一个野指针。如果通过返回的地址去访问内存,就是非法访问内存。
4.3 题目3
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
请问运行Test 函数会有什么样的结果?
????????以上程序有如下错误:
????????解决方法:释放完内存后,对指针认为置为NULL ,这样就访问不到了
5 柔性数组
5.1 柔性数组的概念
C99 中,结构体中的最后一个元素 允许是未知大小 的数组,这就叫做柔性数组 成员。
可表示为:
struct S
{
int n;
//其余成员变量...
int arr[0]; //写法1:大小是未知的
int arr[]; //写法2:(推荐)
};
5.2 柔性数组的特点
1、柔性数组前面至少有一个成员变量 2、sizeof 返回的结构体的大小不包括柔性数组的大小 3、包含柔性数组成员的结构体使用 malloc 函数进行内存的动态分配,并且分配的内存应该大于 结构体的大小,以适应柔性数组的预期大小。
5.3 柔性数组的使用
struct S
{
int n;
int arr[];//柔性数组成员
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
printf("%s\n",strerror(errno));
return 1;
}
ps->n = 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]);
}
//增加柔性数组的空间
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+80);
if (ptr != NULL)
{
ps = ptr;
ptr = NULL;
}
//释放
free(ps);
ps = NULL;
return 0;
}
由上述代码可知:malloc 所开辟的空间是专门为柔性数组成员 申请的,其中:
????????1、malloc 里的+ 号前面的是给其余成员变量开辟空间的。
????????2、+ 号后面的是给柔性数组成员 开辟的空间【不用遵循内存对齐 ,所以可以直接按需开辟 】
5.4 柔性数组的优势
????????大家先观察以下两段代码:
//1、柔性数组成员
struct S
{
int n;
int arr[];//柔性数组成员
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
printf("%s\n",strerror(errno));
return 1;
}
ps->n = 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]);
}
//增加柔性数组的空间
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+80);
if (ptr != NULL)
{
ps = ptr;
ptr = NULL;
}
//释放
free(ps);
ps = NULL;
return 0;
}
//2、指针模拟柔性数组
struct S
{
int n;
int* arr;
};
int main()
{
struct S*ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
return 1;
}
ps->n = 100;
ps->arr = (int*)malloc(40);
if (ps->arr == NULL)
{
printf("%s\n",strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
//扩容
int*ptr = (int*)realloc(ps->arr, 80);
if (ptr == NULL)
{
return 1;
}
//使用
//释放
free(ps->arr); //先释放arr再释放ps,不然就会找不到ps的地址
free(ps);
ps = NULL;
return 0;
}
?
我们不难发现:?上述代码1 和代码2 可以完成同样的功能,但是方法1 的实现有两个好处 :
-
1、方便内存释放:
-
2、这样有利于访问速度:
- 连续的内存有益于提高访问速度,也有益于减少内存碎片
????????OK,以上就是本期知识点“动态内存管理”的知识啦~~,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟~
本期封面 ↓ : 夕阳无限好,只是近黄昏。
?
|