目录
——————————————— That Girl ———————————————
正文开始——
引言: 为什么要有动态内存分配?
首先了解内存的分区,见下:
我们已经掌握的开辟内存的方式:
int a=0; //在栈空间上开辟4个字节的空间
char ch[100]={0}; //在栈空间上开辟100个字节连续的空间
上面两种开辟空间的方式有两个特点:
- 开辟空间大小是固定的;
- 数组在申明的时候,必须指定数组的长度,数组空间一旦确定就不能调整。
但是有时候我们需要的内存空间只有在程序运行时才能知道,那上面开辟空间的方式就不合适了。
怎样才能灵活的申请和释放空间呢?下面介绍几个库函数来实现以动态内存开辟空间的方式。
1. malloc 和 free
1.1 malloc
void* malloc(size_t size);
头文件:stdlib.h
作用:向内存申请一块连续可用的空间,返回所申请空间的起始地址。
返回:如果开辟成功,则返回所申请空间的起始地址;如果开辟失败,则返回空指针。因此 malloc 的返回值一定要检查。返回值的类型是 void*,所以不知道 malloc函数所返回的指针的类型,根据需要来定。
【注意】size的值如果是0,malloc的行为是未定义的,取决于编译器。(何必呢,申请空间,结果申请个0)。
1.2 free
作用:专门用来做动态内存释放和回收的。头文件和 malloc 函数一样。
void free(void* ptr);
ptr指向的空间必须是动态内存开辟的,否则free函数的行为是未定义的。如果 ptr 是空指针NULL,那就啥都不做。
int arr[10]={0};
int* p=arr;
free(p); //err
p=NULL;
int *p=(int*)malloc(40);
free(p); //right
p=NULL;
//...
int* p=NULL;
free(p);
p=NULL; //啥都没干
举个栗子:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p=(int*)malloc(10*sizeof(int));//开辟动态内存
int i=0;
if(p!=NULL) //有必要检查是否开辟成功,p是否为空指针
{
for(i=0;i<10;i++)
{
p[i]=i;
printf("%d ",p[i]);
}
}
free(p); //释放动态内存
p=NULL; //目的是啥?
return 0;
}
【注意】
- p指向的空间在释放后不属于当前程序,但仍然存有其起始地址,此时p就是野指针。所以将p置为NULL,防止非法访问。
- 记得检查p是否为空指针。
- 记得释放申请的动态内存。
- malloc 和 free 最好成对使用。
问:原本方式申请空间和malloc申请空间有何不同?
答:原本申请的空间出了大括号会自动释放空间,还给操作系统,但malloc申请的空间需要free函数来手动释放,如果不释放,程序结束时也会被操作系统自动回收,但最好手动释放。
2. calloc 和 realloc
2.1 calloc
作用:用来动态内存分配,开辟 num 个大小为 size 的动态内存。
(void*) calloc (size_t num,size_t size);
【calloc与malloc区别】
- callloc有两个参数,malloc只有一个参数;
- malloc申请的空间不初始化,calloc申请的空间初始化为0。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p=(int*)callloc(10,sizeof(int));
int i=0;
if(p!=NULL)
{
for(i=0;i<10;i++)
{
printf("%d ",p[i]);
}
}
free(p);
p=NULL;
return 0;
}
//运行结果为0 0 0 0 0 0 0 0 0 0,说明初始化为0
2.2 realloc
realloc 函数可以在我们向内存申请的空间过大或过小时对内存大小进行调整,让动态内存管理更加灵活。
void* realloc(void* p,size_t size);
- p为需要调整内存的起始地址,size 为调整后空间大小;
- realloc 调整的必须是动态开辟的(malloc、calloc);
- 返回值为调整后空间的起始地址;
realloc不仅可以调整空间,也可以申请空间,见下操作
realloc(NULL,100) == malloc(100);
realloc 调整内存有两种情况:1.原有空间后有足够大的空间;
2.原有空间后没有足够大的空间。需要重新开辟空间。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p=(int*)calloc(5,4);
if(p!=NULL)
{
int* tmp=(int*)realloc(p,40);
//....
}
return 0;
}
总的来说,realloc 返回的可能是旧空间的起始地址,也可能是新空间的起始地址。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p=(int*)malloc(100);
if(p!=NULL)
{
//...
}
else
{
return 1;
}
//扩容
代码1——直接将p的返回值放到p里面
p=(int*)realloc(p,100);
代码2——先将realloc的返回值放到ptr里面,
int* ptr=NULL;
ptr=(int*)realloc(p,100);
if(ptr!=NULL)
{
p=ptr: //检测是否扩容成功,不可贸然行事,若未扩容成功,p被置为NULL,导致原数据丢失
}
//....
free(p);
p=NULL;
return 0;
}
3. 常见动态内存的错误
3.1 对NULL指针的解引用操作
void test()
{
int* p=(int*)malloc(10);
*p=20; //未检测p是否为空指针,若p=NULL,就会有问题
free(p);
}
3.2 对动态开辟空间的越界访问
void test()
{
int i=0;
int* p=(int*)malloc(10*sizeof(int));
if(p!=NULL)
{
for(i=0;i<100;i++) //err 创建40个字节的空间,这里超出40个,形成越界访问
{
p[i]=i;
}
}
free(p);
p=NULL;
return 0;
}
3.3 对非动态开辟内存进行 free 释放
void test()
{
int a=0;
int* p=&a;
free(p); //err
}
3.4 使用free释放一块动态内存开辟内存的一部分
就是说,free里面必须是要释放动态内存的起始地址。
void test()
{
int* p=(int*)malloc(100);
p++;
free(p); //p不再指向开辟动态内存空间的起始地址了
}
3.5 对同一块动态内存多次释放
void test()
{
int* p=(int*)malloc(100);
free(p);
free(p); //重复释放
}
3.6 动态内存开辟忘记释放(内存泄漏)
void test()
{
int* p=(int*)malloc(100);
if(p!=NULL)
{
*p=20;
}
}
int main()
{
test();
while(1);
}
【注意】
忘记释放不再使用的动态内存一定会造成内存泄漏。所以,动态开辟的空间一定要正确释放。
4. 柔性数组
C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫柔性数组的成员。
struct S
{
int a;
int arr[]; //int arr[0];若编译器报错就改成这个
};
4.1 柔性数组的特点
- 结构体中的柔性数组成员前面必须至少含有一个其他成员;
- sizeof 返回的这种结构大小不包含柔性数组的内存;
- 包含柔性数组成员的结构用 malloc函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
4.2 举栗
#include<stdio.h>
struct s
{
int i;
int arr[];
};
int main()
{
printf("%zd\n",sizeof(struct)); //结果为4
return 0;
}
4.3 柔性数组的优势
代码1
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
struct s
{
int i;
int arr[];
};
int main()
{
struct s* p = (struct s*)malloc(sizeof(struct s) + 20 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
//使用开辟后的空间
for (i = 0; i < 20; i++)
{
p->arr[i] = i + 1;
}
for (i = 0; i < 20; i++)
{
printf("%d ", p->arr[i]);
}
free(p);
p = NULL;
//调整arr的大小
struct s* ptr = (struct s*)realloc(p, sizeof(struct s) + 40 * sizeof(int));
if (ptr != NULL)
{
p = ptr; //若开辟空间失败,ptr会被置为NULL,所以这里要先判断一下,防止数据丢失
ptr = NULL; //避免ptr成为野指针
}
else
{
perror("realloc");
return 1;
}
//使用调整后的空间
for (i = 20; i < 40; i++)
{
p->arr[i] = i + 1;
}
for (i = 20; i < 40; i++)
{
printf("%d ", p->arr[i]);
}
free(p);
p = NULL;
return 0;
}
代码2
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
struct s
{
int i;
int* arr;
};
int main()
{
struct s* ps = (struct s*)malloc(sizeof(struct s));
if (ps == NULL)
{
perror("malloc");
return 1;
}
int* p = (int*)malloc(20 * sizeof(int));
if (p != NULL)
{
ps->arr = p;
}
//使用开辟后的空间
int i = 0;
for (i = 0; i < 20; i++)
{
ps->arr[i] = i + 1;
}
for (i = 0; i < 20; i++)
{
printf("%d ", ps->arr[i]);
}
//调整空间
p = (int*)realloc(ps->arr, 40 * sizeof(int));
if (p != NULL)
{
ps->arr = p;
}
else
{
perror("realloc");
return 1;
}
//使用调整后的空间
for (i = 0; i < 40; i++)
{
printf("%d ", ps->arr[i]);
}
free(p); //先释放第二个malloc开辟的空间,再释放第一个malloc开辟的空间,
p = NULL; //防止因先释放第一个而找不到第二个空间无法释放第二个空间
free(ps);
ps = NULL;
return 0;
}
上述代码1和代码2实现了相同的功能,但代码1有两个好处:
代码1的好处
第一:方便内存释放。如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户不知道这个结构体的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二:这样有利于访问速度。连续的内存有利于提高访问速度,也有益于减少内存碎片。内存碎片:realloc 函数开辟的两块内存空间之间的小的内存空间。(也没多高,少不了要用偏移量的加法来寻址)
完——
彩蛋
——————————————— That Girl ———————————————
期待我们下一次相遇——
再见——