【C++】右值引用详解(移动构造、移动赋值、万能引用和完美转发、关键字default和delete)
创始人
2024-06-02 03:22:45
0

文章目录

    • 1、区分左右值
    • 2、右值引用
      • 2.1 右值引用的作用
      • 2.2 右值引用的左值属性
      • 2.3 万能引用和完美转发
    • 3、默认移动构造和默认移动赋值
      • 3.1 关键字default
      • 3.2 关键字delete

1、区分左右值

什么是左值?
左值是一个数据的表达式,能被取地址和赋值。
左值引用就是给左值取别名。

int main()
{//以下a、b、p、*p都是左值int a = 1;const int b = 2;int* p = new int;//以下ra、rb、rp、value都是左值引用//并且看到左值a、b、p、*p也都可以出现在赋值右边//所以左值既可以在赋值左边,也可以在赋值右边int& ra = a;const int& rb = b;int*& rp = p;int& value = *p;
}

左值重点:
能被取地址的才是左值。
左值既可以出现在赋值左边,也可以出现在赋值右边。

什么是右值?
右值也是一个数据的表达式,右值不能取地址,如:字面常量、表达式返回值,函数返回值等

int main()
{int x = 1, y = 2;//这种字面值常量、函数返回值、表达式都是右值10;fmin(1, 2);x + y;//右值引用只能引用右值int&& rra = 10;int&& rrm = fmin(1, 2);int&& rrb = x + y;
}

右值不能被取地址
右值只能放在赋值右边

左值引用能引用左值和右值,右值引用只能引用右值
左值引用是给左值取别名,右值引用是给右值取别名。

int main()
{int x = 1, y = 2;int a = 1;//const左值引用能引用左值const int& ra = a;//const左值引用能引用右值 本质是权限的缩小const int& rb = x + y;const int& rc = 10;//右值引用只能引用右值int&& rrd = x + y;//int&& rrd = a; //err 无法将右值引用绑定到左值//但是右值引用能引用move修饰的左值int&& rre = move(a);
}

左值引用能引用左值
const左值引用即能引用左值,也能引用右值


右值引用除了引用右值,还可以引用move修饰后的左值。

2、右值引用

左值引用的意义是什么? 函数传参/函数传返回值 – 减少拷贝

template
void func1(const T& x)
{// ...
}template
const T& func2(const T& x)
{// ...return x;
}

但是左值引用没有彻底解决函数中局部对象返回的问题:

template
T func3(const T& x)
{T temp;// ...return temp;
}

右值引用的意义就是为了补齐这个短板

2.1 右值引用的作用

先来看一段代码

#include 
#include 
using namespace std;
namespace test
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}test::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}int main()
{test::string s1 = test::to_string(1122);test::string s2;s2 = test::to_string(2233);return 0;
}

在这里插入图片描述

在这里插入图片描述

左边第一种情况,str因为出作用域销毁所以在返回前拷贝了一个临时对象。
那么对于一个马上要销毁的对象,如果要提高效率,那么只对它进行资源转移(指向的改变)是不是更好,而不是对它进行拷贝。
右值引用就是一个这样的思路,对于将亡值或纯右值,可以直接对它进行资源转移,从而提高效率。

通过移动构造和移动赋值,右值引用就可以实现以下两个作用

右值引用的第一个作用:减少返回值的拷贝
将下面两个函数加入test::string类

// 移动构造
string(string&& s)
{cout << "string(const string& s) -- 移动拷贝" << endl;swap(s);
}// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string s) -- 移动赋值" << endl;swap(s);return *this;
}int main()
{test::string s1 = test::to_string(1122);test::string s2;s2 = test::to_string(2233);return 0;
}

在这里插入图片描述

这样第一种情况,在str返回的时候,只是简单的让两个类的成员变量交换了。
第二种情况,也是仅进行了两次类成员的交换。
这样就大大减少了拷贝,提高了效率。

右值引用的第二个作用:对于插入一些右值数据,也可以减少拷贝

#include 
int main()
{list lt;test::string s("hello");lt.push_back(s);lt.push_back("hello world"); //纯右值 字面值lt.push_back(test::string("hello world")); //将亡值 出作用域销毁lt.push_back(move(s)); //对于不是将亡值或纯右值的要小心printf("%s\n", s); //因为是资源交换,所以结果为空return 0;
}

在这里插入图片描述

2.2 右值引用的左值属性

右值引用在引用右值后具有左值属性,是可以取地址可以修改

int main()
{int x = 1, y = 1;int&& rr1 = x + y;const int&& rr2 = x + y;rr1++;  //右值引用具有左值属性//rr2++; 这也是为什么右值引用有const原因
}

为什么要这样呢?
为了移动构造和移动赋值的实现,否则不能换资源。

那么右值引用的左值属性有什么用呢?
下面通过实现list的移动拷贝和赋值来体会

