1.基本信息
C++11标准为C++编程语言的第三个官方标准,正式名叫ISO/IEC 14882:2011 - Information technology – Programming languages – C++ 。在正式标准发布前,原名C++0x。它将取代C++标准第二版ISO/IEC 14882:2003 - Programming languages – C++11 成为C++语言新标准。相比于 C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个 重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本篇博客主要讲解实际中比较实用的语法。
2.统一的初始化列表
2.1 {}的初始化
C++11当中扩大了花括号内的列表(初始化列表)的适用范围,使其能够适用于所有内置类型和用户自定义类型的初始化,并且使用时可以添加=号,也可以不添加。具体的使用案例如下:`
struct Point
{
int _x;
int _y;
};
class Date
{
public:
//explicit Date(int year, int month, int day)
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int x1 = 1;
int x2 = { 2 };
// int x4(1); - int的默认构造
// 可以省略赋值符号
int x3{ 3 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };// 也可以初始化自定义类型
// c++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2 = { 2022, 1, 2 };
Date d3 { 2022, 1, 3 };
return 0;
}
2.2 std::initializer_list
initializer_list文档描述
那假如我们自己模拟实现的vector可以用列表初始化么?
//假设我们自己模拟实现的vector
yrj::vector<int> v1 = { 1,2,3,4,5 };// 报错
原因就在于类不匹配,我们可以看看用列表初始化构建的常量数组是什么类?
auto i1 = { 10,20,30,1,1,2,2,2,2,2,2,1,1,1,1,1,1,1,1,2,1,1,2 };
auto i2 = { 10,20,30,40 };
cout << typeid(i1).name() << endl;
cout << typeid(i2).name() << endl;
// i1,i2的类型是initializer_list
// 给一个用列表初始化的常量数组,是用initializer_list这个类型去接收这个数组
所以我们可以用initializer_list去构造我们模拟实现的vector,需在vector类中添加一个初始化构造函数即可
//initializer_list<int>::iterator it1 = i1.begin();
//initializer_list<int>::iterator it2 = i2.end();
//(*it1)++;// 指向的内容在常量区,不能修改
vector(initializer_list<T> il)
{
for (auto& e : il)
push_back(e);
}
C++11容器都实现了带有initializer_list类型参数的构造函数:
Date d1(2023,5,20);
Date d2(2023,5,21);
// initializer_list<Date>
vector<Date> vd1 = {d1, d2};
vector<Date> vd2 = { Date(2023,5,20), Date(2023,5,21) };
vector<Date> vd3 = { {2023,5,20}, {2023,5,20} };
// initializer_list<map>
map<string, string> dict = { {"sort", "排序"},{"string", "字符串"},{"Date", "日期"} };
// initializer_list<pair>
pair<string, string> kv1 = { "Date", "日期" };
pair<string, string> kv2 { "Date", "日期" };
std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
3.声明
3.1 auto
auto是C++11当中用于在变量声明时自动推导变量的类型,所以auto类型的变量在声明时必须显示的初始化,可以让编译器推断出变量的类型。
auto x = 10; // x的类型将被推导为int
auto y = 3.14; // y的类型将被推导为double
auto z = "hello"; // z的类型将被推导为const char*
不过值得注意的内容是:auto关键字的使用是在编译时进行类型推导,而不是在运行时进行动态类型推断。因此,一旦变量的类型被推导出来,它将保持不变,不能再改变为其他类型。
我们也可以将auto与容器当中的迭代器一起使用,便于声明迭代器变量,如下述代码:
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
// 使用迭代器访问元素
std::cout << *it << " ";
3.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
int main()
{
const int x = 1;
double y = 2.2;
cout << typeid(x * y).name() << endl;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是const int*
cout << typeid(ret).name() << endl;
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
// vector存储的类型跟x*y表达式返回值类型一致
// decltype推导表达式类型,用这个类型实例化模板参数或者定义对象
vector<decltype(x* y)> v;
return 0;
}
4.可变参数模板
4.1 可变参数模板的基础原理
C++的可变参数模板是怎么做到不需要告诉参数个数,也不需要告诉参数类型的呢?这仰仗于C++以下的功能:
- 函数重载,依靠参数的pattern去匹配对应的函数;
- 函数模板,依靠调用时传递的参数自动推导出模板参数的类型;
- 类模板,基于偏特化来选择不同的实现;
4.2 基础语法和例子说明
可变参数模板的关键字沿用了C语言的ellipsis(…),并且在3种地方进行了使用:
void syszuxPrint(){std::cout<<std::endl;}
template<typename T, typename... Ts>
void syszuxPrint(T arg1, Ts... arg_left){
std::cout<<arg1<<", ";
syszuxPrint(arg_left...);
}
int main(int argc, char** argv)
{
syszuxPrint(719,7030,"civilnet");
}
哇,看起来比C的实现要简单漂亮多了(还没提到其它好处!)Gemfield先做个简单解释:
- typename… Ts,这是template parameter pack(模板参数包),表明这里有多种type;
- Ts… arg_left,这是function parameter pack(函数参数包),表明这里有多个参数;
- arg_left…,这是pack expansion(包展开),将参数名字展开为逗号分割的参数列表;
在上述代码中,main函数里调用了syszuxPrint(719,7030,“civilnet”);会导致syszuxPrint函数模板首先展开为:
void syszuxPrint(int, int, const char*)
在打印第1个参数719后,syszuxPrint递归调用了自己,传递的参数为arg_left…,该参数会展开为【7030,“civilnet”】,syszuxPrint第2次进行了展开:
void syszuxPrint(int, const char*)
在打印第1个参数7030后,syszuxPrint递归调用了自己,传递的参数为arg_left…,该参数会展开为【“civilnet”】,syszuxPrint第3次进行了展开:
void syszuxPrint(){std::cout<<std::endl;}
上面这个函数是函数模板syszuxPrint的“非模板重载”版本,于是展开停止,直接调用这个“非模板重载”版本,递归停止。
4.3 换个花样重载
上面的例子里有个syszuxPrint的“非模板重载”版本,目的就是为了递归能够最终退出,基于这个原理,我们也可以按照如下方式重新实现:`
template<typename T>
void syszuxPrint(T arg){std::cout<<arg<<", "<<std::endl;}
template<typename T, typename... Ts>
void syszuxPrint(T arg1, Ts... arg_left){
std::cout<<arg1<<", ";
syszuxPrint(arg_left...);
}
int main(int argc, char** argv)
{
syszuxPrint(719,7030,"civilnet");
}
这里不再有syszuxPrint的“非模板重载”版本了,而是两个函数模板,区别是模板参数的区别:当两个参数模板都适用某种情况时,优先使用没有“template parameter pack”的版本。
4.4 sizeof…操作符
C++11引入了sizeof…操作符,可以得到可变参数的个数(注意sizeof…的参数只能是parameter pack,不能是其它类型的参数啊),如下所示:
std::cout<<"DEBUG: "<<sizeof...(Ts)<<" | "<<sizeof...(arg_left)<<std::endl;
4.5 逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式
实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
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;
}
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列
表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
参考出处:可变参数模板
5. push_back 和 emplace_back 的区别
在 C++11 之后,vector 容器中添加了新的插入函数:emplace_back() ,和 push_back() 一样的是都是在容器末尾添加一个新的元素进去,不同的是 emplace_back() 在效率上相比较于 push_back() 有了一定的提升。
那么这两个函数有什么区别呢?-- 事实上,emplace_back() 和 push_back() 在底层实现机制上是不同的。
- push_back()在向容器尾部添加元素时,会先创建这个元素,然后再将这个元素拷贝或移动到容器中(若是拷贝的话,还需要在结束后销毁之前创建的这个元素);
- emplace_back() 则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
因此,在 emplace_back() 函数中,是支持直接将构造函数所需的参数传递过去,然后构建一个新的对象出来,然后填充到容器尾部的。
从上图可以看出,push_back 比 emplace_back 多了一步移动构造。若将移动构造函数注释掉,则结果为:这时 push_back 比 emplace_back 多了一步拷贝构造。由此可以看出,push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。
6.包装器
6.1 function包装器的概念
function是一种函数包装器,也叫做适配器。它可以对可调用对象进行包装,C++中的function本质就是一个类模板。function类模板的原型如下:
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
- Ret:被包装的可调用对象的返回值类型。
- Args…:被包装的可调用对象的形参类型。
包装示例:
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator()(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()
{
//1、包装函数指针(函数名)
function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
//2、包装仿函数(函数对象)
function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
//3、包装lambda表达式
function<int(int, int)> func3 = [](int a, int b){return a + b; };
cout << func3(1, 2) << endl;
//4、类的静态成员函数
//function<int(int, int)> func4 = Plus::plusi;
function<int(int, int)> func4 = &Plus::plusi; //&可省略
cout << func4(1, 2) << endl;
//5、类的非静态成员函数
function<double(Plus, double, double)> func5 = &Plus::plusd; //&不可省略
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
注意事项:
- 包装时指明返回值类型和各形参类型,然后将可调用对象赋值给function包装器即可,包装后function对象就可以像普通函数一样使用了。
- 取静态成员函数的地址可以不用取地址运算符“&”,但取非静态成员函数的地址必须使用取地址运算符“&”。
- 包装非静态的成员函数时需要注意,非静态成员函数的第一个参数是隐藏this指针,因此在包装时需要指明第一个形参的类型为类的类型。
6.1.1 function包装器统一类型
对于以下函数模板useF:
- 传入该函数模板的第一个参数可以是任意的可调用对象,比如函数指针、仿函数、lambda表达式等。
- useF中定义了静态变量count,并在每次调用时将count的值和地址进行了打印,可判断多次调用时调用的是否是同一个useF函数。
代码如下:
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;
}
};
int main()
{
//函数指针
cout << useF(f, 11.11) << endl;
//仿函数
cout << useF(Functor(), 11.11) << endl;
//lambda表达式
cout << useF([](double d)->double{return d / 4; }, 11.11) << endl;
return 0;
}
由于函数指针、仿函数、lambda表达式是不同的类型,因此useF函数会被实例化出三份,三次调用useF函数所打印count的地址也是不同的。
- 但实际这里根本没有必要实例化出三份useF函数,因为三次调用useF函数时传入的可调用对象虽然是不同类型的,但这三个可调用对象的返回值和形参类型都是相同的。
- 这时就可以用包装器分别对着三个可调用对象进行包装,然后再用这三个包装后的可调用对象来调用useF函数,这时就只会实例化出一份useF函数。
- 根本原因就是因为包装后,这三个可调用对象都是相同的function类型,因此最终只会实例化出一份useF函数,该函数的第一个模板参数的类型就是function类型的。
int main()
{
//函数名
function<double(double)> func1 = func;
cout << useF(func1, 11.11) << endl;
//函数对象
function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
//lambda表达式
function<double(double)> func3 = [](double d)->double{return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
这时三次调用useF函数所打印count的地址就是相同的,并且count在三次调用后会被累加到3,表示这一个useF函数被调用了三次。
6.1.2 function包装器简化代码
求解逆波兰表达式的步骤如下:
- 定义一个栈,依次遍历所给字符串。
- 如果遍历到的字符串是数字则直接入栈。
- 如果遍历到的字符串是加减乘除运算符,则从栈定抛出两个数字进行对应的运算,并将运算后得到的结果压入栈中。
- 所给字符串遍历完毕后,栈顶的数字就是逆波兰表达式的计算结果
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (const auto& str : tokens)
{
int left, right;
if (str == "+" || str == "-" || str == "*" || str == "/")
{
right = st.top();
st.pop();
left = st.top();
st.pop();
switch (str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
default:
break;
}
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
在上述代码中,我们通过switch语句来判断本次需要进行哪种运算,如果运算类型增加了,比如增加了求余、幂、对数等运算,那么就需要在switch语句的后面中继续增加case语句。
这种情况可以用包装器来简化代码。
- 建立各个运算符与其对应需要执行的函数之间的映射关系,当需要执行某一运算时就可以直接通过运算符找到对应的函数进行执行。
- 当运算类型增加时,就只需要建立新增运算符与其对应函数之间的映射关系即可。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
unordered_map<string, function<int(int, int)>> opMap = {
{ "+", [](int a, int b){return a + b; } },
{ "-", [](int a, int b){return a - b; } },
{ "*", [](int a, int b){return a * b; } },
{ "/", [](int a, int b){return a / b; } }
};
for (const auto& str : tokens)
{
int left, right;
if (str == "+" || str == "-" || str == "*" || str == "/")
{
right = st.top();
st.pop();
left = st.top();
st.pop();
st.push(opMap[str](left, right));
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
其实我们也可以这样理解:**unordered_map的参数function<int(int, int)>其实是一个类型(包装成了一个类型),一个个的运算符lambda表达式也就是不同的实参。**需要注意的是,这里建立的是运算符与function类型之间的映射关系,因此无论是函数指针、仿函数还是lambda表达式都可以在包装后与对应的运算符进行绑定。
6.1.3 function包装器的意义
- 将可调用对象的类型进行统一,便于我们对其进行统一化管理。
- 包装后明确了可调用对象的返回值和形参类型,更加方便使用者使用。
6.2 bind包装器
bind的本质是一个函数模板,它就像一个函数包装器(适配器),可以接收一个可调用对象的,从而生成一个新的可调用对象来"适应"原对象的参数列表。
bind原型如下:
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
注意:
- fn 指的是需要包装的对象.
- args…对应的是给定fn函数中的参数.
调用bind的一般形式如下:
cpp auto newCallable = bind(callable,arg_list);
- callable:需要包装的对象…
- newCallable: 生成的一个新的可调用对象.
- arg_list:
arg_list是一个逗号分隔的参数列表,对应给定的callable的参数.当我们调用newCallable时,newCallable会调用callable,并将arg_list参数列表传给callable中.
6.2.1 bind调整函数形参传参顺序
我们使用bind对Mul函数绑定来调整形参传参顺序:
int Mul(int a, int b, int rate)
{
return (a - b) * rate;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
int x = 3, y = 2, z = 1;
cout << Mul(x, y, z) << endl;
cout << "-----------------------------------------------\n" << endl;
auto funcMul = bind(Mul, _3, _2, _1);
cout << funcMul(x, y, z) << endl;
}
- 由此可见,当我们使用bind将占位符_1,_2,_3的顺序颠倒后,再次传递实参x,y,z给Mul函数,此时,x传递给_1,y传递给_2,z传递给_3.
- 但是,此时占位符_1,_2,_3指代的Mul形参已经发生改变._1由原先的指向a现在指向了rate,_2依旧指向b,_3由原先指向rate现在指向a.
- 即:当我们调用funcMul对象传递实参x,y,z时,此时x传递给形参rate,y传递给b,z传递给a.
6.2.2 bind绑定函数固定参数
为什么要使用bind绑定固定参数呢?
因为使用bind绑定固定参数后,可以通过bind包装后生成新的形参较少的对象来调用原来的对象.
例如:
在以下例子中,对于成员函数来说,要通过bind包装新生成的对象调用,因为成员函数需要对象才能调用,所以我们要比全局成员函数要额外多传递一个参数,但是当我们使用包装器bind同时包装全局成员函数和普通成员函数时,那么包装器此时根本无法判断接收参数的具体个数.`
int Plus(int a, int b)
{
return a / b;
}
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
function< int(int, int)> funcPlus = Plus;
function< int(int, int)>funcSub = bind(&Sub::sub, Sub(), _1, _2);
map<string, function<int(int, int)>> funcMap =
{
{"+",Plus},
{"-",bind(&Sub::sub,Sub(),_1,_2) }
};
cout << funcSub(2, 1) << endl;
cout << funcMap["-"](2, 1) << endl;
}
- 将成员函中要接收三个参数经过bind适配器绑定后,在定义时只需要在&Sub::sub后面直接传递的一个sub()匿名对象进行该参数绑定,然后在以后的调用时,只需要传递对应的函数所需要的形参就可以了.
- 这样也完美的解决了bind同时包装类的成员函数和全局函数接收参数不匹配的问题.
再比如:
当我们再次使用bind包装器对Mul函数的第一个参数进行绑定,此时默认a已经传递了实参,接下来在调用的时候只需要传递两个实参给Mul函数形参中的b,rate就行.