【C++高阶】:C++11的深度解析下

彼方尚有荣光在,何须悲叹少年轻**** 🌏

📃个人主页island1314

🔥个人专栏:C++学习

🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞


🚀前言

若没有了解C++11前面一部分知识的读者朋友们,可以看一下这篇文章:

【C++高阶】:C++11的深度解析上-CSDN博客

1、可变参数模板

1.1 概念

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

  • 可变模版参数
  • 参数类型可变
  • 参数个数可变
  • 打印参数包内容

下面就是一个基本可变参数的函数模板:

cpp 复制代码
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
//参数类型可变,参数个数可变
template <class ...Args>
void ShowList(Args... args)
{}

//以前的模板写法,固定了参数个数,类型不确定
template <class ...T>
void ShowList1(T... x)
{}

//对于Show调用
int main(){
	ShowList();
	ShowList(1);
	ShowList(1, "xxxxx");
}

其对sizeof的使用方法(不常见)

cpp 复制代码
template <class ...Args>
void ShowList(Args... args){
	// 可变参数模版编译时解析
	// 下面是运行获取和解析,所以不支持这样用
	cout << sizeof...(args) << endl;
	for (size_t i = 0; i < sizeof...(args); i++){
		cout << args[i] << " ";//不支持这样打印参数包内容
	}
}

1.2 展开参数包的两种方式

递归函数方式展开参数包
cpp 复制代码
//用一个无参的来进行匹配
void Print() //当是0个参数包的时候,就匹配到该函数
{
	cout << endl;
}

template <class T, class ...Args>
void Print(T&& x, Args&&... args) //三个参数包,先分析参数包类型
{
	cout << x << " "; //打印第一个参数
	Print(args...); //递归调用

//不能用这样来作0个参数包的截止
// 因为这里都是编译时推导的逻辑,而下面这个是运行时逻辑,在运行时判断,无法结束
	//if (sizeof...(args) == 0) return;
}

// 编译时递归推导解析参数
template <class ...Args>
void ShowList(Args&&... args){
	Print(args...);
}

int main(){
	ShowList();
	ShowList(1);
	ShowList(1, "xxxxx");
	ShowList(1, "xxxxx", 2.2);

	return 0;
}

对于递归函数解包其实质分析:

逗号表达式展开参数包
cpp 复制代码
template <class T>
int PrintArg(T t){
	cout << t << " ";
	return 0;
}

template <class ...Args>
void ShowList(Args... args){
	int arr[] = { PrintArg(args)... };
	cout << endl;
}
 
// 编译推演生成下面的函数
void ShowList(int x, char y, std::string z) //参数包换算之后就是这样的
{
	int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) }; //编译器通过解包生成的就是这个
	cout << endl;
}

int main(){
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));

	return 0;
}

对于逗号表达式解包其实质分析:

cpp 复制代码
//之前去掉逗号表达式通过返回值去掉的
template <class ...Args>
void ShowList(Args... args){
	//int arr[] = { cout << (args) << " "... };  //这样编译通过不了
	//ostream arr[] = { cout << (args) << " "... }; //ostream不支持拷贝,也通过不了
	int arr[] = { (cout<<(args)<<" ", 0)...}; //加个逗号表达式,0作那个初始化

	cout << endl;
}

void ShowList(int x, char y, std::string z) //相当于编译器生成了这个
{ //注意要有0
	int arr[] = { (cout<<(x)<<" ", 0), (cout << (y) << " ", 0), (cout << (z) << " ", 0) };

	cout << endl;
}

int main(){
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

2. lambda表达式

2.1 概念

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用sort函数 。如果待排序元素为自定义类型 ,需要用户使用仿函数定义排序时的比较规则 。随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式

2.2 lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}

  • [capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]判断接下来的函数是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

  • (parameters): 参数列表 。与普通函数的参数列表一致,如果不需要参数传递,则可以

    连同()一起省略。

  • mutable: 默认情况下,lambda函数总是一个const函数 ,mutable可以取消其常量性 。使用该修饰符时,参数列表不可省略(即使参数为空)

  • **->returntype:返回值类型。**用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

  • {statement}:函数体 。在该函数体内,除了可以使用其参数外,还可以使用所有捕获

    到的变量。

