C++11新特性(右值引用,万能转发)

news2024/10/7 8:30:17

这篇文章是C++的重中之重,通过这篇文章你能体会到C/C++大佬们对性能的极致追求,你能感受到独属C/C++人的浪漫,对高消耗的零容忍,对高性能的不倦探索。右值引用是由Scott Meyers在他的著名书籍《Effective C++》中提出的,因为其重要性,很快就被C++委员会加入到C++11之中,接下来一起探讨右值引用的神奇之处

在阅读此篇文章前,请务必确保你已经知道引用,构造函数,拷贝构造等概念

 

目录

何为左值,右值 

左值引用 

右值引用 

左值引用的作用和不足 

右值引用的作用 

右值引用的无奈

万能转发


何为左值,右值 

引用我们都知道,给变量取别名,能够像指针一样使用变量,但要搞明白左值引用和右值引用,我们得先区分一下何为左值,何为右值。有些同学认为在赋值符号 "=" 右边的就是右值,在其左边的就是左值,这种说法是不准确的,如下例

int a = 10;
int b = 20;
a = b;
//你能说此时的b是一个右值吗?

还有的同学说,能够被修改值的就是左值,不能被修改值的就是右值,这种说法也是不准确的,看下列代码 

const int a = 10;
int b = 20;
a = b;
//此时的a无法被改值,但是你能说a是右值吗?

基于左值右值的一些特性,笔者对左值右值换个容易理解的定义:对于某个变量我们可以获取它的地址+可以对它赋值(被const修饰的无法赋值),那我们可以称其为左值。注意:左值可以出现在 "=" 的右边,但是右值不能出现在 "=" 左边,右值不能被赋值和取地址

常见的右值:字面常量、表达式返回值,函数返回值,将亡值

int a = 10, b = 20, c = 30; 
//a 是左值, 10是字面常量是右值

a = b + c;
//a是左值,(b + c)是个表达式,表达式是右值

a = min(b, c);
//a 是左值,min(b, c)返回值是个右值

左值引用 

我们平时使用的引用基本都是左值引用,在没了解过右值引用概念之前,很少有人闲着去给引用右值,我们平时使用左值引用,一般都是函数传参使用,代替传指针

void swap(int& a, int& b)
{
    int c = a;
    a = b;
    b = c;
}

int main()
{
    int test1 = 10;
    int test2 = 20;
    swap(test1, test2);

    return 0;
}

 如此以来,test1和test2不用传地址过去就能够交换值,这是左值引用常见的用法

// 以下的b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;

如果对左值仍有不了解的地方, 请翻阅笔者前面写的关于左值的文章

右值引用 

左值引用相当于给左值取别名,那么右值引用就是给右值取别名了,这么说也是可以,C++中的右值引用是使用&&来标识,如下列

int&& test_1 = 10;
//test_1引用字面常量10

int x = 10, y =20;
int&& test_2 = x+y;
//test_2引用表达式的值

右值引用不可以引用左值,如果要引用左值,必须使用move()将左值转换成右值  

int test1 = 10;

int&& b = test1;//不可,因为test1是左值,右值无法引用左值

int&& b = std::move(test1) //可以,move()将test1转化成了右值

右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。虽不能取字面量10的地址,但是b引用后,可以取b的地址,也可以修改b。若不想b被修改,可以用const int&& b 去引用,修改b了解一下即可,右值引用的使用场景并不在于此,不重要 

//10是字面常量不可以取地址
//b右值引用10后需要存储,可以取b的地址
int&& b = 10;
cout << (void*)b <<endl;

//也可以修改b的值,但是这没有什么意义
b = 20;
cout << b <<endl; //b的值此时为20

左值引用的作用和不足 

先来看看左值引用的作用,一般我们使用左值引用最多的场景就是函数传参和传返回值,类似于传指针一样,但是用起来比指针舒服多了,指针还要取地址,还要解引用

typedef struct test {
	int a;
	int b;
	int c;
}test;

void fun(test& tmp1, test& tmp2)
{
	//do something
	//...
}

int main()
{
	test t1 = { 10,10,10 };
	test t2 = { 20,20,20 };
	fun(t1, t2);

	return 0;
}

