C++ 引用类型 — 左值引用、常引用和右值引用

在 C++ 中,引用类型是一种复合类型,可以分为左值引用常引用右值引用

在了解什么是左值引用和右值引用之前,我们需要先了解什么是左值和右值。由于 C++ 本身没有给出左值和右值的标准定义,

  • 可以取地址的,有名称的,非临时的是左值
  • 不可取地址,匿名的,临时的是右值

左值引用

左值引用是我们平时在编程时最常使用到的。它的定义形式如下:

//基本类型 &变量名 = 待绑定对象
int i = 10;
int &r = i;

引用的作用相当于是给变量取了“别名”。引用本身没有自己的内存地址,因此在定义的过程当中必须进行初始化,而且一旦和变量进行了绑定,则无法解绑并重新绑定。另外,左值引用只能和左值进行绑定。例如

string &r = string("test"); // 错误,string("test") 是临时对象,属于右值
int &r = 10 + 1;    // 错误,10 + 1 是表达式,属于右值
string &r = "test"; // "test" 是字符串字面值,属于右值

常引用

常引用是指利用 const 修饰的引用类型。由于引用本身已经绑定不可解绑,因此所用的 const 引用都是底层 const,即引用对象不能改变(俗称常引用)。由于常引用引用的是常量,所以可以使用表达式,字符串字面值等常量为常引用进行初始化。

与 const 指针的比较

根据 const 的位置的不同,可将指针中的 const 分为顶层 const (指针常量)和底层 const (常量指针),遵循“左数右指” 的原则

  1. 指针常量是常量,其指向是不能改变的。这对对象的拷贝操作没有什么影响,因为拷贝操作不会改变对象的值,没有违背“值不变原则”

  2. 常量指针是指针,其指向的内容是不能改变的。这对对象的拷贝操作会有很大的影响,因为常量指针可以指向非常量,但是非常量指针不能指向常量【安全原则:因为常量指针指向非常量是安全的,但非常量指针指向常量是危险的,因为可能一不小心可以通过非常量指针修改到常量】

右值引用

右值引用是 C++ 新标准中引入的。右值引用的定义形式为:

//基本类型 &&变量名 = 待绑定对象
int &&r = 10 + 1;

右值引用的主要作用有两个:

  1. 实现移动语义
  2. 实现完美转发
作用一、移动语义的实现。

C++ 中的 “移动语义” 是相对于 “拷贝语义” 的一个概念。在引入”移动语义“之前,C++ 主要利用拷贝功能来实现对象的移动,通常的操作是在新分配好的内存空间上调用拷贝构造函数,将旧对象复制到新内容中,然后再释放旧对象。而”移动语义“的引入使得 C++ 允许直接将旧对象的所有权直接转让给新分配的内存空间,这样一来避免了旧对象的复制与释放,极大地提高了代码的运行效率。如果说 ”拷贝语义“ 描述了对象的拷贝功能,那么”移动语义“ 描述的则是对象的剪切功能。注:转让意味着将所有权交给新区域的同时,还要放弃自身的所有权

C++ 中的 ”移动语义“ 主要利用右值引用来实现。C++ 允许将一个右值引用 rr 绑定到一个临时对象上,从而将该临时对象的生命周期延长到与 rr 一样长。这样一来,我们便可以通过 rr 在程序当中的任何位置访问该临时对象。

我们先来看一个 swap 模板函数的例子

//基于 “拷贝语义” 的 swap 函数
template <typename T>
void swap(T &a, T &b){
    T tmp = a;  //拷贝构造:将 a 拷贝至 tmp
    a = b;      //拷贝赋值:将 b 拷贝至 a
    b = tmp;    //拷贝赋值:将 tmp 拷贝至 b
}
//基于 “移动语义” 的 swap 函数
template <typename T>
void swap(T &&a, T &&b){
    T tmp(std::move(a));    //移动构造:将 a 的所有权转让给局部对象 tmp
    a = std::move(b);       //移动赋值:将 b 的所有权转让给 a
    b = std::move(tmp);     //移动赋值:将 tmp 的所有权转让给 b
}

