C++11:右值引用、移动语义和完美转发

news2024/11/14 12:09:33

目录

前言

1. 左值引用和右值引用

2. 引用范围

3. 左值引用的缺陷

4. 右值引用的作用

5. 右值引用的深入场景

6. 完美转发

总结


前言

C++11作为一次重大的更新,引入了许多革命性的特性,其中之一便是右值引用和移动语义。本文将深入探讨其中引入的右值及其相关概念,帮助读者更好地理解这一特性,从而在编程实践中更有效地利用它。


1. 左值引用和右值引用

C++98中有引用的用法,而C++11中新增了右值引用的语法特性,之前学习的引用都是左值引用。不管是左值引用还是右值引用,都是给对象取别名。那什么左值和左值引用?还有右值和右值引用?

左值是一个表示数据的表达式。

  • 具有固定的地址,可以获取它们的地址。
  • 可以出现在赋值符号的左侧,对它进行赋值。
  • 当用const修饰左值时,不能进行赋值,但是可以取地址。

左值引用就是对左值进行引用。

下面的例子中,有整型变量x,指针变量ptr,const修饰下整型变量y,字符串类变量s。上面的变量都是左值,都可以取地址。

int main()
{
	// 以下的x、y、ptr、s都是左值
	// 左值:可以取地址
	int x = 10;
	int* ptr = new int(0);
	const int y = 2;
	string s("11111");

	//左值引用
	int& r1 = x;
	int*& r2 = ptr;
	int& r3 = *ptr;
    const int& r4 = y;
	string& r5 = s;

    return 0;
}

右值是一个表示数据的表达式,如字面常量、临时对象、表达式返回值、函数返回值(左值引用类型返回除外)。

  • 没有固定的内存地址,不能够获取其地址。
  • 可以出现在赋值符号的右边,但是不能出现在赋值符号的左边没有。

右值引用就是对右值进行引用。

int main()
{
    //右值:不能取地址
	double x = 2.5;

	//以下是常见的右值
	15;			     //字面常量
	x + 15;			 //表达式返回值
	fmin(x, y);		 //函数返回值
	string("11111"); //临时对象

	//右值引用
	int&& rr1 = 15;
	double&& rr2 = x + 15;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("1111111");

    return 0;
}

2. 引用范围

左值引用范围

  1. 左值引用一般情况下只能引用左值。
  2. 但加上const修饰之后,左值引用可以引用左值,还可以引用右值。

字面常量15被引用为整型变量时需要转换类型,中间会产生临时变量,临时变量具有常性。如果使用普通引用,可以对此引用进行修改,会导致权限放大。

int main()
{
    //右值:不能取地址
	double x = 2.5;

	//以下是常见的右值
	15;			     //字面常量
	x + 15;			 //表达式返回值
	fmin(x, y);		 //函数返回值
	string("11111"); //临时对象

	//右值引用
	int&& rr1 = 15;
	double&& rr2 = x + 15;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("1111111");

    //const修饰后,可以引用右值
    const int& rx1 = 15;
    const double& rx2 = x + 15;
    const double& rx3 = fmin(x, y);
    const string& rx4 = string("1111111");   

    return 0;
}

右值引用范围:

  1. 右值引用一般情况下只能引用右值。
  2. 但是右值引用可以引用move之后的左值。
int main()
{
	//右值引用
	int&& rr1 = 15;
	
    //无法引用左值,下面会报错
    int x =  10;
    int&& rr2 = x; //error

    //move之后,可以右值引用
    int&& rr3 = std::move(x);

    return 0;
}

3. 左值引用的缺陷

既然已经有左值引用,为什么还要搞出一个右值引用的概念,这是为什么呢?下面是用C++简单实现的string类。如果对字符串类不熟悉,可以转到这篇文章http://t.csdnimg.cn/Znclr。构造函数,拷贝构造函数和赋值重载函数内部都有打印函数原型,方便查看函数调用情况。

#include <assert.h>

namespace Rustle
{
	class string
	{
	public:
		typedef char* iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		typedef const char* const_iterator;

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			return _str + _size;
		}

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

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

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

			reserve(s._capacity);
			for (const auto& ch : s)
			{
				push_back(ch);
			}
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;