上述代码中,我们通过传左值引用,可以很好的减少传值拷贝带来的消耗,struct 占用空间越大,传左值引用节省的空间和时间就越明显。指针能做到,但是指针没有引用简洁方便

但是传左值引用也有它解决不了的场景,我们看下述代码示例 

vector<vector<int>> test()
{
      vector<vector<int>> tmp(1000);
      for (int i = 0; i < tmp.size(); i++)
          tmp[i].resize(1000);
      
     //do something...
               
      return tmp;
}

int main()
{
	vector<vector<int>> tt;
	tt = test();
	return 0;
}

遇到这样的场景怎么办,tmp是一个非常大的二级vector, 直接返回tmp,这个拷贝消耗太大了,我们来仔细分析一下,如下图

可见,简简单单的一个返回,会带来一次拷贝构造,一次赋值重载,拷贝构造和赋值重载对空间和性能消耗差不多,都是非常恐怖的,何况这是一个二级vector,涉及到深拷贝。

这里我们换一种写法,编译器会进行一次优化,如下图

说了那么多,好像没提到使用左值引用,因为这里不可以使用左值引用

我们来分析如果以传引用的形式返回会发生什么

这样看着传左值引用返回,确实可以节省一半的开销, 但你不要忘了,tmp出了栈之后会立马调用析构给自身的资源给释放了,也就是说临时空间里引用的tmp是一个已经被销毁释放了资源的二级vector,再去赋值给tt,程序会崩溃的

这种写法,和上述的结果一样,tmp是在test这个栈帧里创建的,返回值返回后,该栈帧及tmp对象被销毁,tt在引用一块已经销毁的空间,这是绝不允许的

这种场景有一种解决方法就是使用输出型参数,C语言经常应用这种写法,但是这种方法用起来比较别扭, 不利于对代码的阅读

所谓输出型参数就是说,在test函数里创建的tmp传引用返回后不是会被销毁嘛,那我就提前在main函数里把tmp给创建好,然后传左值引用把tmp传过去,这样连返回值都不需要,就像C语言要改值传指针一样

这种写法别扭,函数参数看起来也很长,如果多几个参数,用起来就让人头大 

上面讨论的场景就是左值引用没法很好解决的场景

而右值引用的出现,就是在填补这一块的不足

右值引用的作用 

还记得我们前面提到常见的右值有字面常量,表达式,函数返回值,将亡值

前面三个都好理解,可是将亡值是什么意思,顾名思义,就是快死去了,这里的快死去是指该值的生命周期要到头了,举个例子

vector<int> test()
{
	vector<int> tmp(1000);

	//do something...

	return tmp;
    //这里的tmp就是将亡值,因为栈帧销毁后,其生命周期即到头
    //编译器会将这里的tmp其识别为右值
}


int main()
{	
	vector<int> tt = test();
	return 0;
}

tmp是将亡值,属于右值,可这有何用处呢?

虽然你是将亡值, 可你tmp身上存着很多要返回的数据(也就是还背着很多的资源),返回的过程就是tmp把自身的资源拷贝给临时变量,然后自身资源释放,临时变量也是如此,临时变量要把资源拷贝给tt,然后自身资源释放

这里的tmp和临时变量都是将亡值,拷贝后就死掉了,拷贝资源的过程是相当消耗计算机时间和空间的,这个过程也太浪费了

既然将亡值都快死了,死了自身资源也是释放,干脆不要进行资源拷贝了,直接把你身上的资源转移给我,tmp内部指向资源的指针交换给临时变量,临时变量也是将亡值,也不要拷贝了,直接把指向资源的指针交换给tt,交换资源的消耗相比拷贝资源不值一提

可见将亡值能玩得一好手偷天换日,乾坤大挪移

不能光说,得实现出来呀,实现这种玩法的关键就是移动构造,移动赋值