namespace test
{template struct ListNode{struct ListNode* _next;struct ListNode* _prev;T data;ListNode(const T& x = T()):_next(nullptr), _prev(nullptr), data(x){}};template struct list_iterator{typedef struct ListNode node;typedef struct list_iterator Self;list_iterator(node* p):_pnode(p){}bool operator!=(Self x) const{return _pnode != x._pnode;}bool operator==(Self x) const{return _pnode == x._pnode;}Ptr operator->(){return &(_pnode->data);}Ref operator*(){return _pnode->data;}Self& operator++(){_pnode = _pnode->_next;return *this;}Self operator++(int){Self tmp(*this);_pnode = _pnode->_next;return tmp;}Self& operator--(){_pnode = _pnode->_prev;return *this;}Self operator--(int){Self tmp(*this);_pnode = _pnode->_prev;return tmp;}node* _pnode;};template class list{typedef struct ListNode node;public:typedef list_iterator iterator;typedef list_iterator const_iterator;iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}void initlist(){_head = new node(T());_head->_next = _head;_head->_prev = _head;_size = 0;}// 构造list(){initlist();}//拷贝构造template list(InputIterator first, InputIterator last){initlist();while (first != last){push_back(*first);++first;}}void swap(list& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}list(const list& lt){initlist();list tmp(lt.begin(), lt.end());swap(tmp);}list& operator=(list lt){swap(lt);return *this;}bool empty() const{return _size == 0;}size_t size() const{return _size;}~list(){clean();delete _head;_head = nullptr;}void push_back(const T& x){insert(end(), x);}iterator insert(iterator pos, const T& x = T()){node* newnode = new node(x);node* cur = pos._pnode;node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;_size++;return iterator(newnode);}iterator erase(iterator pos){assert(pos != end());node* cur = pos._pnode;node* prev = cur->_prev;node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;_size--;return iterator(next);}void clean(){iterator it = begin();while (it != end()){it = erase(it);}}private:node* _head;size_t _size;};}int main()
{test::list lt;test::string s1("111111");lt.push_back(s1);lt.push_back(test::string("222222"));lt.push_back("333333");return 0;
}

在没有实现接收右值的push_back前,都是深拷贝。
在这里插入图片描述
实现接收右值的push_back后,运行发现还都是深拷贝
这就是右值引用左值属性的原因,在传参给insert后其实就变成了传左值。
所以后面接收这个右值都要move一下。

void push_back(T&& x)
{//insert(end(), x);insert(end(), move(x);
}iterator insert(iterator pos, T&& x = T())
{node* newnode = new node(move(x));node* cur = pos._pnode;node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;_size++;return iterator(newnode);
}ListNode(T&& x = T()):_next(nullptr), _prev(nullptr), data(move(x))
{}

最后
在这里插入图片描述

2.3 万能引用和完美转发

上面的代码可见出现了大部分的冗余代码,有没有方法能够让一种引用能接收左值引用和右值引用呢?
答案是万能引用
(C++11后,stl容器为了兼顾以前的版本,所以不得不再写一份。)

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }// 万能引用
template
void PerfectForward(T&& t)
{// t可能是左值,可能是右值//Fun(move(t));// 完美转发,保持它属性Fun(std::forward(t));
}int main()
{int x = 1;const int y = 1;const int&& rr1 = 10;PerfectForward(x); //int -> int& (编译器将&&折叠成&)PerfectForward(10); //int -> int&&PerfectForward(y); //const int -> const int& 经过折叠PerfectForward(rr1); //右值引用具有左值属性PerfectForward(move(y)); //const int&&return 0;
}

在这里插入图片描述
forward完美转发
forward转发能够保留原来属性

万能引用后的函数,再调用其它函数,参数默认又是左值属性,所以要保留属性 用forward完美转发

3、默认移动构造和默认移动赋值

原来C++类中,有6个默认成员函数:
默认构造和拷贝构造
默认析构和运算符重载
默认取地址重载和const取地址重载

C++11 新增了两个:移动构造函数和移动赋值运算符重载

当类中没有写移动构造函数,并且析构函数 、拷贝构造、拷贝赋值重载,这些都没有实现,那么编译器就会默认写一个移动构造函数。

当类中没有写移动赋值重载,并且析构函数 、拷贝构造、拷贝赋值重载,这些都没有实现,那么编译器就会默认写一个移动赋值重载。(一样的)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

也很好理解,因为在设计上看当实现了析构、拷贝或者赋值重载其中一个编译器就以为用原来一套,如果写了移动构造就让编译器认为用新的一套。

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}~Person(){}private:test::string _name;int _age;
};int main()
{Person p1;Person p2(p1);Person p3(move(Person()));Person s4;s4 = Person();return 0;
}

这个时候不会默认生成移动构造和移动赋值重载,因为写了析构
在这里插入图片描述

3.1 关键字default

