【C++ 第十八章】C++11 新增语法(3)

news2024/11/15 19:32:02

前情回顾:

【C++11 新增语法(1):1~6 点】

        C++11出现与历史、花括号统一初始化、initializer_list初始化列表、 auto、decltype、nullptr、STL的一些新变化

【C++11 新增语法(2):7~8 点】

        右值引用和移动语义∶移动构造和移动赋值、move 函数、左值引用的短板、万能引用、完美转发、push_back 函数重载右值引用版本(借助list的push-back 使用举例)﹔新增两个默认类函数与delete关键字

⭐⭐本文会使用到自己模拟实现的 string 和 list 类,为了更好的观察各种函数的构造过程,建议先将本文最后的 string 和 list 代码拷贝下来创建一个 string.h / list.h 文件

 9. 可变参数模板

一个模板,如果你有这样的需求:内容不变,但是参数的数量需要有 3 个,4个,5个参数,岂不是要写 好几个不同参数但内容相同的重复模板??

因此 可变参数模板的作用就凸显了(其实这个底层也是累死编译器)

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比
C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现
阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大
家如果有需要,再可以深入学习。

下面就是一个基本可变参数的函数模板

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

上面的参数 args 前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
”,它里面包含了0到N(N>=0)个模版参数。参数包的用法有点奇怪,需要时多多尝试后使用即可

可变参数模板 相当于 不限制参数个数的 模板,模板+可变参数 == 参数可变的模板


        同时,可变参数模板 是 编译时获取,即像之前学的模板,这个是 直接在编译阶段,编译器通过实例化的对象,来 实例化出模板

        这个也是模板,因此 参数包 Args… args 需要在编译阶段时 直接确定,因此不能使用 参数包 args 进行一些 运行时获取和解析的程序:sizeof、for循环打印…..

        我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用 args[i] 这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。


🚩递归函数方式展开参数包

ShowList 函数将参数包传给 Print 函数,到 Print 函数这里,会自动解析出一个参数放到 T&& x,剩余的参数留在参数包 Args&&... args,

然后再递归:下一层则再拿一个参数出来放到 x 处,一直递归下去,直到取出所有

Print(T&& x, Args&&... args)

我们写的 程序

void Print()
{
	cout << endl;
}

template <class T, class ...Args>
void Print(T&& x, Args&&... args)
{
	cout << x << " ";
	Print(args...);
}

// 编译时递归推导解析参数
template <class ...Args>
void ShowList(Args&&... args)
{
	Print(args...);
}

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, "xxxxx");
	ShowList(1, "xxxxx", 2.2);

	return 0;
}

在编译器底层其实长这样:

void Print()
{
	cout << endl;
}

template <class T, class ...Args>
void Print(T&& x, Args&&... args)
{
	cout << x << " ";
	Print(args...);
}
 
// 编译器推导的
void Print(double x)
{
	cout << x << " ";
	Print();
}

void Print(const char* x, double z)
{
	cout << x << " ";
	Print(z);
}

void Print(int x, const char* y, double z)
{
	cout << x << " ";
	Print(y, z);
}

// 可变参数模板:编译时递归推导解析参数
template <class ...Args>
void ShowList(Args&&... args)
{
	Print(args...);
}

//编译器实例化生成
void ShowList(int x, const char* y, double z)
{
	Print(x, y, z);
}

int main()
{
	ShowList(1, "xxxxx", 2.2);

	return 0;
}

🚩逗号表达式+在数组中直接展开

后面使用 三个点的省略号代表当前这里要展开

逗号表达式的执行原理:从左到右依次执行逗号表达式,最后结果取最后一个位置的数据

因此,下面使用逗号表达式,既执行了表达式的内容,返回值也是 int 类型

template <class T>
int PrintArg(T t)
{
	cout << t << " ";
	return 0;
}

