内存的五大分区
1、堆区(heap)——由程序员分配和释放, 若程序员不释放,程序结束时一般由操作系统回收。注意它与数据结构中的堆是两回事
2、栈区(stack)——由编译器自动分配释放 ,存放函数的参数值,局部变量等。其操作方式类似于数据结构中的栈
3、静态全局区
1)未初始化静态全局区 —— 静态变量,全局变量,没有初始化的存在此区
2)初始化的静态全局区 —— 静态变量、全局变量,赋过初值的存放在此区
4、文字常量区——常量、字符串就是放在这里的。 程序结束后由系统释放
5、(程序)代码区——用于存放函数体的(二进制)代码
静态分配与动态分配
在数组一章中,介绍过数组的长度是预先定义好的,在整个程序中固定不变。但是在实际的编程中,往往会发生所需的内存空间取决于实际输入的数据,而无法预先确定 。为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。而动态分配内存就是在堆区分配空间。
静态分配
-
在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。如:int a[10]
-
必须事先知道所需空间的大小。
-
一般以数组的形式,分配在栈区或静态全局区。
-
按计划分配。
动态分配
-
在程序运行过程中,根据需要大小自由分配所需空间。
-
分配在堆区,一般使用特定的函数进行分配。
-
堆区开辟空间,手动申请手动释放,更加灵活。
-
按需分配。
动态分配内存函数
#include <stdlib.h>
malloc
void *malloc(unsigned int size);
malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小(size)的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针;
- 如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;
- 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(强制类型转换);
- 如果参数 size 为 0 ,malloc 的行为是标准是未定义的,取决于编译器。
- 如果多次malloc申请的内存,第1次和第2次申请的内存不一定是连续的。
//我们直接给出分配的大小是可以的
char* rec2=(char*) malloc(20);
//当然,我们一般会以如下这种形式给出分配的大小。因为不同的操作系统可能数据类型的大小不同,这样写更符合规范
//指针 = (指针类型*)malloc(数据数量 *sizeof(指针类型))
char* rec1=(char*) malloc(20*sizeof(char));
free
void free(void *ptr)
我们不能为所欲为的开辟空间,因为空间是有限的,所以应当有借有还。C语言为我们提供了free函数,专门用来做动态内存的释放和回收。
-
ptr:开辟后使用完毕的堆区的空间的首地址
-
如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;
-
如果参数 ptr 是NULL指针,则函数什么操作都不进行。
-
free函数只能释放堆区的空间,其他区域的空间无法使用free
-
free释放空间必须释放malloc或者calloc或者realloc的返回值对应的空间,不能说只释放一部分。
-
free§; 注意当free后,因为没有给p赋值,所以p还是指向原先动态申请的内存。但是内存已经不能再用了,p变成野指针了,所以一般为了防止野指针,会free完毕之后对p赋为NULL
-
一块动态申请的内存只能free一次,不能多次free
实例1:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
// 向内存申请10个整型的空间
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
// 打印错误原因
printf("%s\n", strerror(errno));
}
else
{
// 正常使用空间
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置为NULL,防止野指针
p = NULL;
return 0;
}
实例2:倒序输出一个字符串
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* src="hello,world";
char* dest=NULL;
int len=strlen(src);
dest=(char*)malloc(len+1);// 要为\0分配空间
char* d=dest;
char* s=src+len-1;// 指向最后一个字符
while(len--!=0){
*(d++)=*(s--);// 注意不要丢掉*号
*d ='\0';// 字符串的结尾不要忘记'\0'
}
printf("%s",dest);
free(dest);// 使用完要释放空间,避免内存泄露
dest = NULL; // 释放不等于安全,将其置为空指针的操作不可省略
return 0;
}
calloc
void * calloc(size_t nmemb,size_t size);
- size_t :无符号整型,它是在头文件中,是用typedef定义出来的
- nmemb:要申请的空间的块数
- size:每块的字节数
(1)函数的功能是为 nmemb 个大小为 size 的元素开辟一块空间,并且把这块空间的每个字节初始化为0;
(2)与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0.
realloc
void* realloc(void *s,unsigned int newsize);
如果我们在使用内存的过程中需要对内存的大小进行调整怎么办呢?C语言同样为我们提供了一个函数叫 realloc
- s:原本开辟好的空间的首地址
- newsize:重新开辟的空间的大小
- 返回值:新的空间的首地址
在原本申请好的堆区空间的基础上重新申请内存,新的空间大小为函数的第二个参数
如果原本申请好的空间的后面不足以增加指定的大小,系统会重新找一个足够大的位置开辟指定的空间,然后将原本空间中的数据拷贝过来,然后释放原本的空间
如果newsize比原先的内存小,则会释放原先内存的后面的存储空间,只留前面的newsize个字节
char* p1=(char*)malloc(80*sizeof(char));//申请80个字节的内存
p1=(char*)realloc(p1,100);//将内存重新开辟为100个字节,可以认为是增加了20个字节
p1=(char*)realloc(p1,50);//将内存重新开辟为50个字节,可以认为是减少了30个字节
常见的动态内存易错警示
1、不能对NULL指针的解引用操作
因为NULL是一个特殊的指针值,表示指针没有指向任何有效的对象或地址。对NULL指针解引用会导致程序崩溃或未定义的行为,因为程序在试图访问一个不存在的内存地址。
因此,在使用指针之前,应检查其是否为NULL,并确保指向有效的内存地址。
2、不能对动态开辟空间的越界访问
对动态内存的越界访问可能会导致程序崩溃或产生未定义的行为。
这是因为动态内存分配需要在运行时进行,并且程序员需要手动管理内存的分配和释放。如果程序员在访问动态内存时越界,就会导致访问到未分配的内存或者已经释放的内存,从而可能导致程序崩溃或出现未定义的行为。
此外,动态内存的越界访问还可能会导致数据损坏、安全漏洞等问题。因此,程序员需要注意动态内存的边界,并且避免越界访问。
3、不能对非动态内存使用free
因为非动态开辟的内存是在程序运行时从栈上分配的,而不是从堆上分配的。栈上分配的内存是由系统自动管理的,程序员无法控制其释放。因此,如果试图使用free函数来释放栈上的内存,会导致程序崩溃或不可预测的行为。所以只有动态开辟的内存才能使用free函数进行释放。
4、不能对同一块动态内存free多次
对同一块动态内存多次释放会导致程序崩溃或出现未定义的行为。因为在第一次释放后,操作系统会将该内存块标记为可用,此时这块内存空间就可以被其他变量所占用。所以再次释放时该内存块由于已经被标记为可用,所以释放操作将无法成功,从而导致程序出现异常。
此外,多次释放同一块内存还会导致内存泄漏和程序性能下降的风险。因此,程序员需要确保只释放已经分配的内存,且只释放一次。
其中需要注意的是,free释放的是free释放的是内存空间,而不是指针。free之后,指针仍然存在,指针指向也不变,而指针指向的内容要视情况而定,可能存在也可能不存在,具体还要看环境和编译器(VS2022是将其置为随机值的)。所以释放后的输出可能和原来的内容一样,也可能是乱码。但是综合考虑,为了安全起见还是不要有对同一块动态内存多次释放这种操作。
5、不能使用free释放动态开辟内存的一部分
错误示例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
// 使用free释放动态开辟内存的一部分
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p++ = i;
}
// 回收空间
free(p);
p = NULL;
return 0;
}
我们有这样一个操作 “*p++ = i;” ,当这个操作结束的时候,我们的指针p指向的空间已经不是我们动态开辟的完整空间了,不仅仅局限指向末尾,只要这里的p不再指向空间的初始位置,都会导致程序的崩溃。
6、不能忘记释放动态开辟的内存(内存泄漏)
动态分配的内存是由程序员手动分配的,而不是由系统自动管理的。如果程序员忘记释放动态分配的内存,那么这些内存将一直占据系统资源,导致内存泄漏和程序性能下降。此外,如果程序员在使用未初始化的动态分配内存时发生访问错误,会导致程序崩溃或出现不可预测的行为。因此,释放动态分配的内存是程序员的责任,必须确保释放内存以避免这些问题。
错误案例一:
char* p=(char*)malloc(100);
p="hellow world!";
案例分析:开始定义了一个指针型变量p在堆区开辟了100个字节的空间,而 p=“hellow world!” 之后,p指向了 hellow world! 的文字常量区,p指向的地址内存分区发生变化,那么p在堆区申请的100个字节的内存(的首地址)就丢了,即发生了内存泄漏。
错误案例二:
void fun()
{
char* p=(char*)malloc(80);
}
int main()
{
fun(); //第一次调用
fun(); //第二次调用
//每调用一次则内存泄漏一次(80字节)
return 0;
}
案例分析:fun函数每调用一次内存就会泄漏一次。因为fun函数中定义了一个指针型变量p在堆区开辟了80个字节的空间,而主函数调用完fun函数之后,既没释放也没返回,所以调用完之后开辟的空间就丢了,就会发生内存泄漏。
解决方案:可以设置一个函数的返回值,主调函数接收这个返回值并对其使用、处理或者释放。
两个问题
free(NULL)的问题
在C语言中free(NULL)的操作是合法的,C语言标准规定:如果free的参数是NULL,那么这个函数就什么也不做。
malloc(0)的问题
在C语言中malloc(0)的语法也是对的,而且确实也分配了内存,但是内存空间是0,这个看起来说法很奇怪,但是从操作系统的原理来解释就不奇怪了。
在内存管理中,内存中有栈和堆两个部分,栈有自己的机器指令,是一种先进后出的数据结构。而malloc分配的内存是堆内存,由于堆没有自己的机器指令,所以要由自己编写算法来管理这片内存,通常的做法是用链表在每片被分配的内存前加个表头,里面存储了被分配内存的起始地址和大小。malloc等函数返回的就是表头里的起始指针(这个地址是由一系列的算法得来的,而这些操作又是由编译器的底层为我们做的,我们并不需要关心如何操作)
动态分配内存成功之后,就会返回一个有效的指针。而对于分配0空间来说,算法会得出一个可用内存的起始地址,但可用的空间为0,而操作系统一般不知道其终止地址,一般是根据占用大小来推出终止地址的。所以对malloc(0)返回的指针进行操作就是错误的。
但需要注意,即使malloc(0)也要记得free掉,因为malloc还会额外分配内存来维护申请的空间,malloc(0)时并不是什么也不做。
四道试题
题目1
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
请问:运行 Test 函数会有什么样的结果?
答案为:程序崩溃
对于本题,很多人的注意力会集中于 “printf(str);” ,实际上这里并没有问题,它等价于 “printf(“%s\n”,str);” 。
解析代码:
看到 “GetMemory(str);” ,我们在这里传递的是 str 本身的值,而不是 str 的地址,进入 GetMemory 函数以后,我们在堆上开辟了100个空间,我们将这些空间放置在 p 中,这里的 p 作为一个形参变量,在 GetMemory 函数结束以后,这个 p 就销毁了, 实际上 str 仍然是NULL,而接下来我们想要将 “hello world” copy 到 str 中去,但是 str 作为NULL,它并没有指向一个有效的空间,进行操作的时候,无法避免的进行了非法访问,虽然后边的 printf 操作没有问题,但是程序在 strcpy 操作时就已经崩溃了。
总结:
(1)运行代码程序会出现崩溃现象;
(2)程序存在内存泄漏问题:
str 以值传递的形式给 p
p 是 GetMemory 函数的形参,只在函数内有效
等 GetMemory 函数返回之后,动态开辟内存尚未释放
并且无法找到,所以会造成内存泄漏
题目2
“返回栈空间地址问题”
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
请问:运行 Test 函数会有什么样的结果?
答案为:随机值(或者崩溃)
解析代码:
看到 “str = GetMemory();” ,进入 GetMemory 函数的时候,p[] 这个数组是GetMemory 函数内的形参,它申请了一个空间,这个空间只在 GetMemory 函数内存在,在 GetMemory 函数结束的时候,的确将 p 的地址返回了,放置在 str 中,但是当 GetMemory 调用完成之后,p 这个数组开辟的空间返还给操作系统了,这个空间里存放的值,我们是不清楚的,接下来 “printf(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(str);
}
请问:运行 Test 函数会有什么样的结果?
答案为:
(1)输出hello
(2)但是有内存泄漏
解析代码:
看到 “GetMemory(&str, 100);” ,将 str的地址传入 GetMemory 函数,用二级指针p 来接收,那么 *p 指向的地址即为 str ,然后将 “hello” copy 到 str 当中,再打印出来,这些操作都没有问题,但是当我们使用完 str 以后,忘记释放动态开辟的内存,导致了内存泄漏。
题目4
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
请问:运行 Test 函数会有什么样的结果?
答案为:
(1)world
(2)非法访问内存(篡改动态内存区的内容,后果难以预测,非常危险)
解析代码:
首先,我们向系统申请了100个字节,地址存放在 str 中;然后,我们把 “hello” copy 到 str 当中去;接下来,我们释放了这块空间,之后, str 指向的这块空间已经还给操作系统了;然后,进行判断: str 是否为空指针,虽然之前我们对申请的动态内存进行了释放,但是 str 的值并没有改变,仍然是 “hello”,所以它不为空指针;进入if语句后,将 “world” copy 到 str 当中,world 就把 hello 给覆盖了;所以打印 str 以后结果为 world。
虽然打印了world,但是这个程序依然出了问题,对于 “free(str);” 操作:已经把空间释放掉了,这表明这块空间已经不属于我们了,我们已经不能再使用这块空间了,但是接下来我们还将 world 放进去,并且打印,这就属于非法访问内存了。
参考博文:
https://blog.csdn.net/m0_73759312/article/details/128763422
https://blog.csdn.net/qq_61672347/article/details/125904571
https://blog.csdn.net/WZRbeliever/article/details/121461425