List的模拟实现(2)
前言
上一节我们讲解了list的基本功能,那么本节我们就结合底层代码来分析list是怎么实现的,那么废话不多说,我们正式进入今天的学习:)
List的底层结构
我们先来看一下list的底层基本结构:
这里比较奇怪的一点是底层选择了void*来表示链表的指针,其实可以不用这么做
接下来是迭代器部分:
模拟实现链表难就难在实现迭代器,因为链表的物理空间不是连续的,所以不能简单的通过typedef一个指针变量来达到实现迭代器的目的
链表为空的初始化:
链表为空时不是简单的把所有的指针都赋空指针,而是创建一个节点,让这个节点的next和prev指针都指向自己。这就是创造了一个头节点,这里涉及到数据结构阶段双向带头链表的基本知识
get_node是申请节点,调用了内存池,由于我们还没有学习内存池,所以可以简单地将这里理解为malloc
下面是源代码中的头插尾插接口:
通过这些,我们可以知道,单纯实现链表的功能是没有什么难度的,和我们之前实现string和vector差不多,难就难在理解迭代器的实现
既然底层结构已经看的差不多了,那我们现在就来实现一下list吧
List的模拟实现
节点的基本结构
首先要实现链表的基本结构,链表的基本结构是节点:
template<class T>struct list_node{list_node(const T& data = T()):_data(data),_next(nullptr),_prev(nullptr){}T _data;list_node<T>* _next;list_node<T>* _prev;};
链表的借本结构
构造函数
先来实现一下构造函数,根据底层是双向带头循环链表,写出代码如下:
template<class T>class list{public:typedef list_node<T> Node;list(){_head = new Node(T());_head->_next = _head;_head->_prev = _head;_size = 0;}private:Node* _head;size_t _size;};
size和empty函数
实现size和empty函数,由于代码很简单,就不做讲解了:
size_t size(){return _size;}bool empty(){return _size == 0;}
迭代器
因为不是每一个STL的容器都是存储在连续的物理空间之中,所以并不是每一个容器都支持下标+[]访问,但是所有的容器都有迭代器,都支持范围for,因此实现迭代器是很重要的一个步骤,我们来逐步分析迭代器的实现:
我们知道,对于一个节点而言,*解引用以后找到的不是节点的值,而是节点的地址;因为存储空间不连续,不能通过++来找到下一个节点,所以就不能仅通过typedef来实现迭代器,此时我们就应该考虑封装+运算符重载来实现迭代器,主要内容就是重载 * ++ != 等,让其满足与其他迭代器相同的作用
template<class T>struct list_iterator{Node* _node;};
迭代器的构造函数
list_iterator(Node* node):_node(node){}
重载*
T& operator*(){return _node->_data;}
重载前置++、--
由于++以后返回的数据类型依旧是迭代器,为了书写简便一点,调用typedef:
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;}
重载!=、==
bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}
重载->
先来分析一下为什么需要重载->这个符号:
接下来先构造一个使用->的情景:
假设有一个这样的结构体:
struct AA{int _aa1 = 1;int _aa2 = 2;};
此时我们要在链表中存入这个结构体并且打印:
list<AA> ltaa;ltaa.push_back(AA());ltaa.push_back(AA());ltaa.push_back(AA());ltaa.push_back(AA());list<AA>::iterator itaa = ltaa.begin();while (itaa != ltaa.end()){cout << *itaa << " ";++itaa;}cout << endl;
此时运行以后发现编译不通过
这里面的itaa通过解引用会得到节点中的data,data的数据类型是T,也就是自定义类型AA。因为没有重载流插入,所以这里的自定义类型无法打印,要想解决这个问题有两种方法:
第一种方法:给AA重载流插入
第二种方法:逐一访问结构体中的元素
list<AA>::iterator itaa = ltaa.begin();while (itaa != ltaa.end()){cout << (*itaa)._aa1 << " and " << (*itaa)._aa2 << endl;++itaa;}cout << endl;
此时代码就成功运行了
但是这样写就很麻烦,此时就可以重载->这个运算符,这里先把代码写出来再对其进行分析:
T* operator->(){return &_node->_data;}
list<AA>::iterator itaa = ltaa.begin();while (itaa != ltaa.end()){cout << itaa->_aa1 << " and " << itaa->_aa2 << endl;++itaa;}cout << endl;
疑难解答:为什么operator-> 操作符重载函数返回值 T* 而不是 T&
在 list_iterator 这个迭代器类模板中,operator-> 操作符重载函数设计为返回 T* 而不是 T&,这与 operator-> 操作符的特殊语义和 C++ 语言的规定有关:
在 C++ 中,operator-> 操作符有特殊的处理规则。当我们使用 -> 操作符时,编译器会尝试不断应用 -> 操作,直到得到一个非指针类型的对象
简单地说就是:当你使用迭代器 it 调用 it->member 时,编译器会先调用 it.operator->(),如果 operator->() 返回的是一个指针,那么就直接访问该指针指向对象的成员;如果返回的不是指针,编译器会再次对返回值应用 ->
我们可以分两点来说明返回 T* 的合理性:
1. 符合使用习惯
迭代器的 operator-> 操作符通常用于模拟指针的行为,返回 T* 可以让迭代器像指针一样使用
例如,对于一个存储自定义类型 MyClass 的链表,使用迭代器访问 MyClass 的成员时,我们希望能够像使用指针一样直接通过 -> 操作符访问成员,如下所示:
namespace xiaobai
{template<class T>struct list_node{list_node(const T& data = T()): _data(data), _next(nullptr), _prev(nullptr) {}T _data;list_node<T>* _next;list_node<T>* _prev;};template<class T>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T> Self;list_iterator(Node* node) : _node(node){}T& operator*() {return _node->_data;}Self& operator++() {_node = _node->_next;return *this;}bool operator!=(const Self& s) {return _node != s._node;}T* operator->() {return &_node->_data;}Node* _node;};
}int main()
{xiaobai::list_node<MyClass> node(MyClass(10));xiaobai::list_iterator<MyClass> it(&node);// 使用迭代器的 -> 操作符访问成员std::cout << it->value << std::endl;return 0;
}
在这个例子中,it->value 能够正常工作,因为 operator->() 返回的是 MyClass* 类型的指针,编译器可以直接通过该指针访问 value 成员
2. 避免额外的复杂度
如果 operator->() 返回 T&,编译器会再次对返回的引用应用 -> 操作,这会导致代码逻辑变得复杂,而且可能不符合用户的预期
例如:若T是一个自定义类型,由于T&不是指针类型,编译器会尝试调用 T 类型的 operator->() 函数,这可能会引发编译错误或意外的行为
所以T* operator->() 是为了让迭代器能够像指针一样使用,符合用户的使用习惯,并且遵循了 C++ 中 operator-> 操作符的特殊语义。而 T& operator->() 会破坏这种语义,导致代码逻辑复杂且不符合预期,因此通常不这样设计
这段代码初步看起来会很奇怪,它这里实际应该是两个箭头,但是为了可读性省略了一个箭头
(具体细节请浏览上面的疑难解答)
cout << itaa.operator->()->_aa1 << " and " << itaa.operator->()->_aa2 << endl;
第一个箭头是运算符重载,返回的是AA*类型的变量
第二个箭头是原生指针,通过这个箭头就可以访问到list中的数据了
迭代器代码初步集合
到这里我们就完成了迭代器:
template<class T>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T> Self;list_iterator(Node* node):_node(node){}T& 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;}T* operator->(){return &_node->_data;}Node* _node;};
此时再去链表的类中typedef迭代器,简化书写:
template<class T>class list{public:typedef list_node<T> Node;typedef list_iterator<T> iterator;//.......private:Node* _head;size_t _size;};
重载打印容器以及对迭代器的修缮
实现了迭代器,就满足了范围for遍历,此时就可以写一个访问容器的接口,因为所有容器都具有迭代器,所以这个接口也可以访问其他STL容器
template<class Container>void print_container(const Container& con){for (auto e : con){cout << e << " ";}cout << endl;}
现在测试一下这个打印:
list<int> lt;ltaa.push_back(1);ltaa.push_back(2);ltaa.push_back(3);ltaa.push_back(4);print_container(lt);
此时运行代码却发现编译报错了,为什么函数外面使用范围for可以打印,但是print_container函数里面使用范围for却打印不了,这是为什么呢?
因为函数里面的参数是const参数,函数外面是普通的参数,我们还没有实现const迭代器,所以这里就会报错
再来分析一下const迭代器的特征:先思考一下const迭代器为什么是const_iterator而不是普通的iterator加上前置的const修饰?
要想想清楚这个问题,我们首先需要明白const迭代器是自身不能修改还是指向的内容不能修改。这里我们结合指针不同位置的const意义来理解:
T* const ptr;
const T* ptr;
第一种:指针本身不允许改变
第二种:指针指向的内容不允许改变
很明显的,const迭代器是想完成第二种功能,如果是const iterator的话,const的作用是确保iterator本身不被修改,迭代器本身不能修改的话就无法实现遍历。const迭代器的产生是为了在保证遍历的前提之下保证迭代器指向的内容也不被修改
那么该怎么修改代码来完成这一目的呢?
我们在实现迭代器这一个类时,对数据的修改接口是operator*以及operator->,对于const迭代器而言,返回类型就应该加上const:
const T& operator*(){return _node->_data;}const T* operator->(){return &_node->_data;}
所以这里就需要拷贝之前的普通迭代器代码,再在*和->的重载中加上const完成const迭代器
template<class T>struct list_const_iterator{typedef list_node<T> Node;typedef list_iterator<T> Self;list_iterator(Node* node):_node(node){}const T& 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;}const T* operator->(){return &_node->_data;}Node* _node;};
此时在重载一下begin和end函数,给一个const迭代器版本,并且在list类中typedef const迭代器:
(普通迭代器可以调用const迭代器的begin和end,因为权限可以缩小;反之const迭代器无法调用普通迭代器的begin和end,因为权限不能放大)
template<class T>class list{public:typedef list_node<T> Node;typedef list_iterator<T> iterator;typedef list_const_iterator<T> const_iterator;iterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}//........private:Node* _head;size_t _size;};
**************************************************************************************************************
在 list 类中重载 `begin` 和 `end` 函数(即提供 `const` 和非 `const` 版本)是为了支持对 `const` 对象和非 `const` 对象的迭代操作,下面详细解释原因:
1. 支持非 `const` 对象的迭代
当你有一个非 `const` 的 `list` 对象时,你可能希望能够修改列表中的元素。此时,你需要使用非 `const` 迭代器,因为非 `const` 迭代器允许对其所指向的元素进行修改。非 `const` 版本的 `begin` 和 `end` 函数返回的就是非 `const` 迭代器
2. 支持 `const` 对象的迭代
当你有一个 `const` 的 `list` 对象时,你不应该修改列表中的元素,因为这违反了 `const` 限定的语义。此时,你需要使用 `const` 迭代器,`const` 迭代器只能用于访问元素,而不能修改元素。`const` 版本的 `begin` 和 `end` 函数返回的就是 `const` 迭代器
3. 函数重载的实现
通过函数重载,编译器会根据对象是否为 `const` 来选择合适的 `begin` 和 `end` 函数版本。如果对象是 `const` 的,编译器会调用 `const` 版本的 `begin` 和 `end` 函数,返回 `const` 迭代器;如果对象是非 `const` 的,编译器会调用非 `const` 版本的 `begin` 和 `end` 函数,返回非 `const` 迭代器。
重载 `begin` 和 `end` 函数是为了提供对 `const` 对象和非 `const` 对象的一致迭代接口,同时保证 `const` 对象的元素不被意外修改,遵循了 C++ 的 `const` 正确性原则,使得代码更加安全和灵活。
**************************************************************************************************************
此时用一个很特别的用例来测试一下代码(这里修改了一下print_container函数):
template<class Container>void print_container(const Container& con){list<int>::const_iterator it = con.begin();while(it != con.end()){*it += 10;cout << *it << " ";++it;}cout << endl;for (auto e : con){cout << e << " ";}cout << endl;}void test_list01(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;}
可以看到,这里非常奇怪,明明我们故意在打印容器代码里面修改了const迭代器指向的内容,想让它报错,但是代码却成功运行了,这是为什么呢?
这就涉及到我们之前提及的按需实例化,这里的代码存在着编译错误,' *it += 10 ' 中的*it返回的数据类型应该是const T&,对其进行+=10操作本身是应该编译报错的,但是这里非但没有报错反而还运行成功了
之所以产生这样的结果,是因为模板和类模板都会走按需实例化。模板不能被直接调用,模板用于将对应类型的代码实例化出来,实例化出来的代码才能够使用
因为我们在主函数中没有使用print_container函数,编译器在编译的过程中就不会去实例化print_container函数里面的内容,而没有实例化的代码编译器只会对其进行很简单的语法检查,比如:语句结束没加分号,对于细枝末节的操作编译器不会检查。
此时如果使用print_container函数就会报错:
list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;print_container(lt);
对迭代器代码的简化
刚才那种实现方式我们的确能成功的完成任务,但是const迭代器和普通迭代器除了operator*和->的代码有区别以外,其他地方的代码一模一样,这么设计的话在代码长度上就会非常的冗余,我们去底层观察一下是怎么实现迭代器的:
可以看到,它没有将迭代器和const迭代器定义为两个类,而是同一个类模板。而且它除了传入了 T ,还传入了 T& 和 T* 这两个参数
如果是普通迭代器,它的中的参数是 T T& T* ,分别传给了参数 T Ref Ptr ,而 Ref 和 Ptr 分别被重命名为 reference 和 pointer ,而 reference 和 pointer 分别又是 operator* 和 operator-> 的返回值;
如果是const迭代器,它的中的参数是 T 、const T&、 const T* ,分别传给了参数 T Ref Ptr , Ref 和 Ptr 分别被重命名为 reference 和 pointer ,而 reference 和 pointer 分别又是 operator* 和 operator-> 的返回值;
通过这种形式就控制住了operator*和->的返回值
虽然这两种写法在本质上没有区别,只是之前的写法是自己写了两个类,而这种方法是实现了一个类模板并且传了不同的模板参数给编译器,通过编译器来实例化出两个类。通过这样的方法,我们就可以实现精简代码的需求
那么就根据底层实现的原理来完善一下原来的代码吧:
template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}//.......Node* _node;};
template<class T>class list{public:typedef list_node<T> Node;typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//.......private:Node* _head;size_t _size;};
迭代器代码的最终整合
template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;list_iterator(Node* node):_node(node){}Ref 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;}Ptr operator->(){return &_node->_data;}Node* _node;};
begin、end函数
begin函数用于返回链表第一个元素:
iterator begin(){iterator it(_head->_next);return it;}
也可以用返回匿名对象来简化代码:
return iterator(_head->_next);
还有一种更加简单的写法:
return _head->_next;
因为这里返回的是一个节点的指针,节点的指针是可以隐式类型转换成iterator的,单参数的构造函数支持隐式类型的转换
end函数同理:
iterator end(){return _head->_prev;}
到这里,我们来测试一下:
list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
这里迭代器的设计相当完美,假设链表为空,此时就只有一个哨兵位的头节点。由于begin是_head的next,此时它指向的还是自己。end是_head,也同样指向自己,此时it = begin() == end在条件判断的时候就不会进入循环,造成非法访问
我们在实现迭代器的时候没有写拷贝构造函数,这就意味着指针在与指针的拷贝的时候进行的是浅拷贝,那么浅拷贝会不会出现问题呢?
答案是不会,我们就以上面的测试代码为例,我们给it赋了头节点,此时我们期望的就是it也指向原来的头节点,就是需要浅拷贝,而不是开一个新的链表,指向新的链表中的头节点
这里的迭代器只是一个访问和遍历链表的工具,它不像vector、string等容器一样,它所指向的资源并不是属于它自己的,它指向的资源是属于list的。所以它也不要写析构函数,它不能去释放list的空间
insert函数
insert函数用于在pos位置之前插入数据,要想实现这个功能还是比较简单的,仅需要通过简单的修改指针指向即可:
void insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;}
push_back和push_front函数
可以通过复用insert函数完成这两个函数:
void push_back(const T& x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}
erase函数
erase函数用于删除pos位置的节点,这个函数的实现也很简单,也是只需要修改指针指向再释放节点即可:
注意erase不能删除哨兵位的头节点,在这里加上assert断言
void 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;}
pop_back和pop_front函数
通过复用erase即可实现这两个函数:
void pop_back(iterator pos){erase(--end());}void pop_front(iterator pos){erase(begin());}
结尾
因为链表很多接口的实现非常简单,这里就没有把所有接口的实现代码一一列举出来了,下一节我们接着分析链表,我们将会分析迭代器失效并且完善list。那么本节的内容就到此结束了,谢谢您的浏览!!!!!!!!!
相关文章:

