C++24--右值引用C++11新特性
目录
1.C++11简介
2.统一的列表初始化
2.1{}初始化
2.2std::initializer_list
3.声明
3.1auto
3.2decltype
3.3nullptr
4.范围for循环
5.智能指针
6.右值引用和移动语义
6.1左值引用和右值引用
6.2左值引用与右值引用比较
6.3右值引用使用场景和意义
6.4右值引用引用左值及其一些更深入的使用场景分析
6.5完美转发
1.C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多C++11增加的语法特性非常篇幅非常多,我们这里没办法一一讲解,所以本篇主要讲解实际中比较实用的语法。
https://en.cppreference.com/w/cpp/11
小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++07。但是到06年的时候,官方觉得2007年肯定完不成C++07,而且官方觉得2008年可能也完不成。最后干脆叫C++0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
2.统一的列表初始化
2.1{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1,2,3,4,5 };
int array2[] = { 0 };
Point p = { 1,2 };
return 0;
}
C++11扩大了用大括号扩起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[] = { 1,2,3,4,5 };
int array2[5] = { 0 };
Point p = { 1,2 };
//C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4] {0};
return 0;
}
创建对象时也可以使用初始化列表方式调用构造函数初始化
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date(int year,int month,int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1);
//C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022,1,2 };
Date d3 = { 2022,1,3 };
return 0;
}
2.2std::initializer_list
std::initializer_list的文档介绍:http://www.cplusplus.com/reference/initializer_list/initializer_list/
std::initializer_list是什么类型:
int main()
{
//the type of il is an initializer_list
auto il = { 10,20,30 };
cout << typeid(il).name() << endl;
return 0;
}
std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
http://www.cplusplus.com/reference/list/list/list/
http://www.cplusplus.com/reference/vector/vector/vector/ http://www.cplusplus.com/reference/map/map/map/ http://www.cplusplus.com/reference/vector/vector/operator=/
int main()
{
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
//这里{"sort", "排序"}会初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"},{"insert", "插入"} };
//使用大括号对容器赋值
v = { 10,20,30 };
return 0;
}
让模拟实现的vector也支持{}初始化
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
vector(size_t n, const T& val = T())
{
resize(n, val);
}
vector(int n, const T& val = T())
{
resize(n, val);
}
vector(initializer_list<T> lt)
{
reserve(lt.size());
for (auto e : lt)
{
push_back(e);
}
}
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector()
{}
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
//memcpy(_start, v._start, sizeof(T)*v.size());
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
// v1 = v2
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
//
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * sz);
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
void push_back(const T& x)
{
/*if (_finish == _endofstorage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
++_finish;*/
insert(end(), x);
}
void pop_back()
{
erase(--end());
}
size_t capacity() const
{
return _endofstorage - _start;
}
size_t size() const
{
return _finish - _start;
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
// 解决pos迭代器失效问题
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
3.声明
C++11提供了多种简化声明的方式,尤其是在使用模板时。
3.1auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所提auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"},{"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
3.2decltype
关键字decltype将变量的类型声明为表达式指定的类型。
template<class T1,class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y)ret; //ret的类型是double
decltype(&x) p; //p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
3.3nullptr
由于C++NULL被定义成字面量0,这样就可能会带来一些问题,因为0既能表示指针常量,又能表示整型常量。所以出于清晰和安全的角度考虑,C++11中增加了nullptr,用于表示空指针。
#indef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
4.范围for循环
之前已经讲过了就不再赘述
5.智能指针
过一段时间会单独出一篇关于智能指针的文章
6.右值引用和移动语义
6.1左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中增加了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
//以下的b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
//以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
//以下几个都是常见的右值
10;
x + y;
fmin(x, y);
//以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
//这里编译会报错:error C2106: "=":左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能去字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1.如果不想rr1被修改,可以用const int&& rr1去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; //报错
return 0;
}
6.2左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用即可引用左值,也可引用右值。
int main()
{
//左值引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a;//ra1为a的别名
//int& ra2 = 10; //编译失败,因为10是右值
//const左值引用既可引用左值,也可引用右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结:
- 右值引用只能右值,不能引用左值
- 但是右值引用可以move以后的左值
int main()
{
//右值引用只能右值,不能引用左值
int&& r1 = 10;
//error C2440: “初始化”: 无法从“int”转换为“int &&”
//无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
//右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
6.3右值引用使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是画蛇添足呢?下面我们来看看左值引用的短板,右值引用时如何补齐这个短板的!
namespace bit
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
string& operator=(string&& s)
{
cout << "string& operator=(string && s) -- 移动拷贝" << endl;
swap(s);
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;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
左值引用的使用场景:
做参数和做返回值都可以提高效率
void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
bit::string s1("hello world");
//func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
//string operator+=(char ch) 传值返回存在深拷贝
//string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少一次拷贝构造(如果是老的编译器可能是两个拷贝构造)
namespace bit
{
bit::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::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()
{
//在bit::string to_string(int value)函数中可以看到,这里
//只能使用传值返回,传值返回导致至少一次拷贝构造
bit::string ret1 = bit::to_string(1234);
bit::string ret2 = bit::to_string(-1234);
return 0;
}
右值引用和移动语义解决了左值的短板问题:
在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来。占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
int main()
{
bit::string ret2 = bit::to_string(-1234);
return 0;
}
再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了
6.4右值引用引用左值及其一些更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用一个左值实现移动语义。当需要用右值引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
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() {
bit::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
bit::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造 // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的 // 资源被转移给了s3,s1被置空了。
bit::string s3(std::move(s1));
return 0;
}
void push_back (value_type&& val);
int main()
{
list<bit::string> lt;
bit::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222"); lt.push_back(std::move(s1));
return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义
6.5完美转发
模板中的&&万能引用
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<class T>
void PerfectForward(T&& t)
{
Fun(t);
}
int mian()
{
PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 8;
PerfectForward(b); //const 左值
PerfectForward(move(b)); //const 右值
return 0;
}
std::forward完美转发在传参的过程中保留对象原生类型属性
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;
}
//std::forward<T>(t)在传参的过程中保持了t的原生类型属性
template<class T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 8;
PerfectForward(b); //const 左值
PerfectForward(move(b)); //const 右值
return 0;
}
完美转发实际中的使用场景:
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head,x);
Insert(_head, forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x)
Insert(_head->_next, forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = forward<T>(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}