正文
c语言中的动态内存开辟的函数有三个 :malloc 、calloc 和realloc,有开辟便要有释放,一般在使用这三个函数,都会配套使用一个 free 来进行内存释放。好比一个弹簧当你给他一个压力,他便会望下收缩,一但松手便会有需要释放的力气回弹上来 。除了介绍这几个函数外,还会补充一个下在c99标准中的柔性数组,因为它也是用带动态内存管理。
一、malloc
1.声明
malloc,是我们要第一个要学的内存开辟函数,他的作用是向一个堆区申请一块目标大小的连续空间,如果申请成功,会返回这个块空间的首地址,失败则返回空指针(NULL)。当我们申请内存后,一般会对返回的指针进行判断,如果是空指针,就的结束程序(因为此时已经申请失败,再继续运行就会出现错误),虽然现在空间都比较大,几乎都不会出现申请失败的情况,但最好还是加个判断,确保这个malloc函数严谨性,这项判断操作时适应于所以动态内存申请函。
2.使用
可以看到 malloc 格式还是比较简单的,只需要传递大小,然后准备好指针接收返回值就行了,当然我们再使用时会在此基础上进行完善,比如对返回值进行强制类型转换,传递的字节数通过sizeof(类型)*数量得出,对返回指针进行判断等
插入一个知识点:这边这个perror也是一个函数,相比printf函数写的会更加简单便利,想要了解更多的话就是可以看看我之前博客,也有介绍哈!!!
// malloc 使用的方法
int main()
{
int * p = (int*)malloc(sizeof(int) * 5); //申请五个int的空间
if (p == NULL) //进行判断是否申请成功
{
perror("malloc"); //要是申请失败的话会报错相应的错误值
//printf("申请失败\n");
return 1; //结束程序
}
else
{
//当到这一步的时候,记表示申请成功
//使用 ...
//举例: 下面俩个for 循环时举例
for (int i = 0; i < 5; i++)
{
*(p + i) = i; //为了记录它*p的初始值,这样写可以不用在设变量了
}
for (int i = 0;i<5;i++)
{
printf("%d ", *(p +i)); //便是把每个值申请的值都打印出来
}
}
//紧接便就是释放了
free(p);
p = NULL; //释放后记得将它置空,避免成为野指针
return 0;
}
可能看到这里也会有些疑惑,怎么就讲了一点点,接下来会用画图的方式让大家更好的理解这个函数(free函数,下面也会有更详细的介绍)
3.注意
- malloc 申请后对其返回值进行强制类型的转换
- 申请空间的大小不用自己进行计算,通过sizeof配合目标数量就好了
- 使用前一定要进行判断,使用时也不能越界,使用后要释放空间。
- 开辟成功就返回指向开辟的空间的指针,失败就返回一个空指针
- 申请空间,不要申请0字节大小空间,malloc的行为是标准是未定义,取决于编译器
4.补充例子
当我们申请的内存来自于我们的电脑,如果将申请空间这个操作放在一个死循环中,电脑内存就会被申请满,从而导致电脑运行奔溃,出现死机或者蓝屏(行4环境会蓝屏,x86环境下有保护)
//补充示例
//注意:尝试前确保数据已保存好
int main()
{
//死循环,不断申请
while (1)
{
int* p = (int*)malloc(sizeof(int) * 4);
//申请完不释放
}
return 0;
}
造成这种现象有俩种原因: 1.因没有释放空间导致无线申请空间。 2.申请的空间没有释放或者释放不够彻底。在64环境下,就更激进了,你点了不及时,一会内存就爆满,直接黑屏或者蓝屏(本人已尝试过),如果想试试也是可以,就是前提要保存好数据,可能有这个例子黑过屏,可能也会更加累记这个知识点:重要的点:要合理,用完要释放,避免发生意外情况。
二、free
1.声明
free 就是我们使用未说明函数,它是用于释放已申请内存成功的工具,有申请就要有释放,所以free 一般都是用于动态内存申请函数搭配使用,也可根据实际情况进行释放或者修改,但也随意释放,会出现内存泄漏。free的形式其实也十分简单,只需要在其放入指向待释放空间的指针即可。
2.使用
free 用起来也是十分简单,就是对已申请的空间用完后进行释放,值得一提的是,空间释放后,还有将指向这个块空间的指针置空,否者会出现野指针
//free 使用方法
int main()
{
int* p = (int*)malloc(sizeof(int)); // 向堆区申请了一个字节
if (p == NULL) //进行判断是否未为空指针
{
perror("malloc"); // 具体的报错信息
return 1; // 申请失败,结束程序
}
char* ptr = "123"; //栈区开辟空间 然后在堆区释放空间
free(ptr); //这里便是非法释放,会报错
ptr = NULL;
free(p); //而这个是开辟空间使用完后,合理释放空间
p = NULL; //要置空,避免野指针的出现
return 0;
}
可以看到上述有个非栈区 申请的空间,不能释放还是比较好理解的
3.补充例子
这是我们也可以用之前malloc 无限申请空间的例子,说明free释放的空间的重要性。
//free 实际运行
int main()
{
// 死循环,不断申请
while (1)
{
int *p = (int*)malloc(sizeof(int) * 4);
free(p); // 申请完后释放
p = NULL; //相当于没申请
}
return 0;
}
当然这个代码也很好向我们证明了free释放空间的重要性以及价值,但free虽方便,但也是要注意事项的哈
4.注意
- free的对象必须是已申请的堆区空间
- free 用完之后要对目标指针手动置空
- 不能对同一个对象连续使用free
三.calloc
1.声明
calloc,跟malloc十分的相似,功能也差不多,都是向栈区申请一块目标空间,不过calloc有个小提升,就是 calloc 完后,它会主动把申请的空间初始化为0,这样就不至于申请空间中存放的都是随机数了。当然不用这个的话也可以将 malloc + memset 俩者也可以实现这一功能,但 calloc 一个函数就可以将二者都结合起来,使用更加方便
2.使用
calloc 无非就是在参数部分比 malloc 多了一个参数(其实也没有相差多,因为 calloc 中的是俩个参数,在 malloc 中被我们手动乘为一个参数了),俩者使用都大差不差,就是多了一个初始化为0,都是返回目标空间的首地址,都需要进行判断,保证不会得到一个空指针,当然肯定也是少不了释放
// calloc 使用方法
int main()
{
int *p = (int*)calloc(10, sizeof(int)); // 申请10个整形大小空间
if (p == NULL) //判断是否为空指针
return 1; //如果是空指针就结束程序
for (int i = 0; i < 10l; i++)
{
if (i < 5) //当<5的时候 正常初始化,后面都初始化为0
*(p + i) = i; // 测试是否有初始化为0
printf("%d ", *(p + i));
}
// 释放
free(p);
p = NULL; // 手动置空指针
return 0;
}
3.注意
- calloc 申请后要对其返回值进行强制类型转换
- 申请空间的大小不必自己进行计算,通过sizeof配合目标数量的就可以
- 使用前要进行判断,使用时不能越界,使用完要释放
- calloc 会将申请的空间初始化为0
- 申请要合理,不要无限申请,这样会造成内存爆满的后果
- 申请空间时,不要申请0字节大小空间,这是标准未定义的行为的,具体实现操作取决于编译器
四、realloc
1.声明
有时会我们发生申请的空间太小,有时候又申请多了空间内存,我们一定这会觉得,这咋该咋确定内存大小呢?这会我们就可以用上realloc函数,可以看在英语中 re 表示重复、再次的含义,因此realloc 作用就是对已开辟的空间进行扩容(就是再次申请的意思),可以推测出 realloc 需要俩个参数: 待扩容空间的地址、扩容后的大小的。如果说 realloc 的第一个参数传递为一个空指针,那么此时这个 realloc 就相当于 malloc ,仅仅是申请一块空间罢了。
realloc 在扩容时有俩种情况:1.后续空间足够大,且能够于已开辟好的空间(简称目标空间) 相连,直接开辟即可,2.后续空间不足,此时 realloc 会往后寻找一片足够大的空间,开辟好后会将目标空间中的元素搬过来,然后会对其旧的空间进行释放,这样就相当于增容了。当然realloc 也是需要判断、释放、置空
2.使用
// realloc 实际运用
//情况2,后续空间不够
int main()
{
int * p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
perror("malloc");
return 1;
}
// 空间向继续扩大到100
int* ptr = (int *)realloc(p, sizeof(int) * 100);
if (ptr == NULL)
{
perror("realloc");
return 1;
}
//释放
free(ptr);
ptr = NULL;
free(p);
p = NULL;
return 0;
}
3.注意
- 一样 realloc 申请后要对其返回值进行强制类型转换
- 申请空间戴奥不必要自己进行计算,通过 sizeof配合目标数量就好了,realloc 申请的空间大小至少要大于原空间大小,不然没有意义
- 老规矩使用前要判断,使用时不要越界,使用完后要释放
- 申请要合理 ,不要无限申请,造成不避要麻烦和后果
- realloc 要是参数1传递空指针时,可以把它看成 malloc
- 申请空间时候,不要申请0字节大小空间,这个是标准未定义的行为,具体实现操作还要取决与编译器
五.动态内存开辟笔试题
介绍完上面这几个函数,想必大家对动态内存开辟有着不一样的理解吧,接下来再引领的大家做几道比较经典的动态内存管理开辟的笔试题,来更加乘此理解对动态内存的管理, 题目都处于经典书籍《高质量c\c++编程》
第一题
错误例子
请问运行Test 函数会有什么样的结果?
//第一题
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
第一题中的主要错误是对就空指针的解引用引发的错误,出现空指针的原因:GetMemory函数的传值而不是传址(传值过去是在重新向申请空间),然后再进行动态内存开辟,无法对实参str造成影响,相当于此时的str仍然是空指针,对空指针解引用用时非法的。 当然此题还要其他错误,我这边指点出最重要的错误问题。
注意:
- 传值调用,即使成功开辟空间,也不会对实参str造成影响
- 没有对函数 GetMemory 中的开辟情况进行判断
- 对空指针str的解引用(strcpy 会对其进行解引用)
- 没有对开辟的内存空间进行释放(显然此时只能在GetMemory中释放)不然会出现内存泄漏
纠正方案:
//第一题: 纠正后代码
char* GetMemory(char** p)
{
*p = (char*)malloc(100);
if (p == NULL);
return 1;
}
void Test(void)
{
char* str = NULL; //先创建一个变量 赋值上空指针
GetMemory(&str);
strcpy(str, "hello world"); //从str拷贝过来
printf(str); //这样打印是合理的
}
int main()
{
Test();
}
第二题
错误例子
请问运行Test 函数会有什么样的结果?
//第二题
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
第二题中最主要的错误就是使用了已经回收的空间,也就是在 GetMemory 中的 p 作为数组首元素的地址,同时也是一个位于函数中的 局部变量,生命周期仅仅在函数的内部 ,当函数部分结束时,所指向的指针p指向的空间已经被回收。此时有打印指向p的空间地址,变无法打印出来,换句话来说。此时指向指针str的房客出租到期了,结果你又想进去住,而里面又住着新的出租房客的人,新房客的人好比就是随机的数嘛
纠正方案:
将数组放在存放在静态区也就是把它从局部变量改为全局变量,这样在函数的Test中也能使用了
至于为什么不直接在堆区申请,使用完后释放呢 ,原因也很简单,如果想要把数组存储在堆区上,需要挨个存入,之后才能正常释放,就拿字符串 "hello world" 的话,需要一个字符一个字符的储存,如果直接让指针p 指向字符串常量 "helloc world"的话,也能达到打印的效果。但就是释放不好释放,因为p 此时指向的时只读数据区(非堆区)
//第二题 纠正后
char* GetMemory(void)
{
static char p[] = "hello world"; //存放在静态区中
return p; //放回栈空间地址, 有接收就是野指针
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
第三题
错误例子
请问与运行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);
}
int main()
{
Test();
return 0;
}
第三题中看着感觉都挺对的该有的都有哈,但往往这样的错误例子更容易误导他人,其中最主要的错误就是没有对开辟的空间进行释放,这样会造成内存的泄漏;其次就是没有对开辟空间进行判断。短期使用确实没有啥问题,但当代码日日夜夜的不停运行,不断申请空间,不释放,直到可以运行内存爆满的时候,整个代码就奔溃了,可以看看上方 malloc 函数内存爆满的情况的占用域,长期以往内存就泄漏了,是要比较严重要紧的问题 。
纠正方案:
在申请空间进行判断,使用完内存后记得释放就行了。
//第三题 纠正方法
int GetMemory(char** p, int num)
{
*p = (char*)malloc(num); //加入判断
if (p == NULL)
{
perror("malloc");
return 1; //当为空指针时 就结束程序
}
return 0;
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello world ");
printf(str);
//使用完 进行释放空间
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
第四题
错误例子
请问运行Test 函数会有什么样的结果?
//第四题
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
// str 接下来就是野指针
if (str != NULL)
{
strcpy(str, "world"); //非法访问
printf(str);
}
}
int main()
{
Test();
return 0;
}
第四题主要的问题是将 str 释放后,仍然对其进行操作(也就对野指针进行操作),换句话来说就是释放太早了。这会导致难以预料的后果;其次就是没有对开辟的空间进行判断。当然free语句把ptr指向空间释放,其中的内容会变成随机值,实际上str != NULL 这条语言是不会起作用的,也就是说放太晚了,已经释放完才使,让我又想起来那句:脱裤子放屁,多此一举!!!
纠正方案:
将释放后置,并手动置空 把判断语句提前就可以啦
//第四题 纠正
char* Test(void)
{
char* str = (char*)malloc(100);
if (str == NULL)
return (char*)1;//申请失败
strcpy(str, "hello");
printf(str); //自己添加的打印 hello 哈 不然没有结果
free(str);//释放
str = NULL;//置空
//当进行置空后 后面就不再继续算了
// str 是野指针
if (str != NULL)
{
strcpy(str, "world"); //非法访问
printf(str);
}
return str;
}
int main()
{
Test();
return 0;
}
六.柔性数组
声明
柔性数组(flexible array) , 这是个出现在从99标准中的新特性,其表示形式为数组作为结构体中最后一个成员不进行计算,但用的前提数组前面至少要有一名其他成员。接下来这边时比较官方的回答
柔性数组是引入的一个新特性,它允许你在定义结构体时创建一个空数组,而这个数组的大小可以在程序运行的过程中更根据进行更改特别注意的一点时:这个空数组必须声明为结构体的最后一个成员,并且还要求主页的结构体至少包含一个其他类型的成员
可以看到,在计算包含柔性数组结构体大小的时,并未包含此数组大小,说明此结构体中的最后一个成员(柔性数组)的大小是可控的,而要让大小可控,也可以看到柔性数组也是内存对齐效果就需要用到我们前面介绍的动态内存卦象管理函数,这也正是柔性数组柔的原因。
使用
此时结构体中的柔性数组获得了20个整形大小的空间,可以随意使用,如果觉得不够用,还可以通过realloc 再次扩容
//柔性数组的使用
struct St
{
char c;
int n;
int arr[]; //柔性数组
};
int main()
{
//前面的字节给c、n,后面给了arr
struct St * ps = (struct St*)malloc(sizeof(struct St) + 10 * sizeof(int));
if (ps == NULL) //判断
{
perror("malloc");
return 1;
}
ps->c = "w";
ps ->n = 100;
//开始计算柔性数组的长度
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//数组空间不够,利用 realloc 继续扩张
struct St* ptr = (struct St*)realloc(ps, sizeof(struct St) + 15 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
ps = ptr;
//继续使用
for (int i = 10; i < 20; i++)
{
ps->arr[i] = i;
}
for (int i = 0; i < 20; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n%d\n", ps->c);
printf("%c\n", ps->n);
//释放
free(ptr);
ptr = ps = NULL; //释放要看清楚优先级哈 别乱来,避免出现野指针
return 0;
}
注意
- 柔性数组前至少要有一个其他成员
- 在sizeof计算结构体大小时,并不会包含柔性数组的大小
- 在对柔性数组进行空间分配时,一定要包含结构体本来的大小
- 柔性数组是c99中的新特征,部分编译器可能不支持
- 可以尝试模拟实现柔性数组
模拟实现柔性数组
既然我们有众多动态内存管理函数,是否能直接通过对一个指针指向空间的再次申请来模拟实现柔性数组呢?看着会比较多哈,大家耐心点慢慢琢磨!!!
//模拟实现柔性数组
struct St
{
char c;
int n;
int* arr; //设立一个指针
};
int main()
{
//现在堆区开辟空间 先算完 指针前俩个数
struct St* ps = (struct St*)malloc(sizeof(struct St));
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->c = 'w';
ps->n = 100;
//接下来算 int* 指针
ps->arr = (int*)malloc(10 * sizeof(int));
if (ps->arr == NULL)
{
perror("malloc-2");
return 1;
}
//使用
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//结果发现数组空间不够,利用realloc继续扩张
int* ptr = (int*)realloc(ps->arr, sizeof(int) * 20); //判断
if (ptr == NULL)
{
perror("realloc");
return 1;
}
//开始使用
for (int i = 10; i < 20; i++)
{
ps->arr[i] = i;
}
//打印
for (int i = 0; i < 20; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n%d\n", ps->n);
printf("%c\n", ps->c);
//释放
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
可以看到这个模拟实现柔性数组是相当的麻烦,光是动态内存申请和释放都需要操作俩次,而且还是需要隐藏问题(自己出错好几次)但庆幸的是我们只要会使用这个方法即可,可想而知发明这个的人的多头疼,而这些问题在柔性数组中都可以统统避免。
柔性数组的优势:
- 不易于产生内存碎片,有益于提高访问速度
- 方便内存的释放(只需要释放一次)
七.C/C++中程序内存区域划分
我们都知道,C++ 是C语言的超集(也就是进阶版),因此二者在内存区域划分基本一致。主函数、局部变量、返回地址等占用空间小的数组是存放在栈区上的;而占用空间大或者程序员指定存放的数组是存放在堆区上的;全局变量、静态数据等则是存放在静态区(数据段)中。
- 1.栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 2.堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(系统)回收。分配方式类似于链表。
- 3.数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 4.代码段:存放函数体(类成员函数和全局函数)的二进制代码。
总结
以上便是关于c语言中的动态内存管理的全部内容了,不知不觉从 malloc,到柔性数组结束,这样我们在以后编写程序的时候,就不用在无脑的吧数据都存放在栈区啦,也可以望堆区扔扔,毕竟位置也不差,空间也大;还可以通过函数灵活运行堆区空间,切记用完记得要还回去(释放),好比借的东西,不是自己的,终究是要归还的。总而言之,我们可以学完多去尝试尝试使用动态内存管理函数!
每一篇都在很用新的写,如果觉得不错的话,可以用你发财的小手点点赞!!!