C++11『右值引用 ‖ 完美转发 ‖ 新增类功能 ‖ 可变参数模板』

news2024/10/7 2:25:46

✨个人主页: 北 海
🎉所属专栏: C++修行之路
🎃操作环境: Visual Studio 2022 版本 17.6.5

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1.右值引用
      • 1.1.什么是右值引用?
      • 1.2.move 转移资源
      • 1.3.左值引用 vs 右值引用
      • 1.4.右值引用的使用场景
      • 1.5.右值引用的意义
    • 2.完美转发
      • 2.1.模板中的万能引用
      • 2.2.传参过程中保持右值属性
      • 2.2.完美转发实际应用
    • 3.新增类功能
      • 3.1.移动构造和移动赋值
      • 3.2.插入系列的重载版本
      • 3.3.新增关键字
      • 3.4.其他新功能
    • 4.可变参数
      • 4.1.可变参数列表
      • 4.2.可变参数包
      • 4.3.可变参数包的解析
      • 4.4.emplace 系列函数
  • 🌆总结


🌇前言

自从C++98以来,C++11无疑是一个相当成功的版本更新。它引入了许多重要的语言特性和标准库增强,为C++编程带来了重大的改进和便利。C++11的发布标志着C++语言的现代化和进步,为程序员提供了更多工具和选项来编写高效、可维护和现代的代码


🏙️正文

1.右值引用

右值引用C++11 的重大更新之一,它的出现很好的解决了 临时资源浪费 的问题,同时也给 类和对象 做了一个全面升级,使其能轻松规避很多低效拷贝问题

1.1.什么是右值引用?

在学习 右值引用 之前,需要先来看看 左值引用引用C++ 相对于 C语言 的升级点之一,引用 既能像指针那样获取资源的地址,直接对资源进行操纵,也不必担心多重 引用 问题,对于绝大多数场景来说,引用指针 好用得多

而我们之前使用的所有引用都称为 左值引用,主要用于引用各种 变量,如果想引用 常量,需要使用 const 修饰

// 左值引用
int main()
{
	int a = 10;

	// 引用变量
	int& ra = a;

	// 引用 常量/临时对象
	const int& rb = 10;
	const int& rc = int();
	return 0;
}

C++11 中,新增了 右值引用 的概念,就是将 左值引用 中的 & 变为 &&右值引用 可以直接引用 左值引用 中需要加 const 引用的值;也可以通过函数 move 引用 左值引用 直接引用的值

// 右值引用
int main()
{
	int a = 10;

	// 引用 常量/临时对象
	int&& rrb = 10;
	int&& rrc = int();

	// 引用变量
	int&& ra = move(a);
	return 0;
}

其中,诸如 「变量 / 数组元素 / 解引用后的指针」 等,在表达式结束后仍然存在、并且可以被取地址的值称为 左值;而 「常量 / 临时对象 / 表达式结果」 等,在表达式结束后即将被销毁的临时对象,或者无法被直接取地址的值称为 右值

快速判断 左值 / 右值 的方法之一就是 看看能不能取地址

// 判断左值 / 右值
int main()
{
	int a = 10;

	// 左值
	cout << &a << endl;

	// 右值
	cout << &10 << endl; // 【报错】
	cout << &int() << endl; // 【报错】
	return 0;
}

直接可以引用 左值 的称为 左值引用,直接可以引用 右值 的就是 右值引用

注意:

  1. 左值引用 可以通过其他手段引用 右值,比如加 const右值引用 也可以通过其他手段引用 左值,比如 move 函数
  2. 赋值语句左边的一定是 左值,但右边的不一定是 右值,比如 int a = b

1.2.move 转移资源

无论是 左值引用 还是 右值引用,本质上都是在给 资源 起别名,当 左值引用 引用 左值 时,是直接指向 资源,从而对 左值 进行操作;当 右值引用 引用 右值 时,则是先将 常量 等即将被销毁的临时资源 “转移” 到特定位置,然后指向该位置中的 资源,对 右值 进行操作

int a = 10;

// 左值引用 引用 左值
int& ra = a;

// 右值引用 引用 右值
int&& rr = 10;

正因为将资源 “转移” 了,右值引用 才可以对资源进行利用

所以虽然 右值引用 引用的是 右值,但 右值引用 本身是可以取地址的,比如 &rr 是可以的,毕竟 rr 也指向了一块空间,这块空间中存储的是临时资源,这也就意味着 右值引用 是可以对临时资源进行修改操作的,也就是将临时资源再利用

对于 「常量 / 临时对象 / 表达式结果」右值,编译器会直接转移资源,但对于用户自定义的 左值,编译器不敢轻举妄动,只敢给用户提供一个 转移变量资源 的函数 move,有了 move 之后,右值引用 就能引用 左值

int a = 10;

// 左值引用 引用 右值
const int& r = 10;

// 右值引用 引用 左值
int&& rr = move(a);

语法还支持给 右值引用const,这样做的含义是 不能修改右值引用后的值

int main() 
{
	int a = 10;

	const int&& crr = 10;
	const int&& crra = move(a);

	++crr; // 【报错】
	++crra; // 【报错】

	return 0;
}

一般情况下是不会这样干的,右值引用 是为了移走资源,加了 const 还不如直接改用 const 左值引用


不要轻易使用 move 函数,左值 中的资源可能会被转走,在 C++11 之后,几乎所有的 STL 容器都增加了一个 移动构造 函数,其中就用到了 右值引用

如果此时我们直接将 左值 move 后构造一个新对象,会导致原本左值中的 资源 丢失

