【C/C++】基础:动态内存管理
摘要:本文将介绍关于动态内存开辟的由来与优势,再介绍动态内存开辟的方法,最后会列举常见错误与相应练习帮助大家巩固。
一.概述
在以往学习的过程中,我们通过声明来开辟内存空间,可是这样开辟空间有两个特点:
- 空间大小的开辟是固定的;
- 数组在声明的时候必须是指定的数组长度,它在需要的内存在编译时分配;
可是在现实情况下,会发现在需要开辟内存过程中,有时无法去区分内存开辟的大小,有时内存的大小的差值需要较大,如果只是静态的设定好内存,可能造成内存不足也可能造成内存浪费,非常的不灵活。因此C语言提供了关于动态内存开辟的方法,需要注意的是这些开辟空间的内存在堆区进行动态内存分配,相应函数分别为malloc、calloc、recalloc等。记住必须要对其进行内存释放,并置空指针。
二. 动态内存函数介绍
2.1 malloc
void* malloc (size_t size);
作用:分配内存块,向内存申请一块连续可用的空间,并返回指向这块空间的指针。
参数:size表示需要开辟的内存块的字节数,记住为参数size的类型为无符号数。
返回值:如果成功开辟内存空间,返回开辟成功后的内存块的首地址,地址类型为空指针;如果开辟失败了,将会返回空指针。
解释:
- 开辟后的内存时不进行初始化的;
- size的参数可以为0,但返回类型取决于特定的库函数实现,并且返回后的指针不能被解引用。
- 需要包括头文件 stdlib.h
- 由于开辟空间返回类型为void*指针,因此在开辟后需要强制类型转换一下类型。
- 开辟空间后,需要及时的释放,否则会出现内存泄露等问题。
官方示例:
#include <stdio.h>
#include <stdlib.h>
int main(){
int i, n;
char* buffer;
printf("How long do you want the string? ");
scanf("%d", &i);
buffer = (char*)malloc(i + 1);
if (buffer == NULL) exit(1);
for (n = 0; n < i; n++)
buffer[n] = rand() % 26 + 'a';
buffer[i] = '\0';
printf("Random string: %s\n", buffer);
free(buffer);
return 0;
}
补充说明:以下代码的注释为使用malloc的关键。
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(){
int* p = (int*)malloc(10*sizeof(int));
int* ptr = p;
if (p == NULL){
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++){
*ptr = i;
ptr++;
}
free(p);
p = NULL;
ptr = NULL;
return 0;
}
2.2 free
void free (void* ptr);
作用:对于动态内存的释放和回收
参数:ptr指向先前用malloc、calloc或realloc分配的内存块的指针
解释:
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
- 如果参数 ptr 是NULL指针,则函数不行操作
- 当释放空间时,不得从开辟指针地址后中间开始释放,否则程序会崩溃
- 当释放空间后,需要将指针赋空,否则会出现野指针的情况
- 需要包括头文件stdlib.h
官方案例:
#include <stdlib.h>
int main ()
{
int * buffer1, * buffer2, * buffer3;
buffer1 = (int*) malloc (100*sizeof(int));
buffer2 = (int*) calloc (100,sizeof(int));
buffer3 = (int*) realloc (buffer2,500*sizeof(int));
free (buffer1);
free (buffer3);
return 0;
}
2.3 calloc
void* calloc (size_t num, size_t size);
作用:分配内存块并进行初始化
参数:num表示申请类型的数量,size表示要申请的类型
返回值:如果申请空间成功后,将会返回申请空间的内存块的首地址,返回类型为空指针,因此在申请后需要对其进行强制类型转换;如果申请空间失败,将会返回空指针。
解释:
- 与malloc的区别就是calloc会进行初始化;
- 在使用calloc时,也要注意判断是否会开辟空间失败;
- 对于calloc来说,也需要进行free,并置为空指针;
- 头文件为stdlib.h
官方示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i, n;
int* pData;
printf("Amount of numbers to be entered: ");
scanf("%d", &i);
pData = (int*)calloc(i, sizeof(int));
if (pData == NULL) exit(1);
for (n = 0; n < i; n++)
{
printf("Enter number #%d: ", n + 1);
scanf("%d", &pData[n]);
}
printf("You have entered: ");
for (n = 0; n < i; n++) printf("%d ", pData[n]);
free(pData);
return 0;
}
补充说明:以下代码的注释为使用calloc的细节
int main(){
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL){
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
2.4 realloc
void* realloc (void* ptr, size_t size);
作用:再次申请内存空间,有时我们进行动态内存申请开辟后,可能在后续操作需要更大的空间,这时我们可以通过realloc来完成对空间的再次开辟
参数:ptr为动态申请空间后的指针,size表示调整后的新大小,其单位为字节
返回值:如果成功申请,返回值为申请内存块的首地址,注意不是新申请后的位置的首地址;如果失败,这会返回一个空指针。
解释:
- 一般情况下,会定义一个临时指针来接受地址,在赋值给原指针,原因是,如果内存开辟失败,会返回空指针,这样会导致原内存被丧失,无法释放也无法使用。
- 如果ptr赋予了一个空指针,其作用于malloc相同
- 由于返回类型为空指针,因此需要对其进行强制类型转换为需要的指针类型
官方案例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int input, n;
int count = 0;
int* numbers = NULL;
int* more_numbers = NULL;
do {
printf("Enter an integer value (0 to end): ");
scanf("%d", &input);
count++;
more_numbers = (int*)realloc(numbers, count * sizeof(int));
if (more_numbers != NULL) {
numbers = more_numbers;
numbers[count - 1] = input;
}
else {
free(numbers);
puts("Error (re)allocating memory");
exit(1);
}
} while (input != 0);
printf("Numbers entered: ");
for (n = 0; n < count; n++) printf("%d ", numbers[n]);
free(numbers);
return 0;
}
补充说明:以下代码的注释为使用realloc的细节
int main(){
int* p = (int*)malloc(40);
if (p == NULL)
return 1;
int i = 0;
for (i = 0; i < 10; i++){
*(p + i) = i;
}
int* ptr = (int*)realloc(p, 80);
if (ptr != NULL){
p = ptr;
ptr = NULL;
}
for (i = 10; i < 20; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
对于realloc的内存大小开辟也是有注意的地方,因为realloc开辟的也是一段连续的空间,当空间足够时,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。但当空间不足时,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。画图示例如下:
下面对此细节进行我们在通过一个临时的变量保存返回地址时,需要记得把地址赋值到原指针,不然当内存不足以连续追加开辟时,原指针被释放,会出现一些出乎意料的问题。
二. 动态内存开辟常见错误
2.1 对于NULL指针的解引用操作
void test(){
int* p = (int*)malloc(INT_MAX);
*p = 20;
free(p);
}
查看以上代码,malloc内存时可能会返回空指针,这时候由于空指针是不可以姐应用的,因此会发生错误,造成对NULL指针解引用的误操作,正确修改代码如下:
void test()
{
int* p = (int*)malloc(INT_MAX);
if (p == NULL) {
perror("malloc");
return;
}
*p = 20;
free(p);
}
2.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;
}
free(p);
}
以上代码开辟了10个int类型内存空间的内存大小,在访问时,出现了访问11个int类型大小的内存空间,发生了内存越界访问的错误,这样会使程序发生错误。正确修改如下:
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;
}
free(p);
}
2.3 对非动态开辟内存使用free释放
void test(){
int a = 10;
int* p = &a;
free(p);
}
以上代码对非动态开辟的空间进行了free内存释放,根据对free的标准来看,free是未定义的,因此程序会对此出现错误。
2.4 使用free释放一块动态开辟内存的一部分
void test(){
int* p = (int*)malloc(100);
p++;
free(p);
}
以上代码对p通过自增操作,发生指针的偏移,不再指向动态开辟空间的一部分。当我们对其进行释放时,就是动态开辟内存的一部分进行释放时,会使程序崩溃,出现错误。一般做法是通过一个临时指针来完成操作,而不对原指针直接拿来使用。
2.5 对同一块动态内存多次释放
void test(){
int* p = (int*)malloc(100);
free(p);
free(p);
}
以上代码对已经释放的p指针进行了两次释放,这时相当于释放了一个野指针,同2.3出现的问题一致,会导致程序崩溃,而处理这种问题的方法就是养成良好的习惯,便是在每次释放完后置指针为空,这样free对空指针的使用是什么都不做,从而避免问题的出现。
2.6 动态开辟内存忘记释放(内存泄漏)
int* test(){
int *p = (int*)malloc(40);
return p;
}
int main(){
int *ptr = test();
return 0;
}
内存泄露为动态内存开辟的非常常见也是比较严重的问题,它是在我们开辟空间后,没有做到及时的释放。如以上代码,在申请内存空间后,并没有对内存进行返回,这样就造成了内存泄露。内存泄露的问题在一个小程序上体现不明显,可是当我们为一个服务器时,长期处于运行状态,若内存一直泄露,将会使得内存一直占用,直到最后发生卡死。因此一定要记住释放内存。正确示例:
int* test(){
int *p = (int*)malloc(40);
return p;
}
int main(){
int *ptr = test();
free(ptr);
ptr = NULL;
return 0;
}
三. 经典例题
3.1 练习1
void GetMemory(char* p) {
p = (char*)malloc(100);
}
void Test(void) {
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
请问运行Test函数会有什么样的结果?
解析:在使用GetMemory时,传递的是形式参数,而不是通过指针的方式将地址指向开辟好的空间,因此str指针指的地址仍然是NULL,再对str进行strcpy函数,将会发生断言,程序会出错。而关于在本题的内存泄露问题等就不先讨论,示意图如下:
正确代码书写应该为传递指针的方式,函数参数更改为二级指针,用于接收一级指针的地址,这时为地址传递,而不是拷贝构造,而是对str作用。代码如下:
void GetMemory(char** p) {
*p = (char*)malloc(100);
}
void Test(void) {
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
3.2 练习2
char* GetMemory(void) {
char p[] = "hello world";
return p;
}
void Test(void) {
char* str = NULL;
str = GetMemory();
printf(str);
}
请问运行Test函数会有什么样的结果?
解析:在函数GetMemory为定义一个p数组,赋值为一个字符串,最后返回给指针str,但是在函数栈帧的创建与销毁中,我们知道,函数会出栈,因此就不会有p数组的内容,因此str接受之后变成了一个野指针,可能会导致程序输出错误。
再看一下相似程序练习:
int Test1() {
int a = 10;
return a;
}
int* Test2() {
int a = 10;
return &a;
}
int main() {
int temp1 = Test1();
int* temp2 = Test2();
printf("%d\n",temp1);
printf("%d\n", *temp2);
}
解析:对于temp1,来说输出值为10,因为在函数栈帧出栈时会将a的值存到寄存器中,而temp2解引用后输出的值为随机值,因为函数栈帧出栈后,空间就被释放了,不再使用。
3.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);
}
请问运行Test函数会有什么样的结果?
解析:这个函数与练习1的改进相似,通过指针传递地址,完成堆内存开辟的地址传递,从而开辟空间。输出结果为hellow
3.4 练习4
void Test(void) {
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL){
strcpy(str, "world");
printf(str);
}
}
int main() {
Test();
}
请问运行Test函数会有什么样的结果?
解析:以上代码对开辟空间后的str赋值hellow后,进行空间释放,释放之后对其再次进行strcpy,这时会出现程序错误,因为这时的str为野指针。而要对其进行正确修改则需要养成对空间释放后,对指针赋空。这样就不会进行对野指针的赋值。
补充:
- 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
- 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!
|