上一篇:从0开始学c语言-32-自定义类型:结构体,枚举,联合_阿秋的阿秋不是阿秋的博客-CSDN博客
目录
放个喜欢的图,哈哈哈,内容很长,主要是写给我自己复习看的。
动态内存管理
其实内存里有三个区,每个区放不同的东西。我们的动态内存分配就是在堆区开辟空间的。
1. 为什么存在动态内存分配
我们已经学过的在栈区开辟内存空间的方式有
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
1. 空间开辟大小是固定的。2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
2. 动态内存函数的介绍
2.1 malloc和free
malloc
void* malloc (size_t size); //size单位是字节
如果开辟成功,则返回一个指向开辟好空间的指针。如果开辟失败,则返回一个NULL 指针, 因此malloc的返回值一定要做检查 。返回值的类型是 void* ,所以 malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(加上强制类型转换)。如果参数 size 为 0 , malloc 的行为是标准是未定义的(也就是说,没必要搞这种没意义的数字去试探函数的反应),这种结果取决于编译器。
free
void free (void* ptr);
如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。(因为free就是专门用来释放动态开辟的空间)如果参数 ptr 是 NULL 指针,则函数什么事都不做。
(malloc和free都声明在 stdlib.h 头文件中。 )
应用
假设我们现在要开辟10个int类型的空间
如果是在栈区开辟,那么写一个数组就好了
int arr[10]; //栈区
主要演示动态内存怎么开辟,首先需要malloc来申请空间,
void* malloc (size_t size); //size单位是字节
因为要开辟10个int,那么void* malloc (size_t size); 当中的size便确定了,写成下面这样。
malloc(10*sizeof(int));
我们前面介绍过这个函数会返回一个指向开辟好空间的指针,那么现在我们需要用一个指针来接收它返回的地址。因为是int类型的空间,所以我们用int*指针来接收。又因为返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(加上强制类型转换)。所以我们写成了下面这样。
int* p = (int*)malloc(10 * sizeof(int));
学到这里你是不是觉得,好像这个开辟空间的方式怎么和数组差不多呢?
我们知道数组名是首元素的地址,其实就是指向首元素的指针,在int数组中的数组名就是int*指针。而现在我们接收【开辟动态内存返回值】的 p指针变量 仿佛就是这10个int类型的数组名,p指向首元素,是int*类型的指针。
简单提一下,让你更理解数组。
若开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("main"); //用来报错
return 0;
}
接着,我们试着使用一下。
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", p[i]); //p[i] <=> *(p+i)
}
你会发现,10个int的空间就相当于一个int[10]类型的数组。
现在进行空间释放。使用free函数把p指针指向的动态内存空间回收掉,但是p指针还存放着这块空间的地址,所以释放后我们还需要把这个指针置位空指针,否则它就是个野指针。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int arr[10]; //栈区
//动态内存开辟
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("main");
return 0;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", p[i]);
}
//回收空间
free(p);
p = NULL; //不置为空指针,p就会成为野指针
return 0;
}
这就是比较完整的使用过程了,
1·开头【开辟空间后】 的 指针接收 和 强制类型转换,以及检查是否开辟失败进行报错。
2·结尾的 回收空间,置为空指针
这两个步骤很关键。
和数组的区别
我上面说这个和数组很像,但是并不完全一样。
用这段代码和你说,先和你说calloc开辟的空间会初始化为0,因为我之前说p指针变量好像就和数组名一样,都指向首元素的地址,后面都是9个连续的int空间,所以我进行了如下探索。
int main()
{
int arr[10] = { 0 };
int* p = (int*)calloc(10 ,sizeof(int));
if (p == NULL)
{
perror("main");
return 0;
}
//使用
while (*p++ == *arr++)
printf("qiu");
//释放空间
free(p);
p = NULL; //不置为空指针,p就会成为野指针
return 0;
}
结果发现数组名是不能改变的指针,而p指针是可以改变的。
虽然p指针和arr数组名都可以做到*(p+i)和*(arr+i)来访问并改变元素内容,但是数组名arr这个指针本身并不能被改变,而指针p却是可以改变的。
这大概就是为什么数组大多都是函数传参过去实现更多功能的原因了,因为虽然数组名可以当做指针用,但是我们并不希望在使用的时候改变数组名指向的地址位置,同时却又希望利用数组首元素地址来实现更多功能,如下。
2.2 calloc
void* calloc (size_t num, size_t size);
1·函数的功能是为 num 个、每个元素大小为 size 的元素们开辟一块空间, 并且把空间的每个字节初始化为0 。2·与函数 malloc 的区别只在于 calloc 会在返回地址之前 把申请的空间的 每个字节初始化为全0 。(malloc只是开辟,并不会初始化开辟的动态空间,未初始化之前都是随机值)
![](https://i-blog.csdnimg.cn/blog_migrate/995149b277336c7b1cd482c022c9ca83.png)
可以看到,如果都开辟10个int大小的空间,那么malloc的参数是40,而calloc的参数是(num)10个(size)大小为4byte的空间。本质上都是开辟了40个byte的空间,所达到的结果也一样,区别只有calloc会初始化开辟的空间。
上面监视的窗口输入的是p,可别习惯性加个&写成&p,那就是 保存p指针 的地址了。
int main()
{
//动态内存开辟
/*int* p = (int*)malloc(10 * sizeof(int));*/
int* p = (int*) calloc(10, sizeof(int));
if (p == NULL)
{
perror("main");
return 0;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放空间
free(p);
p = NULL; //不置为空指针,p就会成为野指针
return 0;
}
2.3 realloc
void* realloc (void* ptr, size_t size);
ptr 是要调整的内存地址size 调整后的空间大小返回值为调整之后的内存起始位置。这个函数调整在原内存空间大小的基础上, 还会将原来内存中的数据移动到 新 的空间。
情况2:原有空间之后没有足够大的空间
情况 1当是情况1 的时候,要扩展内存的时候就直接原有内存之后直接追加空间,原来空间的数据不发生变化。情况 2当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。( realloc函数如果后面空间不够,就开辟新空间,拷贝原来数据,返回的地址是新开辟空间的地址。 如果找不到合适的空间,就会返回空指针。 )
绿色代表reallloc函数开辟的空间,情况1有足够的空间来开辟,所以指针和空间都保存原有位置,返回的地址是原有空间的起始地址。
变大
情况二后面的空间不够了,我们会在堆区上找新的空间来开辟,并把原有空间的数据拷贝到这个新空间,返回一个指向新空间的地址。
这是情况二的示范,记住这时候的空间是20000个int大小的空间,包含原有p指针指向的元空间大小。
int main()
{
//动态内存开辟
/*int* p = (int*)malloc(10 * sizeof(int));*/
int* p = (int*) calloc(100, sizeof(int));
if (p == NULL)
{
perror("main");
return 0;
}
printf("%p\n", p);
//使用时候想要在p指针后开辟更大的空间
//别直接用p接收realloc的返回值
//要用另一个指针接收
//为了防止找不到会返回NULL的情况
int* ptr = realloc(p, 20 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
printf("%p\n", p);
printf("%p\n", ptr);
//释放空间
free(p);
p = NULL; //不置为空指针,p就会成为野指针
ptr = NULL;
return 0;
}
需要知道的是,如果我们给realloc函数传一个NULL指针过去,那么所实现的效果和malloc函数类似,就是直接在堆区开辟空间。
int* ptr = realloc(NULL, 20 * sizeof(int));
变小
然后示范一下变小的,可以看到就算空间变小,数据也不会变。
3. 常见的动态内存错误
可别觉得你不会犯这些错误,找不到问题的时候就可以照着这几条找。
3.1 对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
上面的代码就是忘记了对malloc函数的返回值进行NULL的检查,很容易会出问题。
3.2 对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
return 0;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
3.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
这样可不行哦,这个p指针是在栈区上申请空间的,而且free函数是专门释放在堆区动态开辟的内存空间的。可别乱用。
3.4 使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
要注意前置后置++、- - 对于指针的作用,会改变指针的位置。
3.5 对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
动态开辟内存的两种回收方式:1·主动free2·程序结束
4. 几个经典的笔试题
4.1 题目1:
void GetMemory(char *p) {
p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
查阅资料:
char *str = NULL;
或者更直白一些,这个指针变量str会占用4个字节的内存,保存了NULL这个空指针,NULL只是表示这个地址为空。
所以如果我们直接传递str指针变量,是传递过去了指针变量str中保存的地址,而这个地址是NULL。
编译器角度入手
可以看到在vs编译器中str的地址(也就是NULL的地址)会默认为0x00000000,而NULL 指针什么也不指向,自然也就不会再内存中申请空间,所以char*str现在所指向的空间是不存在的。
不懂的话你就拿数组来类比,
我们知道指针是保存地址的,那么现在p这个指针变量所申请的内存空间中就住着arr这个数组首元素的地址。
上面这个图的意思就是p这个指针变量中保存了arr[0]这个元素的地址,这个地址中存的值是{0}。
那么同样的空指针,就可以这样理解。
char *str = NULL;
这是比较具体化的理解了。我的能力到此为止了。
void GetMemory(char *p) {
p = (char *)malloc(100);
}
也就是说现在我们传过来了str指针中保存的地址,而这个地址是NULL。
![](https://i-blog.csdnimg.cn/blog_migrate/e163ecb5ab93f0720f9bb2696af30cd4.png)
那么怎么才可以改变str指针变量中存放的NULL呢?
改善
第一种:给函数返回值,让 str指针 接收动态开辟内存空间的地址。
(用完后,别忘了释放空间并置为空指针)
char* GetMemory(char* p) {
p = (char*)malloc(100);
return p;
}
void Test(void) {
char* str = NULL;
str = GetMemory(str);
if (str != NULL)
{
strcpy(str, "hello world");
printf(str);
}
free(str);
str = NULL;
}
第二种:二级指针接收,传过去str的地址
void GetMemory(char** p) {
*p = (char*)malloc(100);
if(p==NULL)
{
perror("GetMemory");
return 0;
}
}
void Test(void) {
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
4.2 题目2:
char *GetMemory(void) {
char p[] = "hello world";
return p; }
void Test(void) {
char *str = NULL;
str = GetMemory();
printf(str);
}
上面这段也是有错的,要特别清楚,
char *GetMemory(void) {
char p[] = "hello world";
return p; }
这段代码是在栈区开辟空间的,那便是临时变量,退出函数后就会归还空间。
void Test(void) {
char *str = NULL;
str = GetMemory();
printf(str);
}
所以str接收到的地址已经被归还了,是没有访问权限的。
虽然在监视窗口中,你会看到这样的结果。
但实际上这块空间已经被收回了,实际上我们打印的结果会是这样。
4.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);
}
似乎看着很完美了,
但是要知道现在str指针存放的是动态内存分配的区域,所以使用后必须释放空间,并且置为空指针。
4.4 题目4:
void Test(void) {
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这个free掉了这块空间的访问权,我们不能再访问,str虽然还是指向这个空间的地址,但是没有访问权,所以再次访问属于非法访问。
4.5 题目5:
int* f2(void)
{
int* ptr;
*ptr = 10;
return ptr;
}
指针要么指向一个空间,要么是空指针。如果不初始化指针,就随机指向一个空间,对这个不初始化的指针进行解引用就会成为野指针。
4.6 题目6:
我自己出的题,哈哈哈。
void test(int*arr)
{
int*p= (int*)malloc(3 * sizeof(int));
if (p != NULL)
{
arr = p;
}
}
int main()
{
int arr[] = { 1,2 };
test(arr);
*(arr + 2) = 3;
printf("%d\n", *(arr + 2));
free(arr);
arr=NULL;
}
看似很完美?或者问题多多?
其实也就问题很大,你在栈区上开辟了arr空间,又在堆区上让arr指向动态开辟的内存空间。
哈哈哈,不能这样瞎搞。
5. C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
1. 栈区( stack ):在执行函数时, 函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放 。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。2. 堆区( heap ):一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收 。分配方式类似于链表。3. 数据段(静态区)( static )存放全局变量、静态数据。程序结束后由系统释放。4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
实际上普通的局部变量是在栈区 分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static 修饰的变量存放在 数据段(静态区) ,数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。