// move 转移资源
int main()
{
	string str = "Hello World!";
	cout << "str: " << str << endl;

	// 使用 move 函数后
	string tmp = move(str);
	cout << "str: " << str << endl;

	return 0;
}

所以一般情况下不要轻易使用 move 移动函数,除非你确定该资源后续不再使用

1.3.左值引用 vs 右值引用

C++11 之前,使用 const 左值引用 也可以引用 右值,并且在我们之前的学习中只使用 左值引用 也没什么大问题啊,那为什么还要搞出一个 右值引用 呢?

答案是 右值引用可以提高资源的利用率,进而提高整体效率

有了右值引用之后,之前只能 【读取】、【拷贝】的临时资源变得更有价值了,可以在右值引用后进行操作,也可以将资源转移以减少拷贝

下面是 左值引用右值引用 的对比图

特征左值引用右值引用
语法Type& lvalueRef = variable;Type&& rvalueRef = std::move(variable);
绑定对象现有对象临时对象或可移动对象
典型用途函数参数、返回类型移动语义、完美转发
示例int x = 10; int& ref = x;int&& rref = 10;
可重新赋值
可为 nullptr是(需谨慎使用)
引用折叠(C++11)Type&& && 折叠为 Type&&
生命周期延长(C++20)否(延长临时对象的生命周期)是(绑定到临时对象,如果绑定到右值则延长生命周期)

注意: 表格提供了一个高层次的概述,实际上有更多的细节和差异,尤其是在C++的后续版本中引入的一些特性

1.4.右值引用的使用场景

右值 分为

  • 纯右值(内置类型)
  • 将亡值(自定义类型)

纯右值 价值不大,但 将亡值 就不一样了,直接转移 将亡值 资源可以减少 拷贝次数,所以 右值引用 的使用场景主要体现了 拷贝

下面是简单模拟实现的 string,其中并未涉及 右值引用 相关知识

简单模拟实现的 string

namespace Yohifo
{
	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);
		}

		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;
			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()
		{
			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)
		{
			push_back(ch);
			return *this;
		}

		string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}

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

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

为了更好的观察是否发生了 深拷贝行为,在 拷贝构造 函数中加入了对应的打印语句,这里的参数为 const 左值引用

主函数中测试 左值右值 两种拷贝构造

主函数 main.cpp

int main()
{
	Yohifo::string str = "Hello World!";

	// str 为左值
	Yohifo::string s1 = str;

	// str+'\n' 为右值
	Yohifo::string s2 = str + '\n';

	return 0;
}

为什么要加 \n
防止编译器直接将 拷贝构造 优化为 直接构造

