目录
- ❀动态内存管理的意义
- ❀动态内存管理函数
- malloc - 申请空间
- free - 释放空间
- calloc - 申请空间
- realloc - 调整空间大小
- ❀常见的动态内存错误
- 对NULL指针的解引用操作 - err
- 对动态开辟空间的越界访问 - err
- 对非动态开辟内存使用free释放 - err
- 使用free释放一块动态开辟内存的一部分 - err
- 对同一块动态内存多次释放 - err
- 动态开辟内存忘记释放(内存泄漏)
- ❀❀❀
- ❀柔性数组
- 柔性数组的特点
- 柔性数组的优势
❀动态内存管理的意义
基本的内存开辟方式:
int val = 20;//在栈空间上开辟四个字节
int arr1[10];//在栈空间上开辟40个字节的连续空间
但是以上内存开辟的内存空间大小是固定的,不能后续自动改变空间大小。
当实际需要的空间大小比事先开辟的大小小就会造成空间浪费,当不够就需要再另外开辟空间。
但是对于空间的需求,不仅仅是上述的情况。
有时候我们需要的空间大小在程序运行的时候才能知道,编译时开辟空间的方式就不能满足了。
因此就需要动态内存管理的能力。且C语言提供了一些相关函数。
❀动态内存管理函数
malloc - 申请空间
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针。
因此对malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者强制类型转换成需要的类型。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
1、申请成功
//√
#include <stdlib.h>
int main()
{
//申请空间
//void* p = malloc(40);//在堆区开辟指定大小的空间后返回该空间的起始地址
int* p = (int*)malloc(INT_MAX);//用void*指针接收不方便,而此时想存放整形型据就用整型指针接收
//检查返回值
if (p == NULL)
{
//申请失败
perror("malloc");//打印malloc:错误信息
return 1;
}
//申请成功
//初始化
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
return 0;
}
2、申请失败
int* ptr = (int*)malloc(INT_MAX);//申请的空间太大
free - 释放空间
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。
- 如果指向的空间不是动态开辟的,那free函数的行为是未定义的。
int main()
{
//申请空间
int* ptr = (int*)malloc(40);//40B
int* p = ptr;
//检查返回值
if (p == NULL)
{
//申请失败
perror("malloc");
return 1;
}
//申请成功
//初始化
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
释放空间
//free(p);//× - p++后此时p指向的不再是起始位置了
free(ptr);
return 0;
}
//释放空间
free(ptr);//ptr还指向已经被释放的不属于自己的空间,此时ptr就是野指针,因此还要继续将ptr置成空指针
*ptr = 100;//err非法访问
//释放空间
free(ptr);
ptr = NULL;
if (ptr != NULL)
{
*ptr = 100;
}
//√
//写法1:
int main()
{
//申请空间
int* ptr = (int*)malloc(40);
int* p = ptr;
if (p == NULL)
{
//申请失败
perror("malloc");
return 1;
}
//申请成功
//初始化
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
//释放空间
free(ptr);
ptr = NULL;
return 0;
}
//√
//写法2:
int main()
{
//申请空间
int* ptr = (int*)malloc(40);
if (ptr == NULL)
{
//申请失败
perror("malloc");
return 1;
}
//申请成功
//初始化
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
//释放空间
free(ptr);
ptr = NULL;
return 0;
}
- 如果 memblock 是NULL指针,则函数什么事都不做。
int main()
{
int* p = NULL;
free(p);//什么事都不发生
return 0;
}
必须释放空间吗?
当我们不释放动态申请的内存的时候,如果程序结束,动态申请的内存由操作系统自动回收,但是如果程序不结束,动态内存是不会自动回收的,就会形成内存泄露的问题。
int main()
{
while (1)
{
malloc(1000);
}
return 0;
}
//会耗干内存,造成死机
calloc - 申请空间
1、
使用malloc开辟空间:
int main()
{
int*p = (int*)malloc(40);
return 0;
}
使用malloc申请的内存空间都是随机值。
使用calloc申请内存空间:
int main()
{
//申请10个整形的空间
int* p = (int*)calloc(10, sizeof(int));//sizeof(int) - 每个元素的大小
return 0;
}
calloc申请的空间会被初始化为0。
喜欢初始化为0就可以选用calloc了。
2、
int main()
{
//申请10个整形的空间
int* p = (int*)calloc(10, sizeof(int));
//开辟失败
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
free(p);
p = NULL;
return 0;
}
malloc的底层实现非常复杂。
realloc - 调整空间大小
1、使用场景:
有时会我们发现过去申请的空间太小了,有时候又会觉得申请的空间过大了,realloc 函数可以对动态开辟内存的大小做灵活的调整。
2、realloc在调整内存空间的存在两种情况:
(1)当原空间后有足够的未用空间时,直接追加开辟新的空间,返回整个空间的起始地址(同原起始地址);
(2)当没有足够未用的空间时,realloc会重新在空间中的其它位置找一块足够未用的空间,先将原空间的数据复制到新空间,然后再追加,realloc会将原空间主动free掉,返回新空间的起始地址;
int main()
{
int*p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;//0 1 2 3 4 5 6 7 8 9
}
//空间仍不够,希望能放20个元素,考虑扩容 - realloc
//int* p = (int*)realloc(p, 80);//当找不到足够的空间扩容失败返回空指针时,还会弄丢原空间
int*ptr = (int*)realloc(p, 80);//80B
if (ptr != NULL)
{
p = ptr;
}
//扩容成功了,开始使用
//不再使用,就释放
free(p);
p = NULL;
return 0;
}
❀常见的动态内存错误
对NULL指针的解引用操作 - err
开辟空间失败的时候,有可能出现对空指针解引用的操作。
int main()
{
int* p = (int*)malloc(1000);
//解决办法:对malloc函数的返回值进行判断,防止空指针
int i = 0;
if (p == NULL)
{
//....
return 1;
}
//使用
for (i = 0; i < 250; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
对动态开辟空间的越界访问 - err
动态开辟空间的方式类似于数组,也是一片连续的内存空间。
解决:对内存边界要自行主动检查。
int main()
{
int* p = (int*)malloc(100);//25个整型空间
int i = 0;
if (p == NULL)
{
//....
return 1;
}
使用
越界访问了
//for (i = 0; i <= 25; i++)//访问到第26个整型空间
for (i = 0; i < 25; i++)
{
*(p + i) = i;
}
return 0;
}
对非动态开辟内存使用free释放 - err
int main()
{
int a = 10;
int* p = &a;
//.....
free(p);//free不能释放不是动态开辟的空间,编译会报错
p = NULL;
return 0;
}
对于局部变量,出作用域时编译器会主动释放。而动态开辟的空间如果程序不结束,不会主动释放空间,需要free来帮助释放。
使用free释放一块动态开辟内存的一部分 - err
int main()
{
int* p = (int*)malloc(100);
if (p == null)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
//释放空间
free(p);//err - 此时p在++后指向的不是起始地址了
p = null;
return 0;
}
用free释放必须从起始位置开始释放。
必须提前记住起始地址。避免内存泄露。
对同一块动态内存多次释放 - err
int main()
{
int* p = malloc(100);
if (p == NULL)
return 1;
free(p);
//....
free(p);//err
p = NULL;
return 0;
}
动态开辟内存忘记释放(内存泄漏)
1、
void test()
{
int* p = malloc(100);
使用
忘记释放
//free(p);
//p = NULL;
}
int main()
{
test();
//.....
while (1)
{
;
}
//已经无法释放
return 0;
}
2、
即使成对使用 malloc 和 free ,仍可能内存泄漏。
void test()
{
int* p = malloc(100);
//使用
if (1)
return;
free(p);
p = NULL;
//没机会执行free,仍然会内存泄漏
}
❀❀❀
1、
#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);
}
int main()
{
Test();
return 0;
}
程序崩溃。
原因:(1)内存泄漏;(2)对NULL的解引用操作。
p是str的临时拷贝,函数调用结束后,形参变量p会被销毁,但p指向的动态空间并没有被释放,造成内存泄漏;而str仍指向空指针,strcpy造成对空指针的非法访问。
注意:
int main()
{
char* p = "hehe\n";//p指向的是字符串的首地址 - h的地址
printf("hehe\n");//传给printf函数的是字符串的首地址
char arr[] = "hehe\n";
printf(arr);
return 0;
}
//√
#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;
}
2、
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的内存空间在函数调用结束后会销毁,又返回了数组p的首地址,str将非法访问一块未知的空间,此时str就是野指针。
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
printf("%d\n", *p);
return 0;
}
如果空间释放后栈帧空间的数据没有被覆盖,则可能能够访问得到原数据,一旦被覆盖就找不到原数据了。
注意与以下代码作区分:以下是正确代码;
int test()
{
int a = 10;
return a;
}
int main()
{
int n = test();
return 0;
}
3、
#include <string.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
//√
#include <string.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
//判断是否申请成功
//...
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
//没有释放
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
4、
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)//释放后使用造成str成野指针
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
//√
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
❀柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,即 柔性数组成员。
柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
struct S1
{
int num;
double d;
int arr[0];//柔性数组成员
};
struct S2
{
int num;
int arr[];//柔性数组成员
};
- sizeof 返回的这种结构大小不包括柔性数组的内存。
struct S2
{
int num;//4
int arr[];//柔性数组成员
};
int main()
{
printf("%d\n", sizeof(struct S2));//?
return 0;
}
- 包含柔性数组成员的结构要用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
#include <stdlib.h>
struct S2
{
int num;//4
int arr[];//柔性数组成员
};
int main()
{
//struct S2* ps = (struct S2*)malloc(sizeof(struct S2));//× - 只为num开辟了空间
struct S2* ps = (struct S2*)malloc(sizeof(struct S2)+40);//根据情况为柔性数组分配预期空间,此时期望柔性数组内可以放10个整型
if (ps == NULL)
{
perror("malloc");
return 1;
}
//访问num
ps->num = 100;
//访问柔性数组 - 同普通数组
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
//释放
free(ps);
ps = NULL;
return 0;
}
- 方便对数组的扩容。
#include <stdlib.h>
struct S2
{
int num;//4
int arr[];//柔性数组成员
};
int main()
{
struct S2* ps = (struct S2*)malloc(sizeof(struct S2)+40);//根据情况为柔性数组分配预期空间,此时期望柔性数组内可以放10个整型
if (ps == NULL)
{
perror("malloc");
return 1;
}
//访问num
ps->num = 100;
//访问柔性数组 - 同普通数组
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//for (i = 0; i < 10; i++)
//{
// printf("%d ", ps->arr[i]);
//}
//扩容
struct S2* ptr = (struct S2*)realloc(ps, sizeof(struct S2)+80);//为柔性数组扩容80B
if (ptr == NULL)
{
perror("realloc\n");
return 1;
}
else
{
ps = ptr;
}
//继续访问柔性数组
for (i = 10; i < 20; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 20; i++)
{
printf("%d ", ps->arr[i]);
}
//释放
free(ps);
ps = NULL;
return 0;
}
柔性数组的优势
struct S3
{
int num;
int* arr;
};
int main()
{
struct S3* ps = (struct S3*)malloc(sizeof(struct S3));
if (ps == NULL)
{
return 1;
}
ps->arr = (int*)malloc(40);
if (ps->arr == NULL)
{
free(ps);
ps = NULL;
return 1;
}
//使用
//...
//释放 - 注意释放顺序
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
以上代码同样可以调节数组的大小,但在实现细节上会有所差异。
优势1: 方便内存释放。
柔性数组的空间是和其它变量一次申请的,是一片连续的内存空间,使用完通过一次就可以释放完。
而以上的方法需要多次申请,多次释放。
优势2: 有利于访问速度。
对在内存中连续存放的数据的访问速度比非连续存放的访问速度快。
连续的内存有益于提高访问速度,也有益于减少内存碎片的浪费。
高并发内存池!
其实实际上,两种动态开辟方式相差不大,都可以使用。