List的模拟实现(2)
前言 上一节我们讲解了list的基本功能,那么本节我们就结合底层代码来分析list是怎么实现的,那么废话不多说,我们正式进入今天的学习:) List的底层结构 我们先来看一下list的底层基本结构: 这里比较奇怪的…...
如何使用SaltStack批量替换SSL证书方案
以下是借助 SaltStack 批量替换 SSL 证书的完整方案,该方案结合了自动化更新与回滚机制,以保障操作的高效性与安全性: 一、准备工作 目录结构搭建 在 Salt Master 的 /home/salt/ssl_update 目录下构建如下结构:ssl_update/ ├──…...
Golang快速上手01/Golang基础
最近有需求,需要使用go,这几天快速过一遍基础语法,这是今天的总结 项目结构 #mermaid-svg-qpF09pnIik9bqQ4E {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-qpF09pnIik9bqQ4E .e…...
[Web 安全] 反序列化漏洞 - 学习笔记
关注这个专栏的其他相关笔记:[Web 安全] Web 安全攻防 - 学习手册-CSDN博客 0x01:反序列化漏洞 — 漏洞介绍 反序列化漏洞是一种常见的安全漏洞,主要出现在应用程序将 序列化数据 重新转换为对象(即反序列化)的过程中…...

【学习笔记】Google的Lyra项目:基于神经网络的超低比特率语音编解码技术
一、引言:语音通信的带宽挑战与技术突破 在实时音视频通信占据全球数字化生活核心地位的今天,Google于2021年推出的Lyra编解码器标志着语音编码技术进入新的时代。这款基于机器学习的新型音频编解码器以3kbps的极低比特率实现接近原始音质的语音重建能力…...

