引言
在编程中,动态内存管理是一个至关重要的概念,它对于开发高效、灵活且可扩展的程序至关重要。接下来我们就来学习一下c语言——动态内存管理的知识。
为什么需要动态内存分配
我们现在已经掌握了两种内存开辟的方法:
#include<stdio.h>
int main()
{
int val = 20; //在栈空间上开辟四个字节
char arr[10] = { 0 }; //在栈空间上开辟10个字节的连续空间
return 0;
}
上述的开辟内存的方法为静态开辟内存。
这种方法存在两个缺点:
(1)空间开辟的大小是固定的。
(2)数组在声明的时候,必须要指定数组的长度,数组的长度一旦确定就不能修改。
有时候我们可能会遇到:需要的空间大小不能确定的情况,此时使用静态开辟内存就无法解决问题。
为了解决这类问题,c语言引进了动态内存开辟,也就是我们今天要重点学习的内容。
动态内存
与动态内存相关的函数声明均在<stdlib.h>中。
1.perror函数
在学习动态内存开辟函数和释放函数之前,我们可以先学习一下perror函数。
perror是c语言标准库中的一个函数,用于错误处理和调试。
2.动态内存开辟函数
2.1 malloc
(1)malloc的用法
malloc函数可以向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个null指针,因此malloc的返回值一定要做检查。
(2)malloc的使用
我们可以将malloc和perror结合使用:
#include <stdio.h>
#include<stdlib.h>
int main()
{
int* arr = (int*)malloc(sizeof(int) * 5);
//开辟5个大小为整型的空间
//返回类型强制转换为(int*)
if (arr == null)
{
perror("malloc fail:");//如果开辟失败则打印错误信息
return 1;
}
//存储数据
for (int i = 0; i < 5; i++)
{
arr[i] = i;
}
//打印数据
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
输出结果为:
2.2 calloc
(1)calloc的用法
calloc函数同样可以向内存申请一块连续可用的空间,并返回指向这块空间的指针。
但与malloc函数不同的是:calloc会将申请的空间中的每个字节都初始化为0
由此我们得知:calloc不仅分配内存,还负责初始化内存,这是它与malloc的一个重要区别。
(2)calloc的使用
#include <stdio.h>
#include<stdlib.h>
int main()
{
int* arr = (int*)calloc(5, sizeof(int));
//开辟5个大小为整型的空间
//返回类型强制转换为(int*)
if (arr == null)
{
perror("calloc fail:");//打印错误信息
return 1;
}
return 0;
}
我们通过监视窗口来观察一下:
2.3 realloc
(1)realloc的用法
realloc函数用于重新分配之前已经分配的内存块的大小。
realloc在调整内存空间之后存在两种情况:
情况1:本地扩容
原有空间之后有足够大的空间,直接在原有内存之后直接追加空间,原有的数据不发生变化。
情况2:异地扩容
原有空间之后没有足够大的空间时,在堆空间上另外找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。
(2)realloc的使用
由于在扩展空间时会出现两种情况,因此我们使用realloc的时候就需要注意,避免出错。
int main()
{
int* arr = (int*)malloc(sizeof(int) * 10);
//开辟十个大小为整型的空间
//返回类型强转为int*
if (arr == null)//如果开辟失败
{
perror("malloc fail: ");//打印错误信息
return 1;//直接返回
}
//继续新增空间
int* tmp = (int*)realloc(arr, sizeof(int) * 15);
//不用arr是为了防止开辟失败,被至为null
if (tmp == null)//如果开辟失败
{
perror("realloc fail: ");//打印错误信息
return 1;//直接返回
}
arr = tmp;
return 0;
}
如果新增内存较小时一般是在原有基础上新增空间。两者地址相同。
如果我们把开辟的空间比较大:
int* tmp = (int*)realloc(arr, sizeof(int) * 150);
新增内存较大时则会重新开辟一段空间,将原来的空间释放。两者地址不同。
3.动态内存释放函数
1.free函数
(1)free的用法
c语言提供了另外一个函数free,专门用于做动态内存的释放的回收。
(2)free的使用
代码如下:
int main()
{
int* arr = (int*)malloc(sizeof(int) * 10);
//开辟十个大小为整型的空间
//返回类型强转为int*
if (arr == null)//如果开辟失败
{
perror("malloc fail: ");//打印错误信息
return 1;//直接返回
}
//继续新增空间
int* tmp = (int*)realloc(arr, sizeof(int) * 150);
//不用arr是为了防止开辟失败,被至为null
if (tmp == null)//如果开辟失败
{
perror("realloc fail: ");//打印错误信息
return 1;//直接返回
}
arr = tmp;
free(arr);
arr = null;
return 0;
}
常见的内存分布
c语言中的内存分布主要可以分为以下几个区域,这些区域由操作系统进行管理,用于存储不同类型的数据和代码。
常见的动态内存的错误
1.对null指针的解引用操作
void test()
{
int* p = (int*)malloc(int_max / 4);
*p = 20; //如果p的值是null,就会有问题
free(p);
}
1.int_max是一个宏定义,他表示整型的最大值,值为2147483647。
2.malloc申请的空间太大导致内存开辟失败,失败返回null
3.系统无法访问到null指针指向的地址,这时编译器就会报警告
我们要对代码进行修改:
void test()
{
int* p = (int*)malloc(int_max / 4);
if (null == p)
{
perror("malloc fail: ");//打印错误信息
return 1;
}
*p = 20;
free(p);
p = null;
}
这就体现出判断空指针的重要性了
2.对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (null == p)
{
perror("malloc fail: ");//打印错误信息
return 1;//直接返回
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i; //当i是10的时候越界访问
}
free(p);
p=null;
}
malloc开辟的空间只有10个,但是for循环循环了十一次,这时就会越界访问。
修改方法:
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (null == p)
{
perror("malloc fail: ");//打印错误信息
return 1;//直接返回
}
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = null;
}
3.对非动态开辟内存使用free释放
void test()
{
int a = 10;
int* p = &a;
free(p);
p=null;
}
1.free函数只能释放有动态内存开辟在堆上的空间。
2.p开辟的空间在静态区,free函数无法释放。
修改方法:
void test()
{
int a = 10;
int* p = &a;
}
动态内存开辟的空间不需要释放
4.使用free函数释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p); //p不再指向动态内存的起始位置
p = null;
}
1.p++跳过了一个整型大小的空间。
2.free函数释放p只会释放当前位置开始之后的空间,这导致有一个整型大小的空间未被释放,导致内存泄漏。
修改方法:
void test()
{
int* p = (int*)malloc(100);
free(p);
p = null;
}
我们要注意不能随意改变p指向的位置,开辟多少内存就释放多少
5.对同一块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p); //重复释放
}
1.p已经被释放归还给操作系统,但是此时p还指向该内存,是一个野指针。
2.再次释放p就会出现内存出错问题。
修改方法:
void test()
{
int* p = (int*)malloc(100);
free(p);
p = null;
}
6.动态开辟内存没有释放
void test()
{
int* p = (int*)malloc(100);
if (null != p)
{
*p = 20;
}//内存泄漏
}
忘记释放不再使用的动态开辟的空间会导致内存泄漏
修改方法:
void test()
{
int* p = (int*)malloc(100);
if (null != p)
{
*p = 20;
}
free(p);
p = null;
}
动态开辟的内存一定要释放,并且正确释放
动态内存经典笔试题分析
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);
}
我们来分析一下:
1.空间非法访问:
使用传值调用时,形参只是实参的临时拷贝,对形参的改变无法影响实参,这时str仍是空指针,而strcpy拷贝会对空指针进行解引用操作,对null指针解引用会出错。
2.空间内存泄漏:
在getmemory()函数内部动态申请了100字节的空间,因为p随着函数结束而被销毁,所以已经再也找不到该空间,会造成内存泄漏。
修改方法:
1.我们要使用传址调用,str本身是指针变量,传递指针变量的地址需要使用二级指针。
2.释放申请的空间
代码如下:
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;
}
2.题目二
char* getmemory(void)
{
char p[] = "hello world";
return p;
}
void test(void)
{
char* str = null;
str = getmemory();
printf("%s\n",str);
}
我们来分析一下:
等getmemory函数返回后,使用str访问p数组,这是非法访问:局部变量离开作用域就会返还给操作系统,此时str就会变成野指针。
修改方法:
我们可以把p的值放在静态区,可以使用static或者常量字符串。
代码如下:
const char* getmemory1(void)
{
const char* p = "hello world";
return p;
}
char* getmemory2(void)
{
static char p[] = "hello world";
return p;
}
void test(void)
{
const char* str = null;
str = getmemory1();
printf("%s\n", str);
str = getmemory2();
printf("%s\n", str);
}
3.题目三
void getmemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void test(void)
{
char* str = null;
getmemory(&str, 100);
strcpy(str, "hello");
printf("%s\n",str);
分析:
又是十分经典的问题:内存没有释放。
修改方法:
把开辟的空间及时释放。
代码如下:
void test(void)
{
char* str = null;
getmemory(&str, 100);
strcpy(str, "hello");
printf("%s\n",str);
free(str);
str = null;
}
4.题目四
void test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != null)
{
strcpy(str, "world");
printf("%s\n",str);
}
}
分析:
str所开辟的空间已经归还给了操作系统,这时再将world拷贝进str就会出错。
修改方法:
归还内存之后随手将其值为null指针。
代码如下:
void test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = null;
if (str != null)
{
strcpy(str, "world");
printf("%s\n",str);
}
}
柔性数组
1.什么是柔性数组
在c99中,结构体中的最后一个元素允许是位置大小的数组,这就是【柔性数组】成员
举个例子:
typedef struct st_type
{
int i;
int a[0]; //柔性数组成员
}type_a;
有些编译器会报错,可以改成:
typedef struct st_type
{
int i;
int a[]; //柔性数组成员
}type_a;
2.柔性数组的特点
例如:
typedef struct st_type
{
int i;
int a[0]; //柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));
return 0;
}
输出结果为:4
3.柔性数组的使用
// 代码1
typedef struct st_type
{
int i;
int a[];
} type_a;
int main()
{
int i = 0;
type_a* p = malloc(sizeof(type_a) + 100 * sizeof(int));
if (p == null)
{
return;
}
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
这样柔性数组成员a,相当于获得了100个整型元素的连续空间。
4.柔性数组的优势
我们来看这段代码:
// 代码2
typedef struct st_type
{
int i;
int* p_a;
} type_a;
int main()
{
type_a* p = (type_a*)malloc(sizeof(type_a));
if (p == null)
{
perror("malloc fail:");
return;
}
p->i = 100;
p->p_a = (int*)malloc(p->i * sizeof(int));
if (p->p_a == null)
{
perror("malloc fail:");
return;
}
for (int i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
free(p->p_a);
p->p_a = null;
p = null;
return 0;
}
代码1 和 代码2 都能实现同样的功能,但是 代码1 的实现有两个好处:
结束语
求点赞收藏加关注!!!十分感谢各位的支持!!!
发表评论