一、为什么要有动态内存分配
二、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的值仍然是NULL
strcpy(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));//4
return 0;
}