C++11:可变参数模板、lambda表达式和包装器

news2024/11/26 23:38:01

目录

一. 可变参数模板

1.1 什么是可变模板参数

1.2 参数包具体值的获取

1.3 emplace/emplace_back接口函数 

二. lambda表达式

2.1 lambda表达式的概念和定义方法

2.2 捕捉列表说明

2.3 lambda表达式的底层实现原理

三. 包装器

3.1 function包装

3.2 bind绑定

3.2.1 bind改变参数顺序

3.2.2 bind调整参数个数 

四. 总结


一. 可变参数模板

1.1 什么是可变模板参数

在C++98标准中,模板只能包含固定个数的参数,但到了C++11,就允许了模板参数的个数可变,可变模板参数主要用于函数模板,其定义方式为:

template<class ...Args>
void Func(Args... args) { ... }

其中,...Args表示可变参数模板类型,args可以接收0~N个不同类型的参数,我们称args为参数包。通过 std::cout << sizeof...(args) << std::endl 我们可以获取可变参数的个数。

template<class ...Args>
void Func(Args... args) 
{ 
	std::cout << sizeof...(args) << std::endl;   //获取可变参数列表的参数个数
}

int main()
{
	Func(1, 2, 3, 4); // 4
	Func(1, 2, 3);    // 3
	Func(1, 2);       // 2
	Func(1);          // 1
	return 0;
}

1.2 参数包具体值的获取

注意,C++语法不支持通过args[i]来获取参数包的值。因此,无法通过下标来直接获取值。这就要求我们采取巧妙的办法,逐个遍历参数包args的每个值。主要的方法有两种:

  • 递归函数展开参数包。
  • 逗号表达式展开函数包。

如果采用递归函数展开函数包,就要去控制递归终止的条件,一般采用重载参数个数为0的函数,作为控制递归终止的函数。演示代码1.1以PrintList函数为例,不将第一个参数val归入可变参数包args,再使用完val时,以args...作为形参,传给PrintList函数实现递归调用,这是参数包args的第一个参数就充当递归调用函数的val参数。如果args中参数个数为0,就走PrintList的重载形式,函数递归调用终止。

代码1.1:递归方法展开参数包

void PrintList()  //递归终止控制函数
{
	std::cout << std::endl;
}

template<class T, class ...Args>
void PrintList(const T& val, Args... args) 
{
	std::cout << "<ListVal, 参数包中参数个数> : " << "<" << val << "," << sizeof...(args) << ">" << std::endl;
	PrintList(args...);
}

int main()
{
	std::string s = "zhang";
	PrintList(30, 12.15, s, 'b', 100);
	return 0;
}
图1.1 代码1.1运行结果

如果采用逗号表达式来处理参数包,就需要定义一个int型数组(int a[]),这个int型数组要省略元素个数的声明,因为我们无法知道参数包中参数的具体个数。如代码1.2所示,定义了数组int a[] = { (_printList(args), 0)... },其中_printList(args)会对args的每个元素进行处理,依次将参数包中的每个数据作为参数带入到_printList函数中处理。

(_printList(args), 0)... 相当于依次执行:(_printList(args1), 0)、(_printList(args2), 0)、... 、(_printList(argsn), 0),其中argsi表示参数包的第i个参数。

代码1.2:采用逗号表达式展开参数包

template<class T>
void _PrintList(const T& val)
{
	std::cout << val << std::endl;
}

template<class ...Args>
void PrintList(Args... args)
{
	int a[] = { (_PrintList(args), 0)... };
}

其实,我们也并非一定要采取逗号表达式展开可变参数列表,也可以直接利用数组展开,具体做法为:让_PrintList函数返回int类型的数据,定义int a[] = { _PrintList(args)... }即可。

代码1.3:直接采用数组展开参数包

template<class T>
int _PrintList(const T& val)
{
	std::cout << val << std::endl;
	return 0;
}

template<class ...Args>
void PrintList(Args... args)
{
	int a[] = { _PrintList(args)... };
}

1.3 emplace/emplace_back接口函数 

