目录
一:为什么存在动态内存分配?
前面已经常用的两种内存开辟方式是:
int val=20;//在栈空间上开辟四个字节
char arr[10]={0};//在栈空间上开辟10个字节的连续空间
这两种方式的特点:
1、空间开辟的大小是固定的。
2、数组在申明的时候,必须指定数组的长度,它所需要的内存在编译 时开辟。
问题是:这样太死板,比如有时候需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟的空间也许不能满足,这时就只能动态开辟了。
动态内存管理:
静态往往和“编译时”相关,而动态往往和“运行时”相关,动态是指程序运行过程中,更灵活的进行申请和释放。
二、动态内存函数的介绍
malloc
动态内存开辟的函数:
void * malloc (size_t size)
- 头文件:#include<stdio.h>
- 参数是无符号整型,即要申请的字节数,且是一个连续的内存空间。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查(监控报警)。
- 监控报警:就针对内存而言,当内存使用超过50%的时候就报警,以防万一有紧急情况出现。
free
释放动态开辟的内存:
void free (void* ptr)
- 头文件:#include<stdio.h>
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc函数的使用:
int main()
{
int *ptr=(int*)malloc(10 * sizeof(int));//分配40个字节大小的内存空间,在堆上
//int* ptr = (int*)malloc(10 * 1024*1024*1024);//分配失败,最多分配2G左右大小
if (ptr == NULL)
{
printf("分配失败\n");
}
assert(ptr);
for (int i = 0; i < 10; i++)
{
printf("%d ", ptr[i]);//随机值
}
free(ptr);//程序员手动释放这段空间
if (ptr != NULL)
{
printf("ptr!=NULL");//打印出来了,说明只是释放了ptr所指向的这一段内存,但是ptr还是指向这个首地址,没有NULL,要手动赋值NULL,不然ptr就是野指针
ptr = NULL;
}
return 0;
}
calloc
也用来动态内存分配:
void* calloc (size_t num, size_t size)
- 函数的功能是为num个大小为size的元素开辟一块内存空间,并把空间的每个字节初始化为0,所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成。
- 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化全为0.
calloc函数的使用:
int main()
{
int* ptr = (int*)calloc(10 , sizeof(int));//分配40个字节大小的内存空间,在堆上
//int* ptr = (int*)malloc(10 * 1024*1024*1024);//分配失败,最多分配2G左右大小
if (ptr == NULL)
{
printf("分配失败\n");
}
assert(ptr);
for (int i = 0; i < 10; i++)
{
printf("%d ", ptr[i]);//初始值都为0
}
free(ptr);//程序员手动释放这段空间
if (ptr != NULL)
{
printf("ptr!=NULL");//打印出来了,说明只是释放了ptr所指向的这一段内存,但是ptr还是指向这个首地址,没有NULL,要手动赋值NULL,不然ptr就是野指针
ptr = NULL;
}
return 0;
}
realloc
- realloc函数的出现让动态内存管理更加灵活。
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,因此用realloc函数对动态开辟内存大小的调整(扩容):
void* realloc (void* ptr, size_t size)
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
- realloc进行扩容的两种情况:
- 1、原有空间之后有足够大的空间,就直接在原内存空间后面直接追加,原来空间的数据不发生变化。
- 2、原有空间之后没有足够大的空间,此时就需要在堆空间上另外找一个更大的连续的空间,把原来内存的数据拷贝过去,返回新的空间的地址,并释放旧的空间。
void * ptr2= realloc(ptr,20)
此处的ptr和ptr2可能相等,也可能不相等
int main()
{
int* ptr = (int*)malloc(10 * sizeof(int));//分配40个字节大小的内存空间,在堆上
assert(ptr);
for (int i = 0; i < 10; i++)
{
*(ptr + i) = i;
printf("%d ", *(ptr + i));
}
printf("\n");
int *ptr2=(int*)realloc(ptr, 10 * sizeof(int) * 2);//要括存的地址空间,括存为原来的几倍。
assert(ptr2);
if (ptr2 != NULL)//万一没有扩容成功,反而会使得ptr为NULL,找不到原来的值,因此要判断扩容是否成功,如果成功则将ptr2赋值给ptr
{
free(ptr);
ptr = ptr2;
}
for (int i = 0; i < 20; i++)
{
printf("%d ", *(ptr + i));//扩充后的内容是随机值,并且会把原来的值复制过来
}
return 0;
}
三、常见的动态内存错误
- 对NULL指针的解引用操作:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
- 对动态开辟空间的越界访问:
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
- 对非动态开辟内存使用free释放:
void test()
{
int a = 10;
int *p = &a;
free(p);//ok? wrong
}
- 使用free释放一块动态开辟内存的一部分:
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
- 对同一块动态内存多次释放:
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
- 动态开辟内存忘记释放(内存泄漏):
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。 切记: 动态开辟的空间一定要释放,并且正确释放 。
四、经典例题
题目1:请问运行Test 函数会有什么样的结果?答:不能打印。
char* GetMemory(char* p)
{
p = (char*)malloc(100);
char* pp = p;
free(p);
p = NULL;
return pp;//改进之后则可以打印,但是本质上已经释放了p所指向的内存,电脑已经不保证那段内存,只是pp还保存了那段内存的地址
}
void Test(void)
{
char* str = NULL;
str=GetMemory(str);
strcpy(str, "hello world");//此时str是空指针,没有申请的内存空间,如果str是数组则可以拷贝
printf(str);//等同于printf("%s",str);
}
错误:
- 没有free
- 没有判定malloc的返回值是否为NULL
- 当前GetMemory函数并没有修改实参str的值,str仍然是NULL,后面strcpy操作就会出现越界访问的情况
题目2:
char* GetMemory(void)
{
char p[] = "hello world";
return p;//局部变量,当前代码块就是生命周期,当当前的函数执行结束,这个内存就释放了
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
分析:
p是局部变量,退出GetMemory这个函数之后p这个数组占用的栈被回收,但p这个地址依然存在并且被返回,但里面存的内容是随机的,因此打印出来是随机值。
题目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是一级指针,&str是二级指针,p是二级指针,*p是一级指针,在GetMemory函数中修改一级指针*p即str指针的值,给其开辟空间并返回初始地址值,故可以拷贝成功。
注意:形参怎么写,关键看实参是什么类型
错误:
- malloc返回结果没有检查
- 没有free
题目4:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//没有置str=NULL,str是野指针,不为空
if (str != NULL)
{
strcpy(str, "world");//赋值成功,但有可能会篡改,访问是非法的
printf(str);
}
}
分析:
free只是释放了str所指向的内存空间,str没有变,但str是野指针。
五、C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
1、栈区(stack):空间比较小,申请释放速度非常快。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些 存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、 堆区(heap):空间比较大,申请释放的速度比较慢。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似 于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁 所以生命周期变长。
5.C++开辟内存的方式&C开辟内存方式的对比
void main()
{
int* p1 = (int*)malloc(sizeof(int));
if (p1 == NULL)
return;
free(p1);
int* p2 = new int(1);
delete p2;
}
从上述代码段可以发现:
①malloc需要说明大小,new不用
②malloc需要强转,new不用
③malloc需要判断返回值是否为空,new不用
④malloc不能初始化,new可以随机初始化
六、柔性数组
C99 中,结构中的最 后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
有些编译器会报错无法编译可以改成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
柔型数组的特点:
- 结构中的柔性数组成员前面必须有至少一个其他成员。
- sizeof返回的这种结构大小不包括柔性数组的内存。
- 包含柔型数组成员的结构用malloc()函数进行动态的内存分配,并且分配的内存应该大于结构的大小,以适应柔型数组的预期大小。
例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4