当实现了析构、拷贝构造或赋值重载,此时编译器不会实现默认移动构造以及默认移动赋值重载,如果此时要编译器强制实现,就可以对其函数进行default关键字标识。

强制生成默认函数的关键字default:

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}/*Person(Person&& p):_name(std::forward(p._name)), _age(p._age){}*/// 强制生成Person(Person&& p) = default;Person& operator=(Person&& p) = default;Person(const Person& p):_name(p._name), _age(p._age){}Person& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;}return *this;}~Person(){}private:test::string _name;int _age;
};int main()
{Person p1;Person p2(p1);Person p3(move(Person()));Person s4;s4 = Person();return 0;
}

在这里插入图片描述

3.2 关键字delete

如果想要限制某些默认函数的生成,在C++11之前,是将只将声明放入私有,这样外部调用生成函数就爆错。C++11之后,只要对声明加上=delete就可以限制其生成。

下面代码如果不限制默认拷贝构造生成程序就会崩溃。

class A
{
public:void func(){A tmp(*this);}A(){}~A(){delete[] p;}//C++11A(const A& aa) = delete;private:// 只声明不实现,声明为私有 C++98//A(const A& aa);
private:int* p = new int[10];
};int main()
{A aa1;aa1.func();return 0;
}

相关内容

热门资讯

Python|位运算|数组|动... 目录 1、只出现一次的数字(位运算,数组) 示例 选项代...
张岱的人物生平 张岱的人物生平张岱(414年-484年),字景山,吴郡吴县(今江苏苏州)人。南朝齐大臣。祖父张敞,东...
西游西后传演员女人物 西游西后传演员女人物西游西后传演员女人物 孙悟空 六小龄童 唐僧 徐少华 ...
名人故事中贾岛作诗内容简介 名人故事中贾岛作诗内容简介有一次,贾岛骑驴闯了官道.他正琢磨着一句诗,名叫《题李凝幽居》全诗如下:闲...
和男朋友一起优秀的文案? 和男朋友一起优秀的文案?1.希望是惟一所有的人都共同享有的好处;一无所有的人,仍拥有希望。2.生活,...
戴玉手镯的好处 戴玉手镯好还是... 戴玉手镯的好处 戴玉手镯好还是碧玺好 女人戴玉?戴玉好还是碧玺好点佩戴手镯,以和田玉手镯为佳!相嫌滑...
依然什么意思? 依然什么意思?依然(汉语词语)依然,汉语词汇。拼音:yī    rán基本解释:副词,指照往常、依旧...
高尔基的散文诗 高尔基的散文诗《海燕》、《大学》、《母亲》、《童年》这些都是比较出名的一些代表作。
心在飞扬作者简介 心在飞扬作者简介心在飞扬作者简介如下。根据相关公开资料查询,心在飞扬是一位优秀的小说作者,他的小说作...
卡什坦卡的故事赏析? 卡什坦卡的故事赏析?讲了一只小狗的故事, 我也是近来才读到这篇小说. 作家对动物的拟人描写真是惟妙...
林绍涛为简艾拿绿豆糕是哪一集 林绍涛为简艾拿绿豆糕是哪一集第三十二集。 贾宽认为是阎帅间接导致刘映霞住了院,第二天上班,他按捺不...
小爱同学是女生吗小安同学什么意... 小爱同学是女生吗小安同学什么意思 小爱同学,小安同学说你是女生。小安是男的。
内分泌失调导致脸上长斑,怎么调... 内分泌失调导致脸上长斑,怎么调理内分泌失调导致脸上长斑,怎么调理先调理内分泌,去看中医吧,另外用好的...
《魔幻仙境》刺客,骑士人物属性... 《魔幻仙境》刺客,骑士人物属性加点魔幻仙境骑士2功1体质
很喜欢她,该怎么办? 很喜欢她,该怎么办?太冷静了!! 太理智了!爱情是需要冲劲的~不要考虑着考虑那~否则缘...
言情小说作家 言情小说作家我比较喜欢匪我思存的,很虐,很悲,还有梅子黄时雨,笙离,叶萱,还有安宁的《温暖的玄》 小...
两个以名人的名字命名的风景名胜... 两个以名人的名字命名的风景名胜?快太白楼,李白。尚志公园,赵尚志。
幼儿教育的代表人物及其著作 幼儿教育的代表人物及其著作卡尔威特的《卡尔威特的教育》,小卡尔威特,他儿子成了天才后写的《小卡尔威特...
海贼王中为什么说路飞打凯多靠霸... 海贼王中为什么说路飞打凯多靠霸气升级?凯多是靠霸气升级吗?因为之前刚到时确实打不过人家因为路飞的实力...
运气不好拜财神有用吗运气不好拜... 运气不好拜财神有用吗运气不好拜财神有没有用1、运气不好拜财神有用。2、拜财神上香前先点蜡烛,照亮人神...