C++类型引用浅析

引言

从最早被Bjarne Stroustrup 发明,作为C语言的扩展,到广为人知C++98标准,再到最新的C++11、C++14和C++17标准,C++一直在不断地进步、演化。面向对象、泛型编程、模板、range based for、lamnda表达式,一个又一个强大的功能概念被不断地提出并最终采纳到标准当中。C++正在向着更加现代化的方向前进。

然而,也许是因为C++包容的太多的缘故,它总有一些偏僻而生涩的角落,暗藏着陷阱,时常让用户迷惑。类型引用就是这样的一个语言特性,很多书籍中对它只是一笔带过,让用户把它想象成一个指针。但是,引用的用法却和指针不同,使用者经常在没有深入理解引用概念的情况下将两者混为一谈。

本文从实际工程应用出发,探讨引用在使用上相比指针的优点;建立测试,对比两者在代码效率方面的差别;并从底层切入,以编译器实现的视角探索引用类型的实质。

引用初始化

引用的声明语法为: <Type>&
<Name>,它的初始化必须同时伴随赋值。也就是说,引用类型必须同时声明和初始化。而指针不一样,指针可以将声明与初始化分离,不需要在声明时初始化。

那为什么引用的语法会有这样的要求呢?因为引用概念的出现是为了改善C++中的安全问题。指针声明与初始化的分离固然带来了使用上的灵活性,却也在一定程度上加大了程序出错的可能性: 变量可能在初始化之前被使用。尤其是在工程中,错综复杂的模块关系和难以理解的算法代码容易让开发者在代码的阅读中丢失上下文,而短至一两行的初始化代码往往难以辨析。

引用赋值

引用不允许单独赋值,唯一的赋值就是在初始化时。同样的,引用牺牲了灵活性来获得更多的安全性。

考虑如下的代码片段:

void* ptr = malloc(1);

ptr = malloc(1);

指针ptr被两次赋值,但对于第一次获取的内存而言,我们不能再次使用它,也没有办法释放它因为没有任何指针指向它(典型的内存泄漏)。

但是如果使用引用的话,就能够在语法上今早发现这种问题,消除内存泄漏存在的可能性(编译器将会在第二行处报错):

void* const & ref = malloc(1);

ref = malloc(1);

空引用

引用不能为空,每一个引用都引用某个对象或内建类型。

对于指针ptr,可以以ptr = NULL 或者 ptr = nullptr的形式声明空指针,但是这就意味着指针可能为空。在代码中,需要显示地检测这种情况。大量的实践表明,这会造成逻辑的不连续,扰乱代码的一致性。

而引用不允许空引用。对于引用ref,形似ref = NULL 或ref = nullptr的引用对象的方式是不被允许的,因为每一个引用都必须引用(也就是指向)某个用户自定义对象或内建类型。引用语法上的限制,既消除了多余的空值检查,保证了自身的有效性,又减轻了开发者的负担,间接改善了代码的可读性,使工程易于维护和发展。

引用语义

使用引用进行的操作,相当于直接在被引用对象上进行这些操作。

这与指针非常相似,除了语法方面的不同:通过指针进行的操作使用->操作符,通过引用进行的操作使用.操作符。考虑下面的代码片段:

int a = 0;

int& b = a;

b = 9;

代码非常简单,只有三行:第一行声明整型变量a,第二行声明整型引用b,第三行对b进行赋值。最后结果是:a和b的值都为9 。因为b只是对a的引用,对b赋值语义上就是对a赋值,b只是a的一个别名,实际上都指向同一块内存。

虽然上述例子中是举例内建类型的引用,但引用语义同样适用于自定义类型(即类)。这种环境下,引用的使用效果与指针相同,但引用使得我们能够以一种更现代化、更贴近面向对象的方式进行对象的操作(即.操作符),使代码在形式上更符合人类的逻辑。

引用类型的汇编级代码量比较

从实际角度看引用类型的编译后代码量,我们对C++内建类型以及两个极端的自定义类进行测试,类定义如下:

class CusOne         {};

class CusOne

{

Int        a;

Short        b;

Float        c;

Double        d;

CusOne    one;

};

CusOne类型不包含任何成员,而CusTwo类则包含多个内建类型成员以及一个自定义类成员。

编译环境为X86_64机器,Win8.1系统下,编译采用clang编译器3.8版本(-O0为禁止优化选项,为了防止编译器对测试代码进行优化,妨碍测试结果的正确性)。以下是编译后代码量结果:

表6-1 单个引用和指针变量的编译后代码量(汇编代码)

Type

Pointer (-O0)

Reference (-O0)

Int

40 byte

40 byte

Short

40 byte

40 byte

Long

40 byte

40 byte

Long Long

40 byte

40 byte

Float

