深入解析C++中的引用类型

c++比起c来除了多了类类型外还多出一种类型:引用。这个东西变量不象变量,指针不象指针,我以前对它不太懂,看程序时碰到引用都稀里糊涂蒙过去。最近把引用好好地揣摩了一番,小有收获,特公之于社区,让初学者们共享。

引用指的是对一个对象的引用。那么什么是对象?在c++中狭义的对象指的是用类,结构,联合等复杂数据类型来声明的变量,如 MyClass myclass,CDialog  mydlg,等等。广义的对象还包括用int,char,float等简单类型声明的变量,如int a,char b等等。我在下文提到“对象”一词全指的是广义的对象。c++
的初学者们把这个广义对象的概念建立起来,对看参考书是很有帮助的,因为大多数书上只顾用“对象”这个词,对于这个词还有广义和狭义两种概念却只字不提。

一。引用的基本特性
首先让我们声明一个引用并使用它来初步认识引用。
例一:


代码如下:

int v,k,h;
            int &rv=v;
            rv=3;      //此时v的值也同时变成了3。
            v=5;
            k=rv+2;    //此时k=5+2=7。
            h=12;
            rv=h;
            rv=20;

第1句声明了三个对象(简单变量)。

第2句的意思是:声明了一个引用,名字叫rv,它具有int类型,或者说它是对int类型的引用,而且它被初始化为与int类型的对象v“绑定”在一起。此时rv叫做对象v的引用。

第3句把rv的值赋为3。引用的神奇之处就在这里,改变引用的值的同时也改变了和引用所绑定在一起的对象的值。所以此时v的值也变成了3。

第4句把v的值改为5,此时指向v的引用的值也被改成了5。所以第5句的中k的值是5+2等于7。

上述5句说明了引用及其绑定的对象的关系:在数值上它们是联动的,改变你也就改变了我,改变我也就改变了你。事实上,访问对象和访问对象的引用,就是访问同一块内存区域。

第6,7,8三句说明了引用的另一个特性:从一而终。什么意思?当你在引用的声明语句里把一个引用绑定到某个对象后,这个引用就永远只能和这个对象绑定在一起了,没法改了。所以这也是我用了“绑定”一词的原因。而指针不一样。当在指针的声明语句里把指针初始化为指向某个对象后,这个指针在将来如有需要还可以改指别的对象。因此,在第7句里把rv赋值为h,并不意味着这个引用rv被重新绑定到了h。事实上,第7句只是一条简单的赋值语句,执行完后,rv和v的值都变成了12。第8句执行完后,rv和v的值都是20,而h保持12不变。

引用还有一个特性:声明时必须初始化,既必须指明把引用绑定到什么对象上。大家知道指针在声明时可以先不初始化,引用不行。所以下列语句将无法通过编译:


代码如下:

int v;
              int &rv;
              rv=v;

再举一例:
例二:


代码如下:

class MyClass
          {
              public:
                  int a;
                  ...
                  ...
          };

MyClass  myclass;
          Myclass& cc=myclass;
          myclass.a=20;          //等价于cc.a=20
          cc.a=60;               //等价于myclass.a=60

从以上例子可以看到,无论这个对象有多复杂,使用该对象的引用或是使用该对象本身,在语法格式上是一样的,在本质上我们都使用了内存中的同一块区域。

取一个引用的地址和取一个对象的地址的语法是一样的,都是用取地址操作符"&"。例如:


代码如下:

int i;
          int &ri;
          int *pi=&ri;//这句的作用和int *pi=&i是一样的。

当然了,取一个对象的地址和取这个对象的引用的地址,所得结果是一样的。

二。引用在函数参数传递中的作用
现在让我们通过函数参数的传递机制来进一步认识引用。在c++中给一个函数传递参数有三种方法:1,传递对象本身。2,传递指向对象的指针。3,传递对象的引用。

例三:


代码如下:

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的实参,再次证明了:使用对象和使用对象的引用,在语法格式上是一样的。

void fun1(MyClass mc)
          {
                mc.a=40;
                mc.method();
          }

void fun2(MyClass* mc)
          {
                mc->a=60;
                mc->method();
          }

void fun3(MyClass& mc)
          {
                mc.a=80;
                mc.method();
          }

我们有了一个MyClass类型的对象myclass和三个函数fun1,fun2,fun3,这三个函数分别要求以对象本身为参数;以指向对象的指针为参数;以对象的引用为参数。