Unity Dedicated Server 控制台 输出日志LOg 中文 乱码
现象: 中文乱码 原因: Unity打包出来的.exe文件,语言一栏是英文,VS控制台出来不一样 解决方案: 新建.bat文件 ,并使用命令chcp 65001,运行时启动.bat,而不是.exe, 改不了exe属性,虽然有点奇怪ÿ…...

【Excel】 Power Query抓取多页数据导入到Excel
抓取多页数据想必大多数人都会,只要会点编程技项的人都不会是难事儿。那么,如果只是单纯的利用Excel软件,我还真的没弄过。昨天,我就因为这个在网上找了好久发好久。 1、在数据-》新建查询-》从其他源-》自网站 ,如图 …...

去耦电容的作用详解
在霍尔元件的实际应用过程中,经常会用到去耦电容。去耦电容是电路中装设在元件的电源端的电容,其作用详解如下: 一、基本概念 去耦电容,也称退耦电容,是把输出信号的干扰作为滤除对象。它通常安装在集成电路…...

HTTPS 与 HTTP 的区别在哪?
HTTP与HTTPS作为互联网数据传输的核心协议,其通信机制与安全特性深刻影响着现代网络应用的可靠性与用户体验。本文将解析两者的通信流程、安全机制及核心差异。 一、HTTP的通信机制 先来看看HTTP是什么吧。 HTTP基于TCP/IP协议栈,采用经典客户端-服务…...

