目录
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
1、左值与右值
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边,左值可以出现在=的右边。定义时const修饰符后的左值,不能给他赋值(也就是不能出现在=左边,如下面的c),但是可以取它的地址。
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值大多数时候是一些常量、临时对象、匿名对象。
判断左值和右值,最好的判断条件就是看其是否能取地址
int main()
{
// 左值:可以取地址
// p,b,c,d,*p,s,s[0]都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
const int d = b;// 左值是可以出现在右边的
*p = 10;
string s("7777777");
s[0]; // operator[]返回的是里面的左值的引用
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值,通常为常量、临时对象、匿名对象
10;
x + y;
fmin(x, y);
string("7777777");
return 0;
}
fmin是返回两者之中较小的那一个
左值和右值不一定是值,也可以是表达式,像这里面*p就是表达式
2、左值引用与右值引用
左值引用就是给左值取别名,右值引用就是给右值取别名
int main()
{
// 左值:可以取地址
// p,b,c,d,*p,s,s[0]都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
const int d = b;// 左值是可以出现在右边的
*p = 10;
string s("7777777");
s[0]; // operator[]返回的是里面的引用
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值,通常为常量、临时对象、匿名对象
10;
x + y;
fmin(x, y);
string("7777777");
// 左值引用:给左值取别名
int*& r1 = p;
int& r2 = b;
int& r3 = *p;
// 右值引用:给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x , y);
return 0;
}
上面是左值引用给左值取别名,右值引用给右值取别名
实际上,左值引用可以给右值取别名,右值引用也可以给左值取别名
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值,通常为常量、临时对象、匿名对象
10;
x + y;
fmin(x, y);
string("7777777");
// 左值引用给右值取别名:不能直接引用,需要加const
//int& rx1 = 10; // 是错的
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("7777777");
这在我们之前设计STL的时候有很多应用,如vector的push_back就是用的const 左值引用,void push_back(const T& x)
// 左值:可以取地址
// p,b,c,d,*p,s,s[0]都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
const int d = b;// 左值是可以出现在右边的
*p = 10;
string s("7777777");
s[0]; // operator[]返回的是里面的引用
// 右值引用给左值取别名:不能直接引用,需要move(左值),先将左值移动为右值
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
move是传入左值,返回右值,传入的左值是不会变的
3、左值引用和右值引用的底层
int main()
{
int x = 0;
int& r1 = x;
int&& rr1 = x + 10;
return 0;
}
现在我们将上面这段代码转到反汇编,观察以下底层是如何实现的
这里面的rbp+64h是地址,可以将显示符号名取消
ebp是高地址,栈是向下生长的
所以,无论是左值引用还是右值引用,到了汇编层,都是指针(左值引用、右值引用、类型等只是编译器检查这一层的概念而已,到了汇编层就都没有了)。左值引用的指针直接指向对象的空间,右值引用会先开辟一块空间,再指向开辟的空间。move的本质就是强制类型转换,只是实现较为复杂。所以,右值也是有地址的。底层与语法有时候是背离的。
所以,右值开了空间,并且有地址,取不了是因为编译器的限制,具有常性也是因为编译器的限制
int main()
{
int x = 0;
int& r1 = x;
// 因为底层没有引用,所以下面这样只是语法上不允许而已,只需对其强制类型转换即可
//int&& rr1 = x;
int&& rr1 = (int&&)x;
int&& rr2 = move(x);
return 0;
}
4、左值引用的不足
我们引入引用的意义就是为了减少拷贝
左值引用解决的场景:引用传参、引用传返回值
左值引用没有彻底解决的场景:引用传返回值(当返回值出了函数作用域就没有时,不能用左值引用传返回值)
为了能有一个真实场景,这里使用之前自己实现的string,并且为了观察,这个string不使用现代写法
string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace cxf
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
//默认成员函数
string(const char* str = "");
~string();
string(const string& s);
string& operator=(const string& s);//传统写法的赋值运算符重载
//string& operator=(string s);//现代写法的赋值运算符重载
//迭代器
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
//插入删除函数
void reserve(size_t n);
void resize(size_t n, char ch = '\0');
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
string& erase(size_t pos, size_t len = npos);
//relational operators
bool operator<(const string& s) const;
bool operator==(const string& s) const;
bool operator<=(const string& s) const;
bool operator>(const string& s) const;
bool operator>=(const string& s) const;
bool operator!=(const string& s) const;
//其他函数
void swap(string& s);
const char* c_str() const;
int size() const;
int capacity() const;
char& operator[](size_t i);
const char& operator[](size_t i) const;
size_t find(char ch, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0) const;
void clear();
private:
char* _str = nullptr;
int _size;
int _capacity;
const static size_t npos;
};
//输入输出函数
std::ostream& operator<<(std::ostream& out, const string& s);
std::istream& operator>>(std::istream& in, string& s);
std::istream& getline(std::istream& in, string& s);
// 将整型转为string
string to_string(int value);
}
string.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"string.h"
namespace cxf
{
const size_t string::npos = -1;
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string::string(const char* str)
:_size(strlen(str))
{
_str = new char[_size + 1];
strcpy(_str, str);
_capacity = _size;
}
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//传统写法的拷贝构造
string::string(const string& s)
{
cout << "拷贝构造" << endl;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
//现代写法的拷贝构造
//string::string(const string& s)
// :_str(nullptr)// 若_str为随机值析构时可能会报错
//{
// string tmp(s._str);
// swap(tmp);
//}
//传统写法的赋值运算符重载
string& string::operator=(const string& s)
{
cout << "赋值运算符重载" << endl;
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
return *this;
}
//现代写法的赋值运算符重载
/*string& string::operator=(string s)
{
swap(s);
return *this;
}*/
const char* string::c_str() const
{
return _str;
}
string::iterator string::begin()
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::begin() const
{
return _str;
}
string::const_iterator string::end() const
{
return _str + _size;
}
int string::size() const
{
return _size;
}
int string::capacity() const
{
return _capacity;
}
char& string::operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& string::operator[](size_t i) const
{
assert(i < _size);
return _str[i];
}
void string::reserve(size_t n)
{
assert(n > _capacity);
char* tmp = new char[n + 1];
strcpy(tmp, _str);
_capacity = n;
delete[] _str;
_str = tmp;
}
void string::resize(size_t n, char ch)
{
if (n < _size)
{
_str[n] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (int i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
}
_size = n;
}
void string::push_back(char ch)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void string::append(const char* str)
{
int len = strlen(str);
if (_capacity < _size + len)
{
reserve(_size + len + 1);
}
strcpy(_str + _size, str);
_size += len;
_capacity = _size;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
string& string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
int i = _size;
while (i >= (int)pos)
{
_str[i + 1] = _str[i];
i--;
}
_str[pos] = ch;
_size++;
return *this;
}
string& string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (_capacity < _size + len)
{
reserve(_size + len + 1);
}
int i = _size;
while (i >= (int)pos)
{
_str[i + len] = _str[i];
i--;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& string::erase(size_t pos, size_t len)
{
assert(pos <= _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
}
else
{
int i = pos + len;
while (i <= _size)
{
_str[i - pos] = _str[i];
i++;
}
}
_size -= len;
return *this;
}
size_t string::find(char ch, size_t pos) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos) const
{
char* p = strstr(_str, str);
if (p == nullptr) return npos;
else return p - _str;
}
bool string::operator<(const string& s) const
{
int ret = strcmp(_str, s._str);
return ret < 0;
}
bool string::operator==(const string& s) const
{
int ret = strcmp(_str, s._str);
return ret == 0;
}
bool string::operator<=(const string& s) const
{
return *this < s || *this == s;
}
bool string::operator>(const string& s) const
{
return !(*this <= s);
}
bool string::operator>=(const string& s) const
{
return !(*this < s);
}
bool string::operator!=(const string& s) const
{
return !(*this == s);
}
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
out << s[i];
return out;
}
//std::istream& operator>>(std::istream& in, string& s)
//{
// s.clear();
// while (1)
// {
// char ch;
// ch = in.get();
// if (ch == ' ' || ch == '\n')
// break;
// s += ch;//这样子容易造成频繁的扩容
// }
//}
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();//需要先将string原先的内容清空
char buff[128];
int i = 0;
while (1)
{
char ch;
ch = in.get();
if (ch == ' ' || ch == '\n')
break;
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
i = 0;
s += buff;
}
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
std::istream& getline(std::istream& in, string& s)
{
s.clear();
char buff[128];
int i = 0;
while (1)
{
char ch;
ch = in.get();
if (ch == '\n')
break;
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
i = 0;
s += buff;
}
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
cxf::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
cxf::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()
{
cxf::string s1 = cxf::to_string(1234);
return 0;
}
to_string是传值返回(因为返回的str出了作用域就没了,所以不能使用传左值引用返回)。按照语法,应该是用str拷贝构造一个临时对象,然后根据这个临时对象拷贝构造s1。但是通常会被编译器简化为直接使用str去拷贝构造s1。这样,从2次深拷贝减少到了1次深拷贝。为了再减少拷贝,这时候就可以使用右值引用了
5、移动构造
//传统写法的拷贝构造
string::string(const string& s)
{
cout << "拷贝构造" << endl;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
我们之前写的拷贝构造的参数是const string& s,此时无论是左值调用拷贝构造,还是右值调用拷贝构造,都会调用到这个拷贝构造函数,进行深拷贝。像前面用左值str去拷贝构造临时对象,再用临时对象去拷贝构造s1,都会调用到这个函数。此时可以使用右值引用去写一个移动拷贝
//移动构造
string::string(string&& s)
{
swap(s);
}
拷贝构造和移动构造构成函数重载,当左值去拷贝构造另一个值时,调用的是拷贝构造,当右值去拷贝构造另一个值时,调用的时移动构造。以我们上面那个例子为例,当有了移动构造,按照语法,会先使用左值str(str是左值,因为能够取地址)去拷贝构造出一个临时对象,再使用这个临时对象去移动构造出s1(这里移动构造就是交换临时对象空间中的内容和s1空间中的内容,交换完后临时对象析构时就顺便释放了s1原先的空间,没有深拷贝就让s1的内容变成了临时对象的内容,本质就是将右值引用的内容转移了),此时只有1次深拷贝。这个过程通常会被编译器优化为直接使用str去调用移动构造s1,这里面有两步关键点,第一,不生成之间的临时变量,第二,把str隐式move为右值。
前面所说的编译器优化指的是vs2019,在vs2022中,优化很大,是直接将s1当成是str,类似于str是s1的左值引用
6、移动赋值运算符重载
//传统写法的赋值运算符重载
string& string::operator=(const string& s)
{
cout << "赋值运算符重载" << endl;
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
return *this;
}
// 移动赋值
string& string::operator=(string&& s)
{
swap(s);
return *this;
}
可以应用在下面这个场景中,同样是为了减少拷贝
int main()
{
cxf::string s1;
s1 = cxf::to_string(1234);
cout << s1 << endl;
return 0;
}
移动赋值也是类似的,编译器优化后直接使用str去调用移动赋值给s1赋值
像我们之前写的杨辉三角的题,可以直接返回vv,也是因为C++11之后,STL均实现了移动拷贝和移动赋值,否则消耗是巨大的,若没有移动拷贝和移动赋值,通常会传要变化的矩阵的引用过来
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv;
vv.resize(numRows);//直接在vv中开辟并初始化numRows个
for(int i = 0;i<numRows;i++)
{
vv[i].resize(i+1);//给每一行开
vv[i][0]=1;
vv[i][vv[i].size()-1]=1;//第一个和最后一个都给成1
}
for(int i = 0;i<vv.size();i++)
{
for(int j = 0;j<vv[i].size();j++)
{
if(vv[i][j] != 1)
{
vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
}
}
}
return vv;
}
};
7、插入操作
在C++11中,STL不仅仅对类增加了移动构造和移动赋值运算符重载,还对每个插入函数增加了右值引用的版本,这里以list的push_back操作为例
int main()
{
list<cxf::string> lt;
cxf::string s1("111111111111");
lt.push_back(s1);
lt.push_back(cxf::string("2222222222"));
lt.push_back("7777777");
return 0;
}
list的push_back会先创建一个新结点,然后用传进来的值去拷贝构造里面对应的值。这里面的lt.push_back(s1)会调用拷贝构造进行深拷贝,而后面两个都是调用移动拷贝,减少了拷贝。
我们现在就来模拟实现以下插入操作使用右值,首先,直接使用之前写的List
namespace cxf
{
template<class T>
struct __List_node//创建一个T类型的链表结点
{
__List_node(const T& data = T())//构造函数
:_data(data)
, _next(nullptr)
, _prev(nullptr)
{}
__List_node<T>* _next;
__List_node<T>* _prev;
T _data;
};
template<class T, class Ref, class Ptr>
struct __List_iterator//封装链表的迭代器
{
typedef __List_node<T> Node;
typedef __List_iterator<T, Ref, Ptr> Self;
Node* _node;//成员变量
__List_iterator(Node* node)//构造函数。将迭代器中的结点初始化成传过来的结点
:_node(node)
{}
// *it
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
// ++it
Self& operator++()
{
_node = _node->_next;
return *this;
}
// it++
Self operator++(int)
{
Self tmp(*this);//调用默认的拷贝构造,因为是指针类型所以直接用默认的
//_node = _node->_next;
++(*this);
return tmp;
}
// --it
Self& operator--()
{
_node = _node->_prev;
return *this;
}
// it--
Self operator--(int)
{
Self tmp(*this);
//_node = _node->_prev;
--(*this);
return tmp;
}
// it != end()
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
template<class T>
class List//真正的链表
{
public:
typedef __List_node<T> Node;//将链表结点的名称重命名为Node
typedef __List_iterator<T, T&, T*> iterator;
typedef __List_iterator<T, const T&, const T*> const_iterator;
//带头双向循环链表
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
List(std::initializer_list<T> il)
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
for (auto e : il)
push_back(e);
}
~List()
{
clear();
delete _head;
_head = nullptr;
}
List(const List<T>& lt)
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
//const_iterator it = lt.begin();//这里迭代器不需要指定是那个类域,因为就是在这个类中使用
//while (it != lt.end())
//{
// push_back(*it);
// ++it;
//}
for (const auto& e : lt)//这里与上面用迭代器一样,因为最终也会被替换成迭代器
push_back(e);
}
/*List<T>& operator=(const List<T>& lt)
{
if (this != <)
{
clear();
for (const auto& e : lt)
push_back(e);
}
return *this;
}*/
List<T>& operator=(List<T> lt)
{
swap(_head, lt._head);//原来的空间给这个临时变量,因为这个临时变量是自定义类型,出了作用域后会自动调用析构函数
return *this;
}
void clear()//clear是清除除了头节点意外的所以结点
{
iterator it = begin();
while (it != end())
{
erase(it++);
}
}
void push_back(const T& x)//一定要用引用,因为T不一定是内置类型
{
/*Node* tail = _head->_prev;
Node* newnode = new Node(x);
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;*/
insert(end(),x);
}
void pop_back()
{
/*Node* tail = _head->_prev;
Node* prev = tail->_prev;
delete tail;
_head->_prev = prev;
prev->_next = _head;*/
erase(--end());
}
void push_front(const T& x)
{
/*Node* first = _head->_next;
Node* newnode = new Node(x);
_head->_next = newnode;
newnode->_prev = _head;
newnode->_next = first;
first->_prev = newnode;*/
insert(begin(), x);
}
void pop_front()
{
/*Node* first = _head->_next;
Node* second = first->_next;
delete first;
_head->_next = second;
second->_prev = _head;*/
erase(begin());
}
void insert(iterator pos, const T& x)
{
Node* newnode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
void erase(iterator pos)
{
assert(pos != end());//不能删除头节点
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
cur = nullptr;
}
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);
}
private:
Node* _head;
};
}
我们很容易就能想到对push_back增加一个右值引用的版本,因为push_back中调用了insert,所以insert也到增加一个右值引用的版本
void push_back(const T& x)//一定要用引用,因为T不一定是内置类型
{
insert(end(),x);
}
void push_back(T&& x)
{
insert(end(), x);
}
void insert(iterator pos, const T& x)
{
Node* newnode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
void insert(iterator pos, T&& x)
{
Node* newnode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
一运行,会发现并没有使用移动构造,这是为什么呢?
因为右值引用本身是左值,insert向下传递时是匹配到了左值引用的版本
为什么右值引用本身是左值呢?
在前面,也可以看到实现移动构造是将右值引用的内容转移了,如果右值引用本身是右值,是转移不了的,所以只能是左值。因为右值引用本身是左值,所以在向下传递时需要move一下
注意,这里是每一次传右值,都需要move,push_back调用insert需要,insert调用new需要,还需要实现一个右值引用版本的构造函数(此时这个构造函数不能有缺省值,否则两个默认构造会出错),并且构造函数里面也需要move
#pragma once
namespace cxf
{
template<class T>
struct __List_node//创建一个T类型的链表结点
{
__List_node(const T& data = T())//构造函数
:_data(data)
, _next(nullptr)
, _prev(nullptr)
{}
__List_node(T&& data)//构造函数
:_data(move(data))
, _next(nullptr)
, _prev(nullptr)
{}
__List_node<T>* _next;
__List_node<T>* _prev;
T _data;
};
template<class T, class Ref, class Ptr>
struct __List_iterator//封装链表的迭代器
{
typedef __List_node<T> Node;
typedef __List_iterator<T, Ref, Ptr> Self;
Node* _node;//成员变量
__List_iterator(Node* node)//构造函数。将迭代器中的结点初始化成传过来的结点
:_node(node)
{}
// *it
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
// ++it
Self& operator++()
{
_node = _node->_next;
return *this;
}
// it++
Self operator++(int)
{
Self tmp(*this);//调用默认的拷贝构造,因为是指针类型所以直接用默认的
//_node = _node->_next;
++(*this);
return tmp;
}
// --it
Self& operator--()
{
_node = _node->_prev;
return *this;
}
// it--
Self operator--(int)
{
Self tmp(*this);
//_node = _node->_prev;
--(*this);
return tmp;
}
// it != end()
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
template<class T>
class List//真正的链表
{
public:
typedef __List_node<T> Node;//将链表结点的名称重命名为Node
typedef __List_iterator<T, T&, T*> iterator;
typedef __List_iterator<T, const T&, const T*> const_iterator;
//带头双向循环链表
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
List(std::initializer_list<T> il)
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
for (auto e : il)
push_back(e);
}
~List()
{
clear();
delete _head;
_head = nullptr;
}
List(const List<T>& lt)
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
//const_iterator it = lt.begin();//这里迭代器不需要指定是那个类域,因为就是在这个类中使用
//while (it != lt.end())
//{
// push_back(*it);
// ++it;
//}
for (const auto& e : lt)//这里与上面用迭代器一样,因为最终也会被替换成迭代器
push_back(e);
}
/*List<T>& operator=(const List<T>& lt)
{
if (this != <)
{
clear();
for (const auto& e : lt)
push_back(e);
}
return *this;
}*/
List<T>& operator=(List<T> lt)
{
swap(_head, lt._head);//原来的空间给这个临时变量,因为这个临时变量是自定义类型,出了作用域后会自动调用析构函数
return *this;
}
void clear()//clear是清除除了头节点意外的所以结点
{
iterator it = begin();
while (it != end())
{
erase(it++);
}
}
void push_back(const T& x)//一定要用引用,因为T不一定是内置类型
{
/*Node* tail = _head->_prev;
Node* newnode = new Node(x);
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;*/
insert(end(),x);
}
void push_back(T&& x)
{
insert(end(), move(x));
}
void pop_back()
{
/*Node* tail = _head->_prev;
Node* prev = tail->_prev;
delete tail;
_head->_prev = prev;
prev->_next = _head;*/
erase(--end());
}
void push_front(const T& x)
{
/*Node* first = _head->_next;
Node* newnode = new Node(x);
_head->_next = newnode;
newnode->_prev = _head;
newnode->_next = first;
first->_prev = newnode;*/
insert(begin(), x);
}
void pop_front()
{
/*Node* first = _head->_next;
Node* second = first->_next;
delete first;
_head->_next = second;
second->_prev = _head;*/
erase(begin());
}
void insert(iterator pos, const T& x)
{
Node* newnode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
void insert(iterator pos, T&& x)
{
Node* newnode = new Node(move(x));
Node* cur = pos._node;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
void erase(iterator pos)
{
assert(pos != end());//不能删除头节点
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
cur = nullptr;
}
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);
}
private:
Node* _head;
};
}
实际上,左值和右值都是可以自由转换的,因为左值和右值就是语法概念上的区别
void func(const cxf::string& s)
{
cout << "void func(const cxf::string& s)" << endl;
}
void func(cxf::string&& s)
{
cout << "void func(cxf::string&& s)" << endl;
}
int main()
{
cxf::string s1("111111111");
func(s1);
func((cxf::string&&)s1);
func(cxf::string("7777777"));
func((cxf::string&)cxf::string("7777777"));
return 0;
}
8、完美转发
当我们在写函数模板时,如果每个模板都要写一个左值引用版本,一个右值引用版本,未免太麻烦,可以直接使用万能引用模板
template<class T>
void Func(T&& x)
{
// ...
}
当传过来的参数是左值,就会被实例化为左值引用,右值则会被实例化为右值引用。注意,这里能够这样用是因为这是一个函数模板,前面STL的插入操作之所以要写两个版本,是因为那是类模板,类模板实例化之后T就是固定的了。
假设我们现在有下面这个场景,要看t是什么类型
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<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
因为t是一个引用,所以不能直接使用typeid,只能使用函数。但是上面这样写仍然是有问题的,如果T是左值,没什么问题,如果T是右值,这时t本身是左值引用或const 左值引用,需要move。两种情况就无法处理
要解决这个问题,我们先来看一下这个函数模板会实例化出怎样的函数
void PerfectForward(int& t)
{
Fun(t);
}
void PerfectForward(int&& t)
{
Fun(t);
}
void PerfectForward(const int& t)
{
Fun(t);
}
void PerfectForward(const int&& t)
{
Fun(t);
}
会实例化出这4个函数,我们要的就是当t是左值引用或const 左值引用时,直接传t,而t是右值引用或const 右值引用时,传move(t),也就是下面这样
void PerfectForward(int& t)
{
Fun(t);
}
void PerfectForward(int&& t)
{
Fun(move(t));
}
void PerfectForward(const int& t)
{
Fun(t);
}
void PerfectForward(const int&& t)
{
Fun(move(t));
}
此时就可以用到完美转发了
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t)); // 完美转发
}
完美转发也是一个模板。作用:保持属性进行传递。也就是先识别一下T,当T是左值时,直接传t,当T是右值时,传move(t)
move和forward本质都是类型转换,move是传入左值返回右值,forward可自行判断
注意,move(t)会比(string&&)t好一些,因为move(t)还能识别t是否为const
前面的类模板中使用move的地方都可以使用forward替代,但这里只能使用forward