template <class ...Args>
void ShowList(Args... args)
{
	// 不能使用 ostream 作为这里的类型,这个对象不允许拷贝
	//ostream arr[] = { cout(args)... };

	// 逗号表达式
	// (PrintArg(args), 0) :先执行前面的内容即 printarg(args),再得到逗号表达式的结果 0
	// 这样间接的达到目的
	int arr[] = { (PrintArg(args), 0)... };

	cout << '\n';

	// 或者说,逗号表达式的目的是为了控制返回值
	// 同时执行想要的表达式
	// 如下:先执行前面的 cout 表达式子,然后逗号表达式的最后返回值 是最后的那个值 0
	int arr2[] = { (cout << (args) << ' ', 0)...}; 

	cout << endl;


	// int 数组,逗号表达式依次运行了,顺便最后返回 0 给数组
}


int main()
{
	//ShowList(1);
	//ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

🚩也可以不使用 逗号表达式

逗号表达式的作用主要是控制返回值是数组的 int 类型,你也可以控制 打印函数的返回值是 int 类型,则数组的元素类型也就是 int 类型了

template <class T>
int PrintArg(T t)
{
	cout << t << " ";
	return 0;
}

template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { PrintArg(args)... };   // 三个点代表当前这里要展开
	cout << endl;
}

// 编译推演生成下面的函数
void ShowList(int x, char y, std::string z)
{
	int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) };
	cout << endl;
}

int main()
{
	//ShowList(1);
	//ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));

	return 0;
}

10. emplace_back 函数:升级版 push_back

emplace_back 函数使用了 可变参数模板


这个是函数模板,是 可变参数模板

push_back 的所有操作,本函数都能做,那本函数的特别之处在于?

🚩使用模拟实现的 list 和 string,演示使用过程

string 和 list 的代码在本文的 末尾

// emplace_back总体而言是更高效,推荐使用
int main()
{
    // 一、 push_back 的行为
	bit::list<bit::string> lt;
	// 左值
	bit::string s1("111111111111");
	lt.emplace_back(s1);

	// 右值
	lt.emplace_back(move(s1));

	// 直接把构造string参数包往下传,直接用string参数包构造string
	lt.emplace_back("111111111111");
	lt.emplace_back(10, 'x');
	cout << endl << endl;

    
    // 构造pair + 拷贝/移动构造pair到list的节点中data上
	bit::list<pair<bit::string, int>> lt1;
	pair<bit::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	lt1.emplace_back(move(kv));
	cout << endl << endl;

	
	
    // 二、特别之处
    // 直接把构造pair参数包往下传,直接用pair参数包构造pair
	lt1.emplace_back("苹果", 1);
    //lt1.emplace_back(1, "苹果"); // 为什么不支持这样写?因为参数包也是从左到右依次拿出的,要和参数顺序对应上

	return 0;
}

上面的代码演示可以发现: 给 emplace_back 传递构造 pair 的参数包,最终只会产生一次 直接构造,效率好高!

单独拿出来解释原理:

pair<my::string, int> kv1("苹果", 1);
lt1.emplace_back(kv1);

lt1.emplace_back("苹果", 1);

上面这组:先直接构造一个 pair,创建链表节点时再 拷贝构造 pair

下面这组:直接拿着参数包,层层传递到 节点初始化列表构造data = pair,只需调用一次 string 的直接构造(相当于有个构造pair的积木零件包,在上头先不构造出来,零件包一直传递到下层,再拼积木 直接构造 节点的 pair出来

template <class... Args>
void emplace_back(Args&&... args)
{
    insert(end(), forward<Args>(args)...);
}


template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
    Node* newnode = new Node(std::forward<Args>(args)..);
    //....
}


template <class... Args>
ListNode(Args&&... args)
    , _data(std::forward<Args>(args)...)
{}

上面这组:参数包中放一个 pair 对象,转发给 insert,再转发给 Node 的构造函数:pair里面的 string 已经是成型的 string 对象,则调用 string 移动拷贝构造

下面这组:参数包中放一个 string 和 int,转发给 insert,再转发给 Node 的构造函数:string 仅仅是参数,未成型,调用 stirng 的直接构造

所谓的参数包 实际上不存在,底层就是编译器根据模板生成好多个 函数,每个函数对应传递 一个 参数包里面的 参数

比如 emplate_back 接收了一个参数包 args,其中有参数 string 和 int

向下传递实际上是 实例化两组函数:传递string的左右值版本函数、传递 int 的左右值版本函数

