【C++】C++11新特性之右值引用与移动语义

news2024/12/18 15:06:47

文章目录

  • 一、左值与左值引用
  • 二、右值与右值引用
  • 三、 左值引用与右值引用比较
  • 四、右值引用使用场景和意义
    • 1.左值引用的短板
    • 2.移动构造和移动赋值
    • 3.STL中右值引用的使用
  • 五、万能引用与完美转发
    • 1.万能引用
    • 2.完美转发

一、左值与左值引用

在C++11之前,我们把数据分为常量和变量,在C++11之后,我们将数据分为左值和右值

此外,传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,还可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边,定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址,如下:

// 以下的p、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;
int& pvalue = *p;

二、右值与右值引用

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址,如下:

double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;

右值引用就是对右值的引用,给右值取别名

// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

注意事项:

1.为什么函数返回值是右值:当函数返回的是一个局部变量的时候,因为局部变量出了函数作用域生命周期就会结束,所以返回时会将该变量拷贝到寄存器中,然后返回这个寄存器中的内容,而寄存器中的变量是临时变量,临时变量具有常量,属于右值。其实在函数建立栈帧的时候,不仅会有参数的压栈,还会有返回值的压栈,即在两个函数的栈帧之后的一个空间在存贮函数的返回值。通过这个中间值将函数的返回值进行返回。

2.为什么右值不能取地址:在C++中,右值则是一个临时使用的,不可寻址的内存空间,右值没有独立的内存空间,它只是存储在寄存器或者其他临时内存中的一个值,我们也不能将右值放入内存,因为右值没有确定的内存位置,所以右值不能取地址

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

int main()
{
	int x = 1, y = 2;
	int&& rr1 = 10;
	const int&& rr2 = x + y;
	
	rr1++;
	cout << &rr1 << endl;
	cout << rr1 << endl;
	cout << &rr2 << endl;
	return 0;
}

在这里插入图片描述

rr2 = 5;  // 报错

所以如果我们不希望改变右值引用,我们就需要将右值引用定义为const右值引用

三、 左值引用与右值引用比较

左值引用只能引用左值,不能引用右值。但是const左值引用既可引用左值,也可引用右值,因为 const左值引用也是只读的,而权限可以平移

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a;    // ra为a的别名
	//int& ra2 = 10;   // 编译失败,因为10是右值
	
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值。

int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;

	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	//int a = 10;
	//int&& r2 = a;

	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}

四、右值引用使用场景和意义

1.左值引用的短板

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?

我们先看看左值引用的两个作用:

1.修改实参的值

2.引用做参数/函数返回值可以减少拷贝

我们可以把函数形参定义为实参的引用,这样函数在传参时就不用拷贝构造形参了,从而提高程序的效率,特别是对于需要深拷贝的自定义类型来说。左值引用作为返回值的效果也一样,当返回的对象出了作用域还存在时,直接使用引用返回可以减少一次拷贝构造:

void func1(string s)
{}
void func2(const string& s)
{}
int main()
{
	string s1("hello world");
	// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
	func1(s1);
	func2(s1);
	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += '!';
	return 0;
}

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。因为局部变量出了函数作用域就不存在了,此时引用就是一个野指针

template <class T>
T func(const T& x)
{
    T tmp;
    //...
    
    return tmp;
}

这种情况下编译器会使用这个临时对象拷贝构造一个临时对象,然后再返回这个临时对象,也就是说,这样会比引用返回多一次拷贝构造,当局部对象是一个需要进行深拷贝的自定义类型的时候,比如vector<vector>,拷贝构造的代价就会很大,所以右值引用的提出就是为了补足左值引用存在的这些短板

2.移动构造和移动赋值

为了更好的演示左值引用和右值引用对拷贝构造的优化,我们自己实现一个string类,在拷贝构造/赋值重载函数中进行打印相关的信息便于观察:

