yo!这里是STL::vector类简单模拟实现

news2024/10/2 8:29:45

目录

前言

重要接口模拟实现

默认成员函数

1.构造函数

2.析构函数

3.拷贝构造函数

4.赋值运算符重载

迭代器

简单接口

1.size()

2.capacity()

3.swap()

操作符重载

1.操作符[]

扩容接口

1.reserve()

2.resize()

增删查改接口

1.push_back()

2.pop_back()

3.insert()

4.erase()

迭代器失效问题

1.问题及解决

 重载调用问题

1.问题及解决

后记


前言

        在模拟完string类之后,下一个我们来模拟实现的是STL中的vector类,相当于c语言中的顺序表,在一些接口的实现上可以参考顺序表的实现,所以这篇文章是在讲解vector的重要接口,一些普通接口不过多赘述。

        根据STL库里的vector,成员变量有_start、_finish、_end_of_storage,是三个指针变量,分别指向第一个数据的位置地址、最后一个数据的下一个位置地址、总申请空间的下一个位置地址,实则vector是一个类模板,实现为tempale <class T> class vector{ ... };,这里的细节不多说,重点看看下面接口的实现、底层逻辑以及参杂在其中的问题吧!

重要接口模拟实现

  • 默认成员函数

1.构造函数

        构造函数有多种重载形式,包括普通无参的默认构造函数、传迭代器区间构造函数、用n个值构造函数,

        ①无参的构造函数很简单,将三个指针变量置空即可,在初始化列表可以,在函数体内实现也可;

        ②定义函数模板InputIterator,而不直接使用iterator?因为可以传入不同类型的迭代器区间进行构造,不局限于类模板中的数据类型,这里也是复用的方式用尾插数据;

        注意:一定要在初始化列表置空成员变量,因为pushback一开始肯定会reserve,释放空间delete遇到未初始化指针会报错。

        ③用n个值构造也很好理解,传入值的个数和值,循环尾插值即可,其中,这里T()是匿名对象,若不传值进来,就会调用默认构造初始化val,(相当于int类型不传值就默认是0,指针不传值就默认是nullptr),如果T是自定义类型能理解,如果是内置类型,难道内置类型也有默认构造函数?yes!比如:int a = int(),此时a是0 。

        注意:使用 n个值构造函数构造时,需要加上下面代码中最后一个重载,这涉及到重载调用问题,在文章最后会介绍到。

代码:

    //无参构造函数
    Vector()
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{

	}

	//传迭代器区间拷贝
	template <class InputIterator>   
	Vector(InputIterator first, InputIterator last)
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		while (first != last)
		{
			push_back(*first);  
			++first;
		}
	}

	//n个值构造
	Vector(size_t n, const T& val= T())  
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		reserve(n);   //reserve里有delete,需要先在初始化列表置空
		for (size_t i; i < n; i++)
		{
			push_back(val);
		}
	}

	Vector(int n, const T& val = T())  
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		reserve(n);
		for (int i = 0; i < n; i++)
		{
			push_back(val);
		}
	}

2.析构函数

        对应的析构函数也不难,因为后面插入数据肯定是要申请空间的,所以在析构函数中需要去释放,也就是_start指向的地址空间,释放之后将三个指针置空即可。

代码:

	~Vector()
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}

3.拷贝构造函数

        对于拷贝构造函数,与模拟实现string时差不多,除了使用传统写法,是不是可以尝试使用现代写法呢?

        先看传统写法,很好理解,先开辟相同大小的地址空间,再将数据拷贝过去,然后将成员变量指向正确的位置,其中,size()是返回元素个数,capacity()是返回容量,值得注意的是,拷贝数据时不能使用memcpy,因为是值拷贝,如果T是自定义类型,就会在析构时出错,而要用赋值的方式(每一次赋值都是深拷贝);

        复用的方法也很简单,将原对象的数据循环尾插到目标对象,也要注意,在范围for那里传引用,因为里面的元素可能是自定义类型(比如string),那么传值给e,又是一次深拷贝,代价很大,所以建议用引用传值;

        现代写法:使用传迭代器构造函数定义一个临时对象,再使用swap函数(下面有介绍)交换两个对象的成员,而临时对象作为局部变量,出了作用域自动调用析构函数销毁,完美拷贝构造了目标对象。

