关于值/引用传递的一点深入理解

关于值传递和引用传递,第一次在考卷之外认识到它的重要性是在去年八月做类分析器的演习项目中,在那之后仔细翻阅了钱能的那本C++教材,觉得它讲得不够彻底,于是又去翻阅了谭浩强的那本著名的C语言教材,基本上能够从本质上认清了值/引用传递,前两天在翻阅Deitel父子的<C# for Experienced Programmers>时再一次巩固了对值/引用传递的认识,遂作此文。

函数(方法)传值,从本质上来说其实是只有一种机制——将实参在栈中生成一个备份,称作”形参”。然后在栈中展开函数,对形参进行操作。函数中所有对形参的更改都不会保留到实参中。比如下面这个常见的例子:


void swap(int, int);

void main()
{
int a=3;
int b=5;
swap(a, b);

cout << “a=” << a << “; b=” << b <<endl;
}

void swap(int var1, int var2)
{
int temp = var1;
var1 = var2;
var2 = temp;
}

在上面的例子中,本意是想通过调用swap函数来更改a,b的值,结果因为函数中只对形参进行操作,当函数调用结束,栈被清空,存储在内存数据段的a,b值没有受到任何影响。

这时,如果对上 面的swap函数进行如下调整:

void swap(int *p1, int *p2)
{
int temp = (*p1);
(*p1) = (*p2);
(*p2) = temp;
}

对应的函数调用也应该改为 swap(&a, &b);再次输出结果就会看出变量a,b的值的确被对调过了。

BUT WHY?

原因很简单,关键就在于更改后的函数签名中的参数已经不再是单纯的int型,而是int型指针——说白了,就是两个内存地址。于是,当swap函数被调用时。变量a和b的地址会各被复制一份用作函数的形参。而在上例的函数体中,进行交换的则是两个地址中所存储的数值。而这一点是相当重要的,因为保存有两个地址的形参仍然会在函数调用结束后从栈中清空,但由于两个地址是数据段的地址,而在形参被清空之前,两个数据段地址实际保存的值已经被交换了(就是变量a,b的地址所保存的值被交换),所以在函数调用结束后可以看出a和b的值被交换过了

由于很多资料对引用传递的机制 表述很含糊,就会读者造成这么一种错觉——如果传给参数的是指针,那么在函数体内操纵的就是实际的指针。因此,下面一种错误就出来了。

void swap(int *p1, int *p2)
{
int *temp = p1;
p1 = p2;
p2 = temp;
}

调用函数的方法依旧为swap(&a, &b);作者本以为函数体内操纵的就是a,b的实际地址,所以预料输出结果应该是正确的。但实际上,它在swap函数内操作的仍旧为a, b地址在栈中的备份。

现在来看看小wing在去年八月所犯下的一个错误:我想要写一个函数来生成一个单链表的头节点,所以写了如下方法(之前已经定义了节点 Node——是一个结构体):

int GetFirstNode(Node *pHead)
{
if (pHead)
{
return 0;
}
else
{
pHead=(Node*)malloc(sizeof (Node));
// Assign the value to the members of Node
… …
return 1;
}
}

当我调用该函数时,我首先让头节点指针pHead为NULL,之后通过int flag = GetFirstNode(pHead)的方式来调用函数。满以为调用结束后,pHead会指向新生成的节点,结果一跟踪却 发现pHead仍旧为 NULL。

当然,现在来看这段代码很容易就能发现问题的所在(这是一段比较危险的代码):由于pHead一开始为空,调用函数时,pHead的形参自然也是为空,之后的代码中却改变了 pHead的值(是个地址)。但是调用结束后,形参被销毁,pHead没有变化。事实上,这段代码中,pHead 没有正确指向预期的地址倒是小时,关键问题是在函数体中调用了malloc在堆上申请了空间后却没有任何指针可以控制它,更不用说去释放这段内存空间。这便造成了烦人的内存泄漏。

实际上,这段代 码可以进行如下修正就可以解决这个问题:

int GetFirstNode(Node **pHeadPoint)
{
if ((*pHead))
{
return 0;
}
else
{
(*pHead)=(Node*)malloc(sizeof (Node));
// Assign the value to the members of Node
… …
return 1;
}
}

实际调用时,假设一开始声明仍旧是Node * pHead = NULL;那么就可以利用 GetFirstNode(&pHead)来让pHead指向新生成的节点。因为指向pHead的指针(即双重指针)在函数体中没有发生变化。

SO…

到现在,我们就可以得出这样一个结论——
值传递 和引用传递在本质上没有区别,都是利用函数的形参。关键在于,如果在函数体中想要改变一个外部变量(可能是个基本数据类型变量,也有可能是一个指针变量)的值,并且希望这种改变能够在函数调用完成之后仍旧被保持。那么,就请将该外部变量的地址作为函数的参数传进去。

另外,由于C#的托管代码中没有指针和取址符&amp;amp;所以C#提供了两个特有的关键字ref和out,前者更像C/C++中的取址符 &,而后者则更能应付在调用函数前没有经过初始化的变量。而C#中需要记住的另一个原则就是,任何class类型的变量其本质都是一个指针——即变量所保存的值其实是一个指向该class实例的地址。

 

Advertisements

About 小wing

☞ INTP星人☞爱猫家 ☞钝感男 ☞Google粉 ☞第70004号维基人 ☞民主自由控 ☞伪技术爱好者 ☞挨踢民工 ☞无证程序员 ☞游戏宅 ☞摇滚乐拥趸 ☞原版CD收藏癖 ☞反对爱国主义
此条目发表在技术分类目录,贴了, 标签。将固定链接加入收藏夹。

5 Responses to 关于值/引用传递的一点深入理解

  1. Lee_vina说道:

    这篇嘛,trop专业,俺们外行就不评论啦~~~
    还是说说跟的贴吧:不去泡吧,好好攒钱,不吸烟,少喝酒,简直是宇宙超级乖宝宝嘛~~~
    明天考听力,居然还有新闻,偶耳朵里除了PUNK还真是钝,练习去了,88~~~

  2. 上文讲的应该是传值调用和传址调用吧,确实是没有本质区别的,而我认为引用应该是不同的
    比如
    [CODE]
    int  GetFirstNode(Node *pHead){    if (pHead)     {        return 0;    }     else     {        pHead=(Node*)malloc(sizeof (Node));        // Assign the value to the members of Node        … …        return 1;    } }
    [/CODE] 

  3. 刚才没写完,就错发了不好意思,这个函数可以通过引用参数来改进

    int  GetFirstNode(Node * & pHead)//一个指针的引用{    if (pHead)     {        return 0;    }     else     {        pHead=(Node*)malloc(sizeof (Node));        // Assign the value to the members of Node        … …        return 1;    } }
    调用时依然可以通过GetFirstNode(pHead),而不需要使用“&pHead“,你可以试一下看

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s