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. Chrome远程调试Android上Chrome的页面

    下载最新的google chrome canary 手机chrome开启usb网页调试,如图所示: 在终端运行 ? SHELL adb forward tcp:9222 localabstract:c ...

  2. mysql 查看数据库大小

    select table_schema, concat(truncate(sum(data_length)/1024/1024,2),' mb') as data_size,concat(trunca ...

  3. Python工程文件中的名词解释---Module与Package的区别

    当我们在已有的Python工程文件中创建新的内容是,通常会有两种类型文件供你选择---Module和Package,对于初学者来说会搞不清楚这两种文件直接的关系.这里就来解释一下这两者之间的关系. M ...

  4. poj 2778 DNA Sequence ac自动机+矩阵快速幂

    链接:http://poj.org/problem?id=2778 题意:给定不超过10串,每串长度不超过10的灾难基因:问在之后给定的长度不超过2e9的基因长度中不包含灾难基因的基因有多少中? DN ...

  5. chrome常用插件

    1. ModHeader 功能:修改请求头部信息 安装地址:https://chrome.google.com/webstore/detail/idgpnmonknjnojddfkpgkljpfnnf ...

  6. Eclipse启动Tomcat错误:Several ports (8080, 8009) required by Tomcat v6.0 Server at localhost are already(转载)

    转载自:http://blog.csdn.net/aigochina/article/details/7891107 Eclipse启动Tomcat错误: Several ports (8080, 8 ...

  7. 深入浅出分析C#接口的作用

    1.C#接口的作用 :C#接口是一个让很多初学C#者容易迷糊的东西,用起来好像很简单,定义接口,里面包含方法,但没有方法具体实现的代码,然后在继承该接口的类里面要实现接口的所有方法的代码,但没有真正认 ...

  8. Collection和Collections的区别

    Collection 是集合类的上级接口,继承它的接口主要有set和list.Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索,排序,线程安全化等操作.

  9. Automotive Security的一些资料和心得(6):AUTOSAR

    1.1 Introduction AUTOSAR(汽车开放系统架构)是一个开放的,标准化的汽车软件架构,由汽车制造商,供应商和开发工具共同开发.它联合了汽车OEM ,供应商和开发工具供应商,其目标是创 ...

  10. 简单的php表单

    表单的三种传递机制: $_GET:不安全,传递的参数会显示在url中. $_POST:相对安全,隐形传递. $_REQUEST:宽松的,包含所有 GET.POST.COOKIE 和 FILE 的数据. ...