本文主要对指针和引用的概念、区别以及参数传递三个方面进行阐述。
吴秦的博文C++中的指针与引用写的很经典,所以本文会部分引用,并在引用处说明。
指针及引用概念
指针及引用定义
该定义部分引用自吴秦的博文。
指针的权威定义
也即,对于一个类型T,T* 就是指向 T 的指针类型
,也即一个 T* 类型的变量能够保存一个 T 对象的地址,而类型 T 是可以加一些限定词的,如 const、volatile 等等。见下图,所示指针的含义:
引用的权威定义
也即,引用是一个对象的别名
,主要用作函数参数和返回值类型,符号 X& 表示 X 类型的引用。见下图,所示引用的含义:
从内存角度来看指针和引用
指针是有类型的
通过上边的定义我们知道指针的类型是 T*,也就是说指针实际上是有类型的
。那指针为什么是要有类型的?这只能从内存的角度来回答:
一个非空指针(仅仅)指向了内存中的某个地址。如果该指针不清楚它所指向的数据类型(T)是什么,它不可能知道每次读取或者写入的内存块大小,这样必然导致内存操作错误。
引用的本质是内存块的别名
上边的定义说到引用是一个对象的别名。如果从内存的角度来看,引用是该对象所代表的内存块的别名。
该内存块的变化会导致引用的变化,反之亦然。
那就有一个问题了,引用本身占内存吗?
答案请见下文。
指针及引用区别
吴秦的博文总结了一下几个区别:
引用不可以为空,但指针可以为空。
前面也说过了引用是对象的别名,引用为空——对象都不存在,怎么可能有别名!故定义一个引用的时候,必须初始化。因此如果你有一个变量是用于指向另一个对象,但是它可能为空,这时你应该使用指针;如果变量总是指向一个对象,i.e.,你的设计不允许变量为空,这时你应该使用引用。引用不可以改变指向,对一个对象"至死不渝";但是指针可以改变指向,而指向其它对象。
说明:虽然引用不可以改变指向,但是可以改变初始化对象的内容。例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。引用的大小是所指向的变量的大小,因为引用只是一个别名而已;(32 位系统中)指针是指针本身的大小,4个字节。
引用比指针更安全。
由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free 掉一个指针之后,别的指针就成了野指针)。
指针及引用实现
下边的例子简化自吴秦的博文,并在 64 位 Linux 系统下编译。
1 |
|
对上边代码编译g++ test.cpp
之后,再反汇编objdump -d a.out
,得到 main 函数的一段汇编代码如下:
1 | 00000000004006e0 <main>: |
上述汇编可用图表示如下:
也就是说,引用本质上跟指针是一样的,是 C++ 为了简化指针操作,对指针进行了封装,从而产生引用类型。引用本身也需要占用内存。
指针传递及引用传递
先来看一个例子:
1 |
|
想必大家都猜到了,只有 swapByRef 函数才能够成功将两个数交换。下边我们来看看原因。
值传递
swapByVal 函数的反汇编代码如下:
1 | (gdb) disassemble swapByVal(int, int) |
从汇编代码可以看出,swapByVal 只是将传进来的值在该函数的栈空间进行了交换,没有交换外边的变量(内存)之间的值。函数一旦返回,整个栈空间也就清空了。
指针传递
swapByPtr 函数的反汇编如下:
1 | (gdb) disassemble swapByPtr(int*, int*) |
汇编代码基本跟“值传递”的一样,没有交换外边变量(内存)之间的值。
引用传递
swapByRef 函数的反汇编如下:
1 | (gdb) disassemble swapByRef(int&, int&) |
从以上汇编代码可以看出,该函数实际传进来的是外部变量的地址,而且都是对他们实际内存的值进行交换
,所以最终能够将外部变量的值进行交换,即使最后栈空间清空。
小结
吴秦在博文最后引用别人的博文C++中引用传递与指针传递区别(进一步整理)作为指针传递和引用传递的一个小结:
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。
值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(?)
引用传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
对于第 2 点,我觉得没有问题,因为前边的实验可以证明。但对第 1 点(打?
号那句)其实是有问题的。请看下边例子:
1 | void swapByPtr2(int *p, int *q) |
该函数能够实现值的交换,而且swapByPtr2 的汇编代码与 swapByRef 的一模一样。也就是说,这两个函数实际上是等价的。
这就说明,指针传递虽然是值传递,但究竟有没有影响主调函数的实参是取决于我们对指针的操作,即有没有"真正"操作这些实参的"内存"。