【深度C++】之“引用”

0. 什么是引用

参考【深度C++】之“类型与变量”,我们知道引用是一种复合类型

引用(reference) 为对象起了另外一个名字,引用另外一种类型。

int a = 10;
int &ra = a;  // 为a起了另外一个名字,ra

关于引用,我们要了解:

  1. 引用的声明
  2. 引用的初始化
  3. 引用的拷贝&赋值
  4. 引用的使用
  5. 左值引用与右值引用

1. 引用的声明

如之前的示例,使用符号&

int a = 10;
int &ra = a;  // 定义了一个引用

2. 引用的初始化

声明一个引用时必须初始化,编译器将引用的对象和引用变量绑定在一起。

用于初始化引用的目标类型,必须和引用所定义的严格匹配:

int a = 10;
double &ra = a;  // ERROR!类型不匹配

但是有2个例外情况

2.1 初始化的例外1

一是const限定符修饰的常量引用,可以用一个非常量类型的目标类型甚至是临时量绑定给引用:

int a = 10;

// 这样是OK的,即使a不是const
const int &ra = a;

// 这样也是ok的,可以执行类型转换
// 且此时绑定的是一个临时量
const double &rda = a;

具体的原因,请参考【深度C++】之“const”

这样的绑定,只不过是引用的“自以为是”,我们告诉引用我们需要一个常量引用,所以引用就不由自主的不去修改源目标,但是它不关心原目标是否是真的不可以修改。

2.2 初始化的例外2

二是可以将子类实例绑定到父类引用:

class Base {
    int a;
public:
    Base() = default;
    virtual void print() { cout << "Base"; }
};

class Derived : public Base {
public:
    void print() override { cout << "Derived"; }
};

int main() {
    Derived d1;
    Base &b = d1;
    b.print();  // 输出Derived
}

这是为了多态。

3. 引用的拷贝&赋值

引用不是对象,使用引用就相当于是使用原对象,因此引用不存在关于拷贝和赋值问题的讨论。

引用在定义之后,就会与原对象一直绑定,中途不能修改。

4. 引用的使用

使用引用,就像使用原对象一样。

引用最常见的应用,就是传递函数参数。

在以下情况,可以考虑使用引用:

  1. 修改外部的对象的值
  2. 避免拷贝开销
  3. 传递不能拷贝的自定义类型

C++中的参数传递,默认是值传递,这就意味着形参与实参毫无关联,除了形参的初始值是调用函数时实参的内容;而且某些成员众多的类,在传递参数时使用值传递,会增加很多开销。

bool isShorter(const string &s1, const string &s2) {
    return s1.size() < s2.size();
}

我们为了避免函数意外的修改原对象,通常将形参定义为常量引用

5. 左值引用与右值引用

左值与右值,是表达式的属性。

但是我们却可以定义一种特殊的引用,来引用表达式的右值结果,即右值引用

请注意,左值引用、右值引用指的是一种数据类型,是C++中的复合数据类型。左值和右值是表达式的属性(参考【深度C++】之“左值与右值”)。

关于右值引用,我们要了解:

  1. 右值引用的声明&初始化
  2. 右值引用的使用原则
  3. 左值引用与右值引用的类型转换

5.1 右值引用的声明&初始化

右值引用,使用&&运算符声明。为右值引用初始化的一定是右值,不可以使用左值代替。如下:

int a = 0;
int &&rr_a = a + 3;

可以将右值引用绑定在要求转换的表达式、字面值常量和返回右值的表达式上,不可以将右值引用绑定在左值上。因此下面的代码十分费解:

int &&rr_1 = 42;
int &&rr_2 = rr_1;  // 编译错误

rr_1单独使用,是一个表达式——一个没有运算符的表达式,它返回的是左值;因此不可以将右值引用绑定到左值rr_1上。

5.2 右值引用的使用原则

我们是可以使用右值引用进行赋值的(2.2例外情况),这也很意外,因为明明是右值的引用,却可以进行内容上的修改。因为右值引用是具有表达式的左值属性(如上例中的rr_1)。

int a = 0;
int &&rr_a = a + 3;
rr_a = 6;

然而是否修改成功,就要看编译器以及所引用的对象了。

使用右值引用有2个重要原则,我们必须保证接下来:

  1. 所引用的对象要被销毁
  2. 该对象没有其他的代码在使用

因此可以对右值引用进行赋值,也是为了方便处理右值引用绑定的源对象中的内存指针,使整体对象处于一种可以被销毁,但是无法被访问的安全状态(nullptr算是一种情况)。

对于返回左值的表达式:

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。

对于返回右值的表达式:

返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但是我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

(以上两句话摘选自摘选自《C++ Primer》第5版本,抽象却精湛,建议背诵)

使用右值引用的两个原则,在对象移动时,可以完美的使用。C++引入右值引用,也是为了进行对象的移动,详细内容,请参考【C++深陷】之“构造函数”移动构造函数的相关内容以及【C++深陷】之“运算符重载”中移动赋值运算符相关内容。

5.3 类型转换

既然是数据类型,就存在转换关系。

我们可以使用标准库函数std::move得到左值的右值引用类型。

int &&rr_1 = 42;
int &&rr_2 = std::move(rr_1);

move调用告诉编译器,我们有一个左值rr_1,但是我希望像一个右值一样处理它。

我们必须保证,接下来除了对rr_1赋值或销毁它,我们不再使用它。我们不能对rr_1的值作任何假设。

《C++ Primer(第5版)》推荐使用std::move而不是move,可以避免潜在的命名冲突。

6. 总结

引用是一种复合类型,是C++语言中常用的间接访问对象的方式。

通过使用引用,我们定义了一个原对象的别名,可以快速、方便的使用原对象。

使用&定义的引用是左值引用,使用&&定义的引用是右值引用。右值引用在对象进行移动操作时具有很好的效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值