				reserve(s._capacity);
				for (auto& ch : s)
				{
					push_back(ch);
				}
			}

			return *this;
		}

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

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				if (_str)
				{
					strcpy(tmp, _str);
					delete[] _str;
				}
				_str = tmp;
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			strcpy(_str + _size, str);
			_size += len;
		}

		//s += 'a'
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

        //s += "11111"
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};
}

左值引用的场景

左值引用给左值取别名。传递函数参数或者返回函数值时,可以减少拷贝,提高效率。

  • TestNonLeftRef函数参数类型是string类,函数参数是实参的临时拷贝,如果是自定义类型,会调用相应的拷贝构造,创建新的变量。
  • 而TestLeftRef函数参数是string类引用,相当于给传过来的参数取别名,不会调用拷贝构造。
  • operator[]函数返回值类型是字符类型的引用,对字符进行取别名,不用进行拷贝。
void TestNonLeftRef(Rustle::string s)
{}

void TestLeftRef(Rustle::string& s)
{}

int main()
{
	Rustle::string s("111111");
	TestNonLeftRef(s);
	TestLeftRef(s);

	Rustle::string s1("xxxxxx");
	//operator[]返回值类型是
	//Rustle::char& operator[](size_t n);
	s1[0];

	return 0;
}

运行结果如下,main函数中创建s变量时调用了构造函数。接受参数为s的两个函数,只有TestNonLeftRef调用了拷贝构造。最后一个构造是创建s1变量调用的。

左值引用的缺陷

当函数返回的值是一个函数内的局部变量时,局部变量出了函数作用域就会被销毁,无法使用左值引用返回,只能进行传值返回。

Rustle::string GetStr(int flag)
{
	Rustle::string str;

	if (flag)
		str += "true";
	else
		str += "false";

    return str;
}

int main()
{
	Rustle::string s1;
	s1 = GetStr(1);

	return 0;
}

如上面代码所示,str是一个局部变量,出作用域就销毁,只能使用传值返回。下图中,一般情况下str变量传值返回时,会拷贝此变量来创建一个临时变量,如果是自定义类型就会调用拷贝构造函数。s1会调用拷贝构造函数,拷贝临时变量

但是某些编译器进行优化之后,只用进行一次拷贝构造函数,不产生中间的临时变量。

 不过VS2022编译器对这个场景优化的十分厉害。下面是运行结果示意图,只调用了一次构造函数。说明编译器已经识别s1要使用GetStr返回值进行构造。相当于str跟s1是同一个变量。

 

但如果先创建string类,再使用赋值重载函数拷贝函数返回值。

int main()
{
	Rustle::string s2;
	s2 = GetStr(0);

	return 0;
}

运行结果如下,调用了两次构造函数和一个赋值重载函数。mian函数内创建一个string类对象,GetStr函数内创建了一个string类对象。在返回该对象时,本来会产生一个临时对象,调用一个拷贝构造,不过编译器优化掉这一步骤,直接调用赋值重载函数。

4. 右值引用的作用

通过上面的讲述,我们知道左值引用不能解决传值返回的场景,大部分编译器起码至少会调用一次拷贝构造,这样会降低运行效率。右值引用的出现就是为了解决这种场景。

  • 右值分为纯右值和将亡值。其中将亡值指的是那些即将被销毁的对象,函数返回值就是将亡值。右值引用可以引用将亡值,虽然左值引用不能当做返回类型,但是右值引用可以当做返回类型。
  • 既然右值引用左为函数返回类型,string类就要重载一份右值引用版本的拷贝构造函数和赋值重载函数。这类函数分别叫做移动构造函数和移动赋值函数。
  • 如下面代码,移动构造函数的本质是将右值的资源转移,或者叫做“窃取”。这样就不用做深拷贝了,所以叫做移动构造,就是转移别人的资源进行构造。
		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

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

增加了移动构造和移动赋值之后,我们测试一下刚才的代码。

Rustle::string GetStr(int flag)
{
	Rustle::string str;

	if (flag)
		str += "true";
	else
		str += "false";

    return str;
}

int main()
{
	Rustle::string s2;
	s2 = GetStr(0);

	return 0;
}

调用了两次构造函数,还有一次移动构造。

5. 右值引用的深入场景

