啃书《C++ Primer Plus》之 C++ 引用

笔者正在学习C++语言,啃书系列将会持续更新,希望可以同大家一起学习,一起进步。如果文章有帮助的话,记得点赞、收藏、关注一条龙哦。

系列文章:
啃书《C++ Primer Plus》之 C++ 函数指针
啃书《C++ Primer Plus》之 C++ 名称空间1
啃书《C++ Primer Plus》之 C++ 名称空间2


这一节,我们来梳理C++中相对于C新增的机制——引用。下面是本节的导图
在这里插入图片描述


引用概述

在了解引用的各种使用规范和技巧之前,我们先来对引用进行一些基本的了解。作为C++语言新引进的机制,它的存在更多是为了优化在C语言编程过程中的一些过程,同时也和C++新增的特性——面向对象有关。
所以,了解引用这个概念,我们主要从它是什么和它用来解决什么问题两方面进行。

引用是什么

要回答引用是什么这个问题其实很简单。引用,其实就是给已定义的变量别名。通俗些说,就是给一个已经存在的变量起一个外号。在这之后,外号就是这个变量的另一个名称。

引用用来解决什么问题

有时我们也将起外号这个过程放在函数的参数值传递中,这样一来,“起外号”这个行为就有了新的意义:它让一个变量以一个新的名称以参数的形式出现在了另一个函数中,这同以往的传递值很不相同。
更多时候,我们使用引用来传递整个对象或是结构,这种起外号的方式省去了重新创建并拷贝的过程打打节约了时间和空间。在这个意义上,它的作用倒是和指针有些类似,虽然笔者坚信引用底层是通过指针实现的,但是他们终究是存在区别的。关于概述,就先说到这里,只通过比喻来认识引用是不够的,我们需要在下面对其进行更加深入的了解。


引用的创建和赋值

有关引用的创建于赋值,这一块的内容相对简单。我们先说引用的创建形式:

类型名 &应用变量名称 = 被引用变量;

在这个行驶中,需要注意到,引用的创建类似于变量的创建(毕竟只是起一个外号)。不同于创建变量,创建引用的时候需要在类型名后面加上一个 & 符号表示这是一个引用变量,这个机制倒是同指针的创建有些许相似。
在此需要注意的是:
创建引用使用的 & 符号不是取地址符号!!
创建引用使用的 & 符号不是取地址符号!!
创建引用使用的 & 符号不是取地址符号!!

(这个道理同创建指针使用 * 不是取值符号相同,它们在不同的地方表示不同的含义。但是这并不影响我将重要的事情重复三遍!)

说完创建形式,还有一个点需要注意,也许你也注意到了,在说明创建形式的时候,有意的给创建的引用变量赋予了初值。这是非常必要的。因此,我有必要将它也强调三遍:
引用变量创建时必须进行初始化!!
引用变量创建时必须进行初始化!!
引用变量创建时必须进行初始化!!

嗯,舒服了~~

那么,我们接着说,引用这种必须进行初始化的设计,是否尤其用意呢?或者说,我们可不可以在初始化过后再使他引用另一个变量呢?
对于后者,答案是不可以!其实这个问题很好回答,只需要稍加思考,就会得到答案。
从初始化的形式来看,让引用变量指向一个变量的方式是:

引用变量 = 普通变量

那如果仍保持这种等号的赋值含义,这个引用变量如何能想普通变量一般赋予其他变量的值呢?
例如如下代码:

扫描二维码关注公众号,回复: 11296865 查看本文章
int b = 10;
int &a = b;
int c = 20;
a = c;

我们更应该把最后一句解释为 a 更改为引用 c 呢?还是解释为把 a 的值改变为 c 的值呢?
直觉告诉我们,应该是后者。而事实也正是如此。因此,让我们把这条规则再来念上三遍:
引用在初始化后不能转而引用其他变量!!
引用在初始化后不能转而引用其他变量!!
引用在初始化后不能转而引用其他变量!!

你可以这样认为:引用变量在初始化后就变成了一个普通变量。 也因此,可以引用变量引用一个引用变量也是允许的:

int a = 0;
int &b = a;
int &c = b;

这样,a b c 就是同一个变量了。


使用引用

说完了如何创建一个引用变量,我们来探索一下如何使用它。这一小节的内容有些杂,主要是因为它会同很多C语言或C++语言的机制产生作用。例如,引用会被 const 修饰,引用作为参数或返回值、引用之作用于对象等等。下面我们来分别介绍一下。

const和引用

