C++11新特性讲解
文章目录
- 1. 统一的列表初始化
- 2. auto
- 3. final
- 4. override
- 5. decltype
- 6. 右值引用和移动语义
- 6.1 右值引用使用场景和意义
- 6.1.1 移动构造
- 6.1.2 移动赋值运算符重载
- 7. 新的类功能
- 8. 模板中的万能引用
- 9. 可变模版参数
- 9.1 递归函数方式展开参数包
- 9.2 emplace_back()
- 10 lambda表达式
- 10.1 lambda表达式:
- 10.2 lambda语法格式:
- 10.3 lambda和仿函数
- 捕捉列表说明
- 11包装器
- 12 Bind (绑定)
1. 统一的列表初始化
在C++98中,标准规定了可以使用大括号来初始化数组和结构体,而C++11之后,任何内置类型和自定义类型都可以使用大括号来进行初始化。这给我们初始化操作带来的很大的便利。
而我们的map
等容器也可以使用大括号来进行初始化,这里就要了解一下initializer_list
这个类模版了,它也提供了迭代器的访问方式,这也使得我们的容器都可以使用他来进行初始化,看下如的vector
构造函数中,就有initializer_list
。
用法:
2. auto
auto可以自动推导 等号右边的类型。
如上图,我们使用auto,那么编译器自动推导it是一个迭代器,那么他就可以访问map里面的元素,同时我们还可以用typeid().name()
方式查看这个it的类型
3. final
final
基类成员函数后表示这个函数不能被继承。
4. override
override
加在成员函数后作用是验证是否构成重写,如果不构成重写报错。
5. decltype
decltype
也可以推到类型,不过他还可以用它推导的类型去指定类型,如下图:
( x ∗ y ) (x*y) (x∗y)最终的类型为 d o u b l e double double当我们使用 d e c l t y p e ( x ∗ y ) decltype(x*y) decltype(x∗y)的时候就自动推导为 d o u b l e double double,同时指定 r e t ret ret类型为 d o u b l e double double,并且可以进行正常的赋值操作。
6. 右值引用和移动语义
这里先来了解一下什么左值和右值。
左值是表示一个数据的表达式(如变量或者指针),我们可以获取它的地址,同时左值可以出现在赋值符号左边,但是右值不能出现在赋值符号的左边。
左值和左值引用
int main()
{//以下都是左值int x = 0;int* p = &x;string s1("1111");//以下是左值引用int& r1 = x;int*& r2 = p;string& r3 = s1;return 0;
}
右值也是一个数据的表达式(字面常量,临时变量),我们不可以获取它的地址,同时右值可以出现在赋值符号右边,但是右值不能出现在赋值符号的左边。
右值和右值引用
int main()
{int x = 0, y = 0;//以下都是右值10;x + y;string("111");//以下是右值引用int&& rx1 = 10;int&& rx2 = x+y;string&& rx3 = string("111");return 0;
}
左值引用和右值引用的比较
- 左值引用可以引用左值,但不可以引用右值,但是const修饰的左值引用可以引用右值。(临时变量具有常性)
- 右值引用可以引用右值,但不可以引用左值,但是右值引用可以引用
move
后的左值
move就相当于把左值引用转化为右值引用。
6.1 右值引用使用场景和意义
6.1.1 移动构造
这里有一份 s t r i n g string string模拟实现代码,我们有一个to_string
函数,它的作用是把一个整数转为字符串,然后返回回来。
namespace bit
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{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);}char& operator[](size_t pos){assert(pos < _size);return _str[pos];} 移动构造//string(string&& s)// :_str(nullptr)// , _size(0)// , _capacity(0)//{// cout << "string(string&& s) -- 移动语义" << endl;// swap(s);//} 移动赋值//string& operator=(string&& s)//{// cout << "string& operator=(string&& s) -- 移动语义" << endl;// swap(s);// return *this;//}~string(){delete[] _str;_str = nullptr;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){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;}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;reserve(s._capacity);for (auto& ch : s){push_back(ch);}}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 移动构造//把临时变量的构造优化了string(string&& s):_str(nullptr){cout << "string(const string& s) -- 移动拷贝" << endl;swap(s);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};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 s1 = bit::to_string(1111);return 0;
}
如上图,在正常的情况下,我们会进行拷贝构造,但是编译器会对这个返回值进行优化处理,我们临时变量的拷贝构造过程直接优化了。但是这个时候我们还是深拷贝了一份 s t r str str,那如果我们加上移动构造呢? 移动构造依靠的是右值引用,前面我们也说了,右值引用一般都是临时变量、字面常数等。那么此时返回的使我们在to_string
这个函数栈帧里面开辟的,函数结束了也就是释放了,所以此时的str的值会拷贝一份到临时变量中,那么此时就满足了右值引用可以进行移动构造。
这里的还有一个构造是我们to_string
类里面创建的变量str
如上图,由于我们传过来的 s s s已经快要释放了,那么此时我们直接把 s s s中的值交换过来就可以了。这也是之前左值引用做不到的事情,到了现在的右值引用才可以彻底解决。同时也要注意这里的 s s s虽然我们函数参数写的是右值引用类型,但是 s s s的属性还是一个左值,不然swap根本进行不了。
移动构造效果如下:
好,那么此时我们在返回的过程中就不要多开辟一份空间了。所以以后返回的如果是局部变量直接返回即可。
6.1.2 移动赋值运算符重载
同样的普通赋值拷贝我们还需要再构造一份出来,移动赋值拷贝不需要构造,直接把 s s s的值拿过来。
这里再来看一种情况:
我们发现此时这里进行的竟然是深拷贝?这时为什么呢(此时写了移动拷贝)。
void push_back(const T& val) { insert(end(), val); }//新增右值引用void push_back(T&& val){insert(end(), val);}//新增右值引用iterator insert(iterator pos, T&& val){Node* newNode = new Node(val);Node* next = pos._node;Node* pre = (--pos)._node;newNode->_prev = pre;newNode->_next = next;pre->_next = newNode;next->_prev = newNode;return iterator(pos._node->_prev);}// 在pos位置前插入值为val的节点iterator insert(iterator pos, const T& val){Node* newNode = new Node(val);Node* next = pos._node;Node* pre = (--pos)._node;newNode->_prev = pre;newNode->_next = next;pre->_next = newNode;next->_prev = newNode;return iterator(pos._node->_prev);}
如上图,我们进入的**push_back()**确实是右值引用的,但是为什么到了插入这里却进的左值呢?
因为右值引用,引用了一个右值,但是右值引用它本身的属性却是左值(可以取到地址),这也是为什么上面 s t r i n g string string可以进行
swap
的原因。如下图:
所以我们还需要把 v a l val val的类型move一下转为右值类型,同时还需要添加一个右值引用的构造
修改后代码:
// 新增移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}void push_back(T&& val){insert(end(), move(val)); //转为右值引用}iterator insert(iterator pos, T&& val){Node* newNode = new Node(move(val));//转为右值引用Node* next = pos._node;Node* pre = (--pos)._node;newNode->_prev = pre;newNode->_next = next;pre->_next = newNode;next->_prev = newNode;return iterator(pos._node->_prev);}
7. 新的类功能
在类和对象中我们说到类中的默认成员函数只有6中:
- 默认构造
- 默认拷贝构造
- 默认赋值拷贝
- 析构
- 取地址重载
- const取地址重载
这里重要的为前四个默认成员函数,而现在有多了两个。
- 默认移动构造
- 默认移动赋值运算符重载
这两个使用的注意事项为:
- 当一个类中没有写析构函数、拷贝函数、赋值运算符重载函数时,编译器会默认生成一个默认移动构造函数,对于内置类型,按照字节一个一个拷贝,自定义类类型会执行他自己的默认构造函数。
- 当一个类中没有写析构函数、拷贝函数、赋值运算符重载函数时,编译器会默认生成一个 默认移动赋值运算符重载 ,对于内置类型,按照字节一个一个拷贝,自定义类类型会执行他自己的默认构造函数。
8. 模板中的万能引用
如上图:我们此时在模版下,这里的T&&
它并不是上面所说的右值引用,而是万能引用。意思就是他可以接受左值和右值。那么根据main
函数中的注解此时输出结果应该是对应的引用,此时我们运行一下:
如上图:发现输出的都是左值引用,这是因为虽然万能引用具有接受左值和右值的能了,但是引用类型却会进行退化。如果是左值,那么就是左值引用,但是如果是右值,会产出退化,变为左值引用(右值引用本身也是左值),所以才会输出上述结果。那么如果把t
都move
一遍转为右值引用呢?那么输出的结果都会变为右值引用。
这个时候我们就有一个完美转发的函数模版,它的作用如下:
- 如果是传入的左值引用,那么返回左值引用
- 如果传入的是右值引用,那么返回右值引用
用法如下:
如上图:输出结果变为对应的引用。同时注意,万能引用只有再模版下起作用,也就是说这个类型是需要去推导的。例如我们上述举的移动构造的例子是因为我们确定了他是右值引用所以我们才能直接move
转为右值引用,上述这种不确定的我们都需要完美转发。
9. 可变模版参数
可变模版参数可能很陌生,我们举个例子:printf
其实就是一个可变模版参数,它可以传入任意个任意类型的参数。
如上图:我们这里的Args
取的是argument
的缩写。
template<class ...Args> //Args是一个模版参数包,args是一个函数形参参数包
void Showlist(Args... args)//可以包含 0 ~ 任意个模版参数
{PrintArg(args...)
}
由于上述 A r g s Args Args前面带着 . . . (三个点) ...(三个点) ...(三个点)所以我们称他为参数包,上面的 a r g s args args有省略号所以我们管他叫做可变模版参数。同时,我们无法直接获取参数包里面的参数,所以只能通过展开参数包的方式来获取每个参数,这也是可变模版参数的一个特点,这使我们无法使用args[i]
这样来获取参数,只能通过一些技巧来获取。
9.1 递归函数方式展开参数包
观看如下代码:
//递归结束函数
void PrintArg()
{cout << endl;
}
template<class T,class ...Args>
void PrintArg(T&& x, Args... args)
{cout << x << " ";PrintArg(args...);
}
可变模版参数
template<class ...Args>
void Showlist(Args... args)
{PrintArg(args...);
}
int main()
{int x = 10;double y = 2.2;std::string s("1111");Showlist(x, y, s);return 0;
}
这里我们可以把void Showlist(Args... args)
这里看做这样:void Showlist(int x,duoble y, std::string s)
。那么我们如何进行推导呢?
推导过程:
void Showlist(int x,duoble y, std::string s)
{PrintArg(x,y,s);
}||\/
void PrintArg(int&& x, double y, std::string s)
{cout << x << " ";PrintArg(y,s);
} ||\/
void PrintArg(double&& y, std::string s)
{cout << y << " ";PrintArg(s);
} ||\/
void PrintArg(std::string&& s, )// 空的参数包
{cout << s << " ";PrintArg(); // 传入了空的参数包
} ||\/
//递归结束函数
void PrintArg()
{cout << endl;
}
我们应该是需要进行如上的推导过程的,但是当我们写了可变模版参数后,这种事情都让编译器去承担了 ^^。
这里还可以这样写:
template<class T>
int print(T&& x)
{cout << x << " ";return 0;
}
//第二种方式
//可变模版参数
template<class ...Args>
void Showlist(Args... args)
{cout << "agrs size: " << sizeof...(args) << endl;int a[] = { print(args)... };//逗号表达式 //int a[] = { (cout << (args) << " ", 0)...}; cout << endl;
}
int main()
{int x = 10;double y = 2.2;std::string s("1111");Showlist(x, y, s);return 0;
}
这里的int a[] = { print(args)... }
== int a[] = {print(int),print(double),print(bit::string)}
只不过这种事情编译器帮我们做了。
同时int a[] = { (cout << (args) << " ", 0)...};
这样的写法也可以,由于我们是一个int数组,那么每个元素必须是整数,逗号表达式 括号中两边都会执行但是最后返回的是右边的值。
9.2 emplace_back()
观察到STL容器里面都会有一个emplace_back
这个函数,那么他和普通的push_back()
有什么不同呢?
功能:emplace_back 也是用来插入元素的,只不过它的参数使用的是可变模版参数。
区别:以下是一段测试代码
void Test_emplace_back()
{list<bit::string> lst;bit::string s1("11111");lst.emplace_back(s1);cout << endl << endl;lst.emplace_back("22222222"); //直接使用const char* 进行构造无需拷贝,节省空间cout << endl << endl;lst.emplace_back(bit::string("333333"));cout << endl << endl;lst.emplace_back(10, 'c');//直接使用bit::string(n,char) 的拷贝构造,无需再拷贝,节省空间
}void Test_push_back()
{list<bit::string> lst;bit::string s1("11111");lst.push_back(s1);cout << endl << endl;lst.push_back("22222222"); //直接使用const char* 进行构造无需拷贝,节省空间cout << endl << endl;lst.push_back(bit::string("333333"));cout << endl << endl;lst.push_back(bit::string(10, 'c'));//直接使用bit::string(n,char) 的拷贝构造,无需再拷贝,节省空间
}int main()
{cout << "\\\\\\\\\\\\\\\\\\\\测试emplace_back()\\\\\\\\\\\\\\\\\\\\\\\\" << endl;Test_emplace_back();cout << "\\\\\\\\\\\\\\\\\\\\测试push_back()\\\\\\\\\\\\\\\\\\\\\\\\" << endl;Test_push_back();return 0;
}
通过上图我们可以看到emplace_back()
和push_back()
的区别在于当我们插入自定义类型的时候,如果自定义类型有对应的构造函数那么emplace_back可以直接构造,而push_back()还需要先构造再拷贝构造给容器对应的位置。所以emplcae_back()对比push_back()效率是高的。
模拟实现:
template<class ...Args>void emplace_back(Args&&... args){insert(end(), std::forward<Args>(args)...);}template<class ...Args>iterator insert(iterator pos, Args&&... args){Node* newNode = new Node(std::forward<Args>(args)...);Node* next = pos._node;Node* pre = (--pos)._node;newNode->_prev = pre;newNode->_next = next;pre->_next = newNode;next->_prev = newNode;return iterator(pos._node->_prev);}
同时这里的insert(end(), std::forward<Args>(args)...);
以及new Node(std::forward<Args>(args)...);
我们都需要进行完美转发,前面也讲过了,虽然万能引用可以接受左值和右值,但是引用类型如果是右值引用的话会退化,所以为了准确的识别类型我们需要使用完美转发。同时注意这里的写法:std::forward<Args>(args)...
。同时emplace_back() 并不支持插入多个相同的值。
例如这样:
list<string> lst;
string s1("11111");
lst.emplace_back(s1,s1,s1); // 这样是不行的。
// 如果是多个参数的话,只有当对应容器参数类型实现了对应的构造函数的时候才可以。
// 例如:string类型实现了通过n个char类型构造string的构造函数,那么此时我们可以这样写:
lst.emplace_back(10,'c');
10 lambda表达式
在C++中排序我们用的都是 s o r t sort sort, s o r t sort sort默认是升序,但是我们可以通过传入不同的仿函数来实现降序。如果是内置类型直接使用库里面的比较函数即可。
#include <algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
但是如果是像下方的自定义类型,则需要自己写比较函数。
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式 。
10.1 lambda表达式:
下面就是用lambda实现在比较函数。
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) ->bool{return g1._price < g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price > g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate; });
}
如上段代码,可以看出我们的lambda是一个匿名函数。
10.2 lambda语法格式:
捕捉列表 参数列表 返回值 函数体
[capture-list] (parameters) mutable -> return-type { statement}
mutable:默认我们函数为const修饰的,如果需要改变它的常属性,添加mutable即可。同时如果需要写多行代码直接这样即可:
auto fun1 = []()->void{cout << "hello world" << endl;cout << "hello world" << endl;};fun1();
同时lambda是没有类型的,而它的返回值我们需要使用auto来接受,他跟我们的函数很相似,只是没有函数名字罢了。
//多行代码的书写方式 ,这里的返回值和参数都可以省略auto fun1 = []()->void auto fun1 = []{ {cout << "hello world" << endl; -> cout << "hello world" << endl;cout << "hello world" << endl; cout << "hello world" << endl;}; };fun1(); fun1();
10.3 lambda和仿函数
其实我们这里的参数列表就是我们lambda类初始化的成员变量,当我们调用的时候,会去lambda调用operator() 同时这个lambda类名每次都不一样,所以即使两个lambda表达式相同也不可以进行赋值,因为不是同一个类。 因为这个lambda名字每次都不一样。
捕捉列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
- [ v a r ] [var] [var]:表示值传递捕捉变量
- [ & ] [\&] [&]: 表示引用传递捕捉所有父作用域中的变量(包括this)
- [ = ] [=] [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [ & v a r ] [\&var] [&var]:表示引用传递捕捉变量var (混合传递)
[ v a r ] [var] [var]:表示值传递捕捉变量:
同时传值传递影响不了外面的a和b,所以想要交换两个值只有传引用传参
[ & v a r ] [\&var] [&var]: 表示引用传递捕捉所有父作用域中的变量(包括this) :
[ = ] [=] [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[ & ] [\&] [&]: 表示引用传递捕捉所有父作用域中的变量(包括this)
[ & v a r ] [\&var] [&var]:表示引用传递捕捉变量var (混合传递)
11包装器
示例代码:
//包含functional头文件(仿函数less和greater也在这里)
#include <functional>int f(int x, int y)
{return x + y;
}struct functor
{int operator()(int x, int y){return x + y;}
};int main()
{std::function<int(int, int)> func1 = f; //函数指针std::function<int(int, int)> func2 = functor(); //仿函数std::function<int(int, int)> func3 = [](int x, int y) {return x + y; }; //lambda表达式cout << func1(1, 1) << endl;cout << func2(1, 1) << endl;cout << func3(1, 1) << endl;return 0;
}
包装器包装类成员函数的时候需要注意的是:
- 如果是静态成员函数要声明类域
- 如果是普通成员函数,由于成员函数里面有一个this指针,所以我们需要多传入一个参数。同时普通成员函数去处函数名的地址需要加取地址符号。
class Plus
{
public:int plusi(int a, int b){return a + b;}static int plusd(int a, int b){return a + b;}
};
如上图,我们包装类中的普通成员函数的时候还需要考虑this指针的问题。
如上图,当我们包装类成员函数的时候,需要把this指针也包含进去,这里传入类对象也是可以的,所以可以传入 P l u s Plus Plus。
12 Bind (绑定)
bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表 。
如上图为参数原型,我们 f n fn fn为可调用对象, a r g s args args为参数列表(placceholders)。
这个参数列表只有**_1, _2 , _3**等等,他表示的是传入参数的第几个。注意这里是函数调用时传入实参的第几个。同时这里也可以得出它的返回值就是仿函数。
上述我们包装器包装类成员函数的时候需要多传入一个this指针,我们就可以使用bind来把this指针设为默认值,这样就不需要调用的时候传入类对象了,如下图: