【C++】C++11--右值引用

文章目录

  • C++11 --- 右值引用
    • 右值 and 左值
    • 左值引用 and 右值引用
      • 右值引用的使用场景和意义
      • 右值引用和移动语义
      • 右值引用左值
    • 完美转发
      • 万能引用
      • 完美转发保持属性

C++11 — 右值引用

右值 and 左值

左值是什么

左值是一个表示数据的表达式,比如变量名和可以解引用的指针

  • 左值可以出现在赋值符号的两边

  • 左值可以被取地址,也可以被修改(const修饰的左值除外)

右值是什么

右值也是一个表达数据的表达式,如常量,表达式的返回值,函数的返回值等等

  • 右值不可以被取地址,也不可以被修改
  • 右值可以出现在赋值符号的右边,到那时不能出现在赋值符号的左边
int x = 1, y = 2;
// 以下几个表达式都是常见的右值
10;
x + y;
min(x, y);
  • 右值本质就是一个临时变量或者是常量,10就是常量而x + y,min(x, y)的返回值都是临时变量,这种无法被更改的值我们称之为右值
  • 这些临时变量和常量目前并未被存储起来,也就无法取地址
  • 需要注意,传值返回的函数的返回值才是右值,因为传值返回的函数返回的是对象的拷贝,这个拷贝出来的对象是一个临时变量

对于左值引用返回的函数来说,返回值是左值。比如unordered_map[]运算符重载其返回的就是kv中的value的引用,我们可以对其进行赋值

左值引用 and 右值引用

C++11中新增了右值引用的语法特性,但是不论是左值引用还是右值引用,本质都是给对象去别名

左值引用

左值引用就是给左值去别名,通过&来声明

int	a = 10; 								int& ra = a;
int* p = new int(10);  			int*& rp = p;
const int c = 2; 						const int& rc = c;

右值引用

右值引用解释给右值取别名,通常使用&&来声明

int x = 1, y = 2;

int&& rr1 = 10;
int&& rr2 = x + y;

需要注意:右值是不可以取地址的,但是给右值取别名后,会导致右值被存储到特定位置,这个时候到右值可以被取到地址,并且可以被修改,如果不想让被引用的右值被修改,可以使用const修饰右值引用

const 左值引用 and const 右值引用

void test5() {
    const int&& ra = 10;
    int *p = const_cast<int*>(&ra);
    *p = 2;
    cout << "&ra = " << &ra << endl;
    cout << "p = " << p << endl;
    cout << "ra = " << ra << endl;
    cout << "*p = " << *p << endl;

    cout << endl;
    const int x = 10;
  	const int& b = x;
    int *pb = const_cast<int*>(&b);
    *pb = 5;
    cout << "&b = " << &b << endl;
    cout << "pb = " << pb << endl;
    cout << "b = " << b << endl;
    cout << "*pb = " << *pb << endl;
}
&ra = 0x30445b464
p = 0x30445b464
ra = 2
*p = 2

&b = 0x30445b454
pb = 0x30445b454
b = 5
*pb = 5

可以看到左值引用或是右值引用它们都是一样的,他们都在内存上开辟了空间并将数据存到了空间中,当我们访问空间时就会将空间中的值返回给我们。

注意:左值引用的const 变量是不会被写入常量表的,也不会进行宏替换。其会保持内存可见性访问该变量时会到内存中获取该变量的值

Const 左值引用右值

左值引用不可以直接引用右值,因为这涉及到权限的放大,右值不可被修改但是左值引用可以修改。但是const修饰的左值引用可以引用右值,因为const左值引用能搞保证被引用的数据不被修改

void test6() {
    const int& c = 10;
    int* pc = const_cast<int*>(&c);
    *pc = 5;
    cout << "c = " << c << endl;     // 5
    cout << "*pc = " << *pc << endl; // 5
}

可以看到不管是左值引用还是右值引用亦或是const左值引用右值都不会被写入常量表

右值引用move左值

右值引用只能引用右值,不能引用左值。但是右值引用可以引用move以后的左值。move函数是C++标准提供的函数,被move后的左值能够赋值给右值引用

void test7() {
    int a = 10;
    int&& b = std::move(a);  
    b = 3;	
    cout << a << endl;				 // 输出3
}

右值引用的使用场景和意义

虽然const左值引用既能够接收左值,也可以接收右值,但是左值引用终究存在短板,而C++11提出的右值引用就是来解决左值引用的短板的

准备工作