请看fun1函数,它使用对象本身作为参数,这种传递参数的方式叫传值方式。c++将生成myclass对象的一个拷贝,把这个拷贝传递给fun1函数。在fun1函数内部修改了mc的成员变量a,实际上是修改这个拷贝的成员变量a,丝毫影响不到作为实参的myclass的成员变量a。

fun2函数使用指向MyClass类型的指针作为参数。在这个函数内部修改了mc所指向的对象的成员变量a,这实际上修改的是myclass对象的成员变量a。

fun3使用myclass对象的引用作为参数,这叫传引用方式。在函数内部修改了mc的成员变量a,由于前面说过,访问一个对象和访问该对象的引用,实际上是访问同一块内存区域,因此这将直接修改myclass的成员变量a。

从fun1和fun3的函数体也可看出,使用对象和使用对象的引用,在语法格式上是一样的。

在fun1中c++将把实参的一个拷贝传递给形参。因此如果实参占内存很大,那么在参数传递中的系统开销将很大。而在fun2和fun3中,无论是传递实参的指针和实参的引用,都只传递实参的地址给形参,充其量也就是四个字节,系统开销很小。

三。返回引用的函数
引用还有一个很有用的特性:如果一个函数返回的是一个引用类型,那么该函数可以被当作左值使用。什么是左值搞不懂先别管,只需了解:如果一个对象或表达式可以放在赋值号的左边,那么这个对象和表达式就叫左值。

举一个虽然无用但很说明问题的例子:
例四:


代码如下:

int i;
             int& f1(int&);
             int  f2(int);
             f1(i)=3;
             f2(i)=4;

int& f1(int&i)
                {
                   return i;
                }

int f2(int i)
                {
                   return i;
                }

试试编译一下,你会发现第4句是对的,第5句是错的。对这个例子而言,i的引用被传递给了f1,然后f1把这个引用原样返回,第4句的意义和i=3是一样的。

查了查书,引用的这个特性在重载操作符时用得比较多。但是我对重载操作符还是稀里糊涂,所以就举不出例子了。
强调一个小问题,看看如下代码有何错误:


代码如下:

int &f1();

f1()=5;
                ...
                ...
                int &f1()
                {
                    int i;
                    int &ri=i;
                    return ri;
                }

注意函数f1返回的引用ri是在函数体内声明的,一旦函数返回后,超出了函数作用域,ri所指向的内存区域,即对象i所占据的内存区域就被收回了,再对这片内存区域赋值会出错的。

四。引用的转换
前面所举的例子,引用的类型都是int类型,并且这些引用都被初始化为绑定到int类型的对象。那么我们设想是否可以声明一个引用,它具有int类型,却被初始化绑定到一个float类型的对象?如下列代码所示:
float f;
int &rv=f;

结果证明这样的转换不能通过msvc++6.0的编译。但是引用的转换并非完全不可能,事实上一个基类类型的引用可以被初始化绑定到派生类对象,只要满足这两个条件:
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。引用必须初始化。

4。引用在初始化中被绑定到某个对象上后,将只能永远绑定这个对象。

5。基类类型的引用可以被绑定到该基类的派生类对象,只要基类和派生类满足上文提到的两个条件   。这时, 该引用其实是派生类对象中的基类子对象的引用。

6。用传递引用的方式给函数传递一个对象的引用时,只传递了该对象的地址,系统消耗较小。在函数体内访问    形参,实际是访问了这个作为实参的对象。

7。一个函数如果返回引用,那么函数调用表达式可以作为左值。

六。其他
1。本文中的代码在msvc++6.0中调试验证过。
2。第四节“引用的转换”中的例子:
float f;
int &rv=f;

查看bc++3.1的资料,据说是合法的。此时编译器生成了一个float类型的临时
对象,引用rv被绑定到了这个临时对象上,就是说,此时rv并不是f的引用。不知
道bc++3.1里的这个特性有什么用。

3。可以在msvc++6.0里声明这样的引用:
const int &rv=3;

此时rv的值就是3,而且无法更改。这可能没有有什么用。因为如果我们要使
用一个符号来代表常数的话,有的是更常见的方法:
#define rv 3

4。把第四节中的例子稍稍修改一下:
float f;
int &rv=(int&)f;