namespace hdp
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			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)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

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

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

		 移动构造
		//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;
		//}

		~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];
				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';
		}

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

		const char* c_str() const
		{
			return _str;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

现在假设我们要实现一个to_string函数,代码如下:

namespace hdp
{
	hdp::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		hdp::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}

我们可以看到,由于to_string函数的返回值是一个局部的对象,所以我们这里只能使用传值返回,而传值返回对于string来说需要进行深拷贝

在这里插入图片描述

其实这里程序的执行结果和我们预想的并不一样,正常的情况应该是str先拷贝构造一个临时对象,然后再由这个临时对象来拷贝构造ret,所以应该是两个拷贝构造(),上面的结果第一次构造是to_string函数内部构造str,第二次是函数返回时调用拷贝构造,而我们实现的拷贝构造函数中又调用了一次构造函数,所以打印了两个构造。但是编译器的优化只能适用于部分场景,对于很多场景还是会拷贝构造产生临时对象

在这里插入图片描述

尽管编译器进行了优化,这里还是会有一次拷贝构造,那么我们能不能想办法将str的资源直接赋值给s,中间不产生拷贝构造呢,此时我们就需要用到右值引用 了

C++11中的右值可以分为两种:

1.纯右值:内置类型表达式的值

2.将亡值:自定义类型表达式的值:所谓的将亡值就是指声明周期马上就要结束 的值,一般来说匿名对象,临时对象,move后的自定义类型都可以看做将亡值

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

我们需要注意的是,上面我们说右值不能取地址其实是右值的严格定义,但其实将亡值也是可以被当做右值看待的,而将亡值有独立的内存空间,可以取地址。既然将亡值的声明周期马上就要结束了,那么在拷贝构造中我么可以直接将将亡值的资源拿过来给我们自己使用,这样就需要需要进行深拷贝了。

void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
// 移动构造
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;
}

在这里插入图片描述

有人提出这样的一个观点:右值引用延长了变量的声明周期,这种说法是不准确的,因为右值引用只是将变量的资源转移给了另一个变量,让它的资源能够不随着变量的销毁而释放,而该变量本身的生命周期是没有变化的

【总结】

1.左值引用让形参称为实参的别名,直接减少拷贝

2.右值引用是通过实现移动构造和移动赋值,将将亡值的资源进行转移,间接的减少拷贝(浅拷贝的类不需要进行资源的转移,所以也没有移动赋值和移动拷贝)

3.STL中右值引用的使用

C++11在设计出右值引用之后,为STL所有容器都提供了移动构造和移动赋值,包括容器适配器

在这里插入图片描述

在这里插入图片描述

此外,还提供了右值版本的插入接口:

在这里插入图片描述

所以,以后如果我们要向容器中插入需要深拷贝的自定义类型的数据时,我们尽量使用匿名构造对象进行插入,这样调用的就是右值插入接口,元素会调用移动拷贝函数完成拷贝,从而提高程序的效率

我们可以将我们自己实现的list类支持右值版本的插入接口,部分代码如下:

#pragma once
#include <assert.h>
#include <algorithm>

namespace hdp
{
	// 定义节点结构
	template<class T>
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _data;

		// 构造
		list_node(const T& x)
			:_prev(nullptr)
			, _next(nullptr)
			, _data(x)
		{}