首先是测试 C++11 之前的结果(没有 右值引用


可以看到这里发生了 3深拷贝 行为,其中一次为 str 拷贝构造,一次为 str + '\n' 拷贝构造,还有一次是 operator+() 函数中的拷贝行为(无法避免)

现在足以证明,在没有使用 右值引用 的情况下,即便是传入 右值,触发的也是 深拷贝,浪费了 右值 这个临时资源

注意: 如果此时只显示了两次深拷贝,那是因为 VS 的平台工具集 v143 存在问题,会将 str+'\n' 这次拷贝构造优化掉,解决方法就是将平台工具集改为 v142

接下来在 string 中重载一个 拷贝构造 函数,参数为 右值引用,此时称为 移动构造

移动构造 string() — 位于 string

// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}

编译后再次运行,可以看到此时少了一次 深拷贝,多了一次 移动构造

移动构造 是由 str+'\n' 拷贝构造时触发的,又因为参数是 右值(临时对象),所以这里的 string 对象只需与 “右值” 进行 swap 就行了

可以通过调试证明 s2 的资源是从其他地方 “转移” 过来的


如今的编译器都很智能,会自动进行优化以减少拷贝,比较典型的就是 构造 + 拷贝构造 优化为直接构造,那么对于 移动构造 编译器是否会做出优化?

为了模拟优化场景,这里简单实现一个 to_string,目的是为了在函数结束后返回一个 临时对象

整型转为字符串 to_string() — 位于命名空间 Yohifo

string to_string(int val)
{
	bool flag = false;
	if (val < 0)
	{
		flag = true;
		val *= -1;
	}

	string ret;
	while (val)
	{
		int n = val % 10;
		ret += n + '0';
		val /= 10;
	}

	if(flag)
		ret += '-';

	std::reverse(ret.begin(), ret.end());

	return ret;
}

主函数中就负责调用 to_string() 获得一个临时对象,然后通过该临时对象去构造一个对象

int main()
{
	Yohifo::string str(Yohifo::to_string(100) + 'a');

	return 0;
}

首先来看看 C++11 之前(屏蔽 移动构造

结果为 3 次深拷贝


分析:第一次为 to_string() 函数执行完后返回的临时对象的拷贝,第二次为 operator+() 函数中生成的临时对象(不可避免),第三次为 strto_string() + 'a' 形成的临时对象的拷贝

实际拷贝次数不止 3 次,就拿 to_string() 函数来说,需要先将 ret 拷贝给 临时对象,再将 临时对象 拷贝给 调用者,编译器在这里会优化,优化成一次拷贝构造:ret 拷贝给调用者

这里的 深拷贝 是可以避免的,现在重新启用 移动构造 函数,再看看结果

可以看到 3深拷贝 变成了 2移动构造 + 1深拷贝(不可避免)

分析:第一次拷贝的对象是 临时对象(右值),资源即将销毁,触发 移动构造,将资源及时转移;第三次拷贝也是如此,同样可以通过 移动构造 将临时对象资源转移

对于 to_string() 函数来说,也不应该只发生一次 移动构造,实际应该先把 ret 拷贝给 临时对象,再将 临时对象 中的资源转移;但编译器判断 ret 是一个局部变量,出了函数就销毁了,于是就优化成了 return move(ret); 函数返回时将 ret 中的资源通过 move 函数转移

由此可以看出,编译器会在 临时对象 当作中间人连续赋值的场景中,直接将 临时对象 优化掉,尽量减少拷贝,这才有了 to_string() 函数中最终看到的 一次拷贝构造 / 一次移动构造

言归正传,得益于 移动构造临时对象 的资源得到了回收利用,传值返回时不再需要经过无意义且低效的 深拷贝

这里只是一个小小的 string,如果是 vectormapunordered_map 等基于模板的复杂容器,移动构造 带来的效率提升是非常显著的


关于 移动构造 相关问题

Q1:能否将函数返回值设为 右值引用?

答案是 不行,不是说单纯的 右值引用 解决了 无效深拷贝 问题,而是基于 右值引用 实现的 移动构造 解决了问题,所以无论是 右值引用 还是 左值引用,在面对 传值返回时,都不能作为函数返回值类型,返回局部对象引用会导致程序异常退出

并且在使用 右值引用 作为返回类型时,需要手动把 ret 这个左值 move,否则无法编译(右值引用不能直接引用左值),即使编译通过了,运行后也是有问题的

有问题的函数 to_string()

string&& to_string(int val)
{
	bool flag = false;
	if (val < 0)
	{
		flag = true;
		val *= -1;
	}

	string ret;
	while (val)
	{
		int n = val % 10;
		ret += n + '0';
		val /= 10;
	}

	if(flag)
		ret += '-';

	std::reverse(ret.begin(), ret.end());

	return move(ret);
}

可以看到,不仅没有触发 移动构造,还迫使程序异常终止(退出码不为 0

Q2: 函数传值返回,但在返回时能否手动 move 返回值?

答案是 可以的,前面说过,编译器优化后,会自动给返回值加上 move 以取出其中的资源,所以这里手动加上也没问题,但没必要

结果也是正常的

Q3: 右值引用什么时候作为参数类型使用?

当传入的参数为 右值 时,推荐使用 右值引用 作为参数类型;如果既有传入 左值 也有传入 右值 的情况,可以重载一个 右值引用 参数版本,编译器会匹配最合适的版本,确保资源不被浪费

常见的 右值引用 作为参数类型的有:拷贝构造函数赋值重载函数(这两个函数都是重载版本),传值拷贝是比较低效的行为,有了这两个函数后, 中其他函数可以放心传值返回

力扣题目 「杨辉三角」中的函数返回值为 vector<vector<int>>,只要 vector 中实现了 移动构造 函数,就可以避免深拷贝,轻松返回结果

1.5.右值引用的意义

右值引用 是个好东西,它的核心功能在于再次利用 临时资源,避免无意义且低效的拷贝行为

右值引用左值引用 各有各的适用场景:左值引用 是引用返回以提高效率(减少拷贝);右值引用 则是移动构造提高效率(减少拷贝),两者的角度不同

  • 左值引用:直接引用对象以减少拷贝
  • 右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝

2.完美转发

泛型编程C++ 中的核心功能之一,典型的让程序员少走弯路,让编译器多干活,伴随着 右值引用 的新概念加入,泛型编程 也需要随之升级

2.1.模板中的万能引用

泛型编程 的核心在于 模板根据参数类型推导函数,当我们分别传入 左值引用右值引用 时,模板 是否能正确推导呢

下面这段代码的含义是 分别传入 左值const 左值右值const 右值,并设计对应参数的回调函数,将参数传给模板,看看模板是否能正确回调函数

void func(int& a)
{
	cout << "func(int& a) 左值引用" << endl;
}

void func(const int& a)
{
	cout << "func(const int& a) const 左值引用" << endl;
}

void func(int&& a)
{
	cout << "func(int&& a) 右值引用" << endl;
}

void func(const int&& a)
{
	cout << "func(const int&& a) const 右值引用" << endl;
}

template<class T>
void perfectForward(T&& val)
{
	// 调用函数
	func(val);
}

int main()
{
	int a = 10;
	const int b = 10;

	// 左值
	perfectForward(a);
	perfectForward(b);

	// 右值
	perfectForward(move(a));
	perfectForward(move(b));

	return 0;
}

注:move(const 左值) 可以获取 const 右值

模板中涉及引用参数传递时,可以将函数参数类型写为 T&&,因为模板具有自动推导的特性,当传入的参数为 左值 时,触发 引用折叠 机制,实际参数类型会变为 T&;当传入的参数为 右值 时,正常使用 T&& 就行了
这一机制在模板中称为 万能引用(引用折叠),既能推导 左值引用,也能推导 右值引用

预期结果:先调用 左值引用const 左值引用 版本的 func,再调用 右值引用const 右值引用 版本的 func

实际运行结果如下

调用的全是 左值引用 相关的 func,难道一向靠谱的模板推导出现问题了吗?

当然不是模板 是根据我们传入的参数类型,来推导出相应的函数,如果说 模板 推导没有问题,那问题就出在 回调函数 的参数上了,只有推导后,无论传的 左值 还是 右值,编译器都会把 val 变为 左值,这样才能解释为什么最终结果全部为 左值引用const 左值引用

编译器这么做合理吗?

非常合理,首先要明白 右值 是无法被取地址的,而 右值引用 是将 右值 中的资源转存到一块特定的空间中,这也就意味着 右值引用 后的值,必定是一个 左值(拥有空间,可取地址),只有为 左值 才可以对其进行修改等操作

简单来说就是 右值属性转早了

解决问题的核心在于 perfectForward 传递 val 参数时,如何保证它的 右值属性 不丢失

2.2.传参过程中保持右值属性

要想在参数传递过程中保持其 右值属性,就需要使用 forward 函数,也就是 完美转发

forward 是一个带有参数模板的函数,主要在传参时使用: 如果参数原本是右值,但在右值引用后失去了右值属性,使用 forward 函数可以恢复它的右值属性

template<class T>
void perfectForward(T&& val)
{
	// 调用函数
	func(forward<T>(val));
}

再次运行程序,可以发现调用结果符合预期

注意: forward 是一个模板函数,需要指定模板参数类型 T,确保能正确推导并传递

2.2.完美转发实际应用

完美转发 在实际开发中会经常用到,前面说过,在 C++11 之后,所有的类都可以新增一个 移动构造 以规避无意义的低效拷贝行为,并且由于大部分类中会涉及 模板 的使用,保持右值属性 就是一个必备的技巧,如果没有 完美转发,那么 移动构造 顶多也就减少了一次 深拷贝

接下来看看 完美转发 如何应用

首先准备一个模拟实现的 list

#pragma once
#include<assert.h>

namespace Yohifo
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> self;
		node* _node;

		__list_iterator(node* n)
			:_node(n)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		self& operator++()
		{
			_node = _node->_next;

			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		self& operator--()
		{
			_node = _node->_prev;

			return *this;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}

		bool operator==(const self& s)
		{
			return _node == s._node;
		}
	};

	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

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

		void empty_init()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		template <class Iterator>
		list(Iterator first, Iterator last)
		{
			empty_init();

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		
		void swap(list<T>& tmp)
		{
			std::swap(_head, tmp._head);
		}

		list(const list<T>& lt)
		{
			empty_init();

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

		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//it = erase(it);
				erase(it++);
			}
		}

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

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

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

			node* new_node = new node(x);

			prev->_next = new_node;
			new_node->_prev = prev;
			new_node->_next = cur;
			cur->_prev = new_node;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

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

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			return iterator(next);
		}
	private:
		node* _head;
	};
}