代码:

    //传统方法
	Vector(const Vector<T>& v)
	{
		_start = new T[v.capacity()];
		//memcpy(_start, v._start, sizeof(T) * v.size());  
		for (size_t i = 0; i < v.size(); i++)   
		{
			_start[i] = v._start[i];
		}
		_finish = _start + v.size();
		_end_of_storage = _start + v.capacity();
	}
	//复用的方式
	Vector(const Vector<T>& v)
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		reserve(v.size());
		for (const auto& e : v) 
		{
			push_back(e);
		}
	}
	//现代写法
	Vector(const Vector<T>& v)
	{
		Vector<T> tmp(v.begin(), v.end());
		swap(tmp);
	}
	

4.赋值运算符重载

        赋值运算符重载的实现不多赘述,参考拷贝构造函数的现代写法。

代码:

	Vector<T>& operator=(const Vector<T>& v)
	{
		Vector<T> tmp(v.begin(), v.end());
		swap(tmp);
		return *this;
	}
  • 迭代器

        vector的迭代器也是原生指针,是里面存放的数据类型的指针,与string一样,可以参考http://t.csdn.cn/dYgNp ,同时也要加上const对象可以调用的迭代器。

 代码:

	typedef T* iterator;
	typedef const T* const_iterator;
	iterator begin()
	{
		return _start;
	}
	iterator end()
	{
		return _finish;
	}
	const_iterator begin() const
	{
		return _start;
	}
	const_iterator end() const
	{
		return _finish;
	}
  • 简单接口

1.size()

        size()是指数据元素的个数,因为_start指向第一个数据的位置地址、_finish是最后一个数据的下一个位置地址,所以元数个数就是_finish - _start 。

 代码:

	size_t size() const
	{
		return _finish - _start;
	}

2.capacity()

        capacity()是指所申请空间的个数,因为_start指向第一个数据的位置地址,_end_of_storage是总申请空间的下一个位置地址,所以元数个数就是_end_of_storage- _start 。

 代码:

	size_t capacity() const
	{
		return _end_of_storage - _start;
	}

3.swap()

        由于stl库里algorithm中的swap是深拷贝,会将两个对象的变量连同地址空间一块交换,代价较大,所以一般情况下,每个自定义类型要有自己的swap,交换过程则是复用stl里的swap函数,仅交换指针的指向。

 代码:

    void swap(Vector<T>& v)   
	{
		std::swap(_start, v._start);   //这里仅是交换这两个指针的指向
		std::swap(_finish, v._finish);
		std::swap(_end_of_storage, v._end_of_storage);
	}
  • 操作符重载

1.操作符[]

        由于vector底层就是个数组,所以stl库里也是提供[]加下标访问元素,实现与string中的[]操作符重载一致,不再赘述。

 代码:

	T& operator[](size_t pos)
	{
		assert(pos < size());
		return _start[pos];
	}
	const T& operator[](size_t pos) const
	{
		assert(pos < size());
		return _start[pos];
	}
  • 扩容接口

