C++特性之引用

C++特性之引用

本章内容:
1 引用的不同用例
1.1 引用变量
1.2 引用数据成员
1.3 引用参数
1.4 引用作为返回值
1.5 使用引用还是指针
1.6 右值引用


1 引用

  • 在C++中,引用是变量的别名。所有对引用的修改都会改变被引用的变量的值。可将引用当作隐式指针,这个指针没有取变量地址和解除引用的麻烦。
  • 也可以将引用当作原始变量的另一种名称。可以创建单独的引用变量,在类中使用引用数据成员,将引用作为函数和方法的参数,也可以让函数或者方法返回引用。

1.1 引用变量

  • 引用变量在创建时必须初始化,如下:
    int ival = 3;
    int &iRef = x;
  • 在赋值后,iRef就是ival的另一个名称。使用iRef就是使用ival的当前值。对iRef赋值会改变ival的值。
    无法在类外面声明一个引用而不初始化它:
    int &emptyRef;      //编译出错!
  • 不能创建对未命名值(例如一个整数字面值)的引用,除非这个引用是一个const值。在下面的示例中,unnamedRef1将无法编译,因为这是一个针对常量的non-const引用。
  • 这条语句意味着可以改变常量5的值,而这样做没有意义。由于unnamedRef2是一个const引用,因此可以运行,不能写成"unnamedRef2=7"。
    int &unnamedRef1 = 5;           //编译出错
    const int &unnamedRef2 = 5;   //正常运行
(1) 修改引用
  • 引用总是引用初始化的那个变量:引用一旦创建,就无法修改。这一规则导致了许多令人迷惑的语法。如果在声明一个引用时用一个变量"赋值",那么这个引用就指向这个变量。然而,如果在此后使用变量对引用赋值,被引用变量的值就变为赋值变量的值。引用不会更新为指向这个变量。示例代码如下:
    int x = 3,y = 4;
    int &iRef = x;
    iRef = y;       //Changes value of x to 4. Doesn't make iRef refer to y.
  • 如果试图在赋值时取y的地址,以绕过这一限制:
    int x = 3,y = 4;
    int &iRef = x;
    iRef = &y;      //编译出错
  • 上面的代码无法编译。y的地址是一个指针,但iRef声明为一个int的引用,而不是一个指针的引用。
  • 如果将一个引用赋值给另一个引用时,只是修改了其指向的值,而不是修改所指向的引用变量。(在初始化引用之后无法改变引用所指的变量;而只能改变该变量的值)
(2) 指针的引用和指向引用的指针
  • 可以创建任何类型的引用,包括指针类型。下面给出一个指向int指针的引用例子:
    int *pVal;
    int *&ptrRef = pVal;
    ptrRef = new int;
    *ptrRef = 5;
  • 这一语法有一点奇怪:你可能不习惯看到*和&彼此相邻。然而,该语义上很简单:ptrRef是pVal的引用,pVal是一个指向int的指针。修改ptrRef会更改pVal。指针的引用很少见,但是在某些场合下很有用,在1.3节中会讨论这一内容。
  • 注意:
    (i)对引用取地址的结果与被引用变量取地址的结果是相同的。
    (ii)无法声明引用的引用,或者指向引用的指针。

1.2 引用数据成员

  • 类的数据成员可以引用,但是如果不是指向其他变量,引用就无法存在。因此,必须在构造函数初始化器(constructor initializer)中初始化引用数据成员,而不是在构造函数体内。下面举例说明:
    class MyClass
    {
    public:
        MyClass(int &iRef) : m_ref(iRef) {}
    private:
        int &m_ref;
    };

1.3 引用参数

  • C++程序员通常不会单独使用引用变量或者引用数据成员。引用经常用作函数或者方法的参数。默认的参数传递机制是值传递:函数接收参数的副本。修改这些副本时,原始的参数保持不变。引用允许指定另一种向函数传递参数的语义:按引用传递。当使用引用参数时,函数将引用作为参数。如果引用被修改,最初的参数变量也会修改。下面给出交换两个数的例子来说明:
    void swap(int &first, int &second)
    {
        int temp = first;
        first = second;
        second = temp;
    }
  • 可以采用下面的方式调用这个函数:
    int x = 5, y = 6;
    swap(x, y);
  • 当使用x和y做参数调用函数swap()时,first参数初始化为x的引用,second参数初始化为y的引用。当swap()修改first和second时,x和y实际上也被修改了。
    就像无法使用常量初始化普通引用变量一样,不能将常量作为参数传递给按引用传递参数的函数:
    swap(3, 4);     //编译出错
(1) 指针转换为引用
  • 某个函数或者方法需要一个引用做参数,而你拥有一个指向被传递值的指针,这是一种常见的困境。在此情况下,可以对指针解除引用(dereferencing),将指针"转换"为引用。这一行为会给出指针所指的值,随后编译器用这个值初始化引用参数。例如,可以这样调用swap():
    int x = 5, y = 6;
    int *px = &x;
    int *py = &y;
    swap(*px, *py);
