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

4 篇文章 1 订阅
本文介绍了在编程中,尤其是针对初学者,由于指针参数传递不当导致的常见问题。作者分析了三种解决方法:使用指向指针的指针、直接申请内存并返回地址、以及使用引用。文中详细解释了每种方法的原理,并强调了引用的安全性和指针操作的注意事项。
摘要由CSDN通过智能技术生成

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

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

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 = &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不是。

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值