一. 统一的列表初始化 {} 适用于各种STL容器
- 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; }
- 创建对象时也可以使用列表初始化方式调用构造函数初始化
struct Date { int _year; int _month; int _day; }; int main() { int a{ 2 }; //支持使用{}的统一初始化了 vector <int > intv{ 1, 2, 3, 5 }; vector <Date > datev{ { 2001, 10, 9 }, Date{ 2001, 10, 21 } }; //上述的这些方式都是支持的了 {} 其实是调用的构造函数 return 0; }
- 为什么可以支持 {} 这种方式来调用各种容器的构造函数?
- 本质原因: 支持了 std::initializer_list 作为参数的构造函数的产生
在容器中构造函数中的出现例子:
- 从此以后终于知道为啥引入 initializer_list头文件之后就可以进行各种容器的{} 初始化形式了.
光光知道还不够, 我们一定要去看看它的底层实现是怎样的, 如下
namespace tyj { template <class T> class vector { typedef T* iterator; typedef const T* const_iterator; public: //如何进行一个初始化, 范围形式的初始化 vector(initializer_list<T>& l) { _start = new T[l.size()]; _finish = _start + l.size(); _endofstorage = _start + l.size(); iterator sit = _start; //然后就是范围形式的构造了 //如下是方式1: 基于范围的实现 /*for (auto& e : l) { *sit++ = e; }*/ //然后是第二种形式, 使用迭代器进行赋值, 其实也就是上述范围的赋值的底层 typename initializer_list<T>::iterator lit = l.begin(); while (lit != l.end()) { *sit++ = *lit++; } } //针对这个 operator = 赋值运算符的重载 还是复用上述的构造函数 vector<T>& operator=(initializer_list<T> l) { vector<T> tmp(l); std::swap(_start, tmp._start); std::swap(_finish, tmp._finish); std::swap(_endofstorage, tmp._endofstorage); return *this; } private: iterator _start; iterator _finish; iterator _endofstorage; }; } int main() { //测试上述的东西: //断点测试, 进去查看其中的内存即可 tyj::vector<int> intv = initializer_list<int>{ 1, 2, 3, 4, 5 }; return 0; }
二. 类型推导 auto 和 decltype的出现
- auto 关键字的作用在编译阶段对于=右边的对象进行自动的类型推导
int main() { int i = 10; auto p = &i; auto pf = strcpy; map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; //map<string, string>::iterator it = dict.begin(); auto it = dict.begin(); return 0; }
既然有了auto 可以自动推导=右边的类型, 为啥需要decltype呢????
decltype的出现是为了补齐auto 不支持对于表达式的类型推导的缺陷的, 经常适用于后置返回类型的推导. 使用形式如下: 如下包含了万能引用, 完美转发lambda表达式等等知识点, 后序会一一讲解清除
template<class T, class U> auto Add(T&& t, U&& u) ->decltype(std::forward<T>(t) +std::forward<T>(u)) { return std::forward<T>(t) +std::forward<T>(u); } int main() { auto func = [](int a, double b)->decltype(a + b){ return a + b; }; cout << Add(2, 5); while (1); return 0; }
三. 右值引用移动语义 (特别重要的新特性)
- 故名思意, 对左值的引用就是左值引用, 对于右值的引用就是右值引用
- 定义左值和右值的区别, 可否进行取地址, 可以取地址的就是左值, 不可以取地址的就是右值
定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址, 所以本质还是左值.
int main() { int a = 10; int& b = a; //此处是左值引用 int&& c = 2; //此处是右值引用 const int& d = 100; //突然发现此处也是可行的? const int& e = a; int&& h = std::move(a); //std::move 作用 将左值引用转换为右值引用 //先引出结论: const 左值引用既可以引用左值也可以引用右值 //右值引用 就只能引用右值不可以引用左值 return 0; }
- const 左值引用既可以引用左值也可以引用右值.
- std::move() 方法可以将左值转换为右值
注意点: 我们不可以对于右值进行一个取地址, 但是一旦给右值取别名之后, 会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量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; }
左值引用作为参数和返回值完美解决了深拷贝的问题, 但是存在一些情况像是局部对象的返回, 就没办法以左值引用返回, 这个时候需要进行深拷贝, 于是右值引用的出现使得这个问题的解决成为可能, 有没有什么办法可以将局部对象作为返回值的这个深拷贝也优化掉?????
首先移动构造的本质: 本质是一种资源窃取, 资源转移.....
比如 return str; str 如果是一个局部对象的话, 它出不了函数, 一旦函数调用结束, 就会随着栈帧一起释放掉, 但是它的底层存在 char* _str 这样一个字符串数组的成员. 要是一同释放掉着实浪费, 我们是否可以将其利用起来??????
移动构造的本质就是 将 即将返回的局部对象的所有底层的堆区资源进行转移 窃取, 反正函数调用结束你即将消亡, 然鹅我拷贝构造做深拷贝正好需要的也是这个, 于是将这个即将消亡的 str 的 底层的 堆区 资源转移,进行移动构造出新的对象
void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } //提供swap方法方便转移资源 string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0) { swap(s); //直接通过交换, 转移财产, 我的全是空, 和你换 //反正你即将死亡, 不如将你的资源换给我助我快速构造加以利用 std::cout << "string(string&& s) ---移动构造" << std::endl; } string& operator= (string&& s) { swap(s); std::cout << "string& operator= (string&& s) ---移动赋值" << std::endl; return *this; } tyj::string to_string(int val) { tyj::string s; bool flag = 0; if (val < 0) { val *= -1; flag = 1; } while (val) { s.push_back(val % 10); val /= 10; } if (flag) s.push_back('-'); std::reverse(s.begin(), s.end()); return s; // 返回即将消亡的局部对象, 如何优化掉这个深拷贝? }
图解分析, 存在移动构造和不存在移动构造的区别:
移动构造相对于拷贝构造: 比较区别????
移动构造和拷贝构造本质都是构造一个对象: 只是两者采取的构造方式不一样, 拷贝构造的话如果是深拷贝, 也就是底层存在堆区数据, 存在指针, 就需要新开堆区空间, 且需要进行堆区数据的拷贝, 效率低... 移动构造, 我还是需要堆区空间存储数据, 但是我不自己新开辟, 我直接将拷贝对象的堆区资源转移过来成为我的即可.,.... 不需要new 空间 + 数据转移, 效率提高
注意: 移动构造 和 拷贝构造相比, 它的高效仅仅体现在深拷贝 上面, 如果不存在深拷贝. 仅仅只是栈区数据的拷贝, 两者效率是相同的
深拷贝: 存在堆区空间的拷贝.... 也就是存在底层存储数据的空间的拷贝
移动构造高效就高效在了这个底层存储数据空间的获取上面, 不是从新申请空间 + 拷贝数据的方式来获取的, 而是直接的获取对方的现有空间 + 数据
全部代码如下, 可以测试上述推论: 分别测试存在移动语义和不存在的情况看看调用如何??
namespace tyj { class string { typedef char* iterator; typedef const char* const_iterator; public: const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; } iterator begin() { return _str; } iterator end() { return _str + _size; } string(const char* s = "") : _size(strlen(s)) , _capacity(_size) , _str(new char[_size]) { //std::cout << "string(const char* s) ---构造对象" << std::endl; } //提供一个swap 函数 一切都是为了方便后序的资源转移拷贝构造等等复用代码 void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { //复用代码 string tmp(s._str); swap(tmp); std::cout << "string(const string& s) --- 深拷贝" << std::endl; } string& operator=(const string& s) { //复用拷贝构造代码 string tmp(s); swap(tmp); std::cout << "string& operator=(string s) --- 深拷贝" << std::endl; return *this; } //然后右值引用出现了, 出现了另外一种方式, 叫做移动构造 string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0) { swap(s); std::cout << "string(string&& s) ---移动构造" << std::endl; } string& operator= (string&& s) { swap(s); std::cout << "string& operator= (string&& s) ---移动赋值" << std::endl; return *this; } void reserve(size_t n) { //扩容 if (n <= _capacity) return; char* pTemp = new char[n]; _capacity = n; memcpy(pTemp, _str, _size + 1); //拷贝_size + 1个过去, 结束字符也拷贝过去 delete[]_str; _str = pTemp; } void push_back(char c) { if (_size == _capacity) { reserve(_capacity > 0 ? (_capacity << 1) : 8); } _str[_size++] = c; //放入数据 _str[_size] = 0; //后序制结束 } private: size_t _size; size_t _capacity; char* _str; }; //来一个函数, 方便测试右值引用使用案例 tyj::string to_string(int val) { tyj::string s; bool flag = 0; if (val < 0) { val *= -1; flag = 1; } while (val) { s.push_back(val % 10); val /= 10; } if (flag) s.push_back('-'); std::reverse(s.begin(), s.end()); return s; } } int main() { tyj::string s1 = tyj::to_string(123456); tyj::string s2; s2 = tyj::to_string(23456); return 0; }
- STL 容器中全部都是怎加了移动构造和移动赋值的: 如下图:
就连 STL的 push_back 等等这种接口上都是同样增添了右值引用版本的:
四. 万能引用 + 完美转发
万能引用: 就是 既可以引用左值 也可以引用右值 模板中的&& 万能引用
为了引出完美转发 首先先看如下的一段代码
void f(int& a) { std::cout << "左值引用" << endl; } void f(const int& a) { std::cout << "const 左值引用" << endl; } void f(int&& a) { std::cout << "右值引用" << endl; } void f(const int&& a) { std::cout << "const 右值引用" << endl; } template <class T> void PerfectForward(T&& t) { f(t); } int main() { PerfectForward(2); int a = 10; PerfectForward(a); PerfectForward(move(a)); return 0; }
结果不尽人意, 全部都是左值引用????? 为啥 仅仅经过了一次参数 t 接收之后t 退化了 退化成了左值?????
前面我们学过 右值一旦 被引用之后就可以取地址了, 其实也就自然退化为左值了, 这个时候需要调用 std::forward<类型>() 进行完美转发, 保持之前的类型属性不退化
void PerfectForward(T&& t) { //先尝试一下不是完美转发 //f(t); //然后进行完美转发 f(std::forward<T>(t)); //转发之后效果就恢复正常了 }
- 万能引用 + 完美转发, 在 过程中保持住 右值属性不退化
- 接下来就是完美转发在实际案例中的使用场景
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, std::forward<T>(x)); //完美转发 } void PushFront(T&& x) { //Insert(_head->_next, x); Insert(_head->_next, std::forward<T>(x)); } void Insert(Node* pos, T&& x) { Node* prev = pos->_prev; Node* newnode = new Node; newnode->_data = std::forward<T>(x); // 关键位置 // prev newnode pos 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 newnode pos prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } private: Node* _head; }; int main() { List<tyj::string> lt; lt.PushBack("1111"); lt.PushFront("2222"); while (1); return 0; } //上述所有的 传入 && 右值引用作为参数的地方后序进一步传参全部需要使用forward<>()完美转发 //完美转发保持之前原有的类型属性不变, 如果不使用完美转发效果就是后序全部变成左值引用退化了 //可以取地址的就是左值了, 不可以取地址的才是右值, 右值一旦被变量接收其实也就退化成左值了 //如果想要继续保持右值的属性就需要完美转发
五. 可变参数模板 (参数包)
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
简单的理解可以理解为一个参数包, 可以支持传入数量不固定的参数, 而且还是模板, 使用起来更加的灵活
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) {}
模板参数包的简单使用. 第一种解包方式, 递归解包
//设置递归终点, 当参数包解包完全, 适配空包 void ShowList() { cout << endl; } //递归形式调用解包, 每一次解出一个参数 template<class T, class ...Args > void ShowList(T& val, Args... args) { cout << val << endl; ShowList(args...); } int main() { ShowList("dsadsa", 2, 5, 6, "edsad"); return 0; }
- 解包方式2: 利用数组结合 {} 初始化的方式 进行解包
template<class T> int PrintArg(T& val) { cout << val << endl; return 0; } template<class ...Args> void ShowList(Args... args) { int arr[] = { PrintArg(args)... }; } int main() { ShowList(1, 43, 6, 7, 8, "dfsads", "dsaw", 'a'); return 0; }
六. emplace_back 的出现和对比分析 push_back接口 emplace_back 是 结合这 可变模板参数出现的
int main() { // 下面我们试一下带有拷贝构造和移动构造的tyj::string,来试试 // 我们会发现其实差别也不到,emplace_back是直接构造了,push_back // 是先构造,再移动构造,其实效率也还好, 差别不算很大 std::list< std::pair<int, tyj::string> > mylist; mylist.emplace_back(10, "sort"); mylist.emplace_back(make_pair(20, "sort")); mylist.push_back(make_pair(30, "sort")); mylist.push_back({ 40, "sort" }); return 0; }