		// 移动构造
		list_node(T&& x)
			:_prev(nullptr)
			, _next(nullptr)
			, _data(move(x))
		{}
	};

	// 封装迭代器
	// 同一个类模板实例化出的两个类型
	// typedef __list_iterator<T, T&, T*> iterator;
	// typedef __list_iterator<T, const T&, const T*> const_iterator;
	template<class T, class Ref, class Ptr>
	// 迭代器类
	struct __list_iterator
	{
		typedef list_node<T> node;   // 重命名为list节点
		typedef __list_iterator<T, Ref, Ptr> Self;
		// 成员变量
		node* _pnode;

		// 构造
		__list_iterator(node* p)
			:_pnode(p)
		{}

		// 重载箭头
		Ptr operator->()
		{
			return &_pnode->_data;
		}

		// 重载*
		Ref operator*()
		{
			return _pnode->_data;
		}

		// 重载前置++
		Self& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}

		// 重载后置++
		Self operator++(int)
		{
			Self tmp(*this);
			_pnode = _pnode->next;
			return tmp;
		}

		//重载前置--
		Self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		// 重载后置--
		Self operator--(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}

		// 重载不等于
		bool operator!=(const Self& it) const
		{
			return it._pnode != _pnode;
		}

		// 重载等于
		bool operator==(const Self& it) const
		{
			return _pnode == it._pnode;
		}
	};

	// 定义list类
	template<class T>
	class list
	{
		// list 节点
		typedef list_node<T> node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;  //迭代器
		typedef __list_iterator<T, const T&, const T*> const_iterator;  //const迭代器

		// 迭代器
		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			// iterator it(_head);
			// return it;
			// 匿名对象构造
			return iterator(_head);
		}

		// const 迭代器
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		// 创建哨兵节点
		void empty_initialize()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
		}

		// 构造 不是list<T>的原因,构造函数名和类名相同,而list<T>是类型
		list()
		{
			empty_initialize();
		}

		// 迭代器构造
		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_initialize();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		// 拷贝构造
		// lt2(lt1)
		list(const list<T>& lt)
		{
			empty_initialize();

			for (const auto& e : lt)
			{
				push_back(e);
			}

		}


		// 拷贝构造现代写法
		list(list<T>& lt)
		{
			empty_initialize();

			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}

		// 赋值重载
		list<T>& operator=(const list<T>& lt)
		{
			if (this != &lt)
			{
				clear();
				for (const auto& e : lt)
				{
					push_back(e);
				}
			}

			return *this;
		}

		// 赋值重载现代写法
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		// 交换
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			// return _head->_next == _head;
			// return _head->_prev == _head;
			return _size == 0;
		}

		// 析构
		~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

		// 清理
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
			_size = 0;
		}

		// 尾插
		void push_back(const T& x)
		{
			//node* newnode = new node(x);
			//node* tail = _head->_prev;
			 _head tail newnode
			//tail->_next = newnode;
			//newnode->_prev = tail;
			//newnode->_next = _head;
			//_head->_prev = newnode;

			insert(end(), x);
		}


		// 尾插--右值版本
		void push_back(T&& x)
		{

			insert(end(), move(x));
		}

		// 头插
		void push_front(T& x)
		{
			insert(begin(), move(x));
		}

		// 头插  -- 右值版本
		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		// 尾删
		void pop_back()
		{
			//earse(end()->prev);
			erase(--end());
		}

		// 头删
		void pop_front()
		{
			erase(begin());
		}

		// 在pos之前插入数据
		iterator insert(iterator pos, const T& x)
		{
			node* newnode = new node(x);
			node* cur = pos._pnode;
			node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			++_size;

			return iterator(newnode);
		}

		// 在pos之前插入数据  -- 右值版本
		iterator insert(iterator pos, T&& x)
		{
			node* newnode = new node(move(x));
			node* cur = pos._pnode;
			node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			++_size;

			return iterator(newnode);
		}

		// 删除pos位置的数据
		iterator erase(iterator pos)
		{
			assert(pos != end());

			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;

			prev->_next = next;
			next->_prev = prev;

			delete pos._pnode;
			--_size;

			return iterator(next);
		}
	private:
		node* _head;
		size_t _size = 0; // 保存节点个数
	};
}

在这里插入图片描述

list的函数调用逻辑如下:

在这里插入图片描述

我们需要注意的是,右值函数的形参的类型都是T&& x,而不是const T&& x,这是因为最终在hdp::string类中我们需要将x的资源转移给别人,这就要求x必须是可以修改的,能够交换_str,_size,_capacity三个指针,此外,右值引用x之所以能够被修改是因为给右值取别名之后,右值会被存储起来,右值引用虽然引用的是右值,但是右值引用本身是左值,所以当我们继续往下一层进行传递参数的时候,我们需要将x重新move为右值,否则下一层调用时就会调用参数为左值的函数

五、万能引用与完美转发

1.万能引用

我们上面都是单独定义一个参数为右值引用的函数,然后让编译器根据实参的类型来判断调用左值引用还是右值引用的函数,我们能不能让函数能够根据实参的类型自动实例化出对应的不同函数呢,万能引用就实现了这个功能

万能引用是一个函数模板,且函数的形参类型为右值引用,对于这样的函数模板模板,编译器能够自动根据实参的类型–左值/const 左值/右值/const右值,自动推演实例化出不同参数形参分别为左值引用/const左值引用/右值引用/const右值引用的函数

template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);  // 右值
	int a;
    const int b = 8;
	PerfectForward(a);  // 左值
	PerfectForward(std::move(a)); // 右值
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