然后传递参数包给 insert 函数接收,insert 函数也用一个参数包接收,然后底层进行一样的操作

template <class... Args>
    void emplace_back(Args&&... args)
{
    insert(end(), args...);
}

// 原理:编译器根据可变参数模板生成对应参数的函数
void emplace_back(string& s){
    insert(end(), s);
}

void emplace_back(string&& s){
    insert(end(), s);
}

void emplace_back(const char* s){
    insert(end(), s);
}

void emplace_back(size_t n, char ch){
    insert(end(), n, ch);
}

🚩【面试题】emplace_back 为什么会更加高效?

push 左值时:这个左值是一个深拷贝对象

push_back 和 emplace_back 需要 直接构造 + 深拷贝

push_back 和 emplace_back 可以 move变成右值: 直接构造 + 移动构造

push 左值时:这个左值是一个浅拷贝对象

push_back 和 emplace_back 需要 直接构造 + 浅拷贝

push_back 和 emplace_back 可以 move变成右值: 直接构造 + 移动构造

其实浅拷贝对象一般不会使用移动构造,也没什么意义

push 右值时:匿名对象

push_back 和 emplace_back 需要 直接构造 + 移动构造

push 参数包:只有 emplace_back 可以

只需调用 直接构造

综合而言:emplace_back 整体而言会更加高效

🚩总结:

以后如果有需要 push_back 和 push_front 的地方都可以换成使用  emplace_back 和 emplace_front

本章节使用到的 模拟实现 string 代码

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
#include<vector>



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

		string(size_t n, char ch = '\0')
		{
			cout << "string(size_t n, char ch = '\0')" << endl;
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				_str[i] = ch;
			}
			_size = n;
			_str[_size] = '\0';
		}

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

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

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

		// 移动构造
		// 临时创建的对象,不能取地址,用完就要消亡
		// 深拷贝的类,移动构造才有意义
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;
			swap(s);
		}

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

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

			return *this;
		}

		// 移动赋值
		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];
				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);
			}

			// 可以使用 strcat 追加字符串:遍历字符串,找到 \0 ,从这个位置开始追加字符串
			// 遍历一遍效率较低
			// 使用 strcpy 指定起始位置:_str + _size,刚好是 \0 的位置

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

		}
		string& operator+=(const char ch)
		{
			this->push_back(ch);
			return *this;
		}
		string& operator+=(const char* str)
		{
			this->append(str);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
		const size_t size() const {
			return _size;
		}
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}


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



	my::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		my::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;
	}
}

本章节使用到的 模拟实现 list 代码

#pragma once
#include<assert.h>
#include<iostream>
#include<vector>

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

		T _data;

		ListNode(const T& data = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(data)
		{}

		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{}

		template <class... Args>
		ListNode(Args&&... args)
			: _next(nullptr)
			, _prev(nullptr)
			, _data(std::forward<Args>(args)...)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;

		ListIterator(Node* node)
			:_node(node)
		{}

		// ++it;
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

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

			return tmp;
		}

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

			return tmp;
		}

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

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

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}

		bool operator==(const Self& it)
		{
			return _node == it._node;
		}
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<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();
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		list(initializer_list<T> il)
		{
			empty_init();

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

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

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


		list<T>& operator=(list<T> lt)
		{
			swap(_head, lt._head);

			return *this;
		}

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

		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		void push_back(const T& x)
		{
			insert(end(), x);
			cout << '\n';
		}

		void push_back(T&& x)
		{
			insert(end(), move(x));
			cout << '\n';
		}

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

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

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

		// 没有iterator失效
		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(move(x));
			Node* prev = cur->_prev;

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

			return iterator(newnode);
		}

		template <class... Args>
		void emplace_back(Args&&... args)
		{
			insert(end(), forward<Args>(args)...);
		}

		 原理:编译器根据可变参数模板生成对应参数的函数
		//void emplace_back(string& s)
		//{
		//	insert(end(), std::forward<string>(s));
		//}
		//
		//void emplace_back(string&& s)
		//{
		//	insert(end(), std::forward<string>(s));
		//}
		//
		//void emplace_back(const char* s)
		//{
		//	insert(end(), std::forward<const char*>(s));
		//}
		//
		//void emplace_back(size_t n, char ch)
		//{
		//	insert(end(), std::forward<size_t>(n), std::forward<char>(ch));
		//}

		template <class... Args>
		iterator insert(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(std::forward<Args>(args)...);
			Node* prev = cur->_prev;

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

			return iterator(newnode);
		}

		// erase 后 pos失效了,pos指向节点被释放了
		iterator erase(iterator pos)
		{
			assert(pos != end());

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

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

			delete cur;

			return iterator(next);
		}

	private:
		Node* _head;
	};
}

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

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