我们逐个分析,为了更好的分析这个过程,笔者把曾经实现的string给拿出来,并进行大量删减,把用不到内容都给删掉,先熟悉一下代码,后续分析过程就使用这些代码


	class mystring
	{
	public:

		mystring(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void swap(mystring& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		mystring(const mystring& s)
		{
			mystring tmp(s._str);
			swap(tmp);
            cout << "string(const string& s) -- 深拷贝" << endl;
		}

		// 赋值重载
		mystring& operator=(const mystring& s)
		{		
			mystring tmp(s);
			swap(tmp);
            cout << "string& operator=(string s) -- 深拷贝" << endl;
			return *this;
		}


		~mystring()
		{
			delete[] _str;
			_str = nullptr;
		}


	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; 
	};

	mystring test()
	{
		mystring str = "this is a test";
		return str;
	}

为了测试结果更加清晰,笔者把调用拷贝构造和调用赋值重载的情况都给打印出来,这样运行之后,我们就知道总共进行了几次拷贝,先演示一下没有移动构造和移动赋值的情况

调用了两次拷贝构造,一次赋值重载,为什么是两次拷贝构造呢?因为我们赋值重载的底层实现就是调用拷贝构造,所以就多打印了一次拷贝构造

ps:如果你使用的是vs2022,编译器会对返回值进行优化,看不到上述的情况,这时可以给返回值放到std::move()里面即可取消优化

分析可知,上述返回值是将亡值,临时变量也是将亡值,遇到这种将亡值直接转走资源 

这两次乾坤大挪移的关键就是移动构造和移动赋值,移动构造及移动赋值的定义框架如下 

mystring(mystring&& tmp_obj)
{

}

mystring& operator=(mystring&& tmp_obj)
{

}

移动构造和移动赋值,本质上还是干着拷贝和赋值的工作,只不过以往拷贝构造和赋值重载接收到的对象是const 左值引用,因为不确定这个左值引用接下来是否还继续使用,我们不敢动这个左值引用,只能老老实实一步一步拷贝

但是移动构造和移动赋值接收到的参数是右值引用,右值引用意味着传过来的是一些将亡的,即将要销毁的对象,放心大胆的把它的资源给转走

库中的各个容器也都实现移动构造和移动赋值,以vector为例

库中的实现要考虑非常多的情况,我们仅仅是学习这种思想,怎么简单怎么来,我们自己动手实现简易版的移动构造和移动赋值,实现如下  

        //移动拷贝
		mystring(mystring&& s)
		{
			swap(s);
			cout << "string(const string&& s) -- 移动拷贝" << endl;
		}

		//移动赋值
		mystring& operator=(mystring&& s)
		{
			if (this != &s)
			{
				swap(s);
			}
			cout << "string(const string&& s) -- 移动赋值" << endl;
			return *this;
		}

实现起来是不是非常简单,其实本质就是交换资源,直接swap一下指向资源的指针即可

下图是给mystring类增加移动构造,移动赋值后调用test函数的运行结果 

叫移动构造和移动拷贝都是可以的,如下是添加移动构造和移动赋值后的mystring类  

class mystring
	{
	public:

		mystring(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void swap(mystring& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		mystring(const mystring& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			mystring tmp(s._str);
			swap(tmp);
		}

		// 赋值重载
		mystring& operator=(const mystring& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			mystring tmp(s);
			swap(tmp);

			return *this;
		}

		//移动拷贝
		mystring(mystring&& s)
		{
			swap(s);
			cout << "string(const string&& s) -- 移动拷贝" << endl;
		}

		//移动赋值
		mystring& operator=(mystring&& s)
		{
			if (this != &s)
			{
				swap(s);
			}
			cout << "string(const string&& s) -- 移动赋值" << endl;
			return *this;
		}

		~mystring()
		{
			delete[] _str;
			_str = nullptr;
		}

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; 
	};

右值引用搭配移动构造和移动赋值,补齐了左值引用的短板,如此,C++提升程序效率的能力更近一步,不过右值引用可不仅是用于返回值的处理,接下来看看右值引用的另一个场景

下图代码,我们使用了list,元素类型为我们写的mystring类,并且屏蔽掉了我们写好的移动构造和移动赋值,然后push一个mystring匿名对象

由运行结果可知,mystring这个匿名对象如果作为参数传递给list,在没有移动构造的情况下,就是使用拷贝构造把这个匿名对象拷贝给list中的元素值,然后自身销毁,有了右值的概念,我们一眼就能看出这个匿名对象是一个将亡值,根本没有必要去拷贝,直接把资源转移给list中的元素值,下图是加上移动构造和移动赋值后的调用情况

可以看出,编译器会自动识别出这个匿名类对象是一个将亡值,然后会调用我们写好的移动构造,这可比之前调用拷贝构造的效率高多了

右值引用的无奈

右值引用也有一些无奈的场景

1.待拷贝对象中的成员变量只有浅拷贝

右值引用大显神威的场景就是有深拷贝,需要耗费大量资源去完成拷贝的地方

但是有些场景类本身所占用的内存容量不大,比如一个类中的成员变量是一个数组,这个时候右值引用并不会发挥更高效能,也是只能老老实实去拷贝一个数组,所以想提高程序的效率,成员变量占用内存稍大一点可以考虑换成堆区,而非栈区

2.待拷贝对象不明确是否为一个右值,编译器不敢将其识别为右值,如下图场景

这种场景下,即使test_2我们后续不使用了,但是它并没有明确指定为右值,至少它现在还是一个左值,编译器又不知道你后续是否还使用test_2,所以编译器不敢去调用移动赋值,把test_2的资源给换走,只能老老实实走赋值重载

当然这种场景也是可以解决的,如果你明确test_2不再使用,那么你可以使用std::move(),将test_2给转换为右值,这样编译器就敢去交换了,但是切记,不要乱用,如果你后续某个东西用到了test_2,后悔都来不及,如下图 

使用std::move()将test_2明确转换为右值,编译器就会去调用移动赋值

万能转发

在前面讲述过程中,我们提到了这么一个场景,就是用库中的list去push一个我们实现的mystring匿名类,然后程序就调用了我们的移动构造,这就意味着我们要重载一次push函数,一个push的参数是左值引用,另一个就是右值引用,根据传过来的参数选择调用哪个

现在请大家看这么一个场景   

    void fun(int& x) { std::cout << "左值引用" << endl; }

	void fun(int&& x) { std::cout << "右值引用" << endl; }


	void fun_call(int&& val)
	{
		fun(val);
	}

	int main()
	{
		fun_call(10);
		return 0;
	}

看一下运行结果

怎么回事,怎么调用了左值引用,我们传的参数可是字面常量10呀,仔细分析就可以发现,确实我们传的参数就是字面常量10,也确实被右值引用val给接收了,但不要忘了,右值引用变量val本身是一个左值,它只是接收并绑定一个右值,你把val作为参数是在传左值

这就引出了一个问题,右值身份的中转失效,上列的代码中,我们通过中转函数fun_call中转调用fun,而这一次中转,传过去的右值会被右值引用变量接收,但是右值引用变量本身是一个左值,这就导致右值身份在中转过程中失效

还有一个问题呀,你这个中转函数fun_call()的参数是int&& val,val只能接收右值,不能接收左值,,改成int& val倒是可以接收右值和左值,但是右值会当成左值被中转,就丧失了传右值的作用,有没有办法能够让这个中转函数fun_call()既能接收右值,又能接收左值呢?

有的,C++给我们提供了模板&&引用,看下述代码

    void fun(int& x) { std::cout << "左值引用" << endl; }

	void fun(int&& x) { std::cout << "右值引用" << endl; }

	template <class T>
	void fun_call(T&& val)
	{
		fun(val);
	}

如此以来,val既能接收右值又能接收左值了

这好像没啥变化呀,不就是把前面int给改成了模板类型T了吗?本质不还是右值引用吗?

这里的 T&&val 可不是T类型的右值引用,而是C++模板提供的不确定类型引用

也就是说这个东西不仅可以接收右值引用,还可以接受左值引用,除此之外,还可以接收const左值引用和const右值引用,我们也称其为引用折叠

现在我们解决了fun_call()这个中转函数只能接收固定类型参数的问题

现在我们该解决最初提到的右值中转失效的问题

C++给出的解决方法就是完美转发:std::forward

完美转发在传参的过程中保留对象原生类型属性,也就是说val如果是右值引用,那么使用完美转发后,把val作为参数,就会将其视为右值

val如果是左值引用,那么使用完美转发后,把val作为参数,就会将其视为左值

用咱们之前的程序测试一下结果如何

使用模板和完美转发后,确实解决了我们前面提到的这些问题,这些在程序编写中还是很常用的,希望大家能够掌握

我们前面模拟实现的list,其就涉及到了中转调用的问题,当你发现中转调用,右值引用失效时,那么你可以尝试使用完美转发来解决

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

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

相关文章

【软件测试学习】—软件测试知识点总结(二)

【软件测试学习】—软件测试的分类&#xff08;二&#xff09; 一、软件测试的分类 二、软件的生命周期 三、软件测试的工作流程 四、软件测试用例设计方法 &#xff08;一&#xff09;、等价类划分 定义&#xff1a;等价类划分是一种典型的、重要的黑盒测试的方法&#xff…

Python:如何在一个月内学会爬取大规模数据

Python爬虫为什么受欢迎 如果你仔细观察&#xff0c;就不难发现&#xff0c;懂爬虫、学习爬虫的人越来越多&#xff0c;一方面&#xff0c;互联网可以获取的数据越来越多&#xff0c;另一方面&#xff0c;像 Python这样的编程语言提供越来越多的优秀工具&#xff0c;让爬虫变得…

Python+Tkinter 图形化界面基础篇:多线程和异步编程

PythonTkinter 图形化界面基础篇&#xff1a;多线程和异步编程 引言为什么需要多线程和异步编程&#xff1f;使用多线程多线程示例步骤 1 &#xff1a;导入必要的模块步骤 2 &#xff1a;创建主窗口和按钮步骤 3 &#xff1a;创建下载线程步骤 4 &#xff1a;启动主事件循环 使…

SIT1050,可替代TIJA050,5V 供电,±40V 接口耐压,1Mbps 高速 CAN 总线收发器

SIT1050 是一款应用于 CAN 协议控制器和物理总线之间的接口芯片&#xff0c;可应用于卡车、公交、 小汽车、工业控制等领域&#xff0c;速率可达到 1Mbps &#xff0c;具有在总线与 CAN 协议控制器之间进行差分信 号传输的能力。 特点 ➢ 完全兼容 “ ISO 11898 ” 标…

谷粒商城中消息队列的使用

目录 一、概述 二、步骤 三、说明 四、详细步骤 五、总结 一、概述 在订单服务中使用到了消息队列 具体就是解决关单还有自动解锁库存的功能 其实就是使用消息队列的延迟队列的功能 达到一个定时任务的作用 使用消息队列到达最终一致性的效果 比如说库存 当下单之后 …

超强大的 Nginx 可视化管理平台 Nginx-Proxy-Manager

一、简介 Nginx-Proxy-Manager 是一个基于 Web 的 Nginx 服务器管理工具&#xff0c;它允许用户通过浏览器界面轻松地管理和监控 Nginx 服务器。通过 Nginx-Proxy-Manager&#xff0c;可以获得受信任的 SSL 证书&#xff0c;并通过单独的配置、自定义和入侵保护来管理多个代理…

通过线程池方式改造Stream.parallel()并行流

目录 一、IntStream.rangeClosed并行流二、线程池方式改造1、创建线程池2、线程类3、信心满满&#xff0c;走起来 三、再次解决并发时i原子性问题四、并行流与多线程1、并行和并发的区别&#xff1f;2、并行和并发的使用场景 大家好&#xff0c;我是哪吒。 上一篇简单聊一聊公…

从解决问题到人生规划

从解决问题到人生规划&#xff0c;如何通过深度思考&#xff0c;让自己成为这个世界上最顶级的人才&#xff1f; 我们对于问题的理解一般有6个层次&#xff0c;每个层次的深度不同&#xff0c;决定了我们思考的深度和看问题的眼界。 首先&#xff0c;来想象这样一个场景&#x…

graphviz 绘制二叉树

代码 digraph BalancedBinaryTree {node [fontname"Arial", shapecircle, stylefilled, color"#ffffff", fillcolor"#0077be", fontsize12, width0.7, height0.7];edge [fontname"Arial", fontsize10, color"#333333", arr…

上海亚商投顾:沪指冲高回落 医药、芯片股全天领涨

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 沪指昨日小幅反弹&#xff0c;创业板指盘中涨超1.6%&#xff0c;午后涨幅有所收窄。医药医疗股全线走强&#…

俩个el-select的联动选择

需求&#xff1a; 1.有俩个select下拉框&#xff0c;之后左边选中后右边根据左边的选择自动选择内容 2.右边自动选择之后可以取消。 3.右侧的下拉框只能选中左侧下拉框的内容&#xff0c;左边没选中的右边也不能被选中 4.左侧下拉添加全选功能 5.左侧选择右侧没选择就把右侧数据…

如何避免 IDEA 每次重启都index

如何避免 IDEA 每次重启都index 在 IntelliJ IDEA 中&#xff0c;可以通过以下几个步骤来避免每次重启时索引&#xff1a; 打开 File -> Settings 菜单。在左侧的菜单栏中选择 “Appearance & Behavior” -> “System Settings” -> “Synchronization”。 在右…

与艺术同频!卡萨帝在海外崭露头角

在品牌全球化步伐日益加快的当下&#xff0c;高端品牌如何真正实现业务全球化、品牌全球化乃至用户圈层全球化&#xff1f; 作为国际高端家电引领者&#xff0c;卡萨帝今年以来在全球范围内展开了一系列的品牌布局活动。1月&#xff0c;卡萨帝于巴基斯坦召开品牌发布会&#x…

生产ERP管理系统源码 ERP系统源码

生产ERP管理系统 1、产品管理系统 产品资料系统包括两方面的内容&#xff1a;物料主文件和产品结构&#xff0c;ERP系统企业管理软件平台最基本的信息&#xff0c;绝大多数物流、制造、甚至财务类系统均要使用到产品资料的信息。 &#xff08;1&#xff09;、全方位描述物料…

网工实验笔记:匹配工具ACL的使用

一、概述 访问控制列表简称为ACL&#xff0c;它使用包过滤技术&#xff0c;在路由器上读取第3层及第4层包头中的信息&#xff0c;如源地址、目的地址、源端口和目的端口等&#xff0c;根据预告定义好的规则对包进行过滤从而达到访问控制的目的。ACL分很多种&#xff0c;不同场…

用ChatGPT+Midjourney 5分钟生成30条爆款小红书图文(内有详细教程)

本期是赤辰第35期AI项目教程&#xff0c;文章底部准备了粉丝福利&#xff0c;看完后可免费领取&#xff01;今天给大家讲一下如何5分钟生成30条爆款小红书图文先说个账号&#xff0c;这个应该有同学也看过&#xff0c;前几个月在小红书有个涨粉很快的AI绘画项目&#xff0c;就是…

python自动化操作邮箱

POP3、IMAP、SMTP&#xff0c;CardDAV、CalDAV协议特点 POP3 POP3是Post Office Protocol 3的简称&#xff0c;即邮局协议的第3个版本,它规定怎样将个人计算机连接到Internet的邮件服务器和下载电子邮件的电子协议。它是因特网电子邮件的第一个离线协议标准,POP3允许用户从服…

微信小程序支持h5实现webrtc h264 h265低延迟传输渲染

微信小程序自成体系&#xff0c;自身也带了很强的rtc音视频能力&#xff0c;但是他捆绑了他自己的服务&#xff0c;开发也相对受限于他的api。基于以前的了解可以采webview的方式内嵌h5网址来实现自定义的webrtc.但实践起来并不轻松&#xff0c;主要是小程序的严格限制&#xf…

docker-compose Install hfish

前言hfish HFish是一款社区型免费蜜罐,侧重企业安全场景,从内网失陷检测、外网威胁感知、威胁情报生产三个场景出发,为用户提供可独立操作且实用的功能,通过安全、敏捷、可靠的中低交互蜜罐增加用户在失陷感知和威胁情报领域的能力。 HFish具有超过40种蜜罐环境、提供免费…

VS编译的时候不生成Release文件夹

方法描述&#xff1a; Build>Configuration Manager>Release 编译》配置管理》选择发布版本 再编译就有了 具体操作过程 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 特此记录 anlog 2023年10月12日