let、const【ES6】
“我唯一知道的就是我一无所知。” - 苏格拉底 目录 块级作用域:var、let、const的对比:Object.freeze(): 块级作用域: 块级作用域指由 {} 包围的代码块(如 if、for、while、单独代码块等)形成的独立作用…...

openharmony5.0中hdf框架中实现驱动程序的动态加载和管理的技术细节分析
在分析openharmony的hdf框架的设备驱动加载器(IDriverLoader)时发现在创建实例时会首先判断一下是否完成了驱动入口的构建(HdfDriverEntryConstruct),如果没有构建会重新构建,这与我开始以为的不一致(我一直以为是采用的linux内核方式,只是由…...

TVS管学习记录
文章目录 前言一、TVS是什么?二、TVS关键参数1.反向截至电压**实际意义** 2.钳位电压**定义与作用****选择依据** **4. 实际应用示例****场景:通信端口的ESD保护** 3.反向截至电压和钳位电压的关联和区别**. 小结** 三、实际应用电路举例总结 前言 TVS管…...

数据库表的各种设计
本篇文章,主要讲解项目开发时,遇到不同的情况,要学会对数据库的表进行合理设计。 1、将表的某个字段,存到一张新表中 ①情况描述 ②操作步骤 第一步:创建role表 第二步:在user表中,删除role字…...

JWT使用教程
目录 JWT (JSON Web Token)1. JWT简介(1) 什么是JWT(2) JWT有什么用(3) JWT认证方式 2. JWT的组成部分3. 签名的目的4. JWT与Token的区别5 JWT的优势6 JJWT签发与验证token(1) 引入依赖(2) 创建 Token(3) 解析Token(4) 设置过期时间(5) 自定义claims 7. JWT自定义工具类 JWT (J…...

【大模型系列篇】如何解决DeepSeek-R1结构化输出问题,使用PydanticAl和DeepSeek构建结构化Agent
今日号外:🔥🔥🔥 DeepSeek开源周:炸场!DeepSeek开源FlashMLA,提升GPU效率 下面我们开始今天的主题,deepseek官方明确表示deepseek-r1目前不支持json输出/function call,可…...

老旧android项目编译指南(持续更)
原因 编译了很多项目,找到了一些可观的解决办法 1. android studio里面的jdk版本切换 jdk版本切换在这里,一般安卓开发需要用到4个版本的jdk,jdk8, jdk11, jdk17, jdk21新版的android stuio是默认使用高版本的jdk,所以切换版本是很有必要的 2. 命令…...

linux中安装部署Jenkins,成功构建springboot项目详细教程
参考别人配置Jenkins的git地址为https,无法连上github拉取项目,所以本章节介绍通过配置SSH地址来连github拉取项目 目录: 1、springboot项目 1.1 创建名为springcloudproject的springboot项目工程 1.2 已将工程上传到github中,g…...

AI开发利器:Anaconda
在Python开发过程中,不同的项目可能会依赖不同版本的Python以及各种不同版本的库。比如,项目A可能依赖Python 3.8和某个特定版本的numpy、TensorFlow和PyTorch,而项目B可能需要Python 3.9以及另一个版本的numpy库。如果直接在系统中安装Pytho…...

java网络编程--基于TCP协议的网络编程
Scoket介绍 利用 TCP 协议进行通信的两个应用程序是有主次之分的, 一个是服务器程序,一个是客户端程序, 两者的功能和编写方法不太一样, 其中 ServerSocket 类表示 Socket 服务器端,Socket 类表示 Socket 客户端。 服…...
PageHelper新发现
PageHelper 背景解决reasonablepageSizeZero 背景 今天发现了一个很有趣的现象,接手一个很老的项目springmvc项目、使用PageHelper分页实现常见的后端接口分页功能。但是发现当页码参数大于实际的页码数时、正常不应该返回数据,但是目前确一直返回数据不…...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...

Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...

MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...
Vue3中的computer和watch
computed的写法 在页面中 <div>{{ calcNumber }}</div>script中 写法1 常用 import { computed, ref } from vue; let price ref(100);const priceAdd () > { //函数方法 price 1price.value ; }//计算属性 let calcNumber computed(() > {return ${p…...

CSS3相关知识点
CSS3相关知识点 CSS3私有前缀私有前缀私有前缀存在的意义常见浏览器的私有前缀 CSS3基本语法CSS3 新增长度单位CSS3 新增颜色设置方式CSS3 新增选择器CSS3 新增盒模型相关属性box-sizing 怪异盒模型resize调整盒子大小box-shadow 盒子阴影opacity 不透明度 CSS3 新增背景属性ba…...