目录
为什么要有动态内存管理
int a = 10;
char arr [20]={0};
在以上代码中,我们一旦创建变量便向内存分别申请了4字节和20字节的空间,这种方式无法在对内存大小进行调整。
如此我们便会遇到一个问题:申请的内存空间不灵活。举个例子:假如我们想要记录一个班上同学的成绩,用以上方式不难想到创建一个数组。
int grade[30] = {0};
假设我们创建一个可存放30人成绩的数组,如果班上有26个人,我们将会浪费16字节的内存空间,假如班上有35人,将没有足够大小存下。
对此,我们想一下,有没有什么解决方案呢?答案是有!如果我们让程序员去根据自己的需要申请内存大小,这个问题将得以解决。在C/C++中,这个方法就是——动态内存分配。在C/C++中,给程序员足够的权限去申请内存大小,这种方式大大增加了灵活性,但同时也会带来风险。
为此,我将介绍四个动态内存相关的函数:malloc,free,calloc,realloc
malloc和free函数
1.malloc函数——开辟动态内存的函数:
void* malloc (size_t size);
该函数的功能是向内存申请一块空间(单位字节),并返回该空间的地址。
- 如果开辟成功,将返回该空间地址。
- 如果开辟失败,将返回NULL(空指针),所以在使用前一定要做好检查。
- 返回类型为void*,具体类型需要使用者调整。
- 如果size为0,malloc的行为标准是未定义的,取决于编译器。
如果我们希望开辟一块20字节的空间来存放五个整形
#include <stdlib.h>
#include <stdio.h>
int main()
{
int* p = (int*)malloc(20);//向内存申请20字节的空间,并创建p来存放地址
if (p == NULL)
{
perror("malloc");
return 1;//如果开辟失败,将报错并且返回1
}
for (int i = 0; i < 5; i++)
*(p + i) = i;
return 0;
}
在以上代码,我们向内存申请20个字节用来存放5个int类型的变量,如果申请失败将会报错
我们用内存的角度来看一下代码
上图可以看出我们申请的空间,返回的地址是空间的起始地址。
2.free函数
释放之前调用 calloc、malloc 或 realloc 所分配的内存空间,函数原型如下:
void free(void *ptr);
- 如果 ptr 不是动态分配的,free 的行为标准是未定义的。
- 如果 ptr 是NULL,free将没有任何行为。
使用该函数后,将申请的空间重置并把权限还给操作系统。
tips:我们使用free后,最好将ptr赋值为NULL。
我们可以看到使用free函数后,空间被重置为 0xdd 。
calloc和realloc函数
1.calloc函数
分配所需的内存空间,并返回一个指向它的指针。malloc 和 calloc 之间的不同点是,malloc 不会设置内存为零,而 calloc 会设置分配的内存为零
原型如下:
void *calloc(size_t nitems, size_t size);
- 为nitems个大小为size的元素开辟空间,并把空间每个字节初始化为0。
举个例子:
int main()
{
int i, k, n;
int* a;
printf("要输入的元素个数:");
scanf("%d", &n);
a = (int*)calloc(n, sizeof(int));
printf("开辟空间为初始化前\n");
for (k = 0; k < n; k++)
{
printf("%d ", *(a + k));
}
printf("\n");
printf("输入 %d 个数字:\n", n);
for (i = 0; i < n; i++)
{
scanf("%d", &a[i]);
}
printf("输入的数字为:");
for (i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
free(a); // 释放内存
return 0;
}
上述代码的运行示例:
要输入的元素个数:5
开辟空间为初始化前
0 0 0 0 0
输入 5 个数字:
1 2 3 4 5
输入的数字为:1 2 3 4 5
2.realloc函数
尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小,原型如下:
void *realloc(void *ptr, size_t size);
- ptr为需调整空间的首元素地址,size是调整后的大小。
- 返回值是调整内存后的起始地址。
在扩容过程中,我们会遇到三种情况:
1. 我们需要重新调整的空间未分配。正常在未分配空间申请
2. 需分配空间与已分配空间冲突。
在情况2下:第一步,我们在堆区中找到一块满足需求的空间
第二步,将旧空间内容拷贝到新的空间
第三步,释放旧空间,返回新空间的起始地址
3.调整失败,返回空指针
下面是使用案例:
int main()
{
int *p=malloc(20);//向内存申请空间
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
*(p + i) = i;
}
int* ptr = realloc(p, 40);//将空间调整为40字节
if (ptr == NULL)
perror("realloc");
else
p = ptr;
return 0;
}
tips:在以上代码中,我们创建了一个新的整形指针ptr来存放调整后空间的起始地址,这是因为如果调整失败,原本的空间和数据还可以使用。
常见动态内存错误
1.对NULL的解引用操作
这就是一种经典的错误,我们使用malloc函数后,并未对整形指针p进行判断,这样会带来风险。
正确的写法是:
int main()
{
int* p = (int*)malloc(20);
assert(p); //需包含头文件assert.h
*p = 20;
free(p);
}
2.对动态内存的越界访问
int main()
{
int* p = malloc(10 * sizeof(int));
if (p == NULL)
{
exit(EXIT_FAILURE);
}
for (int i = 0; i <= 10; i++)//越界访问
{
*(p + i) = i;
}
free(p);
return 0;
}
上述是错误的代码,我们将for循环中的i<=10改成i<10便解决了这一问题。
3.对非动态内存的空间使用free
int main()
{
int i = 20;
int* p = &i;
free(p);
p = NULL;
return 0;
}
i 变量在栈区申请空间,而我们的free函数只能释放向动态内存申请的空间,所以造成了错误。
4.使用free释放动态内存的一部分
int main()
{
int* p = malloc(100);
assert(p);
for (int i = 0; i < 10; i++)
{
*(p + i) = i + 1;
p++;
}//for循环执行完毕后,p向后移动了10次,不再是动态内存申请的起始位置
free(p);
p = NULL;
return 0;
}
当for循环执行完毕后,p指向的内存不在是动态内存的空间,会出现以下错误:
4.5对同一块动态内存多次free释放
int main()
{
int* p = malloc(20);
free(p);
free(p);
return 0;
}
上述代码同样出现以下错误:
5.忘记free释放动态内存
void test()
{
int* p = malloc(20);
assert(p);
*p = 20;
}
int main()
{
test();
while (1);
return 0;
}
在调用test函数后,申请的动态内存没有及时释放,且test执行完毕后,栈帧被销毁,无法再释放动态内存,这种现象叫内存泄漏。其实malloc / realloc / calloc申请动态内存后,可以使用free释放内存,即使没有释放,操作系统也会回收这一部分内存。
区别
- 函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题。
- 函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零。
- 函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void类型.void表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针。
- realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.当然,对于缩小,则被缩小的那一部分的内容会丢失**.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址**.相反,realloc返回的指针很可能指向一个新的地址。
- realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;如果数据后面的字节不够,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动。
例题
例题1:计算字符串长度
题目要求:编写一个C程序,定义一个函数int strLength(char *str)
,用于计算传入字符串(以空字符'\0'结尾)的长度。在main()
函数中,测试该函数。
#include <stdio.h>
// 函数声明
int strLength(char *str);
int main() {
char input[] = "Hello, World!";
int length = strLength(input);
printf("Length of the string \"%s\" is: %d\n", input, length);
return 0;
}
// 函数定义
int strLength(char *str) {
int len = 0;
while (*str != '\0') {
len++;
str++;
}
return len;
}
解析:
strLength
函数接受一个指向字符串的指针char *str
作为参数。- 定义一个整型变量
len
初始化为0,用于记录字符串的长度。 - 使用
while
循环遍历字符串,条件为当前字符不等于结束符\0
。在循环体内:- 增加
len
的值,表示遇到一个新的字符。 - 将指针
str
向前移动一位,使其指向下一个字符。
- 增加
- 当遍历完字符串,即遇到
\0
时,跳出循环。此时len
已累计了字符串的长度,将其作为函数返回值。 - 在
main()
函数中,定义了一个字符串input
,调用strLength(input)
获取其长度,并打印结果。
例题2:动态分配二维数组
题目要求:编写一个C程序,定义一个函数int** createDynamicMatrix(int rows, int cols)
,该函数创建并返回一个动态分配的rows
行cols
列的整型二维数组。在main()
函数中,测试该函数,创建一个3x5的矩阵,为其赋随机值,并在使用后释放内存。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 函数声明
int** createDynamicMatrix(int rows, int cols);
void freeDynamicMatrix(int** matrix, int rows);
int main() {
int rows = 3, cols = 5;
int** dynamicMatrix = createDynamicMatrix(rows, cols);
srand(time(NULL));
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
dynamicMatrix[i][j] = rand() % 100;
printf("%d ", dynamicMatrix[i][j]);
}
printf("\n");
}
freeDynamicMatrix(dynamicMatrix, rows);
return 0;
}
// 函数定义
int** createDynamicMatrix(int rows, int cols) {
int** matrix = (int**)malloc(rows * sizeof(int*));
for (int i = 0; i < rows; ++i) {
matrix[i] = (int*)malloc(cols * sizeof(int));
}
return matrix;
}
void freeDynamicMatrix(int** matrix, int rows) {
for (int i = 0; i < rows; ++i) {
free(matrix[i]);
}
free(matrix);
}
解析:
createDynamicMatrix
函数接受两个整数参数rows
和cols
,分别表示要创建的二维数组的行数和列数。- 首先,使用
malloc
分配一个包含rows
个整型指针的数组,并将其地址赋给int** matrix
。 - 然后,使用嵌套循环对每一行进行处理,为每一行分配一个包含
cols
个整型元素的数组,并将新分配内存的地址赋给matrix[i]
。 - 函数返回创建好的
matrix
指针,表示动态分配的二维数组。 freeDynamicMatrix
函数负责释放之前动态分配的二维数组内存。它接受矩阵指针matrix
和行数rows
作为参数。- 使用外层循环遍历每一行,对每一行使用
free
释放内存。 - 最后,释放包含行指针的一维数组
matrix
本身。
在main()
函数中,我们创建了一个3x5的动态二维数组,为其赋随机值,并在使用后调用freeDynamicMatrix
释放内存。
总结
动态内存管理是C/C++程序员必须掌握的核心技能之一。理解其基本原理,熟练运用相关函数与运算符,遵循最佳实践,能够有效避免内存泄漏、悬挂指针等问题,提升程序的稳定性和资源效率。随着C++11引入智能指针等现代内存管理工具,动态内存管理变得更加安全便捷。善用这些工具,将使您的代码更具可维护性和可靠性。