当前位置: 首页 > news >正文

【c++】:STL中vector的模拟使用及模拟实现

 

 

文章目录

  • 前言
  • 一.使用库中vector常用接口
  • 二.vector的模拟实现
  • 总结

 


前言

上一篇我们讲解了STL中的string的使用和模拟实现,这次我们就来讲解STL中的vector,vector相对于string来说模拟实现会难一些,难点在于迭代器失效问题和深浅拷贝问题。

首先介绍一下vector:

1. vector是表示可变大小数组的序列容器。
2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vecto的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完的。
5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。

 

一、使用库中的vector的常用接口

1.首先是对象的创建,使用vector必须确定类型否则会报错,push_back就是尾插一个数据,size()是求一个对象的长度与string相同,我们实现过string知道string会多开一个空间放\0,但是vector的capacity就是实际空间。

a155b3b18a2e4ff7bb60b461a6a55020.png

 2.vector的迭代器与string的使用方法相同,如下图:

c3af1c29782b43cfaca174a96a517496.png

3.reserve与string中的一样,只开空间改变capacity

c5e37c21d2904082978f61c21e628f96.png 4.resize与string一样,改变空间并且初始化,如果空间小于原先的size就删除多余的数据:

7d0d249cbeb7439d898c3d45222de62b.png

f0b1c329c5e343bcbecd0489d14d4d31.png 5.pop_back是尾删

9c859f55a4114d9fb46520b03c3d71fd.png

6.insert 通常是配合find去使用

72cab77ebb3545578374f5f029064d21.png

int main()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);vector<int>::iterator pos = find(v.begin(), v.end(), 2);if (pos != v.end()){v.insert(pos, 50);}vector<int>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;return 0;
}

fad86f40024c47c38d4685ce7afa8cef.png

 上面我们是用find函数找到2的位置,然后将50插入到2的位置。

7.erase是删除一个数据,与insert一样要搭配使用

356c676c12054acfb021ea40cde7b817.png

60538ea778a24ea5b69e4323abb003ce.png

 我们修改刚刚的代码将插入改为删除,就成功删除了2这个数据,当然也可以删除多个需要迭代器实现:

0fcc3c8d22f84573acec748cc4130825.png

以上就是一些常用的vector的接口,vector的接口不再像string那样太冗余,我们可以通过下图看到库中vector的接口也不是很多:

f3d19ea199204ced88309909854b1b63.png

二、vector的模拟实现

vector的模拟实现与string一样,我们都会按照库中的源码进行实现但也不是完全的模仿源码,因为源码的封装性太强而我们并不需要像源码那样封装。

cbd88976a85e43b6a4193b7d5d363d62.png

 首先源码中的开头是禁止有人将源码售卖等并且有改动的话需要公开,对于源码来说我们只看最核心的那部分,至于其他的编译条件语句什么的我们并不关心。

84a110c5b10c43f4b9f22fac93368f68.png

 上图是源码中的成员变量,最重要的是iterator,这里还不容易看出来,我们再看下一张图:

20436ba7840341eda1bcd06b809c9c39.png

 相信大家已经看出来了吧,实际上库中的vector底层就是原生指针,只不过通过封装将指针名重命名为value_type,然后又将指针重命名为iterator,我们可以看到一共三个指针,_start,_finish,_end_of_storage通过名称我们可以辨认它们三个作用,再结合size()和capacity()的实现我们就能知道start指向起始位置,finish指向最后一个数据的后一个位置,因为只有这样指针相减才是size大小,而end很明显就是容量了。如下图所示:

7cc677f562b84a2faef7e67f45d2ef87.png

知道了底层是如何实现的我们就可以来模拟实现了,首先为了防止命名冲突我们将要模拟的vector写到我们自己的命名空间里。

namespace sxy
{template<class T>class vector{public:typedef T* iterator;private:iterator _start;iterator _finish;iterator _end_of_storage;};
}

 在这里我们用模板的目的是创造一个任意类型的指针,因为我们的vector是可以放任意类型的。首先我们typedef一个iterator来代表T*的指针,然后私有成员写出和源码一样的三个成员变量。

