C和C++使用&符号来表示变量的地址。C++ &符号还可用于声明引用。
例如,要将rodents作为rats变量的别名,可以这样做:
int rats;
int & rodents = rates;
必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值。
举个例子:
// secref.cpp -- defining and using a reference
#include <iostream>
int main()
{
using namespace std;
int rats = 101;
int &rodents = rats; // rodents is a reference
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents << endl;
return 0;
}
输出:
rats=101,rodents=101
rats address=0x00065fd44,rodents address= 0x00065fd44
也就是说,rats=bunnies 意味着“ 将bunnies变量的值付给rodents变量 ”。简而言之,可以通过初始化声明来设置引用,但是不能用赋值来设置。
只能初始化来设置引用
假设程序员试图这样做:
int rats=101;
int *pt =&rats;
int &rodents=*pt;//“只能初始化来设置引用”:将rodents初始化为*pt使得rodents指向rats
int bunnies=50;
pt =&bunnies; //将pt改为指向bunnies,rodents引用的还是rats,因为“只能初始化来设置引用”
将rodents初始化为*pt使得rodents指向rats。接下来将pt改为指向bunnies,并不能改变这样的事实,即rodents引用的是rats。
将引用用作函数参数
// cubes.cpp -- regular and reference arguments
#include <iostream>
double cube(double a);
double refcube(double &ra);
double cube(double a){
a *= a * a;
return a;
}
double refcube(double &ra){
ra *= ra * ra;
return ra;
}
int main ()
{
using namespace std;
double x = 3.0;
cout << cube(x);
cout << " = cube of " << x << endl;
cout << refcube(x);
cout << " = cube of " << x << endl;
// cin.get();
return 0;
}
输出:
27=cube of 3
27 = cube of 27
常量引用
如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用:
double refcube(const double &ra);
如果实参与引用参数的类型不匹配,C++将生成临时变量。但需要注意的是,仅当参数为const引用时,C++才允许生成临时变量。如果类型不匹配,编译器会创建一个临时变量,并将实参的值拷贝到该临时变量中,而实参本身不会被修改。这意味着,函数对参数的修改只会影响临时变量,而不会影响传入的实参。
换句话说,如果接受引用参数的函数的意图是通过引用修改传入的变量,那么创建临时变量将阻止这一意图的实现。因为临时变量是匿名的,且仅在函数调用期间存在。
例如,以下是一个交换函数:
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
如果我们调用这个函数时传入的参数类型不匹配,比如:
long a = 3, b = 5;
swap(a, b);
由于a
和b
的类型是long
,而函数参数是int &
,类型不匹配。在这种情况下,编译器会创建两个临时的int
变量,将a
和b
的值(分别是3和5)转换为int
类型并赋值给临时变量。然后,函数swap
将交换这两个临时变量的值。然而,原始的a
和b
(类型为long
)的值将保持不变。
右值引用&&
右值引用是C++11引入的一个新特性,它允许你直接操作右值(即临时对象,比如x+y当时的结果)。右值引用的主要用途是实现移动语义和完美转发,从而提高性能和代码灵活性。
基本概念
- 左值(Lvalue):表达式结束后仍存在的对象,如变量。
- 右值(Rvalue):表达式结束时不再存在的临时对象,如字面量或临时对象。
右值引用通过 &&
符号表示。例如:
int&& rvalue_ref = 42; // 42 是一个右值,rvalue_ref 是右值引用
右值引用的作用: 右值(临时变量,如函数当时的返回值)被存储到特定的内存位置
int n;
int &rn = n; // rn 是 n 的引用
// n 标识地址 &n 处的数据
int *pt = new int;
int &rt = *pt; // *pt 标识地址 pt 处的数据
const int b = 101; // 不能给 b 赋值,但 &b 是有效的
const int &rb = b; // rb 是 b 的引用,b 是常量数据
C++11 新增了右值引用(这在第 8 章讨论过),这是使用 &&
表示的。右值引用可以关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C 风格字符串除外,它表示地址)、诸如 x + y
等表达式以及返回值的函数(条件是该函数返回的不是引用):
int &&rr = 42; // 右值引用关联到字面常量
int &&rx = x + y; // 右值引用关联到表达式
int &&rf = func(); // 右值引用关联到函数返回值(假设 func 返回非引用类型)
--------------------
int x = 10;
int y = 23;
int &&r1 = 13; // 右值引用关联到字面常量 13
int &&r2 = x + y; // 右值引用关联到表达式 x + y 的结果
double &&r3 = std::sqrt(2.0); // 右值引用关联到 std::sqrt(2.0) 的结果
注意:
r2
关联的是当时计算x + y
得到的结果,即33
,即使以后修改了x
或y
,也不会影响到r2
的值。r3
关联的是std::sqrt(2.0)
的结果,即1.41421
,而不是其他值。
趣的是:
- 将右值(x+y)关联到右值引用(r2)会导致该右值被存储到特定的内存位置,并且可以获取该位置的地址。也就是说,虽然不能直接对
13
应用地址运算符&
,但可以对r1
应用&把13存储起来
。 - 通过将数据与特定的内存地址关联,右值引用允许我们访问该数据,即使右值本身是临时的。
右值引用的目的: 实现移动定义
引用右值的首要目的是实现移动定义。
C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
纯右值(prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10
, true
;要么是求值结果相当于字面量或匿名临时对象,例如 1+2
。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda 表达式都属于纯右值。
将亡值(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();
将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。
C++11 之前
在 C++11 之前的版本中,这样的代码中,函数 foo
的返回值 temp
在内部创建,然后被赋值给 v
。然而,v
获得这个对象时,会将整个 temp
拷贝一份,然后把 temp
销毁。如果这个 temp
非常大,这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。不过,编译器可以通过“返回值优化(RVO)”技术优化,直接在 v
的位置构造 temp
,从而避免拷贝。在最后一行中,v
是左值,foo()
返回的值是右值(也是纯右值)。
但是,v
可以被别的变量捕获到,而 foo()
产生的那个返回值作为一个临时值,一旦被 v
复制后,将立即被销毁,无法获取、也不能修改。
C++11 之后
C++11 引入了右值引用和移动语义,编译器在某些情况下会自动进行优化,避免不必要的拷贝操作。上面的例子中,编译器不仅可以使用“返回值优化”(Return Value Optimization,RVO),还可以使用“移动语义”的优化技术,以避免拷贝 temp
。
-
返回值优化(RVO):
- RVO 是一种编译器优化技术,允许编译器在返回局部对象时,直接在调用者的栈帧中构造该对象,从而避免拷贝。在 C++11 及以后的标准中,RVO 在许多情况下是强制要求的,称为 NRVO(Named Return Value Optimization)。
-
移动语义:
- 如果 RVO 不能应用(例如,返回的是一个 unnamed 临时对象),编译器会使用移动语义来转移资源,而不是拷贝。
- 对于
std::vector
等支持移动语义的类,移动构造函数会转移内部资源,而不是复制。
显式使用 std::move
虽然编译器会自动进行这些优化,但在某些情况下,你可能需要显式地使用 std::move 来确保资源被移动:
std::vector<int> foo() {
std::vector<int> temp = {1, 2, 3, 4};
return std::move(temp); // 显式移动
}
然而,在你的例子中,这是不必要的,因为编译器已经会自动应用移动语义。
这样完整的文本不仅解释了 C++11 之前和之后的行为差异,还介绍了如何显式使用 std::move
来控制资源的移动,使内容更加全面和实用。