首当其冲 首先介绍的是 const 关键字在引用变量中的使用。众所周知,const 关键字本意是修饰变量为常量,意味着它不会被修改。那么,如果我们将 const 关键字使用在引用变量的声明,引用变量会产生什么样的性质呢?
在前面,我们讨论过引用变量的再赋值问题,说到引用变量一旦初始化就不能再转而引用其他的变量。因此,引用并不会像指针那样产生两个可被 const 修饰的部位(也就是说一个引用只能始终指向一个变量,这点是已经存在 const 性质的)。那么剩下的可被修饰地方,就是它的值了。
简单说,就是被 const 修饰的引用变量作为常量存在,其值不能够发生改变。

但是这就产生一个问题了,如果引用变量是被 const 修饰的,而被引用变量不是,会发生什么事情呢?
看如下代码:

int x - 9;
const int &y = x;
x = 5;
y = 15;

编译结果是:在这里插入图片描述
所以,结论显而易见,就是被修饰为 const 的名称不能改值,而没有被 const 修饰的却可以。

临时变量

除了被引用变量是否被修饰的问题,有些时候,被 const 修饰的引用在初始化时还会遇到一些情况,那就是被引用变量同引用变量类型的不匹配问题,这可就尴尬了。这时,编译器往往会为引用变量创建一个适配类型的临时变量给引用。但是这样的操作会导致引用失去了原有的作用,变成了另外一个变量。后来经过一代代版本的不断修改,临时变量的机制被保留了下来,但却别严格控制使用,具体的,现在的标准会使编译器在一下情况创建临时变量:

  • 被引用变量的类型正确,但不是左值
  • 被引用变量的类型不正确,但可以被转换成正确形式

(注1:有关左值的概念可以参考书籍,这里可以简单地理解为可被引用的数据对象而非数据)
(注2:书中的说法是实参,这里将其一般化到初始化上。笔者不能保证百分百正确,但在多次试验下表明上述结论是成立的)
具体的,我们来看下面简单的例子:
由于被 const 修饰,不能更改值,因此我们采用地址来表示不同。

#include<iostream>
using namespace std;
int main()
{
    long long a = 10;
    const int &b = a;  //情况2
    const int &c = 10; //情况1

    cout << "the address of a is :" << &a << endl;
    cout << "the address of b is :" << &b << endl;
    cout << "the address of c is :" << &c << endl;
}

运行结果如下:在这里插入图片描述
结果表示 a 和 b 不在同一个地址下存放,这就直接的说明了他们不是同一个变量。
(引用变量和被引用变量的地址是相同的,这点读者可以亲自试一试。)

有关临时变量的内容,我们就说到这里,可以说,临时变量是一种语言兼容性与逻辑严密性的一种妥协之举,也是基于 const 修饰保证不进行修改的前提。(不进行修改的话,我们只使用变量的值,这样一来临时变量与原始变量在使用时候并没有什么不同,所以在这里是可行的。)
最后,还是需要强调一下,临时变量机制是 const 修饰的引用具有的性质,这点不要同一般的引用搞混淆。

引用对象

说到引用,不得不说它在对象上的使用。其实引用最开始设计的初衷,就是为了便于对象和结构尤其是类对象的传递。这一点在之后的很多面向对象语言中有所体现(Java就只保留了引用而放弃了指针,当然它也对引用机制做出了很大的改变。)。
在引用对象时,操作方式与写法同引用一个变量没有什么不同。

作为参数传递

对对象的引用在函数传参时起到了很大的作用,当一个对象作为参数传递给函数时,如果按照以往的值传递,程序将会创造一个一模一样的对象作为实参,而引用则没有这个问题。这样一来,就能为程序节省很多的时间和空间。但是这样也就意味着函数中的对象与传递的对象是同一个,修改函数中的对象也会引起函数外对象的改变。所以我们提倡尽可能使用 const 修饰的实参。

返回引用

既然引用可以被当做实参接受传递,那么引用应当也支持被作为返回值返回。不过,返回一个引用有什么用呢?
再说引用的作用之前,我们先来梳理一下什么东西可以用来被当做引用返回:

首先需要禁止一件事情,那就是返回一个作用域仅限于函数的局部对象。
接着说,通常情况下,我们返回两种对象:

  • 作为参数传递进来的对象
  • 被 new 关键字创造出来的对象

这些不难理解,而且对于后者,我们还可以由此将 new 关键字获取对象的过程封装到函数中进而使用引用而非指针来获得对象:

#include<iostream>
using namespace std;
class Person
{
public:
    Person(int a,int b)
    {
        age = a;
        code = b;
    }
private:
    int age;
    int code;
};