由于C++11支持了可变模板参数,为此,在STL容器有关插入数据的接口函数中,新增了emplace和emplace_back接口,emplace系列接口函数支持传递可变个数的参数,其在vector容器中的声明见图1.2。

图1.2 vector容器的emplace系列接口函数声明

以vector的emplace_back接口为例,其与push_back的的对比如下:

  • 如果vector中存储的是内置类型数据,那么emplace_back与push_back无论从用法还是底层实现层面,基本一致。
  • 如果vector中存储自定义类型数据,如std::pair<int,int>,那么push_back必须以键值对作为参数,先构造函数std::pair作为函数参数,在调用std::pair的拷贝构造函数,而emplace_back可以直接传两个值作为键值对pair的first成员变量和second成员变量实现构造。即:v.emplace_back(1, 1)是可行的,但v.push_back(1, 1)是被禁止的,必须使用v.push_back(std::make_pair(1,1))显示构造键值对传参。

代码1.4:vector容器的emplace_back接口的使用

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{ }

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	std::vector<std::pair<int, int>> v1;
	v1.emplace_back(1, 1);
	v1.emplace_back(2, 2);
	v1.push_back(std::make_pair(3, 3));
	//v.push_back(1, 1);   //禁止

	std::vector<Date> v2;
	v2.emplace_back(2023, 5, 29);
	v2.emplace_back(2022, 6, 1);
	v2.push_back(Date(2020, 5, 1));

	return 0;
}

二. lambda表达式

2.1 lambda表达式的概念和定义方法

lambda表达式,其实本质上就是一个匿名函数对象,定义lambda表达式的语法为:

  • [captrue-list](parameter)mutable->return_type { statement };

其中,每个部分的意义为:

  • [captrue-list]:捕捉列表,将特定的变量拉取作为lambda表达式这个匿名函数对象的成员变量,使指定的变量可以在lambda表达式的函数体{ statement }中使用。
  • paremeter:参数列表,与普通函数的参数列表一致。
  • mutable:如果lambda表达式不声明mutable,那么就认为它是const属性的成员函数,如果[captrue-list]以值捕捉的方法捕捉特定变量,那么在lambda表达式中就不能对捕捉的变量进行修改。注意:若使用引用捕捉,即使不声明mutable,依旧可以修改引用对象的值。
  • ->return_type:返回值类型,在大部分情况下省略,由编译器自动推导。
  • { statement }:函数的具体实现代码。

代码2.1定义了两个用于加法计算lambda表达式对象add1和add2,用于实现加法计算函数,add2省略返回值类型。还定义了一个利息计算函数函数InterestIncome函数,在捕获列表中以值捕捉的方式拉取利率rate作为成员变量,在函数实现的过程中直接使用rate。

代码2.1:lambda表达式的定义

int main()
{
	auto add1 = [](int x, int y)->int {return x + y; };
	auto add2 = [](int x, int y) {return x + y; };   //两个进行加法计算的lambda表达式

	int x = 10, y = 20;
	int ret1 = add1(x, y);
	int ret2 = add2(x, y);
	std::cout << "ret1 = " << ret1 << std::endl;
	std::cout << "ret2 = " << ret2 << std::endl;

	double rate = 0.05;
	int money = 10000, year = 3;
	auto Interest = [rate](int money, int year) {return rate * year * money; };  //利息计算lambda表达式

	double InterestIncome = Interest(money, year);
	std::cout << "InterestIncome = " << Interest(money, year) << std::endl;

	return 0;
}

2.2 捕捉列表说明

捕捉列表捕捉变量,有值捕捉和引用捕捉两种方式:

  • [变量名]:值捕捉。
  • [=]:采用值捕捉的方式捕捉父类作用域所有变量,包括this指针,父类作用域指lambda表达式所在的函数。
  • [&变量名]:采用引用捕捉的方式捕捉指定变量。
  • [&]:采用引用捕捉的方式捕捉父类作用域所有变量,包括this指针。

