您的位置:首页 > 编程语言 > C语言/C++

C++引用详解及与指针异同点

2016-08-12 11:22 253 查看

一、引用详解

引用:就是对某一变量(或对象)取一个别名,对变量的引用 的操作与对变量本身直接操作完全一样。
引用的声明:类型标识符 &引用名=目标变量名;
  如下:定义变量a的引用aa,即别名。
    int a = 5;
    int &aa=a;  //以后对aa的操作运算,就直接对变量a本身起作用
  (1)&在此不是取地址运算符,而是起标识作用。&aa ---是一个整体。
  (2)类型标识符是指目标变量的类型。
  (3)声明引用时,必须同时对其进行初始化;类似于const常量初始化。
  (4)引用声明完毕后,相当于目标变量有两个名称即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名,也就是说一旦声明一个引用绑定到每个变量或对象,就不能再作其他变量的引用,即引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量,具有“终身制”
  (5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。所以:对引用求地址,就是对目标变量求地址。&aa与&a相等。引用在定义上是说引用不占据任何内存空间,但是编译器在一般将其实现为const指针,即指向位置不可变的指针,所以引用实际上与一般指针同样占用内存。
  (6)不能建立引用数组,因为数组是一个由若干个元素所组成的集合,所以无法建立一个由引用组成的集合,但是可以建立数组的引用。引用不可以作数组中的元素。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。另外数组名本质是一个指针。
  (7)使用对象和使用对象的引用,在语法格式上是一样的。对象和对象的引用在某种意义上是一个东西,访问对象和访问对象的引用其实访问的是同一块内存区。
  (8)基类类型的引用可以被绑定到该基类的派生类对象,只要基类和派生类满足两个条件。这时,该引用其实是派生类对象中的基类子对象的引用。
  (9)引用常见的使用用途:作为函数的参数[最常用]、函数的返回值,常引用。

二、引用常见的使用用途

1、常引用

a.常引用声明方式:const  类型标识符  &引用名 = 目标变量名;
b.用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
c.引用型参数如果为const,则函数体不能对引用型参数修改目标变量。
·  void TestReference1()  
·  {  
·       int d1 = 4;  
·       const int & d2 = d1;  
·       d1 = 5;    //d1改变,d2的值也会改变。  
·       //d2 = 6;//不能给常量(不能被修改的量)赋值。 常引用不能对引用型参数修改目标变量。
·    
·       const int d3 = 1;  
·       const int & d4 = d3;  
·       //int&d5 = d3;  
·       const int & d6 = 5;//常量具有常量性,只有常引用可以引用常量  
·  }

2、引用作为函数的返回值

函数最多只能有一个返回值。那如果需要一个函数返回多个类型的值怎么办?解决方案之一就是:将待返回的都定义为全局变量,通过引用参数传入函数,数据操作之后,那些待返回的值就改变了,但会造成全局变量混杂。使用引用作为函数的返回值,函数执行完,返回的是引用,可以引向的是很大的数据内存区,可以更有效解决。
引用作为函数的返回值,函数定义时要按以下格式:
    类型标识符  &函数名 (形参列表及类型说明)
    {  函数体  }
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
(3)如果一个函数返回的是一个引用类型,那么该函数可以被当作左值使用。什么是左值搞不懂先别管,只需了解:如果一个对象或表达式可以放在赋值号的左边,那么这个对象和表达式就叫左值。
#include<iostream.h>
float temp;//定义全局变量temp
float fn1(float r);//声明函数fn1
float &fn2(float r);//声明函数fn2 r
float fn1(float r){//定义函数fn1,它以返回值的方法返回函数值
    temp=(float)(r*r*3.14);
    return temp;
}
float &fn2(float r){//定义函数fn2,它以引用方式返回函数值
    temp=(float)(r*r*3.14);
    return temp;
}
void main(){
    float a=fn1(10.0);//第1种情况,系统生成要返回值的副本(即临时变量)
//    float &b=fn1(10.0); //第2种情况,可能会出错(不同 C++系统有不同规定)
/*
    编译错误:cannot convert from 'float' to 'float &'
    A reference that is not to 'const' cannot be bound to a non-lvalue
*/
    //不能从被调函数中返回一个临时变量或局部变量的引用
    float c=fn2(10.0);//第3种情况,系统不生成返回值的副本
    //可以从被调函数中返回一个全局变量的引用
    float &d=fn2(10.0); //第4种情况,系统不生成返回值的副本
    cout<<"a="<<a<<",c="<<c<<",d="<<d<<endl;
    //a=314,c=314,d=314
}
引用作为返回值,必须遵守以下规则:
  (1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
  (2)不能返回函数内部new分配的内存的引用。这 条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
  (3)可以返回类成员的引用,但最好是const。这 条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
  (4)引用与一些操作符的重载:流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << \"hello\" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回 一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一 个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这
就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
    (5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一 个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。

3、引用作为函数的参数[最常用]

C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免将整块数据[参数]全部压栈,可以提高程序的效率。但是现在(C++中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。
//此处函数的形参p1, p2都是引用
void swap(int &p1,int &p2){
    int p=p1;
    p1=p2;
    p2=p;
}
调用
    int a = 2;
    int b = 3;
    Swap(a ,b );  //注意:此处不可直接传入字面量;Swap(2, 3)是错误的;

三、参数传递的方式比较:

1.【值传递】如果形参为非引用的传值方式,则生成局部临时变量接收实参的值
void Swap (int left, int right) //值传递的方式无法实现交换,因为传参时对于参数left和right拷贝一临时副本,交换的是副本值,因为其是临时变量函数退出,变量销 {                                //毁,并不会影响外部left和right的值。
     int temp = left;
     left = right ;
     right = temp ;
}
2.【引用传递】如果形参为引用类型,则形参是实参的别名。
void Swap (int& left, int& right)//使用引用的话,不做临时拷贝,&的使用说明此处只是原参数的另一个名字而已,所以修改时直接在原参数的基础上修改变量值。
{
     int temp = left;
     right = left ;
     left = temp ;
}
3.【指针传递】
void Swap (int* pLeft, int* pRight)//传入的是地址,因为地址是唯一的,所以指针通过地址的访问进而可修改其内容。
{
     int temp = *pLeft;
     *pLeft = *pRight;
     *pRight = temp;
}  

四、对象的引用作为参数

         class MyClass

          {

              public:

                  int a;

                  void method();

          };

          MyClass  myclass;

          void fun1(MyClass);

          void fun2(MyClass*);

          void fun3(MyClass&);
          fun1(myclass);     //执行完函数调用后,myclass.a=20不变。

          fun2(&myclass);    //执行完函数调用后,myclass.a=60,改变了。
          fun3(myclass);     //执行完函数调用后,myclass.a=80,改变了。
          //注意fun1和fun3的实参,再次证明了:使用对象和使用对象的引用,在语法格式上是一样的。
      fun1函数,它使用对象本身作为参数,这种传递参数的方式叫传值方式。c++将生成myclass对象的一个拷贝,把这个拷贝传递给fun1函 数。在fun1函数内部修改了mc的成员变量a,实际上是修改这个拷贝的成员变量a,丝毫影响不到作为实参的myclass的成员变量a。
      fun2函数使用指向MyClass类型的指针作为参数。在这个函数内部修改了mc所指向的对象的成员变量a,这实际上修改的是myclass对象的成员变量a。
      fun3使用myclass对象的引用作为参数,这叫传引用方式。在函数内部修改了mc的成员变量a,由于前面说过,访问一个对象和访问该对象的引用,实际上是访问同一块内存区域,因此这将直接修改myclass的成员变量a。  
      在fun1中c++将把实参的一个拷贝传递给形参。因此如果实参占内存很大,那么在参数传递中的系统开销将很大。而在fun2和fun3中,无论是传递实参的指针和实参的引用,都只传递实参的地址给形参,充其量也就是四个字节,系统开销很小,这也是为什么最常使用引用作为函数的参数。

五、引用的转换[引用与多态]

一个基类类型的引用可以被初始化绑定到派生类对象,即一个基类的引用可以指向它的派生类实例。只要满足这两个条件:
1,指定的基类是可访问的。

2,转换是无二义性的。

          class A

          {

            public:

                int a;

          };

          class B:public A

          {

           public:

                int b;

          };

          A Oa;

          B Ob;

          A& mm=Ob;

          mm.a=3;
      基类A和派生类B,并且有一个基类对象Oa和派生类对象Ob,我们还声明了一个引用mm,它具有基类类型但被绑定到派生类对象Ob上。由于我们的这两个类 都很简单,满足那两个条件,因此这段代码是合法的。在这个例子中,mm和派生类Ob中的基类子对象是共用一段内存单元的。所以,语句mm.a=3相当于 Ob.a=3,但是表达式mm.b却是不合法的,因为基类子对象并不包括派生类的成员。

六、引用和指针的区别和联系(笔试热点)

1. 引用只能在定义时初始化一次,之后不能改变指向其它变量(从一而终);指针变量的值可变。
2. 引用必须指向有效的变量,指针可以为空。
3. sizeof指针对象和引用对象的意义不一样。sizeof引用得到的是所指向的变量的大小,而sizeof指针是对象地址的大小。
4. 指针和引用自增(++)自减(--)意义不一样。
5. 相对而言,引用比指针更安全。
★不同点
1. 指针是一个实体,而引用仅是个别名;
 2. 引用使用时无需解引用(*),指针需要解引用;
 3. 引用只能在定义时被初始化一次,之后不可变;指针可变;
 4. 引用没有 const,指针有 const;const修饰的指针不可变;
 5. 引用不能为空,指针可以为空;
 6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
7. 指针和引用的自增(++)运算意义不一样;
 8.从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。
★相同点:两者都是地址的概念,指针指向一块儿内存,其内容为所指内存的地址;引用是某块儿内存的别名。
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c