注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情

捕获列表说明:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

  • [var]:表示值传递方式捕捉变量var。
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括成员函数中的this)。
  • [&var]:表示引用传递捕捉变量var。
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括成员函数中的this)。
  • [this]:表示值传递方式捕捉当前的this指针。

代码示例1:

cpp 复制代码
// lambda 匿名函数的对象
int main(){
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 2) << endl;

	auto func1 = []()->int{
		cout << "hello bit" << endl;
		cout << "hello world" << endl;
		return 0;
	};
	func1();

	// 返回值类型可自动推导类型,所以可以省略
	// 无参数可以省略
    // 返回值一般都不能省略
	auto func2 = []{
		cout << "hello bit" << endl;
		cout << "hello world" << endl;
		return 0;
	};
	cout << func2() << endl;

    //无返回值时可以省略返回值,但是建议最好还是有返回值
	auto func3 = []{
		cout << "hello bit" << endl;
		cout << "hello world" << endl;
	};
	func3();

	return 0;
}

代码示例2:

cpp 复制代码
int main(){
	int a = 0, b = 1;

	auto swap1 = [](int& x, int& y){ //参数列表
		// 只能用当前lambda局部域和捕捉的对象
		int tmp = x;
		x = y;
		y = tmp;
	};
	swap1(a, b);

	// 传值捕捉本质是一种拷贝,并且const修饰了
	// mutable相当于去掉const属性,可以修改了
	// 但是修改了不会影响外面被捕捉的值,因为是一种拷贝
	auto swap2 = [a, b]()mutable{ //捕捉列表
		int tmp = a;
		a = b;
		b = tmp;
	};
	swap2();

	auto swap3 = [&a, &b](){ //这里 & 是引用
		int tmp = a;
		a = b;
		b = tmp;
	};
	swap3();
     
    //...此处Goods的结构体构造三个参数分别为string _name,int _price,_evaluate
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, 
        { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } }; 
    //bool返回类型可以省略
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool
	{
		return g1._price > g2._price;
	});
    
	return 0;
}

