【STL】模拟实现简易 list

news2024/11/25 13:00:14

目录

1. 读源码

2. 框架搭建 

3. list 的迭代器

4. list 的拷贝构造与赋值重载

拷贝构造

赋值重载

5. list 的常见重要接口实现

operator--() 

insert 接口

erase 接口

push_back 接口

push_front 接口

pop_back 接口

pop_front 接口

size 接口

clear 接口

别忘了析构函数

源码分享

写在最后:


1. 读源码

读源码千万不能一行一行读啊,不然你就看晕在那里了,

我们先从核心框架开始抓取,比如说先找到 list 在哪:

 然后老规矩,我们先找他的成员变量:

那我们就来找找这个 link_type 是什么:

link_type 是 list_node*,list_node 是一个类类型,那我就知道了,

成员变量 node 就是链表的一个节点指针。

那问题又来了,有单链表,双链表,带头的链表等等,库里实现的是什么链表呢?

我们需要确定他的结构,还是老样子,我们先从构造函数和插入(核心)接口开始看:

我们先看看这个无参的构造是怎么实现的:

他先 get_node() 获取一个节点,然后再两个指针指向自己,

那我们基本就能确定这是一个带头双向循环的链表了。 

那我们奖励自己再看一眼他的 get_node() 吧

我们就可以看到他是通过空间配置器的接口开空间了,

再往下看其实就是定位 new 的那一套操作了。

我们继续接着来看 push_back() 接口是怎么样的:

 

我们可以看到,他这里就是复用的 insert,在 end() 位置插入,

 

他这里调用的就是这个 insert 的重载,就是普通的插入操作。

最后我们再来瞅一眼 node 这个节点类库里是怎么定义的:

他这里用了 void* 作为他的类型,我比较菜,不太懂这样做有什么妙用,

我就不这么麻烦去用 void* 作为我的指针类型了,不然之后每次用都得强转,我用 T* 就好了。

那么源码看到这里就差不多了,框架看的差不多了,到时候有问题再来看细节。

2. 框架搭建 

框架搭建主要就是把 list 的核心框架搭建出来,让代码快速跑起来:

#pragma once

#include <iostream>
#include <list>

#include <assert.h>

using namespace std;

namespace xl {
	template<class T>
	struct list_node {
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		list_node(const T& val = T())
			: _next(nullptr)
			, _prev(nullptr)
			, _val(val)
		{}
	};

	template<class T>
	class list {
	public:
		typedef list_node<T> Node;

	private:
		Node* _head;

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

	public:
		void push_back(const T& x) {
			Node* tail = _head->_prev;
			Node* newnode = new Node;
			
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	};
}

这里我们实现了 list 的节点,以及 list 的构造函数和尾插接口,

来看看测试:

#include "list.h"

void test1() {
	xl::list<int> lt1;
	lt1.push_back(1);
	lt1.push_back(2);
	lt1.push_back(3);
	lt1.push_back(4);

}

int main()
{
	test1();

	return 0;
}

通过调试来看结果:

我们确实是插入了 4 个节点,

你有没有觉得少了点什么,之前我们搭框架的时候都会实现一个基本的迭代器,

但是这次没有,问题来了, list 的迭代器很明显是不能用原生指针实现的,

毕竟你有见过链表能用指针或者说下标直接访问吗,那肯定是没见过,

那 list 的迭代器该怎么实现呢?

3. list 的迭代器

当我们不明白一件事情的时候,就去看源码是怎么做的:

找到了,但是更迷惑了,怎会有三个模板参数啊,

先就此打住,我们一点一点慢慢看,他的类型是一个类模板,那我们先去找到这个类:

我们看到这里,发现他是用一个叫 __list_iterator 的类来封装他的迭代器, 

而这个类的成员变量就是链表的节点:

那我们再来看看他的迭代器是怎么跑起来的(也就是++是怎么实现的)

我们发现这不就是让 node = node->next 的操作吗。

再来看看解引用的操作:

不出所料确实就是返回该节点的值,

但是他这个返回值的类型 reference 是啥东东呢?

这个是他的一个模板参数,看来看去,这源码很复杂,又有许多意义不明的操作,

我们还是先根据大思路上手试一下,遇到问题了再来细看源码的实现。

在搭完基本的架子之后,我们遇到了第一个问题,

begin 和 end 该指向什么位置?我们来看看库:

库里的 begin 返回的是第一个节点,end 返回的是哨兵位的头结点,

所以我们就这样实现即可:

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

iterator end() {
	return _head;
}

这个时候你可能又有疑问了,迭代器不是自定义类型吗?他怎么能返回节点的指针呢?

这就又用到我们前面学的知识了:单参数的构造函数支持隐式类型转换:

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

是的,我们在迭代器的类里实现了这样一个东西。

然后我们再把解引用实现了:

T& operator*() {
	return _node->_val;
}

最后还剩 ++ 和 != 需要实现:

iterator operator++() {
	_node = _node->_next;
	return *this;
}

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

这样我们的迭代器就跑通了,来看看测试:

void test2() {
	xl::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);

