C++11特性
一.C++的发展历史
C++11是C++的第二个主要版本,从C++98起的重要更新,引入了大量更改,从C++11起C++规律的进行每3年更新一次。
二.列表初始化
2.1 C++98和C++11中的 { }
传统的C++98中使用 { } 来进行列表初始化,结构体函数体都使用此类方法,到了C++11中,对对象进行初始化时,可以省略 = 直接使用 { } 进行初始化。
2.2 C++11中的initializer_list
上面的 { }列表初始化已经很方便了,但是面对容器时,仍有些麻烦,例如vector,我们需要对数一个一个进行书写。initializer_list这个类底层是一个数组,保存起始和末尾位置的指针,这样我们可以一次性传给容器中,而无需考虑自身是否因空间增大要重新扩容等因素。
vector(initializer_list<T> l) { for (auto e : l) push_back(e) }
如图可以直接传给容器。
int main() {std::initializer_list<int> mylist;mylist = { 10, 20, 30 };cout << sizeof(mylist) << endl;// 这⾥begin和end返回的值initializer_list对象中存的两个指针// 这两个指针的值跟i的地址跟接近,说明数组存在栈上int i = 0;cout << mylist.begin() << endl;cout << mylist.end() << endl;cout << &i << endl;return 0; }//8 //00EFF4B0 //00EFF4BC //00EFF824
initializer_list类的底层是数组,存储了两个指针分别指向开头和结尾地址,如图,sizeof的输出为8为两个int指针,当我们输出begin和end时,发现与i的地址非常相近,说明他们都是存储在栈上的。
三.右值引用和移动语义
3.1左值和右值
左值和右值都是表示数据的表达式,左值具有持久性,我们可以访问他的地址,一般是一些变量。右值是一些字面值常量,和表达式求值过程中创建的临时对象。
下面举例一些左值和右值的例子
int main()
{// 左值:可以取地址// 以下的p、b、c、*p、s、s[0]就是常⻅的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl;// 右值:不能取地址double x = 1.1, y = 2.2;// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值10;x + y;fmin(x, y);string("11111");//cout << &10 << endl;//cout << &(x+y) << endl;//cout << &(fmin(x, y)) << endl;//cout << &string("11111") << endl;return 0;
}
如上代码所示,所以通常我们区分左右值的办法就是看它是否能取地址。
3.2 左值引用与右值引用
左值引用int &r = x,右值引用int &&r = y。左值引用就是给左值取别名,同理右值引用就是给右值取别名。
左值引用不能直接引用右值,需要添加const来修饰,使左值变为常量 ,而右值也不能直接引用左值,需要添加move(左值)修饰。
int main() {int a;const int& r1 = 10;//若直接为 int& r1 = 10 就会报错int&& r2 = move(a);//若直接为 int &&r2 = a 就会报错return 0; }
当右值变量右值引用右值时这个右值将带有左值变量的属性
int main() {int&& r1 = 10;//r1是右值变量,&&右值引用,10右值后,r1带有左值属性。return 0; }
3.3 左值和右值的参数匹配
在函数调用时,通过调用不同的左值和右值,会相应调用不同的重载函数,这里在STL容器接口中有体现,下面我们来分析一下不同参数调用到的不同接口。
void f(int& x) {std::cout << "左值引⽤重载 f(" << x << ")\n"; } void f(const int& x) {std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n"; } void f(int&& x) {std::cout << "右值引⽤重载 f(" << x << ")\n"; } int main() {int i = 1;const int ci = 2;f(i); // 调⽤ f(int&)f(ci); // 调⽤ f(const int&)f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)f(std::move(i)); // 调⽤ f(int&&)// 右值引⽤变量在⽤于表达式时是左值int&& x = 1;f(x); // 调⽤ f(int& x)f(std::move(x)); // 调⽤ f(int&& x)return 0; }
如上图f函数分别重载构造了左值引用,const 左值引用,以及右值引用。i 作为左值变量相应的调用左值引用;ci是const int类型的变量,相应调用const的左值引用;3为右值调用右值引用;i 为左值在move之后变为右值调用右值引用;x为右值在右值引用右值后它带有左值属性 ,调用左值引用;相应的move(x)变为右值属性调用右值引用。
3.4 右值引用和移动语义的使用场景
3.4.1 移动构造和移动赋值
移动构造和拷贝构造类似,是一种构造函数,但是不同的是他要求参数是右值引用;移动赋值是一个赋值运算符重载,类似赋值函数,但是要求赋值参数需是右值引用。
与拷贝构造和赋值类似,移动构造和移动赋值的目的就是窃取对象资源,实现深拷贝从而提高效率。
namespace wu
{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);}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;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷⻉赋值" <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}~string(){cout << "~string() -- 析构" << endl;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];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){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}
private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;
};
}
这里我们提供了一个自己手动实现的string类对象,这里并不是重点后续就不再附上此代码。我们的重点在于传值是调用了什么构造。
int main()
{wu::string s1("xxxxx");// 拷⻉构造wu::string s2 = s1;// 构造+移动构造,优化后直接构造wu::string s3 = wu::string("yyyyy");// 移动构造wu::string s4 = move(s1);cout << "******************************" << endl;return 0;
}
第一个s1没有什么疑问,直接进行构造;s2拷贝构造s1;第三个先构造出 “yyyy” 为右值,在调用移动构造,最后编译器直接优化为直接构造,第四个s1为左值move(s1)后带有右值属性调用移动构造。
3.4.2 右值引用和移动语义在传参中的提效
在C++11后容器的push和insert新增了右值引用的接口。当实参是一个左值时,调用拷贝构造进行拷贝;当实参是一个右值时,调用移动构造进行拷贝。
int main()
{std::list<wu::string> lt;wu::string s1("111111111111111111111");lt.push_back(s1);cout << "*************************" << endl;lt.push_back(wu::string("22222222222222222222222222222"));cout << "*************************" << endl;lt.push_back("3333333333333333333333333333");cout << "*************************" << endl;lt.push_back(move(s1));cout << "*************************" << endl;return 0;
}//string(char* str) - 构造
//string(const string & s) --拷⻉构造
//* ************************
//string(char* str) - 构造
//string(string && s) --移动构造
//~string() --析构
//* ************************
//string(char* str) - 构造
//string(string && s) --移动构造
//~string() --析构
//* ************************
//string(string && s) --移动构造
//* ************************
//~string() --析构
//~string() --析构
//~string() --析构
//~string() --析构
//~string() --析构
第一个先构造了s1变量,push_back拷贝一份s1调用拷贝构造;第二个先构造“22222”为右值,随后调用移动构造;第三个直接在push_back中调用构造,完成移动构造;最后一个move转为右值调用移动构造。
3.5 引用折叠
通过模板或typedef中的类型构成引用的引用时,此时存在一个引用折叠的规则:右值引用的右值引用折叠成右值引用,其余的都折叠成为左值引用。
int main()
{typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n; // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n; // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&return 0;
}
左值+左值=左值,左值+右值=左值,右值+左值=左值,右值+右值=右值。
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{int n = 0;// 没有折叠->实例化为void f1(int& x)f1<int>(n);f1<int>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&>(n);f1<int&>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&&>(n);f1<int&&>(0); // 报错// 折叠->实例化为void f1(const int& x)f1<const int&>(n);f1<const int&>(0);// 折叠->实例化为void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x)f2<int>(n); // 报错f2<int>(0);// 折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0); // 报错// 折叠->实例化为void f2(int&& x)f2<int&&>(n); // 报错f2<int&&>(0);return 0;
}
n为左值,在f1中可以调用int,int&,int&&(右值+左值=左值),const int&,const int&&。0为右值,在f1中可以调用const int&(const 可以对左值进行常量化),const int&&。
在f2中,n可以调用int& (左值+右值=左值),不可以调用int(若调用则n变为右值)。0可以调用int,int&&(右值+右值=右值)。
四. 可变参数模板
4.1 基本语法原理
C++11支持可变参数模板,也就是说支持可变数量参数的函数和类,被称为参数包。
template <class ...Args>
void Print(Args&&... args)
{cout << sizeof...(args) << endl;
}
int main()
{double x = 2.2;Print(); // 包⾥有0个参数Print(1); // 包⾥有1个参数Print(1, string("xxxxx")); // 包⾥有2个参数Print(1.1, string("xxxxx"), x); // 包⾥有3个参数return 0;
}
若要使用参数包需先进行模板设置,...Args,上面的代码计算了参数包的大小,参数包会根据包内参数数量的不同类型的不同,变成不同的模板类型函数,使函数的使用更加灵活多变。
4.2 包扩展
参数包还支持包扩展,可以将包传给不同的函数递归进行拆包操作。
void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{cout << x << " ";// args是N个参数的参数包// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);
}
int main()
{Print(1, string("xxxxx"), 2.2);return 0;
}
如上图,print有三个参数,被装到包里,把包传给showlist函数,showlist函数进行解包操作,showlist通过调用递归进行解包,当包为空时调用showlist()结束。
template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回或者到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);
}
int main()
{Print(1, string("xxxxx"), 2.2);return 0;
}
如上图,Arguments函数体为空,它的主要作用是将参数转换为参数包,GetArg负责将参数包传来的参数进行解包处理,最后完成Print函数。
4.3 emplace接口
emplace接口均为模板可变参数,功能上兼容push和insert,他可以通过判断左右值调用不同的构造拷贝方式。
#include<list>
// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
int main()
{list<wu::string> lt;// 传左值,跟push_back⼀样,⾛拷⻉构造wu::string s1("111111111111");lt.emplace_back(s1);cout << "*********************************" << endl;// 右值,跟push_back⼀样,⾛移动构造lt.emplace_back(move(s1));cout << "*********************************" << endl;// 直接把构造string参数包往下传,直接⽤string参数包构造string// 这⾥达到的效果是push_back做不到的lt.emplace_back("111111111111");cout << "*********************************" << endl;list<pair<wu::string, int>> lt1;// 跟push_back⼀样// 构造pair + 拷⻉/移动构造pair到list的节点中data上pair<wu::string, int> kv("苹果", 1);lt1.emplace_back(kv);cout << "*********************************" << endl;// 跟push_back⼀样lt1.emplace_back(move(kv));cout << "*********************************" << endl;// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair// 这⾥达到的效果是push_back做不到的lt1.emplace_back("苹果", 1);cout << "*********************************" << endl;return 0;
}
第一个先构造出s1,再将s1拷贝给lt;第二个判断出为右值直接走移动构造;第三个直接进行构造;第四个先构造出kv,再调用拷贝构造;第五个括号内为右值直接调用移动构造;最后一个直接调用构造。
五. 新的类功能
5.1 默认的移动构造和移动赋值
C++有默认生成的6个成员函数,构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载。当成员列表中,没有实现析构函数/拷贝构造函数/拷贝赋值函数时(三个函数都不存在),成员列表就会默认生成移动构造函数和移动赋值重载函数。
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& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;}return *this;}*//*~Person()
{}*/
private:wu::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}//string(char* str) - 构造
//string(const string & s) --拷⻉构造
//string(string && s) --移动构造
//string(char* str) - 构造
//string & operator=(string && s) --移动赋值
//~string() --析构
//~string() --析构
//~string() --析构
//~string() --析构
如上图,当我们屏蔽了析构/拷贝构造/拷贝赋值函数时,当值为右值时,会默认调用移动构造和移动赋值。
六. lambda表达式
6.1 lambda表达式语法
lambda表达式我们通常用auto的类型来接收,如下面的式子。
int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
auto func1 = []()
{x++;
};
int main()
{// 只能⽤当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;auto func1 = [a, &b]{// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};cout << func1() << endl;// 隐式值捕捉// ⽤了哪些变量就捕捉哪些变量auto func2 = [=]{int ret = a + b + c;return ret;};cout << func2() << endl;// 隐式引⽤捕捉// ⽤了哪些变量就捕捉哪些变量auto func3 = [&]{a++;c++;d++;};func3();cout << a << " " << b << " " << c << " " << d << endl;// 混合捕捉1auto func4 = [&, a, b]{//a++;//b++;c++;d++;return a + b + c + d;};func4();cout << a << " " << b << " " << c << " " << d << endl;// 混合捕捉1auto func5 = [=, &a, &b]{a++;b++;/*c++;d++;*/return a + b + c + d;};func5();cout << a << " " << b << " " << c << " " << d << endl;// 局部的静态和全局变量不能捕捉,也不需要捕捉static int m = 0;auto func6 = []{int ret = x + m;return ret;};// 传值捕捉本质是⼀种拷⻉,并且被const修饰了// mutable相当于去掉const属性,可以修改了// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉auto func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};cout << func7() << endl;cout << a << " " << b << " " << c << " " << d << endl;return 0;
}
6.2 lambda的应用
lambda表达式相比于仿函数需要定义一个类的方法来说,会更加的简便好用。
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 } };// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中// 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());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._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;});return 0;
}
如上图,我们需要比较成员之间的大小,通过sort加上compar函数进行比较。若我们不使用lambda表达式,则需要在类外定义一个函数实现功能比较,而lambda表达式则可以直接在括号内进行函数表达式书写。相对于仿函数,lambda表达式更加的明显,可以更清楚的知道函数需要实现的功能。
6.3 lambda的原理
lambda表达式的底层是通过仿函数实现的,在底层编译器会给lambda表达式命名。
七. 包装器
7.1 function
首先调用function包装器前,需要添加头文件<functional>,function可以调用函数,调用类,调用lambda表达式,调用类中的函数。调用了包装器相当于包装了一个函数功能,可以方便我们进行调用。
#include<functional>
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};
class Plus
{
public:Plus(int n = 10):_n(n){}static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return (a + b) * _n;}
private:int _n;
};
int main()
{// 包装各种可调⽤对象function<int(int, int)> f1 = f;function<int(int, int)> f2 = Functor();function<int(int, int)> f3 = [](int a, int b) {return a + b; };cout << f1(1, 1) << endl;cout << f2(1, 1) << endl;cout << f3(1, 1) << endl;// 包装静态成员函数// 成员函数要指定类域并且前⾯加&才能获取地址function<int(int, int)> f4 = &Plus::plusi;cout << f4(1, 1) << endl;// 包装普通成员函数// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以function<double(Plus*, double, double)> f5 = &Plus::plusd;Plus pd;cout << f5(&pd, 1.1, 1.1) << endl;function<double(Plus, double, double)> f6 = &Plus::plusd;cout << f6(pd, 1.1, 1.1) << endl;cout << f6(pd, 1.1, 1.1) << endl;function<double(Plus&&, double, double)> f7 = &Plus::plusd;cout << f7(move(pd), 1.1, 1.1) << endl;cout << f7(Plus(), 1.1, 1.1) << endl;return 0;
}
function<int(int, int)> f1 = f中 <int>为返回类型,()括号内是调用参数的类型,若包装的为静态成员,取类型时不需要添加取地址符号,若包装的为类内的成员函数,则()内首位需要包含类名指针。类可以通过左值和右值区分模板。
f1调用的是f函数,f2调用的是Functor仿函数,f3调用的是lambda表达式,f4调用的为类中的静态函数(成员函数在指定类域需要加&),f5调用为类中的函数,f6调用plus类,f7调用右值模板。