右值引用引用左值及其一些更深入的使用场景分析
来看这样一段代码:
#include<iostream> #include<cassert> #include<algorithm> using namespace std; namespace baiye { 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) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } // 拷贝构造 string(const string& s) :_str(nullptr) { 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); 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; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; } namespace baiye { baiye::string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } baiye::string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } reverse(str.begin(), str.end()); return str; } } int main() { baiye::string str = baiye::to_string(-1234); return 0; }
先看main函数中创建str然后调用to_string函数返回一个string类型赋值给str。
这里编译器优化就变成了拷贝构造。
这样就少了一次拷贝构造。
但如果是这种情况就无法进行优化。
那么这种情况下C++11是怎么解决问题的呢?
// 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { swap(s); }
如果传的参数是右值就会走这个函数。
注意:C++11给右值分为
纯右值(内置类型)
将亡值(自定义类型)
那么在to_string函数中返回了一个将亡值,如果在进行拷贝构造有些没必要:
那么这里在进行拷贝传值的时候就会传给移动构造函数,移动构造函数内部其实就是交换两个对象的值,反正将亡值也要销毁了,这样就不用进行深拷贝了。(深拷贝代价太大,如果深拷贝的对象是vector<vector< int>>效率就非常低了)
但是刚才这种情况还没有解决:
那么这里就可以再写一个移动赋值:
// 移动赋值 string& operator=(string&& s) { swap(s); return *this; }
总结
右值引用是间接起作用,如果右值是将亡值,那么就转移资源。
这里用vector举例:如果传进去的是右值,就会走这个接口,会提升效率。
**注意:**右值引用被引用一次之后,引用的这个别名就变成了左值。
如果不变成左值怎么传给swap。
完美转发
万能引用
#include<iostream> using namespace std; template<class T> void func(T&& x)//这里也可以称为引用折叠,如果传的是左值,就折叠成一个引用符号 { } int main() { int x = 10; func(x);//左值也可以 func(2);//右值也可以 const int y = 20; func(y);//const左值 func(move(y));//const右值 return 0; }
那么这个时候如果func函数中要去调用这四个函数,结果是怎么样的呢?
#include<iostream> using namespace std; 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 func(T&& x) { Fun(x); } int main() { int x = 10; func(x);//左值 func(2);//右值 const int y = 20; func(y);//const左值 func(move(y));//const右值 return 0; }
这里只会调用前两个函数,因为func中的参数x都是左值属性,这里就需要一个叫完美转发的在传参的过程中保持了 x 的原生类型属性。
新的类功能
默认成员函数
C++11 新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
default与delete
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。
#include<iostream> using namespace std; class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) :_name(p._name) , _age(p._age) {} Person(Person && p) = default;//强制生成 private: string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; }
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
#include<iostream> using namespace std; class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) = delete; private: string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = move(s1); return 0; }
这样吴凯伦是内部和外部都无法使用这个拷贝构造函数了。
可变参数模板
参数包
这个也是为了对标C语言的可变性参数,比如printf和scanf。
#include<iostream> using namespace std; // Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) {} int main() { return 0; }
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,
只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特
点,也是最大的难点,即如何展开可变模版参数。
如何查看参数包有几个参数呢?
#include<iostream> using namespace std; template <class ...Args> void ShowList(Args... args) { cout << sizeof...(args) << endl;//查看参数包有几个参数 } int main() { ShowList(1); ShowList(1, 2.2); ShowList(1, 2.2, string("xxx")); return 0; }
遍历参数包中的参数
递归函数方式展开参数包
#include<iostream> using namespace std; // 递归终止函数 void ShowList()//当参数包中的参数变成0个的时候就会调用这个函数 { cout << endl; } // 展开函数 template <class T, class ...Args> void ShowList(T value, Args... args)//第一个参数传给value,剩下的参数传给args参数包 { cout << value << " "; ShowList(args...); } int main() { ShowList(1);//这里1传给value,然后参数包没有参数调用终止函数 ShowList(1, 2.2);//这里第一次1传给value,2.2传给参数包,第二次2.2传给value,参数包没有值,调用终止函数 ShowList(1, 2.2, string("xxx")); return 0; }
非常的怪异。
逗号表达式展开参数包
#include<iostream> using namespace std; template <class T> void PrintArg(T t) { cout << t << " "; } //展开函数 template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; }
这里的arr数组是一个辅助的作用,里面调用的是PrintArg函数,编译器自行初始化arr数组,参数包中有多少个参数数组的空间就有多大。
这里的逗号表达式只是为了初始化arr数组,初始化为0。