40 byte

40 byte

Double

40 byte

40 byte

CusOne

40 byte

40 byte

CusTwo

39 byte

39 byte

可以看到,类型引用的代码量与单纯的指针是一样,不需要产生额外的代码。

引用的运行效率比较

接下来测试引用的效率。我们对每种类型的变量赋值10亿次,分别通过指针和引用,统计它们的运行时间。编译及测试环境同上(同样禁止编译优化)。

表7-1 引用和指针的效率测试

Type

Pointer (-O0)

Reference (-O0)

Int

2.421s

2.406s

Short

2.343s

2.343s

Long

2.343s

2.328s

Long Long

2.328s

2.328s

Float

2.359s

2.328s

Double

2.375s

2.343s

CusOne

2.390s

2.390s

CusTwo

2.562s

2.531s

在效率上,引用与指针相差无几,几乎没有效率上的包袱。以上测试是针对引用的'存'操作,'取'操作与'存'操作几乎相同,这里不再重复检测。

底层实现分析

想要了解引用在底层的实现,最好的方法就是从汇编语言探究其实现。因为任何高级语言特性,都是在汇编的基础上实现的。我们将从一小段C++代码出发,将其编译成汇编语言进行研究。

  1. 我们取以下C++代码作为内建类型引用的例子:
    1. int a = 0;
    2. int& b = a;
    3. b = 9999;
    4. a = b;

C++代码非常简单,但汇编代码却不容易理解,比较抽象(以下的每一个序号表明对应的C++代码行号):

  1. movl    $0, -12(%rbp)        //    -12(%rbp) -> a
  2. leaq    -12(%rbp), %rax

    movq    %rax, -8(%rbp)    //    -8(%rbp) -> b

  3. movq    -8(%rbp), %rax

    movl    $9999, (%rax)

  4. movl    $9999, (%rax)

    movl    (%rax), %eax

    movl    %eax, -12(%rbp)

其中-12(%rbp)处存放的是变量a,-8(%rbp)处存放的是边变量b。现在分别来分析每行C++语句的实现:

  1. 将常量值0赋值给a。
  2. 取变量a的地址赋值给寄存器rax,再将寄存器rax的值赋值给变量b。
  3. 将寄存器rbx的值(也就是a的地址)赋值给rax,再将常量9999赋值给寄存器rax所指向的内存单元(也就是变量a)。
  4. 将常量9999赋值给寄存器rax指向的内存单元(也就是变量a),将寄存器rax指向的内存值(也就是变量a)赋值给寄存器rax,再将rax赋值给a。

从上面的分析可见,在汇编语言级,引用的实现是通过指针来实现的:变量b存放的是变量a的指针。引用在底层上的实现非常直接,既没有额外的空间消耗,也没有多余的时间消耗。

  1. 最后来看看对于自定义类型,引用的实现机制,以下是C++代码:
    1. 类定义:

      class CusOne

      {

      Int     a;

      Flaot     b;

      Void*     c;

      };

    2. 引用测试代码:
      1. CusOne     one;
      2. CusOne&     ref = one;
      3. ref = one;

下面是汇编代码:

  1. subq    $32, %rsp

    leaq    8(%rsp), %rcx

    movl    $0, 28(%rsp)

  2. movq    %rcx, (%rsp)    //    (%rsp) -> ref
  3. movq    (%rsp), %rcx

    movq    8(%rsp), %rdx    //    8(%rsp) -> [one.a, one.b]

    movq    %rdx, (%rcx)

    movq    16(%rsp), %rdx//    16(%rsp) -> one.c

    movq    %rdx, 8(%rcx)

其中%rsp为栈指针寄存器,汇编代码先将栈增加32个字节,用以存储one变量和引用变量,并将one变量的地址存储在rcx寄存器中。第三个指令用来初始化多余的填充字节,这里与主题无关,不多加考虑。因此,(%rsp)处存放的是引用变量ref,8(%rsp)开始24个字节存放的是one变量。如下图(注:途中每个单元为8个字节大小):

接着,代码将存放有one变量地址的寄存器rcx赋值给寄存器rsp所指向的内存单元,即变量ref。也就是说,自定义类型引用在底层的实现,同样是通过指针。最后是自定义类型变量的赋值,代码先将ref值(也就是one的地址)存放在寄存器rcx中,然后以8个字节为单元将one变量的内容赋值给ref,完成ref
=
one赋值语句的实现。

结束语

本文对比C指针,介绍了C++引用的语法语义特殊性及其优点;通过实验,测试引用在汇编级的代码生成量大小和运行时的效率;并从底层切入,分析了引用的实现机制。希望本文可以抛砖引玉,帮助开发人员深入理解C++中的引用机制,高效地加以利用。