注意:

  • 父作用域指包含lambda函数的语句块
  • 语法上捕捉列表可由多个捕捉项组成 ,并以逗号分割 。比如:[=, &a, &b] :以引用传递 的方式捕捉变量a和b,值传递 方式捕捉其他所有变量 [&,a,this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
  • 捕捉列表不允许变量重复传递 ,否则就会导致编译错误 。 比如:[=, a]= 已经以值传递方式捕捉了所有变量,捕捉a重复。
  • 块作用域以外 的lambda函数捕捉列表必须为空。
  • 在块作用域中的lambda函数仅能捕捉父作用域中局部变量 ,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  • lambda表达式之间不能相互赋值,即使看起来类型相同。

总的来说,lambda表达式就是一个仿函数,底层被处理成为一个lambda_uuid的一个仿函数类。

代码示例如下:

cpp 复制代码
int x = 0;
int main(){
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	// 所有值传值捕捉
	auto func1 = [=]{
		int ret = a + b + c + d + x;
		return ret;
	};
	func1();
	cout << "auto func1 = [=]:"<< a << " " << b << " "  << c << " " << d << endl;

	// 所有值传引用捕捉
	auto func2 = [&]{
		a++, b++, c++, d++;
		int ret = a + b + c + d;
		return ret;
	};
	func2();
	cout <<"auto func2 = [&]:"<< a << " " << b << " " << c << " " << d << endl;

	// 混合捕捉
	auto func3 = [&a, b]{
		a++;
		//b++; //由于b是传值捕捉,b就不能修改
		int ret = a + b;
		return ret;
	};
	func3();
	cout<<"auto func3 = [&a, b]:" << a << " " << b << " " << c << " " << d << endl;
	

	// 混合捕捉
	// 所有值以引用方式捕捉,d用传值捕捉
	auto func4 = [&, d]{
		a++,b++,c++;
		//d++; //故d不能修改
		int ret = a + b + c + d;
	};
	func4();
	cout << "auto func4 = [&, d]:" << a << " " << b << " "  << c << " " << d << endl;


	auto func5 = [=, &d](){
		//a++, b++, c++; //a,b,c都不能修改
		d++; 
		int ret = a + b + c + d;
	};
	func5();
	cout << "auto func5 = [=, &d]():" << a << " " << b << " " << c << " " << d << endl;

	auto func6 = [=, &d]() mutable {
		a++, b++, c++, d++; //都可以修改,但是只有d对外面的值有影响
		int ret = a + b + c + d;
	};
	func6();
	cout << "auto func6 = [=, &d]() mutable:" << a << " " << b << " "<< c << " " << d << endl;
	
	return 0;
}

2.3 函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的

类对象。

lambda表达式可以很好的代替仿函数使用,如果使用仿函数,阅读代码的人想要知道其中的逻辑还需要去对应的地方去找,而lambda表达式可以很好的避免这个问题,增强了代码的可读性:

cpp 复制代码
class Rate{
public:
	Rate(double rate) : _rate(rate)
	{}

	double operator()(double money, int year){
		return money * _rate * year;
	}
private:
	double _rate;
};

//捕捉列表的对象是成员变量存在lambad类对象中
//捕捉的本质是构造函数的初始化列表

int main(){
	// 函数对象
	double rate = 0.015;
	Rate r1(rate);
	cout << r1(10000, 2) << endl;

	// lambda
	auto r2 = [rate](double monty, int year)->double{
		return monty * rate * year;
	};
	cout << r2(10000, 2) << endl;

	int x = 1, y = 2;
	auto r3 = [=](double monty, int year)->double{
		return monty * rate * year;
	};
	cout << r3(10000, 2) << endl;

	return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可

以直接将该变量捕获到。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如

果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

3、包装器

3.1 function

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

ret = func(x);

上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!

举个例子:

cpp 复制代码
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;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

通过上面的程序验证,我们会发现useF函数模板实例化了三份。

但是包装器可以很好的解决上面的问题

std::function在头文件**<functional>**

// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

模板参数说明:

  • Ret : 被调用函数的返回类型
  • Args...:被调用函数的形参

使用方法如下:

cpp 复制代码
#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:
	static int plusi(int a, int b){
		return a + b;
	}
	double plusd(double a, double b){
		return a + b;
	}
};


int main(){
	// 包装可调用对象
	//function<int(int)> f1 = f; //类型不匹配会报错的
	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;

	// 包装非静态成员函数
	//非静态的成员函数要取函数指针的时候,需要加个&,静态可以不加
	//function<double(double, double)> f5 = &Plus::plusd;  
	
	//由于上面编译通过,因为其还有一个隐含的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(Plus(), 1.1, 1.1) << endl;

	return 0;
}

有了包装器,如何解决模板的效率低下,实例化多份的问题呢?

cpp 复制代码
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(){
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	// lamber表达式
	std::function<double(double)> func3 = [](double d)->double{ return d / 4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}
 

值得注意的是:对成员函数进行包装时,需要取地址

包装器的一些其他使用场景:

逆波兰表达式求值

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        map<string, function<int(int, int)>>opFuncMap = {
            {"+", [](int x, int y){return x + y;}},
            {"-", [](int x, int y){return x - y;}},
            {"*", [](int x, int y){return x * y;}},
            {"/", [](int x, int y){return x / y;}}
        };
        // //方法一:
        // for(auto &e : tokens){
        //     if(e == "+" || e == "-" || e == "*" || e == "/"){
        //         int r = st.top();st.pop();
        //         int l = st.top(); st.pop();
        //         switch(e[0]){
        //             case '+':
        //                 st.push(l + r);
        //                 break;
        //             case '-':
        //                 st.push(l - r);
        //                 break;
        //             case '*':
        //                 st.push(l * r);
        //                 break;
        //             case '/':
        //                 st.push(l / r);
        //                 break;
        //         }
        //     }
        //     else {
        //         st.push(stoi(e));
        //     }
        // }
        
        //方法二:
        for(auto &e : tokens)
        {
            //if(e == "+" || e == "-" || e == "*" || e == "/")
            if(opFuncMap.count(e)) {  // == 1就是操作符
                int r = st.top();st.pop();
                int l = st.top();st.pop();
                int ret = opFuncMap[e](l,r);
                st.push(ret);
            }
            else{
                st.push(stoi(e));
            }
        }

        return st.top();
    }
};