因为在构建链表节点时,是不需要 深拷贝 的,可以给 节点类 增加 移动构造函数

新增链表节点的移动构造 list_node — 位于 list_node

list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}

主函数中只需创建一个 list<string> 对象,,查看 移动构造是否被正确调用

注意: 这里的 liststring 都是模拟实现的

测试移动构造是否生效

int main()
{
	Yohifo::list<Yohifo::string> l;
	l.push_back("Hello World!");

	return 0;
}

执行结果为 两次深拷贝

第一次深拷贝为构造时触发(默认构造传的是 右值),第二次则是插入时触发(插入的也是 右值

这里在 构造 / 插入 时使用的可是 右值 啊,为什么 string 中的 移动构造 函数没有被正确调用呢?进入调试模式,发现第一个问题:没有给 list 提供右值引用版本的 push_back()

这里先提供一个 右值引用版本push_back(),并在参数传递时使用 完美转发,看看能不能解决问题

右值引用版的 push_back() — 位于 list

// 右值引用版
void push_back(T&& x)
{
	// 完美转发
	insert(end(), std::forward<T>(x));
}

结果仍然是两次 深拷贝

原因是因为 push_back() 并没有干实事,它自己也在调用 insert(),而 insert() 还没有提供 右值引用 版,这里先试着补上

// 右值引用版
void insert(iterator pos, T&& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	// 完美转发
	node* new_node = new node(std::forward<T>(x));

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

已经增加了两次 完美转发 了,结果仍是两次 深拷贝

仔细观察 insert() 的代码可以发现,在插入节点之前,需要先构建一个 node 节点对象,构建对象时已经进行了 完美转发,意味着当前参数传递没有问题,顺着线索来到 node移动构造 函数中

list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}

其中的 _data 也就是 string 对象,在构造时,是直接传递了 x,并没有对其进行 完美转发,从而导致最终传给 string 的是一个 左值,自然调用的就是 深拷贝 了,话不多说,再加上一次 完美转发

list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(std::forward<T>(x))
{}

再次运行程序,发现这次终于成功调用了 string移动构造 函数


要想让我们之前模拟实现的 list 成功进行 移动构造,需要增加:一个移动构造、两个右值引用版本的函数、三次完美转发,并且整个 完美转发 的过程是层层递进、环环相扣的,但凡其中有一层没有进行 完美转发,就会导致整个传递链路失效,无法触发 移动构造

所以对于这种涉及多次函数回调的类,需要确保 右值 传递的每一层都不会丢失 右值属性,否则 移动构造 就断了


3.新增类功能

C++11 中新增了 右值引用 + 移动语义,应用到类中就诞生了 移动构造移动赋值 函数,除此之外,还对类中参数可能为 右值 的函数重载了 右值引用 版本

3.1.移动构造和移动赋值

之前类中有六个天选之子:构造函数、析构函数、拷贝构造、赋值重载、取地址重载 和 const 取地址重载

有了 右值引用 + 移动语义 后,对 拷贝构造赋值重载 进行了 “升级”,增加了 移动构造移动赋值 这两个新函数,至此,类中共有八个天选之子(编译器会默认生成)

天选之子 的意思就是 即使我们不写,编译器也会默认生成(有条件)

之前六个 天选之子 的生成规则这里就不再阐述了,主要来说说 移动语义 相关的两个函数

移动语义就是通过右值引用将资源转移再利用

这两个函数生成的条件比较苛刻:

  1. 如果自己没有写 移动构造 ,并且没有实现 析构拷贝构造赋值重载 中的任意一个,那么编译器才会自动生成一个 移动构造 函数,移动构造 函数对于内置类型,会按字节拷贝,对于自定义类型,会去调用它的 移动构造 函数,如果没有,就调用 拷贝构造(目的:涉及深拷贝的类编译器期望我们自己设计 移动构造 函数)
  2. 移动赋值 的生成逻辑与上面一致

编译器为什么会这么要求?

如果我们实现了 析构、拷贝构造、赋值重载,就证明当前的类中涉及到了 动态内存管理,是需要自己进行 深拷贝 的,编译器无能为力,移动语义 也应该根据自己的实际场景进行设计,所以编译器就没有自动生成

如何自己实现这两个 移动语义 相关函数?

得益于 右值引用,这个实现起来并不复杂,以 string 为例,移动构造移动赋值 的实现如下

// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	swap(s);
}

// 移动赋值
string& operator=(string&& s)
{
	swap(s);
	return *this;
}

核心在于 与临界资源(将亡值)交换资源

默认生成的 移动构造 或者 移动赋值 并非没有用,就像 拷贝构造 一样,默认生成的拷贝构造会去调用该函数中涉及类的 拷贝构造,也就是说,只要底层类没问题,自动生成的函数也可以实现 深拷贝 / 移动构造 / 移动赋值


如果非要使用编译器默认生成的呢?

在想让编译器生成的函数之后加上 default 关键字,如果类中涉及 动态内存管理(比如这里的 string,是不推荐使用默认生成函数的,因为会涉及到 深拷贝

并且由于 移动构造 属于 构造 家族,移动赋值 属于 赋值 家族,移动构造 / 移动赋值 存在的前提是 拷贝构造 / 赋值重载 也存在,如果都使用默认的,自然就无法 深拷贝


STL 中的容器都增加了 移动构造移动赋值

3.2.插入系列的重载版本

除了 构造 / 赋值 时提高效率,插入 时也能提高效率,也就是通过 右值引用 重载实现 移动语义 版的 插入函数

比如之前实现的 list

// 右值引用版
void insert(iterator pos, T&& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	// 完美转发
	node* new_node = new node(std::forward<T>(x));

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

注意: 如果移动语义版的插入函数中涉及函数回调、构造对象等,就需要使用 完美转发 保持右值的属性,确保能成功调用移动语义版本的函数


STL 中同样更新了一波 移动语义 版的 插入函数


说到底 移动语义 其实就是通过 右值引用 进行资源转移的行为

移动语义是否能延长临时对象(将亡值)的生命周期?
不能,只是将其中的资源转移了,但临时对象(将亡值)本身仍然会被销毁

const 引用延长生命周期问题
这是 C++11 之前对于右值的处理手段,在 push_back() 等插入函数值,常常会传入一个临时对象,此时就可以使用 const 引用作为参数类型来延长临时对象的生命周期,伴随 push_back() 栈帧销毁而被销毁


注意不要认为 const 引用做返回值时能延长局部对象的生命周期,局部对象出了作用域就被销毁了,而 const 引用此时指向被销毁的对象,这是不合理的,是一种类似 “野指针” 的 “野引用” 行为

3.3.新增关键字

default 关键字

可以指定编译器生成默认的函数,比如在下面这个类 Test 中,我们指定编译器生成 构造拷贝构造

测试类 Test

class Test
{
public:
	// 构造
	Test() = default;
	
	// 拷贝构造
	Test(const Test&) = default;

private:
	Yohifo::string _str;
};

这里的是 string 是之前模拟实现的,方便查看调用的是 深拷贝 还是 移动构造

分别传入 左值右值 查看函数调用情况

int main()
{
	Test t1;

	Test t2(t1); // 传入左值
	Test t3(move(t1)); // 传入右值
	return 0;
}

可以看到当前两次都是 深拷贝,可以推断出编译器并没有给 Test 自动生成 移动构造,原因在于我们已经指定生成了 拷贝构造,编译器认为 Test 类中不具备自动生成 移动构造 的条件

可以使用 defalut 指定编译器自动生成 移动构造

Test(Test&&) = default; // 指定生成移动构造

再次运行程序,可以看到当传入 右值 进行构造时,调用的是 移动构造

这里想强调的是 default 可以指定编译器自动生成类中的默认成员函数

能否使用 default 生成除默认成员函数之外的其他成员函数?

答案是 不行,如果这都可以的话,编译器都能自动写代码了,能自动生成默认成员函数,是因为这些函数的实现方式都是有模板的,编译器可以直接套用


delete 关键字

除了 default 关键字,C++11 还提供了 delete 关键字,用法和 default 一样,不过 delete 是声明该函数已被手动删除,不可以使用,比如将 Test 中的 构造 函数删除,就无法构造对象了

// 删除构造函数
Test() = delete;

什么情况下需要删除函数?

比如在 单例模式 中,只允许创建一个对象,为了避免外部再次创建对象,需要将 构造、拷贝构造、移动构造 等函数删除;再比如 C++ 中的 IO 流类中,是不允许 IO 对象之间进行拷贝的,因为每个 IO 对象中的缓冲区都不一样,随意拷贝会造成资源混乱,索性直接删除了


至于 finaloverride 已经在 继承和多态 相关章节介绍过了

  • final 修饰类,类不能被继承
  • final 修饰成员函数,子类继承时,成员函数不能被重写
  • override 修饰子类虚函数,确保完成重写

更多新增关键字详见 C++11 官网

3.4.其他新功能

C++11 还修复之前 中的一个大坑:内置类型不会初始化

这就导致如果你没有在编写 构造 函数时对 内置类型 进行处理,会导致后续使用时出现 随机值

比如下面这个类中就没有对 内置类型 进行处理

class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}

private:
	int _a;
};

