C++进阶:C++11(2)
目录
- 1. 新的类功能
- 2. C++11新增的类相关语法特性与关键字
- 3. 可变参数模板
- 3.1 可变模板参数定义
- 3.2 可变模板参数的解析
- 3.3 可变参数模板与emplace类型接口
- 3.4 可变参数模板传参的特殊使用
- 3.5 自定义容器emplace接口的简单模拟实现
- 4. lambda表达式
- 4.1 lambda表达式的存在意义
- 4.2 lambda表达式的格式与定义
- 4.3 lambda表达式的类型
- 5. function包装器
- 6. bind包装器
1. 新的类功能
C++11中为类新添加了两个默认成员函数,分别为移动构造 与 移动赋值,这两个新增默认成员函数的功能已经在前文右值引用的移动语义阐述。
默认生成的拷贝构造,对内置类型会进行浅拷贝,对自定义类型会调用其的拷贝构造。默认生成的移动构造,对内置类型会进行浅拷贝,对自定义类型会调用其的移动构造,当自定义类型没有移动构造时,会调用其的拷贝构造。默认生成的移动赋值,对内置类型进行浅拷贝,对自定义类型调用其移动赋值,当自定义类型没有移动赋值时,就调用其的赋值重载。
- 各个重要默认成员函数的自动生成条件
构造函数: 当拷贝构造存在时,就不会生成默认的构造,拷贝构造也属于构造的范畴。
拷贝构造: 没写就会自动生成,当移动构造显示定义时,会干扰默认生成的拷贝构造。
赋值重载: 没写就会自动生成。
移动构造: 当类的析构、拷贝构造、赋值重载都没有被显示定义时,会自动生成移动构造。
移动赋值: 当类的析构、拷贝构造、赋值重载都没有被显示定义时,会自动生成移动赋值。
移动构造与移动赋值的自动生成条件与析构、拷贝构造、赋值重载绑定,这是因为当一个类需要去显示写出上面的三个成员函数时,就证明其大概率就需要进行深拷贝操作。而一个类存在深拷贝操作,其的移动构造与移动赋值就有显示定义的价值和必要,这也是C++语法特性上的保护性设置。
2. C++11新增的类相关语法特性与关键字
- 类的成员变量初始化:
C++11增加了在定义处类的成员变量初始化,通过赋予缺省值的方式达成这一操作,缺省值会在构造函数(自定义/默认生成)的初始化列表处被自动调用。
class A
{
public://默认生成构造...
private:int* _ptr = nullptr;
};
- 默认成员函数的强制生成
在编写代码的过程中,有时会因为某些原因导致破坏了类的默认成员函数自动生成的条件。此种情况,如何能让其强制默认生成呢,C++11中新增了关键字defualt
的使用方式来让默认成员函数强制生成。
class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p) = default; //拷贝构造Person(Person&& p) = default; //移动构造Person& operator=(Person&& p) = default;//移动赋值//显示写析构~Person(){}private:string _name = "张三";int _age;
};
- 禁止生成默认成员函数:
C++中总会有一些类不期望自己被拷贝,诸如C++库中的iostream
,线程,锁。他们为了防止自己被拷贝,就要对自己的拷贝构造做一些处理操作,从而来达到拷贝构造不生成使得别人无法拷贝。在C++98及之前,会采用只声明不定义与再将拷贝构造私有化的操作来实现。若想通过只声明不定义的方式,是无法达到进行拷贝的效果的,若只是对拷贝构造进行声明编译器会报出链接错误,而且也无法杜绝他人定义拷贝构造的可能。
class A
{
public:A() = default;
private:// C++98// 只声明不实现// 放到私有A(const A& aa);int a = 0;
};
C++11之后就新增了关键字delete
的使用方式,来直接达到禁止生成拷贝构造的效果,既进行默认拷贝构造的生成,又禁止自定义拷贝构造。
class A
{
public:A(const A& aa) = delete;private:int a = 0;
};
- 关键字final与override
使用关键字final
修饰类,代表这个类就是最终类,其不能被继承。使用关键字override
修饰虚函数,会在子类继承时要求强制重写这一被修饰的虚函数,否则会进行报错。
//final关键字
class A final
{};//override关键字
class Person()
{
public:virtual void func1(){//...}
};class Student : public Person
{
public:virtual void func1() override{//...}
}
3. 可变参数模板
C语言中有着可变参数的概念,其底层是通过动态数组/指针的方式来传递变量实现。而在C++11中则添加了可变参数模板的概念,及类模板的参数与函数模板的参数数量都是可变的。相较于C++98之前的固定模板参数,这一新增语法点无疑是巨大的进步。可变模板参数较为抽象,理解起来很困难,这里我们只进行对其简单特性的学习,达到可以简单应用的效果。
3.1 可变模板参数定义
template<class ...Args>
void ShowList(Args... args)
{cout << sizeof...(args) << endl;//计算参数包中模板参数个数
}
声明方式template<class ...Args>
,函数模板参数处Args... args
表示声明了一个模板参数包,这个模板参数包内可能包含0到N任意个模板参数。
3.2 可变模板参数的解析
当想要获取模板参数包内的每一个参数时,我们不能采用类似args[i]
数组下标索引的方式,其一语法不支持此种方法,其二数组中各元素的获取是在程序运行起来后,在程序执行的过程中执行的,而模板需要在程序编译时就实例化确定出来。
方法1:
编译器会在编译时就将可变参数模板示例化出来,可是编译器实例化出的函数我们编写代码时是不可见。所以,我们要如何才能在编写代码时自己将模板参数包中的各个参数解析出来呢。有一种编译时递归解析的方式可以解决上面的问题,具体如下:
//编译时递归返回条件,函数定义时在函数模板上面,程序向上检查
void _ShowList()
{cout << endl;
}//依次拿到每个参数和值
template<class T, class ...Args>
void _ShowList(const T& val, Args... args)
{cout << val << ' ';_ShowList(args...);
}template<class ...Args>
void ShowList(Args... args)
{_ShowList(args...);//传递模板参数的方式
}
参数包在递归推演时,会一层层实例化,参数会在多个函数中一一被取出,具体过程如下:
// 实例化以后,推演生成的过程
void ShowList(int val1, char ch, std::string s)
{_ShowList(val1, ch, s);
}void _ShowList(const int& val, char ch, std::string s)
{cout << val << " ";_ShowList(ch, s);
}void _ShowList(const char& val, std::string s)
{cout << val << " ";_ShowList(s);
}void _ShowList(const std::string& val)
{cout << val << " ";_ShowList();
}
模板参数包中参数过多,在编译时程序可能会崩溃。编译时的速度不会影响运行时的速度,参数模板的编译时递归解析的速度不会因为程序时release或debug版本而受影响。
template<size_t N>
void func()
{cout << N << endl;func<N - 1>();
}//特化模板,要定义在正常模板下面
template<>
void func<1>()
{cout << 1 << endl;
}
方法2:
除开上述的编译时递归解析方式外,还有另一种解析可变模板参数包的方法,具体如下:
template <class T>
int PrintArg(T t)
{cout << t << " ";return 0;
}//展开函数
template <class ...Args>
void ShowList(Args... args)
{//有几个参数就调用几次PrintArg函数,此函数的返回值用来初始化数组arrint arr[] = { PrintArg(args)... };cout << endl;
}
3.3 可变参数模板与emplace类型接口
可变参数模板在实践中往往被写成万能引用配合完成转发来使用,这样可变参数模板既可以匹配右值又可以匹配左值。C++11为各种容器新增了,emplace
系列接口对应其他类型的容器插入接口,如push_back、insert等。那么,既然其也是用来给容器插入数据的,在用法与效果上与旧的接口有什么不同呢,以emplace_back
接口为例。
templace<class ...Args>
void emplace_back(Args&&.. args);
emplace_back
接口为在容器尾部插入,其参数类型为可变参数模板并使用了万能引用。emplace
系列接口也是一次插入一个元素,此点与旧的插入接口没有不同。容器list配合自定义string验证观察,自定义string插入时构造、拷贝构造、移动构造操作可见。
std::list<zyc::string> lt1;zyc::string s1("xxxx");
lt1.push_back(s1);//插入左值
lt1.push_back(move(s1));//插入右值
cout << "=============================================" << endl;zyc::string s2("xxxx");
lt1.emplace_back(s2);//插入左值
lt1.emplace_back(move(s2));//插入右值
cout << "=============================================" << endl;lt1.push_back("xxxx");//插入右值
lt1.emplace_back("xxxx");//传构造函数的参数
cout << "=============================================" << endl;std::list<pair<zyc::string, zyc::string>> lt2;
pair<zyc::string, zyc::string> kv1("xxxx", "yyyy");
lt2.push_back(kv1);//插入左值
lt2.push_back(move(kv1));//插入右值
cout << "=============================================" << endl;pair<zyc::string, zyc::string> kv2("xxxx", "yyyy");
lt2.emplace_back(kv2);//插入左值
lt2.emplace_back(move(kv2));//插入右值
cout << "=============================================" << endl;lt2.emplace_back("xxxx", "yyyy");//传构造函数的参数
cout << "=============================================" << endl;
- emplace接口的功能:
当插入普通左值与右值时,接口push_back
与emplace_back
的效果一致。插入左值进行拷贝构造,插入右值进行构造加移动构造。可当传入插入对象的构造函数参数(单参数构造函数)时,push_back
接口与emplace_back
接口两者就产生了差别。前者会将其视作右值,进行构造与移动构造,而后者则是直接使用参数去构造出一个对象,产生的效果就是直接调用了构造函数。emplace_back
不仅可以传入单参数构造函数的参数,多参数的构造同样可以。 - emplace接口的优势与意义:
对于深拷贝的类,emplace_back
只是减少了一次移动构造,其产生的价值没有非常的大。但对于浅拷贝的类来说,浅拷贝的类不会去定义移动构造,所以在进行插入时会直接进行拷贝构造。那么,整个插入过程就需要进行一次构造加一次拷贝构造,此时,emplace_back
就相当于减少了一次拷贝构造。对于浅拷贝的类来说emplace_back
效率高于push_back
。大多数情况下两种接口的效率差距不大,但emplace
系列接口在某些情况具有优势,在日常使用中建议使用。
3.4 可变参数模板传参的特殊使用
当类的构造函数是全缺省的情况时,函数的可变参数模板传参时支持0~N(N:构造函数参数)任意参数个数,具体如下:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)//全缺省:_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}
private:int _year = 1;//定义时缺省,当构造是无参时,成员变量会在初始化列表被初始化int _month = 1;int _day = 1;
};template<class ...Args>
void CreateDate(Args&&... args)
{Date d(args...);
}int main()
{CreateDate(1, 1, 1);//必须构造函数的参数全缺省CreateDate(2,2);CreateDate(3);CreateDate();return 0;
}
3.5 自定义容器emplace接口的简单模拟实现
自定义list中对emplace_back
接口服用了emplace
接口,emplace
接口到调用成员变量构造前都要确保将参数包传递下去。可变模板参数包通过配合万能引用与完美转发一直传递到调用相应的成员变量构造处,而后成员变量会使用参数包直接进行构造。
自定义list与Date类配合验证push_back
与emplace_back
接口的实现效果。当list存储节点类型是深拷贝,存在移动构造时,可变模板参数包就会做出相应的匹配。当插入元素是左值时调用拷贝构造,当插入元素是右值时调用移动构造,当插入的是节点构造的参数时就直接调用构造。
emplace
系列接口不支持使用initializer_list
作为参数,因为emplace
接口的参数为模板参数,会将参数类型直接就实例化为initializer_list
,无法调用相应类型的initializer_list
构造进行转换。
4. lambda表达式
4.1 lambda表达式的存在意义
在一些日常的应用场景中,常常会遇到需要将函数作为参数传递的情况。在C语言是通过传递函数指针来实现,但C语言中的函数指针类型复杂,使用起来比较麻烦。在C++中又新增了一种仿函数的方式,仿函数不是函数而是一个类,其重写自己的operator()
函数,让其可以类似函数的方式被调用(类名(参数, …)),达到目标效果。仿函数相较于C语言中的函数指针就显得更简洁明了,便于使用。此种将函数、函数对象(仿函数)作为参数传递给另一个函数的方式被称为回调函数。
仿函数虽相较于函数指针在使用上有明显的进步,但仍有局限。当回调函数内部的逻辑需要改变时,就需要重写一个全新不同类型的仿函数。在多人合作编码的情况下,若他人注释清晰命名规范还不会出现什么大问题,但若反之仿函数的使用就着实会有不少麻烦,以下面一个场景为例。
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};//若命名Compare1
struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};//若命名Compare2
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());//根据价格排降序//lambda表达式实现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;});
}
C++11中新增了一个语法lambda
表达式就很好的解决了这一问题,lambda表达式的定义其内部实现逻辑在调用处是可见的。
4.2 lambda表达式的格式与定义
//lambda表达式格式
[]()mutable->returntype{statement};
lambda
表达式是一个局部的匿名函数对象,其由[]
捕捉列表、()
参数列表、mutable
关键字、->
箭头、returntype
函数返回值类型、statement
函数体这几部分构成。这几部分在表达式中的作用功能具体如下:
- 捕捉列表[]: 用来捕捉局部域内的变量,捕捉的方式有多种,传值捕捉而来的变量是一份拷贝,不允许被修改。
lambda
表达式的返回值是一个函数对象,捕获而来的变量会变成其的成员变量(类似仿函数的成员变量),定义时此部分不允许省略。
int main()
{int x = 0, y = 0;//传值捕捉,不可修改auto func1 = [x]() {};//传引用捕捉,可修改auto func2 = [&x]() { x = 3; };//传值捕捉当前域所有对象auto func3 = [=]() {};//传引用捕捉当前域所有对象auto func4 = [&]() {};//混合捕捉,整个作用域的变量传引用捕捉、变量x传值捕捉auto func5 = [&, x] {};return 0;
}
-
()参数列表: 与普通函数的参数一样,用来传递函数内部需要用到的函数参数。此部分定义时不可省略。
-
mutable关键字: 用此关键字修饰
lambda
表达式时,捕获列表传值捕获的变量就变得可以被修改,但被修改的只是被拷贝的变量,不影响外界变量的值。此部分可以被省略。
-
->箭头与函数返回值类型: 当不写函数返回值类型时,
->
就不用写,当写出了函数返回值类型时,就需要同时也写出->
。函数返回值类型一般不写,其可以根据函数内部的返回值自行推导而得。 -
{}函数体: 函数的具体实现细节,不可省略。
4.3 lambda表达式的类型
- lambda表达式的类型
lambda表达式是一个局部的匿名函数对象,既然其是一个的对象,那么它的类型究竟是什么呢,我们来使用关键字typeid
来观察一下。
lambda表达式的类型名称是由lambda + 字符串
组成的,这段字符串被称为uuid
,是一段根据算法生成的乱序字符,概率上其重复的可能极低(避免了其与其他lambda表达式重复的风险)。lambda表达式的原理与范围for类型,都是新瓶装旧酒,其底层实际是转为了仿函数,但在语法层来说确实用起来更便捷。 - lambda表达式的decltype用法
int main()
{auto DateLess = [](Date* d1, Date* d2) {return *d1 < *d2;};//将lambda表达式作为compare仿函数使用时,需要传一个此lambda表达式的参数priority_queue<Date*, vector<Date*>, decltype(DateLess)> p(DateLess);return 0;
}
当遇到需要传递类型的情况时,可以使用decltype
关键字获取lambda表达式并传递,但lambda表达式禁用默认构造函数,而需要传递类型的场景,往往都是根据传递的模板参数类型构造出一个函数对象进行使用,所以只传递lambda表达式的类型是不行的。需要在参数中传递一个lambda表达式对象,让其使用时进行拷贝构造。
5. function包装器
- function包装器的意义
function
包装器是对常用回调函数(可调用对象lambda表达式,函数指针,仿函数)的再封装。只要三种回调函数的参数,返回值类型相同,经过封装之后它们就都被视作是同一种类型。
// 函数模板
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}double f(double i)
{return i / 2;
}struct Functor
{double operator()(double d){return d / 3;}
};
- function包装器的使用方式
template<class Ret, class... Args>
class function<Ret(Args...)>
Ret为函数的返回值类型,Args…为可变模板参数包,其中包装着函数的参数。function包装器是一个类模板。
- function包装器的应用场景
funciont包装器在Linux操作系统的设计中就有广泛的应用,具体方式为使用map将指令与相应的函数对象映射起来,当调用对应的指令时就去运行与之映射的函数对象。实现逻辑参考逆波兰表达式求值的新解决方式,题目链接
class Solution {
public:int evalRPN(vector<string>& tokens) {map<string, function<int(int, int)>> oper({{"+", [](int left, int right){ return left + right; }},{"-", [](int left, int right){ return left - right; }},{"*", [](int left, int right){ return left * right; }},{"/", [](int left, int right){ return left / right; }}});stack<string> st;for(const auto& e : tokens){if(oper.count(e))//遇到操作符{int right = stoi(st.top());st.pop();int left = stoi(st.top());st.pop();string ret = to_string(oper[e](left, right));st.push(ret);}else{st.push(e);}}return stoi(st.top());}
};
- function包装器与类的成员函数
int f(int a, int b)
{return a + b;
}class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};int main()
{// 普通函数function<int(int, int)> fc1 = f;cout << fc1(1, 1) << endl;// 静态成员函数function<int(int, int)> fc2 = &Plus::plusi;cout << fc2(1, 1) << endl;// 非静态成员函数function<double(Plus, double, double)> fc3 = &Plus::plusd;cout << fc3(Plus(), 1, 1) << endl;return 0;
}
相较于普通的函数,对于类的静态成员函数使用function包装器时,用于赋值的函数需要在函数名前加&
,使用方式为&类域::函数名
。
非静态的成员函数,其有一个隐藏的默认参数,即指向自己的this指针,在使用function包装器包装时,需要将this指针的类型也在function包装器中声明出来。定义方式为function<返回值(成员函数类的类型, 其他参数...)>
。在调用function包装过后的成员函数时,传参中需要传入成员变量所在类的一个对象。
6. bind包装器
- bind包装器的使用方法
template<class Fn, class ...Args>
bind(Fn&& fn, Args&&... args);
fn
为包装的函数,args
为包装函数的参数。placeholder::_1
代指包装函数的第一个参数,后续参数格式类似placeholder::_N
。
int Sub(int a, int b)
{return a - b;
}int main()
{int x = 10, y = 20;auto f1 = bind(Sub, placeholders::_1, placeholders::_2);cout << f1(x, y) << endl;return 0;
}
- bind包装器的特殊用法与意义
1. 调整函数的参数顺序
bind包装器可以通过定义时,对placeholders::_N
的顺序调整来达到传参时对函数参数的顺序调整。
2. 调整参数个数
在包装函数时,将某些参数的值写成定值,绑死指定的参数,达到调整参数个数的效果。