在这里插入图片描述

我们可以看到,无论实参是什么类型,模板函数都能正确的接收并实例化出对应的引用类型,所以我们把形参为右值引用的函数模板你叫做万能引用,其中,当实参为左值或者const左值的时候,T&&会被实例化为T& 或者const T&,我们称其为引用折叠,即将&&折叠为&

2.完美转发

尽管完整引用能够接收任何类型的参数,但是这里还是存在一个很大的问题,万能引用实例化后函数的形参的属性全都是左值,如果实参为左值/const左值,那么实例化函数的形参是左值/const左值,如果实参是右值/const右值,虽然实例化函数的形参是右值引用/const右值引用,但是右值引用本身是左值,所以就会出现下面的情况:

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 PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	int a;
	const int b = 8;
	PerfectForward(a);  // 左值
	PerfectForward(b); // const 左值
	PerfectForward(10);  // 右值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

在这里插入图片描述

此外,我们这里也不能简单的将t move后传递给Fun函数,因为这样会让t全部变为右值,又满足不了实参为左值的情况

在这里插入图片描述

为了在传参的时候能够保留对象的原始类型,C++设计了 完美转发–forward

在这里插入图片描述

在这里插入图片描述
【总结】

1.为了弥补左值引用局部对象返回会发生拷贝构造问题,C++11设计出了右值引用,右值引用可以通过移动构造和移动赋值来实现资源转移,将深拷贝转化为浅拷贝,从而提高了效率,此外,还为STL容器提供了右值版本的插入接口,由于右值引用本身是左值,所以函数参数往下一层传递时不能保证参数仍为右值,所以提供了move,可以将左值变为右值

2.为了使得函数模板能够同时接收const左值和const右值并正确实例化为对应的引用类型,C++11设计了万能引用,但是无论是左值引用还是右值引用,其本身是左值,所以往下一层传递时不能保证其类型了,此时move也不能够解决问题了,所以C++11设计了完美转发,来保证传参过程中原生类型属性能够保持不变

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

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

相关文章

win10专业版驱动开发

我使用的系统版本如何下&#xff1a; 使用的visual studio为VS2019,使用的SDK,WDK如下&#xff1a; 在visual studio单个组件里选择SDK10.0.018362.0 在WDK里面选择版本为&#xff1a; 下载链接如下&#xff1a; 以前的 WDK 版本和其他下载 - Windows drivers | Microsoft Le…

计算机算法分析与设计(16)---Dijkstra算法(含C++代码)

文章目录 一、知识概述1.1 算法描述1.2 例题分析 二、代码编写 一、知识概述 1.1 算法描述 1.2 例题分析 二、代码编写 输入&#xff1a;  第一行&#xff1a;图的顶点数n  第二行&#xff1a;图的边数k  第三行&#xff1a;算法起点begin&#xff0c;算法终点end  接下来…

设计模式-责任链设计模式

核心思想 客户端发出一个请求&#xff0c;链上的对象都有机会来处理这一请求&#xff0c;而客户端不需要知道谁是具体的处理对象让多个对象都有机会处理请求&#xff0c;避免请求的发送者和接收者之间的耦合关系&#xff0c;将这个对象连成一条调用链&#xff0c;并沿着这条链…

Java面试(基础篇)——解构Java常见的基础面试题 结合Java源码分析

fail-safe 和fail-fast机制 Fail-fast&#xff1a;快速失败 Fail-fast &#xff1a; 表示快速失败&#xff0c;在集合遍历过程中&#xff0c;一旦发现容器中的数据被修改了&#xff0c;会立刻抛出ConcurrentModificationException 异常&#xff0c;从而导致遍历失败 package …

经典链表问题:解析链表中的关键挑战

这里写目录标题 公共子节点采用集合或者哈希采用栈拼接两个字符串差和双指针 旋转链表 公共子节点 例如这样一道题&#xff1a;给定两个链表&#xff0c;找出它们的第一个公共节点。 具体的题目描述我们来看看牛客的一道题&#xff1a; 这里我们有四种解决办法&#xff1a; …

Tomcat启动控制台乱码问题

修改Tomcat/conf/logging.properties

