昨天有同学(初学指针)在练习单链表和二叉树的时候,程序老是崩溃,或者得不到正确结果,于是向我求助。问题就出在指针的参数传递上,没传好指针导致内存混乱,其他代码基本全对。这个错误十分可惜。故在此我想做个记录,可能显得十分基础。
如果函数的参数是普通的一级指针,那么就意味着你只能使用指针、改变指针指向或者改变指向的目标变量。不能试图通过这个指针来申请内存。
void getMemory(int *p)
{
p = (int *)malloc(sizeof(int) * 10);
}
void func()
{
int *num = NULL;
getMemory(num);
// 指针p依旧是NULL指针
}
这是一个非常常见的错误。当发生函数调用的时候,函数的形参总是实参的一个copy(副本),就算是指针也是如此。例如上述的代码可以看成:在调用函数的时候,把num指向的目标地址赋值给p指针。现在num指针和p指针为两个完全不同的实体,它们同时指向了同一个目标地址。通过下面的代码可以验证这个说法。
#include <iostream>
using namespace std;
void getMemory(int *p)
{
printf("target address of pointer p is: %x, its value is: %x\n", p, *p);
printf("but address of pointer p itself is: [%x]\n", &p);
}
int main()
{
int val = 4;
printf("address of val is: %x\n", &val);
int *num = &val;
printf("target address of pointer num is:%x, its value is:%x\n", num, *num);
printf("address of pointer num itself is: [%x]\n", &num);
getMemory(num);
// printf("%d %d %d\n", *p, *(p+1), *(p+2));
getchar();
return 0;
}
这样一来的话,形参上的p指针是一个新的指针,它的指向和原来的num指针一样,所以可以正常的修改目标地址的变量和进行输出。
现在函数中的p指针申请了新的内存,现在就很明显了:它的这个行为和原来的指针没有一点关系,那是p指针自己的事情。
解决这个错误的方法有很多,其实怎么做都行,只要搞清楚申请过来的内存到底是给谁用的就行了。
方法一:利用指向指针的指针
void getMemory(int **p)
{
*p = (int *)malloc(sizeof(int) * 10);
}
void func()
{
int *num = NULL;
getMemory(&num);
}
为什么这样可以呢?我们来分析一下。**p是一个指向指针的指针,前面说过,num和p指向了同一个目标,那么num和p都能读取或是修改目标。
既然这样的话,那我直接拿实参,也就是num指针,作为我的目标。
如果你了解&p, p, *p的话,int **p其实一点也不神秘。还记得int *p中的int是什么意思吗?既然它是指向指针的指针,那么理所应该就应该这样写了:int* *p,因为它自身是指针,而且指向的是一个int*类型的指针。不过我非常不建议写成int* *p,毕竟二级指针和一级指针很有不同,为了突出二级指针应该写成:int **p(有些人写成int** p)。
完全不需要搞得那么混乱,二级指针p的目标是个指针,仅此而已。操作二级指针的目标,即*p,就相当于在操作指针num,即*p == num。*num是什么意思呢?在num前面加上了个*,表示想要操作num指针所指向的对象,很遗憾上图中num指向的是NULL,不太好说明。又因为num == *p,所以*num == *(*p) == **p。
相信看了上图你应该对*p和**p很明确了。
好了,回过头来看看代码:
*p = (int *)malloc(sizeof(int) * 10);
*p指向的是num,也就是num自身。现在让num自己去申请内存,而不是让别人代为申请,当然就正确了:)
方法二:直接申请一块内存,然后把地址返回给指针
申请内存其实很简单,我们通常这样写:
int *pBuffer = (int *)malloc(sizeof(int) * 10);
意思是说,让操作系统给我安排一块位置,然后把这块位置的地址告诉我。就是把这块新内存的地址返回给pBuffer这个指针,这样pBuffer指向这块内存以后就可以进行操作了。既然这样的话,那我们就让num指针直接做这件事情,代码如下:
int* getMemory()
{
int *p = (int *)malloc(sizeof(int) * 10);
return p;
}
void func()
{
int *num = NULL;
num = getMemory();
}
这段代码应该很容易理解。函数里p申请了一块内存,然后p指向了这快内存,最后p把新内存的地址返回给了num指针。
这是安全的,前面说过,p指针自身是函数的局部变量,存放于栈中,但p指针申请来的内存是存放在堆中的,所以函数结束后p会被释放,但这内存块不会。如果内存块也存在栈中那就不行了。
知道了上述原理依旧是不够的,我们再来看看如下代码:
char* getString()
{
char *s = "hactrox";
return s;
}
void func()
{
char *str = NULL;
str = getString();
}
getString函数中的字符串"hactrox"存放在文字常量区,顾名思义,既然是"常量"区,肯定是只读的,那么换句话说任何对这个字符串的修改都是不允许的。
上述代码虽然是完全正确的,但同时也埋下了不小的隐患。只要str指针在任何时候试图修改这个字符串,就会导致程序奔溃。
方法三:使用引用
首先要说明一下引用是什么概念。C语言没有引用的概念,&符号只作为取地址用。引用是C++里的概念,很容易就把引用和指针搞混了。引用就是别名。
先来看一个普通到不能再普通的语句:
int value = 10;
这个int型的变量value,显然不是指针,它就是个变量名,代表了这块内存的名字。
int *p = &value;
int &nickname = num;
第二行行语句的意思是说,我创建了一个新的东西,这个东西是num变量的别名,nickname这个东西既不是变量也不是指针也不是副本,更不是字符串,那它总得有个名字吧?就叫它引用好了。引用和指针的区别是,指针自身就是个实体,value手中掌握着10这个数字,指针指向了value手中的数字,他们的"共同目标"是这个数字10。而引用则完全不一样,对引用来说,根本没有"共同目标"这一说法,因为引用本身就是value他自身,是value这个变量的另一个名字。
引用和指针的一些区别如下:
1. 可以先创建指针,然后再指向一个目标。而引用在创建的时候就必须指定目标。总得现有这个人,然后这个人才有昵称吧。
2. 相对于引用来说,指针非常自由,指针可以指向一个目标也可以指向NULL。但是却不能有NULL引用。这不仅违背了引用的设计初衷,而且逻辑上也说不过去。一个事物本身就不存在了,哪来的昵称?就算能有昵称的话,那它本来的名字叫什么?
3. 一旦为一个变量设立引用以后,这个引用就和这个变量绑定了。换句话说,就是这个引用就不能指向别的变量上。所以引用就相当于变量的属性。试想,给一个人取了绰号以后,总不可能用这个绰号去称呼另一个人吧?如果是这样的话,那么肯定有很多人搞不清楚到底谁叫这个绰号,系统也是。所以当然就不行了。
#include <iostream>
using namespace std;
void changeByReference(int &a)
{
a = 5;
}
void changeByPointer(int *p)
{
*p = 8;
}
int main()
{
int val = 5;
changeByReference(val);
printf("%d\n", val);
int num = 12;
changeByPointer(&num);
printf("%d\n", num);
getchar();
return 0;
}
使用引用的话,就可以像操作一个普通变量一样方便,上述代码中的引用a并没有带上*,而指针p在使用的时候,要带上个*p。
使用引用的还有一个好处是安全,C++的指针太强大了,一旦没用好就会造成很多问题。而引用的功能则弱得多,在不需要那么强大功能的时候使用引用显得安全。
现在回过头来看看使用引用如何来申请内存:
void getMemory(int *&p)
{
p = (int *)malloc(sizeof(int) * 10);
}
void func()
{
int *p = NULL;
getMemory(p);
}
指针的引用就代表了指针自身,所以使用引用能正确申请到内存,这个应该没什么疑问。剩下的问题就是:指针的引用怎么表示?通过上述代码我们知道了是*&p。
我们来看看*&p和&*p的区别。
先来解析一下int类型的引用。因为引用的类型肯定是目标变量的类型,所以肯定是int,又因为规定&为引用符,所以int型的引用就很好写了。*&p == *(&p),同样的,对于指针变量,其引用的类型肯定也是指针类型,所以上述代码中的引用变量肯定是int*类型的,指针是p,引用一下,就是&p。好了结果出来了,是int* &p,那不就是int *&p了。
那么&*p又是什么呢?&*p == &(*p),*p是指针所指向的目标,&这个目标变量,那么结果就是"取指针指向的目标的地址",想想看,还有什么地方存了这个地址?当然是指针自身内存上的值了。假设p指针指向变量num,那么&*p、p、&num是等价的。知道了*和&的概念后,指针就可以随意玩转了:
#include <iostream>
using namespace std;
int main()
{
int num = 888;
int *p = #
printf("%d %d %d\n", *(*&p), *&num, *(&*p));
printf("%x %x %x\n", &num, &*p, *&p);
getchar();
return 0;
}
可能你会问,上面代码中的*&p和&*p都是p的意思,为什么申请内存的时候只能写*&p而写了&*p就报错了呢?
*&p是引用,引用是一种类型,而&*p是一种取地址的操作,不是类型。就好像int num = 5,在printf里面你可以写num也可以写5,但是在代码里你只能写num而不能写5,因为num是一种类型而5不是。