从C语言到C++_34(C++11_下)可变参数+ lambda+function+bind+笔试题(中):https://developer.aliyun.com/article/1522408
3.1.2 function的场景(力扣150:逆波兰表达式求值)
包装器的其他一些场景,以前写过的题目:
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
以前的思路和代码:逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作:
如果遇到操作数,则将操作数入栈;
如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,
后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。
整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。
简单来说就是:① 操作数入栈 ② 遇到操作符就取栈顶两个操作数运算
//1 操作数入栈 //2 遇到操作符就取栈顶两个操作数运算 class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; for(auto& e: tokens) { if(e == "+" || e == "-" || e == "*" || e == "/") { int right = st.top();//右操作数先出栈 st.pop(); int left = st.top(); st.pop(); switch(e[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; } } else { st.push(stoi(e)); } } return st.top(); } };
用了lambda表达式和function包装器的代码:(力扣给的测试改了,用long long)
class Solution { public: int evalRPN(vector<string>& tokens) { stack<long long> st; map<string, function<long long(long long, long long)>> opFuncMap = { {"+", [](long long a, long long b) {return a + b;}}, {"-", [](long long a, long long b) {return a - b;}}, {"*", [](long long a, long long b) {return a * b;}}, {"/", [](long long a, long long b) {return a / b;}}, }; for (auto& str : tokens) { if (opFuncMap.count(str)) // 操作符 { int right = st.top();//右操作数先出栈 st.pop(); int left = st.top(); st.pop(); st.push(opFuncMap[str](left, right)); } else // 操作数 { st.push(stoll(str)); } } return st.top(); } };
3.2 bind绑定
学了function包装器,如果出现以下这种场景怎么办?
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(Sub, int, int)> funcSub = &Sub::sub; // 类内函数要传类,多一个this指针参数 cout << funcPlus(7, 5) << endl; cout << funcSub(Sub(), 7, 5) << endl; // 此时三个参数就不和下面opFuncMap两个参数匹配了 //map<string, function<int(int, int)>> opFuncMap = //{ // { "+", Plus}, // { "-", &Sub::sub} // 报错:没有与参数列表匹配的构造函数 //}; return 0; }
这时包装器“包不了了”,类内函数要传类,多一个参数,此时bind就来了:
- bind:也叫做绑定,是一个函数模板,返回一个function,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
这是一个万能引用模板,除了可调用对象模板参数外,其它参数是可变参数,也就是一个参数包。
int Plus(int a, int b) { return a + b; } class Sub { public: int sub(int a, int b) { return a - b; } }; using namespace placeholders; // _1, _2 要加命名空间还要#include <functional> int main() { //function<int(int, int)> funcPlus = Plus; //function<int(Sub, int, int)> funcSub = &Sub::sub; // 类内函数要传类,多一个this指针参数 //cout << funcPlus(7, 5) << endl; //cout << funcSub(Sub(), 7, 5) << endl; // 此时三个参数就不和下面opFuncMap两个参数匹配了 map<string, function<int(int, int)>> opFuncMap = { { "+", Plus}, { "-", &Sub::sub} // 报错:没有与参数列表匹配的构造函数 }; function<int(int, int)> funcPlus = Plus; function<int(int, int)> funcSub = bind(&Sub::sub, Sub(), _1, _2); cout << funcPlus(7, 5) << endl; cout << funcSub(7, 5) << endl; map<string, function<int(int, int)>> opFuncMap = { { "+", Plus}, { "-", bind(&Sub::sub, Sub(), _1, _2)} }; cout << opFuncMap["+"](7, 5) << endl; cout << opFuncMap["-"](7, 5) << endl; return 0; }
这里用bind绑定了第一个参数(固定住了),后面_1就是要传的第一个参数,_2就是要传的第二个参数。
还有调整顺序的功能:
int Sub(int a, int b) { return a - b; } using namespace placeholders; // _1, _2 要加命名空间还要#include <functional> int main() { int x = 2, y = 9; cout << Sub(x, y) << endl; //_1 _2.... 定义在placeholders命名空间中,代表绑定函数对象的形参, //_1,_2...分别代表第一个形参、第二个形参... //function<int(int, int)> bindFunc1 = bind(Sub, _1, _2); // 没换顺序 //function<int(int, int)> bindFunc2 = bind(Sub, _2, _1); // 换顺序了 auto bindFunc1 = bind(Sub, _1, _2); // 没换顺序 auto bindFunc2 = bind(Sub, _2, _1); // 换顺序了 cout << bindFunc1(x, y) << endl; cout << bindFunc2(x, y) << endl; return 0; }
调整顺序的功能比较鸡肋,很少用,除非特殊场景,bind用来占位比较常用。
4. 笔试选择题
1. 下面关于范围for说法错误的是
A.范围for可以直接应用在数组上
B.对于STL提供的所有容器,均可以使用for依次访问器元素
C.使用范围for操作stack,可以简化代码
D.对于自定义类型,想要支持范围for,必须提供begin和end迭代器
E.范围for编译器最终是将其转化为迭代器来进行处理的
2. 下面关于列表初始化说法错误的是
A.在C++98中,{}只能用来初始化数组
B.在C++98中,new单个int类型空间可以直接初始化,new一段连续int类型空间不能直接初始化
C.在C++11中,{}的初始化范围增大了,任意类型都可以初始化
D.使用{}初始化时,必须要加=号
3. 下面关于auto的说法错误的是
A.auto主要用于类型推导,可以让代码的书写更简洁
B.在C++11中,auto即可以用来声明自动类型变量,也可以用来进行变量类型推导
C.auto不能推导函数参数的类型
D.auto是占位符,在编译阶段,推演出初始化表达式的实际类型来替换auto位置
4. 下列关于final说法正确的是()
A.final只能修饰类,表示该类不能被继承
B.final可以修饰任意成员函数
C.final修饰成员函数时,表示该函数不能被子类继承
D.final修饰派生类虚函数时,表示该虚函数再不能被其子类重写
5. 下面关于override说法正确的是()
A.override的作用发生在运行时期
B.override修饰子类成员函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写
C.override可以修饰基类的虚函数
D.override只能修饰子类的虚函数
6. 下面关于默认的构造函数说法正确的是()
A.C++98中用户可以选择让编译器生成或者不生成构造函数
B.在C++11中,即使用户定义了带参的构造函数,也可以通过default让编译器生成默认的构造函数
C.delete的作用是删除new的资源,别无它用
D.以下代码会编译通过: class A { public: A(){} A(int)=delete; }
7. 下面关于lambda表达式说法错误的是()
A.lambda表达式的捕获列表不能省略,但可以使空
B.lambda表达式就是定义了一个函数
C.lambda表达式底层编译器是将其转化为仿函数进行处理的
D.[]{}是一个lambda表达式
8. 关于右值引用说法正确的是()
A.引用是别名,使用比指针更方便安全,右值引用有点鸡肋
B.右值引用不能引用左值
C.右值引用只能引用右值
D.右值引用于引用一样,都是别名
9. 关于引用和右值引用的区别,说法正确的是()
A.引用只能引用左值,不能引用右值
B.右值引用只能引用右值,不能引用左值
C.引用和右值引用都可以引用左值和右值
D.以上说法都不对
10. 下面关于右值引用作用说法错误的是()
A.右值引用于引用一样,只是一个别名,别无它用
B.通过右值引用可以实现完美转发
C.通过右值引用可以将临时对象中的资源转移出去
D.通过右值引用可以实现移动构造函数,提高程序运行效率
11. 下面关于列表初始化说法正确的是()
A.C++语言从C++98开始,就支持列表方式的初始化
B.列表初始化没有什么实际作用,直接调用对应的构造函数就可以了
C.自定义类型可以支持多个对象初始化,只需要增加initializer_list类型的构造函数即可
D.以下关于c和d的初始化,结果完全相同 // short c = 65535; short d { 65535 };
答案及解析
1. C
A:正确,只要是范围确定的,都可以直接使用范围for
B:正确,对于STL提供的容器,采用范围for来进行遍历时,编译器最后还是将范围for转化为迭代 器来访问元素的
C:错误,stack不需要遍历,也没有提供迭代器,因此不能使用范围for遍历
D:正确,因为范围for最终被编译器转化为迭代器来遍历的
E:正确
2. D
A:正确,C++98中只能初始化数组,C++11中支持列表初始化,才可以初始化容器
B:正确,C++11对于new[]申请的空间,可以直接初始化
C:正确,对于自定义类型,需要提供initializer_list<T>类型的构造函数
D:错误,加=和不加=没有区别
3. B
A:正确
B:错误,C++11中已经去除了auto声明自动类型变量的功能,只可以用来进行变量类型推到
C:正确,因为函数在编译时,还没有传递参数,因此在编译时无法推演出形参的实际类型
D:正确,auto仅仅只是占位符,编译阶段编译器根据初始化表达式推演出实际类型之后会替换auto
4. D
A:正确
B:错误,C++11中已经去除了auto声明自动类型变量的功能,只可以用来进行变量类型推到
C:正确,因为函数在编译时,还没有传递参数,因此在编译时无法推演出形参的实际类型
D:正确,auto仅仅只是占位符,编译阶段编译器根据初始化表达式推演出实际类型之后会替换auto
5. D
A:override的作用时让编译器帮助用户检测是否派生类是否对基类总的某个虚函数进行重写,如 果重写成功,编译 通过,否则,编译失败,因此其作用发生在编译时。
B:错误,修饰子类虚函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写
C:错误,不能,因为override主要是检测是否重写成功的,而基类的虚函数不可能再去重写那个 类的虚函数
D:正确
6. B
A:错误,C++98中用户不能选择,用户如果没有定义构造函数,编译器会生成默认的,如果显式定义,编译器将不再生成
B:正确,C++11扩展了delete和default的用法,可以用来控制默认成员函数的生成与不生成
C:错误,C++11扩展了delete的用法,可以让用户控制让编译器不生成默认的成员函数
D:错误,类定义结束后没有分号
7. B
A:正确,捕获列表是编译器判断表达式是否为lambda的依据,即使为空,也不能省略
B:错误,lambda表达式不是一个函数,在底层编译器将其转化为仿函数
C:正确
D:正确,lambda表达式原型:[捕获列表](参数列表)mutable->返回值类型{} ,如果不需要捕获父作用域中内容时,可以为空,但是[]不能省略,如果没有参数,参数列表可以省略,如果不需要改变捕获到父作用域中内容时,mutable可以省略,返回值类型也可以省略,让编译器根据返回的结果进行推演,但是{}不能省略,因此[]{}是最简单的lambda表达式,但是该lambda表达式没有任何意义
8. D
A:错误,右值引用是C++11中的一个重点,可以实现移动语义、完美转发。
B:错误,一般右值引用只能引用右值,如果需要引用左值时,可以通过move函数转。
C:错误,同上。
D:正确,右值引用也是引用,是别名。
9. C
A:错误,T&只能引用左值,const T&是万能引用,左值和右值都可以引用。
B:错误,一般右值引用只能引用右值,如果需要引用左值时,可以通过move函数转。
C:正确,参考A和B的解析。
D:错误。
10. A
A:错误,右值引用是C++11中的一个重点,可以实现移动语义、完美转发
B:正确,实现完美转发时,需要用到forward函数
C:正确,右值引用的一个主要作用就是实现移动语义,以提高程序的效率
D:正确
11. C
A:错误,列表初始化是从C++11才开始支持的
B:错误,列表初始化可以在定义变量时就直接给出初始化,非常方便
C:正确
D:错误,不同,列表初始化在初始化时,如果出现类型截断,是会报警告或者错误的
本篇完。
下一篇:C++异常的使用+异常体系+优缺点。