然后我们先写出size()和capacity()函数,如下:

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

 我们之前说过,当函数内不涉及修改成员变量时就加上const,这样普通对象和const对象就都能调用这个函数了。我们在实现一个判断是否为空的函数:

        bool empty() const{return _start == _finish;}

 要判断是否为空很简单,只需要看start是否等于finish,因为如果有数据finish会指向最后一个数据的下一个位置。

然后我们再实现一下[]符号的重载,因为遍历数组最方便的就是[]:

        T& operator[](size_t pos){assert(pos < size());return _start[pos];}const T& operator[](size_t pos) const{assert(pos < size());return _start[pos];}

 需要注意的是我们返回的对象是T类型的,前面我们说过vector是一个模板任何类型都能够使用所以我们不能只单单返回一个int类型,【】访问需要实现const版本的,因为有的静态对象也需要访问数据,这里我们在string中都讲解过就不再赘述。

我们先实现一个简单的构造函数,能完成初始化指针就可以:

        vector():_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){}

 初始化我们能在初始化列表初始化就在初始化列表初始化。

下面开始进入正题了,我们先实现push_back接口,对于这个接口我们需要考虑的是扩容问题:

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

 当finish等于end_of_storage时就需要扩容了,扩容的时候要注意不能直接扩容2倍的空间,因为刚开始capacity是有可能为0的,所以我们用一个三目操作符来解决这个问题,当容量为0时就开4个空间,否则就是2倍空间。当成功开完空间后我们就将要插入的值放入finish的位置然后让finish自加。下面我们来讲解扩容的问题:

        void reserve(size_t n){if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start){for (size_t i = 0; i < sz; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + sz;_end_of_storage = _start + n;}}

 在我们实现string的时候扩容什么的接口我们可以用memcpy等函数直接搞定那是因为string中的类型是char是内置类型,而vector是一个针对任意类型的模板,如果我们还用memcpy这样的浅拷贝的话是一定会出问题的,比如vector<string>里面存放的是string,如果用memcpy的话就导致vector里的每一个string对象都指向同一块空间,如下图:

c43aaebfb0614a0799bc39445053d3fc.png

这样在我们析构的时候释放空间就会将原来的空间释放两次程序就崩溃了,更具体的深浅拷贝问题我们在文章的最后面会更详细的讲解, 现在的实现大家只需要记住让对象一个一个去赋值即可。

reserve的实现与string中的一样,只有在重新开的空间大于原先的空间的时候我们才进行扩容,先用一个T类型的指针tmp开一个和原先capacity一样的空间,如果原先是有空间的不是空指针我们就将原先空间的值依次赋值给新空间,然后再将旧空间释放掉,在这里大家会有疑问,为什么要用一个变量提前记录size()的大小呢?我们画个图给大家解释一下:

98a7665d8762459697a296733ef0d8ab.png

 我们实现size()函数的时候使用finish-start的,而当我们开新空间后,size()的计算还是用旧空间上的指针来计算的,当我们将新空间给start后,这个时候的size()是计算不出来的如下图:

7d0e49c6f9fa4e8aadbd3975e69c907e.png

 我们很明显的看到两个指针都不在同一块空间上,计算出来的size()不知道有多大,也就是说将新空间交给start后原先的finish失效了计算不出来真实的size()了,这个时候我们就需要提前记录size(),由于finish的位置是start+size()所以我们提前记录size()。而end_of_storage的位置也变了,在这里一定要注意end_of_storage = _start + n而不是end_of_storage = _start + capacity,因为我们扩容空间已经将空间改变了。

实现了push_back后我们就可以实现pop_back了,pop_back就非常的简单了:

        void pop_back(){assert(!empty());--_finish;}

 尾删我们只需要让finish--向前移动一个位置即可,这个时候我们写的empty就派上用场了,我们尾删一定不可以一直让finish--,所以只有当不为空时才可以尾删。

