目录
引言
一. malloc和free
1.1 malloc函数
函数作用
函数原型
使用方式
使用示例
注意事项
1.2 free函数
函数作用
函数原型
使用方式
使用示例
注意事项
二. calloc和realloc
2.1 calloc函数
函数作用
函数原型
使用方式
使用示例
注意事项
2.2 realloc函数
函数作用
函数原型
使用方式
使用示例
注意事项
三. 常见的动态内存的错误
3.1 对NULL指针的解引用操作
3.2 对动态开辟空间的越界访问
3.3 对非动态开辟内存使用free释放
3.4 使用free释放一块动态开辟内存的一部分
3.5 对同一块动态内存多次释放
3.6 动态开辟内存忘记释放(内存泄漏)
四. 动态内存笔试题分析
4.1 题目1
4.2 题目2
4.3 题目3
4.4 题目4
后记
引言
在之前的学习中,我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
• 空间开辟大小是固定的。
• 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知 道,那数组的编译时开辟空间的方式就不能满足了。
那么怎么解决这个问题呢?
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
下面让我们来具体学习动态内存开辟。
一. malloc和free
1.1 malloc函数
函数作用
malloc 函数是 C 语言标准库中的一个非常重要的函数,用于动态内存分配。它定义在 stdlib.h头文件中。使用 malloc,程序可以在运行时请求一定大小的内存块,这在处理大小未知或变化的数据结构时非常有用。
函数原型
void* malloc(size_t size);
- 参数:
size
是一个无符号整数,表示要分配的字节数。 - 返回值:如果分配成功,返回一个指向分配的内存块的指针;如果分配失败,返回
NULL
。
使用方式
使用 malloc
时,需要包含 stdlib.h
头文件,并在使用完分配的内存后,通过 free
函数释放它,以避免内存泄漏。
使用示例
#include <stdio.h>
#include <stdlib.h> int main()
{ int *ptr; int n = 5; // 假设我们要存储5个整数 // 分配内存以存储 n 个整数 ptr = (int*)malloc(n * sizeof(int)); if (ptr == NULL) { printf("Memory allocation failed\n"); return 1; } // 使用分配的内存 for (int i = 0; i < n; i++) { ptr[i] = i * i; // 存储平方数 } // 打印结果 for (int i = 0; i < n; i++) { printf("%d ", ptr[i]); } // 释放内存 free(ptr);ptr = NULL; return 0;
}
注意事项
-
类型转换:虽然 C11 标准及以后版本允许省略
malloc
返回值的类型转换((int*)
),但在旧代码或为了明确性,有时仍会看到类型转换。 -
内存泄漏:使用
malloc
分配的内存必须显式地用free
函数释放,否则会导致内存泄漏。 -
初始化:
malloc
分配的内存不会自动初始化为0。如果需要,可以使用calloc
(它会将内存初始化为0)或手动初始化。 -
内存对齐:
malloc
分配的内存块会根据平台的要求进行对齐,以确保访问效率。 -
失败处理:总是检查
malloc
的返回值是否为NULL
,以处理内存分配失败的情况。 -
越界访问:确保不要访问
malloc
分配的内存块之外的内存,这可能导致未定义行为,包括程序崩溃。 -
重复释放:不要对同一块内存使用
free
多次,这会导致未定义行为。
1.2 free函数
函数作用
free
函数是 C 语言标准库中的一个函数,用于释放之前通过 malloc
、calloc
或 realloc
等函数分配的内存块。它定义在 stdlib.h
头文件中,是动态内存管理的重要部分。使用 free
可以帮助避免内存泄漏,即程序不再需要的内存未被正确释放,导致内存持续占用。
函数原型
void free(void *ptr);
- 参数:
ptr
是一个指向之前分配的内存块的指针。如果ptr
是NULL
,则free
函数什么也不做。 - 返回值:
free
函数没有返回值(即返回类型为void
)。
使用方式
当你使用 malloc
、calloc
或 realloc
成功分配内存后,应该在适当的时候使用 free
释放这些内存。通常,在内存块不再需要时调用 free
。
使用示例
#include <stdio.h>
#include <stdlib.h> int main()
{ int *ptr = (int*)malloc(sizeof(int) * 10); // 分配内存以存储10个整数 if (ptr == NULL) { printf("Memory allocation failed\n"); return 1; } // 使用分配的内存... // 释放内存 free(ptr); // 注意:释放内存后,ptr 仍然是之前那个地址,但它指向的内存已经不再是有效的了 // 因此,不应该再对 ptr 进行解引用操作 // 为了避免潜在的错误,可以将 ptr 设置为 NULL ptr = NULL; return 0;
}
注意事项
-
只能释放动态分配的内存:
free
只能用于释放通过malloc
、calloc
或realloc
分配的内存。尝试释放栈内存(如局部变量)或静态内存(如全局变量或静态局部变量)会导致未定义行为。 -
避免重复释放:不要对同一块内存使用
free
多次。一旦内存被释放,指向它的指针就变成了“悬空指针”,再次释放它将导致未定义行为。 -
内存泄漏:即使使用了
malloc
,如果忘记使用free
,也会导致内存泄漏。因此,良好的编程习惯是在分配内存后立即将其初始化,并在不再需要时立即释放。 -
释放后清零指针:虽然
free
会释放内存,但它不会修改指针本身的值。因此,为了安全起见,通常在释放内存后将指针设置为NULL
,以防止后续对悬空指针的误操作。 -
跨函数使用:在函数内部分配的内存,如果需要在函数外部释放,则需要通过参数将指针传递给能够释放它的函数,或者将指针作为返回值返回给调用者。
二. calloc和realloc
2.1 calloc函数
函数作用
calloc
函数是 C 语言标准库中的一个函数,用于动态内存分配,并自动将分配的内存初始化为零。它定义在 stdlib.h
头文件中,是处理动态内存时非常有用的工具之一。与 malloc
函数相比,calloc
提供了一种更加安全的方式来分配和初始化内存,因为它保证了分配的内存块中的所有位都被清零。
函数原型
void* calloc(size_t num, size_t size);
- 参数:
num
:表示要分配的元素数量。size
:表示每个元素的大小(以字节为单位)。
- 返回值:如果分配成功,返回一个指向分配的内存块的指针;如果分配失败,返回
NULL
。
使用方式
calloc
函数接收两个参数:需要分配的元素数量和每个元素的大小。它会自动计算所需的总内存大小(num * size
),并分配这个大小的内存块,同时将所有位初始化为零。
使用示例
#include <stdio.h>
#include <stdlib.h> int main()
{ int *array = (int*)calloc(5, sizeof(int)); // 分配一个可以存储5个整数的数组,并初始化为0 if (array == NULL) { printf("Memory allocation failed\n"); return 1; } // 使用分配并初始化的内存... for (int i = 0; i < 5; i++) { printf("%d ", array[i]); // 应该打印出5个0 } // 释放内存 free(array);array = NULL; return 0;
}
注意事项
自动初始化:与 malloc
不同,calloc
会自动将分配的内存初始化为零。这对于需要初始状态为“空”或“清零”的数据结构非常有用。
其余注意事项与其他的内存函数类似
2.2 realloc函数
函数作用
realloc
函数是 C 语言标准库中的一个非常有用的函数,它用于重新调整之前通过 malloc
、calloc
或 realloc
函数分配的内存块的大小。这个函数在 <stdlib.h>
头文件中声明。使用 realloc
可以避免在内存大小需要改变时手动释放旧内存并分配新内存的繁琐过程,同时也减少了内存泄漏的风险。
函数原型
void* realloc(void* ptr, size_t size);
- ptr:指向之前通过
malloc
、calloc
或realloc
分配的内存块的指针。如果ptr
是NULL
,则realloc
的行为就像malloc
,分配一个大小为size
的新内存块。 - size:新的内存块大小,以字节为单位。
使用方式
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
• realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2:原有空间之后没有足够大的空间,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址。
使用示例
#include <stdio.h>
#include <stdlib.h> int main()
{ int *ptr = malloc(10 * sizeof(int)); // 分配初始内存 if (ptr == NULL) { // 处理内存分配失败的情况 return 1; } // ... 使用ptr指向的内存 ... // 需要更多空间 int *tmp = realloc(ptr, 20 * sizeof(int)); // 尝试重新分配内存 if (tmp == NULL) { // realloc失败,释放原内存并处理错误 free(ptr);ptr = NULL; // ... 处理错误 ... return 1; } // realloc成功,更新ptr指向新内存块 ptr = tmp; // ... 继续使用ptr指向的内存 ... // 释放内存 free(ptr);ptr = NULL; return 0;
}
注意事项
-
使用临时指针:为了避免在
realloc
失败时丢失对原内存块的引用,建议使用一个临时指针来接收realloc
的返回值。如果realloc
成功,则更新原指针以指向新内存块;如果失败,则保持原指针不变,并可以安全地释放它(如果需要的话)。 -
数据完整性:
realloc
只保证在内存块大小增加时保留原内存块的内容(直到新旧大小中较小的一个)。如果新大小小于原大小,则只有新大小部分的数据会被保留。 -
内存泄漏:如果
realloc
失败且你决定不释放原内存块(例如,因为程序即将退出),则必须确保没有其他指针指向该内存块,以避免潜在的内存泄漏。然而,在大多数情况下,如果realloc
失败,你应该释放原内存块并处理错误。
三. 常见的动态内存的错误
3.1 对NULL指针的解引用操作
void test(){int *p = (int *)malloc(INT_MAX/4);*p = 20;free(p);}
上述代码有什么问题呢?
该代码忘记检验动态内存开辟是否成功,这会导致可能对NULL指针进行解引用操作
3.2 对动态开辟空间的越界访问
void test()
{int i = 0;int *p = (int *)malloc(10*sizeof(int));if(NULL == p){exit(EXIT_FAILURE);}for(i=0; i<=10; i++){*(p+i) = i;//当i是10的时候越界访问}free(p);
}
该代码越界访问,只动态开辟了10个int类型的空间,但却使用了11个。
3.3 对非动态开辟内存使用free释放
int main()
{int a = 10;int* p = &a;free(p);p = NULL;return 0;
}
该代码对非动态开辟的内存使用free释放,这是错误的,只有用内存函数动态开辟出的内存才能用free释放。
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 < 10; i++){*p = i;p++;//这样操作的话,p就不指向这块动态开辟空间的起始位置了}//释放free(p);p = NULL;return 0;
}
该代码对动态内存开辟的起始指针进行自增,这会导致它不再指向这块动态开辟空间的起始位置了,所以在后续用free进行释放的时候只能释放一部分,这会导致内存泄漏。
3.5 对同一块动态内存多次释放
int main()
{int* p = (int*)malloc(40);free(p);free(p);//多次释放return 0;
}
该代码对同一块动态开辟的空间进行多次释放,这是错误的。
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{int* p = (int*)malloc(40);//……int flag = 0;scanf("%d", &flag);//5if (flag == 5)return;//内存泄露//free(p);//p = NULL;
}
int main()
{test();return 0;
}
该代码忘记用free释放动态开辟的内存,这会导致内存泄露。
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
四. 动态内存笔试题分析
4.1 题目1
void GetMemory(char *p)
{p = (char *)malloc(100);
}
void Test(void)
{char *str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}
该代码存在哪些问题呢?
下面我用图片和注释的方式来解释
该代码存在两个问题:
1.GetMemory函数中的形参申请动态内存后被销毁,但是它所申请的空间并未被释放,导致内存泄漏
2.Test函数中虽然调用了GetMemory函数,但是它并没有返回值,也没有用变量来存储,这导致str 依旧是空指针,后续又对空指针进行解引用操作,这也是错误的。
下面给出正确的写法:
void GetMemory(char** p)
{*p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;GetMemory(&str);//此时str存放的就是动态开辟的100字节的地址strcpy(str, "hello world");printf(str);free(str);str = NULL;return 0;
}
该代码使用传址调用确保str指针不会被销毁,且用二级指针来接收它的地址,并用解引用操作来实现动态内存的申请。
4.2 题目2
char *GetMemory(void)
{char p[] = "hello world";return p;
}
void Test(void)
{char *str = NULL;str = GetMemory();printf(str);
}
该代码的错误与第一题类似,p是函数内部的局部变量,这里仅仅只是将p首元素的地址传递给str,但p在函数调用完成后已经销毁,str无法再找到p数组后面的内容,此时的str是野指针
4.3 题目3
void GetMemory(char **p, int num)
{*p = (char *)malloc(num);
}
void Test(void)
{char *str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);
}
该代码的函数调用部分是正确的,所以可以打印出"hello",但是后续并没有用free函数释放内存,这会导致内存泄漏。
下面给出正确的代码:
void GetMemory(char** p, int num)
{*p = (char*)malloc(num);
}
void Test(void)
{char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);free(str);str = NULL;//修改将动态内存释放即可
}
4.4 题目4
void Test(void)
{char *str = (char *) malloc(100);strcpy(str, "hello");free(str);if(str != NULL){strcpy(str, "world");printf(str);}
}
该代码在用free函数释放内存空间的时候并没有将str指针设置为NULL,这会导致str变成野指针,可能会在后续的使用中出现问题。
下面给出正确的代码:
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);//野指针,str并未设置成NULLstr = NULL;这样修改即可if (str != NULL){strcpy(str, "world");printf(str);}
}
后记
偷个懒,明天给大家更后面的柔性数组,嘿嘿。
喜欢这篇文章的小伙伴们点点赞,点点关注哈,你们的支持就是我更新的最大动力,感谢各位大佬们的支持哈,谢谢大家,向大家学习!!!
共勉!!!