C++ 引用和指针
引用与指针
引用 | 指针 |
---|---|
必须初始化 | 可以不初始化 |
不能为空 | 可以为空 |
不能更换目标 | 可以更换目标 |
引用必须初始化,而指针可以不初始化
1 2
int &r; // 不合法,没有初始化引用 int *p; // 合法,但p是野指针
引用不能为空,而指针可以为空
由于引用不能为空,所以我们在使用引用的时候不需要测试其合法性
在使用指针的时候需要先判断指针是否为空指针,否则会引起程序崩溃
1 2 3 4 5 6 7 8 9 10
void test_p(int *p) { if(p != null_ptr) // 对p所指对象赋值时需要先判断p是否为空指针 *p = 3; return; } void test_r(int &r) { r = 3; // 由于引用不能为空,所以无需判断r是否有效 return; }
引用不能更换目标,只能指向初始化时指向的对象,指针可以随时改变方向
1 2 3 4 5 6 7 8
int a = 1; int b = 2; int &r = a; // 初始化引用r,并且指向变量a int *p = &a; // 初始化指针p,并且指向变量a p = &b; // 指针p指向了变量b r = b; // 引用r依然指向了a,但是a的值变成了b
引用
- 左值: 非临时的,可以出现在等号的左边或者右边,可以获取它的地址和对他赋值,分为非常量左值和常量左值
- 右值: 临时的,只能出现在等号的右边,不可以对他取地址,分为非常量右值和常量右值
在赋值符
=
左侧的表达式就是左值;右侧的表达式可能是左值也可能是右值
左值引用
左值引用就是对左值的引用,给左值取别名,主要作用是避免对象拷贝
1
2
3
4
5
6
7
8
int a = 3;
int *p = &a;
const int b = 2;
int & ra = a; // 定义了一个整型的左值引用ra,引用a变量
int *& rp = p; // 定义了一个指向整型的指针的左值引用ra,引用了指针变量p
int & r = *p; // 定义了一个整型的左值引用r,引用了p所指向的对象
const int & rb = b; // 定义了一个对常量整型b的左值引用rb
右值引用
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,给右值取别名,主要作用是延长对象的生命周期
1
2
3
int && rr1 = 10;
double && rr2 = x + y;
double && rr3 = fmin(x, y);
右值引用本质上,就是把右值提升为一个左值,并定义一个右值引用通过std::move
指向该左值
1
2
3
4
5
6
7
int && ref_a = 5;
ref_a = 6;
// 等效于
int temp = 5;
int && ref_b = std::move(temp);
ref_b = 6;
右值引用
引用右值
,会使右值被存储到特定的位置,即右值引用变量
其实是左值,可以对他取地址和赋值,那么左值引用可以引用右值引用
- 左值引用可以指向右值,但需要
const
修饰,不能修改这个值 - 右值引用可以指向左值,需要用
std::move(v)
- 声明出来的左值引用或右值引用都是左值
- 作为函数返回值的
&&
是右值
对比
作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性
1 2 3 4 5 6 7 8 9 10 11 12
void f(const int& n) { n += 1; // 编译失败,const左值引用不能修改指向变量 } void f2(int && n) { n += 1; // ok } int main() { f(5); f2(5); }
左值引用只能引用左值,添加
const
修饰的左值引用既可以引用左值也可以引用右值1 2 3 4 5 6 7 8 9 10 11 12
// 1. 左值引用只能引用左值 int t = 8; int & rt1 = t; int & rt2 = 8; // 编译错误,因为8是左值,不能直接引用左值 // 2. const左值可以引用左值 const int & rt3 = t; // 3. const左值也可以引用右值 const int & rt4 = 8; const double & r1 = x + y; const double & r2 = fmin(x, y);
右值引用只能引用右值不能引用左值,但是右值引用可以引用被
std::move
的左值1 2 3 4 5 6 7 8 9 10 11 12 13
// 1. 右值引用只能引用右值 int && rr1 = 10; double && rr2 = x + y; const double && rr3 = x + y; int t = 10; int && rrt = t; // 编译报错,不能引用左值 // 2. 右值引用可以引用被std::move的左值 int && rrt = std::move(t); int *&& rr4 = std::move(p); int && rr5 = std::move(*p); const int && rr6 = std::move(b);
使用
左值引用场景
左值引用做参数
当引用作为参数时,效果跟利用指针作为参数的效果相当。当调用函数时,函数中的形参会被当成实参变量或对象的一个别名使用,也就是说函数中对形参的各种操作实际上就是对实参本身的操作
优点在于参数的传递中避免了对象拷贝,提高效率
左值引用做返回值
右值引用
移动语义
- 移动构造函数
MyClass(Type && a)
: 当构造函数参数是一个右值时,优先使用移动构造函数而不是拷贝构造函数MyClass(const Type & a)
- 移动赋值运算符
Type& operator = (Type && a)
: 当赋值的是一个右值时,优先使用移动赋值而不是拷贝赋值运算符Type & operator = (const Type & a)
将一个对象中的资源移动到另一个对象,主要解决减少不必要的临时对象的创建、拷贝与销毁
在没有右值引用时,一个简单的数组类通常需要实现
构造函数
,拷贝构造函数
,赋值运算符重载
,析构函数
等1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
class Array { public: Array(int size) : size_(size) { data = new int[size_]; } // 深拷贝构造 Array(const Array& temp_array) { size_ = temp_array.size_; data_ = new int[size_]; for (int i = 0; i < size_; i ++) { data_[i] = temp_array.data_[i]; } } // 深拷贝赋值 Array& operator=(const Array& temp_array) { delete[] data_; size_ = temp_array.size_; data_ = new int[size_]; for (int i = 0; i < size_; i ++) { data_[i] = temp_array.data_[i]; } } ~Array() { delete[] data_; } public: int *data_; int size_; };
这样程序会显得很复杂,而使用右值引用可以解决这个问题。
在STL很多容器里,比如说
std::vector
的push_back
和emplace_back
,都实现了以右值引用为参数的移动构造函数
和移动复制重载函数
通过右值引用,可以将上面的程序优化为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
class Array { public: // ...... // 优雅 Array(Array&& temp_array) { data_ = temp_array.data_; size_ = temp_array.size_; // 为防止temp_array析构时delete data,提前置空其data_ temp_array.data_ = nullptr; } public: int *data_; int size_; }; int main(){ Array a; // 做一些操作 // ..... // 左值a,用std::move转化为右值 Array b(std::move(a)); }
- 移动构造函数
std::move
将左值显式转为右值,以便调用移动构造或移动赋值1 2
MyVector v1(1000); MyVector v2 = std::move(v1); // 强制调用移动构造
vector::push_back
使用std::move
提高性能1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// std::vector和std::string的实际例子 int main() { std::string str1 = "aacasxs"; std::vector<std::string> vec; vec.push_back(str1); // 传统方法,copy vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串 vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值 vec.emplace_back("axcsddcas"); // 当然可以直接接右值 } // std::vector方法定义 void push_back (const value_type& val); void push_back (value_type&& val); void emplace_back (Args&&... args);
完美转发
std::forward
在模板编程中,右值引用 +
std::forward
可以保证参数转发时不丢失值类别与
std::move
相比,std::forward
更强大,std::forward<T>(u)
有两个参数:T
与u
。 a. 当T
为左值引用类型时,u
将被转换为T
类型的左值; b. 否则u
将被转换为T
类型右值。例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
void B(int&& ref_r) { ref_r = 1; } // A、B的入参是右值引用 // 有名字的右值引用是左值,因此ref_r是左值 void A(int&& ref_r) { B(ref_r); // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败 B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过 B(std::forward<int>(ref_r)); // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值 } int main() { int a = 5; A(std::move(a)); }
例2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
void change2(int&& ref_r) { ref_r = 1; } void change3(int& ref_l) { ref_l = 1; } // change的入参是右值引用 // 有名字的右值引用是 左值,因此ref_r是左值 void change(int&& ref_r) { change2(ref_r); // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败 change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过 change2(std::forward<int &&>(ref_r)); // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过 change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过 change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过 // 可见,forward可以把值转换为左值或者右值 } int main() { int a = 5; change(std::move(a)); }
引用折叠
折叠规则
组合情况 | 声明类型 | 折叠类型 | 说明 |
---|---|---|---|
引用的引用 | & & | & | 左值引用 + 左值引用 = 左值引用 |
右值引用的引用 | && & | & | 左值引用 + 右值引用 = 左值引用 |
引用的右值引用 | & && | & | 右值引用 + 左值引用 = 左值引用 |
右值引用的右值引用 | && && | && | 右值引用 + 右值引用 = 右值引用 |
- 模板参数推导
1
2
template<typename T>
void func(T&& arg);
上面的代码中,T&&
并不总是右值引用,他是万能引用/转发引用
在类型推导时,T
根据传入不同的值推导为不同的形式
传入右值
1
func(10);
10
是右值T
推导为int
- 参数类型
int&&
传入左值
1 2
int x = 5; func(x);
x
是左值T
推导为int&
- 参数类型
int& &&
–>int&
typedef
中的折叠引用
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
struct Test {
typedef T&& RRef;
typedef T& LRef;
};
// 实例化时发生折叠
Test<int&>::RRef r1; // int& && → int&
Test<int&>::LRef r2; // int& & → int&
Test<int>::RRef r3; // int&&
Test<int>::LRef r4; // int&
auto
推导
1
2
3
4
5
6
7
int x = 10;
auto&& a1 = x; // x是左值,推导为int& && → int&
auto&& a2 = 10; // 10是右值,推导为int&&
// 等价于
int& a1 = x;
int&& a2 = 10;