引用与指针

可以先一下优快云上的文章和讨论: http://www.youkuaiyun.com/article/2012-03-26/313540
注意文章和讨论中提到的“更好的别名机制”

************************************************************************
引用是什么
从语义上理解,  引用即别名。
引用是某个目标变量的“别名”(alias),它和被引用的变量共享数据的内存单元,对引用的操作与对变量直接操作效果相同。
C++引入引用,能够给编译器带来更大的优化空间,能够提高参数传递的效率。

************************************************************************
引用是怎么实现的:
C++的标准中只规定了引用的语义,并没有规定其实现方式。
而正如我们编程实践中所看到的, 很多时候引用是通过指针实现的。

反汇编(没有优化):
                int aa=1;
00A12E1E  mov         dword ptr [aa],1     //一条指令搞定赋值,[aa]为aa的地址
                int &bb = aa;                
00A12E25  lea         eax,[aa]                           // 将aa的地址读取到eax中
00A12E28  mov         dword ptr [bb],eax     //将其地址存放到内存单元 [bb],可以认为,该单元就是指向aa的常指针
                bb=2;                                       //需要两条指令
00A12E2B  mov         eax,dword ptr [bb]   //后面对bb的操作转换为对 [bb]处所存地址单元的操作
00A12E2E  mov         dword ptr [eax],2     //所有&bb可以看作指针!!!!!这是本质!!!
                int  pp;
                pp  = int(&aa);
00A12E34  lea         eax,[aa]           // int &bb = aa;执行的操作与这里是一样的,只不过对我们隐蔽了[bb]内存单元
00A12E37  mov         dword ptr [pp],eax 
                pp = int (&bb);
00A12E3A  mov         eax,dword ptr [bb]   //这里对aa和bb取地址操作也是有差别的
00A12E3D  mov         dword ptr [pp],eax   //上面一条需要使用mov指令到[bb]读取aa的地址,而不是直接使用lea指令
引用与原变量的比较:
                c = aa;
00F32E40  mov         eax,dword ptr [aa] 
00F32E43  mov         dword ptr [c],eax 
                c = bb;
00F32E46  mov         eax,dword ptr [bb]    //这里需要两句,先取地址
00F32E49  mov         ecx,dword ptr [eax]    //再取内容  
00F32E4B  mov         dword ptr [c],ecx 
引用与指针的比较:
                bb=2; 
009F144B  mov         eax,dword ptr [bb] 
009F144E  mov         dword ptr [eax],2 
                *pp = 3;
009F145A  mov         eax,dword ptr [pp] 
009F145D  mov         dword ptr [eax],3  

上面的情况: 引用在实现上是一个隐藏的常量指针,但是具体实现不用编程者干预什么,而实际的操作是传递了指针,在进行访问时进行了转换。
这种情况下:
1)引用变量在32位系统下占用了4个字节,用来保存被引用对象的地址
2)访问引用变量时,需要先取得被引用对象的地址,然后再进行相关的操作(与原来相比,所用的指令也可能不一样了)
3)这种情况下,引用占用空间,存取速度参考指针操作

正如前面所说,c++没有规定引用的底层实现,编译器实现时,可以为引用分配空间,也可以不分配空间;
然而引用并不总是即用即丢的,因而经常还是需要保存所引用对象的地址。

上面是没有优化的情况,然而与指针相比,如下面的情况,引用是可以优化的,而指针不可以。
int aa = 1;
int* pp = &aa;
*pp  = 2;
由于“&”操作的存在,导致必须在栈上为aa分配内存,并将内存地址赋予pp;
int aa=1;
int &bb = aa;                
bb=2; 
而这段代码中,仅仅是为了通过某种方式访问到aa这个变量,完全不关心其内存地址,
没有取地址操作,编译器可以对这段代码更好地优化,甚至可以不给该变量申请地址,从头到尾将其放在某个寄存器中,直接对寄存器读写。

另外对于如下函数
int add(int* a,int* b){...};
int a,b;
add(&a,&b);
无论编译器决策process函数是否inline,都会认为对变量a和b必须进行取地址的操作;
而如果是引用的话,遇到inline add函数的情况,编译器可以判断出只需要处理a和b,而不在乎a和b的地址。

总结:对于编译器来说,使用引用有机会带来更大的优化空间,编程者不必去假设编译器如何去优化变量的,也不必过多去考虑引用如何实现的。

************************************************************************
引用的用途:
最重要的用途就是用作函数形参,别名的需要,往往是为了传递参数(形参与临时变量)。

