什么是左值?
左值是一个数据的表达式,能被取地址和赋值。
左值引用就是给左值取别名。
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修饰后的左值。
左值引用的意义是什么? 函数传参/函数传返回值 – 减少拷贝
template<class T>
void func1(const T& x)
{
// ...
}
template<class T>
const T& func2(const T& x)
{
// ...
return x;
}
但是左值引用没有彻底解决函数中局部对象返回的问题:
template<class T>
T func3(const T& x)
{
T temp;
// ...
return temp;
}
右值引用的意义就是为了补齐这个短板
先来看一段代码
#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<test::string> 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;
}
右值引用在引用右值后具有左值属性,是可以取地址可以修改
int main()
{
int x = 1, y = 1;
int&& rr1 = x + y;
const int&& rr2 = x + y;
rr1++; //右值引用具有左值属性
//rr2++; 这也是为什么右值引用有const原因
}
为什么要这样呢?
为了移动构造和移动赋值的实现,否则不能换资源。
那么右值引用的左值属性有什么用呢?
下面通过实现list的移动拷贝和赋值来体会
namespace test
{
template <class T>
struct ListNode
{
struct ListNode<T>* _next;
struct ListNode<T>* _prev;
T data;
ListNode(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, data(x)
{}
};
template <class T, class Ref, class Ptr>
struct list_iterator
{
typedef struct ListNode<T> node;
typedef struct list_iterator<T, Ref, Ptr> 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 T>
class list
{
typedef struct ListNode<T> node;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> 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 <class InputIterator>
list(InputIterator first, InputIterator last)
{
initlist();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list(const list<T>& lt)
{
initlist();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
list& operator=(list<T> 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<test::string> 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))
{}
上面的代码可见出现了大部分的冗余代码,有没有方法能够让一种引用能接收左值引用和右值引用呢?
答案是万能引用。
(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<typename T>
void PerfectForward(T&& t)
{
// t可能是左值,可能是右值
//Fun(move(t));
// 完美转发,保持它属性
Fun(std::forward<T>(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完美转发
原来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;
}
当实现了析构、拷贝构造或赋值重载,此时编译器不会实现默认移动构造以及默认移动赋值重载,如果此时要编译器强制实现,就可以对其函数进行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;
}
如果想要限制某些默认函数的生成,在C++11之前,是将只将声明放入私有,这样外部调用生成函数就爆错。C++11之后,只要对声明加上=delete就可以限制其生成。
下面代码如果不限制默认拷贝构造生成程序就会崩溃。
class A
{
public:
void func()
{
A tmp(*this);
}
A()
{}
~A()
{
delete[] p;
}
//C++11
A(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;
}