在了解动态内存管理时,我们首先要了解一个概念:
1.内存的分配与释放
内存分配的意思是:操作系统将部分内存的使用权限给予程序。
内存释放的意思是:操作系统回收该部分内存的使用权限,但是但是并不会清空内存中的遗留数据。
一、动态内存管理
1.为什么要有动态内存分配?
我们已经掌握的内存开辟方式有:
int value= 20;
char arr[10] = {0};
上述开辟空间的方式有两个特点:
- 空间大小是一定的
- 数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知道,那数组的编译时开辟空间的⽅式就不能满⾜了。C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就比较灵活了。
2.malloc函数和free函数
malloc函数是一个动态内存开辟函数。
void* malloc (size_t size);
void free (void* ptr);
malloc函数的参数size表示你要申请多少个字节。这个函数像内存申请一块连续可用的内存空间,并返回指向这块空间的指针。
当使用完这块空间时,要交还内存的使用权限,这时使用free函数释放掉这块空间。
free的参数必须是malloc开辟的内存空间的首地址。如果参数是空指针,则函数什么事也不做。
int main()
{
int* p = NULL;
p = (int*)malloc(10*sizeof(int));//如果想返回什么类型的指针,就强转为什么类型
if (p == NULL)//还要判断一下空间是否开辟成功。如果开辟失败,那么p将会是空指针。
{
perror("malloc");
return 1;//返回1代表函数出问题
}
//使用
//使用完毕
free(p);//释放空间
p = NULL;
return 0;//正常情况返回0
}
以上就是malloc函数的基本用法。
malloc函数申请的空间是在堆区。
3.calloc函数
C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
//第一个参数代表申请多少个空间,第二个参数代表申请空间的类型大小。
函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
4.realloc函数
realloc函数让动态内存管理更加灵活有时会我们发现过去申请的空间太小了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型:
void* realloc (void* ptr, size_t size);
//第一个参数是开辟的内存空间的起始地址,第二个参数代表要申请的新的内存的大小(单位是字节)。
realloc 函数调整失败,会返回NULL。
调整成功,有两种情况:
第一种情况:直接从原来的内存末尾位置再开辟足够的空间。
第二种情况:从原来的内存的末尾位置开始开辟空间,但是发现后面又有内存被使用,并且两块内存之间的空间不足以开辟到足够的数量,那么realloc函数将重新寻找一个新的位置,再次开辟足够的空间,并把旧内存空间里的数据拷贝到新的空间,然后释放旧的空间,并返回这块空间的首地址。
int main()
{
int* p = NULL;
p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;//返回1代表函数出问题
}
//使用
//······
//空间不够,想要扩大空间,20个整形
int* tmp = realloc(p, 20 * sizeof(int));
if (tmp != NULL)
{
p = tmp;
}
else
{
perror("realloc");
return 1;
}
//继续使用······
//使用完毕
free(p);//释放空间
p = NULL;
return 0;//正常情况返回0
}
常常创建一个临时变量,用来接收realloc函数的返回值,因为如果用p来接收,那么如果开辟失败,那么p原来指向的空间就丢失了,所以创建一个临时变量来接收realloc函数的返回值,经过判断没有问题之后再将地址赋给p。
realloc除了能够调整空间,它还能实现malloc函数的功能。
int main()
{
int* p = NULL;
p = (int*)realloc(NULL, 10 * sizeof(int));
free(p);//释放空间
p = NULL;
return 0;//正常情况返回0
}
在使用动态内存开辟空间时,切记要规范的写代码,如每次开辟完都要检查是否开辟成功,使用完毕之后要释放掉,释放掉之后将指针置为NULL,这样规范的写代码能够避免很多问题。
5.柔性数组
结构体中的柔性数组成员前⾯必须至少⼀个其他成员。
sizeof返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构⽤malloc()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的大小,以适应柔性数组的预期大小。
struct st
{
int n;
int arr[];//柔性数组成员
};
int main()
{
struct st* ps = (struct st*)malloc(sizeof(struct st) + 10 * sizeof(int));
//先对结构体开辟一块内存空间,再加上柔性数组的空间
if (ps == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//数组空间不够,使用realloc调整大小
struct st* tmp = (struct st*)realloc(ps, sizeof(struct st) + 15 * sizeof(int));
if (tmp != NULL)
{
ps = tmp;
}
else
{
perror("realloc");
return 1;
}
//柔性数组里面的长度是可以变化的
//继续使用
ps->n = 100;
for (i = 0; i < 15; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 15; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n%d\n", ps->n);
//使用完毕,释放
free(ps);
ps = NULL;
return 0;
}
也可以使用这样的方式:
struct st
{
int n;
int* arr;//变成了一个指针
};
int main()
{
int i = 0;
struct st* ps = (struct st*)malloc(sizeof(struct st));
//为结构体开辟空间
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->n = 100;
ps->arr = (int*)malloc(10 * sizeof(int));
//为柔性数组开辟空间
if (ps->arr == NULL)
{
perror("malloc-2");
return 1;
}
//使用
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//空间不足,使用realloc调整
int* tmp = (int*)realloc(ps->arr, 15 * sizeof(int));
if (tmp == NULL)
{
perror("realloc");
}
else
{
ps->arr = tmp;
}
//继续使用
for (i = 0; i < 15; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 15; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n%d\n", ps->n);
//使用完毕,释放空间和将指针置为NULL
free(ps->arr);
free(ps);//使用了两次malloc其中ps里面存储着数组的地址,
// 应该先释放ps->arr,否则的话先释放ps,那么arr指向的空间就丢失了。
ps = NULL;
return 0;
}
两种方案中,柔性数组相对好一点。
第一个好处:方便内存释放
如果我们的代码是在⼀个给别人用的函数中,你里面做了⼆次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户⼀个结构体指针,用户做⼀次free就可以把所有的内存也给释放掉。
第二个好处:这样有利于访问速度
连续的内存有益于提⾼访问速度,也有益于减少内存碎片。
二、总结C/C++中程序内存区域划分
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。