1.reserve()

        实际上,reserve()的实现也是参考string的模拟实现可以写出,注意点已在下方代码中标记,注意即可。

 代码:

	void reserve(size_t cap)
	{
		if (cap > capacity())
		{
			size_t ts = size();
			iterator tmp = new T[cap];
			//memcpy(tmp, _start, sizeof(T) * ts);  //也不能用memecpy
			for (size_t i = 0; i < ts; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
			_start = tmp;
			_finish = _start + ts;   //这里不能使用size(),因为此时start位置已经更新,应该使用旧size(),即ts
			_end_of_storage = _start + cap;
		}
	}

2.resize()

        resize()比reserve()多个初始化,即不仅要修改_end_of_storage的指向,还要修改_finish的指向,实现过程中,注意好三种情况:①要求容量比实际容量大;②要求容量比实际容量小,但比元素个数大;③要求容量比元素个数小

 代码:

	void resize(size_t cap, const T& val = T())
	{
		size_t ts = size();
		if (cap > capacity())
		{
			reserve(cap);
			for (size_t i = 0; i < capacity() - ts; i++)  //注意:不能是capacity()-size(),因为size()会变化
			{
				push_back(val);
			}
		}
		else
		{
			if (cap > ts)
			{
				for (size_t i = 0; i < cap - ts; i++)
				{
					push_back(val);
				}
			}
			else
			{
				_finish = _start + cap;
			}
		}
	}
  • 增删查改接口

1.push_back()

        push_back ()即尾插,传参最好是引用传参,否则有可能T是string等类型,深拷贝代价特别大,实现简单,不再赘述。

 代码:

	void push_back(const T& t)  
	{
		if (_finish == _end_of_storage)
		{
			reserve(capacity() ? capacity() * 2 : 4);
		}
		*_finish = t;
		_finish++;
	}

2.pop_back()

        pop_back()即尾删,删除最后一个元素,直接_finish指针--即可。

 代码:

	void pop_back()
	{
		assert(_finish > _start);
		_finish--;
	}

3.insert()

        在stl库里的insert()实现中,形参是插入位置的迭代器和需插入元素,返回插入元素的迭代器,根据string中insert的实现逻辑,这里的insert() 也是很容易实现出来,但这不是重点,值得注意的是会出现迭代器失效问题,文章最后会介绍并且解决。

代码:

	iterator insert(iterator pos, const T& t)
	{
		assert(pos >= _start);
		assert(pos <= _finish);
		if (pos == _finish)
		{
			push_back(t);
			return _finish;
		}

		if (_finish == _end_of_storage)  
		{
			size_t pos_len = pos - _start;  //①
			reserve(capacity() ? capacity() * 2 : 4);
			pos = _start + pos_len;  //②
		}
		iterator i = _finish;
		while (i > pos)  //③
		{
			*i = *(i - 1);
			--i;
		}
		*pos = t;
		++_finish;
		return pos;
	}

4.erase()

        erase()则是删除所传入迭代器的元素,规定返回所删除位置的下一个位置的迭代器,实现逻辑参考string模拟实现也不难写出,这里讨论删除一定数量的元素是否应该缩容?

        答:不建议缩容,因为缩容是以时间换空间,效率低下,如果涉及到缩容,就有可能引发迭代器失效问题(看了后面介绍就知道为什么会引发),但也是建议不要删除pos位置的数据之后,再次访问pos迭代器,因为排除不了其他库对erase()的实现没有缩容。

 代码:

    iterator erase(iterator pos)
	{
		assert(pos >= begin());
		assert(pos < end());
		if (pos == end() - 1)
		{
			pop_back();
			return end();
		}

		iterator i = pos;
		while (i < _finish - 1)
		{
			*i = *(i + 1);
			i++;
		}
		--_finish;
		return pos;
	}

迭代器失效问题

1.问题及解决

           在上面的insert()、erase()实现中都有提到一个迭代器失效问题,是什么呢?先说结论:在insert()、erase()函数中,不要直接访问pos,访问了要更新,不然会出现意料之外的结果,这就是迭代器失效。看看下面三种情况:

        情况一(野指针的失效):扩容/缩容之后pos就会失效。见代码一,这是insert实现的一段代码,当需要扩容时,调用reserve函数删除旧空间,开辟新空间,就会导致pos指针所指向的空间被释放,进而导致③处进入死循环。
        解决:加上①②处语句,即记录原本pos距离开头的位置,reserve回来之后更新pos位置即可。

        情况二:调用insert()之后,再次使用pos,此时pos可能因为扩容变了,或者插入了数据之后变了,导致失效。看下面代码二,使用了stl中的insert()的一段,看上去很正确,但执行就会报错,如下图:

 而将标记处注释掉,就可以正常运行,如图:

        说明:若insert函数中发生扩容,释放旧空间开辟新空间后,pos指向空间释放或者指向新位置,但由于是一份拷贝,不会影响实参,所以外面的pos没有发生变化,当再次使用pos时就会失效,那么这里为什么insert函数不传pos的引用呢?
         答:首先库里没用,其次就是会有后患,比如再次调用insert(v.begin(),80);其中begin()函数返回迭代器的拷贝,具有常性,无法成为insert函数实参传引用到insert函数里。

         解决:这是一个固有问题,无法解决,只能避免这种情况,即在pos位置插入数据之后,不要再访问迭代器pos,因为pos可能已经失效。

        

        情况三:循环调用insert()、erase()函数(比如删除所有的偶数)时,注意控制去遍历的迭代器变量,控制不好就会发生失效,如下代码三,错误实现会报错:

 正确与错误实现的关键区别在于:erase函数会返回所删除元素的下一个位置的迭代器,不需要再次it++,而没调用erase函数才需要it++。

        解决:insert()、erase()函数都会返回新的迭代器,在使用这两个函数时一定要注意控制迭代器。

代码一:

		if (_finish == _end_of_storage)  
		{
			size_t pos_len = pos - _start;  //①
			reserve(capacity() ? capacity() * 2 : 4);
			pos = _start + pos_len;  //②
		}
		iterator i = _finish;
		while (i > pos)  //③
		{
			*i = *(i - 1);
			--i;
		}

代码二:

int main()
{
	Vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	Vector<int>::iterator pos = find(v1.begin(), v1.end(), 3);
	if (pos != v1.end())
	{
		v1.insert(pos, 80);

        //再次访问迭代器pos
        cout << *pos << endl;   //标记处
	}

    for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

    return 0;
}

代码三:

//正确
int main()
{
	Vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	//v1.push_back(5);

	
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			it = v1.erase(it);
		}
		else
		{
			++it;
		}
	}

    return 0;
}