int main()
{
	A a;
	a.Print();

	return 0;
}

打印结果为 随机值

使用随机值的危害?
如果将随机值作为循环起始值,会导致循环 “失控”

像这种大坑,估计是 C++ 独有的,为了修复这个问题,C++11 中新增了一个小补丁:类成员变量初始化

就是在类成员定义时,允许给一个 缺省值,比如这样

class A
{
	// ...
	
private:
	int _a = 0; // 此时给的是缺省值
};

此时输出的结果就是可预期的

注意: 这里给的是 缺省值,成员变量最终都是在 初始化列表中 进行初始化的,定义时给缺失值,就可以在初始化列表中使用


C++11 中还新增了 委托构造,就是允许在 初始化列表 中调用构造函数,这个语法作用并不是很大,并且不推荐使用,因为进入初始化列表就已经表示正在初始化了,再去调用其他构造函数会显得调用逻辑混乱

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}

	// 拷贝构造时,进行委托构造
	A(const A& a)
		:A()
	{
		// ...
	}

private:
	int _a;
};

注意: 只有 构造 相关函数才有 初始化列表,其他函数没有这个东西,自然也就不能使用委托构造


4.可变参数

C++11 引入了 可变参数模板可变参数包 的特性,允许定义和使用可接受任意数量参数的模板函数,这对于编写泛型代码、容器等方面提供了更大的灵活性

4.1.可变参数列表

C 语言就已经出现了 可变参数,语法表示为 ...C语言中的输入输出函数就用到了 可变参数列表

可变参数 的意思是你可以随便传入多个 参数,函数都能进行接收,C语言在使用 可变参数模板 时需要依赖 参数数量 + 参数类型 来进行识别,简单使用如下

int main()
{
	int a;
	double b;
	char c;
	scanf("%d %lf %c", &a, &b, &c);

	printf("输入了: %d %lf %c\n", a, b, c);
	return 0;
}

虽然这里也支持接收任意数量的参数,但还得提前确定这些参数的类型,使用起来比较麻烦

4.2.可变参数包

C++11 之前只能像 C语言 那样使用固定参数的 可变参数列表,在 C++11 中进行了重大改动,新增了 可变参数包,支持直接传入任意数量、任意类型的参数,不必像 C语言 那样指定数量和类型,这个改动非常激进,导致整个 可变参数 语法变得十分抽象

把所有传入的参数,不论数量、类型,统统进行打包,也就形成了 可变参数包

下面是使用 可变参数包 的实际例子(由于不知道会传入什么类型的参数,这里需要借助 可变参数模板

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


为了提高参数传递时的效率,可变参数包 的类型一般都会写成 Args&&...
这在模板中称为 万能引用(引用折叠),既可以引用 左值,也可以引用 右值

可变参数模板 允许传入 任意数量、任意类型 的参数

比如下面这几种函数传参都是可以的,由此可见 可变参数模板 的强大

int main()
{
	showList();
	showList(1, 2.2, 'c');
	showList("111111111111111");
	showList(vector<int>(), list<double>());
	return 0;
}

4.3.可变参数包的解析

可变参数模板 传参简单,可变参数包 解析就麻烦了,下面是一种不被编译器支持的错误解析方式

template<class ...Args>
void showList(Args... args)
{
	// 错误的解析参数方式
	int n = sizeof...(args);

	for (int i = 0; i < n; i++)
	{
		// 获取具体的可变参数
		args[i];
	}
}

注:使用 sizeof 计算可变参数包的大小时,需要在 sizeof 之后紧跟 ...,表示要计算的对象是可变参数包

这种解析方式很符合直觉,但编译器并不支持,具体报错信息为 必须在此上下文中扩展参数包

“上下文” 是一个抽象的术语,用于描述代码执行时所处的特定环境,这个环境可能是与函数调用相关的,也可能是其他方面的,这里的 上下文 具体指 模板的实例化和展开时的环境和情境

模板 的实例化和展开可以借助 递归 来实现

// 递归推导时结束时调用的函数
void showList()
{}

template<class T, class ...Args>
void showList(const T& val, Args... args)
{
	cout << val << " ";
	showList(args...); // 递归解析
}

int main()
{
	showList(1, 2.2, 'c');

	return 0;
}

可变参数包 的参数被成功解析了

因为是 递归 解析的,所以需要一个递归出口,也就是 参数为 void 的重载函数,推导逻辑如下

相关模板参数在编译阶段就已经全部推导出来了,也就是说当程序运行时,在当前代码中,会同时存在 4showList() 的重载函数,可以通过 __FUNCTION__ 这个和宏以及 sizeof 验证

template<class T, class ...Args>
void showList(const T& val, Args... args)
{
	cout << __FUNCTION__ << "(" << sizeof...(args) << ")" << endl;
	showList(args...); // 递归解析
}

可以看到 可变参数模板 中的函数共被调用了 3 次,再加上 showList() 无参版的调用,总共就是 4 个重载函数

main 函数第一次调用时,1 被赋给了 valargs 参数个数变成了两个

除了这种 递归 解析参数包的方式外,还有一种奇特的解析方式 通过逗号表达式展开

具体实现如下

template<class T>
void Print(T val)
{
	// 获取参数
	cout << val << " ";
}

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { (Print(args), 0)... };
}

关键点在于 arr 数组创建时,会根据 { } 中的参数进行初始化,可以在此直接将 可变参数包展开,展开过程中就完成了 参数 的解析工作

