首页 > 代码库 > [编程开发] 由指针传參引发的一点分析

[编程开发] 由指针传參引发的一点分析

昨天有同学(初学指针)在练习单链表和二叉树的时候,程序老是崩溃,或者得不到正确结果,于是向我求助。问题就出在指针的參数传递上,没传好指针导致内存混乱,其它代码基本全对。这个错误十分可惜。故在此我想做个记录,可能显得十分基础。

假设函数的參数是普通的一级指针,那么就意味着你仅仅能使用指针、改变指针指向或者改变指向的目标变量。不能试图通过这个指针来申请内存。

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自己去申请内存,而不是让别人代为申请,当然就正确了:)

方法二:直接申请一块内存,然后把地址返回给指针

申请内存事实上非常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 = http://www.mamicode.com/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 = &num;
    
    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不是。

[编程开发] 由指针传參引发的一点分析