C++11新特性(lambda,可变参数模板,包装器,bind)

news2024/11/19 18:43:36

lambda表达式是什么?包装器又是什么?有什么作用?莫急,此篇文章将详细带你探讨它们的作用。很多同学在学习时害怕这些东西,其实都是方便使用的工具,很多情况下我们学这些新的东西觉得麻烦,累赘,其实是实践太少了。初学编程时,C语言基础不扎实,代码没敲过多少,项目没做过。听着别人说C++/Java/Python比C语言功能强大多了,赶紧去学,然而除了多记一堆的语法规则,也没有感觉到好用在哪里

归根到底,实践太少,你没有感受过一个工具带给你的不便,怎会感受到另一个更好工具带给你的舒适呢,刚学的东西还没有实践消化,立马就去学下一个,最后所获寥寥,消耗不多的热情,所以请实践起来

函数指针的不便  

现在有这样一个场景,有进行加法和减法运算的函数add(), sub(),但是我们不能直接调用这两个函数,而是用统一接口fun_call来调用这两个函数,你只需要给出要运算的数字和要进行哪种操作(函数名),有fun_call在内部调用并把结果返回给你

    int add(int a, int b)
	{
		return a + b;
	}

	int sub(int a, int b)
	{
		return a - b;
	}

	int fun_call(int tmp1, int tmp2, int(*op)(int, int))
	{
		return op(tmp1, tmp2);
	}

要实现这种统一调用接口,那我们就得有三个参数,两个是待操作的数字,另一个是进行某种操作的函数的指针,代码实现如上,op就是这个函数指针

 

程序确实可以运行,但函数指针的写法实在不好用,看到函数指针这个参数你可能都要分析半天,如果你觉得还好,那么请看下面这个例子(出自C陷阱与缺陷)

(*(void(*)())0)()

你能分析出这是个什么类型的函数指针吗?

我们根据()的优先级,可将这个表达式分离成两部分

第一部分:(* tmp )() 里面的内容用tmp代替

第二部分:tmp == (void(*)())0

先看第二部分,void(*)()是一个返回值为void,无参的函数指针类型, 那(void(*)())0的意思就是将0强制转换成返回值为void,无参的函数指针

为什么要这么做呢?我们把强制类型转换给去掉,再看一下完整的表达式如下

(*0)(),该语句意思是用0这个地址作为函数指针来调用函数,但是0不能被直接作为函数指针来使用

所以我们要把0强制转换成一个函数指针,这样0就能被作为一个函数指针来使用了

如此以来就出现(*  (void(*)())0  )()这个看起来非常复杂的式子

事实上在操作系统以及C/C++标准库中会出现大量的这种函数指针调用,如果基础不扎实的话,很容易就会被绕的晕头转向

仿函数的出现 

鉴于函数指针这种写者难受,读者也难受的境况,C++推出一个名为仿函数的工具,仿函数笔者有在C++专栏中专门提到过,这里就简单的提一提,不再赘述

仿函数的解决方法就是把函数封装在类里面,并在类中重载(),这样示例化出一个对象,就可以直接当成函数来使用了,在把函数作为参数传递时,也不需传函数指针了,而是把这个类的对象传过去就可以了,如此以来就摆脱了分析函数指针的痛苦

    class ADD {
	public:
		int operator()(int a, int b)
		{
			return a + b;
		}
	};

	class SUB {
	public:
		int operator()(int a, int b)
		{
			return a - b;
		}
	};
	
	template<class T> 
	int fun_call(int tmp1, int tmp2, T op)
	{
		return op(tmp1, tmp2);
	}

如上述代码,把add函数和sub函数分别封装在两个类中,形成两个仿函数,接下来就可以通过实例化出对象来调用这两个函数,传参的时候呢,也是直接传对象过去

 

lambda的出现 

仿函数的出现一定程度上减轻了传函数指针的痛苦,但是这种痛苦是由分析C语言复杂的函数指针转换成对函数进行类封装的繁琐,每写一个简单功能的函数就得封装成一个类,用着用着大家就觉得仿函数还是不够方便

于是C++11就推出了lambda表达式,lambda表达式并不是C++独创的,而是其它语言先提出来的,C++委员会的人觉得lambda表达式用起来很方便,就借鉴到C++中了,所以lambda表达式看起来并不像C++的语法风格,但习惯后用起来还是很方便的

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

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


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


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


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


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