为什么要写出成 (Print(args), 0) 的形式?
这是一个逗号表达式,目的是让整个式子最终返回 0,用于初始化 arr 数组

可以设置 Print() 的返回值来简化代码

template<class T>
int Print(T val)
{
	// 获取参数
	cout << val << " ";

	return 0;
}

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { Print(args)... };
}

编译后代码如下

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { Print(1), Print(2.2), Print('c') };
}

这种参数包展开方式比较少用,简单了解即可


可变参数包 的应用场景在哪?

主要用于 线程回调函数 的参数传递,pthread 提供的线程创建接口 pthread_create 中只能给 线程回调函数 传递一个 指针变量C++11 中的 线程库 借助 可变参数包 进行了封装设计,可以在创建 线程 时轻易传递多个参数

注:这里的 Fn 是可调用的函数对象

关于 C++11 线程库 的更多知识将会放到下一篇文章中详谈

除此之外,可变参数包 还可以用于优化插入相关的函数

4.4.emplace 系列函数

C++11 还升级了 STL 中的插入函数(非右值引用版),这些新增的函数依赖 可变参数包,称为 emplace 系列

比如 listemplace_back()

empalce_back() 具备 push_back() 的所有功能,并且还在它的基础上进行了升级

如果只是单纯插入 左值 或者 move(左值),这两个函数没有区别

int main()
{
	std::list<Yohifo::string> l;

	Yohifo::string str1 = "Hello";
	Yohifo::string str2 = "Hello";

	// 插入左值
	l.push_back(str1);
	l.emplace_back(str2);
	cout << endl;

	// 插入 move 出来的右值
	l.push_back(move(str1));
	l.emplace_back(move(str2));
	cout << endl;

	return 0;
}

但如果插入的是 纯右值,两个函数就有区别了

int main()
{
	//...
	
	// 插入纯右值
	l.push_back("World");
	l.emplace_back("World");

	return 0;
}

插入纯右值时,只发生了一次 移动构造

通过调试发现,emplace_back() 在插入 纯右值 "World" 时,甚至都没有调用 移动构造,而是直接走的 构造函数

得益于 可变参数包emplace 系列函数可以直接将 纯右值 作为参数传递,传递途中不展开参数包,直到 构造函数 才把参数包展开,充分发挥了 可变参数包 的优势(直接传递参数)

因此可以得出结论:在插入纯右值,并且构造函数能正常接收时,emplace 系列函数可以直接构造,省去了调用移动构造函数时的开销

为什么传递 "World" 可以直接构造?
因为当前模拟实现的 string 中,构造函数参数就是 const char*,可以直接将参数包中的参数进行传递

注意: 插入 左值 或者 move(左值) 时,emplace 系列函数和普通函数没区别


🌆总结

以上就是本次关于 C++11 中右值引用和移动语义的相关知识了,右值引用的引入解决了临时资源过度消耗的问题,为类添加了移动语义函数,同时也升级了插入函数以支持右值引用版本。可变参数包的引入简化了多参数传递,尤其在 C++11 线程库的使用中更为方便。新的 emplace 系列函数通过利用可变参数包,为类构造函数提供了更灵活的调用方式,进一步优化了代码的效率和可读性。这些更新使得 C++11 更加强大、灵活


星辰大海

相关文章推荐

C++ 进阶知识

C++11『基础新特性』

C++ 哈希的应用【布隆过滤器】

C++ 哈希的应用【位图】

C++【哈希表的完善及封装】

C++【哈希表的模拟实现】

C++【初识哈希】

C++【一棵红黑树封装 set 和 map】

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

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

相关文章

操作符——C语言初阶

一.算数操作符&#xff1a; - * / % 、-、*、/这四个运算符均可用于整数及浮点数的运算。 当使用/运算符时&#xff0c;如果两个操作数均为整型&#xff0c;那么执行整数除法&#xff0c;运算结果也为整型&#xff1b;如果两个操作数至少一个为浮…

Facebook账号运营技巧

Facebook作为全球知名的社交媒体平台之一&#xff0c;坐拥着庞大的用户群体&#xff0c;吸引大量的跨境电商加入&#xff0c;那么肯定就会有少部分的卖家对于Facebook账号运营不是很了解&#xff0c;下面小编将讲一下Facebook账号运营的一些小技巧。 1、明确目标受众 首先需要明…

32 _ 字符串匹配基础(上):如何借助哈希算法实现高效字符串匹配?

从今天开始,我们来学习字符串匹配算法。字符串匹配这样一个功能,我想对于任何一个开发工程师来说,应该都不会陌生。我们用的最多的就是编程语言提供的字符串查找函数,比如Java中的indexOf(),Python中的find()函数等,它们底层就是依赖接下来要讲的字符串匹配算法。 字符串…

【BIM入门实战】Revit图元的选择方式,总有一款适合你

Revit图元的五种常见选择方式,总有一款适合你。 文章目录 一、直接单击二、加选和减选三、连续框选四、按类别选择五、全选过滤选择操作可以在三维视图、平面视图等多种视图中进行。 一、直接单击 直接单击,即可选中某一个图元,如选择一个扶手。 二、加选和减选 按住ctrl键…

37 _ 贪心算法:如何用贪心算法实现Huffman压缩编码?

基础的数据结构和算法我们基本上学完了,接下来几节,我会讲几种更加基本的算法。它们分别是贪心算法、分治算法、回溯算法、动态规划。更加确切地说,它们应该是算法思想,并不是具体的算法,常用来指导我们设计具体的算法和编码等。 贪心、分治、回溯、动态规划这4个算法思想…

数据库表字段以表格形式写入Word