下面我们实现一下迭代器,迭代器的实现与string没有很大的差别:

namespace sxy
{template<class T>class vector{public: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;}private:iterator _start;iterator _finish;iterator _end_of_storage;};
}

 同样我们要实现普通迭代器和const迭代器,这里与string无任何差别就不在介绍了。

接下来是resize接口,resize与string中的一样,当要开的空间小于capacity,就会将多余的数据删掉,当要开的空间大于capacity的时候,就要开空间并且初始化。

        void resize(size_t n, T val = T()){if (n < size()){_finish = _start + n;}else{if (n > capacity()){reserve(n);}while (_finish != _start + n){*_finish = val;++_finish;}}}

 当开的空间小于size()的时候我们就删掉多余的空间,这里为什么是小于size()而不是小于capacity呢?我们看下图:

97e463b2d48a4680b1a72e3245ec9161.png

我们可以看到如果开的空间小于capacity就删除数据就会有这样的问题,finish往后移动了而实际的数据不仅没删还多出了空白空间,而用size()就不会发生这个问题,当开的空间小于size()的时候我们直接将finish移到start+n的位置,如下图:

f6b1ba6fe78549df9d04f9c9f75e7bb5.png

当要开的空间大于capacity的时候我们就扩容,扩容后从finish的位置开始填要初始化的数据,这里要初始化的数据不可以给0,因为很有可能vector里面放的自定义类型,自定义类型必须用其构造函数进行初始化,所以大家可以看到val的缺省值我们给的是T(),这个我们之前讲过这是一个匿名对象。 

接下里介绍vector的重点insert接口和erase,为什么是重点呢?因为这两个接口会引起经典的迭代器失效问题。

        iterator insert(iterator pos, const T& val){assert(pos >= _start);assert(pos <= _finish);if (_finish == _end_of_storage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());//如果扩容pos指针将会失效所以需要重置pos的位置pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = val;++_finish;return pos;}

我们实现的insert只有从pos位置插入一个值即可,当然插入是有条件的不能随便插入,只有当要插入的位置大于等于start位置,小于等于finish位置才可以插入,等于start位置是头插,等于finish位置是尾插,当finish等于end_of_storage的时候我们就扩容,这里先不解释为什么要记录len等会我们画图解释一下,插入很简单,我们只需要让一个指针指向最后一个数据的位置,当end大于等于pos位置我们就把end位置上的数据放在end+1的位置上,然后让end--,当所有数据移完后我们就将要插入的数据放到pos位置,然后让finish++即可。如下图:

1a6cb2d83dbc4875aab187d86dd1c9cd.png

下面我们讲解失效问题,从上面代码大家应该也看出来了,失效只出现在扩容后如下图:

518407d4d321407a9c8c94e84208b1df.png

当我们扩容后start finish end_of_storage这三个指针都到了新空间上,而pos指针还指向旧空间,如果这个时候我们再往这个pos位置插入数据呢?答案显而易见,原先的那块空间都已经释放了再插入数据不就非法访问了吗,所以我们再扩容前先用len变量记录了pos的位置,然后在扩容后重置pos位置。

大家看下面这段代码的结果:

void test6(){vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);for (auto ch : v1){cout << ch << " ";}cout << endl;v1.insert(v1.begin(), 0);auto pos = find(v1.begin(), v1.end(), 3);if (pos != v1.end()){v1.insert(pos, 30);}for (auto ch : v1){cout << ch << " ";}cout << endl;(*pos)++;for (auto ch : v1){cout << ch << " ";}cout << endl;}

c23d1cd475b44b9d94b8450267575645.png

 我们的目的是在3的位置++,按道理应该是0 1 2 30 4 4 5才对,为什么加的是30这个位置呢?这是因为形参不能改变实参,我们在insert函数里面虽然已经更新了pos位置,但是insert函数内部的pos是形参是一份临时拷贝根本不会影响到test6()中的pos位置,这个pos位置还是指向原先那个位置的。那么如何解决这个问题呢?我们只需要让其insert多一个返回值返回pos位置即可。那么我们既然想要函数内改变引起函数外的改变为什么不用引用传pos呢?如果用引用这里是会报错的,iterator我们设计的时候是传值返回,传值返回会临时拷贝一个对象,临时对象具有常性是不能用引用的。