相关文章

香橙派入手第一天

一、开箱 拿到快递回来以后&#xff0c;兴冲冲的把快递拆开&#xff0c;里面一共有一下几样东西&#xff0c;一个板卡&#xff0c;一个充电器&#xff0c;一个小风扇&#xff0c;还有一些安装用的零件和一把小螺丝刀。 值得一提的是这个充电器是最高支持65w的typec-typec的充电…

回溯法-0/1背包问题

什么是回溯法&#xff1f; 回溯法是一种搜索算法&#xff0c;它通过深度优先搜索的方式来解决决策问题。它从根节点开始&#xff0c;逐步扩展节点&#xff0c;直到找到所有可能的解。 回溯法的基本思想 开始节点&#xff1a;从根节点出发&#xff0c;这个节点是解空间的起点…

LLM 教程——如何为特定任务定制微调 BERT

通过本文&#xff0c;您将学会如何为特定的自然语言处理任务&#xff08;如分类、问答等&#xff09;微调BERT。 1、引言 BERT 是一个强大的预训练语言模型&#xff0c;可以用于多种下游任务&#xff0c;只需进行极小的修改。通过微调 BERT&#xff0c;您可以利用它的大规模知…

系统设计:一致性哈希的概念

目录 一、介绍 二、问题提出 三、朴素实施 四、一致性哈希 4.1 关闭服务器 4.2 添加新服务器 五、分布不均 5.1 虚拟节点 5.2 应用 六、结论 资源 一、介绍 我们生活在一个每天都会生成大量数据的世界里。在大公司中&#xff0c;几乎不可能将所有数据存储在单个服务器…

【区间dp、前缀和】 P1220 关路灯 题解

关路灯 题目描述 某一村庄在一条路线上安装了 n n n 盏路灯&#xff0c;每盏灯的功率有大有小&#xff08;即同一段时间内消耗的电量有多有少&#xff09;。老张就住在这条路中间某一路灯旁&#xff0c;他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。 为了给村里…

APACHE NIFI—wait、notify组件报拒绝连接访问的报错

报错文字&#xff1a; Wait[idele44704-6fb6-1b60-ffff-ffffdcofbba2]Failed to process session due to Failed to get signal for c84c4aec-1287-4216-b1a2-f5c6fod4a3b7 due to java.net.ConnectException:Connection refused: org.apache.nifi.processor.exception.Proces…

jmeter响应断言、json断言、断言持续时间操作

一、响应断言 Apply to&#xff1a;断言应用的范围&#xff0c;这里默认&#xff0c;通常发出一个请求只触发一个服务器测试字段 响应文本&#xff0c;response响应体内的信息响应代码&#xff1a; 响应码&#xff0c;一般是200响应信息&#xff1a;响应码后面的返回的信息&am…

zdppy+vue3+onlyoffice文档管理系统实战 20240831上课笔记 继续完善登录功能

遗留的问题 1、整合验证码的接口2、渲染验证码3、实现验证码校验的功能4、验证码校验通过之后&#xff0c;再校验登录功能 验证码框架怎么使用 安装&#xff1a; pip install zdppy_captcha使用示例&#xff1a; import zdppy_api as api import zdppy_captcha import zdp…

Docker compose 安装 ELK

1. 简介 方案概述 我们使用 Filebeat 作为日志收集器&#xff0c;接入到 Redis 队列&#xff0c;然后消费队列中的日志数据流转到 Logstash 中进行解析处理&#xff0c;最后输出到 Elasticsearch 中&#xff0c;再由 Kibana 展示到页面上。我们采用 Elasticsearch 3 节点集群…

