前言:
在前面我们已经学习动态分配内存,今天我们就来做一做它的几道经典例题,加深巩固我们所学的知识。
知识复习:动态内存管理(1)_从前慢,现在也慢的博客-CSDN博客
题目1:
下面代码存在什么问题,请你指出问题并修改。
#include<stdio.h> #include<stdlib.h> #include<string.h> void GetMemory(char* p) { p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); } int main() { Test(); return 0; }
分析:
1、程序从main开始,运行之后调用Test函数;
2、进入Test函数,①先创建了一个str的字符指针,并赋初值为NULL;②调用GetMemory函数,以值传递的方式将str传给GetMemory(值传递,形参只是实参的一份临时拷贝,形参的改变不影响实参);
3、进入GetMemory函数,①p的初始值为NULL(值传递);②malloc在堆区申请开辟100个字节的连续空间,并将malloc的返回值赋值给p(①忘记判断malloc是否开辟成功;②没有自己free释放申请的动态内存,如果程序一直运行造成内存泄漏);因为函数类型是void,所以代码执行完,直接返回到test函数中的GetMemory调用;
4、GetMemory调用完继续向下执行,调用strcpy函数,但是目标空间的实参为NULL(值传递形参不影响实参),造成了strcpy形参的非法访问,所以程序出错,不再向下执行。
图示:
问题归纳:
问题1:
GetMemory是传值调用,str传给p的时候,p是str的临时拷贝,有自己独立的空间,当GetMemory函数内部申请完空间后,申请空间的起始地址放在p中,str依然是NULL。当GetMemory函数返回之后。执行strcpy函数拷贝时,strcpy的目标空间形参为NULL,造成非法访问内存。
解决方案:
①传址调用(形参影响实参);②通过函数返回值得到。
问题2:
GetMemory函数内部只动态申请了内存,在Test函数中使用,①但是并没有将malloc的返回值传给Test,②在使用动态内存前没有判断开辟是否成功,③使用完并没有释放空间,如果程序一直运行,会内存泄漏。
解决方案:
①使用动态开辟的内存前,判断是否开辟成功;②使用完之后记得free释放。
代码修改1: 通过传址调用,可以间接的得到malloc开辟空间的起始地址,使用前判断是否开辟成功,使用完记得释放
#include<stdio.h> #include<stdlib.h> #include<string.h> //GetMemory内部进行了malloc操作,在Test函数中使用这块空间, //通过地址传递,可以得到malloc开辟空间的起始地址,记得在Test函数释放 void GetMemory(char** p) { *p = (char*)malloc(100); } void Test(void) { char* str = NULL; //调用GetMemory,动态开辟100个字节的空间(传址调用,形参影响实参) GetMemory(&str); //判断malloc是否开辟成功 if (NULL == str) { //开辟失败,打印错误信息,并退出 perror("malloc"); return ; } //将hello world拷贝到malloc开辟的空间 strcpy(str, "hello world"); //打印拷贝后的内容 printf(str); //使用完动态开辟的内存,free释放 free(str); //free不会改变str的值,防止非法访问内存,将其设置为NULL str = NULL; } int main() { Test(); return 0; }
代码修改2:通过函数的返回值得到malloc开辟空间的起始地址,使用完记得释放
#include<stdio.h> #include<stdlib.h> #include<string.h> //GetMemory内部进行了malloc操作,在Test函数中使用这块空间, //通过返回其malloc的返回值可以得到malloc开辟空间的起始地址,记得在Test函数释放 char* GetMemory() { char* p = (char*)malloc(100); return p; } void Test(void) { char* str = NULL; //调用GetMemory,动态开辟100个字节的空间 str = GetMemory(); //判断malloc是否开辟成功 if (NULL == str) { //开辟失败,打印错误信息,并退出 perror("malloc"); return; } //将hello world拷贝到malloc开辟的空间 strcpy(str, "hello world"); //打印拷贝后的内容 printf(str); //使用完动态开辟的内存,free释放 free(str); //free不会改变str的值,防止非法访问内存,将其设置为NULL str = NULL; } int main() { Test(); return 0; }
题目2:
下面代码存在什么问题,请你指出问题并修改。
#include<stdio.h> char* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; }
运行结果:
打印结果为什么不是hello world呢?(小知识:0xcccc打印就是烫)
分析:
1、程序从main开始,运行之后调用Test函数;
2、进入Test函数,①先创建一个字符指针变量str,并赋初值为NULL;②调用GetMemory函数,(因为形参为void,所以不传参),将其返回值赋值给str;
3、进入GetMemory函数,定义了一个局部变量的数组(因为在栈区申请的空间),返回数组的数组名,即返回数组首元素地址(因为数组在该函数的函数栈帧创建,函数执行结束后会自动释放,所以返回了的数组首元素地址已经没有意义了,再通过该地址去访问空间就会造成非法访问内存问题。);函数执行完,直接回到Test函数中的GetMemory调用。
4、GetMemory调用完将返回值赋值给str,继续向下执行,调用printf,但是传的实参str指向的那块空间已经还给操作系统,所以造成非法访问内存,打印的就是烫烫……
图示:
问题归纳:
问题:
return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。(自动销毁就是把空间还给操作系统,空间返回给了操作系统,再使用空间就会造成非法访问内存,所以返回的指针或者引用就没有实际意义了。)。
解决方案:
1、将局部变量改存在静态区(存在静态区的数据,创建好后,直到程序结束才释放):
(1)关键字static修饰局部变量,被static修饰的局部变量,改变了局部变量的存放位置,静态局部变量存放在静态区。
(2)使用字符指针指向常量字符串,常量字符串存储在静态区。
2、将局部变量拷贝到堆区(存在堆区的数据,创建好后,①程序结束后系统回收;②free自己回收)
代码修改1:关键字static修饰局部变量
#include<stdio.h> //创建一个静态局部数组变量,返回其首地址 char* GetMemory(void) { //静态局部变量:存放在静态区,它的生命周期变长,直到程序结束才释放 static char p[] = "hello world"; //返回字符串的首地址 return p; } void Test(void) { char* str = NULL; //调用GetMemory函数,得到字符串的首地址 str = GetMemory(); //打印字符串 printf(str); } int main() { Test(); return 0; }
代码修改2:使用字符指针指向常量字符串
#include<stdio.h> //创建一个常量字符指针,指向一个常量字符串,返回这个字符指针 const char* GetMemory(void) { //常量字符串存储在静态区,直到程序结束才释放 //常量字符串出现在表达式中,这个常量字符串的值就是首字符的地址 const char* p = "hello world"; //返回字符串的首地址 return p; } void Test(void) { const char* str = NULL; //调用GetMemory函数,得到字符串的首地址 str = GetMemory(); //打印字符串 printf(str); } int main() { Test(); return 0; }
代码修改3:将局部变量拷贝到堆区
#include<stdio.h> #include<stdlib.h> #include<string.h> //GetMemory内部进行malloc操作,并赋初值“hello world”,之后在Test中打印 //通过返回其malloc的返回值得到“hello world”的首地址,记得在Test中释放 char* GetMemory(void) { char p[] = "hello world"; //计算数组p的大小 int sz = sizeof(p); //开辟sz个字节的空间 char* ptr = (char*)malloc(sz); //判断是否开辟成功 if (NULL == ptr) { //打印错误信息 perror("malloc"); exit(0); } //将数组p拷贝到动态内存中 strcpy(ptr, p); //返回malloc的返回值 return ptr; } void Test(void) { char* str = NULL; //调用GetMemory函数,得到malloc的返回值 str = GetMemory(); //打印字符串 printf(str); //释放 free(str); str = NULL; } int main() { Test(); return 0; }
题目3:
下面代码存在什么问题,请你指出问题并修改。
#include<stdio.h> #include<stdlib.h> #include<string.h> void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); } int main() { Test(); return 0; }
分析:
1、程序从main开始,运行之后调用Test函数;
2、进入Test函数,①先创建一个字符指针变量str并为其赋初值为NULL;②调用GetMemory函数第一个参数为地址传递(形参影响实参,解引用可以间接操控实参,得到开辟空间的起始地址),第二个参数为值传递(形参不影响实参,决定开辟空间的大小);
3、进入GetMemory函数,GetMemory函数内部开辟好空间,在Test中使用开辟的空间,记得释放,因为函数类型为void,所以执行完之后直接回到Test函数中的GetMemory调用;
4、GetMemory调用完,继续向下执行,①调用strcpy将“hello”拷贝到malloc开辟的空间(使用malloc开辟的空间前完了判断是否开辟成功);②调用printf打印开辟空间的内容,打印完后直接回到main函数中(忘记使用完动态空间之后,free释放,如果程序一直运行,内存泄漏)。
问题归纳:
问题1:
动态开辟的空间,使用前,忘记判断是否开辟成功,造成非法访问。
解决方案:
使用前使用if语句判断其返回值是否为空指针。
问题2:
动态开辟的空间,忘记了释放内存(如果程序一直运行),造成内存泄漏。
解决方案:
1、动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则就会有错误。(使用完动态开辟的内存使用完一定要释放,并且释放完还要将指针设置为NULL,防止非法访问内存。)。
2、使用动态开辟的空间,最好注释空间是否释放
代码修改:
#include<stdio.h> #include<stdlib.h> #include<string.h> //GetMemory函数通过第一个参数间接返回开辟空间的地址, // 通过第二个参数确定开辟空间的大小。 // 在GetMemory函数内部创建好空间,在Test中使用,记得释放空间 void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void Test(void) { char* str = NULL; GetMemory(&str, 100); //判断malloc是否开辟成功 if (NULL == str) { return; } //使用 strcpy(str, "hello"); printf(str); //释放 free(str); str = NULL; } int main() { Test(); return 0; }
题目4:
下面代码存在什么问题,请你指出问题并修改。
#include<stdio.h> #include<stdlib.h> #include<string.h> void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }
分析:
1、从main开始,运行后调用Test函数;
2、进入Test函数,①malloc动态申请100个字节的空间,并将返回值赋给str;②调用strcpy函数将“hello”拷贝到动态申请的空间中(使用前未判断malloc是否申请空间成功);③使用完free释放malloc的空间(free释放完空间之后,不会改变指针str的值,未将其设置为NULL,后面使用到str还能找到释放的那块空间,造成非法访问内存);④free释放完,str的值不为空,条件为真,执行if控制的语句,调用strcpy和printf,使用到了已经释放的空间,造成非法访问空间。
问题归纳:
问题1:
动态开辟的空间,使用前,忘记判断是否开辟成功,造成非法访问。
解决方案:
使用前使用if语句判断其返回值是否为空指针。
问题2:
free释放了内存,却仍然继续使用它(free释放完内存,不会改变指针的指向),造成非法访问内存。
解决方案:
free释放完内存,将指针设置为NULL,防止非法访问内存。
代码修改:
#include<stdio.h> #include<stdlib.h> #include<string.h> void Test(void) { char* str = (char*)malloc(100); //使用前判断是否开辟成功 if (NULL == str) { return; } //使用 strcpy(str, "hello"); //释放 free(str); //free释放并不会改变指针指向,防止非法访问内存,将其设置为NULL str = NULL; if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }
加油站:C/C++程序内存分配的区域:
内存图:
C/C++程序内存分配的几个区域:
1、栈区 (stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中。效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区(heap):动态内存分配。一般由程序员分配释放,生存期由我们决定,非常灵活,但问题也最多。若程序员自己不释放,程序结束时可能由OS回收。分配方式类似于链表。
3、数据段(静态区):内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,程序结束后由系统释放。例如全局变量、static变量。
4、代码段(静态区):存放函数体(类成员函数和全局函数)的二进制代码。也可能包含一些只读的常数变量,如常量字符串。
在之前我们就学过了static关键字修饰的局部变量,现在我们重新回顾:
1、普通的局部变量是放在栈区上的,这种局部变量进入作用域创建,出了作用域释放。
2、但是局部变量被static修饰后,这种变量就放在静态区,放在静态区的变量,创建好后,直到程序结束才释放。
3、本质上:static的修改改变了局部变量的存储位置,因为存储位置的差异,使得执行效果不一样。
注意:被static修饰是不影响作用域的!!!但是生命周期发生了变化,变长了。
1、栈区:变量空间进入作用域创建,出作用域就释放。
2、堆区:变量空间由程序员自己决定,或者程序结束由系统回收。
3、静态区:变量空间创建好之后,直到程序结束才释放。