目录
1.为什么存在动态内存分配?
- 空间开辟大小是固定的
- 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配,但对于空间的需求,不仅仅是上述情况。有时我们需要空间的大小在程序运行的时候才能知道,这时数组在编译时开辟空间的方式就不满足了,我们需要进行动态内存开辟
我们平时创建的局部变量,形式参数等都是在栈区开辟空间,一旦创建大小固定
动态内存函数malloc,free,calloc,realloc是在堆区申请空间,大小可以进行修改
2.动态内存函数
2.1malloc函数
void* malloc (size_t size);
- malloc函数可以在内存中开辟size个字节的空间,并返回这块空间的起始地址;当size为0,即要求动态开辟0字节的空间时,返回值可能是空指针也可能不是空指针,这时返回的指针不能进行解引用操作。
- 注意size的类型为size_t(unsigned int),函数返回的指针类型为void*,void*类型的指针不能直接进行解引用操作,需要先强制类型转换为所需数据类型
- malloc函数分配的内存块未初始化,其中存放的是一些随机值
- 使用malloc函数必须进行指针检查:即检查malloc函数返回的指针是否为空指针,如果其返回空指针,则内存申请失败
malloc函数的使用
#include<stdio.h>
#include<stdlib.h>//动态内存函数所需头文件
#include<string.h>
#include<errno.h>
int main()
{
int arr[10] = { 0 };
//使用malloc开辟40字节的空间
int* p = (int*)malloc(40);
//指针检查
if (p == NULL)
{
printf("%s\n", strerror(errno));//输出错误信息
return 1;//异常返回
}
//内存开辟成功,正常使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
📍 使用malloc函数时,需要对其返回值进行检查,如果开辟失败则进行拦截
2.2free函数
void free (void* ptr);
- free可以释放由malloc,calloc,realloc调用分配的内存块
- 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的
- 如果ptr == NULL,函数不会执行任何的操作
- 动态内存开辟函数和free函数成对使用
free函数的使用
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
int arr[10] = { 0 };
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));//输出错误信息
return 1;//异常返回
}
//内存开辟成功,正常使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//释放
free(p);//free之后内存空间释放,但p的地址还存在
p = NULL;//p赋为空指针
return 0;
}
2.3calloc函数
void* calloc (size_t num, size_t size);
- calloc函数可以为一个元素数组分配一个内存块,元素的数目是num,每个元素大小为size字节;当size为0,即要求动态开辟0字节的空间时,返回值可能是空指针也可能不是空指针,这时返回的指针不能进行解引用操作。
- 注意size的类型为size_t(unsigned int),函数返回的指针类型为void*,void*类型的指针不能直接进行解引用操作,需要先强制类型转换为所需数据类型
- calloc函数分配的内存块会零初始化,即其开辟的内存都被初始化为0
- 使用calloc函数必须进行指针检查:即检查calloc函数返回的指针是否为空指针,如果其返回空指针,则内存申请失败
calloc函数的使用
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>//动态内存函数所需头文件
#include<string.h>
#include<errno.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
//指针检查
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//开辟成功
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//使用结束,释放空间
free(p);
p = NULL;
return 0;
}
可以发现,calloc函数开辟的空间都被初始化为0
2.4realloc函数
void* realloc (void* ptr, size_t size);
- calloc函数可以对指定的空间重新分配一个内存块,ptr指向要更改的内存块(这块内存是先前由malloc,calloc,realloc动态分配的内存块);如果ptr是空指针,则realloc函数的操作类似于malloc函数,分配一个为size字节的新内存块并返回其起始地址。
- 注意size的类型为size_t(unsigned int),函数返回的指针类型为void*,void*类型的指针不能直接进行解引用操作,需要先强制类型转换为所需数据类型
- 使用realloc函数必须进行指针检查:即检查calloc函数返回的指针是否为空指针,如果其返回空指针,则内存申请失败,但并不会释放ptr指向的内存块,且其指向的内存块中内容不变
- 该函数的内存分配有两种情况
🔅追加
可以看到malloc分配的内存块的地址与realloc函数扩容后的内存块的地址相同,这种情况是在原内存块后的内存未被使用,可以继续追加,如下图
🔅重新分配
可以看到malloc分配的内存块的地址与realloc函数扩容后的内存块的地址不同,这种情况是在原内存块后的内存已经被使用,不可以继续追加,需要重新寻找一块满足要求的内存块,返回其起始地址,并释放原内存块,如下图
malloc函数的使用
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
//指针检查
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//开辟成功,正常使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
//扩容
int* ptr = (int*)realloc(p, 48);
if (ptr != NULL)
{
p = ptr;//扩容成功
}
//使用
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//释放
free(p);
p = NULL;
return 0;
}
3.常见的动态内存错误
3.1对NULL指针的解引用操作
int main()
{
int* p = (int*)malloc(40);
*p = 20;
return 0;
}
这里并不确定p是否为空指针,直接对p进行了解引用操作,编译器会对这种行为报警告,检查严格的编译器里这段代码会直接报错
解决方法:对动态内存开辟函数的返回值进行有效性检查
3.2对动态开辟空间的越界访问
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i <= 10; i++)
{
p[i] = i;
}
free(p);
p = NULL;
return 0;
}
应当注意数组的下标,避免越界访问
3.3对非动态开辟内存使用free函数
int main()
{
int a = 10;
int* p = &a;
free(p);
return 0;
}
注意free函数只能释放由动态内存开辟函数开辟的内存块,对于非动态开辟的空间,free函数没有操作权限
3.4使用free函数释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
return 0;
}
++和--操作符会改变变量自身的值,执行free(p)语句时,p已经不指向malloc函数开辟的空间的起始地址,执行free(p)就相当于释放一块动态开辟内存的一部分,这种操作是非法的
3.5对同一块动态内存多次释放
这种情况一般出现在代码较长情况下,忘记之前已经释放
解决方法:free(p);与p=NULL;搭配使用,free(NULL)不执行任何操作
3.6动态内存开辟后忘记释放(内存泄漏)
动态内存函数使用三步:指针有效性检查,使用,释放
4.常见错误代码分析
题一:
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;
}
这段代码是动态开辟一块空间,然后将"hello world"存到这块空间中
GetMemory函数可以动态开辟一块空间,p是一个局部变量,存放这块动态开辟空间的起始地址,当GetMemory函数调用结束时局部变量p销毁,但malloc函数开辟的内存仍然存在,导致了内存泄漏;并且GetMemory函数是传值调用,str并没有指向开辟的动态空间,所以并不能实现最终拷贝的功能
修改:
传址调用:GetMemory(&str);str中存放动态开辟的100byte空间的起始地址
使用完后,释放str指向的空间
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;
}
题二:
int* f1(void)
{
int x = 10;
return (&x);
}
int* f2(void)
{
int* ptr;
*ptr = 10;
return ptr;
}
int main()
{
int* ret1 = f1();
int* ret2 = f2();
printf("%p %p\n", ret1, ret2);
return 0;
}
分析:
f1函数:x是局部变量,出了f1函数的作用域x销毁,&x为野指针
f2函数:指针ptr未初始化,是野指针
题三:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
分析:数组p是一个局部数组,作用域为GetMemory函数,出了GetMemory函数p销毁,p是一个野指针,其指向的空间及空间中的内容是未知的,所以str指向的空间也是未知的
同理还有以下代码:
int* test(void)
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
可以发现printf函数可以成功打印10,原因如下
test函数调用需要创建栈帧
test函数调用结束后,栈帧销毁,printf函数调用又重新开辟栈帧,变量a未被覆盖,p指向的那块空间里仍存放的是变量a,所以可以打印10
题四:
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;
}
分析:
str指向malloc动态开辟的100byte的空间,strcpy(str, "hello");将"hello"拷贝到str指向的空间中,free(str)释放了这块空间,str的地址存在且不变,但是str指向的空间未知,str就是一个野指针,strcpy(str, "world");就是非法访问
所以,free(p)和p=NULL一起使用
5.柔性数组
结构中最后一个元素允许是未知大小的数组,这就叫做柔性数组的成员
如下结构中a就是柔性数组成员
typedef struct st_type
{
int i;
int a[0];//也可以写成int a[]
}type_a;
4.1柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其他成员
- sizeof返回的这种结构大小不包括柔性数组的内存
包含柔性数组成员的结构用malloc函数进行内存的动态分配时,分配的内存大小应该大于结构的大小,以适应柔性数组的预期大小
4.2柔性数组的使用
为含有柔性数组的结构开辟空间有两种方法
🔅malloc一次
🔅malloc俩次(可能导致内存碎片化,使内存利用率下降)
#include<stdlib.h>
struct S
{
int n;
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
return;
}
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//扩容
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
if (ptr != NULL)
{
ps = ptr;
}
free(ps);
ps = NULL;
return 0;
}
#include<stdlib.h>
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
return 1;
}
ps->n = 100;
ps->arr = (int*)malloc(40);
if (ps->arr == NULL)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//扩容
struct S* ptr = (struct S*)realloc(ps->arr, 80);
if (ptr != NULL)
{
ps->arr = ptr;
}
//释放
free(ps->arr);
free(ps);
ps = NULL;
return 0;
}
第二种方法中,结构成员是一个整型变量和一个整形指针,此时数组的大小是可变的
这个结构类型的创建在堆区上
4.3柔性数组的优点
- 方便内存释放
- 有利于提升访问速度