	xl::list<int>::iterator it = lt.begin();
	while (it != lt.end()) {
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

输出:

 这样我们的基本框架算是搭建完了:

#pragma once

#include <iostream>
#include <list>

#include <assert.h>

using namespace std;

namespace xl {
	template<class T>
	struct list_node {
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		list_node(const T& val = T())
			: _next(nullptr)
			, _prev(nullptr)
			, _val(val)
		{}
	};

	template<class T>
	struct __list_iterator {
		typedef __list_iterator<T> iterator;
		
		typedef list_node<T> Node;

		Node* _node;

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

		T& operator*() {
			return _node->_val;
		}

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

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

	template<class T>
	class list {
	public:
		typedef list_node<T> Node;

		typedef __list_iterator<T> iterator;

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

		iterator end() {
			return _head;
		}

	private:
		Node* _head;

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

	public:
		void push_back(const T& x) {
			Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	};
}

所以这里我们可以得出一个小结论,

list 的迭代器是什么?他是通过对自定义类型的封装,改变了他的行为。

那我们继续,现在来设计实现一个 const 迭代器,

我们可以通过加 const 来完成这件事情:

如果我们想要重载一整份迭代器,那岂不是得重新写一份自定义的 const 迭代器?

那这样设计也太冗余了,凭空又多出一大坨代码,有没有什么更好的方法实现呢?

还记得我们一开始看库的时候,那两个意义不明的模板参数吗?

他们还是同一个类,但是传了不同的模板参数。

然后就增加了迭代器的模板参数:

然后他这里就把迭代器重命名成了 self,我们就跟着库里的来:

首先是传模板参数这里,因为我们暂时只需要传 T* 给解引用的重载,

所以暂时先设置这两个模板参数:

实际上,这种做法和我们一开始否决的冗余写法没有本质上的区别,

因为模板的实例化就是再生成一段代码,只不过这个工作原本是由我们做,

使用模板之后变成让编译器帮我做了。

这里我把这个阶段的代码也放出来:

#pragma once

#include <iostream>
#include <list>

#include <assert.h>

using namespace std;

namespace xl {
	template<class T>
	struct list_node {
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		list_node(const T& val = T())
			: _next(nullptr)
			, _prev(nullptr)
			, _val(val)
		{}
	};

	template<class T, class Ref>
	struct __list_iterator {
		typedef __list_iterator<T, Ref> self;
		
		typedef list_node<T> Node;

		Node* _node;

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

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

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

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

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

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

	template<class T>
	class list {
	public:
		typedef list_node<T> Node;

		typedef __list_iterator<T, T&> iterator;
		typedef __list_iterator<T, const T&> const_iterator;

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

		iterator end() {
			return _head;
		}

	private:
		Node* _head;

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

	public:
		void push_back(const T& x) {
			Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	};
}

这里新的问题又来了,为什么库里是有三个模板参数呢?

我们来看看:

  

我们可以看到,还需要这个模板参数的是 -> 操作符的重载,

那事不宜迟,我们也来实现一下:

T* operator->() {
	return &_node->_val;
}

现在我们是正常的实现了这个操作符重载,

那这个操作符有什么应用场景吗?我们为什么要重载他?

来看这样一个场景:

struct A {
	A(int a1 = 0, int a2 = 0) 
		: _a1(a1)
		, _a2(a2)
	{}

	int _a1;
	int _a2;
};

void test3() {
	xl::list<A> lt;
	lt.push_back({ 1, 1 });
	lt.push_back({ 2, 2 });
	lt.push_back({ 3, 3 });
	lt.push_back({ 4, 4 });

	xl::list<A>::iterator it = lt.begin();
	while (it != lt.end()) {
		cout << (*it)._a1 << (*it)._a2 << endl;
		it++;
	}
}

如果我们想取结构体内的成员,可以通过 (*it). 来取,

但是我们一般更喜欢使用 -> 直接取结构体成员:

struct A {
	A(int a1 = 0, int a2 = 0) 
		: _a1(a1)
		, _a2(a2)
	{}

	int _a1;
	int _a2;
};

void test3() {
	xl::list<A> lt;
	lt.push_back({ 1, 1 });
	lt.push_back({ 2, 2 });
	lt.push_back({ 3, 3 });
	lt.push_back({ 4, 4 });

	xl::list<A>::iterator it = lt.begin();
	while (it != lt.end()) {
		cout << it->_a1 << it->_a2 << endl;
		it++;
	}
}

这个就是重载 -> 的意义。

但是,你有没有发现有一些不太对劲的地方?

这个函数返回的只是一个指针,而调用这个操作符重载需要一个 -> ,

然后,使用这个指针去调用结构体成员还需要一个 -> ,那为什么这里只有一个 -> 呢?

实际上是为了代码的可读性,编译器特殊处理让我们可以省略一个 -> 。

明白了这个之后,我们就再来添加一个模板参数给他用。

那这样我们的大框架总算是搭好了:

#pragma once

#include <iostream>
#include <list>

#include <assert.h>

using namespace std;

namespace xl {
	template<class T>
	struct list_node {
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		list_node(const T& val = T())
			: _next(nullptr)
			, _prev(nullptr)
			, _val(val)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator {
		typedef __list_iterator<T, Ref, Ptr> self;
		
		typedef list_node<T> Node;

		Node* _node;

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

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

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

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

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

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

		bool operator==(const self& it) {
			return _node != it._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;

		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;

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

	public:
		void push_back(const T& x) {
			Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	};
}

4. list 的拷贝构造与赋值重载

拷贝构造

直接通过复用 push_back 来完成拷贝构造。

list(const list<T>& lt)
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

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

赋值重载

我们就直接用现代写法实现,还能顺便把 swap 函数提供了:

void swap(list<T>& lt) { ::swap(_head, lt._head); }

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

我们可以来集中测试一下:

void test4() {
	xl::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	
	xl::list<int> lt3;
	xl::list<int> lt2(lt);
	lt3 = lt2;

	xl::list<int>::iterator it = lt3.begin();
	while (it != lt3.end()) {
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

输出:

5. list 的常见重要接口实现

operator--() 

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

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

insert 接口

我们先实现 insert 和 erase 接口,之后直接复用就好了:

// pos 位置之前插入
iterator insert(iterator pos, const T& x) {
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(x);

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

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

	return newnode;
}

erase 接口

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 next;
}

剩下的通通复用~        

push_back 接口

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

push_front 接口

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

pop_back 接口

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

pop_front 接口

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

不知道你爽了没,反正我爽了,现在就再来一些常见接口:

size 接口

我们直接用迭代器来计数:

size_t size() {
	size_t sz = 0;
	iterator it = begin();
	while (it != end()) {
		sz++;
		it++;
	}
	return sz;
}

clear 接口

清理所有的数据,直接复用 erase:

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

对了,差点忘了析构函数还没定义:

别忘了析构函数

直接复用 clear:

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

源码分享

Gitee链接:模拟实现简易STL: 模拟实现简易STL (gitee.com)

写在最后:

以上就是本篇文章的内容了,感谢你的阅读。

如果感到有所收获的话可以给博主点一个哦。

如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~

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

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

相关文章

数字验证码识别新思路及对opencv支持向量机机器学习总结

验证码识别框架 新问题 最近遇到了数字验证码识别的新问题。 由于这次的数字验证码图片有少量变形和倾斜&#xff0c;所以&#xff0c;可能需要积累更多的原始采样进行学习。但按照4个验证码10个数字的理论随机组合(暗含某种数字仅有少量变化&#xff0c;不然此组合数量还应更…

知识图谱--入门笔记

知识图谱–入门笔记-----整体的概念 1.什么是知识图谱&#xff1f; 描述现实世界中各个实体或者概念之间的关系&#xff0c;其构成一张海量的语义网络图&#xff0c;节点表示实体或者概念&#xff0c;边表示属性或者关系。 2.知识图谱中的三个节点 &#xff08;1&#xff09…

【LeetCode每日一题合集】2023.7.17-2023.7.23(离线算法 环形子数组的最大和 接雨水)

文章目录 415. 字符串相加&#xff08;高精度计算、大数运算&#xff09;1851. 包含每个查询的最小区间⭐⭐⭐⭐⭐解法1——按区间长度排序 离线询问 并查集解法2——离线算法 优先队列 874. 模拟行走机器人&#xff08;哈希表 方向数组&#xff09;918. 环形子数组的最大和…

会议OA项目之会议审批(亮点功能:将审批人签名转换为电子手写签名图片)

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于OA项目的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.主要功能点介绍 二.效果展示 三.前端…

AVLTree深度剖析(双旋)

在上一篇文章中我们提到了&#xff0c;单旋的情况&#xff08;无论是左单旋还是右单旋&#xff09;&#xff0c;都仅仅适用于绝对的左边高或者绝对的右边高 b插入&#xff0c;高度变为h1&#xff0c;我们可以来试试单旋会变成什么样子 旋完之后&#xff0c;形成了对称关系&…

自然语言处理之AG_NEWS新闻分类

前言: 先简单说明下&#xff0c;关于AG_NEWS情感分类的案例&#xff0c;网上很多博客写了&#xff0c;但是要么代码有问题&#xff0c;要么数据集不对&#xff0c;要么API过时&#xff0c;在这里我再更新一篇博客。希望对各位同学有一定的应用效果。 1、DataSets 数据集的处理…

部署mycat2

因为mycat是Java写的&#xff0c;要装jdk 下载包 jdk-8u261-linux-x64.rpm 安装 安装好后&#xff0c;查看版本 安装mycat2 解压到data目录 修改权限 把所需的jar复制到mycat/lib目录 查看MyCat目录结构 回为mycat代理连接启动时需要有一个默认的数据源&#xff0c;所以我们…

C#读取写入文件的三种方式

最新对文件的操作比较频繁。这里记录一下常用的几种文件读写的方式。 我这里使用窗体来做测试&#xff0c;例子在文末&#xff0c;可下载。 1&#xff1a;二进制读写 /// <summary>/// 二进制写入文件/// </summary>private void button1_Click(object sender, E…

PuTTY连接服务器报错Connection refused

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

基于SpringBoot + Vue实现单个文件上传(带上Token和其它表单信息)的前后端完整过程

有时遇到这种需求&#xff0c;在上传文件的同时还需要带上token凭据和其它表单信息&#xff0c;那么这个需求前端可以使用FormData数据类型来实现。FormData和JSON一样也是通过body传递的&#xff0c;前者支持字符串和二进制文件&#xff0c;后者只能是字符串&#xff0c;如下图…

[CISCN 2023 初赛]go_session 解题思路过程

过程 下载题目的附件&#xff0c;是用go的gin框架写的后端&#xff0c;cookie-session是由gorilla/sessions来实现&#xff0c;而sessions库使用了另一个库&#xff1a;gorilla/securecookie来实现对cookie的安全传输。这里所谓的安全传输&#xff0c;是指保证cookie中的值不能…

STM32 USB使用记录:HID类设备(前篇)

文章目录 目的基础说明HID类演示代码分析总结 目的 USB是目前最流行的接口&#xff0c;现在很多个人用的电子设备也都是USB设备。目前大多数单片机都有USB接口&#xff0c;使用USB接口作为HID类设备来使用是非常常用的&#xff0c;比如USB鼠标、键盘都是这一类。这篇文章将简单…

向npm注册中心发布包(上)

目录 1、创建package.json文件 1.1 fields 字段 1.2 Author 字段 1.3 创建 package.json 文件 1.4 自定义 package.json 的问题 1.5 从当前目录提取的默认值 1.6 通过init命令设置配置选项 2、创建Node.js 模块 2.1 创建一个package.json 文件 2.2 创建在另一个应用程…

UE5 AI移动无动作问题

文章目录 问题背景解决方法问题背景 在使用行为树让角色移动时,出现角色行走不播放动作的情况: 解决方法 其实有2个问题导致出现这种情况 1、角色动画蓝图的问题 角色动画蓝图可能存在4个问题: ① 无播放行走动画 ② 速度的值未正常传递 ③ 播放移动动作逻辑的值判断错…

【每日一题】——C - Standings(AtCoder Beginner Contest 308 )

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

Clion开发STM32之W5500系列(DNS服务封装)

概述 在w5500基础库中进行封装&#xff0c;通过域名的方式获取实际的ip地址用于动态获取ntp的ip地址 DNS封装 头文件 /*******************************************************************************Copyright (c) [scl]。保留所有权利。****************************…

JVM对象在堆内存中是否如何分配?

1&#xff1a;指针碰撞&#xff1a;内存规整的情况下 2&#xff1a;空闲列表: 内存不规整的情况下 选择那种分配方式 是有 java堆是否规整而决定的。而java堆是否规整是否对应的垃圾回收器是否带有空间压缩整理的能力决定的。 因此当使用Serial,ParNew等带有压缩整理过程的收…

win10环境下,应用无法启动并被删除(无法完成操作,因为文件包含病毒或潜在的垃圾文件)

现象&#xff1a; 解决办法&#xff1a; 一、关闭所有自己安装的杀毒软件&#xff0c;如&#xff1a;360安全卫士&#xff0c;金山毒霸等 二、关闭win10本身的杀毒软件&#xff0c;步骤如下&#xff1a; 1、搜索栏输入“病毒和威胁防护” 2、进入以后&#xff0c;点击"病…

Pytorch迁移学习使用Resnet50进行模型训练预测猫狗二分类

目录 1.ResNet残差网络 1.1 ResNet定义 1.2 ResNet 几种网络配置 1.3 ResNet50网络结构 1.3.1 前几层卷积和池化 1.3.2 残差块&#xff1a;构建深度残差网络 1.3.3 ResNet主体&#xff1a;堆叠多个残差块 1.4 迁移学习猫狗二分类实战 1.4.1 迁移学习 1.4.2 模型训练 1.…

vue3基础+进阶(二、vue3常用组合式api基本使用)

目录 第二章、组合式API 2.1 入口&#xff1a;setup 2.1.1 setup选项的写法和执行时机 2.1.2 setup中写代码的特点 2.1.3 script setup语法糖 2.1.4 setup中this的指向 2.2 生成响应式数据&#xff1a;reactive和ref函数 2.2.1 reactive函数 2.2.2 ref函数 2.2.3 rea…