本文旨在通过通俗易懂的语言全面介绍引用,包括引用的本质、左右引用、以及万能引用、循环引用和完美转发等内容。
目录
????????引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
? ? ? ? 引用是给变量取一个别名,不会开辟新的空间,它和变量共用一块内存空间。
????????引用的本质是一个指针常量,引用分为普通引用和常引用,常引用一般会对其权限进行改变,为缩小,常引用就是定义了一个指向常量的指针常量 const int * const p。常引用不能对其值经过引用来修改。
? 引用的例子:
int a = 10;
int & a1 = a; // a1 = 10
(1)&在上面的代码中不是求地址运算,而是起到了标识作用。
(2)引用类型必须是和目标变量是同一类型的。
(3)引用在声名时必须初始化。
(4)一个变量可以有多个引用
(5)引用一旦引用了一个实体就不能再引用其他实体(从一而终)。
(6 ? 不能建立数组的引用,因为数组是一个由若干个元素所组成的元素集合,所以无法建立一个数组的别名。
? ? ? ? 错误的例子:
int a[3] = {0};
int & a1 = a; // 这是错误的代码,引用不可以引用数组,不可以建立数组的别名
int a = 10;
int & a1 = a; // a1 = 10
? ? ? ? 使用const 修饰的引用,不能通过引用来修改目标值,一般会对其权限缩小。常引用变量既可以是左值也可以是右值。
int b = 78;
const int & b1 = b;
//b1 = 10;// 这是错误的
b = 10;//这是正确的
const int & b2 = 30; // 常引用可以加右值
std::cout << b1 << std::endl; // 10
std::cout << b2 << std::endl; // 30
????????左值和右值是表达式的属性。C++中的表达式,不是左值就是右值。左值可以位于赋值语句的左侧,而右值则不能;
? ? ? ? (1)当一个对象被用作右值的时候,用的是对象的值;
? ? ? (2)当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。需要右值的地方可以用左值代替,但不能把右值当成左值使用。
? ? ? (3)变量都是左值,
????????左值(Lvalue)是指可以出现在赋值语句左边的表达式。左值可以是变量、数组元素、函数返回值等。左值必须满足以下条件之一:
一些例子:
int a = 1; // a是左值,因为它是一个变量
int b[5] = {1, 2, 3, 4, 5}; // b是左值,因为它是一个数组
int (*func())() = &foo; // func是左值,因为它是一个函数指针
struct S { int x; };
S s; // s是左值,因为它是一个对象
int &r = s.x; // r是左值,因为它是一个对象的成员
??????C++中的右值(Rvalue)是指不能出现在赋值语句左边的表达式。右值可以是临时对象、函数返回值、字面量等。右值必须满足以下条件之一:
右值的主要特点有:
????????左值拥有持久的状态,而右值要么是字面常量,要么是在表达式求职过程中创建的临时对象。
????????右值引用的对象将要被销毁。不能将一个右值引用绑定到一个右值引用类型的变量上,因为变量都是左值。
????????左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
????????右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。
而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
????????纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
????????需要注意的是,字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char 数组。例如:
#include <type_traits>
int main() {
// 正确,"01234" 类型为 const char [6],因此是左值
const char (&left)[6] = "01234";
// 断言正确,确实是 const char [6] 类型,注意 decltype(expr) 在 expr 是左值
// 且非无括号包裹的 id 表达式与类成员表达式时,会返回左值引用
static_assert(std::is_same<decltype("01234"), const char(&)[6]>::value, "");
// 错误,"01234" 是左值,不可被右值引用
// const char (&&right)[6] = "01234";
}
但是注意,数组可以被隐式转换成相对应的指针类型,而转换表达式的结果(如果不是左值引用)则一定是个右值(右值引用为将亡值,否则为纯右值)。例如:
const char* p = "01234"; // 正确,"01234" 被隐式转换为 const char*
const char*&& pr = "01234"; // 正确,"01234" 被隐式转换为 const char*,该转换的结果是纯右值
// const char*& pl = "01234"; // 错误,此处不存在 const char* 类型的左值
// 将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念
// 因此在传统 C++ 中, 纯右值和右值是同一个概念,也就是即将被销毁、却能够被移动的值。
// 将亡值可能稍有些难以理解,我们来看这样的代码:
std::vector<int> foo() {
std::vector<int> temp = {1, 2, 3, 4};
return temp;
}
std::vector<int> v = foo();
????????在这样的代码中,就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v, 然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大, 这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。在最后一行中,v 是左值、 foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到, 而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。 而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。
????????在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换, 等价于 static_cast<std::vector<int> &&>(temp),进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。
????????左值引用是常规的引用:变量都是左值。
????????左值拥有持久的状态。引用都可以绑定到左值上。引用都是变量。
一个正确的例子:
int c = 10;
int & c1 = c;
int && c2 = 11;
int & c3 = c1;
int & c4 = c2;
????????右值引用是必须绑定到右值的引用,他有着与左值引用完全相反的绑定特性,我们通过 && 来获得右值引用。右值有一个重要的性质——只能绑定到一个将要销毁的对象上。
? ? ?右值要么是字面常量,要么是在表达式求职过程中创建的临时对象。
????????右值引用的对象将要被销毁。不能将一个右值引用绑定到一个右值引用类型的变量上,因为变量都是左值。
错误的例子:
int c = 10;
int & c1 = c;
int && c2 = 11;
int & c3 = c1;
int & c4 = c2;
//int && c5 = c2; //错误
//int && c6 = c3; //错误
????????万能引用(Universal Reference)是指模板参数使用 && 时,能够接受任何类型的引用,包括左值引用和右值引用。右值引用是指绑定到右值的引用,可以实现移动语义和完美转发。
? ? ? ? (1)万能引用是模板参数的一种表达方式,而右值引用是一种变量类型。
? ? ? ? (2)万能引用可以接受任何类型的引用,包括左值引用和右值引用,而右值引用只能接受右值引用。
? ? ? ? (3)万能引用在模板函数中用于实现完美转发,而右值引用主要用于实现移动语义。
? ? ? ? (4)万能引用的声明方式为 T&&,而右值引用的声明方式为 X&&,其中 T 和 X 都表示类型。
一个例子:
#include <iostream>
#include "common.h"
using namespace std;
namespace QUOTE2_DAY19
{
template<typename T, typename U = int> // 仅仅是默认使用int类型,也可以更改
void f(T && t1, U && t2)
{
cout << t1+t2 << endl;
}
};
int main(int argc, char *argv[])
{
{
__LOG__("万能引用");
using namespace QUOTE2_DAY19;
int per1 = 23;
int per2 = 34;
cout << "左左: ";
f(per1,per2);
cout << "右右: ";
f(23,34);
cout << "左右: ";
f(per1,34);
}
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 万能引用可以作为函数返回值的类型,这样返回值既可以接收左值又可以接收右值。同时万能引用在完美转发中也会用到。
? ? ? ? 右值引用可以实现移动语义。可以将左值转换成对应的右值引用类型,通过std::move()来实现。
一个简单例子:
int && a1 = 10;
int a2 = 23;
cout << "移动语义前a1: " << a1 << endl;// 10
a1 = std::move(a2);
cout << "移动语义前后a1: " << a1 << endl;// 23
? ? ? ? 分析结果得到右值引用通过std::move(a2) 传入了左值,实现了移动语义。
????????完美转发的目的是使用函数函数模板调用另一个函数时,希望既可以接收左值也可以接收右值。要调用的函数的参数不是万能引用,而是既可以接收左值又可以接收右值。因此需要对传入的参数使用std::forward<>做完美转发,以满足左值引用和右值引用。
????????通过函数模板调用另外一个函数,模板的万能引用既可以接收左值也可以接收右值,但是对于函数内部来说,不管接收的是左值还是右值,模板函数内部对于形参都是左值(T && t1 ,t1本身是左值)。
????????如果函数的第一个参数需要右值,必须这样调用f(std::move(t1),t2),但是模板是通用的,我们不能直接将std::move()来写死,这样就不能调用接收左值的函数了,因此我们使用完美转发std::forward<>来实现。
一个例子:
#include <iostream>
#include "common.h"
using namespace std;
template<typename F ,typename T, typename U>
void testFun(F f, T && t1, U && t2)
{
f(std::forward<T>(t1),std::forward<U>(t2));
}
void gu_y(int && t1, int & t2) // 接收左值和右值
{
cout << t1+t2 << endl;
}
int main(int argc, char *argv[])
{
{
__LOG__("完美转发");
int per1 = 23;
int per2 = 34;
cout << "左右: ";
testFun(gu_y,23,per2);// 传入右值和左值
}
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 对于T && t1 ,t1始终是变量是左值,因此在传入函数gu_y()时,需要将一个左值转换成右值,又由于这是一个模板函数,因此不可以使用移动语义将其直接固定为右值,所以提出了完美转发。
? ? ? ? 前置自增为左值,后置自增为右值。
int & ko1 = ++lp;// 前置自增为左值
int && ko2 = lp++; // 后置自增为右值
? ? ? ? 可以用万能引用,来推导类型
int lo = 5;
auto && v1 = lo;//auto 为 int ,v1的类型为int &
auto && v2 = 6;// auto 为int,v2的类型为int &&
auto v3 = lo; // v3的类型为int
定义方式不同:指针是一个变量,存储另一个变量的地址;引用是一个别名,与另一个变量共享同一块内存空间。
操作方式不同:指针可以进行加减运算,表示指向内存中的其他位置;引用只是一个别名,不能进行加减运算。
空值处理不同:指针可以为空,表示不指向任何内存位置;引用必须连接到一个实际的对象,不能为空。
sizeof运算符不同:sizeof(指针)返回的是指针本身所占用的内存大小;sizeof(引用)返回的是引用所绑定的对象所占用的内存大小。
const修饰不同:const指针可以指向常量对象,也可以指向非常量对象;const引用必须连接到一个常量对象。