c++引用
前言
c++ 11之前,将非左值表达式统称右值,函数为左值,引用能绑定到左值但唯有const引用才能绑定到右值。自c++ 11开始,在左值(left value / lvalue)的基础上增加了纯右值(pure rvalue / pvalue)和消亡值(expiring value / xvalue),并对以上三种类型通过是否具名(identity)和可移动(moveable),又增加了泛左值(generalizad lvalue / glvalue)和右值(right value / rvalue)两种组合类型。
- 具名:可以确定表达式是否与另一表达式指代同一实体,例如通过比较它们所标识的对象或函数的(直接或间接获得的)地址
- 可被移动:移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定于这个表达式
lvalue:具名且不可被移动
xvaue:具名且可被移动
prvalue:不具名且可被移动
glvalue:具名,lvalue和xvalue都属于glvalue
rvalue:可被移动的表达式,prvalue和xvalue都属于rvalue
语法
- 左值引用声明符:声明 S& D; 是将 D 声明为 声明说明符序列 S 所确定的类型的左值引用。
- 右值引用声明符:声明 S&& D; 是将 D 声明为 声明说明符序列 S 所确定的类型的右值引用。
int a = 4;
int& b = a;
int&& c = 4;
cout << a << ' ' << b << " " << c << endl; //4 4 4
- 因为引用不是对象,所以不存在引用的数组,不存在指向引用的指针,不存在引用的引用,不存在void的引用
int& a[3]; // 错误
int&* p; // 错误
int& &r; // 错误
- 因为引用不是对象,所以引用不占用空间。尽管编译器会在需要实现所需语义(例如,引用类型的非静态数据成员通常会增加类的大小,量为存储内存地址所需)的情况下分配存储。
引用折叠
- 右值引用的右值引用折叠成右值引用,其他组合均折叠成左值引用:
typedef int& lref;
typedef int&& rref;
int n;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
左值引用
- 用于在函数调用中实现按引用传递语义
- 建立既存对象的别名
- 当函数的返回值是左值引用时,函数调用表达式变成左值表达式
#include <iostream>
#include <string>
char& char_number(std::string& s, std::size_t n)
{
return s.at(n); // string::at() 返回 char 的引用
}
int main()
{
std::string str = "Test";
char_number(str, 1) = 'a'; // 函数调用是左值,可被赋值
std::cout << str << '\n'; // Tast
}
右值引用
如果函数重载能够同时接受:右值引用/常引用参数,则编译器将优先重载:右值引用参数,即引入右值引用的主要目的是实现移动语义。
- 右值引用可用于为临时对象延长生存期(注意,到 const 的左值引用也能延长临时对象生存期,但这些对象无法因此被修改)
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到 const 的左值引用延长生存期 s1 + s1为表达式,是右值
// r2 += "Test"; // 错误:不能通过到 const 的引用修改
std::string&& r3 = s1 + s1; // OK:右值引用延长生存期
r3 += "Test"; // OK:能通过到非 const 的引用修改
std::cout << r3 << '\n';
}
- 当函数同时具有右值引用和左值引用的重载时,右值引用重载绑定到右值(包含纯右值和亡值),而左值引用重载绑定到左值
#include <iostream>
#include <utility>
void f(int& x)
{
std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量在用于表达式时是左值
int&& x = 1;
f(x); // 调用 f(int& x)
f(std::move(x)); // 调用 f(int&& x)
}
- 允许在适当时机自动选择移动构造函数、移动赋值运算符和其他具有移动能力的函数(例如 std::vector::push_back())。
//因为右值引用能绑定到亡值,所以它们能指代非临时对象:
int i2 = 42;
int&& rri = std::move(i2); // 直接绑定到 i2
// 这使得作用域中不再需要的对象可以被移动出去:
std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> v2(std::move(v)); // 绑定右值引用到 v
assert(v.empty()); // v为空, v2 = [1,2,3,4,5]
移动语义
- 如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。如果被拷贝的对象是临时的,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能将对象包含的资源,直接从旧对象移动到新对象,就可以节省资源申请和释放的时间。
- 如果资源对象本身不可拷贝(如智能指针std::unique_ptr)需要定义移动构造/移动赋值函数
拷贝省略(copy elision)
- 拷贝省略是非标准(C++ 17 前)的编译器优化。跳过移动/拷贝构造函数,让编译器直接在移动后的对象内存上,构造被移动的对象。
完美转发
- 如果没用通用引用的概念,那么对于一个变长模板函数,至少需要两个重载
template <typename T, typename ...Args>
void func(T& arg, Args&...args)
{
func(args...);//左值直接展开
}
template <typename T, typename ...Args>
void func(T&& arg, Args&&...args)
{
func(std::move(args...));//右值需要std::move()转发
}
- 通用引用:符号&& 并不一定代表右值引用,它也可能是左值引用
- std::forward实现了针对左右值的参数,能保证被转发参数的左、右值属性不变,即完美转发(perfect forwarding)
- 如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,那么,函数既可以接受左值引用,又可以接受右值引用。
- 提供了模板函数std::forward(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。
template <typename T, typename ...Args>
void func(T&& arg, Args&&...args)
{
func(std::forward<Args>(args)... );
}
转发引用
悬垂引用
std::string& f() {
std::string s = "Example";
return s; // 退出 s 的作用域:调用其析构函数并解分配其存储
}
std::string& r = f(); // 悬垂引用
std::cout << r; // 未定义行为:从悬垂引用读取
std::string s = f(); // 未定义行为:从悬垂引用复制初始化
参考:1、cppreference/
2、https://zhuanlan.zhihu.com/p/551792965