一 类型推导
1.1 意义
理解编译器推导规则有利于高效的使用c++ 从明显或者冗余的类型拼写中解放出来,这样使得c++也更有适配性。
1.2 boost安装
sudo apt update sudo apt-get install libboost-all-dev
1.3 反例
#include <iostream> using namespace std; int main(int argc, char const *argv[]) { const int num = 1; auto tmp = num; tmp++; return 0; }
1.4 类型推导的使用场景
#include <iostream> #include <boost/type_index.hpp> #include <vector> using boost::typeindex::type_id_with_cvr; using namespace std; //引用折叠 //T:int & func(T &&t) --->func(int & &&t) //T:int && func(T &&t) --->func(int && &&t) //折叠规则:有左即为左,全右则为右 //转发:完美转发 //std:forward<T>(); void test(int &num1) { cout<<"int &num1"<<endl; cout<<"num1 = "<<num1<<endl; } /*void test(int &&num2) { cout<<"int &&num2"<<endl; cout<<"num2 = "<<num2<<endl; }*/ /* 万能引用(未定义引用):只能再函数模板中使用 作用:既能接收左值,也能接收右值 */ //注意事项: //1、模板参数必须紧跟&&符号 //void func(vector<T> &&v) //---》这个不是万能引用,因为vector和v之间有<T> //2.const属性会剥夺万能引用的权限 //void func(const T && Parm) //-->这个也不是万能引用,因为被const修饰 //3.万能引用不是一种新的数据类型,它只存在于函数模板中 //test(int &num)表示需要传入左值引用,如果是(int &&num)表示需要传入右值引用 //在万能引用内部,右值传入变成左值,左值传入还是左值 //std::forward:可以解决万能引用内,变成右值std::forward<int &&>(t),传入test(int &&num) //std::move:将左值变右值 //建议:对于右值引用使用std::move,对于万能引用使用std::forward。 template <typename T> void print(T &&t) //右值引用(也称万能引用) //void print(T &num) //左值引用 { t++; cout<<t<<endl; test(std::forward<int &&>(t)); //test(std::forward<int &>(t)); test(t); cout<<"void print(T &&t)"<<endl; cout<<"T type:"<<type_id_with_cvr<T>().pretty_name()<<endl; cout<<"t type:"<<type_id_with_cvr<decltype(t)>().pretty_name()<<endl; } //按值传递 /* int --->推导规则:T:int t:int const int --->推导规则:T: int t:int(忽略const 属性) int& --->推导规则:T: int t:int(忽略&属性) int* --->推导规则:T: int* t:int*(不会忽略指针属性) */ #if 0 template<typename T> void func(T t) { cout<<"void func(T t)"<<endl; cout<<"T type:"<<type_id_with_cvr<T>().pretty_name()<<endl; cout<<"t type:"<<type_id_with_cvr<decltype(t)>().pretty_name()<<endl; } //按指针传递 /* int --->推导规则:T:int t:int* const int --->推导规则:T: const int t:const int* */ template<typename T> void func(T *t) { cout<<"void func(T *t)"<<endl; cout<<"T type:"<<type_id_with_cvr<T>().pretty_name()<<endl; cout<<"t type:"<<type_id_with_cvr<decltype(t)>().pretty_name()<<endl; } #endif //按引用传递 同引用 template<typename T> void func(T &t) //T:int t:int& { cout<<"void func(T &t)"<<endl; cout<<"T type:"<<type_id_with_cvr<T>().pretty_name()<<endl; cout<<"t type:"<<type_id_with_cvr<decltype(t)>().pretty_name()<<endl; } int main(int argc, char const *argv[]) { #if 0 int num = 5; print(5); int &num1 = num; int &&num2 = std::move(num); print(num1); print(num2); //按值传递 推导规则 int num =100; const int count = 0; int &num1 = num; func(num); func(count); func(num1); func(&num); //按指针传递 int num = 1; const int count = 0; func(&num); func(&count); //按引用传递 int num = 1; const int count = 0; func(num); func(count); #endif //万能引用--转发 int num = 1; const int count = 0; print(6); return 0; }
二 可调用对象
2.1 通过函数调用符操作的对象称之为可调用对象
2.2 可调用对象
普通函数 类成员函数/静态函数 类的成员函数指针 可转换为函数指针的类
#include <iostream> using namespace std; class Test { public: void print() //成员函数 { cout << "this is Test" << endl; } }; void print(void *pa) //void print() { cout << "普通函数" << endl; delete pa; } //typedef void(*T)(void *); using P_FUNC = void(*)(void *); class A { public: using P_FUNC = void(*)(void); static void print(void) { cout << "this is class A" << endl; } operator int() { return 1; } operator P_FUNC() //操作隐式转换,使得对象A转化成P_FUNC类型对象,此时的返回值是P_FUNC,就是函数名 { return print; } }; int main(void) { //print(); Test t; t.print(); P_FUNC p1 = print; //p1(); void(Test::*p_func)(void) = &Test::print; //p_func是一个成员函数指针 (t.*p_func)(); //成员函数指针也可以作为可调用对象 A a; a(); cout << a << endl; //------- //shared_ptr<A> p(new A(), print); shared_ptr<A> p(new A(), p1);//传回调函数 }
2.3 函数对象(仿函数)
重载函数调用运算符 函数对象和普通函数的区别
class B { public: void operator()(int a,int b) { cout << "hello class B" << endl; } }; //内置的函数对象 plus<int> p1; cout << p1(5, 6) << endl; minus<int> p2; cout << p2(7, 8) << endl; less<int> p3; cout << p3(7, 8) << endl; greater<int> p4; cout << p4(8, 9) << endl;
三 lambda 表达式
(1)本质
实际就是匿名函数,能够给捕获一定范围的变量 与普通函数不同,可以在函数的内部定义。
(2)格式
[捕获列表](参数列表)->返回值类型{语句块};
说明: 1.返回值类型可以由编译器推导出来,可以省略不写 2.参数可以有默认值 3.c++14的lambda形参可以使用auto声明 4.lambda的调用方法和普通函数一样
#include <iostream> using namespace std; int main(void) { /*void print() { cout << "hello world" << endl; }*/ //auto p = []() {}; //auto p = [](auto num = 10)->int {}; //c++14后lambda可以推导形参 auto p = [](void *) { cout << "hello world" << endl; return 'a'; //自动推导 }; //p(); //lambda的调用时机:1延时调用 2.定义并调用 //shared_ptr<int> p2(new int(6), p);//1延时调用 shared_ptr<int> p3(new int(5), [](void *p) { cout << "hello share ptr" << endl; delete p; }); return 0; }
(3) 捕获列表
#include <iostream> #include <string> using namespace std; class A { public: A(int num, string s):m_num(num),m_s(s) { } void test() { int count = 10; //[&]或者[=]:默认捕获this指针 auto p = [this]() //只捕获this指针,可以修改,但是不能访问外部的变量 { //cout << count << endl; cout << m_num << endl; cout << m_s << endl; cout << "lambda p" << endl; }; p(); } public: int m_num; string m_s; }; int main(void) { #if 0 /*void print() { cout << "hello world" << endl; }*/ //auto p = []() {}; //auto p = [](auto num = 10)->int {}; //c++14后lambda可以推导形参 auto p = [](void *) { cout << "hello world" << endl; return 'a'; //自动推导 }; //p(); //lambda的调用时机:1延时调用 2.定义并调用 //shared_ptr<int> p2(new int(6), p);//1延时调用 shared_ptr<int> p3(new int(5), [](void *p) { cout << "hello share ptr" << endl; delete p; }); #endif //捕获列表: int num = 5; int count = 8; string s1 = "hello world"; //auto p = []() //不捕获外部的任何变量 //auto p1 = [&]() //按引用捕获外部所有的变量 //auto p1=[=]() //按等号捕获,只能使用外部所有的值,不能修改 //auto p1 = [num,s1]() //只能捕获num,count,只能使用,不能修改 //auto p1 = [=,&num,&count] //其它变量都是按照等号捕获,num和count按照引用捕获 auto p1 = [&,num,count]() //其它变量都是按照引用捕获,num,count按照值捕获 { cout << "num1 = " << num << endl; //num = 100; s1 = "hello kitty"; //cout << count << endl; cout << s1 << endl; }; p1(); cout << "num1 = " << num << endl; A a(1,"hello world"); a.test(); return 0; }
(4)注意事项
1.引用悬挂 按引用捕获会导致指向局部变量的引用,当lambda离开局部作用域时,会导致引用的那个变量被释放,lambda里面的那个引用会发生引用悬挂。 2. this陷阱
#include <iostream> #include <vector> #include <functional> #include <algorithm> #include <memory> typedef std::function<void(void)> FP; using namespace std; class Point { public: Point(int x, int y) :m_x(x), m_y(y) { } void print() { //解决方法1:产生副本,捕获副本,x,y也存在失效的风险 int x = m_x; int y = m_y; //this指针已经失效,空间释放,导致访问不到M_x,m_y cout << "x: " << m_x << " y: " << m_y << endl; //v1.push_back([=]() {cout << "x: " << m_x << " y: " << m_y << endl; }); //v1.push_back([x,y]() {cout << "x: " << x << " y: " << y << endl; }); //c++14:广义lambda捕获 v1.push_back([a = m_x, b = m_y]() {cout << "x: " <<a << " y: " << b << endl; }); } static void print_History() { for_each(v1.begin(), v1.end(), [](FP p) { if (p) { p(); } }); } private: int m_x; int m_y; typedef function<void(void)> FP; static vector<FP> v1; }; vector<FP> Point::v1 = vector<FP>(); int main(void) { unique_ptr<Point> p; p.reset(new Point(1, 2)); p->print(); p.reset(new Point(2, 3)); p->print(); p.reset(new Point(3, 4)); p->print(); Point::print_History(); return 0; }
四 function包装器
4.1 本质
一个类模板,用于包装可调用对象,可以容纳除了类成员(函数)指针之外的所有可调用对象。
4.2 作用
可以用同一的方式来保存或者传递可调用对象。
4.3 意义
实现了一台消失机制,可以用统一的方式处理不同类型的可调用对象 function进一步深化以数据为中心的面向对象思想(函数也被对象化)。
4.4 案例
#include <iostream> #include <functional> using namespace std; void print() { cout << "hello world" << endl; } class Test { public: int operator()(int a, int b) { return a + b; } }; class A { public: static int func(int a,int b) { cout << "this is class A" << endl; return a + b; } }; int add(int a, int b) { return a + b; } void test(int a, int b, int(*p)(int, int)) //仿函数无法调用 { cout << " int(*p)(int, int))" << endl; cout << p(a, b) << endl; } //一个函数指针,返回值int 传入(int,int) void test(int a, int b, function<int(int, int)> fp) //适配性更好 { cout << "function<int(int, int)>" << endl; cout << fp(a, b) << endl; } int main(void) { #if 0 function<void(void)> fp(print); fp(); auto p = [](int a, int b) { return a + b; }; function<int(int, int)> fp2(p); cout << fp2(1, 2) << endl; Test t; //function<int(int, int)> fp3(t); function<int(int, int)> fp3 = t; cout << fp3(1, 2) << endl; auto p2 = A::func; function<void(void)> fp4 = p2; #endif //function作为函数的参数 test(1, 2, add); auto p = [](int a, int b) { return a + b; }; test(6,7,p); Test t; test(9, 8, t); test(5, 6, A::func); return 0; }
五 bind适配器
(1)本质
函数模板,返回值是一个仿函数,也是可调用对象 c++11合并之前的bind1和bind2
(2) 作用
将多元的可调用对象与其参数一定绑定成一个仿函数对象 将多元(n)的可调用对象转成一元或(n-1)元的可调用对象,即只绑定部分参数。
(3)bind可以绑定的对象
1> 普通函数
#include <iostream> #include <functional> using namespace std; using namespace std::placeholders; //使用占位符 int add(int a, int b) { cout << "a = " << a << " "; cout << "b = " << b << endl; return a + b; } void test(function<int(void)> t) { cout << t() << endl; } void test(function<int(int)> &t) { cout << t(5) << endl; } int main(void) { add(5, 6); //test(add); auto p = bind(add, 5, 6); //将add转为int(void) test(p); cout << p() << endl; auto p1 = bind(add, placeholders::_1, 6); cout << p1(9) << endl; auto p2 = bind(add, 2, placeholders::_1); cout << p2(5) << endl; auto p3 = bind(add, std::placeholders::_2, std::placeholders::_1); p3(3, 4); return 0; }
2> 函数对象
class Test //函数对象 { public: int operator()(int a, int b) { cout << "a = " << a << " "; cout << "b = " << b << endl; return a + b; } }; //绑定函数对象 Test t; cout << t(5, 6) << endl; //test(t); auto p = bind(t, std::placeholders::_1, 6); test(p); less<int> le; cout << le(5, 6) << endl; auto p1 = bind(le, 5, std::placeholders::_1); cout << p1(6) << endl;
3> 类的成员函数(_1:必须是某个对象的地址)
4> 类的数据成员(_1:必须是某个对象的地址)
//适配成员属性 Test t1; t1.m_a = 100; t1.m_count = 200; auto p1 = bind(&Test::m_a, t1); //将t1和m_a进行绑定,t1按照值进行适配 cout << p1() << endl; p1() = 1000; cout << t1.m_a << endl;
(4)使用案例
注意:bind预先绑定的参数需要传具体的变量或者值进去,对于预先绑定的参数是按值传递的。 stl中算法
#include <iostream> #include <functional> using namespace std; using namespace std::placeholders; //使用占位符 int add(int a, int b) { cout << "a = " << a << " "; cout << "b = " << b << endl; return a + b; } /*void test(function<int(void)> &t) { cout << t() << endl; } void test(const function<int(int)> &t) { cout << t(5) << endl; }*/ class Test //函数对象 { public: int operator()(int a, int b) { cout << "a = " << a << " "; cout << "b = " << b << endl; return a + b; } int add(int a, int b) { cout << "a = " << a << " "; cout << "b = " << b << endl; return a + b; } int m_a; int m_count; }; void test(function<int(Test *,int)> &t) { } int main(void) { //add(5, 6); test(add); //auto p = bind(add, 5, 6); //将add转为int(void) //test(p); //cout << p() << endl; //auto p1 = bind(add, placeholders::_1, 6); //cout << p1(9) << endl; //auto p2 = bind(add, 2, placeholders::_1); //cout << p2(5) << endl; //auto p3 = bind(add, std::placeholders::_2, std::placeholders::_1); //p3(3, 4); //绑定函数对象 //Test t; //cout << t(5, 6) << endl; test(t); //auto p = bind(t, std::placeholders::_1, 6); //test(p); //less<int> le; //cout << le(5, 6) << endl; //auto p1 = bind(le, 5, std::placeholders::_1); //cout << p1(6) << endl; //适配成员函数 //test(成员函数)://error:function不能包装类的成员函数和成员函数指针,需要通过bind适配器成可调用的对象 //因为bind返回的是仿函数,它是一个可调用对象,所以可以用function包装器包装 //适配成员函数时第一个参数是对象的地址 //Test t; //auto p = bind(&Test::add, std::placeholders::_1, 5,std::placeholders::_2); //cout << p(&t,3) << endl; test(p); //适配成员属性 Test t1; t1.m_a = 100; t1.m_count = 200; auto p1 = bind(&Test::m_a, t1); //将t1和m_a进行绑定,t1按照值进行适配 cout << p1() << endl; p1() = 1000; cout << t1.m_a << endl; return 0; }
面试题:什么是右值引用?右值引用与左值引用的区别
一、左值与左值引用
什么是左值引用呢?
左值引用,就是绑定到左值的引用,通过&来获得左值引用。
那么,什么是左值呢?
左值,就是在内存有确定存储地址、有变量名,表达式结束依然存在的值。
左值可以分为两类:非常量左值和常量左值;同理,右值也可以分为两类:非常量右值和常量左值。
左值引用举例说明:
int a=10; //非常量左值(有确定存储地址,也有变量名) const int a1=10; //常量左值(有确定存储地址,也有变量名) const int a2=20; //常量左值(有确定存储地址,也有变量名) //非常量左值引用 int &b1=a; //正确,a是一个非常量左值,可以被非常量左值引用绑定 int &b2=a1; //错误,a1是一个常量左值,不可以被非常量左值引用绑定 int &b3=10; //错误,10是一个非常量右值,不可以被非常量左值引用绑定 int &b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量左值引用绑定 //常量左值引用 const int &c1=a; //正确,a是一个非常量左值,可以被非常量右值引用绑定 const int &c2=a1; //正确,a1是一个常量左值,可以被非常量右值引用绑定 const int &c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定 const int &c4=a1+a2; //正确,(a1+a2)是一个常量右值,可以被非常量右值引用绑定
可以归纳为:非常量左值引用只能绑定到非常量左值上;常量左值引用可以绑定到非常量左值、常量左值、非常量右值、常量右值等所有的值类型。
二、右值与右值引用
顾名思义,什么是右值引用呢?
右值引用,就是绑定到右值的引用,通过&&来获得右值引用。
那么,什么又是右值呢?
右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。
右值引用举例说明:
int a=10; //非常量左值(有确定存储地址,也有变量名) const int a1=20; //常量左值(有确定存储地址,也有变量名) const int a2=20; //常量左值(有确定存储地址,也有变量名) //非常量右值引用 int &&b1=a; //错误,a是一个非常量左值,不可以被非常量右值引用绑定 int &&b2=a1; //错误,a1是一个常量左值,不可以被非常量右值引用绑定 int &&b3=10; //正确,10是一个非常量右值,可以被非常量右值引用绑定 int &&b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量右值引用绑定 //常量右值引用 const int &&c1=a; //错误,a是一个非常量左值,不可以被常量右值引用绑定 const int &&c2=a1; //错误,a1是一个常量左值,不可以被常量右值引用绑定 const int &&c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定 const int &&c4=a1+a2; //正确,(a1+a2)是一个常量右值,不可以被常量右值引用绑定
可以将右值引用归纳为:非常量右值引用只能绑定到非常量右值上;常量右值引用可以绑定到非常量右值、常量右值上。
从上述可以发现,常量左值引用可以绑定到右值上,但右值引用不能绑定任何类型的左值,若想利用右值引用绑定左值该怎么办呢?
C++11中提供了一个标准库move函数获得绑定到左值上的右值引用,即直接调用std::move告诉编译器将左值像对待同类型右值一样处理,但是被调用后的左值将不能再被使用。
std::move使用举例说明:
int a=10; //非常量左值(有确定存储地址,也有变量名) const int a1=20; //常量左值(有确定存储地址,也有变量名) //非常量右值引用 int &&d1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被非常量右值引用绑定 int &&d2=std::move(a1); //错误,将常量左值a1转换为常量右值,不可以被非常量右值引用绑定 //常量右值引用 const int &&c1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被常量右值引用绑定 const int &&c2=std::move(a1); //正确,将常量左值a1转换为常量右值,可以被常量右值引用绑定
可以发现,编译器利用std::move将左值强制转换为相同类型的右值之后,引用情况跟右值是一模一样的。
三、右值引用与左值引用的区别
(1)左值引用绑定到有确定存储空间以及变量名的对象上,表达式结束后对象依然存在;右值引用绑定到要求转换的表达式、字面常量、返回右值的表达式等临时对象上,赋值表达式结束后就对象就会被销毁。
(2)左值引用后可以利用别名修改左值对象;右值引用绑定的值不能修改。
四、引入右值引用的原因
(1)替代需要销毁对象的拷贝,提高效率:某些情况下,需要拷贝一个对象然后将其销毁,如:临时类对象的拷贝就要先将旧内存的资源拷贝到新内存,然后释放旧内存,引入右值引用后,就可以让新对象直接使用旧内存并且销毁原对象,这样就减少了内存和运算资源的使用,从而提高了运行效率;
(2)移动含有不能共享资源的类对象:像IO、unique_ptr这样的类包含不能被共享的资源(如:IO缓冲、指针),因此,这些类对象不能拷贝但可以移动。这种情况,需要先调用std::move将左值强制转换为右值,再进行右值引用。