目录
一、前言
二、内存
三、malloc
三、free
四、calloc
五、 realloc
六、常⻅的动态内存的错误
1.对NULL指针的解引⽤操作
2.对动态开辟空间的越界访问
3.对非动态开辟内存使⽤free释放
4.使⽤free释放⼀块动态开辟内存的⼀部分
5.对同⼀块动态内存多次释放
6.动态开辟内存忘记释放(内存泄漏)
七、柔性数组
八、C/C++中程序内存区域划分
一、前言
为什么会有动态分配呢? 首先我们已经掌握的内存开辟⽅式有:
int a = 20; //在栈空间上开辟四个字节
char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间
- 空间开辟大小是固定的
- 数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了大小不能调整
所以C语⾔引⼊了动态内存开辟,让程序员自己可以申请和释放空间,就⽐较灵活了
二、内存
先简单的介绍一下内存,一般我们会关注内存的三个区域:栈区、堆区、静态区
局部变量跟函数参数都放在栈区,全局变量跟静态变量都放在静态区
而我们今天要讲的动态内存管理,都放在堆区
三、malloc
void* malloc (size_t size);
- 这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针
- 如果开辟成功,则返回⼀个指向开辟好空间的指针
- 如果开辟失败,则返回⼀个 NULL 指针
- 开辟的空间内部的值为随机值
- 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器
int a[10];
//都是开辟了40个空间
malloc(40);
但是开辟成功会返回一个指向开辟好这块空间的指针,但是这个函数返回值为void*,所以在开辟之前就要想好自己想要开辟怎么样一块数据类型的空间,在拿地址接受的时候强制类型转为为该数据类型指针
开辟失败会返回NULL,所以malloc的返回值⼀定要做检查判断
//我想申请一块40个字节的整形空间
//开辟的空间起始地址拿p接受
int *p = (int*)malloc(40);
//如果开辟失败 报错
if(p == NULL)
{
perror("malloc");
return 1;
}
三、free
C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的 ,声明在 stdlib.h 头⽂件
void free (void* ptr);
- free函数⽤来释放动态开辟的内存
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的
- 如果参数 ptr 是NULL指针,则函数什么事都不做
上述malloc所申请的动态空间,在使用完了没有办法释放,所以就有了free 函数,来释放动态内存
申请了一块40空间
int *p = (int*)malloc(40);
//代码.....
//执行完了,传入起始地址,进行释放
free(p);
//避免成为野指针
p = NULL;
但是虽然说内存被释放了,但是指针p还是记录着 当时开辟动态内存的地址,所以为了避免指针p在未来变成野指针,我们在释放完了内存以后,把接收到的指针给她赋值成NULL
四、calloc
void* calloc (size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全为0
malloc开辟的空间内部的值为随机值,而calloc这个所开辟的空间值全为0
int a[10] = {0};
//都是申请了40个内存并且初始化为0
calloc(10,4);
所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务
五、 realloc
C语⾔还提供了⼀个函数叫 realloc,realloc函数的出现让动态内存管理更加灵活
void* realloc (void* ptr, size_t size);
- 有时会我们发现过去申请的空间太小太大,我们会对内存的大小做灵活的调整 realloc 函数就可以做到对动态开辟内存大小的调整
- ptr 是要调整的内存地址
- size 调整之后新⼤⼩
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
就好比现在malloc事情了一块动态内存,然后拿realloc去扩内存
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
代码....
}
else
{
perror("malloc");
}
//扩展容量
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
}
//释放空间
free(ptr);
p = NULL;
return 0;
}
先将realloc函数的返回值放在p中,扩增成功,不为NULL,再放入ptr中
但同时呢,也是分两种情况
第一种:原有空间之后有⾜够⼤的空间
拓展内存,直接补在原有的内存空间大小的后面,然后返回原来地址的首地址,原来空间的数据不发生变化
第二种:原有空间之后没有⾜够⼤的空间
在堆空间上重新找⼀个合适大小 的连续空间来使用,并且清除并拷贝原来的空间数据到新的空间上,并且返回一个新的内存地址
当然realloc也可以创建空间大小
malloc(40);
//都创建了一个大小为40动态的内存空间,实现于malloc一样的功能
realloc(NULL,40);
六、常⻅的动态内存的错误
1.对NULL指针的解引⽤操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
因为malloc创建不成功的时候,会给指针p传一个NULL
对于NULL是不能直接解引用的
正确的做法应该去判断一下
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
if(p != NULL)
{
*p = 20;
}
else
{
perror("malloc");
return 1;
}
free(p);
p = NULL;
}
2.对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
return 1;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
越界内存报错信息
开辟了一个40个字节的空间,但是在for循环语句中 当i = 10的时候,已经约过了10个整形大小,超过了所开辟的空间大小,就存在了越界访问内存
3.对非动态开辟内存使⽤free释放
void test()
{
int a = 10;
int *p = (int*)malloc(20);
p = &a;
free(p);
p = NULL;
}
非动态内存开辟空间
a开辟的空间存放在栈区,*p存放在堆区,当p = &a ,p指向的空间不再是堆区上面的 空间
由malloc、calloc、realloc所开辟的动态空间,如果不主动去释放,出了作用域是不会销毁
释放方式:
1.free主动释放
2.直到程序结束,由操作系统去释放
4.使⽤free释放⼀块动态开辟内存的⼀部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);
}
p++不再指向动态内存的起始位置,,,free()释放 必须从它的起始地址开始释放,而p++已经不是它的起始地址
5.对同⼀块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
多次释放(在一次释放完将p置为空指针就没问题)
6.动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
test函数调用结束,p为局部变量,申请了空间没有及时释放,函数被销毁了,也没有机会释放那块空间就会一直被占着,发生了内存泄漏
七、柔性数组
- 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的大小,以适应柔性数组的预期大小
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
sizeof 返回的这种结构大小不包括柔性数组的内存
int * pa = (int*)malloc(sizeof(type_a) +100*sizeof(int));
这样柔性数组成员a,相当于获得了100个整型元素的连续空间
struct St
{
int i;
char c;
int a[];
};
int main()
{
int i = 0;
struct St* pa = (struct St*)malloc(sizeof(struct St) + 10*sizeof(int));
//为柔性数组开辟空间
if (pa == NULL)
{
perror("malloc");
return 1;
}
pa->c = 'w';
pa->i = 4;
for (i = 0;i < 10;i++)
{
pa->a[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ",pa->a[i]);
}
printf("\n%d\n",pa->i );
printf("%c\n",pa->c );
//内存不够了进行扩容
struct St* ps = realloc(pa,sizeof(struct St) + 15*sizeof(int));
if (ps == NULL)
{
printf("realloc");
return 1;
}
//当ps不等于NULL,把地址传给起始地址pa
pa = ps;
for (i = 0; i < 15; i++)
{
pa->a[i] = i;
}
for (i = 0; i < 15; i++)
{
printf("%d ", pa->a[i]);
}
printf("\n%d\n", pa->i);
printf("%c\n", pa->c);
//释放内存
free(pa);
pa = NULL;
return 0;
}
当然不引入柔性数组这种方案,我们可以使用结构体加指针来实现相同的操作
struct St
{
int i;
char c;
int* a;
};
int main()
{
struct St* pa = (struct St*)malloc(sizeof(struct St));
//先将成员1 2 分配动态空间
if (pa == NULL)
{
perror("malloc");
return 1;
}
pa->c = 'w';
pa->i = 4;
//再分配成员3的动态内存
pa->a = malloc(10*sizeof(int));
if(pa->a == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0;i < 10;i++)
{
pa->a[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ",pa->a[i]);
}
printf("\n%d\n",pa->i );
printf("%c\n", pa->c);
//内存不够了增加
int * ps = (int*)realloc(pa->a,15*sizeof(int));
if (ps == NULL)
{
perror("realloc");
return 1;
}
pa->a = ps;
for (i = 0; i < 15; i++)
{
pa->a[i] = i;
}
for (i = 0; i < 15; i++)
{
printf("%d ", pa->a[i]);
}
printf("\n%d\n", pa->i);
printf("%c\n", pa->c);
free(pa->a);
pa->a;
//先释放pa—>a所指向的空间,再释放整个pa空间不然扩增那一部分就找不到了
free(pa);
pa = NULL;
return 0;
}
明显第一个种柔性数组的实现更为好一点
第⼀个好处是:方便内存释放因为第二种代码里面做了二次内存分配,也需要2次释放,大大提高了出错率
struct St* pa = (struct St*)malloc(sizeof(struct St));
pa->a = malloc(10*sizeof(int));
free(pa->a);
pa->a;
//先释放pa—>a所指向的空间,再释放整个pa空间不然扩增那一部分就找不到了
free(pa);
pa = NULL;
第⼆个好处是:这样有利于访问速度.连续的内存有益于提高访问速度,也有益于减少内存碎⽚
每一次malloc开辟的空间不是连续的,两次开辟的中间都会有空余没有分配的空间,这些没有被分配的空间叫做内存碎片,大大降低了内存利用率
八、C/C++中程序内存区域划分
除了栈区、堆区、静态区,还有其他的一些区域
- 内壳空间里面的内存不可以读和写
- 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时,这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等
- 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 分配⽅式类似于链表
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放
- 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码
希望对你有帮助