3.2 bind

bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个**(M可以大于N,但这么做没什么意义)参数的新函数**。同时,使用bind函数还可以实现参数顺序调整等操作。

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表。 调用bind的一般形式:auto newCallable =bind(callable,arg_list);其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是"占位符",表示newCallable的参数,它们占据了传递给newCallable的参数的"位置"。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

使用如下:

cpp 复制代码
int Sub(int a, int b){return (a - b) * 10;}

int SubX(int a, int b, int c){return (a - b - c) * 10;}

//可以用命名空间展示出来
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int main()
{
	//auto sub1 = bind(Sub,placeholders::_1,placeholders::_2);
	// bind 本质返回的一个仿函数对象
	// 调整参数顺序(不常用)
	// _1代表第一个实参
	// _2代表第二个实参

	auto sub1 = bind(Sub, _1, _2); //代表Sub(_1, _2)
	cout << sub1(10, 5) << endl;//10 相当于_1, 5相当于_2

	auto sub2 = bind(Sub, _2, _1); //代表Sub(_2, _1)
	cout << sub2(10, 5) << endl; //5 相当于_2, 10相当于_1

	//调整参数个数(常用)
	auto sub3 = bind(Sub, 10, _1); //代表Sub(100, _1)
	cout << sub3(5) << endl; //5 相当于_1

	auto sub4 = bind(Sub, _1, 10); //代表Sub(_1, 10)
	cout << sub4(5) << endl; //5 相当于_1

	// 分别绑死第123个参数
	auto sub5 = bind(SubX, 10, _1, _2);
	cout << sub5(5, 1) << endl;

	auto sub6 = bind(SubX, _1, 10, _2);
	cout << sub6(5, 1) << endl;

	auto sub7 = bind(SubX, _1, _2, 10);
	cout << sub7(5, 1) << endl;

	function<double(Plus, double, double)> f6 = &Plus::plusd;
	Plus pd;
	cout << f6(pd, 1.1, 1.1) << endl;
	cout << f6(Plus(), 1.1, 1.1) << endl;

	// bind一般用于,绑死一些固定参数
	function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
	cout << f7(1.1, 1.1) << endl;


	//auto func1 = [](double rate, double money, int year)->double {return monty * rate * year; }; //单利
	auto func1 = [](double rate, double money, int year)->double { //复利
		double ret = money;
		for (int i = 0; i < year; i++)
		{
			ret += ret * rate;
		}

		return ret - money;
	};

	function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
	function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
	function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
	function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);

	cout << func3_1_5(1000000) << endl;
	cout << func5_1_5(1000000) << endl;
	cout << func10_2_5(1000000) << endl;
	cout << func20_3_5(1000000) << endl;

	return 0;
}

**包装器的作用:**统一可调用对象的类型,指明了参数和返回值类型

不包装前可能存在很多问题:

  • 函数指针太复杂,不方便理解。
  • 仿函数类型是一个类名,没有指明参数和返回值,需要去operator()才能看出来。
  • lambda表达式在语法层看不到类型。

📖总结

以上就是C++11的全部内容啦

本篇到此就结束,希望我的这篇博客可以给你提供有益的参考和启示,感谢大家支持!!!

祝大家天天顺心如意。

相关推荐
奋斗的小花生1 小时前
c++ 多态性
开发语言·c++
Nu11PointerException1 小时前
JAVA笔记 | ResponseBodyEmitter等异步流式接口快速学习
笔记·学习
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
UestcXiye2 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
霁月风4 小时前
设计模式——适配器模式
c++·适配器模式
jrrz08284 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
咖啡里的茶i4 小时前
Vehicle友元Date多态Sedan和Truck
c++
海绵波波1074 小时前
Webserver(4.9)本地套接字的通信
c++
@小博的博客4 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
南宫生5 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法