先看一段汇编:
int  add( int  &a,  int  * const  p);
b = add(a,p);     //函数调用
000F2E92  mov         eax,dword ptr [p] 
000F2E95  push        eax 
000F2E96  lea         ecx,[a] 
000F2E99  push        ecx 
函数调用时,实参从右到左入栈(_cdecl方式),关键是引用与指针的区别:
对于引用,入栈的是变量的地址;
对于指针,入栈的是指针变量的值(某变量的地址);
这是一个本质的区别, 作形参时:引用属于“传地址”的一种参数传递方式,而指针却是属于的一种方式。

虽然它们能够用来实现相似的功能,但在C++里,引用是受推荐的,
因为 它能够确保传入的地址确实是参数声明类型(或与之兼容类型)的对象的地址
相反,指针虽然灵活性较强,但 安全性却很差,它不能确保传入地址的合法性(实参只要是地址),如上面的例子,可以改成
double  *q;
b = add(a,(  int *)q);
编译期间不会报错,然而在执行阶段却会产生不可预料的结构。

另外还有一个重要约束就是,传递引用类型实参时必须是变量,而不能使表达式!
而使用指针的话,写成:
add(a,p+1);  //传递过程修改
也不会报错!区别!
上面 int  *  const   形参的const其实毫无意义

地址值的拷贝可能在保存、传递的过程中被无意被篡改或者由于该地址失效(例如该地址指向的临时对象被释放)而引发灾难,糟糕的是编译程序无法对此提供有效的检测手段,但编译程序对于引用对象的合法性则能够进行有效的检测,以确保引用参数对应的实参是合法的。

总结:
指针和引用都是传递参数的有效形式,特别是在传递较大的结构时,能够避免复制操作;
两者作形参的区别在于,指针属于 “传拷贝”,而引用属于 “传地址”的方式;
推荐使用引用的原因在于,引用 够确保传入的地址确实是参数声明类型(或与之兼容类型)的对象的地址,相比于指针,更为安全;
使用引用程序更清晰,代码可读性更好。

************************************************************************
const引用:
想使用传地址方式,而不修改信息,使用常引用 
double  cube( const  double  &a);
如果代码修改了变量a的值, 编译及时地检查与生成错误信息。

如果 实参与引用参数不匹配,C++将产生临时变量。现在, 当且仅当参数为const引用时,C++才允许这样做,但这是一种新的限制(具体要看编译器有没有这种限制了,违反时有的编译器可能只发出警告)。(“C++ Primer Plus” P231)
例子如下:
int  add( int  &a)
{
                  return  a+1;
}
int  b;
b = add(a+1);  //人为修改,将出现编译错误,而指针不会,
将函数原型修改为:
int  add( const  int  &a)
则编译可以通过。
原因是引用参数是const时,如果是实参的类型正确,但不是左值(是字面常量或包含多项的表达式),
那么编译器将创建临时匿名变量,函数体内的引用a指向该变量。

因此,应尽可能使用const:
1)可以避免无意中修改数据的编程错误
2)const使函数能够处理const和非const实参,否则只能接受非const数据
3)const引用使函数能正确地生成并使用临时变量

何时使用引用参数:“C++ Primer Plus” P240

************************************************************************
使用引用要注意什么:

引用必须在定义时进行初始化;
初始化后不能再把该引用名作为其他变量的别名;
不能建立数组的引用;
不能返回局部变量的引用,指针也一样,(注意局部变量的内存分配);
也不要返回函数内部new分配的内存的引用,如果被返回的引用只是作为一个临时变量出现,容易造成内存泄露(参考“Effective C++[1]” Item 31);
对于流操作符<<和>>等运算符重载,为了能够进行连续操作,推荐函数返回值为引用;
对于赋值操作=,返回值必须是一个左值,以便可以被继续赋值(x=10)=100;引用成了惟一返回值选择。

************************************************************************
补充:
若与普通指针比较: int  *i; int  &j=a;
i的左值是它所在的地址单元,i的右值是它所代表的地址值;j的左值是它所代表的对象,j的右值是它所代表的对象的值.
从这里可以解释为什么引用初始化后就不能改变了:
定义j后,开辟一个内存单元用于存放a的地址,j的左值为该内存单元的值,而右值为该内存单元存放的地址处存放的值;
而没有途径改变该内存单元用于存放其他变量的地址。

************************************************************************
常见面试题:
1)指针和引用的区别:

2)在什么时候需要使用“常引用”? 
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。

3)分析代码:
string foo( );
void bar(string & s); 
bar(foo( ));
bar("hello world"); 
第三行和第四行的操作是非法的。
原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。
因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。

4)“引用”与多态的关系?
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
Class A; 
Class B : Class A{...};  
B b; 
A& ref = b;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值