关于捕捉列表,有以下几点注意事项:

  1. 可以采用混合方式进行捕捉:[&, a] -- 采用值捕捉的方式捕捉变量a,采用引用捕捉的方式捕捉父类作用域的其它变量。 [=, &a] -- 采用引用捕捉的方式捕捉变量a,采用值捕捉的方式捕捉除a以外父类作用域的所有变量。
  2. 不能重复捕捉:[=, a] -- 以及以值捕捉的方式捕捉了父类作用域的全部变量,则后面的a属于重复捕捉,编译器会报错。
  3. 以值捕捉的方式捕捉变量时,如果不使用mutable进行修饰,那边捕捉的变量不能在lambda表达式内部被修改。
  4. 不能捕捉父类作用域以外的变量。
  5. 对于全局变量,即使不进行捕捉,也可以在lambda表达式中使用。

代码2.2:采用不同的方式捕捉变量

int g = 1;

int main()
{
	int a, b, c, d;
	a = b = c = d = 1;

	//采用值捕捉的方式捕捉父类作用域全部变量
	auto func1 = [=]()
	{
		std::cout << a << b << c << d << std::endl;
	};

	func1();   // 1111

	//采用引用捕捉的方式捕捉父类作用域全部变量
	auto func2 = [&]()
	{
		++a;
		++b;
		++c;
		std::cout << a << b << c << d << std::endl;
	};

	func2();   // 2221

	//混合捕捉:采用引用捕捉的方式捕捉d,值捕捉捕捉父类作用域其他变量
	auto func3 = [=, &d]() 
	{
		++d;
		std::cout << a << b << c << d << std::endl;
	};

	func3();   // 2222

	//混合捕捉:采用值捕捉的方法捕捉a,采用引用捕捉的方法捕捉父类作用域其他变量
	auto func4 = [&, a]()
	{
		++b;
		++c;
		++d;
		std::cout << a << b << c << d << std::endl;
	};

	func4();  // 2333

	//直接在lambda中使用并更改全局变量g的值
	auto func5 = []()
	{
		++g;
		++g;
	};

	func5();
	std::cout << "g = " << g << std::endl;   // g = 3

	return 0;
}
图2.1 代码2.2的运行结果

2.3 lambda表达式的底层实现原理

lambda表达式的本质为匿名函数对象,在底层的实现原理与仿函数类似。对于lambda表达式,编译器在底层实现时会将其这个匿名对象处理成名称为lambda_uuid的类对象,其中uuid为一种字符串生成算法,其多次调用产生相同字符串的概率微乎其微,可以认为每次都生成不同的字符串。lambda_uuid作为函数对象,调用其operator(),执行lambda表达式函数体内的代码。

图2.2 lambda表达式与仿函数的底层实现原理对比

三. 包装器

3.1 function包装

如代码3.1所示,我们定义了一个模板函数UseF,其中包含一个F的模板,F类型的变量f可以作为函数来使用。F可以接收的类型有:函数(函数指针)、仿函数对象、lambda表达式,但是,当F分别作为函数指针、函数对象和lambda表达式传给去实例化UseF时,会实例化出多个对象,即使三种类型的f参数执行完全一样的工作。

我们通过在UseF中定义static int类型的变量count并让其自加,依次用函数指针、仿函数对象、lambda表达式实例化UseF并运行代码,可以看出count的值并不会随着调用次数的增加而改变,因此实例化了多份UseF对象。

代码3.1:

template<class F, class T>
T UseF(const F& f, const T& x, const T& y) 
{
	static int count = 0;
	++count;
	std::cout << "count = " << count << std::endl;

	return f(x, y);
}

int sub(int x, int y)
{
	return x - y;
}

struct Sub
{
	int operator()(int x, int y) const
	{
		return x - y;
	}
};

int main()
{
	int a = 10, b = 3;
	int ret1 = UseF(sub, a, b);    //函数指针调用
	int ret2 = UseF(Sub(), a, b);  //函数对象调用
	int ret3 = UseF([](int x, int y) {return x - y; }, a, b);  //lambda表达式调用

	std::cout << "ret1 = " << ret1 << std::endl;
	std::cout << "ret2 = " << ret2 << std::endl;
	std::cout << "ret3 = " << ret3 << std::endl;

	return 0;
}
图3.1  代码3.1的运行结果

