昨天有同学(初学指针)在练习单链表和二叉树的时候,程序老是崩溃,或者得不到正确结果,于是向我求助。问题就出在指针的參数传递上,没传好指针导致内存混乱,其它代码基本全对。这个错误十分可惜。故在此我想做个记录,可能显得十分基础。
假设函数的參数是普通的一级指针,那么就意味着你仅仅能使用指针、改变指针指向或者改变指向的目标变量。不能试图通过这个指针来申请内存。
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 ", p, *p); printf("but address of pointer p itself is: [%x] ", &p); } int main() { int val = 4; printf("address of val is: %x ", &val); int *num = &val; printf("target address of pointer num is:%x, its value is:%x ", num, *num); printf("address of pointer num itself is: [%x] ", &num); getMemory(num); // printf("%d %d %d ", *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自己去申请内存,而不是让别人代为申请,当然就正确了:)
方法二:直接申请一块内存,然后把地址返回给指针
申请内存事实上非常easy,我们通常这样写:
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(); }这段代码应该非常easy理解。函数里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++里的概念,非常easy就把引用和指针搞混了。引用就是别名。
先来看一个普通到不能再普通的语句:
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 ", val); int num = 12; changeByPointer(&num); printf("%d ", 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 ", *(*&p), *&num, *(&*p)); printf("%x %x %x ", &num, &*p, *&p); getchar(); return 0; }可能你会问,上面代码中的*&p和&*p都是p的意思,为什么申请内存的时候仅仅能写*&p而写了&*p就报错了呢?
*&p是引用,引用是一种类型,而&*p是一种取地址的操作,不是类型。就好像int num = 5,在printf里面你能够写num也能够写5,可是在代码里你仅仅能写num而不能写5,由于num是一种类型而5不是。