前面我们提到右值引用无法直接引用左值,但是move函数可以将左值转换成右值,从而达到左值被引用。std::move函数实际上对左值没有转移任何资源,只是将左值强制转化成右值。

如下面的代码,string类对象正常使用同类进行拷贝构造,不会影响被拷贝的对象。但是被拷贝对象被move之后,强制转化成右值,就会调用移动版本的构造,窃取s1的资源,将s1置空。

int main()
{
	Rustle::string s1("xxxxxx");

	//调用普通构造函数
	Rustle::string s2(s1);
	s1[0];

    //识别s1为右值,调用移动构造,会转移s1的资源来构造s3
    //那么s1就被置空了,无法用[]访问
	Rustle::string s3(move(s1));
	s1[0]; //error

	return 0;
}

 

左值引用和右值引用本质上都是给变量取别名。我们看下面的代码,s1为左值,通过强制转换成右值。匿名对象本身为右值,也可以通过强制转化成左值。

void func(const Rustle::string& s)
{
	cout << "void func(const Rustle::string& s)" << endl;
}

void func(Rustle::string&& s)
{
	cout << "void func(Rustle::string&& s)" << endl;
}

//左值和右值属性可以互相切换,在数据层没有差别
int main()
{
	//左值
	Rustle::string s1("1111111");
	func(s1);
	//强制转换成右值
	func((Rustle::string&&)s1);

	//右值
	func(Rustle::string("11111111"));
	//强制转换成左值
	func((Rustle::string&)Rustle::string("11111111"));

	return 0;
}

 运行结果如下:

下面是用C++简单实现的list容器,只包含插入函数相关的部分,其他接口函数已经省略。并且还提供了右值引用版本的插入函数。如果对list容器不熟悉,可以看这篇文章http://t.csdnimg.cn/WWsBs。