由于F接收3种不同类型的参数会实例化出3份对象,这回造成编译时开销和空间浪费,那么有没有可能,让UseF实例化出一份对象,就能同时接收函数指针、函数对象和lambda表达式作为F的类型。答案是可以的。

通过使用std::function对函数进行包装,就可以将函数指针、函数对象和lambda表达式的实际类型归一化。

std::function进行包装的语法为:std::function<Ret(Agrs...)>,其中Ret为函数返回值的类型,Args为函数的形参列表。std::function<Ret(Args...)>也某种特殊的函数对象类型。

使用std::function要包头文件<functional>

代码3.2将函数指针、函数对象和lambda表达式用std::function进行包装,将包装后的std::function<int(int, int)>对象作为参数调用UseF模板函数,运行代码,可见每次调用count的值都会+1,证明只实例化了一份UseF函数。

代码3.2:std::function封装

template<class F, class T>
T UseF(const F& f, const T& x, const T& y) 
{
	static int count = 0;
	++count;
	std::cout << "count = " << count << std::endl;

	return f(x, y);
}

int sub(int x, int y)
{
	return x - y;
}

struct Sub
{
	int operator()(int x, int y) const
	{
		return x - y;
	}
};

int main()
{
	int a = 10, b = 3;
	
	std::function<int(int, int)> func1 = sub;
	std::function<int(int, int)> func2 = Sub();
	std::function<int(int, int)> func3 = [](int x, int y) {return x - y; };

	int ret1 = UseF(func1, a, b);
	int ret2 = UseF(func2, a, b);
	int ret3 = UseF(func3, a, b);

	return 0;
}
图3.2 代码3.2的运行结果

3.2 bind绑定

std::bind用于对函数参数的修饰,可用于改变参数顺序和改变参数个数。

3.2.1 bind改变参数顺序

假设定义了整数除法运算函数Div:

int Div(int x, int y)
{
	return x / y;
}

在正常情况下,代码Div(a, b)执行的运算是a/b,那么有没有可能,通过DIv(a, b)来计算b/a呢,答案是可以的。通过std::bind绑定,即可改变参数的顺序。

std::bind绑定改变参数顺序,需要用到占位符,占位符被定义在命名空间std::placeholders里,其中_1为一个参数的位置,_2为第二个参数的位置,...

通过std::bind(函数名, 占位参数)即可调整参数顺序,可以采用std::function<Ret(args...)>类型的对象来接收经std::bind绑定生成的函数对象。

代码3.2:std::bind调整参数顺序

int main()
{
	int a = 2, b = 10;
	std::function<int(int, int)> DivOri = std::bind(Div, std::placeholders::_1, std::placeholders::_2);   // DivOri(x,y)执行x/y
	std::function<int(int, int)> DivSwap = std::bind(Div, std::placeholders::_2, std::placeholders::_1);   // DivSwap(x,y)执行y/x

	std::cout << DivOri(a, b) << std::endl;   // 2/10 = 0
	std::cout << DivSwap(a, b) << std::endl;   // 10/2 = 5

	return 0;
}
图3.3 经bind调整后的参数传递情况

如果std::bind要绑定某个类的成员函数,则std::bind的尖括号<>第一个成员必须为&类域::成员函数名,第二个参数为类对象(充当this),从第三个参数开始才是参数占位符,即使不调整参数顺序,占位符也不能省略。

代码3.3:std::bind绑定成员函数

class Sub
{
public:
	int sub(int x, int y)
	{
		return x - y;
	}
};

int main()
{
	std::function<int(int, int)> sub1 = std::bind(&Sub::sub, Sub(), std::placeholders::_1, std::placeholders::_2);
	int ret = sub1(10, 4);
	std::cout << "ret = " << ret << std::endl;  // ret = 6
	return 0;
}

3.2.2 bind调整参数个数 

假设我们要实现这样一颗搜索树,它的节点的Key值对应+、-、*、/四种操作符,根据输入的key值,匹配Value,Value为函数指针、函数对象、lambda表达式的任意一种,用以完成对应的四则运算操作。