是不是看起来挺复杂的,其实这么多东西只有捕捉列表和函数体是必须的,其他则可以根据自己的需求选择性增添,所以最简单的lambda表达式就是 []{} ,表示什么都不做

接下来笔者逐步演示lambda表达式该如何使用,还拿我们前面的add和sub函数来举例


    int main()
	{
		int val_1 = 20;
		int val_2 = 30;
		auto fun = [](int v1, int v2) {return v1 + v2;}
        int result = fun(val_1, val_2);
		cout << result << endl;
		return 0;
	}

先来分析[ ](int v1, int v2) {return v1 + v2;}

[ ]用来告诉编译器这是一个lambda表达式,当然也有别的作用,我们后面再提

(int v1, int v2)就是参数列表,这个和普通函数的参数列表是一样的,接收传过来的参数

{return v1+v2;} 这个就是函数体,也就是函数具体要干什么事,我们这里要干的事就是将参数列表中的v1和v2相加然后返回

其实把[ ]去掉可以看出,lambda表达式就是一个函数定义嘛,只不过这个函数没有函数名和返回值的类型,但实际上lambda表达式的底层实现和仿函数一样,也是一个类,这也是为什么我们用auto fun来接收lambda表达式

但是请注意,lambda表达式虽然是一个类,但是没有具体的类名,这一点和仿函数很不一样,因为仿函数的类是我们自己封装的,所以我们知道具体的类名

但是lambda表达式是由编译器负责将其封装成一个类,编译器为了保证其唯一性,用随机哈希码来代表其类名,所以我们是不知道其具体的类名的,必须用auto来推导

这样看其实也能够很好理解lambda表达式,我们不是嫌弃仿函数自己手动封装类太麻烦了嘛,lambda表达式就是让编译器自动帮我们封装,我们只要把函数的具体实现传过去就可以了,不过lambda表达式的功能比仿函数还是要强大很多的,我们后面慢慢说

先看看上面程序的运行结果 

 

lambda表达式会自动帮我们推导返回值的类型,所以我们没有在lambda表达式中加上返回值的类型,如果你非想要指定具体的返回值类型,使用表达式中 ->returntype参数 

 

上述代码在lambda表达式中加上了->double,表明指定返回值类型为double类型,从result变量auto的结果可以看出,确实为double类型

接下来看看lambda表达式强于仿函数的功能,看下列代码及运行结果 

    int main()
	{
		int val_1 = 20;
		int val_2 = 30;
		auto fun = [val_1, val_2]{return val_1 + val_2; };
		int result = fun();
		cout << result << endl;
		return 0;
	}

 

什么情况?这个lambda表达式竟然可以直接使用main函数内的变量val_1, val_2

别慌,这就是捕捉列表的作用,仔细看可以发现笔者在[ ]里加上了变量名 val_1, val_2,这表示捕捉父作用域的变量 val_1, val_2(父作用域指包含lambda函数的语句块),也就是说一些情况下lambda表达式可以不用传参,而直接使用捕捉,如下是关于捕捉列表的相关写法

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

咱们看看以引用的形式捕捉

    int main()
	{
		int val_1 = 20;
		int val_2 = 30;
		cout << "交换前:val_1 = " << val_1 << " val_2 = " << val_2 << endl;
		
		auto fun = [&val_1, &val_2] {std::swap(val_1, val_2); };
		fun();
		cout << "交换后:val_1 = " << val_1 << " val_2 = "<< val_2 << endl;
		return 0;
	}

使用捕捉列表需要注意的相关事项

1.捕捉列表不允许变量重复传递,否则就会导致编译错误
如代码[=, val_1]{},已经使用 '=' 全部传值捕捉,后面又传值捕捉val_1,此时会编译报错

2.传值捕捉可以和传引用捕捉配合使用

如代码[=, &val_1] {},表示除val_1传引用捕捉外,其余都传值捕捉 

3.在块作用域以外的lambda函数捕捉列表必须为空

4.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错

5.lambda表达式之间不能相互赋值,即使看起来类型相同,因为是两个不同的类嘛,肯定不能直接进行赋值转换

