动态内存分配的意义
我们之前掌握的内存开辟方式:
int val = 10;//创建变量:在栈空间上开辟了4个字节的空间
double arr [10] ={0.00}; //创建数组:在栈空间上申请开辟了80个字节的连续空间
上面开辟空间的方式有两个特点:
- 空间开辟的大小固定
- 创建数组时,必须指定数组的长度,数组空间确定后不能再调整
动态内存分配的核心作用
-
解决固定大小问题
-
静态分配(如数组)必须提前确定大小,无法适应运行时变化的需求。
-
动态分配允许程序运行时按需申请内存(例如用户输入决定数据量)。
-
-
突破栈空间限制
栈内存(局部变量)大小有限(通常几MB),大内存需求(如处理图像、大型矩阵)必须用堆内存(动态分配)。 -
灵活控制生命周期
静态变量(全局/局部)生命周期固定,动态内存可手动管理(malloc
申请后,直到free
才释放),适合长期存储数据(如链表节点)。 -
节省内存
避免静态分配“按最大可能”预占内存的浪费(如声明int arr[1000]
但只用了10个元素)。
引入动态内存管理,程序员可以自己申请释放资源,比较灵活。
malloc
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
作用:向内存的堆区申请了一块连续可用的空间,如果成功申请,就返回指向这块空间的起始地址。
头文件包含:malloc的使用需要包含头文件<stdlib.h>
参数说明:size表示要申请的空间大小,单位是字节
返回值说明:返回值是一个void*类型的指针,如果想要使用这块空间存储某一类型的数据,可以将其强制类型转换成相应类型的指针。
代码示例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int n;
scanf("%d", &n);
//假设要申请n个大小的整型空间存放整型数据
int* arr = malloc(sizeof(int) * n);
if (arr == NULL)//申请失败的检查
{
perror("malloc fail");
return 1;
}
//使用动态开辟的空间存放数据:
for (int i = 0; i < n; i++)
{
*(arr + i) = i + 1;
}
for (int i = 0; i < n; i++)
{
printf("%d ", *(arr + i));
}
return 0;
}
注意:
- 如果开辟成功,则返回这块空间的起始地址。
- 如果开辟失败,则返回一个
NULL
指针,因此malloc
的返回值一定要做检查。 - 返回值的类型是
void*
,所以malloc
函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 - 如果参数
size
为 0,malloc
的行为是标准是未定义的,取决于编译器。
free
free函数是专门用来做动态内存的释放和回收的,函数原型:
void free (void* ptr);
参数:ptr表示需要释放的内存块的起始地址。
功能:释放动态开辟的内存。
需包含的头文件:<stdlib.h>
动态申请的内存,我们最好在使用完以后手动将其释放掉,所以上面的代码我们写的还不太完整,现在学习了free函数就来将它完善一下吧:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int n;
scanf("%d", &n);
//假设要申请n个大小的整型空间存放整型数据
int* arr = malloc(sizeof(int) * n);
if (arr == NULL)//申请失败的检查
{
perror("malloc fail");
return 1;
}
//使用动态开辟的空间存放数据:
for (int i = 0; i < n; i++)
{
*(arr + i) = i + 1;
}
for (int i = 0; i < n; i++)
{
printf("%d ", *(arr + i));
}
free(arr);
arr=NULL;
return 0;
}
注意:
- 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的
- 如果ptr是空指针,则函数什么事都不用做
- 释放空间后应该及时将ptr值为空,否则ptr就是野指针。这是因为利用free将空间释放掉,实际上是将ptr指向的空间还给操作系统,但是还给操作系统以后,当前程序仍然能够通过ptr找到不属于当前程序的空间,如果再对这块空间进行访问,就是非法访问,所以我们要将ptr及时置空。
- free释放空间时,它的参数一定要是动态申请的空间的起始位置,不能释放空间的一部分
- 不能对同一块空间进行多次释放
calloc
C语言提供了calloc函数来进行动态内存分配,函数原型:
void* calloc (size_t num, size_t size);
- 函数功能是为num个大小为size的元素开辟一块连续的空间,并且把空间的每个字节初始化为0。
- calloc与malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为0
代码示例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* arr = (int*)calloc(10, sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d ", *(arr + i));
}
free(arr);
arr=NULL;
return 0;
}
运行结果:
0 0 0 0 0 0 0 0 0 0
如果我们要对申请的空间进行初始化,可以使用calloc来完成任务。
realloc
函数原型:
void* realloc (void* ptr, size_t size);
功能:realloc的出现让动态内存管理更加灵活。它的核心功能是扩展或缩小之前分配的内存块,并尽可能保留原有数据。
扩展内存:
-
若原内存块后方有足够连续空间,直接扩展,返回原指针。
-
若空间不足,会:分配新的更大的内存块;自动复制旧数据到新内存;释放旧内存块;返回新指针。
-
如果空间开辟失败就会返回空指针,但原有空间保留
缩小内存:
- 减少分配的大小,多余部分被释放
- 可能返回原指针(取决于实现)
参数说明:ptr指向要调整的内存地址,size表示调整之后的空间的大小,单位是字节
返回值:是调整之后的内存的起始位置。
代码示例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int n;
scanf("%d", &n);
//假设要申请n个大小的整型空间存放整型数据
int* arr = malloc(sizeof(int) * n);
if (arr == NULL)//申请失败的检查
{
perror("malloc fail");
return 1;
}
for (int i = 0; i < n; i++)
{
*(arr + i) = i + 1;
}
for (int i = 0; i < n; i++)
{
printf("%d ", *(arr + i));
}
//扩展空间,注意,不要直接写成:arr=realloc(arr, 2 * n * sizeof(int));
//因为如果空间开辟失败,arr就会变成NULL,就无法找到原有的空间
//所以我们最好使用一个临时指针来接受返回值,进行检查后再让arr=tmp
int* tmp = realloc(arr, 2 * n * sizeof(int));
if (tmp == NULL)//realloc失败
{
perror("realloc fail");
return 1;
}
arr = tmp;
//一系列操作……
free(arr);
arr=NULL;
return 0;
}
realloc函数也可以直接用来开辟空间:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* arr = (int*) realloc(NULL, sizeof(int) * 10);//等价于malloc(sizeof(int)*10)
//其他操作…………
return 0;
}
注意哦,当某个函数中使用动态申请的函数申请空间时, 当函数栈桢销毁的时候这块动态申请的空间并没有被销毁,仍属于当前程序。
只有当整个项目/工程结束的时候,或者我们手动使用free函数将这块空间释放的时候
这块空间才会还给操作系统
练习题
- 练习1:下面代码的执行结果:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
解析:
程序崩溃:str传递给getmemory函数的时候,采用的值传递,形参变量p是str的一份拷贝
当我们把malloc申请的空间的起始地址放在p中时,我们不会修改str,str仍然是NULL
所以当getmemory函数返回后,再去调用strcpy函数,将“hello world"拷贝到str指向的空间
时,程序崩溃
另外,由于malloc申请的空间没有free释放,造成内存泄漏
那么,如何修改代码?
//修改程序:将传值调用改为传址调用,形参的改变可以影响实参
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
- 题目2:下面代码的执行结果?
#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
解析:
程序打印的是随机值
GetMemory函数中创建的数组是在栈区上创建的,当出了函数的作用域,函数被销毁,这个数组也就被销毁了
所以这块数组的空间就不属于当前的操作系统了,但是我们将p返回到了Test函数中
并让str接收返回值,那就能通过str找到这块不属于当前程序的空间,就属于非法访问了
而这块空间中存放的值也是随机值,所以运行结果是随机值
如何修改代码:
//修改代码:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
static char p[] = "hello world";//static修饰的变量是放在静态区中的
//所以函数栈桢销毁的时候,这块空间仍然保留
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
例如:
struct st_type
{
int i;
int a[0];//柔性数组成员
};
有些编译器会报错无法编译可以改成:
struct st_type
{
int i;
int a[];//柔性数组成员
};
柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
sizeof
返回的这种结构大小不包括柔性数组的内存。- 包含柔性数组成员的结构用
malloc()
函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
用代码来说明特点2:
#include<stdio.h>
struct S1
{
int i;
int arr[];
};
struct S2
{
int i;
};
int main()
{
printf("%zu\n", sizeof(struct S1));
printf("%zu\n", sizeof(struct S2));
return 0;
}
运行结果:
所以,结构体的大小并不会包括柔性数组的大小,这也就意味着,我们在使用含有柔性数组的结构体时,不会按照下面的方法创建变量:
struct S
{
int i;
int arr[];
};
#include<stdio.h>
int main()
{
struct S s;//一般不会这么写:这样写的话并不会为数组分配空间
return 0;
}
那应该怎样写?如下:
#include<stdlib.h>
#include<stdio.h>
struct S
{
int i;
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
//后面的10*sizeof(int)表示为柔性数组开辟的空间
if (ps == NULL)
{
perror("malloc fail");
return 1;
}
ps->i = 1;
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i + 1;
}
//假设要调整柔性数组的空间:
struct S* tmp =(struct S* )realloc(ps , sizeof(struct S) + 20 * sizeof(int));
if (tmp == NULL)
{
perror("realloc fail");
return 1;
}
ps = tmp;
//释放空间
free(ps);
return 0;
}
其实,如果我们不使用柔性数组,却想达到相同的效果,可以像下面这么写:
//如果不使用柔性数组可以利用指针达到同样的效果:
struct S
{
int i;
int* arr;
};
#include<stdio.h>
#include<stdlib.h>
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("malloc fail");
return 1;
}
ps->i = 1;
ps->arr = (int*)malloc(sizeof(int) * 10);
if (ps->arr == NULL)
{
perror("malloc fail2");
return 2;
}
//使用动态开辟的数组:
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i + 1;
}
//如果想要扩容:
int* tmp = (int*)realloc(ps->arr, sizeof(int) * 20);
if (tmp == NULL)
{
perror("realloc fail");
return 0;
}
ps->arr = tmp;
//继续使用空间…………
//释放空间:注意顺序不能乱,如果先释放ps,那就不能通过ps找到arr所指向的空间了
free(ps->arr);
free(ps);
return 0;
}
使用柔性数组(第一个代码)的好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用 free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉。
第二个代码是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(内存碎片(Memory Fragmentation)是指内存空间被分割成许多小块,导致虽有足够总空闲内存,但无法分配连续大块内存的现象。)