我们在设定map的类型时,键值对Value必须给死类型,也就是说,Value的类型不可变,那么即使传经function封装后的对象,也必须保证Ret和args完全一致。但是,某些四则运算函数可能是某个类的成员函数,那么,我们就要采用bind绑定,来调整参数个数。如代码3.4,sub和add为成员函数,都要经function包装处理,然后作为map的Value对map初始化。

bind调整参数个数的本质,就是将某个确定的参数写死,如:调用某个成员函数,显示给定第一个参数为匿名对象,这个匿名对象充当指针。

代码3.4:std::bind绑定改变参数个数

struct Add
{
	int add(int x, int y)
	{
		return x + y;
	}
};

struct Sub
{
	int sub(int x, int y)
	{
		return x - y;
	}
};

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

int main()
{
	std::function<int(int, int)> addFunc = std::bind(&Add::add, Add(), std::placeholders::_1, std::placeholders::_2);
	std::function<int(int, int)> subFunc = std::bind(&Sub::sub, Sub(), std::placeholders::_1, std::placeholders::_2);   //bind绑定改变参数个数

	std::function<int(int, int)> mulFunc = Mul;
	std::function<int(int, int)> divFunc = Div;

	std::map<char, std::function<int(int, int)>> opMap =
	{
		{'+', addFunc}, {'-', subFunc},
		{'*', Mul}, {'/', divFunc}
	};

	int a = 6, b = 3;

	std::string op = "+-*/";
	for (const auto& ch : op)
	{
		if (opMap.find(ch) != opMap.end())
		{
			std::cout << opMap[ch](a, b) << std::endl;
		}
	}
}

std::bind增加的参数,并不一定是类对象,也可能是某个内置类型的变量,或是某个字面常量,代码3.5中定义了函数Interests来计算利息,函数有三个参数:rate表示利率、money表示存钱数、year表示存储年份,先定义两个函数对象InterestsFunc1和InterestsFunc2,通过bind写死rate的值,调用两个函数对象来计算利息时只用给定money和year的值即可。

代码3.5:bind增加参数个数(增加的参数为内置类型变量/字面常量)

double Interest(double rate, double money, double year)
{
	return rate * money * year;
}

int main()
{
	double rate = 0.05;
	double money = 1000, year = 3;

	std::function<double(double, double)> InterestFunc1 = std::bind(Interest, rate, std::placeholders::_1, std::placeholders::_2);
	std::function<double(double, double)> InterestFunc2 = std::bind(Interest, 0.05, std::placeholders::_1, std::placeholders::_2);

	std::cout << InterestFunc1(money, year) << std::endl;  // 150
	std::cout << InterestFunc2(money, year) << std::endl;  // 150

	return 0;
}

四. 总结

  • C++11支持了可变参数模板,语法为:template<class ...Args> func(Args... args) { ... },不可以通过args[i]获取参数包args中参数的值,可以通过递归调用逗号表达式的方式,获取参数包中参数的值。
  • lambda的本质为匿名函数表达式,可用于实现与函数指针和仿函数相同的功能,定义lambda的语法格式为:[captrue-list](parameters)mutable->return_type { statement },其中[capture-list]为捕捉列表,(parameters)为函数参数列表,mutable如果省略则默认lambda表达式为类的const成员函数,return_type为返回值类型,一般省略由编译器自动推导,{statement}为函数体。
  • 捕捉列表[capture-list]捕捉变量的方式可分为值捕捉和引用捕捉,[&]和[=]可分别实现以引用捕捉和值捕捉的方式对父类作用域全部变量的捕捉。还可以存在混合捕捉如[&, a]/[=, a],不可以重复捕捉如[=, a]。
  • lambda表达式的底层实现也是通过operator()成员函数实现的。
  • 通过std::function可以实现对函数指针、函数对象和lambda表达式的封装,经std::function封装后,实现相同功能的函数指针、函数对象和lambda表达式的类型可以相同,统一为:std::function<Rets(args...)>,其中Rets为函数返回值类型,args...为函数参数列表。
  • bind绑定可以实现对函数参数顺序和参数个数的调整。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/585573.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

重估老板电器:加速增长飞轮,迸发品类红利