namespace Rustle
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;

		ListNode(const T& data = T())//匿名对象,调用的是默认构造
			:_next(nullptr)
			,_prev(nullptr)
			,_data(data)
		{}
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;

	public:
		list()
		{
			_head = new Node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		void push_back(const T& x)
        {
			insert(end(), x);
		}

		//移动插入
		void push_back(T&& x)
		{
			insert(end(), x);
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;

			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;

			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;

			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;

			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

	private:
		Node* _head;
	};

我们尝试运行下面的代码。我把代码分为五个部分,每个部分都有对应的解释。

int main()
{
    //1.创建一个list类变量lt
	Rustle::list<Rustle::string> lt;
    
    //2.显示对象插入
	Rustle::string s1("1111111111111111");
	lt.push_back(s1);

    //3.匿名对象
	lt.push_back(Rustle::string("1111111111111111"));

    //4.字符串类隐式类型转换为string类
	lt.push_back("1111111111111111");

    //5.通过move函数强制转化s1为右值
	lt.push_back(move(s1));

	return 0;
}
  • 运行结果如下。第一个string类的构造函数和拷贝构造函数,是创建list容器变量lt调用的。因为list容器有个哨兵位结点,内部不存储有效数据,用来简化list的插入和删除工作。
  • list的默认构造函数会开辟一个ListNode类的结点,ListNode类的构造函数是全缺省的,如果没传参数,会使用缺省值,缺省值是调用string类的默认构造函数。参数列表中拷贝data创建ListNode的_data变量,会调用拷贝构造函数。
		ListNode(const T& data = T())//匿名对象,调用的是默认构造
			:_next(nullptr)
			,_prev(nullptr)
			,_data(data)
		{}

		list()
		{
			_head = new Node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}
  • 正常来说,序号3之后的代码中push_back参数都是右值,应该调用string类中的移动构造才对,但是全部调用的是拷贝构造,进行深拷贝。这是为什么呢?

尝试运行下面的代码,观察结果。

void TestRightRef(Rustle::string&& str)
{
	cout << &str << endl;
}

int main()
{
	TestRightRef(Rustle::string("111111111111111"));

	Rustle::string&& r1 = Rustle::string("1111111111111111");
	cout << &r1 << endl;

    //通过测试,发现右值引用本身属性是左值,那为什么会是左值呢?
	//因为只有右值引用本身的属性是左值,才能传递参数,转移他的资源
	return 0;
}

观察运行结果,发现右值引用本身可以取地址,说明右值引用本身属性是左值。第一种是传递右值参数,TestRightRef函数用右值引用接受,可以取地址。第二种是直接对右值引用。

这两种方式都会导致右值被引用之后退化成左值。只有右值引用本身的属性是左值的情况下,才可以进行赋值,做函数参数和做函数返回值的操作。

所以说,函数用右值引用接收右值时,右值引用的属性会退化成左值。

当使用push_back函数,插入右值对象时,会调用移动插入函数。在移动插入函数中,右值引用参数x本身属性已经退化成左值。而移动插入函数主要是复用了insert函数完成尾插的操作,虽然重载了右值引用版本的insert函数,但是由于x属性为左值,还是会调用到左值引用版本的insert函数。

这就导致string类调用的是拷贝构造函数。

namespace Rustle
{
	template<class T>
	struct ListNode
	{
        //...
		ListNode(const T& data = T())//匿名对象,调用的是默认构造
			:_next(nullptr)
			,_prev(nullptr)
			,_data(data)
		{}
	};

	template<class T>
	class list
	{
    //...
	public:
		//移动插入
		void push_back(T&& x)
		{
			insert(end(), x);
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
            
            //...
			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;

            //...
			return iterator(newnode);
		}
        //...
	};

那我们该怎么解决呢?根据右值被引用之后属性退化成左值的问题,每次函数使用右值引用参数接收右值后,如果需要使用右值引用,就必须使用move函数改变右值引用的属性。

那么list容器的push_back函数中需要加上move函数,insert函数中也要加上move函数,ListNode的构造函数也需要提供一个右值引用版本的。

namespace Rustle
{
	template<class T>
	struct ListNode
	{
        //...
		ListNode(const T& data = T())//匿名对象,调用的是默认构造
			:_next(nullptr)
			,_prev(nullptr)
			,_data(data)
		{}

		ListNode(T&& data)//右值版本
			:_next(nullptr)
			,_prev(nullptr)
			,_data(move(data))
		{}
	};

	template<class T>
	class list
	{
    //...
	public:
		//移动插入
		void push_back(T&& x)
		{
			insert(end(), move(x));//加上move函数
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
            
            //...
			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(move(x));//加上move函数
			Node* prev = cur->_prev;

            //...
			return iterator(newnode);
		}
        //...
	};

运行结果如下:

 

6. 完美转发

前面提到右值引用的属性是左值,所以一旦要使用右值引用,需要使用move函数强制转换属性。如list的push_back函数,一层一层传递右值,每一层都要重载左值引用和右值引用两个版本的函数,十分麻烦。有什么办法可以解决呢?那就要介绍C++11引入的新特性——完美转发。

完美转发(Perfect Forwarding)是C++11中引入的一个特性,它允许在函数模板中,将参数连同其类型信息一起不变地传递给其他函数。这意味着,无论是左值引用还是右值引用,都能保持其原有的引用类型,在传递过程中不会意外地变成左值引用。

使用完美转发时,尖括号里面是放变量类型,圆括号是放变量。

    std::forward<T>(x)

下面的代码就是使用完美转发的效果对比。其中模版中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模版函数可以接收左值和右值,不过第一个模版函数没有使用完美转发,第二个函数使用了完美转发。

void Fun(int& x)  { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void NonPerfectForward(T&& t)
{
	Fun(t);
}

template<typename T>
void PerfectForward(T&& t)
{
	//模版实例化是左值引用,保持属性直接传参给Fun
	//模版实例化是右值引用,右值引用属性会退化成左值,转换成右值属性在传参给Fun
	Fun(forward<T>(t));
}

void Test1()
{
    int a;    
    const int b = 8;

    NonPerfectForward(a);			   // 左值
	NonPerfectForward(std::move(a));  // 右值
	NonPerfectForward(b);             // const 左值
	NonPerfectForward(std::move(b));  // const 右值

	cout << endl;
    PerfectForward(a);			   // 左值
	PerfectForward(std::move(a));  // 右值
	PerfectForward(b);             // const 左值
	PerfectForward(std::move(b));  // const 右值
}

int main()
{
    Test1();

	return 0;
}

 运行结果如下,第一个模版函数接收右值后,右值引用属性退化成左值,调用的还是左值引用类型的函数。第二个函数使用了完美转发,如果模版实例化是左值引用,保持属性直接传参给Fun,如果实例化后是右值引用,会转换成右值属性在传参给Fun。


总结

经过长篇累牍的讲解,相信大家对右值引用和移动语义的概念有了初步的认识。通过对这些特性的学习,我们可以编写出更加高效和精炼的代码。如果亲自上手敲写上述示例代码,会有更加深刻的理解。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

ee192b61bd234c87be9d198fb540140e.png

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

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

相关文章

Unity抖音直播玩法开发流程

前言 近两年直播玩法逐渐新兴起来了&#xff0c;也出现不少质量还不错的作品&#xff0c;比如下列《红蓝对决》《三国全战》等。近期我们也做了一款直播玩法&#xff0c;就此记录下开发流程。 1&#xff0c;申请应用 进入抖音开发者平台&#xff0c;在首页入驻平台。 如果是…

Unity的粒子系统

目录 基础参数与模块 创建与编辑 功能与应用 实例与教程 结论 Unity粒子系统的最新功能和更新有哪些&#xff1f; 如何在Unity中使用Visual Effect Graph创建复杂粒子效果&#xff1f; Unity粒子系统的高级应用技巧有哪些&#xff1f; 在Unity中实现粒子系统时的性能优…

回溯算法(基于Python)

递归 递归(recursion)是一种算法策略&#xff0c;通过函数调用自身来解决问题。"递"指程序不断深入地调用自身&#xff0c;通常传入更小或更简化的参数&#xff0c;直到达到“终止条件”。"归"指触发终止条件后&#xff0c;程序从最深层的递归函数开始逐层…

代码块分类

局部代码块 public class Test {public static void main(String[] args) {{int a 10;}// 执行到此处时候,变量a已经从内存中消失了。 // System.out.println(a);} } 构造代码块 public class Test {private String name;private int age;{// 构造代码块System.out.…

【STM32 Blue Pill编程】-定时器与中断

定时器与中断 文章目录 定时器与中断1、硬件准备及接线2、GPIO配置3、代码实现STM32F103C8 配有四个定时器,分别为 TIM1、TIM2、TIM3 和 TIM4。 它们充当时钟并用于跟踪基于时间的事件。 我们将展示如何使用 HAL 库在 STM32Cube IDE 中对这些定时器进行编程。 本文将涉及如下内…

【网络】抓包工具的使用

抓包工具 文章目录 1.tcpdump抓包1.1安装 tcpdump1.2常见使用 2.wireshark抓包 1.tcpdump抓包 TCPDump 是一款强大的网络分析工具&#xff0c; 主要用于捕获和分析网络上传输的数据包。 1.1安装 tcpdump tcpdump 通常已经预装在大多数 Linux 发行版中。 如果没有安装&#…

常见java OOM异常分析排查思路分析

Java 虚拟机&#xff08;JVM&#xff09;发生 OutOfMemoryError&#xff08;OOM&#xff09;异常时&#xff0c;表示 JVM 在尝试分配内存时无法找到足够的内存资源。以下是几种常见的导致 OOM 异常的情况&#xff1a; 1. Java 堆空间不足 (Java Heap Space) 这种情况发生在 J…

【小球下落反弹】小球自由落下,每次落地后反跳回原高度的一半

一小球从100米高度自由落下&#xff0c;每次落地后反跳回原高度的一半&#xff1b;再落下&#xff0c;求它在第10次落地时&#xff0c;共经过多少米&#xff1f;第10次反弹多高&#xff1f; 使用C语言实现&#xff0c;具体代码&#xff1a; #include<stdio.h>int main(…

wo是如何克服编程学习中的挫折感的?

你是如何克服编程学习中的挫折感的&#xff1f; 编程学习之路上&#xff0c;挫折感就像一道道难以逾越的高墙&#xff0c;让许多人望而却步。然而&#xff0c;真正的编程高手都曾在这条路上跌倒过、迷茫过&#xff0c;却最终找到了突破的方法。你是如何在Bug的迷宫中找到出口的…

HarmonyOs透明弹窗(选择照片弹窗样式)

1.鸿蒙中需要实现一个如下图的弹窗 2.由上图中可以得出&#xff0c;只需要三个Text组件依次向下排列&#xff0c;弹窗背景设置透明即可&#xff0c;弹窗代码如下(仅展示弹窗样式)&#xff1a; /**** 自定义选择图片弹窗** 外部定义需要导出*/ CustomDialog //自定义弹窗 export…

Linux驱动学习之点灯(一)

学习不同的板子我们都是从点灯开始&#xff0c;linux驱动也不例外 驱动开发基础知识 何为驱动&#xff1f; 驱使硬件正常工作的代码就叫做驱动。 在一些mcu里&#xff1a; 无非就是直接操作寄存器&#xff0c;或者用库函数初始化外设&#xff0c;使外设正常工作如初始化iic&…

leetcode13. 罗马数字转整数,流程图带你遍历所有情况

leetcode13. 罗马数字转整数 示例 1: 输入: s “III” 输出: 3 示例 2: 输入: s “IV” 输出: 4 示例 3: 输入: s “IX” 输出: 9 示例 4: 输入: s “LVIII” 输出: 58 解释: L 50, V 5, III 3. 示例 5: 输入: s “MCMXCIV” 输出: 1994 解释: M 1000, CM 900, XC…

一个贼好用的开源导航网站项目——pintree!【送源码】

这两天发现了一个项目&#xff0c;它可以快速的将收藏夹里的网址导出&#xff0c;然后快速生成一个在线的网站。这个项目就是 pintree。 项目简介 Pintree 是一个开源项目&#xff0c;旨在将浏览器书签转换为导航网站。只需几个简单的步骤&#xff0c;就可以将书签转变为美观…

【CAN-IDPS】汽车网关信息安全要求以及实验方法

《汽车网关信息安全技术要求及试验方法》是中国的一项国家标准,编号为GB/T 40857-2021,于2021年10月11日发布,并从2022年5月1日起开始实施 。这项标准由全国汽车标准化技术委员会(TC114)归口,智能网联汽车分会(TC114SC34)执行,主管部门为工业和信息化部。 该标准主要…

集团数字化转型方案(二)

集团数字化转型方案通过整合物联网&#xff08;IoT&#xff09;、大数据分析、人工智能&#xff08;AI&#xff09;和云计算技术&#xff0c;构建了一个全面智能化的业务平台&#xff0c;从而实现了全集团范围内的业务流程自动化、数据驱动决策优化、以及客户体验的个性化提升。…

windows11 安装 Rancher Desktop

从官网下载了最新版的 Rancher.Desktop.Setup.1.8.1.msi 安装包&#xff0c;安装很顺利。 但是安装完&#xff0c;启动时报错 The k3s cache is empty and there is no network connection. 不明所以&#xff0c;网上查了&#xff0c;原来是github访问不了的原因&#xff0c;具…

这些星座比你想象的还努力

TOP 3. 金牛座   金牛座对于操劳操心的忍受度本来就比较高&#xff0c;对于金牛座来说这些都是踏实的象征&#xff0c;金牛座比较不相信不劳而获这件事情&#xff0c;多少血汗多少付出&#xff0c;得到多少收获&#xff0c;这让金牛座比较踏实&#xff0c;不会觉得很不安&…

CE游戏教程第三关解密

游戏规则&#xff1a;雪人可以左右移动&#xff0c;跳跃&#xff0c;跳跃到红线上&#xff0c;红线变绿&#xff0c;所有红线变率时&#xff0c;门开启&#xff0c;雪人通过门后&#xff0c;游戏胜利。如果游戏中触碰到NPC&#xff0c;游戏失败&#xff01; 经过研究&#xff…

DHCP的原理与配置

目录 DHCP的原理 DHCP是什么 DHCP的好处 DHCP的分配方式 DHCP的工作原理 DHCP的配置 环境设置 DHCP配置 验证配置是否成功 DHCP的原理 DHCP是什么 DHCP:Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议。由Internet工作小组开发&#xff0c;专门用…

自然语言处理NLP四范式

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhlNLP四范式概述 自然语言处理(NLP)的四范式是NLP领域发展历程中的重要里程碑,它们代表了NLP技术从传统方法到现代深度学习技术的转变。第一范式是非神经网络时代的完全监督学习,它依赖于人工设…