这里我们使用了前面模拟实现STL容器中的string类。类中实现了一些基本函数

//
// Created by 陈李鑫 on 2023/7/16.
//
#ifndef SIMULATION_REALIZATION_STL_CLX_STRING_HPP
#define SIMULATION_REALIZATION_STL_CLX_STRING_HPP

#endif //SIMULATION_REALIZATION_STL_CLX_STRING_HPP

#include 
#include 
#include 
#include 


class clx_string{
public:
    typedef char* iterator;
    iterator begin() { return _str;}
    iterator end() { return _str + _size; }
    const char* c_str() const { return const_cast<const char*>(_str); };
    void swap(clx_string& s);
    clx_string(const char* str = "");
    clx_string(const clx_string& s);
    ~clx_string();
    clx_string& operator=(const clx_string& s);
    char& operator[](size_t i);
    void reserve(size_t n);
    void push_back(char ch);
    clx_string& operator+=(char ch);
private:
    char* _str;
    size_t _size;
    size_t _capacity;
};

void clx_string::swap(clx_string& s) {
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
    std::swap(_str, s._str);
}

clx_string::clx_string(const char* str) {
    std::cout << "clx_string(const char* str) -- 直接构造" << std::endl;
    _size = strlen(str);
    _capacity = _size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

// 拷贝构造函数 以前的写法
// clx_string::clx_string(const clx_string& s) {
//     _size = strlen(s.c_str());
//     _capacity = _size;
//     _str = new char[_capacity + 1];
//     strcpy(_str, s.c_str());
// }

// 拷贝构造函数 现代写法
clx_string::clx_string(const clx_string& s)
    : _str(nullptr), _size(0), _capacity(0)
{
    std::cout << "clx_string(const clx_string& s) -- 拷贝构造"  << std::endl;
    clx_string tmp(s.c_str());
    swap(tmp);
    std::cout << std::endl;
    std::cout << std::endl;
}

clx_string::~clx_string() {
    _size = 0;
    _capacity = 0;
    delete[] _str;
    _str = nullptr;
}
clx_string& clx_string:: operator=(const clx_string& s) {
    std::cout << "clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载" << std::endl;
    clx_string tmp(s.c_str());
    clx_string::swap(tmp);
    std::cout << std::endl;
    std::cout << std::endl;
    return *this;
}
char& clx_string::operator[](size_t i) {
    assert(0 <= i && i < _size);
    return _str[i];
}

void clx_string::reserve(size_t n) {
    if (n > _capacity) {
        char* tmp = new char[n + 1];
        strncpy(tmp, _str, _size + 1);
        delete[] _str;
        _str = tmp;
        _capacity = n;
    }
}
void clx_string::push_back(char ch) {
    while (_size >= _capacity) {
        reserve(_capacity == 0 ? 4 : _capacity * 2);
    }
    _str[_size] = ch;
    _str[_size + 1] = 0;
    _size++;
}
clx_string& clx_string::operator+=(char ch) {
    push_back(ch);
    return *this;
}


这里主要关注两个函数,一是拷贝构造函数,二是赋值运算符重载,它们内部都包含了一个构造函数

 clx_string tmp(s.c_str());
// 拷贝构造函数 现代写法
clx_string::clx_string(const clx_string& s)
    : _str(nullptr), _size(0), _capacity(0)
{
    std::cout << "clx_string(const clx_string& s) -- 拷贝构造"  << std::endl;
    clx_string tmp(s.c_str());
    swap(tmp);
    std::cout << std::endl;
    std::cout << std::endl;
}

clx_string& clx_string:: operator=(const clx_string& s) {
    std::cout << "clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载" << std::endl;
    clx_string tmp(s.c_str());
    clx_string::swap(tmp);
    std::cout << std::endl;
    std::cout << std::endl;
    return *this;
}

我们可以写一个简单的案例测试一下每个函数的调用打印是否清晰

void clx_string_test1() {
    clx_string s1;
    cout << endl;
    clx_string s2(s1);
    clx_string s3;
    s3 = s1;
}
clx_string(const char* str) -- 直接构造        // s1 的直接构建

clx_string(const clx_string& s) -- 拷贝构造		 // s2 的拷贝构建
clx_string(const char* str) -- 直接构造


clx_string(const char* str) -- 直接构造        // s3 的直接构建后调用赋值函数重载 
clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载
clx_string(const char* str) -- 直接构造

这样我们的准备工作就算完成了,接下来就要进入右值引用的价值的正是讲解

左值引用的使用场景

在介绍左值引用的短板之前,我们先强调一下左值引用的价值

  • 左值引用做参数,防止传参时进行拷贝
  • 左值引用做返回值,防止返回时对返回对象进行拷贝
clx_string func1(clx_string s) { return s; }
clx_string& func2(clx_string& s) { return s; };

void clx_string_test2() {
    clx_string s1;
    std::cout << std::endl;
    std::cout << "func1 begin" << std::endl;
    func1(s1);
    std::cout << "func1 end" << std::endl;
    std::cout << std::endl;
    std::cout << "func1 begin" << std::endl;
    func2(s1);
    std::cout << "func1 end" << std::endl;
}
clx_string(const char* str) -- 直接构造
  
func1 begin
clx_string(const clx_string& s) -- 拷贝构造
clx_string(const char* str) -- 直接构造

clx_string(const clx_string& s) -- 拷贝构造
clx_string(const char* str) -- 直接构造

func1 end

func1 begin
func1 end

可以看到使用引用传参和引用返回减少了两次的拷贝构造,我们知道stirng这种类型进行的拷贝都是深拷贝,如果string很大那么深拷贝的代价是非常高的,使用左值传参和做返回值起到的作用还是非常明显的

左值引用的短板

左值引用虽然在某些情况下可以避免不必要的拷贝操作,但是并不能完全避免。

  • 函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况不能使用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板(左值引用使用的前提是出了这个域,左值任然存在)
clx_string clx_string::to_string(int value) {
    clx_string res;
    bool flag = false;
    if (value < 0) {
        flag = true;
        value = -1 * value;
    }
    while (value > 0) {
        char ch = static_cast<char>(value % 10);
        res += ch + '0';
        value /= 10;
    }
    if(flag) res += '-';
    std::reverse(res.begin(), res.end());
    return res;
}
void clx_string_test3() {
    clx_string s;
    s = clx_string::to_string(1);
}
clx_string(const char* str) -- 直接构造  // 直接构造s
clx_string(const char* str) -- 直接构造  // to_string 内部构建 res
clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载    // s接收返回值
clx_string(const char* str) -- 直接构造

比如to_string函数就不可能使用左值返回,因为该函数生成的字符串就在函数域中,出了函数域就销毁了,因此该函数只能返回一个局部变量所以必须传值返回

当传值返回后我们需要调用变量来接收to_string函数返回的局部变量这里又要调一次拷贝函数,那么to_stirng函数在函数内生成字符串(第一次),传值返回(第二次),将其的数据传递给接收变量(第三次)可以看到同一份数据生成了三次,也就是说在这之间白白进行了两次深拷贝

注意:近代编译器对上述情况进行了优化,使得传值返回的变量可以直接给父域的对象拷贝构造,可以减少一次拷贝

C++11提出右值引用就是为了解决左值引用这个短板的,但解决方式并不是简单的将右值引用作为函数的返回值

右值引用和移动语义

右值引用和移动语义解决上述问题的方法就是,给当前模拟实现的string类增加移动构造和移动赋值的方法

移动构造

移动构造是一个构造函数,该构造函数的参数就是右值类型的,移动构造的本质就是将传入的右值资源窃取过来,占为己有,这样就避免了深拷贝

在当前string类中新增一个移动构造函数,该函数要做的就是调用swap函数将传入的右值的资源窃取过来

clx_string::clx_string(clx_string&& s)
    :_size(0), _capacity(0), _str(nullptr)
{
    std::cout << "clx_string::clx_string(clx_string&& s) -- 移动构造" << std::endl;
    swap(s);
}

clx_string& clx_string::operator=(clx_string&& s) {
    std::cout << "clx_string& clx_string::operator=(clx_string&& s) -- 移动赋值重载" << std::endl;
    swap(s);
    return *this;
}

移动构造和拷贝构造的区别:

  • 拷贝构造采用的一直是const 左值引用接收参数,因此无论拷贝构造对象传入的是左值还是右值,都会调用拷贝构造函数

  • 增加移动构造之后,如果传入的参数是右值,那么就会匹配到移动构造函数(最匹配原则)

  • string的拷贝构造函数做的就是深拷贝,而移动构造函数只需要调用swap函数进行资源转移,移动构造的代价比拷贝构造小很多

void clx_string_test3() {
    clx_string s1;
    s1 = clx_string::to_string(1);
}
clx_string(const char* str) -- 直接构造				// to_string 	创建res
clx_string(const char* str) -- 直接构造  			// 创建 s1
clx_string& clx_string::operator=(clx_string&& s) -- 移动赋值重载  // 返回值移动赋值给s1

可以看到又了移动构造和移动拷贝,就不会调用原来的拷贝创建以及拷贝赋值了,使用了移动构造替换拷贝构造,移动赋值替换拷贝赋值,提高了效率

注意⚠️:虽然to_string中的局部string 对象是一个左值,但由于该string对象在当前函数调用结束后就会被立即销毁,我们可以把这种被消耗的值叫做将亡值,比如匿名对象也可以叫做将亡值。即然将亡值都要被销毁了,还不如把自己的资源转移给别人,因此编译器会讲这种将亡值识别为右值,这样就可以匹配到参数类别为右值的移动构造函数

STL中的容器

C++11标准出来后,STL容器都增加了移动构造和移动赋值,以我们刚刚说的string为例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gF0dvrSO-1689499058827)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230716141556485.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rByFUe8c-1689499058828)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230716141712003.png)]

