0. 什么是引用
参考【深度C++】之“类型与变量”,我们知道引用是一种复合类型。
引用(reference) 为对象起了另外一个名字,引用另外一种类型。
int a = 10;
int &ra = a; // 为a起了另外一个名字,ra
关于引用,我们要了解:
- 引用的声明
- 引用的初始化
- 引用的拷贝&赋值
- 引用的使用
- 左值引用与右值引用
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. 引用的使用
使用引用,就像使用原对象一样。
引用最常见的应用,就是传递函数参数。
在以下情况,可以考虑使用引用:
- 修改外部的对象的值
- 避免拷贝开销
- 传递不能拷贝的自定义类型
C++中的参数传递,默认是值传递,这就意味着形参与实参毫无关联,除了形参的初始值是调用函数时实参的内容;而且某些成员众多的类,在传递参数时使用值传递,会增加很多开销。
bool isShorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
我们为了避免函数意外的修改原对象,通常将形参定义为常量引用。
5. 左值引用与右值引用
左值与右值,是表达式的属性。
但是我们却可以定义一种特殊的引用,来引用表达式的右值结果,即右值引用。
请注意,左值引用、右值引用指的是一种数据类型,是C++中的复合数据类型。左值和右值是表达式的属性(参考【深度C++】之“左值与右值”)。
关于右值引用,我们要了解:
- 右值引用的声明&初始化
- 右值引用的使用原则
- 左值引用与右值引用的类型转换
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个重要原则,我们必须保证接下来:
- 所引用的对象要被销毁
- 该对象没有其他的代码在使用
因此可以对右值引用进行赋值,也是为了方便处理右值引用绑定的源对象中的内存指针,使整体对象处于一种可以被销毁,但是无法被访问的安全状态(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++语言中常用的间接访问对象的方式。
通过使用引用,我们定义了一个原对象的别名,可以快速、方便的使用原对象。
使用&
定义的引用是左值引用,使用&&
定义的引用是右值引用。右值引用在对象进行移动操作时具有很好的效果。