//错误
int main()
{
	Vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	//v1.push_back(5);

	
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			it = v1.erase(it);
		}

		++it;
	}

    return 0;
}

 重载调用问题

1.问题及解决

        介绍:当有多个重载形式时,调用此函数要格外小心,因为会调用形参与实参最匹配的重载形式,导致没能调用到目标函数,而产生意料之外的结果。

        eg:当使用n个值构造函数构造时,会发现Vector<int> v(10, 1)会报错,但Vector<int> v(10, 'a')不会报错,因为什么?
        原因:对于多个重载形式,调用时编译器会用参数匹配度最高的函数,Vector<int> v4(10, 1)中的实参类型相同且都是int,所以会调用传迭代器区间构造函数,而不是n个值构造函数,因为10和1都是int类型,正好符合first和last(又不是一定要传迭代器),而n个值构造函数是size_t和int类型,不够符合,从而进入传迭代器区间构造函数,而函数中有个*first,即对int类型解引用,所以会报间接寻址的错,而Vector<int> v4(10, 'a')由于参数类型不同,正好匹配n个值构造函数,所以不会报错。

        解决:重载下方代码中的第二个构造函数(Vector(int n, const T& val = T()) ),传两个int类型时,就会调用此构造函数,因为比传迭代器区间拷贝函数匹配度更高。

代码:

	//n个值构造
	Vector(size_t n, const T& val= T())  
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		reserve(n);   //reserve里有delete,需要先在初始化列表置空
		for (size_t i; i < n; i++)
		{
			push_back(val);
		}
	}

	Vector(int n, const T& val = T())  
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		reserve(n);
		for (int i = 0; i < n; i++)
		{
			push_back(val);
		}
	}

后记

        在模拟实现string的基础上,模拟出大部分vector的接口函数并不难,难以捉摸的是此外出现的问题,而模拟实现vector类的重点也正是如此,要不然也并不值得作为一篇博客,希望能够抓住重点,理解vector的重点接口实现以及衍生问题的解决,多多练习,拜拜!


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

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

相关文章

vue页面布局

