目录
前言:
小伙伴们大家好,今天龙宝要给大家分享的知识是 动态内存管理。相信大家在使用数组的时候都遇到过开辟的空间不够用或者开辟的空间用不完的窘境,这是因为数组大小一旦确定好,就会向内存申请一块固定大小的连续空间,后面再想增加或者减少空间是非常麻烦的。而今天要介绍的动态内存管理就会很好的帮助大家解决这一窘境,我们可以根据自己的需求向内存申请空,那接下来就让我们一起来看看动态内存管理都有哪些有趣的知识吧。
一:动态内存函数的介绍
在介绍动态内存管理之前,大家需要知道一点,动态内存管理开辟和释放的空间都是在堆区上
1.1:malloc:
malloc:内存空间申请函数
void* malloc (size_t size);
- 申请一块大小为 size 的内存空间,申请成功,返回指向这块空间起始位置的指针
- 新分配的内存块没有初始化,保留不确定的值
- 如果函数无法分配申请的内存块,会返回一个空指针(NULL),因此 malloc 函数的返回值一定要检查
- 此函数只负责申请 size 大小的内存空间,并不知道未来会存放什么类型的数据,因此函数的返回值是
void*
- 如果参数 size 为 0 0 0 ,malloc的行为是标准是未定义的,取决于编译器。
实际应用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
//申请40个字节,用来存放10个整型
int* p = (int*)malloc(40);
if (p == NULL)//判断是否申请成功
{
printf("%s\n", strerror(errno));
return 1;//申请失败就直接返回
}
//存放1~10
int i = 0;
for (i = 0; i < 10; i++)//malloc申请的是一块连续的内存空间,因此可以把它当成数组来使用。
{
*(p + i) = i + 1;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
malloc
函数在使用的时候一定要注意,在空间申请结束后,要对返回的指针进行判断,看其是否为空指针(NULL),如果为空指针说明空间申请失败了,程序就要直接返回,只有当申请成功了才能继续使用这块空间。malloc函数申请到的是一块连续的内存空间,因此我们在使用这块空间的时候,可以把它当作数组。上面的代码看似完美,却存在着一个巨大的缺陷,我们在使用完这块空间后并没有把其归还给操作系统,俗话说得好:有借有还,再借不难。虽然说程序在结束之后,这块空间会自动归还给操作系统,但是,让“人家”强制收回和自己主动归还性质是完全不一样。为了避免不必要的麻烦,我们应该养成在使用完动态开辟的空间之后及时将其归还的好习惯。那如何归还呢?这叫要用到接下来这个函数了。
1.2:free:
free:内存块释放函数
void free (void* ptr);
- 参数是待释放空间起始位置的指针
- 如果参数
ptr
指向的空间不是动态开辟的,那free
函数的行为是未定义的 - 如果参数为空指针(NULL),则该函数不执行任何操作。
修改代码:
int main()
{
//申请40个字节,用来存放10个整型
int* p = (int*)malloc(40);
if (p == NULL)//判断是否申请成功
{
printf("%s\n", strerror(errno));
return 1;//申请失败就直接返回
}
//存放1~10
int i = 0;
for (i = 0; i < 10; i++)//malloc申请的是一块连续的内存空间,因此可以把它当成数组来使用。
{
*(p + i) = i + 1;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
return 0;
}
通过上图我们发现,free(p)
仅仅是把 p
指针指向的这块空间归还给了操作系统,但是 p
指针还是指向这块空间,为了避免之后的非法访问,我们还需再添加一步,把 p
置为空指针。
完整代码:
int main()
{
//申请40个字节,用来存放10个整型
int* p = (int*)malloc(40);
if (p == NULL)//判断是否申请成功
{
printf("%s\n", strerror(errno));
return 1;//申请失败就直接返回
}
//存放1~10
int i = 0;
for (i = 0; i < 10; i++)//malloc申请的是一块连续的内存空间,因此可以把它当成数组来使用。
{
*(p + i) = i + 1;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
1.3:calloc:
calloc:动态内存分配函数
void* calloc (size_t num, size_t size);
- 函数的功能是为
num
个大小为size
的元素开辟一块空间,并且把空间的美国i字节初始化为 0 0 0 - 与函数
malloc
的区别只在于calloc
会在返回地址之前把申请的空间的每一个字节初始化为 0 0 0
实际应用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));//开辟10个整型大小的空间
if (p == NULL)
{
perror("aclloc");//判断是否开辟成功
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//使用完了,释放空间
free(p);
p = NULL;
return 0;
}
//结果:
0 0 0 0 0 0 0 0 0 0
1.4:realloc:
realloc:重新分配内存块函数
realloc
函数的出现让动态内存管理更加灵活,有时候我们会发现国企申请的空间太小了,有时候我们优惠觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。此时realloc
函数就可以做到对动态开辟内存大小的调整
void* realloc (void* ptr, size_t size);
ptr
指向要调整的内存地址size
是调整之后的大小- 返回值是一个指向调整之后内存起始位置的指针
- 这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc
在调整内存空间的时候存在两种情况- 情况一:原有空间之后有足够大的空间,此时要扩展内存就直接在原有内存之后追加空间,原来空间的数据不发生变化
- 情况二:原有空间之后没有足够多的空间时,此时扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,把旧的空间的数据,拷贝到新的空间的前面的位置,并且把旧的空间释放掉,这样函数返回的是一个新的内存地址
- 如果传给
realloc
函数的是一个空指针,此时就像调用malloc
int main()
{
int* p = (int*)malloc(5 * sizeof(int));//先用malloc申请5个整型大小的内存空间
if (p == NULL)//判断是否开辟成功
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = 1;//把5个整型全部初始化为1
}
//不够用了,要再增加5个整型
int* ptr = (int*)realloc(p, 10 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(ptr + i));
}
free(ptr);
p = NULL;
ptr = NULL;
return 0;
}
上面代码中,我觉得有两点需要大家注意:
- 有必要同时
free
掉两个新、旧两个指针嘛? realloc
申请到的空间有没有初始化?
先来说第一点:指向旧空间的指针到底要不要 free
?答案是不用。因为 realloc
对空间扩容无非就两种情况,第一种情况在就的空间后边追加,此时 realloc
的返回值还是旧空间的起始地址,也就是说在这种情况下,上面代码中的 p
和 ptr
指针都指向同一个地址,当空间使用结束后只用 free
掉一个就行,因为他俩都指向同一块空间,然后把他俩全部置为空,以免它们成为野指针。第二种情况,realloc
会重新找一块空间,此时它的返回值就是一个全新的地址,但是注意,针对这种情况,realloc
函数本身会把 p
指向的那块旧空间 free
掉,因此这种情况下,在使用完内存空间之后,只需要把 ptr
给 free
掉就行
再来说第二点:这一点很容易验证,在 realloc
成功的时候,我们直接打印出内存中存的值就可以知道了。
通过打印结果我们不难发现,realloc
开辟的新空间是没有初始化的,前面的五个
1
1
1 是旧空间当时赋值的。
二:常见的动态内存错误
2.1:对NULL指针的解引用操作:
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + 1) = 0;
}
return 0;
}
上面代码中,在 malloc
执行后,没有对 p
指针进行检查,因为 malloc
也可能失败,失败时会返回一个空指针,如果是这样,那下面就是对空指针进行解引用操作,这样是不合适的。
2.2:对动态开辟空间的越界访问:
int main()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return 1;
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
上述代码,当 i
等于
10
10
10 的时候就会造成越界访问,越界访问最终会导致程序挂掉。
2.3:对非动态开辟内存使用free释放:
int main()
{
int a = 10;//栈区
int* p = &a;
free(p);
return 0;
}
需要注意free
针对的是堆区上的空间,而上述代码中的 p
指针指向一个整型变量,整型变量是在栈区申请的空间
2.4:使用free释放一块动态开辟内存的一部分:
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = 0;
p++;
}
free(p);
}
上面代码中的 p
指针,在经历过多次 p++
之后就不再指向这
5
5
5 个整型的起始地址了,此时再去free
就会出问题。
2.5:对同一块动态内存多次释放:
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
free(p);
free(p);
return 0;
}
如上面代码,在程序的最后对 p
指针 free
了两次,此时程序就会报错。其实关于这一点,我在前面 realloc
函数中已经提到过了,当时就问:有必要同时 free
掉两个新、旧两个指针嘛?答案是不需要,本质原因就是对同一块动态内存多次释放程序会报错。但是在第一个 free
的后面把 p
指针赋为空指针,然后再 free(p)
就没有任何问题了。
2.6:动态开辟内存忘记释放(内存泄漏):
void test()
{
int* p = (int*)malloc(100);//在函数内部申请了100个字节的空间
}
int main()
{
test();
return 0;
}
&emps;上述代码存在两个问题,一是:p
指针指向的内存空间在使用结束后没有释放;二是:出了 test
函数后 p
指针就被销毁了,像释放这块内存空间也没办法释放了,直到程序结束才能才被释放,因此我们应该养成良好的习惯,malloc
和free
必须同时出现。后有一种就是,在函数内部申请空间,然后把申请到的空间的起始地址返回到主函数里面,在主函数里用一个指针变量接收,此时也一定要记得释放空间。
int* test()
{
int* p = (int*)malloc(100);//在函数内部申请了100个字节的空间
if (p == NULL)
{
perror("malloc");
return 1;
}
return p;
}
int main()
{
int* ptr = test();
free(ptr);//此时一定要记得释放掉ptr
ptr = NULL;
return 0;
}
内存释放的意义在于,有一些机器是长时间工作的,如果一直 malloc
向内存申请空间,用完了不释放,不把这块空间还给操作系统,那可用内存就会越来越少,可能会影响机器工作,只有重启才能解决问题,这其实就是忘记释放内存导致的后果。
三:经典练习题
3.1:题一:
下面代码的运行结果是?
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
这个程序会挂掉。问题的原因在于,GetMemory
函数采用的是值传递,实参 str
是一个字符指针变量,形参同样用字符指针变量 p
来接收,形参是实参的一份临时拷贝,此时 p
和 str
是各自独立的两个指针变量,但它们都是空指针,此时在函数内部让 p
重新指向新开辟出来的空间,此时 p
就不再是空指针了,但是这一切和 str
有什么关系呢?p
和 str
唯一的关系就是,p
的值最初是从 str
拷贝过期的,从这之后 p
和 str
再无瓜葛,当GetMemory
函数结束的时候 p
会被释放掉,接下来执行 strcpy
,但此时此刻的 str
依然是一个空指针,NULL
就表示
0
0
0 ,也就是是地址为
0
0
0的内存空间,这块空间是不允许普通程序去访问的,因此在执行 strcpy
的时候程序会报错,这是上面代码存在的一个问题,还有一个问题就是:内存泄漏,GetMemory
函数中动态申请的空间没有释放,之后想释放都释放不掉。
正确写法:
void GetMemory(char** p)//形参用二级指针接收,此时p里面存的是str的地址
{
*p = (char*)malloc(100);//*p得到str,让str指向新开辟的空间
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);//址传递
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
3.2:题二:
下面代码的执行结果是:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
上面代码打印出来的是:烫烫烫烫烫烫烫烫圉7。这里问题的关键在于,数组p
是一个局部变量,在出 GetMemory
函数的时候,数组 p
的内存空间就被销毁了,还给了操作系统,虽然把这个数组首元素的地址返了回去,但此时再通过地址去访问这一块空间,就成了非法访问。这种问题通常也被叫做返回栈空间地址的问题
正确写法:
char* GetMemory(void)
{
char* p = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
"hello world"
作为字符串常量,存储在静态区,不会随着 GetMemory
执行结束而销毁。当然这了还可以在数组 p
前面加上 static
来修饰。
3.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);
}
int main()
{
Test();
return 0;
}
这段代码可以成功打印出hello,但是仔细观察就能发现,这段代码里面之见 malloc
却不见 free
这就是典型的内存泄漏。
正确写法:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
3.4:题四:
下面代码执行的结果是?
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
这段代码可以成功打印出world。但上面这段代码是有问题的,因为我们已经把 str
给 free
掉了,意思也就是,已经把这块空间归还给操作系统了,这块空间的操作权限属于操作系统。在 free
完后没有把 str
置为空,所以 str
还是指向那块空间,此时的 str
已经变成了一个野指针,后面一些列涉及 str
的操作都属于非法访问。正确的做法是在 free
的后面,把指针置为空。
四:C/C++程序的内存区域的划分
- 内核空间: 用户代码不能访问
- 栈区(stack): 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap): 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
- 数据段(静态区)(static): 存放全局变量、静态数据。程序结束后由系统释放。
- 代码段: 存放函数体(类成员函数和全局函数)的二进制代码,只读常量区(字符串)。
总结:
今天的分享到这里就结束啦。今天我们学习了动态内存分配的有关知识,了解了和动态内存分配有关的四个函数 malloc
、free
、calloc
、realloc
的用法,通过许多例子,我们发现在使用完动态空间后,一定要记得把它归还给操作系统,不然会造成内存泄漏,归还完了之后,还需把指针置为空,否则会造成非法访问。我们还列举了许多有关动态内存分配的常见错误,大家要牢记这些错误,在使用的时候注意避免。
如果觉得文章还不错的话,记得三连支持一下小恐龙,您的支持就是小恐龙不断前进的动力!