取地址符&做函数形参?
C语言强大且危险
引入
这个问题花去了整整一天的研究。
先看一段严蔚敏的《数据结构》中栈的例程:
这里面的&S第一遍看的时候想当然的认为是取了SqStack结构体的S的地址,没有细想。然后又看到了这句。
// platform: VC++6.0
Status Pop(SqStack &S, SElemType &e); //取地址符?
1
2
我开始突然发现,这真的是取地址符吗,对照了我自己写的程序,仔细推敲发现不太对。
仔细看这里的&e,如果这是个整型的栈,那么SElemType就是int,那么这里就等于:
Status Pop(SqStack &S, int &e); //很奇怪
1
类比的疑问
我们都知道:
int a,b; /* 定义了两个整型的指针 */
int **a, *b; / 定义了整型指针的指针 */
1
2
那么难道说是
int &e; // 定义了以一个整型数为地址的变量e?
1
仔细看下接下来的函数定义:
显然这里可以看出由于top指针指向的是SElemType类型,所以e是SElemType类型的。所以以上类比显然是不对的。
C/C++中的引用参数
查找了很多的资料发现,这个实际上是C++里的形参符号,必须要在跟在数据类型的后面使用。在函数内部对形参的操作都等同于直接操作原变量。
先说形参和实参
学过C语言的都知道,一个经典的例子是关于写一个交换两个变量a,b的值的函数:
// “形参不等于实参”的经典错误示范
void swap(int a, int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
void main()
{
int a = 1, b = 2;
swap(a,b)l
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
我们都知道把a,b作为形参传入时,会临时的分配形参空间读取实参a,b的值存入,这里的形参a,b实际地址是不同于原来的实参。
形象的说,实参a是一份讲义,你在调用函数的时候,函数就像学生一样去要讲义(传递的实参)。函数向系统要了张白纸(栈区空间),然后把这篇文章抄了一份拿去用了,取名也叫作a。然后他怎么修改都不会
继续准确点说, 在程序运行的时候会分配一个全局区,我们这里说的a,b实际上属于全局变量,存储在全局区,也有的地方叫做静态区。而这里的形参存储在栈区,仅仅是保存了全局量的值,所以所有对形参a,b的操作都和静态区的a,b无关。
这里实参传递给形参的过程叫做值传递。
附:C/C++程序的内存分配知识
一个由C/C++编译的程序占用的内存分为以下几个部分 :
1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。这个空间是公用的,如果没有释放会使得可用堆区空间变小,最好在申请后手动释放。
3、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放
4、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区―存放函数体的二进制代码。
所以我们可以理解为,这里的&e是为了说明e变量不是仅仅的把值传递进了函数内部。
那怎么通过函数操作函数外部的参数呢?
根据C语言学习中标准解法,一是将实参的地址传递进函数中函数中,通过地址直接操作原变量的值;二是利用函数本身的返回。
// 利用指针的经典解法
void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void main()
{
int a = 1, b = 2;
swap(&a,&b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
必须先弄清楚各种地址
要理清引用参数的使用和原理,明白这个&符号和指针的区别,先必须搞清楚数据的地址。
在《操作系统》中,可以得知三种地址的区别:逻辑地址、线性地址和物理地址。
关于这三者的区别可以看这里:
操作系统逻辑地址、线性地址和物理地址
http://www.cnblogs.com/dirichlet/archive/2011/03/18/1987746.html
或者这里
http://blog.csdn.net/geekwill/article/details/52449855
这里用图简单通俗的说下,为了通俗易懂,严格上并不准确:
我们的程序在操作系统中运行的时候,会给我们的程序(进程)在内存中分配一些空间。为了方便说明,这里假设内存是16位地址(实际上32位地址支持4G内存),我们可以看到a的物理地址是0x23。
然后0x2300是什么呢,这个是进程数据段的首地址,一般我们习惯叫做程序运行的入口地址。
像上面的图所示,我们通过&a把a的逻辑地址传递进了函数swap中,然后swap函数通过*a找到a的物理地址,这个是操作系统完成的,其中会经过一些过程,需要先变换为线性地址。
那么我们可以总结:
实际上在C语言中,使用&取地址符取出的是变量的[逻辑地址],就算用汇编进行操作也是一样。变量的物理地址只有操作系统知道,实际上逻辑地址和物理地址都是32位整数(32位机)。两个不同进程,就算逻辑地址一样,实际的物理地址也不同。
这里关于各种变量的内存地址相关可以参考:
C语言内存地址基础
http://blog.jobbole.com/44845/
关于C语言的函数调用过程更加深度严谨(也更难懂)的知识,墙裂推荐这篇文章:
深入理解C语言的函数调用过程
http://blog.chinaunix.net/uid-23069658-id-3981406.html
通过引用传递和通过指针传递?
之前的两个例子,分别用常规的值传递和指针的传递实现数据交换的过程看起来不同,其实都是差不多的。实质上都是值传递。
第一个例子的执行过程:
第二个例子的执行过程:
可以看出实际上利用指针的方法也只是把a,b的逻辑地址作为一个整数通过值传递到形参里存储起来了,值传递的内容是a,b的逻辑地址。这两种方式都需要额外的开辟栈区的内存,而且指针操作是易错且不安全的。
下面是通过引用参数完成的交换过程。
// 引用参数实现交换
void swap(int &a, int &b){
int temp;
temp = a;
a = b;
b = temp;
}
// Using main to test
void main(){
void swap(int&, int&);
int a = 1, b = 2;
swap(a,b);
printf("%d %d\n",a,b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
有些文章说道,通过引用的方式传递给函数的是变量的地址,这种方式叫做地址传递方式,还提到这是和“值传递”十分不同的方式。
有些书说道:“引用实际上是取了个‘别名’”
还有的书和文章说道引用是比通过指针传递更加高效的方式,因为不需要开辟新的内存空间用来拷贝实参的地址。
真的吗?
引用实现原理的讨论
先讨论引用实现的系列文章,大佬们讲得比较透彻,而且论据丰富。
c++中的引用的使用原理以及使用实例
http://blog.csdn.net/ztz0223/article/details/1639305
C++ 引用 参数传递 机制【强烈推荐】
http://blog.csdn.net/huqinweI987/article/details/50769096
C++引用的本质与修改引用的方法
http://blog.csdn.net/huqinweI987/article/details/24874403
举例剖析C++中引用的本质及引用作函数参数的使用
http://www.jb51.net/article/80911.htm
如果不想看干货长文的就看下下面的通俗简短讨论吧。
我们看下下面这段小程序:
int i = 0;
int &j = i; // 设j为i的一个引用
j = 10; // 将j试着改变为10
int *p = &j; // 观察引用的取地址操作
1
2
3
4
汇编(伪汇编)解析如下:
;int i = 0;
mov dword ptr [i],0; // i的内容置为0;
;int &j = i;
lea eax, [i]; // 将i的地址放入eax寄存器
mov dword ptr[j],eax; // 将i的地址传入j的内容
; j = 10;
mov eax, dword ptr[j]; // 取j的内容(i的地址)放入eax
mov dword ptr [eax], 0Ah; // 将eax地址指向修改为10;
;int *p = &j;
mov eax,dword ptr [j] // 将j的内容传给eax
mov dword ptr [p],eax // 把eax内容传入p的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
实际上,通过对汇编的分析可以看出:
“引用是一个别名”的说法并不准确,实际上实现过程中引用也可以看成是一种指针,实际上引用变量存储的就是引用对象的地址,也要占用内存空间(和指针占用大小不同),只不过C++的标准规定了引用初始化完毕之后,对引用的操作就等于是对实际对象的操作。
虽然引用可以看做特殊的指针,对引用的操作会被编译器解释成对地址指向的目标的操作。但和*p这种取指针指向对象的方式不同,这种方式不会开辟临时空间存储指针指向的对象。如果指向对象很大,操作重复数很多,这个差异就会对性能有十分大的影响。
引用的本身值,即引用对象的地址不可以像指针变量一样修改,对引用的操作只会解释成对引用对象的操作,可以理解引用变量是一个静态的指针。
对第2条的解释,关于指针操作拷贝副本和引用节省空间的详细解释可以看上面的文章—— C++ 引用 参数传递 机制【强烈推荐】。