[C++] C++入门

☃️个人主页&#xff1a;fighting小泽 &#x1f338;作者简介&#xff1a;目前正在学习C和Linux &#x1f33c;博客专栏&#xff1a;C入门 &#x1f3f5;️欢迎关注&#xff1a;评论&#x1f44a;&#x1f3fb;点赞&#x1f44d;&#x1f3fb;留言&#x1f4aa;&#x1f3fb; …

【Mysql】B+树索引的使用(七)

前言 每个索引都对应一棵 B 树&#xff0c; B 树分为多层&#xff0c;最下边一层是叶子节点&#xff0c;其余的是内节点&#xff08;非叶子节点&#xff09;。所有用户记录都存储在 B 树的叶子节点&#xff0c;所有目录项记录都存储在内节点。 InnoDB 存储引擎会自动为主键&am…

Spring Cloud Alibaba Seata 实现分布式事物

Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案 Seata 官网&#xff1a;https://seata.io/zh-cn/ Spring Cloud Alibaba 官…

信息检索与数据挖掘 | (五)文档评分、词项权重计算及向量空间模型

目录 &#x1f4da;词项频率及权重计算 &#x1f407;词项频率 &#x1f407;逆文档频率 &#x1f407;tf-idf权重计算 &#x1f4da;向量空间模型 &#x1f407;余弦相似度 &#x1f407;查询向量 &#x1f407;向量相似度计算 &#x1f4da;其他tf-idf权值计算方法 …

【OpenGL】五、光照

OpenGL Lighting 文章目录 OpenGL Lighting一、 冯氏光照模型(Phong Lighting Model)环境光&#xff08;Ambient lighting&#xff09;漫反射光照&#xff08;Diffuse lighting&#xff09;漫反射光照&#xff08;Specular Lighting&#xff09; 二、 材质(Materials)光照贴图(…

JUC并发编程笔记2

省流&#xff1a; 自己笔记&#xff0c;划走~~~~ 缓存更新策略

vue3里面vant组件的标签页使用?

一、绑一个v-model事件 二、让activeName的初始为0也就是默认是显示第一个标签页的下标 三、给标签页下面的东西进行一个判断 想让哪个优先显示就把哪个判断作为初始值存入

【试题040】多个逻辑或例题2

1.题目&#xff1a;设int n0;&#xff0c;执行表达式n ||(n-1) ||(n0)||(n1)||(n2)后n的值是 &#xff1f; 2.代码解析&#xff1a; 逻辑或 || 运算符是一个短路运算符&#xff0c;它从左到右依次计算表达式&#xff0c;如果遇到一个为真&#xff08;非零&#xff09;的值&am…

No171.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

第一章概述

一、学习目的与要求 本章对软件测试作了概括性的介绍&#xff0c;目的是使学生对软件测试有个初步的认识。通过本章的学习&#xff0c;应使学生掌握软件测试的基本概念&#xff0c;了解软件测试的发展历程和行业现状&#xff0c;掌握软件测试技术的分类&#xff0c;理解软件测试…

【JAVA-Day49】Java LinkedList集合详解

Java LinkedList集合详解 摘要引言Java LinkedList集合详解一、什么是LinkedList集合1.1 链表数据结构1.2 双向链表1.3 动态大小1.4 插入和删除元素1.5 适用场景 二、LinkedList集合的使用2.1 创建 LinkedList 集合、添加元素、遍历元素2.2 在指定位置插入元素2.3 获取指定位置…

模拟 Junit 框架

需求 定义若干个方法&#xff0c;只要加了MyTest注解&#xff0c;就可以在启动时被触发执行 分析 定义一个自定义注解MyTest&#xff0c;只能注解方法&#xff0c;存活范围是一直都在定义若干个方法&#xff0c;只要有MyTest注解的方法就能在启动时被触发执行&#xff0c;没有这…

开源博客项目Blog .NET Core源码学习(5:mapster使用浅析)

开源博客项目Blog使用mapster框架映射对象&#xff0c;主要是在数据库表对象及前端数据对象之间进行映射&#xff0c;本文学习并记录项目中mapster的使用方式。   App.Hosting项目的program文件中调用builder.Services.AddMapper函数进行对象模型自动映射&#xff0c;而该函数…