布局 用element-plus自带的布局&#xff1b; 左边菜单 用他的Menu 菜单、自带收缩和展开&#xff1b;数据可以接口获取或者写死&#xff1b; 使用的如下操作、把主题和默认打开的index存到缓存中 头部&#xff1b; 简单的先分成左右&#xff1b;再简单的分成左右 1、左…

CS 144 Lab Four -- the TCP connection

CS 144 Lab Four -- the TCP connection TCPConnection 简述TCP 状态图代码实现完整流程追踪 测试 对应课程视频: 【计算机网络】 斯坦福大学CS144课程 Lab Three 对应的PDF: Lab Checkpoint 4: down the stack (the network interface) TCPConnection 简述 TCPConnection 需…

Python系统学习1-3-变量,运算符

1、变量 变量&#xff1a;关联一个对象的标识符 学习目标&#xff1a;学会画变量的内存图 命名规则:字母数字下划线&#xff0c;所有单词小写&#xff0c;单词之间下划线隔开 赋值&#xff1a;创建一个变量或改变一个变量关联的数据。 语法&#xff1a;变量名数据&#xf…

vue运行在IE浏览器空白报错SCRIPT1006: 缺少‘)‘ -【vue兼容IE篇】

其他浏览器均正常&#xff0c;但是切换ie模式&#xff0c;打开空白&#xff0c;F12打开报错缺少‘)‘ &#xff0c;如下图 在搜狗浏览器下点开报错&#xff1a;定格在crypto-js处 解决&#xff1a; 步骤一&#xff1a;使用npm安装babel-polyfill 依赖&#xff08;已安装了可忽…

Java与Kotline Funcation函数与参数函数的详解

一.介绍 在现在以IDE为开发工具的时代&#xff0c;各种开发语言都有&#xff0c;kotlin的语法势头比较强&#xff0c;今天我们将介绍在项目中出现比较多的两种函数&#xff0c;一种是参数函数&#xff0c;还有一种就是Function函数 如果你不了匿名函数请阅读以下文档&#xff…

IT 运营分析 (ITOA)

IT 运营 &#xff08;ITOps&#xff09; 是指向组织实施、管理、交付和支持 IT 服务&#xff0c;ITOps 可帮助组织维护和运行所需的所有技术工具&#xff0c;以保持业务活动以最高质量正常运行&#xff0c;同时降低成本。 一些常见的 ITOps 过程是&#xff1a; 问题整改&…

el-table 去掉边框(修改颜色)

原始&#xff1a; 去掉表格的border属性&#xff0c;每一行下面还会有一条线&#xff0c;并且不能再拖拽表头 为了满足在隐藏表格边框的情况下还能拖动表头&#xff0c;修改相关css即可&#xff0c;如下代码 <style lang"less"> .table {//避免单元格之间出现白…

Clickhouse 优势与部署

一、clickhouse简介 1.1 clickhouse介绍 ClickHouse的背后研发团队是俄罗斯的Yandex公司&#xff0c;2011年在纳斯达克上市&#xff0c;它的核心产品是搜索引擎。我们知道&#xff0c;做搜索引擎的公司营收非常依赖流量和在线广告&#xff0c;所以做搜索引擎的公司一般会并行推…

【LeetCode-简单】剑指 Offer 52. 两个链表的第一个公共节点

题目 输入两个链表&#xff0c;找出它们的第一个公共节点。 如下面的两个链表&#xff1a; 在节点 c1 开始相交。 输入&#xff1a;intersectVal 8, listA [4,1,8,4,5], listB [5,0,1,8,4,5], skipA 2, skipB 3 输出&#xff1a;Reference of the node with value 8 输…

想参加华为杯竞赛、高教社杯和数学建模国赛的小伙伴看过来

本文目录 ⭐ 赛事介绍⭐ 辅导比赛 ⭐ 赛事介绍 ⭐ 参赛好处 ⭐ 辅导比赛 ⭐ 写在最后 ⭐ 赛事介绍 华为杯全国研究生数学建模竞赛是由华为公司主办的一项面向全国研究生的数学建模竞赛。该竞赛旨在通过实际问题的建模和解决&#xff0c;培养研究生的创新能力和团队合作精神&a…