#王一博同款洗碗机&#xff0c;5月28日&#xff0c;这个话题登上微博热搜&#xff0c;并获得不小关注。数据显示&#xff0c;截至5月29日9:00&#xff0c;该话题一天内引发了166.1万人讨论&#xff0c;阅读量破2.7亿。同时&#xff0c;抖音上&#xff0c;官宣王一博为代言人的话…

Java on Azure 开发工具路线图新发布!

大家好&#xff0c;欢迎来到Java on Azure工具产品的4月更新。让我们首先来谈谈我们对未来几个月的Java on Azure开发工具的投资。在这次更新中&#xff0c;我们还将介绍Azure Service Bus支持和Azure Spring Apps入门模板增强功能。要使用这些新功能&#xff0c;请下载并安装用…

ASEMI单向可控硅BT151参数,BT151封装,BT151体积

编辑-Z 单向可控硅BT151参数&#xff1a; 型号&#xff1a;BT151 存储接点温度范围Tstg&#xff1a;-40~150℃ 工作接点温度范围Tj&#xff1a;-40~125℃ 断态重复峰值电压VDRM&#xff1a;650V 重复峰值反向电压VRRM&#xff1a;650V RMS导通电流IT(RMS)&#xff1a;12…

【P42】JMeter 运行时间控制器(Runtime Controller)

文章目录 一、运行时间控制器&#xff08;Runtime Controller&#xff09;参数说明二、测试计划设计 一、运行时间控制器&#xff08;Runtime Controller&#xff09;参数说明 可以通过时间来确定其后代元素运行多长时间&#xff0c;在时间范围内&#xff0c;后代元素会一直运…

uniapp中根据不同状态跳转不同页面

大纲&#xff1a; 今天我们讲 在uniapp中&#xff0c;如何根据不同的状态跳转到不同的页面。 以下代码&#xff0c;是Tabs标签的展示 &#x1f33f; :list"list" 是参数配置&#xff0c;该参数要求为数组&#xff0c;元素为对象&#xff0c;且对象要有name属性&…

mciSendString函数简介(播放音乐以及录音相关操作)

函数功能&#xff1a;播放多媒体音乐&#xff0c;视频等 mciSendString是用来播放多媒体文件的API指令&#xff0c;可以播放MPEG,AVI,WAV,MP3,等等。这个函数有自己的mci指令&#xff0c;可以通过不同的指令实现不同的功能。这里我会详细讲解mciSendString这个函数的常见用法&…

【Web】HTTP代理和反向代理

直接访问 就是从客户端直接访问服务端&#xff0c;相当于我直接去厂家买可乐&#xff0c;没有中间商赚差价 HTTP代理 HTTP代理指在客户端先访问代理服务器&#xff0c;然后由代理服务器去访问服务端&#xff0c;代理服务器收到响应后再转发个客户端&#xff0c;就像我去商店…

【C++】类与对象——六个默认成员函数、构造函数的概念和特征,析构函数的概念和特征

文章目录 1.类的六个默认成员函数2.构造函数2.1构造函数的概念2.2构造函数的特性 3.析构函数3.1析构函数的概念3.2析构函数的特征 1.类的六个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。   空类中真的什么都没有吗&#xff1f; 并不是&#xff0c;任何…

跨域图像识别

跨域图像识别 跨域图像识别&#xff08;Cross-domain Image Recognition&#xff09;是指在不同的数据集之间进行图像分类或识别的任务。由于不同数据集之间的分布差异&#xff0c;跨域图像识别面临着很大的挑战。 以下是几种代表性的跨域图像识别算法&#xff1a; DDC&#…

利用代码实现自动刷网课阅读时长功能 JAVA

目录 前言&#xff1a;理论依据&#xff1a;现实依据&#xff1a;朴素版只能循环阅读不能翻页&#xff1a;升级版 翻页 阅读&#xff1a;如何使用&#xff1a; 前言&#xff1a; 最近不也快结课了&#xff0c;网课该刷的都要刷掉&#xff0c;最近不就把一门思政课刷完了&#…

粉丝经济:互帮互助,众筹,人人帮我我帮人人