下面我们讲解erase接口:

        iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish);iterator end = pos + 1;while (end != _finish){*(end - 1) = *end;++end;}--_finish;return pos;}

erase的接口实现起来很简单,首先判断要删除的位置是否大于等于start并且小于finish。然后我们用一个指针指向pos+1的位置,只要end不等于finish我们就将end位置的值放入end-1的位置,移完数据后让finish--即可,这里要向insert一样返回pos位置,如下图:

e6ece421fb9746cf9f15f1477ae24153.png

那么在什么情况下erase会导致迭代器失效失效呢?如下图:

d20038ffddc146e08516379b1f1b41a9.png 

 通过上面的图片我们应该了解了迭代器失效问题,下面我们讲几个简单的接口:

        void swap(vector<T>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);}

与某个vector<T>类型的对象交换,我们传参传的是引用,直接用库中的swap即可,将指针交换指向即可。

析构函数:

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

析构我们直接delete掉start的空间,然后将三个指针置为空。

拷贝构造函数:

        vector(const vector<T>& v){_start = new T[v.capacity()];for (size_t i = 0; i < v.size(); i++){_start[i]=v._start[i];}_finish = _start + v.size();_end_of_storage = _start + v.capacity();}

 对于拷贝构造,我们直接给start开capacity大小的空间,然后将v中的数据依次挨个赋值给start,将数据都传到start后我们要将finish放在start+ size()的位置,然后将end_of_storage的位置放在start+capacity()的位置。

 拷贝构造我们实现了后赋值就非常简单了。

赋值运算符重载:

        vector<T>& operator=(vector<T> v){swap(v);return *this;}

我们直接传值传参,这样就会调用拷贝构造函数生成一份临时拷贝,然后用这份临时拷贝和*this交换,交换后*this就拥有了与v同样的数据,然后我们直接返回*this即可,这里既然我们要swap就一定不要穿引用传参,因为传引用会改变v的值,而赋值操作符是不会改变右边操作数的值的。

用n个val初始化的构造函数:

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

 首先我们在初始化列表将三个指针初始化为空指针,然后提前开n个大小的空间,然后依次将初始化的valpush_back到空间中,这里我们用的依旧是匿名对象做缺省值。

迭代器构造函数:

        template<class InputIterator>vector(InputIterator first, InputIterator last){while (first != last){push_back(*first);++first;}}

 迭代器的构造函数为什么我们用重新弄一个模板呢?这是因为我们用迭代器初始化不是一定要用vector的迭代器去初始化,我们还可能用string的迭代器去初始化,所以这里为了能使用各种迭代器我们直接弄一个迭代器模板,当first不等于last我们就push_back数据,然后让first++即可。

下面我们再来详细的讲解一下深拷贝问题:

a7af2bea3cb84f9291f14475607622f8.png

如上图所示的代码,如果我们用memcpy浅拷贝去扩容,去拷贝构造会发生什么呢?

0bcb786d101f4814886d77f33fbf44de.png

这个时候虽然vector的两个对象不在同一块空间,但是vector的每个string对象却指向了同一块空间,那么为什么我们用memcpy不行,用挨个赋值的方式就可以呢?这是因为string中对于赋值的运算符重载是深拷贝,是会给被赋值的对象先开好空间再将数据依次放入空间中,而我们的vector在扩容,拷贝构造函数中都用依次赋值的方法,只有我们将赋值运算符重载了变成深拷贝了才能解决vector的浅拷贝问题。下图是挨个赋值后的样子:

184fcce93e154338a7e938815d38395e.png

 这样我们就将vector模拟实现完成了。

 


总结

