动态内存分配
一、为什么要有动态内存分配
二、malloc和free
栈区中的数据出了作用域就会销毁;而静态区中数据的生命周期与全局变量一致,出了作用域也不会被销毁,直至程序结束后才会销毁。
malloc函数与free函数需要包含的头文件是<stdlib.h>
①malloc
②free
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main()
{//开辟10个整型的空间int* p = (int*)malloc(10 * sizeof(int));//p的值是开辟的10个整型空间中,第一个整型空间的地址assert(p != NULL);for (int i = 0; i < 10; i++){*(p + i) = 1 + i;}return 0;//释放空间//free将申请的10个整型的空间释放后,p中依然存放的是原来申请的10个整型中,第一个整型空间的地址,此时p就是野指针free(p);p=NULL;//如果不用free函数手动释放这块空间的话,等程序运行结束后,这块空间也会自动被操作系统回收。}
二、calloc与realloc
使用calloc与realloc函数需要包含的头文件是<stdlib.h>
①calloc
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
int main()
{int* p = (int*)calloc(10, sizeof(int));//向内存申请10个整型空间,并将每个字节初始化为0//p中存放的是第一个整型空间的地址assert(p != NULL);for (int i = 0; i < 10; i++){printf("%d ", p[i]);//p[i]等价于*(p+i)}//释放空间free(p);//free函数将这10个整型空间回收后,p中存放的依旧是原先第一个整型空间的地址,此时p就是野指针p = NULL;return 0;
}
②malloc与calloc的区别
a.malloc函数只有一个参数,而calloc函数有两个参数
b.calloc函数会将动态开辟的每个字节初始化为0,而malloc函数动态开辟的每个字节都是随机值。
③realloc
a.realloc函数的介绍
有时会我们发现原先动态开辟的空间太小了(或者太大了),那为了合理的使用内存,我们需要对该内存的大小做灵活的调整。那么 realloc 函数就可以调整动态开辟的内存大小。
b.realloc函数调整动态开辟的空间时的两种情况:
情况1:原有空间之后有足够大的空间时,若想扩展空间的话,就直接在原有空间之后直接扩展,且原来空间的数据不发生变化。
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
int main()
{int* p = (int*)calloc(10, sizeof(int));//向内存申请10个整型空间,并将每个字节初始化为0//p中存放的是第一个整型空间的地址assert(p != NULL);for (int i = 0; i < 10; i++){printf("%d ", p[i]);//p[i]等价与*(p+i)}//将原先动态开辟的10个整型空间扩展为12个整型空间int* ptr = (int*)realloc(p, 12 * sizeof(int));//用一个新变量ptr来接收新的空间的起始地址的目的:如果用p来接收的话,倘若扩展空间失败了,则p的值变为NULL了,此时原来扩展的10个整型空间就找不到了。assert(ptr != NULL);p = ptr;//释放空间free(p);p = NULL;ptr = NULL;return 0;
}
情况2:原有空间之后没有足够大空间,扩展的方法是:在堆区上另找⼀个大小合适
的连续空间来使用,并将旧的数据拷贝到新的空间,然后回收旧的空间。此时realloc函数返回的是⼀个新的内存地址。
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
int main()
{int* p = (int*)calloc(10, sizeof(int));//向内存申请10个整型空间,并将每个字节初始化为0//p中存放的是第一个整型空间的地址assert(p != NULL);for (int i = 0; i < 10; i++){printf("%d ", p[i]);//p[i]等价与*(p+i)}//将原先动态开辟的10个整型空间扩展为50个整型空间int* ptr = (int*)realloc(p, 50 * sizeof(int));//用一个新变量ptr来接收新的空间的起始地址的目的:如果用p来接收的话,倘若扩展空间失败了,则p的值为NULL,此时原来扩展的10个整型空间就找不到了。assert(ptr != NULL);p = ptr;//释放空间free(p);p = NULL;ptr = NULL;return 0;
}
④realloc除了可以调整动态开辟空间的大小外,还可以动态开辟空间
#include<stdlib.h>
#include<assert.h>
int main()
{int* p = (int*)realloc(NULL, 10 * sizeof(int));//等价于int* p = (int*)malloc(10 * sizeof(int));//在堆区申请了10个连续整型的空间assert(p != NULL);//使用空间//......//回收空间free(p);p = NULL;return 0;
}
三、动态内存分配常见的错误
①对空指针解引用
#include<stdlib.h>
int main()
{int* p = (int*)malloc(10 * sizeof(int));*p = 20;//p如果是空指针,编译器就会报错return 0;
②对动态开辟的空间造成了越界访问
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main()
{//开辟10个整型的空间int* p = (int*)malloc(10 * sizeof(int));assert(p != NULL);for (int i = 0; i < 40; i++){*(p + i) = 1 + i;}//只动态开辟了10个整型空间,却访问了40个整型的空间,造成了越界访问。return 0;//释放空间free(p);p=NULL;}
③对非动态开辟的空间使用free来回收
#include<stdlib.h>
int main()
{int arr[10] = { 0 };free(arr);return 0;
}
④使用free函数回收动态开辟空间的一部分
#include<stdlib.h>
#include<assert.h>
int main()
{int* p = (int*)malloc(10 * sizeof(int));assert(p != NULL);for (int i = 0; i < 5; i++){*p = i;p++;}free(p);//此时p指向的不是动态开辟空间的起始位置,编译器将会报错p = NULL;return 0;
}
⑤动态开辟的空间忘记回收(造成内存泄漏)
void test()
{int* p = (int*)malloc(100);if (p != NULL){*p = 20;}
}
int main()
{test();//......return 0;
}
//直至程序执行到return 0 前,动态开辟的10个整型空间不会被回收(当动态开辟的空间不再使用时,应该及时将其回收)
四、动态内存分配经典笔试题
补充:将常量字符串直接传给printf函数时,printf本质上接收的是常量字符串中首字符的地址
#include<stdio.h>
int main()
{printf("abcdef\n");//printf本质上接收的是字符串首字符的地址char* p = "abcdef";printf(p);return 0;
}
第一道
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)
{p = (char*)malloc(100);
}
//错误1:动态开辟了100个字节的空间,但是没有回收,因此会造成内存泄漏
void Test(void)
{char* str = NULL;GetMemory(str);//传值调用//将str的值传给了p,但GetMemory函数调用结束后,str的值仍然是NULLstrcpy(str, "hello world");//错误2:对空指针进行解引用,程序会崩溃printf(str);
}
int main()
{Test();return 0;
}
将上述代码修改正确
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p)
{*p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;GetMemory(&str);strcpy(str, "hello world");printf(str);//回收空间free(str);str = NULL;
}
int main()
{Test();return 0;
}
第二道(返回栈空间的地址的问题)
#include<stdio.h>
char* GetMemory(void)
{char p[] = "hello world";return p;
}
//将数组p首字符的地址返回给str后,数组就销毁了(局部变量出了作用域就会销毁),此时str就是野指针
void Test(void)
{char* str = NULL;str = GetMemory();printf(str);//随机值
}
int main()
{ Test();return 0;
}
第三道
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);//当str指向的动态开辟的空间被操作系统回收后,str就变成了野指针if (str != NULL){strcpy(str, "world");//对野指针解引用,造成了非法访问printf(str);}
}int main()
{Test();return 0;
}
五、柔性数组
①何为柔性数组?
结构体在创建时,最后一个成员变量(前面至少还有一个成员变量)是大小未知的数组,该数组就被称为柔性数组。
struct S
{int a;int arr[0];//并不是指数组arr的大小是0个字节//arr就是柔性数组
};
有些编译器中,上述代码可能会报错,可以改为下面的代码
struct S
{int a;int arr[];//arr就是柔性数组
};
②sizeof在计算包含柔性数组的结构体类型大小时,会忽略柔性数组所占的大小。
#include<stdio.h>
struct S
{int a;int arr[];
};
//其实这个结构体类型的大小应该是大于4个字节的,但是sizeof在计算包含柔性数组的结构体类型大小时,会忽略柔性数组的大小
int main()
{printf("%zd\n", sizeof(struct S));//4return 0;
}