👦个人主页:@Weraphael
✍🏻作者简介:目前是C语言学习者
✈️专栏:C语言航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注
目录
一、动态内存分配的由来
我们已经见过的内存开辟方式有:
int a = 3;//在内存空间开辟4个字节
char a[10] = {0};//在内存开辟10个字节且连续的空间
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的
- 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。因为有时候我们需要的空间大小在程序运行的时候才知道,那么以上的方式就不能满足了。而 动态内存分配恰好能在需要时随时开辟,不需要的时候随时释放,且分配的大小就是程序要求的大小。
二、动态内存函数的介绍
2.1 malloc函数
【文档描述】
解读:
malloc
函数会向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好的空间的指针如果开辟失败,则返回一个
NULL
指针(空指针),因此malloc
函数的返回值一定要做检查- 返回值是
void*
,表示的是无具体类型的指针,这说明malloc
函数并不知道开辟空间的类型,具体是在使用的时候让使用者来决定- 如果参数
size
为0,malloc
的行为是标准未定义的,是否报错取决于编译器
【malloc
函数实例】
strerror
讲解传送门 -> 点击此处
#include <stdio.h>
#include <stdlib.h> //malloc函数所需头文件
#include <errno.h> //errno所需头文件
#include <string.h>//strerror所需头文件
int main()
{
//假设向内存申请40个字节
//用来存放10个整型
int* p = (int*)malloc(40);
//判断返回值是否为空指针
if (p == NULL)
{
//说明开辟内存空间失败了,那就显示为什么失败了
printf("%s\n", strerror(errno));
return 1;
}
//否则返回值就不为空指针
//假设存放数据1~10
for (int i = 0; i < 10; i++)
{
*(p + i) = 1 + i;
}
//打印
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
【输出结果】
2.2 free函数
在
malloc
函数实例中,代码会存在 内存泄漏。malloc
函数既然在内存中申请了空间,用完了就应该还给操作系统。所以C语言提供了另外一个函数free
,专门用做动态内存的释放和回收的。
【文档描述】
解读:
free
函数是用来 释放内存块free
函数无返回值- 参数
ptr
是指向先前使用malloc
、calloc
或realloc
分配的内存块的指针。- 如果参数
ptr
指向的空间不是动态开辟的,那么free
函数的行为就是未定义的- 如果参数
ptr
是NULL
指针,则函数不会做任何事。- 注意:使用玩
free
函数后,还要将指针置为NULL
,防止使用者非法访问
【free
函数实例(完善malloc
函数实例)】
#include <stdio.h>
#include <stdlib.h> //malloc函数所需头文件
#include <errno.h> //errno所需头文件
#include <string.h>//strerror所需头文件
int main()
{
//假设向内存申请40个字节,用来存放整型
int* p = (int*)malloc(40);
//判断返回值是否为空指针
if (p == NULL)
{
//说明开辟内存空间失败了,那就显示为什么失败了
printf("%s\n", strerror(errno));
return 1;
}
//否则返回值就不为空指针
//假设存放数据1~10
for (int i = 0; i < 10; i++)
{
*(p + i) = 1 + i;
}
//打印
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//释放申请的内存
free(p);
p = NULL;
return 0;
}
释放完空间后,
p = NULL
的原因:
我们可以通过F10
调试来观察
释放前:我们将数据1~10
存放到内存了
free
释放后:我们发现,p
在释放后还指向释放前的空间
所以,为了防止使用者非法访问,我们就将p = NULL
2.3 calloc函数
C语言还提供一个函数
calloc
,calloc
函数也用来开辟内存空间的
【文档描述】
解读:
- 第一个参数
num
是开辟元素的个数- 第二个参数
size
是一个元素的大小- 要对
calloc
的返回值进行判断- 用完内存空间要及时释放
free
calloc
与malloc
区别在于:calloc
会在返回地址之前把申请的空间的每个字节初始化为0
【calloc函数实例】
calloc
会将申请的空间全部初始化为0
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
//向内存申请10个元素,每个元素都是int类型
int* p = (int*)calloc(10, sizeof(int));
//判断p是否为空指针
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//否则不是空指针
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//用完释放空间free
free(p);
p = NULL;
return 0;
}
【输出结果】
2.4 realloc函数
realloc
函数的出现让动态内存管理更加灵活- 有时候会发现过去申请的空间太小了,有时候又会觉得申请的空间过大,那为了合理的使用内存,就要对内存的大小做灵活的调整。使用
realloc
函数就的功能:调整动态开辟内存大小
【文档描述】
解读:
- 第一个参数
ptr
是要调整的内存地址- 第二个参数
size
为调整之后的新大小(要算上旧的大小)- 返回值为调整之后的内存的起始地址
- 如果第一个参数为
NULL
,那么可以说realloc
就是malloc
realloc
函数在调整内存空间的时候存在两种情况:
- 当原有空间之后有足够大的空间,要扩容内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化,返回的是原来的地址
- 当原有空间之后没有足够大的空间,扩展的方法:在堆空间上另找一个合适大小的连续空间。然后把旧的空间的数据拷贝到新的空间,并且会自动把旧的空间给释放掉,最后返回新空间的起始地址。
【realloc函数实例】
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
//假设向内存申请20个字节来存放5个整型
int* p = (int*)malloc(20);
//判断p是否为空指针
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//否则p就不是空指针
//接下来就将5个整型全部赋值成1
for (int i = 0; i < 5; i++)
{
*(p + i) = 1;
}
//那么接下来假设我还要再开辟5个空间(realloc)
int* ptr = (int*)realloc(p, 40);
if (ptr != NULL)//说明扩容成功
{
p = ptr;
}
//打印10个整型
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//释放空间
free(p);
p = NULL;
return 0;
}
【输出结果】
三、常见的动态内存写法错误
3.1 对NULL指针的解引用操作
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
*p = 20;
free(p);
p = NULL;
return 0;
}
未对
malloc
的返回值进行判断。若p = NULL
,对于空指针解引用可能会导致程序异常终止。
3.2 对动态开辟空间的越界访问
#include <stdio.h>
#include <stdlib.h>
int main()
{
//向内存申请10个整型的空间
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
for (int i = 0; i <= 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
当指针指向的范围超过数组的范围时,
p
就是野指针,而野指针是没有访问权限的地址,会造成非法访问。
3.3 对非动态开辟内存使用free释放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
这个涉及到了
free
函数的概念,free
的参数指向的空间一定是动态开辟的,也就是说,free
只能和malloc
、realloc
、calloc
搭配使用
3.4 使用free释放一块动态开辟内存的一部分
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if(p == NULL)
{
return 1;
}
p++;
free(p);
p = NULL;
return 0;
}
要知道不管是
前置++
还是后置++
,它都是会有副作用的。p++
后,p
就会指向p++
的位置,然后使用free
释放只会释放p + 1
后面的内存空间,只释放了一部分(蓝色部分)
3.5 对同一块动态内存多次释放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
free(p);
free(p);
return 0;
}
对第一次
free
回收了p
申请的内存空间,此时的p
已经是野指针,而 野指针是没有访问地址的权限,若再一次使用free
来回收内存空间,可能会造成非法访问
3.6 动态开辟内存忘记释放(内存泄漏)
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
*p = 20;
return 0;
}
使用完内存空间后,并没有释放内存空间,会导致 内存泄漏
四、内存泄漏的危害
-
资源浪费:内存泄漏会导致程序占用的内存越来越多,最终耗尽系统的可用内存资源。这将导致系统变得缓慢、不稳定甚至崩溃。
-
性能下降:内存泄漏会导致程序运行速度变慢,因为系统不得不频繁地进行内存交换或从硬盘上进行虚拟内存的读写操作。
-
系统崩溃:当内存泄漏严重时,系统可能会因为内存不足而崩溃。这将导致数据丢失、任务中断以及用户体验下降。
-
安全漏洞:内存泄漏可能会导致敏感信息泄露。例如,如果程序在内存中存储了用户的密码或其他敏感数据,并且没有正确释放这些内存,那么恶意攻击者可能通过访问这些未释放的内存来获取敏感信息。
五、笔试题
5.1 例1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void getmemory(char* str)
{
str = (char*)malloc(40);
}
int main()
{
char* p = NULL;
getmemory(p);
strcpy(p, "hello world");
printf("%s\n", p);
return 0;
}
【解释】
【正确代码】
如果想让p
指向str
所申请的空间的话,实参和形参需要建立联系,因此可以传p
的地址。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void getmemory(char** str)
{
*str = (char*)malloc(40);
}
int main()
{
char* p = NULL;
getmemory(&p);
strcpy(p, "hello world");
printf("%s\n", p);
// 释放内存空间
free(p);
p = NULL;
return 0;
}
5.2 例2
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* getmemory()
{
char str[] = "hello world";
return str;
}
int main()
{
char* p = NULL;
p = getmemory();
printf("%s\n", p);
return 0;
}
【解释】
【正确代码】
想要通过h
的地址打印字符串,则需要延长str数组的生命周期,因此可以用static来修饰或者用常量字符串,它们的生命周期是:当整个程序结束,变量就会被自动销毁。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* getmemory()
{
// static修饰变量
//static char str[] = "hello world";
//字符串常量
char* str = "hello world";
return str;
}
int main()
{
char* p = NULL;
p = getmemory();
printf("%s\n", p);
return 0;
}
5.3 例3
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void getmemory(char** str, int n)
{
*str = (char*)malloc(n);
}
int main()
{
char* p = NULL;
getmemory(&p, 100);
strcpy(p, "hello world");
printf("%s\n", p);
return 0;
}
【解释】
代码能正常打印"hello world"
,唯一的缺点就是内存泄漏了
【正确代码】
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void getmemory(char** str, int n)
{
*str = (char*)malloc(n);
}
int main()
{
char* p = NULL;
getmemory(&p, 100);
strcpy(p, "hello world");
printf("%s\n", p);
// 释放内存空间
free(p);
p = NULL;
return 0;
}
5.4 例4
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char* str = (char*)malloc(100);
if (str == NULL)
{
return 1;
}
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf("%s\n", str);
}
return 0;
}
【解释】
六、C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量都在栈区上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于链表。
- 数据段(静态区) (static) :存放全局变量、静态数据。程序结束后由系统释放
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
-
有了这幅图,我们就可以更好的理解在曾经讲过
static
关键字修饰局部变量的例子了 -> 点击跳转
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static
修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长 -
const
修饰的变量的生命周期与普通变量的生命周期基本相同。const
关键字用于声明一个不可修改的变量,即常量。常量在声明时必须进行初始化,并且不能再被赋予新的值。
在C/C++
中,const
修饰的变量可以分为两种情况:
- 在函数内部声明的局部常量:这些常量在函数调用时被创建,在函数退出时被销毁。它们的生命周期仅限于函数的执行期间。
- 在全局作用域或命名空间中声明的全局常量:这些常量在程序启动时被创建,在程序结束时被销毁。它们的生命周期与程序的生命周期相同。
需要注意的是,const
修饰的变量只能在声明时进行初始化,而不能在运行时修改其值,这使得cons变量更加安,可以防止意外修改变量值,同时,const
变量在编译过程中也可以进行一些优化,提高程序的效率。