vector的模拟实现相对于string来说会稍微难一些,不知道大家会不会有疑问,为什么string没有迭代器失效问题呢?因为我们访问string是采用下标的形式,而vector用的是指针。对于vector,我们学的重点就是迭代器失效问题以及深浅拷贝问题,这里尤其是深浅拷贝问题一不小心就会发现程序莫名其妙崩溃,对于vector的学习建议大家一定要多练习并且配合画图来理解。

 

相关文章:

【c++】:STL中vector的模拟使用及模拟实现

文章目录 前言一.使用库中vector常用接口二.vector的模拟实现总结前言 上一篇我们讲解了STL中的string的使用和模拟实现&#xff0c;这次我们就来讲解STL中的vector&#xff0c;vector相对于string来说模拟实现会难一些&#xff0c;难点在于迭代器失效问题和深浅拷贝问题。 首…...

C++ STL:vector的使用方法及模拟实现

目录 一. vector概述 二. vector接口函数的使用方法和模拟实现 2.1 vector类模板的成员变量 2.2 构造函数的使用和模拟实现 2.2.1 构造函数的使用方法 2.2.2 构造函数的模拟实现 2.3 析构函数的模拟实现 2.4 赋值运算符重载函数的使用和模拟实现 2.4.1 函数的使用 2.…...

naive UI 的upload组件自定义手动上传图片的base64位

<template><n-upload ref"uploadRef" action"#" :default-upload"false" :custom-request"myUpload"><n-button>点击选择文件</n-button></n-upload><n-button click"submitUpload"> 上…...

信创办公–基于WPS的PPT最佳实践系列(表格和图标常用动画)

信创办公–基于WPS的PPT最佳实践系列&#xff08;表格和图标常用动画&#xff09; 目录应用背景操作步骤图表常用动画效果&#xff1a;擦除效果表格常用动画效果&#xff1a;轮子效果应用背景 文不如表&#xff0c;表不如图。在平时用ppt做总结时&#xff0c;我们会经常用到图…...

Spring Bean实例化和初始化的过程

承接上文Spring Bean生命周期应用程序在运行过程中能否去读取当前系统的环境变量或系统属性?这里涉及到一个非常重要的接口Environment&#xff0c;System.getenv&#xff0c;System.getProperties都是获取当前系统环境变量&#xff0c;Environment接口的实现类AbstractEnviro…...

WorkTool企微机器人接入智能问答

一、前言 最新版的企微机器人已经集成 Chat &#xff0c;无需开发可快速搭建智能对话机器人。 从官方介绍看目前集成版本使用模型为 3.5-turbo。 二、入门 创建 WorkTool 机器人 你可以通过这篇快速入门教程&#xff0c;来快速配置一个自己的企微机器人。 实现的流程如图&…...

C导入正则库问题

