提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文
目录
1.存在动态内存分配的原因
之前我们掌握的内存开辟方法有:
int num = 0; //在栈空间开辟四个空间
char arr[5] = {0}; //在栈空间开辟一段连续的空间
但是上述的方式有两个特点:
- 空间开辟大小是固定的
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
这时候就需要动态存开辟
2.动态内存函数
malloc,calloc,realloc,free这些函数操作的空间都是在堆区
2.1malloc和free
malloc函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回一个NULL指针,因此malloc返回值一定要检查
- 返回值的类型是void*,需要使用者自己强制类型转换
- 如果参数size为0,malloc的行为是标准未定义的,取决于编译器分配
free专门做动态内存的释放和回收
- 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的,报错
- 如果参数ptr是NULL指针,则函数什么事都不做
malloc和free函数都声明在stdlib.h的头文件中
//malloc函数(申请)
void* malloc (size_t size);
//返回值为泛型指针,调用时需要强制类型转换
//参数是需要开辟的字节数
//free函数(释放)
void free (void* ptr);
举例:
#include <stdio.h>
#include <stdlib.h> //malloc函数的调用
#include <string.h> //strerror函数的调用
#include <errno.h> //errno的使用
int main() {
//动态内存分配
int* p = (int*)malloc(40); //INT_MAX:自定义类型整型最大
if (p == NULL) {
//打印运行错误信息
//strerror会返回指向错误消息字符串的指针
//errno是记录系统的最后一次错误代码,类型是int型
//在程序运行出错时,一般会将errno变量赋一个整数值,不同的值表示不同的含义
//可以通过查看该值推测出错的原因
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;
//释放内存
//free(p);
//p = NULL;
//释放后p仍指向那个地址,但确实已经还给系统,此时p为野指针
//因此在free之后理应将p指向NULL
//没有free
,当程序退出的时候系统会自动回收内存空间
}
运行正确结果:
内存分配失败返回错误信息(当开辟空间很大时):
2.2calloc
函数声明:
void* calloc (size_t num, size_t size);
- 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化0
- 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0
举例:
int main() {
int* p = 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;
}
运行内存结果:
2.3realloc
realloc的优势:
- realloc函数的出现让动态内存管理更加灵活
- realloc可以做到对动态开辟内存大小的调整
函数声明:
void* realloc (void* ptr, size_t size);
- ptr是要调整的内存首地址
- size是调整后的新大小
- 返回值为调整之后的内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
- realloc在调整内存空间的时候存在两种情况:
- 情况1:原有空间之后没有足够大的空间
- 情况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;
}
//扩容
//realloc(NULL,40)等价于malloc(40)
//不能直接赋给p,如果内存找不到这么大的空间,将会返回NULL
//如果直接赋给p,将会丢失原来的空间,可能会造成内存泄露
int* ptr = (int*)realloc(p, 500);\
if (ptr == NULL) {
printf("%s\n", strerror(errno));
return 1;
}
p = ptr;
ptr = NULL;
//使用
for (i = 0; i < 10; i++) {
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
运行示例:
3.常见的动态内存错误
3.1对空指针的解引用操作
代码示例:
int main() {
//此时内存没有这么大的空间分配,返回了NULL
//p拿到返回值后没有判断,直接解引用,就会出问题
int* p = (int*)malloc(INT_MAX);
*p = 10;
return 0;
}
调试结果:
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;
}
//使用
for (i = 0; i <= 10; i++) {
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
运行结果:
3.3对非动态开辟内存使用free释放
代码示例:
int main() {
int a = 0;
//p指向的内存不是动态开辟出来的
int* p = &a;
free(p);
return 0;
}
运行结果:
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 < 5; i++) {
*p = i;
//p的值改变
p++;
}
//使用
for (i = 0; i < 3; i++) {
printf("%d ", *(p + i));
}
//释放一部分
free(p);
p = NULL;
return 0;
}
运行结果:
3.5对同一块动态内存多次释放
int main() {
int* p = (int*)malloc(40);
//释放一次,p没有赋为空指针
free(p);
//p = NULL;
//释放第二次,仍指向那个地址
free(p);
return 0;
}
代码报错提示:
3.6动态开辟内存忘记释放(内存泄露)
代码示例:
int main() {
int i = 0;
int* p = (int*)malloc(20);
if (p == NULL) {
printf("%s\n", strerror(errno));
return 1;
}
scanf("%d", &i);
if (i == 1) {
//可能还会有一些代码
//初看可能并没有错误
//但在这个if语句中,如果直接返回而没有释放内存
//就极有可能导致内存泄露
//也是最容易出的错误
return 0;
}
free(p);
p = NULL;
return 0;
}
4.柔性数组
4.1定义
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做[柔性数组]成员
//第一种写法,有些编译器会报错无法编译
typedef struct data {
int num;
char str[0];//柔性数组成员
}data1;
//第二种写法,更普遍
typedef struct data {
int num;
char str[];//柔性数组成员
}data2;
4.2特点
- 结构中的柔性数组成员前面必须至少一个其他成员
- sizeof 返回的这种结构大小不包括柔性数组的内存
代码示例:
int main() {
int sz = sizeof(struct data1);
printf("%d\n",sz);
return 0;
}
运行结果:
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
代码示例:
int main() {
struct data1* p = (struct data1*)malloc(sizeof(struct data1) + 40);
if (p ==NULL) {
printf("%s\n",strerrno(errno));
return 1;
}
return 0;
}
4.3使用
代码示例:
int main() {
//这样柔性数组成员就相当于获得了40个字节大小的空间
struct data1* p = (struct data1*)malloc(sizeof(struct data1) + 40);
if (p ==NULL) {
printf("%s\n",strerror(errno));
return 1;
}
int i = 0;
p->num = 10;
for (i = 0; i < p->num - 1; i++) {
p->str[i] = 'a' + i;
}
p->str[p->num - 1] = '\0';
printf("%s\n", p->str);
free(p);
p = NULL;
return 0;
}
运行结果:
4.4柔性数组的优势
以上代码也可以设计成另一种形式
代码示例:
struct Data {
int num;
int* p;
};
int main() {
struct Data* pd = (struct Data*)malloc(sizeof(struct Data));
if (pd == NULL) {
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
pd->num = 10;
pd->p = (int*)malloc(pd->num * sizeof(int));
if (pd->p == NULL) {
printf("%s\n", strerror(errno));
return 1;
}
for (i = 0; i < pd->num; i++) {
pd->p[i] = i;
}
for (i = 0; i < pd->num; i++) {
printf("%d ", pd->p[i]);
}
//需要释放两次,忘记的可能性增大
free(pd->p);
free(pd);
pd = NULL;
//pd->p不用置空,pd置空后pd->p就找不见了
return 0;
}
运行截图:
上述两种方式都可以使用,但第一种方式的实现有两个好处:
- 方便内存释放
- 有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片