最近在看程序员面试宝典的书中遇到了传递动态内存的问题。
#include <iostream> using namespace std; void GetMemory(char *p,int num) { p=(char *)malloc(sizeof(char) * num); } void Test(void) { char *str=NULL; GetMemory(str,100); //str仍然为NULL strcpy(str,"hello");//运行错误 } int main() { Test(); return 0; }
书上的解释是:在函数GetMemory(char *p,int num)中,*p实际上是主函数中str的一个副本,p申请了内存,只是把p指向的内存地址改变,而str并没有改变,所以str依然没有获得内存,在执行字符串复制时就会出错。而且每次p申请的内存都不会得到释放,最终会造成内存泄露。
不胜理解,查找相关资料发现是值传递和地址传递的问题。
在C语言中函数参数的传递有:值传递,地址传递,引用传递这三种形式。
1、值传递:形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。
典型代码:
#include <iostream> using namespace std; void swap(int p,int q) { int temp; temp=p; p=q; q=temp; //cout<<p<<""<<q<<endl; } int main() { int a=1,b=3; swap(a,b); cout<<a<<endl; cout<<b<<endl; return 0; }
最后输出的结果a=1,b=3;可以看到a、b的值并没有改变,实参a、b在传递参数给形参p、q的时候,本身并不会改变,最后形参的值不会再重新传回给实参,改变的是形参p、q的值,实参a、b不会发生任何改变。注意在函数调用的时候,参数的值传递只是将实参a、b的值传递给p、q,并不是用a、b取代形参p、q进行操作(这就是我们理解的误区)。
2、地址传递:形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。
#include <iostream> using namespace std; void swap(int *p,int *q) { int temp; temp=*p; *p=*q; *q=temp; //cout<<p<<""<<q<<endl; } int main() { int a=1,b=3; swap(&a,&b); cout<<a<<endl; cout<<b<<endl; return 0; }
最后输出的结果a=3,b=1;达到了a、b交换的效果。函数swap(int *p,int *q)中形参p、q均为指针,在函数引用的时候:swap(&a,&b);即相当于p=&a;q=&b;将a、b的地址赋值给了p、q,自然指针p指向了a,q指向了b,对*p,*q进行操作自然也就是对a、b的操作了,所以最后a、b的值进行了交换。这就是地址传递(将a、b的地址传递给形参)。
3、引用传递:形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
#include <iostream> using namespace std; void swap(int &p,int &q)//注意定义处的形式参数的格式与值传递不同 { int temp; temp=p; p=q; q=temp; //cout<<p<<""<<q<<endl; } int main() { int a=1,b=3; swap(a,b); cout<<a<<endl; cout<<b<<endl; return 0; }
最后输出的结果a=3,b=1;达到了a、b交换的效果。
此时我们要注意与值传递的区别,函数定义的时候swap(int &p, int &q);其中标注了形参为引用,在调用函数swap(a,b)的时候进行参数传递,此时相当于&p=a;q=&b;即p为a的引用,q为b的引用,此时p、q就相当于a、b的别名,对p、q的操作也就是直接对a、b的操作。
回到最上面的代码处,在调用函数GetMemory(str,100)的时候,str为指针,存储的为地址,进行参数传递的时候将str的地址传递给指针p,最开始指针p指向NULL,使用malloc开辟新的内存后,p就指向了新开辟的内存空间,但是实参str并没有改变,还是指向了NULL,p指向新开辟的内存空间后也没有释放,如此就造成了内存泄露,导致错误。
对于修改方法有三种修改方法。
1、使用引用。
#include <iostream> #include <malloc.h> #include <string.h> using namespace std; void GetMemory(char *&p,int num) { p=(char *)malloc(sizeof(char) * num); } void Test(void) { char *str=NULL; GetMemory(str,100); strcpy(str,"hello");// cout<<str<<endl; } int main() { Test(); return 0; }
输出结果为hello,符合预期期望。在函数定义时GetMemory(char* &p,int num),将指针p定义为引用,在参数传递的时候&p=str,即p为指针str的别名,对指针p的操作也就是对str的操作,所以指针str也会指向新开辟的内存空间。
2、使用函数返回值。
#include <iostream> #include <malloc.h> #include <string.h> using namespace std; char *GetMemory(char *p,int num) { p=(char *)malloc(sizeof(char) * num); return p; } void Test(void) { char *str=NULL; str=GetMemory(str,100); strcpy(str,"hello"); //cout<<&str<<endl; cout<<str<<endl; } int main() { Test(); return 0; }
输出结果为hello,符合预期期望。调用函数GetMemory(str,100);的时候返回的是指针p,再将str=GetMemory(str,100);进行赋值,相当于str=p,即str也指向了新开辟的内存单元,不再指向NULL,可以对其进行操作,最后输出我们所需要的结果。
3、使用二级指针即指向指针的指针。
#include <iostream> #include <malloc.h> #include <string.h> using namespace std; void GetMemory(char **p,int num) { *p=(char *)malloc(sizeof(char) * num); } void Test(void) { char *str=NULL; GetMemory(&str,100); strcpy(str,"hello"); //cout<<&str<<endl; cout<<str<<endl; } int main() { Test(); return 0; }
输出结果为hello,符合预期期望。函数定义中GetMemory(char **p,int num),p为二级指针,在函数调用GetMemory(&str,100)进行值传递的时候,此时相当于地址传递,指针*p=&str,*p存储的是指针str的地址,我们通过修改*p即是修改了str的地址,将*p指向新开辟的内存空间即是将str的地址指向了新开辟的内存空间,进而str也就指向了新开辟的内存空间,从而达到了动态内存传递的效果。
char *str=NULL;栈中操作的时候,内存为内存为0x28ff2c的地址存储内容为NULL
内存分配方式有三种:
- 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
- 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活。