到目前为止,我们基本把lambda表达式语法格式用了一遍,但是好像mutable还没有提过,接下来我们看看mutable的用法,看下列代码

可以发现,传值捕捉val后,我们没有办法去修改val,这是因为lambda默认是一个const函数,也就是传值捕捉后的变量具有常性,无法修改,加上关键字mutable可取消常性 

 

加上mutable后,确实可以修改val的值了,但程序打印val的值仍为20,这是因为lambda是传值捕捉,在lambda内部修改不会影响外部的val

所以mutable适合那种想捕捉借用父作用域的某个局部变量,并对其进行修改满足自己的要求,但是不希望修改后影响原父作域的这么一个场景  

lambda表达式底层实现 

前面提到过,lambda表达式的底层实现就是类,和仿函数一样,用类封装函数来直接调用,接下来我们用汇编源码来看看究竟是不是这么一回事

	class ADD {
	public:
		int operator()(int x, int y)
		{
			return x + y;
		}
	};
	

	int main()
	{
		int tmp1 = 10;
		int tmp2 = 20;
		
		ADD fun_1;
		auto fun_2 = [](int x, int y) {return x + y; };

		//仿函数调用
		int fun_1_ret = fun_1(tmp1, tmp2);

		//lambda调用
		int fun_2_ret = fun_2(tmp1, tmp2);

		return 0;
	}

 调试后查看这段代码的汇编代码

通过汇编代码可以看出,lambda表达式的底层确实和仿函数一样,都是将函数封装成一个类,然后重载(),只是lambda的类名对我们不可见,编译器才能够识别

可变参数模板 

日常使用模板都是几个模板类型,C++11之后推出了可变模板参数,也就是模板类型的个数不需要再固定个数,而是想传几个就传几个

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

怎么知道是不是想传几个就传几个呢?可以使用如下语句计算参数包里的参数个数

    template <class ...Args>
	void ShowList(Args... args)
	{
        cout << sizeof...(args) <<endl;
    }

    //计算args参数包中参数的个数

参数个数确定正确,不见得参数内容就是我们传过去的,所以接下来我们把参数包展开,看看是不是我们传过去的参数(可变参数模板参数包展开的语法比较晦涩,难以理解,并不需要掌握,大家了解一下即可) 

	//不可省略该函数,不然无法展开
	void ShowList()
	{
		cout << endl;
	}
	
	template <class T, class ...Args>
	void ShowList(T val, Args... args)
	{
		cout << val << "  ";
		ShowList(args...);
	}

	int main()
	{
		ShowList(10, 7.5, 'Y', string("hello"));
		return 0;
	}

 

这个语法很晦涩难懂,简单理解成递归式的调用,val每次接受参数包的一个参数,将其打印出来,然后参数包个数减1继续向下调用,直到参数包的个数为0,展开结束

可变参数模板的参数包展开还有一种方法,不过那个语法更加晦涩难懂,笔者就不展示了

如果从事库函数的开发的话,可变参数模板是个非常有用的工具,举个参数模板在容器中应用的例子,就以list为例,去官网可以查到list在C++11中多了一些成员函数

 

以emplace_ back()为例,这个成员函数是干嘛的呢?其实干得事情和push_back()是一样的,就是尾插元素,那有了push_back为何还要再多一个emplace_back()呢?

看笔者慢慢分析 

list<string> test;