在项目的开发中&#xff0c;难免会有编写概要设计、详细设计文档的要求&#xff0c;而在这些文档中&#xff0c;不可避免的就是要把数据库表的字段信息以表格的形式体现出来。例如下面这种格式 表数量少点还可以一点点粘贴&#xff0c;多了的话真的会疯&#xff0c;所以自己编写…

【计算机网络笔记】DHCP协议

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

35 _ Trie树:如何实现搜索引擎的搜索关键词提示功能?

搜索引擎的搜索关键词提示功能,我想你应该不陌生吧?为了方便快速输入,当你在搜索引擎的搜索框中,输入要搜索的文字的某一部分的时候,搜索引擎就会自动弹出下拉框,里面是各种关键词提示。你可以直接从下拉框中选择你要搜索的东西,而不用把所有内容都输入进去,一定程度上…

C++二分查找算法:132 模式解法二枚举2

题目及解法一&#xff1a; https://blog.csdn.net/he_zhidan/article/details/134362273 分析 第一步&#xff0c;选择各3对应的1&#xff0c;如果有多个符合对应最小的1&#xff0c;记录num[0,j)中的最小值iMin&#xff0c;如果nums[j]大于iMin&#xff0c;则m3To1 [nums[j…

认知升级:模型与范式转换

你好&#xff0c;我是 EarlGrey&#xff0c;一名双语学习者&#xff0c;会一点编程&#xff0c;目前已翻译出版《Python 无师自通》、《Python 并行编程手册》等书籍。 点击上方蓝字关注我&#xff0c;持续获取编程干货、好书推荐和高效工具分享&#xff0c;一起提升认知和思维…

excel中正态分布函数NORM.DIST和NORMDIST,以及它们之间的区别

NORM.DIST和NORMDIST的区别 NORM.DIST和NORMDIST函数都可以返回正态分布的概率密度、或者正态累积分布。 根据微软官网上的说法&#xff0c;NORMDIST函数已经不建议使用了&#xff0c;它已经被一个或者几个新的函数代替&#xff08;例如NORM.DIST&#xff09;&#xff0c;这些…

Rust图形界面编程:egui平直布局

文章目录 平直布局with_layout 平直布局 在前面的示例中&#xff0c;已经用到了ui.horizontal用来布局&#xff0c;其特点是水平摆放控件。相应地&#xff0c;ui.vertical则是垂直摆放控件。根据控件的摆放顺序不同&#xff0c;这两个布局组件衍生出一系列布局函数 horizonta…

深入Rust:探索所有权和借用机制

大家好&#xff01;我是lincyang。 今天&#xff0c;我们将一起深入探索Rust语言中的一个核心概念&#xff1a;所有权和借用机制。 这些特性是Rust区别于其他语言的重要特点&#xff0c;它们在内存管理和并发编程中扮演着关键角色。 一、Rust所有权机制 1. 什么是所有权&#x…

Java --- JVM之StringTable

目录 一、String的基本特性 二、String的内存分配 2.1、String内存分布图 三、字符串拼接操作 3.1、字符串拼接操作底层原理 3.2、拼接操作与append操作效率对比 四、intern()方法 4.1、intern()效率 五、StringTable的垃圾回收 一、String的基本特性 1、String字符…

C++二分查找算法:数组中占绝大多数的元素

题目 设计一个数据结构&#xff0c;有效地找到给定子数组的 多数元素 。 子数组的 多数元素 是在子数组中出现 threshold 次数或次数以上的元素。 实现 MajorityChecker 类: MajorityChecker(int[] arr) 会用给定的数组 arr 对 MajorityChecker 初始化。 int query(int left, …

【算法】区间(差分约束)

题目 给定 n 个区间 [ai,bi] 和 n 个整数 ci。 你需要构造一个整数集合 Z&#xff0c;使得 ∀i∈[1,n]&#xff0c;Z 中满足 ai≤x≤bi 的整数 x 不少于 ci 个。 求这样的整数集合 Z 最少包含多少个数。 输入格式 第一行包含整数 n。 接下来 n 行&#xff0c;每行包含三个…

21 Linux 自带的LED驱动

一、Linux 自带 LED 驱动使能 其实 Linux 内核自带 LED 抢夺那个&#xff0c;但在此之前需要配置 Linux 驱动来使能 LED 驱动。 输入以下命令&#xff1a; cd linux/atk-mpl/linux/my_linux/linux-5.4.31 make menuconfig 根据以下路径找到 LED 驱动&#xff1a; → Device D…

622.设计循环队列(LeetCode)

思路 先确定什么情况为空&#xff0c;什么情况为满。 这里有两种解决方案&#xff0c; 1.留一个空间空置&#xff0c;当rear1 front时 &#xff0c;则队列为满 &#xff08;这里我们选用方案一&#xff09; 2.增加一个size变量记录数据个数&#xff0c;size 0则为空&#xff…

asp.net数字档案管理系统VS开发sqlserver数据库web结构c#编程web网页设计

一、源码特点 asp.net 数字档案管理系统 是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver2008&#xff0c;使用c#语 言开发。 asp.net数字档案系统1 应用技…

通过右键用WebStorm、Idea打开某个文件夹或者在某一文件夹下右键打开当前文件夹用上述两个应用

通过右键用WebStorm、Idea打开某个文件夹或者在某一文件夹下右键打开当前文件夹用上述两个应用 通过右键点击某个文件夹用Idea打开 首先打开注册表 win R 输入 regedit 然后找到HKEY_CLASSES_ROOT\Directory\shell 然后右键shell 新建一个项名字就叫 Idea 第一步&#xf…