右值引用还在各种容器的插入中使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NFd0hPw-1689499058828)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230716144526460.png)]

C++11后很多STL容器的push_back类似接口都提供了右值引用版本,如果传入的参数是右值可以直接进行资源转移,避免了深拷贝,提高了效率

右值引用左值

右值引用虽然不能直接引用左值,但是可以通过move函数将左值转化成右值。move函数的名称非常具有迷惑性,move函数其实并不能搬移任何东西,该函数的唯一功能就是将一个左值强转成右值引用,然后实现移动语义

// 声明
template <class T>
typename remove_reference<T>::type&& move (T&& arg) noexcept;

//实现
template <class T>
typename remove_reference<T>::type&& move (T&& arg) noexcept {
  return ((typename remove_reference<T>::type&&)_Arg);
}

move函数中arg参数类型并非右值引用,而是万能引用。万能引用跟右值引用的形式相同,但是右值引用必须得钥匙确定类型的。一个左值被move后,它的资源有可能已经被转移给别人了,因此要慎用一个被move后的左值

void clx_string_test4() {
    clx_string s1("hello world");
    clx_string s2;
    s2 = std::move(s1);
    cout << "s1 : " << s1.c_str() << endl;
    cout << "s2 : " << s2.c_str() << endl;
}

int main() {
    clx_string_test4();
    return 0;
}
clx_string(const char* str) -- 直接构造
clx_string(const char* str) -- 直接构造
clx_string& clx_string::operator=(clx_string&& s) -- 移动赋值重载
s1 : 
s2 : hello world