环境 操作系统:win11 专业版 gcc: gcc (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0 编辑器&#xff1a;vscode 要求 在c中使用正则表达式 遇到的问题以及解决思路 C标准中并没有正则表达式库 从其他地方下载正则表达式库即可。 http://gnuwin32.sourcefo…...

尚融宝05-Node.js入门

目录 一、Node.js的概念 1、JavaScript引擎 2、什么是Node.js 二、下载和安装 1、下载和安装 2、查看安装是否成功 三、初始Node.js程序 1、运行一个程序 常见问题 2、文件的读取 3、服务器端程序 三、Node.js的作用 1、Node.js的应用场景 2、BFF 解决什么问题 …...

「SAP ABAP」OPEN SQL(八)【WHERE语句大全】

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学本科在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后端的开发语言A…...

Ribbon负载均衡的原理(源码分析)

SpringCloud底层其实是利用了一个名为Ribbon的组件&#xff0c;来实现负载均衡功能的。1&#xff09;LoadBalancerIntercepor可以看到这里的intercept方法&#xff0c;拦截了用户的HttpRequest请求&#xff0c;然后做了几件事&#xff1a;1.request.getURI()&#xff1a;获取请…...

用sql计算两个经纬度坐标距离(米数互转)

目录 一、sql示例&#xff08;由近到远&#xff09; 二 、参数讲解 三、查询效果 - 距离&#xff08;公里 / 千米&#xff09; 四、查询效果 - 距离&#xff08;米&#xff09; 五、距离四舍五入保留后2位小数&#xff08;java&#xff09; 一、sql示例&#xff08;由近到远…...

C语言详解KMP算法

如果给你一个字符串 和 该字符串的一个子字符串 你能否快速找出该子字符串的所在位置我猜 这里会有一群杠精 说可以找到 真的吗 那下面这个字符串你可以一眼看出来吗你能找出来吗 如果能 算你眼神好 如果不能 那就看看接下来我怎么做你有想到暴力求解法吗&#xff1f;——来自百…...

redis在window上安装与自启动

需求&#xff1a; 客户window服务器使用redis&#xff0c;需要配置成在window服务中&#xff0c;并且可以随着电脑自启动服务。 下载 https://github.com/tporadowski/redis/releases打开上面的下载地址&#xff0c;这里我们下载zip压缩版本。 解压到待安装目录下&#xff…...

字符串匹配【BF、KMP算法】

文章目录:star:BF算法代码实现BF的改进思路:star:KMP算法&#x1f6a9;next数组&#x1f6a9;代码实现优化next数组最终代码⭐️BF算法 BF算法&#xff0c;即暴力(Brute Force)算法&#xff0c;是普通的模式匹配算法&#xff0c;BF算法的思想就是将主串S的第一个字符与模式串P…...

Leetcode.1616 分割两个字符串得到回文串

题目链接 Leetcode.1616 分割两个字符串得到回文串 Rating &#xff1a; 1868 题目描述 给你两个字符串 a和 b&#xff0c;它们长度相同。请你选择一个下标&#xff0c;将两个字符串都在 相同的下标 分割开。由 a可以得到两个字符串&#xff1a; aprefix和 asuffix&#xff0c…...

剑指 Offer II 033. 变位词组

题目链接 剑指 Offer II 033. 变位词组 mid 题目描述 给定一个字符串数组 strs&#xff0c;将 变位词 组合在一起。 可以按任意顺序返回结果列表。 注意&#xff1a;若两个字符串中每个字符出现的次数都相同&#xff0c;则称它们互为变位词。 示例 1: 输入: strs [“eat”,…...

spring-cloud-sentinel ---流控算法---review

计数器算法 计数器算法&#xff0c;限定每个固定时间能处理的请求总数&#xff0c;例如1分钟100&#xff0c;如果在第一个一分钟&#xff0c;总共请求60次&#xff0c;接着第二个一分钟&#xff0c;counter又会从0 开始技术&#xff0c;如果在1.5分钟的时候&#xff0c;达到了…...

1.浅析NIO 多路复用器selector

一&#xff1a;IO基本介绍 Java共支持3种网络编程IO模式&#xff1a;BIO&#xff0c;NIO&#xff0c;AIO 0.Java对BIO、NIO、AIO的支持&#xff1a; Java BIO &#xff1a; 同步并阻塞&#xff0c;服务器实现模式为一个连接一个线程&#xff0c;即客户端有连接请求时服务器端…...

Day920.结构化日志业务审计日志 -SpringBoot与K8s云原生微服务实践

结构化日志&业务审计日志 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于结构化日志&业务审计日志的内容。 1、什么是结构化日志 结构化日志&#xff08;Structured Logging&#xff09;是一种将日志信息组织为结构化数据的技术。 传统的日志通常是一些文…...

前端代码复用学习笔记:整洁架构与清晰架构

基础代码的复用往往比较简单&#xff0c;但是业务代码的复用通常是困难的&#xff0c;如果没有特殊的手段去治理项目会逐渐发展为难以维护的巨石应用&#xff0c;按照维基百科记载&#xff0c;代码的复用形式主要有三种&#xff0c;程序库&#xff0c;应用框架&#xff0c;设计…...

【python刷题】leecode官方提示“->“,“:“这些符号是什么意思?什么是Type Hints?

作者&#xff1a;20岁爱吃必胜客&#xff08;坤制作人&#xff09;&#xff0c;近十年开发经验, 跨域学习者&#xff0c;目前于海外某世界知名高校就读计算机相关专业。荣誉&#xff1a;阿里云博客专家认证、腾讯开发者社区优质创作者&#xff0c;在CTF省赛校赛多次取得好成绩。…...

【华为OD机试真题2023 JAVA】最佳对手

华为OD机试真题,2023年度机试题库全覆盖,刷题指南点这里 最佳对手 知识点排序DFS搜索回溯 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 游戏里面,队伍通过匹配实力相近的对手进行对战。 但是如果匹配的队伍实例相差太大,对于双方游戏体验都不会太好。 给定n个…...

css实现文字大小自适应

在页面编写中经常会碰到页面自适应的问题&#xff0c;也就是页面内部的元素会随着窗口的放大缩小而放大缩小&#xff0c;box可以通过calc 百分比的形式做到页面自适应&#xff0c;但是box内的字体却无法做到这点&#xff0c;往往box自适应大小了&#xff0c;内部的字体还是原来…...

【Redis】搭建哨兵集群

目录 集群结构 准备实例和配置 启动 测试 集群结构 这里我们搭建一个三节点形成的Sentinel集群&#xff0c;来监管之前的Redis主从集群。如图&#xff1a; 三个sentinel实例信息如下&#xff1a; 节点IPPORTs1192.168.150.10127001s2192.168.150.10127002s3192.168.150.…...

CTFHub | .htaccess

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习&#xff0c;实训平台。提供优质的赛事及学习服务&#xff0c;拥有完善的题目环境及配套 writeup &#xff0c;降低 CTF 学习入门门槛&#xff0c;快速帮助选手成长&#xff0c;跟随主流比赛潮流。 0x01 题目描述…...

微机原理 || 8253 芯片 (详细讲解 + 经典例题)

一点点看&#xff01;一定可以看懂&#xff01;考试没有问题的&#xff01;加油&#x1f4aa; 前面知识写的详细&#xff0c;看不懂可以先看典例&#xff0c;回头来梳理就明白了【典例就是常考的题】 目录 Part 1: 芯片知识总结 &#xff08;一&#xff09;8253 芯片特点 …...

python Django高级操作-分页-定义CVS-发送邮件

分页 分页是指在web页面有大量数据需要显示&#xff0c;为了阅读方便在每个页页中只显示部分数据。优点: 1.方便阅读2.减少数据提取量&#xff0c;减轻服务器压力。Paginator对像 负责分页数据整体的管理对象的构造方法Paginator属性 Paginator方法 Paginator异常exception pag…...

React 用一个简单案例体验一遍 React-dom React-router React-redux 全家桶

一、准备工作 本文略长&#xff0c;建议耐心读完&#xff0c;每一节的内容与上一节的内容存在关联&#xff0c;最好跟着案例过一遍&#xff0c;加深记忆。 1.1 创建项目 第一步&#xff0c;执行下面的命令来创建一个 React 项目。 npx create-react-app react-example cd rea…...

9. C#面向对象基础

一、C# 类 在 C# 中&#xff0c;类是引用类型的。类由成员属性和成员方法构成。我们可以动态创建类的实例&#xff08;instance&#xff09;&#xff0c;这个实例也被称为对象&#xff08;object&#xff09;&#xff0c;我们可以通过类和对象来设计程序。 1、类的定义 类的定…...

【MIT 6.S081】Lab2: system calls

本Lab包括两个简单系统调用的实现&#xff0c;进一步熟悉系统调用接口。 笔者用时约1.5h 概述 根据文档说明&#xff0c;当我们添加一个系统调用时&#xff0c;比如第一个任务是添加一个trace&#xff0c;需要进行以下操作&#xff1a; 首先将系统调用的原型添加到user/user…...