C++类型引用浅析的更多相关文章

  1. 在编写wpf界面时候中出现如下错误: 类型引用不明确。至少有两个名称空间(“System.Windows”和“System.Windows”)中已出现名为“VisualStateManager”的类型。请考虑调整程序集 XmlnsDefinition 特性。

    wpf中类型引用不明确.至少有两个名称空间(“System.Windows”和“System.Windows”)中已出现名为“VisualState 你是不是用了WPFToolKit?如果是的,那原因 ...

  2. c++函数参数类型-引用、指针、值

    c++函数参数类型-引用.指针.值 https://www.cnblogs.com/lidabo/archive/2012/05/30/2525837.html

  3. C++右值引用浅析

    一直想试着把自己理解和学习到的右值引用相关的技术细节整理并分享出来,希望能够对感兴趣的朋友提供帮助. 右值引用是C++11标准中新增的一个特性.右值引用允许程序员可以忽略逻辑上不需要的拷贝:而且还可以 ...

  4. 临时变量不能作为非const类型引用形参的实参

    摘要:     非const 引用形参只能与完全同类型的非const对象关联.      具体含义为:(1)不能用const类型的对象传递给非const引用形参:                  ( ...

  5. "运行时"如何解析类型引用

    先将下面的代码保存到文本中,存放到一个目录下面,E:\aa.txt public sealed class Program{ public static void Main(){ System.Con ...

  6. [C++]引用浅析

    Date:2013-12-22 Summary: 引用数据类型的一些概念记录(沟通中提到引用必须结合语境才能知道说的是引用变量还是“引用”这一行为,再次提到引用指的一般是引用变量) Contents: ...

  7. Swift 学习笔记 (三) 之循环引用浅析

    原创:转载请注明出处 110.自动引用计数实践 下面的例子展示了自动引用计数的工作机制.例子以一个简单的Person类开始,并定义了一个叫name的常量属性: class Person {     l ...

  8. 读经典——《CLR via C#》(Jeffrey Richter著) 笔记_运行时解析类型引用

    public sealed class Program{ public static void Main() { System.Console.WriteLine("Hi"); } ...

  9. 关联容器--保存指针时要指定容器的比较类型---引用Effective STL

    无论何时你建立指针的关联容器,注意你也得指定容器的比较类型.大多数时候,你的比较类型只是解引用指针并比较所指向的对象(就像上面的StringPtrLess做的那样).鉴于这种情况,你手头最好也能有一个 ...

随机推荐

  1. JQuery 绑定事件时传递参数的实现方法

    如题,比如我想在$(":text").bind("keyup",funcionName);将当前的文本框作为参数传递给 functionName所代表的函数,应 ...

  2. compass(sass)+seajs+frozenui+frozenjs+svn主干分支

    1.compass框架 sass编译 1.compass create 项目名 2.cd目录,执行compass watch 2.frozen框架 js(frozen.js),css(global.c ...

  3. 解决IE 下div与img重叠无法触发鼠标事件的问题

    在IE下当我想在img标签上层显示一个div元素时,此时如果该div的background为空白(没有设置图片.或者颜色填充),会导致该div的鼠标事件失效:如果设置border为1px solid ...

  4. Hbase实例

    import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.had ...

  5. 安装使用adobe_photoshop_cs6

    1.先断开网络,安装官方原版PS程序. 2.在安装程序界面选择“试用”安装. 3.等安装完成后,退出程序. 4.复制破解补丁到安装路径覆盖,如:D:\Program Files\Adobe\Adobe ...

  6. Linux PS 命令详解

    Linux操作系统PS命令详细解析 要对系统中进程进行监测控制,用 ps 命令满足你. /bin/ps ps 是显示瞬间行程的状态,并不动态连续:如果想对进程运行时间监控,应该用 top 工具. ki ...

  7. BZOJ 3715: [PA2014]Lustra

    Description Byteasar公司专门外包生产带有镜子的衣柜.刚刚举行的招标会上,有n个工厂参加竞标.所有镜子都是长方形的,每个工厂能够制造的镜子都有其各自的最大.最小宽度和最大.最小高度. ...

  8. How Does #DeepDream Work?

    How Does #DeepDream Work? Do neural networks hallucinate of electronic dogs? If you’ve been browsing ...

  9. 自定义 Lint 规则简介

    上个月,笔者在巴黎 Droidcon 的 BarCamp 研讨会上聆听了 Matthew Compton 关于编写自己的 Lint 规则的讲话.深受启发之后,笔者想就此话题做进一步的探索. 定义 如果 ...

  10. MySql从服务器延迟解决方案

    在从服务器上执行show slave status;可以查看到很多同步的参数,我们需要特别注意的参数如下:Master_Log_File:                      SLAVE中的I/ ...