可以看到s1本来是一个左值,我们将其强转成右值赋值给了s2,那么s1内部的资源就已经被转走了,所以一个左值被move后资源可能被转移其是很危险的,慎用

完美转发

万能引用

模版中的&&不能代表右值引用,而是万能引用,其既能接受左值也能接受右值。万能引用和右值引用的区别就是,右值引用必须要确定类型,而万能引用是根据传入实参的类型进行推导

void Func(int& x) {
    cout << "左值引用" << endl;
}
void Func(const int& x) {
    cout << "const 修饰的左值引用" << endl;
}
void Func(int&& x)  {
    cout << "右值引用" << endl;
}
void Func(const int&& x) {
    cout << "const 修饰的右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t) {
    Func(t);
};

void clx_string_test5() {
    int a = 10;
    PerfectForward(a);          // 左值
    PerfectForward(move(a));    // 右值

    const int b = 10;
    PerfectForward(b);					    // const修饰的左值
    PerfectForward(std::move(b));   // const修饰的右值
}
左值引用
左值引用
const 修饰的左值引用
const 修饰的左值引用

由于PerfectForward函数的参数类型是万能引用,因此既可以接受左值也可以接受右值,我们在PerfectForward中调用Func函数,希望我们传什么类型的值就能给我们匹配什么类型的函数

  • 但是我们实际调用发现,无论我们传左值还是右值,匹配到的全部都是左值版本的Func函数,这时因为右值引用后对导致右值被存储到特定位置,这时候的右值可以被取到地址也可以被修改,会被识别成左值

完美转发保持属性

如果想要在参数的传递过程中保持其原有的属性,就需要在传参的时候调用forward函数

template<class T>
void PerfectForward(T&& t) {
    Func(std::forward<T>(t));
};

经过完美转发后,调用PerfectForward函数传入的右值就会被保持右值属性,就会匹配到右值版本,这就是完美转发的价值

你可能感兴趣的:(C++学习,c++,java,开发语言)