Person &getPerson()
{
    return *(new Person(10,110));
}
int main()
{
    Person tom = getPerson();
}

说过了那些可以被用来进行引用返回,我们来谈谈返回引用有什么好处。

首先是同上文提到的传递参数的原因相同,返回引用也可以大大节省程序在临时对象上的开支。

除此之外,返回引用还有一个重要的作用:套娃!(当然,这个名字是我自己起的,主要是想引出对象方法的连续调用)
如果一个函数返回了一个引用,那么这个函数在程序中使用时本身就代表着一个有效的左值,可以当做一个对象来调用其中的方法。
而如果它返回的是对象自身,也就是 this 指针指向的对象(有关 this 指针的知识以后会提到,简单的说,this指针就是指向对象本身的指针,它隐式的包含在方法参数列表中)。那么这个方法可以连续调用,这在字符串的处理和IO流的处理中很有用处。
参考如下程序:

#include<iostream>
using namespace std;
class Person
{
public:
    Person(int a,int b)
    {age = a;code = b;}
    Person &grow();
private:
    int age;
    int code;
};

Person &getPerson()
{
    return *(new Person(10,110));
}
int main()
{
    Person tom = getPerson();
    tom.grow().grow().grow();
}

Person &Person::grow()
{
    this->age++;
    return (*this);
}

在这份程序中,我们就实现了对 tom grow 方法的连续调用。

返回const引用

如果我们为返回引用类型加上 const 修饰,这就代表着返回的引用将是一个被 const 修饰的引用,这意味着,直接使用函数返回的引用进行赋值或是调用非 const 成员方法是不被允许的。但如果使用一个对象接下了这个引用,那么这个 const 的修饰就没有了。
如果你感觉上面的描述比较绕,那么下面的程序展示了这些问题:

#include<iostream>
using namespace std;
class Person
{
public:
    Person(int a,int b)
    {age = a;code = b;}
    const Person &grow();
    void setAge(int);
private:
    int age;
    int code;
};
const Person &getPerson()
{
    return *(new Person(10,110));
}
int main()
{
    Person tom = getPerson();
    getPerson().setAge(230); //这这里的调用是错误的
    tom.grow().grow();       //这种调用也是错误的

    tom.setAge(20);          //这个调用是允许的
    tom.grow();              //这个调用是允许的

    Person jack = tom.grow();
    jack.grow();             //这个调用也是允许的
}
const Person &Person::grow()
{
    this->age++;
    return (*this);
}
void Person::setAge(int a)
{
    this->age = a;
}

总的说,给返回值加上 const 只能限制调用函数时整体代表的左值的更改。

继承问题

最后一点,是关于对象在引用下的继承问题,不过这里说是继承,笔者更喜欢使用多态这个概念(这个内容在这里有些超纲,在以后说到面向对象机制的文章中我们还会详细介绍,跳过这块内容将不影响读者对引用的理解,这里就是简单的提一下)

我们先说多态的体现:

  • 父类的引用指向自己的子类对象;
  • 父类的引用接收自己的子类对象。

按照这种体现,父类的引用可以指向子类对象也可以接受子类对象。所以在函数传参使用引用做实参时,可以使用父类做引用类型,当形参传递一个相同的类对象或子类对象时,这个引用可以很好的接受而不会报错,这个过程也成为上转型。

例如书上提到,ofstream 是 ostream 的子类,因此一个函数完全可以以 ostream 类型的引用实参而接受 ofstream 类型的对象。

好了,有关继承问题就说到这里。


引用、指针、值传递

说了这么多有关引用的使用技巧,现在是时候来辨析一下它和传统的传值方式的异同及其分别的使用场合,这有助于我们更加正确的使用引用在正确的地方。

使用引用的情况

引用参数的主要原因在于:

  • 可以直接修改调用函数中的数据对象
  • 减少因复制对象产生的时间与空间的浪费

因此我们常在需要函数修改或是传递较大结构尤其是是类对象的时候使用引用。

另外,如果只是传递使用而不修改则尽可能的使用 const

使用指针的情况

当需要修改变量值的时候,使用指针也可以办到。其实,指针才是最原始的选择,从C语言一开始就是这么设计的。

另外,当需要传递数组时,只有指针可以选择。

还需要注意的是,如果要修改基础数据类型,尽可能使用指针来完成,这可以避免实现时的一些失误。

使用值传递的情况

由于有了引用指针两种包容性很强的传值方式,值传递的生存空间非常有限。通常只是在传递基础数据类型或是很小的结构时才会选择使用值传递。

猜你喜欢

转载自blog.csdn.net/wayne_lee_lwc/article/details/105043499