目录 用户精准定位&#xff1a; 用户裂变 用户在线“买卖需要注册&#xff1a;为后期思域流量变现 用户容器“APP&#xff0c;小程序”&#xff1a;用户资产化 LBS(一人千面&#xff0c;个性化定制&#xff0c;根据地理位置进行提醒&#xff1a;优惠券”) 粉丝渠道化&…

信息安全实践1.1(网络嗅探)

前言 这个网络嗅探其实就是用wireshark抓包。那时候赶着做&#xff0c;就随便写了点。参考价值比较少。 第一次实践是因为寒假在家摆烂&#xff0c;然后开学前两天做的&#xff0c;所以质量不是很好。不过也算是一次实践&#xff0c;看看就好。 要求 使用网络嗅探工具抓获网络…

TiDB x Bolt丨超强可扩展性与弹性助力超 1 亿用户畅享出行服务

作者&#xff1a;PingCAP 封小明 通过 TiDB 连接全球极限场景和创新场景&#xff0c;是 PingCAP 长期坚持的国际化战略。目前&#xff0c;在全球已有超过 3000 家企业选择 TiDB。无论在游戏、金融、物流、互联网还是智能制造等行业&#xff0c;基于规模化 OLTP 扩容、实时 HTA…

为什么说企业需要搭建产品手册?

企业需要搭建产品手册的原因有很多&#xff0c;其中包括提高产品使用体验、降低售后服务成本、促进产品销售等。本文将从这些方面来介绍企业为什么需要搭建产品手册&#xff0c;并探讨如何有效地搭建和管理产品手册。 一、提高产品使用体验 产品手册是一份指导用户如何正确使…

【数据结构】二叉树——链式结构的实现(代码演示)

目录 1 二叉树的链式结构 2 二叉树的创建 3 二叉树的遍历 3.1 前序遍历 3.1.1运行结果&#xff1a; 3.1.2代码演示图: 3.1.3 演示分析&#xff1a; 3.2 中序遍历 3.3 后序遍历 3.4 层序遍历 4 判断是否是完全二叉树 5 二叉树节点的个数 5.1 总个数 5.2 叶子节点…

Electron-Builder Windows系统代码签名

前言 项目打包签名是两年前做的了&#xff0c;使用Electron-Bulder&#xff0c;打包工具版本迭代较少&#xff0c;倒是electron版本更新飞快&#xff0c;目前官方推荐使用Electron Forge进行打包&#xff0c;后续再对两者进行对比&#xff0c;重新整理现在的实现方案。 签名简…

微信扫码授权到登录网页,中间究竟发生了什么?

关于我昨天突然接到神秘“面试”&#xff1a;微信扫码授权登录的实现逻辑是神魔&#xff1f;在这个扫码授权的过程中客户端、服务端、微信平台都做了些神魔&#xff1f;二维码里面又有哪些信息&#xff1f; 从手机微信扫一扫二维码到登录个人的知乎账户&#xff0c;中间究竟发生…

智警杯赛前学习1.2--excel统计函数

常用统计函数 count countif&#xff08;区域&#xff0c;条件&#xff09; countifs&#xff08;区域&#xff0c;条件&#xff0c;区域&#xff0c;条件&#xff09; 求和函数 sum sumif&#xff08;区域&#xff0c;条件&#xff0c;[求和区域]&#xff09; sumifs&#xff…

AOP日志功能实现

AOP日志功能实现 1、添加两个工具类2、新建一个接口为 LogAnnotation3、新建一个类 LogAspect4、使用自定义注解 LogAnnotation5、运行结果6、项目结构 转载自b站&#xff1a;码神之路 1、添加两个工具类 HttpContextUtils 用于获取当前请求的 HttpServletRequest 对象&#xf…

Pycharm中安装jupyter 以及一些会遇到的问题

1、确保电脑安装了 anaconda 和jupyter notebook 2、在命令行 启动jupyter Notebook &#xff08;启动后不要关闭这个命令窗口&#xff09; 命令&#xff1a;juputer Notebook 成功运行后的网页界面&#xff1a; 3、打开Pycharm 创建新的项目 &#xff08;注意是Conda) 4、 创…