hello树先生——AVL树

AVL树 一.什么是AVL树二.AVL树的结构1.AVL树的节点结构2.插入函数3.旋转调整 三.平衡测试 一.什么是AVL树 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。…

【计组 | Cache原理】讲透Cache的所有概念与题型方法

Cache 写在前面&#xff1a;高速缓存Cache一直408中的重点以及绝对的难点&#xff0c;前几天我在复习计组第三章的知识&#xff0c;Cache这一节把我困住了&#xff0c;我发现很多概念我都不记得了&#xff0c;一些综合性强的计算题根本无从下手&#xff0c;我深知Cache对于每个…

1分钟把高质量AI知识库站点嵌入小程序

许多企业都有把 AI 知识库装进小程序、网站、企业微信、钉钉等的需求&#xff0c;让用户能够在小程序上访问到高品质的内容。奈何有太多限制&#xff0c;往往会遇到IP地址不被信任或技术对接接口配置等困难。HelpLook能帮你节省这些繁琐的程序&#xff0c;0代码快速将AI知识库站…

工程师们都爱看的Docker容器技术,一看就会!保姆级教程(上)

文章目录 Docker简介Docker在企业中的应用场景Docker与虚拟化的对比Docker的优势 部署Docker部署DockerDocker的基本操作Docker镜像管理容器的常用操作 Docker镜像构建Docker镜像结构镜像运行的基本原理镜像获得方式镜像构建Docker镜像构建企业实例 镜像优化方案镜像优化策略镜…

一款免费强大的快速启动工具,快速打开程序,软件,网站,工具等

Lucy是一款由个人开发者针对个人需求开发的快速启动工具&#xff0c;其最大的特点在于简洁和快速。它允许用户通过简单的拖拽操作将文件、文件夹、网址等添加到启动列表中&#xff0c;实现快速访问常用程序和文件的目的。Lucy不依赖于网络连接&#xff0c;避免了隐私泄露的风险…

Xcode插件开发

Xcode插件开发 文章目录 Xcode插件开发一、插件开发流程创建插件Extension文件介绍文件说明 二、插件使用安装说明 一、插件开发流程 创建插件的过程并不复杂&#xff0c;只是官方教程&#xff0c;过于简单&#xff0c;所以这里补充下创建细节 创建插件 环境&#xff1a;Xco…

公安智慧大楼信息化整体建设设计方案

1. 项目背景与需求分析 《公安智慧大楼信息化整体建设设计方案》针对一个用地面积和建筑面积均具规模的建设项目&#xff0c;提出了信息化建设的全方位设计方案&#xff0c;以满足现代公安业务的需求。 2. 信息化设计理念 方案强调了信息化设计的顶层设计方法论&#xff0c;…

【Qt】窗口概述

Qt 窗口概述 Qt窗口是由QMianWindow类来实现的。 QMainWindow 是⼀个为⽤⼾提供主窗⼝程序的类&#xff0c;继承⾃ QWidget 类&#xff0c;并且提供了⼀个预定义的布局。QMainWindow 包含 ⼀个菜单栏&#xff08;menu bar&#xff09;、多个⼯具栏(tool bars)、多个浮动窗⼝&a…

基于Swagger自动生成离线API文档(Word、Markdown文档)

在做项目时通常需要给客户提供离线Word的API文档归档&#xff0c;不要跟客户说有Swagger在线API文档&#xff0c;客户不会用也不会去看。只要你有Swagger&#xff0c;TableGo就能自动生成一份漂亮的Word离线API文档给客户&#xff0c;大大提高了写文档的效率&#xff0c;客户看…

【0-1背包】3180. 执行操作可获得的最大总奖励 I

给你一个整数数组 rewardValues&#xff0c;长度为 n&#xff0c;代表奖励的值。 最初&#xff0c;你的总奖励 x 为 0&#xff0c;所有下标都是 未标记 的。你可以执行以下操作 任意次 &#xff1a; 从区间 [0, n - 1] 中选择一个 未标记 的下标 i。 如果 rewardValues[i] 大…

openwrt结合智能家居(相关搜索:路由器)

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…