test.push_back(string("hello workd");

上述代码就是我们把一个匿名string给push到list中,整个过程就是用"hello workd"去构造这个匿名类,然后这个匿名类会被识别为右值,调用移动构造,转移资源给list中的string,这个过程主要有两部分的资源消耗,一是构造匿名类,二是调用移动构造

list<string> test;

test.emplace_back("hello workd");
//emplace支持这种写法

使用emplace_back后,就不需要去创建一个匿名类了,而是可以直接把参数给传过去,怎么做到的呢?可以发现emplace_back的实现用到了可变参数模板, 也就是说,"hello world"会作为一个参数存放在可变参数模板包中,然后编译器会用参数包中的参数去直接构造list中存放的string类,不需要我们先创建一个匿名类作为载体了

相比于push_back(),使用emplace_back()可以减少一次移动构造操作,所以说在传右值的时候 emplace_back()会比push_back()高效那么一些。真说高效多少也不见得,毕竟深拷贝下移动构造消耗资源可忽略,但若不涉及深拷贝,那还是不错的,能省一次浅拷贝呢

包装器  

有了函数指针,仿函数,lambda三个传递函数的法宝,大家各取所需,有些情况下仿函数好用些,有些情况下lambda好用些,但是用多了就容易乱。于是C++委员会就考虑设计一种统一的接口,这个接口既可以接收函数指针,仿函数,也可以接收lambda,我们可以用这种统一的接口来调用函数指针,仿函数,及lambda,这个接口就是包装器

看一下包装器的定义

std::function在头文件<functional>

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

模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

包装器的本质是一个模板类,把函数指针,仿函数,lambda等类型再进行一次包装从而达到统一调用的目的,使用包装器要包含头文件<functional>,使用时和使用模板类一样

function< 返回值类型 (参数1类型,参数2类型...)>  类名 = (函数指针/仿函数/lambda/类成员函数); 下面提供了包装器的使用示例

    void fun_ptr(int x, int y)
	{
		cout << "函数指针: " << x+y << endl;
	}

	class FUN {
	public:
		void operator()(double x, double y)
		{
			cout << "仿函数: " << x + y << endl;
		}
	};

	int main()
	{
		//把函数指针传给包装器,该函数无返回值,两个int参数
		function<void(int, int)> fun_1 = fun_ptr;
		//通过包装器使用函数指针指向的函数
		fun_1(10, 10);

		//把仿函数传给包装器,该函数无返回值,两个double参数
		function<void(double, double)> fun_2 = FUN();
		//通过包装器使用仿函数封装的函数
		fun_2(20.5, 20.5);

		//把lambda表达式传给包装器,该函数无返回值,两个longlong参数
		function<void(longlong, longlong)> fun_3 = [](longlong x, longlong  y) 
		{ cout << "lambda表达式: " << x + y << endl; };
		//通过包装器使用lambda表达式
		fun_3(30, 30);
		
		return 0;
	}

包装器看着定义比较复杂,实际用起来还是挺简单的,建议大家在实际开发过程中多使用包装器,因为包装器能够有效的减少代码膨胀,具体示例如下

    void fun_ptr(int x, int y)
	{
		cout << "函数指针: " << x+y << endl;
	}

	class FUN {
	public:
		void operator()(int x, int y)
		{
			cout << "仿函数: " << x + y << endl;
		}
	};

	template<class fun>
	void call_fun(fun f, int tmp1, int tmp2)
	{
		f(tmp1, tmp2);
	}
	

	int main()
	{
		call_fun(fun_ptr, 10, 10);
		call_fun(FUN(), 10, 10);
		call_fun([](int x, int y){ cout << "lambda表达式: " << x + y << endl; }
		, 10, 10);	
		return 0;
	}

学过模板我们,仔细分析都能够明白,call_fun函数会被实例化出三份,因为函数指针,仿函数,lambda是三种不同的类型,模板要为每一种类型都实例化出一份call_fun(),三个函数都是一样的功能,就因类型不同而被实例化出三份,导致代码膨胀

使用包装器就可以解决这个问题,因为函数指针,仿函数,lambda传给包装器后,类型就变为了包装器类,此时传给模板函数,会识别出三个都是包装器类型,只实例化一份

包装器可不仅能包装函数指针,仿函数,lambda表达式,还能包装类内成员函数 

    class TEST {
	public:
		static void test_fun_static(int x, int y)
		{
			cout << "类内静态成员函数: " << x + y << endl;
		}

		void test_fun(int x, int y)
		{
			cout << "类内成员函数: " << x + y << endl;
		}
	};

	
	int main()
	{
		function<void(int, int)> fun_1 = &TEST::test_fun_static;
        fun_1(10,20);

		function<void(int, int)> fun_2 = &TEST::test_fun;
        fun_2(10,20);
		return 0;
	}

但是在编译器上,你会发现,无法直接用包装器封装test_fun函数,而可以封装test_fun_static函数,这是因为类内成员函数隐藏了一个this指针参数,而类内静态成员则不含this指针

缺this指针得补上呀,故而在封装类内非静态成员时还要加一个类名模板参数,然后在调用时传递一个匿名类,修改如下

    function<void(TEST,int, int)> fun_2 = &TEST::test_fun;
    fun_2(TEST(), 10, 20);

可能有些同学会觉得不对劲,仿函数和lambda不也是类嘛,为什么可以直接将仿函数和lambda传给包装器而没有this指针的问题呢?这是因为你传仿函数和lambda传的是一个对象呀,调用函数是通过对象中重载的()来完成,传对象要什么this指针,但你传类内成员函数就不一样了,传的是一个函数,需要考虑隐含的this指针参数

bind

接下来时此篇文章最后一部分内容,bind是一个函数模板,起到了适配器的作用,接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表

什么意思呢?咱们简单理解,前面不是使用包装器去封装类内成员函数嘛,但是类内成员有个隐藏的this指针,所以不得不在包装器模板参数列表中又加了一个类名,如代码

function<void(TEST,int, int)> fun_2 = &TEST::test_fun;

但是TEST这么一加,就会把包装器的书写格式又给破坏了,调用的时候还要给一个匿名类,看着就很突兀,这个时候我们可以使用bind,充当适配器的功能,将其从新适配回funcion<void(int, int)>这种统一的格式

调用bind的一般形式:auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一个可调用对象,callable是要被bind的对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数

注意arg_list不是给定具体的参数类型,而是用占位符placeholders::_n表示,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”

先看一个简单的绑定函数指针的例子,了解绑定的用法后再解决上面问题 

    void fun_ptr(int x, int y)
	{
		cout << "函数指针: " << x + y << endl;
	}

	int main()
	{		
        //绑定fun_ptr函数
		auto fun_1 = std::bind(fun_ptr, placeholders::_1, placeholders::_2);
		fun_1(10, 20);

        //将fun_1再赋给包装器
        function<void(int, int)> fun = fun_1;
		return 0;
	}

在上面的代码中,因为fun_ptr有两个参数,因此我们使用placeholders::_1, placeholders::_2给适配后的对象fun_1占两个参数位,在调用fun_1时,fun_1会调用fun_ptr,并将占位符中的参数传给fun_ptr

来看看bind如何使调用类内成员函数时的格式统一,示例如下

class TEST {
	public:
		static void test_fun_static(int x, int y)
		{
			cout << "类内静态成员函数: " << x + y << endl;
		}

		void test_fun(int x, int y)
		{
			cout << "类内成员函数: " << x + y << endl;
		}
	};
	

	int main()
	{		
		auto fun_1 = std::bind(&TEST::test_fun, TEST(), placeholders::_1, placeholders::_2);
        //如此便可以直接赋给包装器
		function<void(int, int)> fun_2 = fun_1;
		return 0;
	}

使用bind绑定类内成员时,除了要给定具体的类内成员,还要明确绑定的对象(可以给匿名对象),绑定完成后,就会将其适配成统一格式,可以直接赋给包装器

除此之外,bind还有其他的作用,以函数指针为例

    void fun_ptr(int x, int y)
	{
		cout << "我是参数x: " << x << endl;
        cout << "我是参数y: " << y << endl;
	}

	int main()
	{		
        //绑定fun_ptr函数
		auto fun_1 = std::bind(fun_ptr, placeholders::_1, placeholders::_2);
		fun_1(10, 20);

        //如果把placeholders::_1, placeholders::_2位置对换则传过去的参数位置也会换
        auto fun_2 = std::bind(fun_ptr, placeholders::_2, placeholders::_1);
        fun_2(10, 20);

		return 0;
	}

 这是因为placeholders::_1就绑定到第一个参数了,placeholders::_2就绑定到第二个参数上了,即使你给其交换位置,placeholders::_1接收到20后,还是会把它传给x,因为x是第一个参数,还有一个玩法如下

void fun_ptr(int x, int y)
	{
		cout << "我是参数x: " << x << endl;
        cout << "我是参数y: " << y << endl;
	}

	int main()
	{		
        //绑定fun_ptr函数且将fun_1的参数绑定具体的值
		auto fun_1 = std::bind(fun_ptr, 100, 200);
		fun_1(10, 20);

        //绑定fun_ptr函数且将fun_2的参数绑定具体的值
        auto fun_2 = std::bind(fun_ptr, 300, 400);
        fun_2(10, 20);

		return 0;
	}

 

 至此,此篇文章结束,希望大家多敲一敲,把这些工具都用起来

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

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

相关文章

自学嵌入式多久才可以达到找工作的水平

自学嵌入式多久才可以达到找工作的水平 时间以及达到嵌入式工作水平所需的具体努力因人而异。但一般而言&#xff0c;自学嵌入式系统开发需要时间和毅力。以下是一些关键因素&#xff0c;影响着您能够在多久内达到找工作的水平&#xff1a;最近很多小伙伴找我&#xff0c;说想要…

YOLOv4 论文总结

贡献&#xff1a; 1.有效且强大的模型&#xff0c;常规GPU&#xff08;1080ti or 2080ti&#xff09;可得到实时、高质量的检测结果。 2.在训练中&#xff0c;验证 Bag-of-Freebies 和 Bag-of-Specials 方法 3.提出了两种数据增强手段&#xff0c;马赛克和自对抗训练&#x…

LeetCode【74】搜索二维矩阵

题目&#xff1a; 代码&#xff1a; public static boolean searchMatrix(int[][] matrix, int target) {int rows matrix.length;int columns matrix[0].length;// 先找到行&#xff0c;行为当前行第一列<target&#xff0c;当前行1行&#xff0c;第一列>targetfor…

详细教程:Postman 怎么调试 WebSocket

WebSocket 是一个支持双向通信的网络协议&#xff0c;它在实时性和效率方面具有很大的优势。Postman 是一个流行的 API 开发工具&#xff0c;它提供了许多功能来测试和调试 RESTful API 接口&#xff0c;最新的版本也支持 WebSocket 接口的调试。想要学习更多关于 Postman 的知…

Linux友人帐之环境变量

一、环境变量 1.1 环境变量的概念 1. 什么是环境变量&#xff1f; 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。 2. 为什么会有环境变量&#xff1f; 在Linux系统中&#xff0c;我们发现我们在执行一些指令时&#xff0c;比如…

10个打工人必备AI神器,升职加薪靠AI

HI&#xff0c;同学们&#xff0c;我是赤辰&#xff0c;本期是第18篇AI工具类教程&#xff0c;文章底部准备了粉丝福利&#xff0c;看完后可领取&#xff01;1. Runway&#xff08;文字转视频AI工具&#xff09; 只需要一句提示词就能精确生成你所想象的视频场景&#xff0c;还…

natapp内网穿透-将本地运行的程序/服务器通过公网IP供其它人访问

文章目录 1.几个基本概念1.1 局域网1.2 内网1.3 内网穿透1.4 Natapp 2.搭建内网穿透环境3.本地服务测试 1.几个基本概念 1.1 局域网 LAN&#xff08;Local Area Network&#xff0c;局域网&#xff09;是一个可连接住宅&#xff0c;学校&#xff0c;实验室&#xff0c;大学校…

百乐钢笔维修(官方售后,全流程)

文章目录 1 背景2 方法3 结果 1 背景 在给钢笔上墨的途中&#xff0c;不小心总成掉地上了&#xff0c;把笔尖摔弯了&#xff08;虽然还可以写字&#xff0c;但是非常的挂纸&#xff09;&#xff0c;笔身没有什么问题&#xff0c;就想着维修一下笔尖或者替换一下总成。 一般维…

Vulnhub系列靶机---Raven: 2

文章目录 信息收集主机发现端口扫描目录扫描用户枚举 漏洞发现漏洞利用UDF脚本MySQL提权SUID提权 靶机文档&#xff1a;Raven: 2 下载地址&#xff1a;Download (Mirror) 信息收集 靶机MAC地址&#xff1a;00:0C:29:15:7F:17 主机发现 sudo nmap -sn 192.168.8.0/24sudo arp…

操作系统中的(进程,线程)

操作系统是一个搞管理的软件&#xff0c;它对上给各个应用程序提供稳定的运行环境&#xff1b;对下管理各种硬件设备。 进程 一个操作系统由内核和配套的应用程序组成。而进程就是操作系统内核中众多关键概念中的一个。进程通俗一点来讲就是一个已经跑起来的程序。 每个进程…

【数据结构与算法】二叉树的镜像实现

需求分析&#xff1a; 将所有节点的左子树与右子树交换&#xff0c;以达到交换前与交换后成为镜像的效果。 如图&#xff1a; 实现代码&#xff1a; 先准备一个二叉树具有节点类&#xff0c;添加左右子节点的方法和层序遍历方法。 /*** author CC* version 1.0* since2023/10…

数学术语之源——“齐次(homogeneity)”的含义

1. “homogeneous”的词源 “homogeneous”源自1640年代&#xff0c;来自中古拉丁词“homogeneus”&#xff0c;这个词又源自古希腊词“homogenes”&#xff0c;词义为“of the same kind(关于同一种类的)”&#xff0c;由“homos”(词义“same(相同的)”&#xff0c;参见“ho…

msvcr110dll是干嘛的,win系统提示缺少msvcr110.dll解决步骤分享

今天&#xff0c;要和大家探讨一个非常重要的话题——由于找不到msvcr110.dll无法执行代码的五种解决方案。首先&#xff0c;请允许我为大家简要介绍一下msvcr110.dll这个文件。 msvcr110.dll是Visual Studio 2012的一个动态链接库文件&#xff0c;它是Microsoft Visual C 2012…

如何报考产品总监认证(UCPD)?

从产品经理到产品总监&#xff0c;是我们职业生涯中锦鲤化龙的一次历程。中、高级管理人员所需要的知识和能力常常会泾渭分明&#xff0c;甚至大相迳庭。所以&#xff0c;当我们走向高级管理岗位前&#xff0c;尤其是有机会应聘大厂总监岗位时&#xff0c;我们需要一张产品总监…

ESP32网络开发实例-从SPIFFS加载Web页面文件

从SPIFFS加载Web页面文件 文章目录 从SPIFFS加载Web页面文件1、应用介绍2、软件准备3、硬件准备4、Web页面代码与SPIFFS文件系统上传4.1 Web页面代码实现4.2 Web页面代码上传5、Web服务器代码实现在文中,将展示如何构建一个 Web 服务器,为存储在 ESP32 的SPIFFS文件系统中的 …

sklearn处理离散变量的问题——以决策树为例

最近做项目遇到的数据集中&#xff0c;有许多高维类别特征。catboost是可以直接指定categorical_columns的【直接进行ordered TS编码】&#xff0c;但是XGboost和随机森林甚至决策树都没有这个接口。但是在学习决策树的时候&#xff08;无论是ID3、C4.5还是CART&#xff09;&am…

使用 GitHub Action 自动更新 Sealos 集群的应用镜像

在 IT 领域&#xff0c;自动化无疑已成为提高工作效率和减少人为错误的关键。Sealos 作为一个强大的云操作系统&#xff0c;已经为许多企业和开发者提供了稳定可靠的服务。与此同时&#xff0c;随着技术不断发展&#xff0c;集成更多的功能和服务变得尤为重要。考虑到这一点&am…

【学习笔记】项目进行过程中遇到有关composer的问题

composer.json内容详解 以项目中的composer.json为例&#xff0c;参考文档。 name&#xff1a;composer包名type&#xff1a;包的类型&#xff0c;project和library两种keywords&#xff1a;关键词&#xff0c;方便别人在安装时通过关键词检索&#xff08;没试过&#xff0c;好…

成为一个黑客要多久?

一个暑假能成为黑客吗&#xff1f;资深白帽黑客告诉你答案&#xff0c;如果你想的是能到阿里五角大楼内网四处溜达&#xff0c;但是不可能的&#xff0c;但是成为一个初级黑客还是绰绰有余&#xff0c;你只需要掌握好渗透测试、外攻防、数据库等基本内容&#xff0c;搞懂外部安…

探索云原生技术之容器编排引擎-Kubernetes/K8S详解(9)

❤️作者简介&#xff1a;2022新星计划第三季云原生与云计算赛道Top5&#x1f3c5;、华为云享专家&#x1f3c5;、云原生领域潜力新星&#x1f3c5; &#x1f49b;博客首页&#xff1a;C站个人主页&#x1f31e; &#x1f497;作者目的&#xff1a;如有错误请指正&#xff0c;将…