正如前面关于左值和右值定义中描述的那样,在基于 “移动语义” 的 swap 函数中, tmp、a 和 b 都是有名称的变量,因此它们都是左值。要想实现 “移动” 的功能,就必须将左值引用转变为右值引用,而 std::move 标准库函数则正好能够完成这一任务,它的作用是接受一个左值并返回该左值的右值引用。通过 std::move 和 “移动语义”,第二个版本的 swap 比第一个版本的 swap 函数减少了三次拷贝数据的过程,大大提高了 swap 的运行效率。

注:关于移动构造函数和移动赋值运算符可见:C++ 拷贝控制(二) — 移动构造函数和移动赋值运算符

作用 二:完美转发

完美转发主要应用于这样一组场景:我们需要将一个函数的参数原封不同地传递给另一个函数。在 C++ 中,函数参数除了类型和值以外,还有 const 和 non-const 以及 左值和右值 两组属性。所谓的完美转发,就是在将一个函数的参数传递给另一个参数时,参数的类型、值以及所有属性都不能改变,这在泛型编程当中有着广泛的应用。我们来看下面两段代码:

//代码 1
template <typename T> void f1(T &t){
    cout << "hello world" << endl;
}
template <typename T> void f1(const T &t){
    cout << "hello world" << endl;
}
int main(void){
    int i = 10;
    const int ci = 11;
    f1(i);  //调用 f1(int&),模板参数 T 是 int
    f1(ci); //调用 f1(const int&),模板参数 T 是 int
    f1(31); //调用 f1(const int&),模板参数 T 是 int
    return 0;
}

代码 1 中,为了兼容 f1(i)、f1(ci) 与 f1(31) 这三种调用方式,我们需要为 f1 函数重载 T& 和 const T& 两个版本。这种做法不仅费时费力,而且随着函数参数的增加,所需要重载的模板数量也会急速增加。我们来看看右值引用是如何解决这个问题的:

//代码 2
template <typename T> void f1(T &&t){
    cout << "hello world" << endl;
}
int main(void){
    int i = 10;
    const int ci = 11;
    f1(i);  //实参是一个左值,模板参数 T 是 int&
    f1(ci); //实参是一个左值,模板参数 T 是 const int&
    f1(31); //实参是一个右值,模板参数 T 是 int
    return 0;
}

针对模板函数的实现,C++ 11 在原有的正常绑定规则上增加一个新的绑定规则 —— 引用折叠,具体规则如下:

  • 若模板函数 func 的形参类型为 T&, 则不论传递给 func 的实参是左值还是右值,一律统统折叠为左值引用
  • 若模板函数 func 的形参类型为 T&&, 则传递给函数的左值实参折叠为左值引用,右值实参折叠为右值引用

总之一句话:在定义函数模板时使用右值引用做形参,能够保证参数的属性(const 属性和 左/右值属性)不发生变化。

代码 2 中,虽然 i 和 ci 是左值,但由于发生了引用折叠,所以 f1(i) 和 f1(ci) 是合法的。在引用折叠的规则下,无论一个函数有多少个参数,借助右值引用我们都只需要定义一个函数模板即可,这就大大减少了编程时的低效劳动。

右值引用和常引用的异同

相同点:

  • 右值引用和常引用都允许和表达式,字符串字面值等进行绑定

不同点:

  • 常引用可以和普通变量绑定,而右值引用只能绑定到右值上面
  • 常引用类型的变量是不可修改的,而右值引用类型的变量是可以修改的

引用和指针的区别

  1. 引用和指针都是复合类型,但指针是变量,有自己的内存地址,而引用是别名,没有自己的内存地址
  2. 引用一旦定义就必须初始化,而指针可以先定义,然后再初始化
  3. 引用一旦和某个具体的变量绑定后,就不能再解除绑定,而指针变量在指向某一变量后,依然可以改变指向。
  4. 存在多维指针,但不存在多维引用【C++ 中不允许直接定义引用的引用,但可以通过类型别名或者模板类型参数间接定义】
  5. 因为引用没有自己的内存地址,所以只存在引用指针的引用,但不存在指向引用的指针

注:实际上,C++ 当中引用的实现也是利用到了 const 指针来实现的,但是由于编译器做了一部分工作,因此引用本身对于程序员而言是透明的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335