(2) 按引用传递与按值传递
  • 如果要修改参数,并修改传递给函数或者方法的变量,就需要使用按引用传递。然而,按引用传递的用途并不局限于此。按引用传递不需要将参数副本复制到函数,在有些情况下会带来两面的好处:
    (i)效率:复制较大的对象或者结构需要较长的时间。按引用传递只是把指向对象或者结构的指针传递给函数。
    (ii)正确性:并非所有对象都允许按值传递,即使允许按值传递的对象,也可能不支持正确的深度复制(deep copying)。(如果需要深度复制,动态分配内存的对象必须提供自定义复制构造函数。)
  • 如果要利用好这些好处,但不想修改原始对象,可将参数标记为const,从而实现按常理引用传递参数。按引用传递的这些优点意味着,只有在参数是简单的内建类型(int或double),且不需要修改参数的情况下才应该使用按值传递。其他情况下都应该按引用传递。

1.4 引用作为返回值

  • 可以让函数或者方法返回一个引用,这样做的主要作用是提高效率。返回对象的引用不是返回整个对象可以避免不必要的复制。当然,只有涉及的对象在函数终止之后仍然存在的情况才能使用这一技巧。(如果变量的作用域局限于函数或者方法,例如:堆栈中分配的变量,在函数结束时会被销毁。这个时候绝对不能返回这个变量的引用。)
  • 返回引用的另一个原因是希望将返回值直接赋为左值(lvalue)(赋值语句在左边)。一些重载的运算符通常会返回引用。

1.5 使用引用还是指针

  • 在C++中,引用有可能被认为是多余的:几乎所有使用引用可以完成的任务都可以用指针来代替完成。例如,可以这样编写swap()函数:
    void swap(int *first, int *second)
    {
        int temp = *first;
        *first = *second;
        *second = temp;
    }
  • 然而,这些代码不如使用引用版本那么清晰:引用可以使程序整洁并易于理解。此外,引用比指针安全:不可能存在无效的引用,也不需要显式地解除引用,因此不会遇到像指针那样的解除引用问题。
  • 大多数情况下,应该使用引用而不是指针。对象的引用甚至可以像指向对象的指针那样支持多态性。只有在需要改变所指地址时,才需要使用指针,因为无法改变引用所致的对像。例如,动态分配内存时,应该将结果存储在指针而不是引用中。需要使用指针的第二种情况是可选参数。例如,指针参数可以定义为带默认值nullptr的可选参数,而引用参数不能这样定义。
  • 还有一种方法可以判断使用指针还是引用作为参数和返回类型:考虑谁拥有内存。如果接受变量的代码负责释放相关对象的内存,必须使用指向对象的指针,最好是智能指针,这是传递拥有权的推荐方式。如果接受变量的代码不需要释放内存,那么应该使用引用。
  • 注意:如果不需要改变所指的地址,就应该使用引用而不是指针。

1.6 右值引用

  • 在C++中,左值(lvalue)是可以获取其地址的一个量,例如一个有名称的变量。由于经常出现在赋值语句的左边,因此称其为左值。另一方面,所有不是左值的量都是右值(rvalue),例如常量值,临时对象或者临时值。通常右值位于赋值运算符的右边。
  • 右值引用是一个对右值(rvalue)的引用。特别地,这是一个当右值是临时对象时使用的概念。右值引用的目的是提供在涉及临时对象时可以选用的特定方法。由于知道临时对象会被销毁,通过右值引用,某些涉及复制大量值的操作可以通过简单的复制指向这些值的指针来实现。
  • 函数可以将&&作为参数说明的一部分(例如 type&&name),来指定右值引用参数。通常,临时对象被当作const type&,但当函数重载使用了右值引用时,可以解析临时对象,用于该重载。下面的示例说明了这一点。代码首先定义了两个incr()函数,一个接受左值引用;另一个接受右值引用:
    // Increment value using lvalue reference parameter.
    void incr(int &value)
    {
        cout << "increment with lvalue reference" << endl;
        ++value;
    }
    // Increment value using rvalue reference parameter.
    void incr(int &&value)
    {
        cout << "increment with rvalue reference" << endl;
        ++value;
    }
  • 可以使具有名称的变量作为参数调用incr()函数。于是a是一个具有名称的变量,因此调用接受左值引用的incr()函数。调用完incr()后,a的值将是11。
    int a = 10, b = 20;
    incr(a);        //调用incr(int &value);
  • 还可以用表达式作为参数来调用inrc()函数。此时无法使用接受左值引用作为参数的incr()函数,因为表达式a+b的结果是临时的,这不是一个左值。在此情况下,会调用右值引用版本。由于参数是一个临时值,当incr()函数调用结束后,会丢失这个增加的值。
    incr(a + b);    //将调用incr(int &&value);
  • 字面量也可以作为inrc()调用的参数,此时同样会调用右值引用版本,因为字面量不能作为左值。
    incr(3);        //将调用incr(int &&value);
  • 如果删除接受左值引用的incr()函数,使用名称的变量调用incr(),例如:incr(b),此时会导致编译错误,因为右值引用参数(int &&value)永远不会与左值(b)绑定。如下所示可以使用std::move()将左值转换为右值,强迫编译器调用incr()的右值版本。当incr()调用结束后,b的值为21。
    incr(std::move(b)); //将调用incr(int &&value);
  • 右值引用并不局限于函数的参数。可以声明右值引用类型的变量,并对其赋值,尽管这种用法并不常见。查下看如下代码:
    int &i = 2;     //invalid:reference to a constant
    int a = 2, b = 3;
    int &j = a + b; //invalid:reference to a temporary
  • 使用右值引用后,下面的代码完全合法:
    int &&i = 2;
    int a = 2, b = 3;
    int &&j = a + b;
  • 前面示例中单独使用右值引用的情况很少见。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容