【ASP.NET MVC】使用动软(四)(12)

一、筛选器类和Cookie实现路由 需解决的问题&#xff1a; 网站登录往往需要用户名密码验证&#xff0c;为避免重复验证&#xff0c;一般采用Cookie 、Session等技术来保持用户的登录状态&#xff1a; Session是在服务端保存的一个数据结构&#xff0c;用来跟踪用户的状态&…

EtherCAT转MODBUS RTU/RS485/232总线协议网关

产品功能 JM-ECT-RTU是一款EtherCAT从站功能的通讯网关。该产品主要功能是将EtherCAT网络和MODBUS-RTU网络连接起来。 JM-ECT-RTU网关连接到EtherCAT总线中作为从站使用&#xff0c;连接到MODBUS-RTU总线中作为主站或从站使用。 本网关产品将基于MODBUS 的设备或串行RS-232/…

10分钟理解React生命周期

前言 学习React&#xff0c;生命周期很重要&#xff0c;我们了解完生命周期的各个组件&#xff0c;对写高性能组件会有很大的帮助。 一、简介 React /riˈkt/ 组件的生命周期指的是组件从创建到销毁过程中所经历的一系列方法调用。这些方法可以让我们在不同的时刻执行特定的…

科班应届生,我选择来黑马提升技能!

不论是因为对未来的迷茫和焦虑&#xff0c;还是对生活的现状不满意&#xff0c;又或者是想完善自己的专业知识&#xff0c;亦或是跨界迎接新的挑战&#xff0c;都可以来黑马…… 学科 | JavaEE 校区 | 武汉 薪资 | 10k&#xff08;应届生&#xff09; 黑马程序员的学弟、学妹…

【方法】Excel表格如何拆分数据?

当需要把多个数据逐个填到Excel单元格的时候&#xff0c;我们可以利用Excel的数据拆分功能&#xff0c;可以节省不少时间。 小编以下面的数据为例&#xff0c;看看如何进行数据拆分。 首先&#xff0c;要选择数字所在的单元格&#xff0c;然后依次点击菜单栏中的“数据”>…

Django实现音乐网站 ⑹

使用Python Django框架制作一个音乐网站&#xff0c; 本篇主要是在添加编辑过程中对后台歌手功能优化及表模型名称修改、模型继承内容。 目录 表模型名称修改 模型继承 创建抽象基类 其他模型继承 更新表结构 歌手新增、编辑优化 表字段名称修改 隐藏单曲数和专辑数 姓…

Redis 单线程VS多线程

面试题 redis到底是单线程还是多线程&#xff1f;IO多路复用是什么&#xff1f;redis为什么快&#xff1f; Redis单线程 是什么 Redis的版本很多3.x、4.x、6.x&#xff0c;版本不同架构也是不同的&#xff0c;不限定版本问是否单线程也不太严谨。 1、版本3.x &#xff0c;最…

中外人工智能专家共话大语言模型与 AI 创新

文章目录 一、前言二、主要内容三、总结 &#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、前言 智源社区活动&#xff0c;中外人工智能专家共话大语言模型与 AI 创新。 对谈书目&#xff1a; 《大模型时代》&#xff0c;龙志勇、黄雯 著&#xf…

.Net6 Web Core API --- Autofac -- AOP

目录 一、AOP 封装 二、类拦截 案例 三、接口拦截器 案例 AOP拦截器 可开启 类拦截器 和 接口拦截器 类拦截器 --- 只有方法标注 virtual 标识才会启动 接口拦截器 --- 所有实现接口的方法都会启动 一、AOP 封装 // 在 Program.cs 配置 builder.AddAOPExt();//自定义 A…

软件测试方案模板

第一章 概述 ​ 软件的错误是不可避免的&#xff0c;所以必须经过严格的测试。通过对本软件的测试&#xff0c;尽可能的发现软件中的错误&#xff0c;借以减少系统内部各模块的逻辑&#xff0c;功能上的缺陷和错误&#xff0c;保证每个单元能正确地实现其预期的功能。检测和排…