这时就可以通过msvc++6.0的编译了。此时rv被绑定到了f上,rv和f共用一片存储区。不过由于引用rv的类型是int,所以通过rv去访问这片存储区时,存储区的内容被解释为整数;通过f去访问这片存储区时,存储区的内容被解释为实数。

(0)

相关推荐

  • 探讨:C++中函数返回引用的注意事项

    函数 返回值 和 返回引用 是不同的函数返回值时会产生一个临时变量作为函数返回值的副本,而返回引用时不会产生值的副本,既然是引用,那引用谁呢?这个问题必须清楚,否则将无法理解返回引用到底是个什么概念.以下是几种引用情况:1,引用函数的参数,当然该参数也是一个引用 复制代码 代码如下: const string &shorterString(const string &s1,const string &s2)      {             return s1.size()&l

  • C++中对象的常引用总结

    直接传递对象名 用对象名做函数参数时,在函数调用时将建立一个新的对象,它是形参对象的拷贝. ================下面给出一个直接传递对象名的例子程序1.1================== 复制代码 代码如下: #include<iostream>using namespace std;class Time { public:  Time(int,int,int);//构造函数   void Print();//输出信息函数   void reset(Time t);//重置函数

  • 浅析C++中结构体的定义、初始化和引用

    定义:结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构. 声明一个结构体类型的形式是: 复制代码 代码如下: struct Student{      //声明一个结构体类型Student  int num;         //声明一个整形变量num  char name[20];   //声明一个字符型数组name  char sex;        //声明一个字符型变量sex  int age;         //声明一个整形变量age  float

  • C++标准之(ravalue reference) 右值引用介绍

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下CopyElision.RVO,对此不感兴趣的可以直接跳过: (1)CopyElision CopyElision技术是为了防止某些不必要的临时对象产生和拷贝,例如: 复制代码 代码如下: structA{ A(

  • 从C语言过渡到C++之引用(别名)

    今天要讲的是C++中我最喜欢的一个用法--引用,也叫别名. 引用就是给一个变量领取一个变量名,方便我们间接地使用这个变量.我们可以给一个变量创建N个引用,这N + 1个变量共享了同一块内存区域. 1. 声明引用 创建引用的格式如下: 数据类型 引用名 = 原变量 比如: int a = 1; int& b = a; 在这段代码中,我们给变量a创建了一个别名b.它们公用同一块内存区域,就是创建变量a时申请的区域. 注意:由于引用并不需要申请一块新的内存空间,因此在建立引用时只能声明,不能定义. 面

  • C++中引用&与取地址&的区别分析

    C++中的引用&与取址&是很多初学者经常容易出错的地方,今天本文就对此加以分析总结,供大家参考之用. 具体而言,一个是用来传值的 一个是用来获取首地址的 &(引用)==>出现在变量声明语句中位于变量左边时,表示声明的是引用.      例如: int &rf; // 声明一个int型的引用rf &(取地址运算符)==>在给变量赋初值时出现在等号右边或在执行语句中作为一元运算符出现时表示取对象的地址. 在C++中,既有引用又有取地址,好多人对引用和取地址不

  • C++中引用的使用总结

    1引用的定义 引用时C++对C的一个重要的扩充,引用的作用是给变量起一个别名. 例如: int a; int &b=a;//声明b是a的引用 经过以上的声明,b就成为了a的别名,a和b的地位以及作用都是一样的. 将b声明为a的引用,不需要再为b开辟新的单元,b和a在内存中占同一存储单元,它们具有相同的地址. 复制代码 代码如下: #include<iostream>using namespace std;int main(){         int a=10;         int

  • 深入解析C++中的引用类型

    c++比起c来除了多了类类型外还多出一种类型:引用.这个东西变量不象变量,指针不象指针,我以前对它不太懂,看程序时碰到引用都稀里糊涂蒙过去.最近把引用好好地揣摩了一番,小有收获,特公之于社区,让初学者们共享. 引用指的是对一个对象的引用.那么什么是对象?在c++中狭义的对象指的是用类,结构,联合等复杂数据类型来声明的变量,如 MyClass myclass,CDialog  mydlg,等等.广义的对象还包括用int,char,float等简单类型声明的变量,如int a,char b等等.我在

  • 全面解析Java中的引用类型

    如果一个内存中的对象没有任何引用的话,就说明这个对象已经不再被使用了,从而可以成为被垃圾回收的候选.不过由于垃圾回收器的运行时间不确定,可被垃圾回收的对象的实际被回收时间是不确定的.对于一个对象来说,只要有引用的存在,它就会一直存在于内存中.如果这样的对象越来越多,超出了JVM中的内存总数,JVM就会抛出OutOfMemory错误.虽然垃圾回收的具体运行是由JVM来控制的,但是开发人员仍然可以在一定程度上与垃圾回收器进行交互,其目的在于更好的帮助垃圾回收器管理好应用的内存.这种交互方式就是使用J

  • 解析Golang中引用类型是否进行引用传递

    目录 引言 引用类型 引用变量(reference variable)和引用传递(pass-by-reference) Golang是否存在引用变量(reference variable) 字典为什么可以做到值传递但是可以更改原对象? 结语 引言 开篇明义,Go lang中从来就不存在所谓的“引用传递”,从来就只有一种变量传递方式,那就是值传递.因为引用传递的前提是存在“引用变量”,但是Go lang中从来就没有出现过所谓的“引用变量”,所以也就不可能存在引用传递这种变量传递的方式. 引用类型

  • 解析iOS10中的极光推送消息的适配

    iOS10发布后,发现项目中的极光推送接收消息异常了. 查了相关资料后才发现,iOS10中对于通知做了不少改变.同时也发现极光也很快更新了对应的SDK. 现在就把适配修改的做法分享一下,希望对有需要的童鞋有所帮助. 具体做法如下: 注意:必须先安装Xcode8.0版本. 一.添加相关的SKD,或framework文件 1.添加UserNotification.framework 2.更新jpush的SDK(最新版本:jpush-ios-2.1.9.a)https://www.jiguang.cn

  • Python解析xml中dom元素的方法

    本文实例讲述了Python解析xml中dom元素的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: from xml.dom import minidom try:     xmlfile = open("path.xml", "a+")     #xmldoc = minidom.parse( sys.argv[1])     xmldoc = minidom.parse(xmlfile) except :     #updatelogger.

  • 解析MySQL中存储时间日期类型的选择问题

    一般应用中,我们用timestamp,datetime,int类型来存储时间格式: int(对应javaBean中的Integer或int) 1. 占用4个字节 2. 建立索引之后,查询速度快 3. 条件范围搜索可以使用使用between 4. 不能使用mysql提供的时间函数 结论:适合需要进行大量时间范围查询的数据表 datetime(javaBean中用Date类型) 1. 占用8个字节 2. 允许为空值,可以自定义值,系统不会自动修改其值. 3. 实际格式储存(Just stores w

  • 实例解析Java中的构造器初始化

    1.初始化顺序 当Java创建一个对象时,系统先为该对象的所有实例属性分配内存(前提是该类已经被加载过了),接着程序开始对这些实例属性执行初始化,其初始化顺序是:先执行初始化块或声明属性时制定的初始值,再执行构造器里制定的初始值. 在类的内部,变量定义的先后顺序决定了初始化的顺序,即时变量散布于方法定义之间,它们仍就会在任何方法(包括构造器)被调用之前得到初始化. class Window { Window(int maker) { System.out.println("Window(&quo

  • 全面解析JavaScript中“&&”和“||”操作符(总结篇)

    1.||(逻辑或), 从字面上来说,只有前后都是false的时候才返回false,否则返回true. alert(true||false); // true alert(false||true); // true alert(true||true); // true alert(false||false); // false 这个傻子都知道~~ 但是,从深层意义上来说的话,却有另一番天地,试下面代码 alert(0||1);//1 显然,我们知道,前面0意味着false,而后面1意味着true,

  • 通过实例解析java8中的parallelStream

    这篇文章主要介绍了通过实例解析java8中的parallelStream,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 about Stream 什么是流? Stream是java8中新增加的一个特性,被java猿统称为流. Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator.原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作:高级版本的 Stream

  • 浅谈Java 中的引用类型

    Java 中的引用类型:强引用.软引用.弱引用和虚引用 强引用 如 Object object = new Object(),那 object 就是一个强引用,如果一个对象具有强引用,垃圾回收器就永远不会回收它. 软引用 软引用用来描述一些还有用但非必需的对象.在内存即将发生内存溢出之前,会把这些对象列进回收范围之中进行二次垃圾回收.如果这次回收还没有足够内存,才会发生内存溢出现象. 另:软引用可用来实现内存敏感的高速缓存. 弱引用 用来描述非必需的对象.被弱引用关联的对象只能存活到下一次垃圾收

随机推荐