目录
🌈前言
本篇文章进行C++11中右值引用的学习!!!
🚁1、右值引用
🚂1.1、左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
什么是左值?什么是左值引用?
-
左值是一个表示数据的表达式(如变量名、解引用的指针或返回左值引用的函数调用),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边
-
被const修饰符后的左值,不能给他二次赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
int& Test(int& x)
{
return x;
}
void lvalue_reference()
{
// 左值:表示数据的表达式(如变量名,解引用的指针或左值引用返回的函数) -- 以下p,a,b和Add()都是左值
int* p = new int(0);
int a = 10;
const int b = 20;
int c = 10
Test(c);
// 下面是对上面左值的引用
int*& rp = p;
// 解引用p(相当于类的*this)可以拿到该地址存储的左值,然后对其进行绑定
int& pvalue = *p;
int& pa = a;
const int& pb = b;
int& padd = Test(c);
}
什么是右值?什么是右值引用?
-
右值也是一个表示数据的表达式,如:字面常量、表达式返回值、函数返回值(这个不能是左值引用返回)、临时对象,匿名对象等等…
-
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边
-
右值不能对其取地址,右值引用就是对右值的引用,给右值取别名
int Add(int x, int y)
{
return x + y;
}
void rvalue_reference()
{
// 右值:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回,临时对象),匿名对象...
10;
10 + 20;
"abcdef";
Add(1, 2);
string("abcdef");
// 对上面右值进行绑定
int&& pa = 10;
int&& pb = 10 + 20;
int&& pc = Add(1, 2);
string&& str2 = string("abcdef");
// 注意:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址
cout << &pa << endl;
}
注意:
-
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置(可能存储在栈区、数据段或寄存器,因编译器而异),且可以取到该位置的地址
-
也就是说不能取字面量10的地址,但是被pa引用后,可以对pa取地址,也可以修改pa绑定的值。如果不想pa被修改,可以用const int&& pa 去引用
-
很多文章说右值被引用后生命周期被延长了,其实就是被存储到了特定空间,变成了一个左值
void Test()
{
// 常量右值引用:被绑定的右值不能进行修改
double x = 0.0, y = 1.0;
double&& pe = x + y;
const double&& pp = x + y;
pe = 20;
pp = 20; //error
}
🚃1.2、左值引用和右值引用的区别
左值引用总结:
-
左值引用只能引用左值,不能引用右值
-
被const修饰的左值引用既可引用左值,也可引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a;
int& ra2 = 10; // error
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结:
-
右值引用只能右值,不能引用左值
-
右值引用不能直接引用左值,但是可以std::move以后的左值
void Test()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
关于move函数:
-
move函数可以将右值引用绑定到一个左值身上,move被定义在utility头文件中
-
move告诉编译器,希望这个左值像右值一样区处理它。但我们必须认识到,move就像一个承诺,右值引用一个左值,意味着左值的资源可能被窃取,我们不能对源对象的值进行任何假设
-
我们可以销毁一个移后源对象,也可以对其重新赋值,但不能使用一个移后源对象的值
string& Add(string& str, char ch)
{
str += ch;
return str;
}
void Test_move()
{
int* p = new int(0);
int a = 10;
string str1("abcdfe");
string str = Add(str1, 'a');
// move:将一个左值像右值一样处理它 承诺:我们要清楚的认识到,调用move就意味着只能对这个值进行赋值或销毁后,将不能再使用它
// 调用move之后,不能对移动后的源对象的值做任何假设
int*&& pp = std::move(p);
int&& aa = std::move(a);
string&& strs = std::move(str1);
}
左值引用和右值引用的区别:
- 左值持久,右值短暂
右值引用只能绑定到临时对象和字面常量中,所以我们可以得出:
-
所引用的对象将要被销毁
-
该对象没有其他用户
- 这二个特性意味着:使用右值引用的代码可以自由的接管所引用的对象的资源
🚄1.3、右值引用的使用场景和意义
namespace mystring
{
class string
{
public:
typedef char* iterator;
public:
iterator begin() { return _p; }
iterator end() { return _p + _size; }
//=======================================================================
public:
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
{
_p = new char[_capacity + 1];
strcpy(_p, str);
}
// 移动构造
string(string&& s) noexcept
: _p(nullptr)
, _size(0)
, _capacity(0)
{
this->swap(s);
cout << "移动构造函数: " << "string(string&& s) noexcept -- 资源转移" << endl;
}
// 拷贝构造
string(const string& s)
: _p(nullptr)
, _size(0)
, _capacity(0)
{
cout << "拷贝构造: " << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._p);
this->swap(tmp);
}
//移动赋值拷贝
string& operator=(string&& s) noexcept
{
cout << "移动赋值构造: " << "string& operator=(string&& s) noexcept -- 资源转移" << endl;
this->swap(s);
return *this;
}
// 赋值拷贝
string& operator=(const string& s)
{
cout << "赋值构造: " << "string& operator=(const string& s)" << endl;
string tmp(s);
this->swap(tmp);
return *this;
}
~string()
{
if (_p != nullptr)
{
delete[] _p;
_p = nullptr;
}
}
//=======================================================================
void push_back(char c)
{
insert(_size, c);
}
string& operator+=(char c)
{
push_back(c);
return *this;
}
void reserve(size_t newCapacity)
{
if (newCapacity > _capacity)
{
size_t size_tmp = _size;
char* tmp = new char[newCapacity + 1];
strcpy(tmp, _p);
delete[] _p;
_p = tmp;
_size = size_tmp;
_capacity = newCapacity;
}
}
//=======================================================================
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
reserve(_capacity == 0 ? 2 : _capacity * 2);
size_t end = _size + 1;
while (pos < end)
{
_p[end] = _p[end - 1];
--end;
}
_p[pos] = c;
++_size;
return *this;
}
//=======================================================================
void swap(string& s)
{
std::swap(_p, s._p);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
private:
char* _p;
size_t _size;
size_t _capacity;
};
}
左值引用的场景:
-
做函数的参数或返回值类型都可以提高效率
-
一般用于类成员函数中返回*this
void fun1(mystring::string str)
{}
void fun2(mystring::string& str)
{}
int main()
{
mystring::string str1("abcdef");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
fun1(str1);
fun2(str2);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
str1 += '1';
}
左值引用的短板:
-
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了(销毁),就不能使用左值引用返回,只能传值返回,而且传值返回会导致拷贝构造临时变量
-
例如:mystring::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)
mystring::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
mystring::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()
{
// 在mystring::string to_string(int value)函数中可以看到,这里
// 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
mystring::string ret1 = ::to_string(1234);
mystring::string ret2 = ::to_string(-1234);
return 0;
}
右值引用和移动语义解决上述问题:
-
在mystring::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
-
移动构造函数后面的noexcept关键字意思是该函数不会抛异常,因为是窃取它人的资源
// 移动构造
string(string&& s) noexcept
: _p(nullptr)
, _size(0)
, _capacity(0)
{
this->swap(s);
cout << "移动构造函数: " << "string(string&& s) noexcept -- 资源转移" << endl;
}
int main()
{
mystring::string ret = ::to_string(-1234);
return 0;
}
再运行上面to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了
图解:
注意:
-
在C++98中,调用to_string会拷贝构造二次(新编译器会优化成一次)
-
在C++11中,调用to_string会先调用移动构造然后调用拷贝构造(新编译器会优化成一次移动构造)
除了移动构造函数,还有移动赋值:
- 在mystring::string类中增加移动赋值函数,再去调用to_string(1234),不过这次是将to_string(1234)返回的右值对象赋值给ret对象,这时调用的是移动构造
//移动赋值拷贝
string& operator=(string&& s) noexcept
{
cout << "移动赋值拷贝: " << "string& operator=(string&& s) noexcept" << endl;
this->swap(s);
return *this;
}
int main()
{
mystring::string ret;
ret = ::to_string(1234);
return 0;
}
注意:
-
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了
-
to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为to_string函数调用的返回值赋值给ret,这里调用的移动赋值
STL中的容器都是增加了移动构造和移动赋值::【string移动构造和赋值】【vector移动构造和赋值】
🚆1.4、右值引用引用左值及其一些更深入的使用场景分析
-
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
真的需要用右值去引用左值实现移动语义 -
当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于utility头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
int main()
{
mystring::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
mystring::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
mystring::string s3(std::move(s1));
return 0;
}
注意:
- move(s1)后拷贝到s3是调用移动构造,移动构造将s1和s3的资源进行了交换,但s1是一个左值,资源被窃取后,不能再对s1在做任何假设
STL容器插入接口函数也增加了右值引用版本:
【list模拟实现】
void push_back(value_type&& val);
int main()
{
list<mystring::string> lt;
mystring::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造 -- list底层new一个string的节点会调用string的移动构造函数
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
🚇2、完美转发
🚈2.1、模板中的万能引用
void Func(int& x) { cout << "左值引用:" << "void Func(int& x)" << endl; }
void Func(const int& x) { cout << "常量左值引用: " << "void Func(const int& x)" << endl; }
void Func(int&& x) { cout << "右值引用:" << "void Func(int&& x)" << endl; }
void Func(const int&& x) { cout << "常量右值引用: " << "void Func(const int&& x)" << endl; }
template <typename T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
PerfectForward(10); // 期望右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 期望右值
const int b = 8;
PerfectForward(b); // 期望const 左值
PerfectForward(std::move(b)); // 期望const 右值
return 0;
}
通过该代码引出下面的问题:
-
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
-
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
-
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
🚉2.2、std::forward 完美转发在传参的过程中保留对象原生类型属性
void Func(int& x) { cout << "左值引用:" << "void Func(int& x)" << endl; }
void Func(const int& x) { cout << "常量左值引用: " << "void Func(const int& x)" << endl; }
void Func(int&& x) { cout << "右值引用:" << "void Func(int&& x)" << endl; }
void Func(const int&& x) { cout << "常量右值引用: " << "void Func(const int&& x)" << endl; }
template <typename T>
void PerfectForward(T&& t)
{
// 保留对象原生类型属性
Func(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 期望右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 期望右值
const int b = 8;
PerfectForward(b); // 期望const 左值
PerfectForward(std::move(b)); // 期望const 右值
return 0;
}
🚐2.3、完美转发实际中的使用场景
list模拟实现(简版)-- 加入了移动语义版本的push_back和insert
#include <iostream>
#include <cassert>
using namespace std;
namespace mylist
{
template <typename T>
struct ListNode
{
ListNode() = default;
// 移动构造
ListNode(T&& val)
: pre(nullptr)
, next(nullptr)
, date(std::forward<T>(val))
{}
ListNode<T>* pre; // 前驱指针
ListNode<T>* next; // 后驱指针
T date; // 值域
};
template <typename T, typename Ref, typename Ptr>
struct List_Iterator
{
typedef ListNode<T>* PNode;
typedef List_Iterator<T, Ref, Ptr> self;
public:
//========================== constructe =====r=============================
List_Iterator(PNode pNode = nullptr)
: _PNode(pNode)
{}
//========================== operator =====================================
Ref operator*() { return _PNode->date; }
Ptr operator->() { return&(this->operator*()); }
bool operator==(const self& s) { return _PNode == s._PNode; }
bool operator!=(const self& s) { return _PNode != s._PNode; }
self& operator++()
{
_PNode = _PNode->next;
return *this;
}
self operator++(int)
{
self tmp(*this);
_PNode = _PNode->next;
return tmp;
}
self& operator--()
{
_PNode = _PNode->pre;
return *this;
}
self operator--(int)
{
self tmp(*this);
_PNode = _PNode->pre;
return tmp;
}
public:
PNode _PNode;
};
template <typename T>
class list
{
typedef ListNode<T>* PNode;
typedef ListNode<T> Node;
public:
//========================== 正向 iterator =====================================
typedef List_Iterator<T, T&, T*> iterator;
typedef List_Iterator<T, const T&, const T*> const_iterator;
iterator begin() { return iterator(pNode->next); }
iterator end() { return iterator(pNode); }
const_iterator cbegin() const { return const_iterator(pNode->next); }
const_iterator cend() const { return const_iterator(pNode); }
//========================== constructor =====================================
list() { empty_init(); }
list(const list& l)
{
empty_init();
// 用l中的元素构造临时的temp, 然后与当前对象交换(复用区间构造函数进行拷贝构造)
list<T> tmp(l.cbegin(), l.cend());
this->swap(tmp);
}
list<T>& operator=(list<T> l)
{
this->swap(l);
return *this;
}
list(int n, const T& value = T())
{
empty_init();
while (n--)
push_back(value);
}
//========================== Node insertion and deletion =============================
//void push_back(const T& val) { insert(pNode, val); }
// push_back -- 移动语义版本
void push_back(T&& val)
{
insert(pNode, std::forward<T>(val));
}
// insert -- 移动语义版本
iterator insert(iterator pos, T&& val)
{
PNode newNode = new Node(std::forward<T>(val));
PNode pCur = pos._PNode;
PNode posPre = pCur->pre;
newNode->next = pCur;
pCur->pre = newNode;
posPre->next = newNode;
newNode->pre = posPre;
return pos;
}
//======================= Head and tail data access ==================================
T& front() { return pNode->next->date; }
const T& front()const { return pNode->next->date; }
T& back() { return pNode->pre->date; }
const T& back()const { return pNode->pre->date; }
//========================== Auxiliary interface =====================================
void empty_init()
{
// 构造头节点,带头双向循环链表一开始指向自己
pNode = new Node;
pNode->next = pNode;
pNode->pre = pNode;
}
void swap(list& l)
{
std::swap(pNode, l.pNode);
}
private:
PNode pNode;
};
}
-
万能引用这个东西,一般对我们没有用,因为我们不是写库的人
-
不到万不得已的时候,我们不会经常使用它
🚑3、新的类功能
🚒3.1、类的默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 拷贝构造函数
- 拷贝赋值重载
- 析构函数
- 取地址重载函数
- 常量取地址重载函数
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的
C++11 新增了两个:移动构造函数和移动赋值运算符重载:
-
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
-
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
-
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
定义了拷贝构造,拷贝赋值和析构,无法默认生成移动构造和移动赋值
namespace New
{
class Person
{
public:
Person(mystring::string str = "")
: name(str)
{}
Person(const Person& p)
: name(p.name)
{}
Person& operator=(const Person& p)
{
Person tmp(p);
std::swap(*this, tmp);
return *this;
}
~Person() {}
private:
mystring::string name;
};
void Test()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
}
}
没有定义拷贝构造,拷贝赋值和析构时,编译器自动生成移动构造和移动赋值
namespace New
{
class Person
{
public:
Person(mystring::string str = "")
: name(str)
{}
/*Person(const Person& p)
: name(p.name)
{}
Person& operator=(const Person& p)
{
Person tmp(p);
std::swap(*this, tmp);
return *this;
}
~Person() {}*/
private:
mystring::string name;
};
void Test()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
}
}
🚓3.2、类成员变量初始化
类和对象章节已经详细讲解
【https://blog.csdn.net/weixin_59400943/article/details/124890264】
🚓3.2、强制生成默认函数的关键字default;
-
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成 -
比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成
namespace New
{
class Person
{
public:
Person(mystring::string str = "")
: name(str)
{}
// 强制生成移动构造函数
Person(const Person&& p) = default;
Person(const Person& p)
: name(p.name)
{}
Person& operator=(const Person& p)
{
Person tmp(p);
std::swap(*this, tmp);
return *this;
}
~Person() {}
private:
mystring::string name;
};
void Test()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
}
}
🚕3.3、禁止生成默认函数的关键字delete;
-
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错 -
在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
namespace New
{
class Person
{
public:
Person(mystring::string str = "")
: name(str)
{}
Person(const Person& p) = delete;
private:
mystring::string name;
};
void Test()
{
Person s1;
Person s2 = s1;
}
}
🚖3.4、继承和多态中的final与override关键字
在多态章节已经做讲解
https://blog.csdn.net/weixin_59400943/article/details/125335633
- C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失
- 因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
- final:修饰父类虚函数,表示该虚函数不能再被重写,形象的称为"最后的函数"
class A
{
public:
virtual void Test() final {}
};
class B : public A
{
public:
virtual void Test() {}
};
- final:修饰类(ckass)时,表示该类不能被继承,形象的称为"最后的类"
class Base final
{
public:
Base() {}
};
// class Derive : public Base {}; // 父类不能被继承
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class A
{
public:
virtual void Test(){}
};
class B : public A
{
public:
virtual void Test() override {}
};