【C++】vector 的使用及其模拟实现

news2024/11/26 4:32:07

一、vector 的使用

vector 是我们学习的第一个真正的 STL 容器,它接口的使用方式和 string 有一点点的不同,但大部分都是一样的,所以这里我们就只演示其中一些接口的使用,大家如果有疑惑的地方直接在 cplusplus 是上面查看对应的文档即可。

1、构造函数

vector 提供了四种构造方式 – 无参构造、n 个 val 构造、迭代器区间构造以及拷贝构造:image-20221130162427586

其中构造函数的最后一个参数 alloc 是空间配置器,它和内存池有关,作用是提高空间分配的效率;我们日常使用时不用管这个参数,使用它的缺省值即可,但是可能有极少数的人想要用自己实现的空间配置器来代替 STL 库提供的,所以留出了这一个参数的位置。image-20221130163906531

需要注意的是,迭代器区间构造是一个函数模板,即我们可以用其他类来构造 vector 对象:image-20221130164323555

同时,上面还有一个非常重要的细节:

在 n 个 val 的构造中,val 的缺省值是 T 的匿名对象,该对象使用 T 的默认构造来初始化,而不是以 0 作为缺省值,这是因为 T 不仅仅可能是内置类型,也可能是自定义类型,比如 string、list、vector;

当 T 为自定义类型时,0 就不一定能够对 val 进行初始化,所以我们需要使用 T 的匿名对象来调用默认构造完成初始化工作;当 T 为内置类型时,我们仍然可以以这种方式进行初始化,因为 内置类型也具有构造函数,你没听错,内置类型也是有构造函数的,大家可以理解为,为了解决上面这种情况,编译器对内置类型进行了特殊处理;image-20221130104431455

利用匿名对象调用默然构造函数来作为缺省值的方法在下面 resize、insert 等接口中也有体现。

2、扩容机制

vector 的扩容机制和 string 的扩容机制是一样的,因为它们都是动态增长的数组:VS 下大概是 1.5 被扩容,Linux g++ 下是标准的二倍扩容,测试用例如下:

void TestVectorExpand() {
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:\n";
	for (int i = 0; i < 100; ++i) {
		v.push_back(i);
		if (sz != v.capacity()) {
			sz = v.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

image-20221130165120492

image-20221130165513728

3、三种遍历方式

和 string 一样,vector 也支持三种遍历方式 – 下标加[]遍历、迭代器遍历、范围for遍历:image-20221130170102726

需要注意的是,vector 和 string 之所以支持 下标 + [] 的方式遍历,是因为它们底层都是数组,而数组支持随机访问,但是像我们后面要学习的 list set map 等容器,它们的底层不是数组,不支持随机访问,就只能通过迭代器和范围 for 的方式进行遍历了;不过,范围 for 只是一个外壳,它在使用时也是被替换成迭代器,所以其实迭代器遍历才是最通用的遍历方式。

4、容量操作

vector 有如下容量相关的接口:image-20221130171031156

其中,最重要的两个函数是 reserve 和 resize,reserve 只用于扩容,它不改变 size 的大小;而 resize 是扩容加初始化,既会改变 capacity,也会改变 size;image-20221130171946440

注意:reserve 和 resize,包括后面的 clear 函数都不会缩容,因为缩容需要开辟新空间、拷贝数据、释放旧空间,而对于自定义类型又有可能存在深拷贝问题,时间开销极大;vector 中唯一可能缩容的函数就只有 shrink_to_fit,对于它来说,如果 capacity 大于 size,它会进行缩容,让二者相等。image-20221130172054823

5、元素访问

vector 提供了如下接口来进行元素访问:image-20221130173538875

其中,operator 和 at 都是返回 pos 下标位置元素的引用,且它们内部都会对 pos 的合法性进行检查;不同的是,operator[] 中如果检查到 pos 非法,那么它会直接终止程序,报断言错误,而 at 则是抛异常;image-20221130174119352

image-20221130174711329

注:release 模式下检查不出断言错误。

6、修改 – 迭代器失效

vector 提供了如下接口来进行修改操作:image-20221130174817482

assign && push_back && pop_back

assign 函数用来替换 vector 对象中的数据,支持 n 个 val 替换,以及迭代器区间替换,push_back 尾插、pop_back 尾插,这些接口的使用和 string 一模一样,这里就不再过多阐释;image-20221130195759960

insert && erase

和 string 不同,为了提高规范性,STL 中的容器都统一使用 iterator 作为 pos 的类型,并且插入/删除后会返回 pos:image-20221130202538822

image-20221130202557436

所以,以后我们如果要在中间插入或删除元素的话,必须配合算法库里面的 find 函数来使用:image-20221130202728072

image-20221130203446103

同时,在 VS 下,insert 和 erase 之后会导致 pos 迭代器失效,如果需要再次使用,需要更新 pos,如下:image-20221130203640015

image-20221130203738460

不过,在 Linux 下不会出现这个问题:image-20221130204055497

造成这个问题的根本原因是 VS 使用的 PJ 版本对 iterator 进行了封装,在每次 inset 和 erase 之后对迭代器进行了特殊处理,而 g++ 使用的 SGI 版本中的 iterator 是原生指针,具体细节在后文 vector 的模拟实现中我们再讨论;

但是为了代码的可移植性,我们 统一认为 insert 和 erase 之后迭代器会失效,所以,如果要再次使用迭代器,我们必须对其进行更新;我们以移除 vector 中的所有偶数为例:image-20221130204931492

swap

和 vector 一样,由于算法库 swap 函数存在深拷贝的问题,vector 自己提供了一个不需要深拷贝的 swap 函数,用来交换两个 vector 对象:image-20221130205429423

同时,为了避免我们不使用成员函数的 swap,vector 还将算法库中的 swap 进行了重载,然后该重载函数的内部又去调用成员函数 swap:image-20221130205953712

image-20221130210831942


二、vector 的模拟实现

1、浅析 vector 源码

对于编程来说,学习初期进步最快的方式就是阅读别人优秀的代码,理解其中的逻辑和细节后自己独立的去实现几次,学习 STL 也是如此;我们可以适当的去阅读 STL 的源码,当然我们并不是要逐行的进行阅读,因为这样太耗费时间,况且其中很多 C++ 的语法我们也还没学。

当前阶段,我们阅读 STL 源码是为了学习 STL 库的核心框架,然后根据这个框架自己模拟实现一个简易的 vector (只实现核心接口);阅读源码与模拟实现能够让我们更好的了解底层,对 STL 做到 能用,并且 明理

我们在 【STL简介 – string 的使用及其模拟实现】 中对 STL 做了一些基本的介绍,知道了 STL 由原始版本主要发展出了 PJ、RW 和 SGI 版本,其中,微软的 VS 系列使用的就是 PJ 版,但是由于其命名风格的原因,我们阅读源码时一般选择 SGI 版,而且 Linux 下 gcc/g++ 也是使用的 SGI 版本,再加上侯捷老师有一本非常著名的书 《STL源码剖析》也是使用的 SGI 版本,所以以后阅读和模拟实现 STL 时我都使用这个版本。

《STL源码剖析》电子版和 《stl30》源码我都放在下面了,需要的可以自取:

STL源码剖析:https://www.aliyundrive.com/s/Nc4mpLC43kj
stl30:https://www.aliyundrive.com/s/pnwMuB9uwEN

vector 的部分源码如下:

//vector.h
#ifndef __SGI_STL_VECTOR_H
#define __SGI_STL_VECTOR_H

#include <algobase.h>
#include <alloc.h>
#include <stl_vector.h>

#ifdef __STL_USE_NAMESPACES
using __STD::vector;
//stl_vector.h
template <class T, class Alloc = alloc>
class vector {
public:
  typedef T value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type* iterator;
  typedef const value_type* const_iterator;
  typedef value_type& reference;
  typedef const value_type& const_reference;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;
    
   //成员函数
    
protected:
  typedef simple_alloc<value_type, Alloc> data_allocator;
  iterator start;
  iterator finish;
  iterator end_of_storage;
}

可以看到,vector.h 仅仅是将几个头文件包含在一起,vector 的主要实现都在 stl_vector.h 里面。

2、核心框架

我们可以根据上面的 vector 源码来得出 vector 的核心框架:

namespace thj {
	template<class T>
    class vector {
    public:
        typedef T* iterator;
        typedef const T* const_iterator;

    public:
        //成员函数

    private:
        T* _start;
        T* _finish;
        T* _end_of_storage;
    };
}

可以看到,vector 的底层和 string 一样,都是一个指针指向一块动态开辟的数组,但是二者不同的是,string 是用 _size 和 _capacity 两个 size_t 的成员函数来维护这块空间,而 vector 是用 _finish 和 _end_of_storage 两个指针来维护这块空间;虽然 vector 使用指针看起来难了一些,但本质上其实是一样的 – _size = _finish - _start, _capacity = _end_of_storage - _start;image-20221130093303775

3、构造函数错误调用问题

在我们模拟实现了构造函数中的迭代器区间构造和 n 个 val 构造后,我们会发现一个奇怪的问题,我们使用 n 个 val 来构造其他类型的对象都没问题,唯独构造 int 类型的对象时会编译出错,如下:

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

//n个val构造
vector(size_t 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);
    }

image-20221130223227953

这是由于编译器在进行模板实例化以及函数参数匹配时会调用最匹配的一个函数,当我们将 T 实例化为 int 之后,由于两个参数都是 int,所以对于迭代器构造函数来说,它会直接将 InputIterator 实例化为 int;

但对于 n 个 val 的构造来说,它不仅需要将 T 实例化为 int,还需要将第一个参数隐式转换为 size_t;所以编译器默认会调用迭代器构造,同时由于迭代器构造内部会对 first 进行解引用,所以这里报错 “非法的间接寻址”;

解决方法有很多种,比如将第一个参数强转为 int,又或者是将 n 个 val 构造的第一个参数定义为 int,我们这里和 STL 源码保持一致 – 提供第一个参数为 int 的 n 个 val 构造的重载函数:image-20221130224838261

//n个val构造
vector(size_t 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个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);
    }

4、insert 和 erase 迭代器失效问题

我们模拟实现的 insert 和 erase 函数如下:

//任意位置插入
iterator insert(iterator pos, const T& x)
{
    assert(pos >= _start);
    assert(pos <= _finish);

    //扩容导致 pos 迭代器失效
    if (size() == capacity())
    {
        size_t oldPos = pos - _start;  //记录pos,避免扩容后pos变为野指针
        size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newCapacity);
        pos = _start + oldPos;  //扩容之后更新pos
    }

    iterator end = _finish - 1;
    while (end >= pos)
    {
        *(end + 1) = *end;
        --end;
    }

    *pos = x;
    ++_finish;
    return pos;
}

//任意位置删除 -- erase 之后也认为 pos 迭代器失效
iterator erase(iterator pos)
{
    assert(pos >= _start);
    assert(pos < _finish);

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

我们在 vector 的使用中就提到 VS 下 insert 和 erase 后迭代器会失效,再次访问编译器会直接报错,这是因为 PJ 版本下 iterator 不是原生指针,如下:image-20221130225551770

image-20221130225619075

可以看到,VS 中的迭代器是一个类,当我们进行 insert 或者 erase 操作之后,iterator 中的某个函数可能会将 pos 置为空或者其他操作,导致再次访问 pos 报错,除非我们每次使用后都更新 pos:image-20221130230528665

image-20221130230625835

而 Linux 下的 g++ 却不会出现这样的问题,因为 g++ 使用的是 SGI 版本,该版本的源码我们在上面也已经见过了,其迭代器是一个原生指针,同时它内部 insert 和 erase 接口的实现也和我们模拟的类似,可以看到,我们并没有在函数内部改变 pos (改变也没用,因为这是形参),所以 insert、erase 之后 pos 可以继续使用;image-20221130231000841

image-20221130231437769

但是这里也存在一个问题,insert 和 erase 之后 pos 的意义变了 – 我们插入元素后 pos 不再指向原来的元素,而是指向我们新插入的元素;同样,erase 之后 pos 也不再指向原来的元素,而是指向该元素的后一个元素;特别是当 erase 尾部的数据后,pos 就等于 _finish 了;

那么对于不了解底层的人就极易写出下面这样的代码 – 删除 vector 中的所有偶数:image-20221130233018001

image-20221130233101684

可以看到,第一个由于删除元素后 pos 不再指向原位置,而是指向下一个位置,所以 erase 之后会导致一个元素被跳过,导致部分偶数没有被删除,但好在末尾是奇数,所以程序能够正常运行;

但是第二个就没那么好运了,由于最后一个元素是偶数,所以 erase 之后 pos 直接指向了 _finish 的下一个位置,循环终止条件失效,发生越界。

综上,为了保证程序的跨平台性,我们统一认为 insert 和 erase 之后迭代器失效,必须更新后才能再次使用。

5、reserve 函数的浅拷贝问题

除了上面这两个问题之外,我们的 vector 还存在一个问题 – reserve 函数 深层次的浅拷贝问题,模拟实现的 reserve 函数如下:

void reserve(size_t n)
{
    if (n > capacity())  //reserve 函数不缩容
    {
        T* tmp = new T[n];
        memcpy(tmp, _start, sizeof(T) * size());
        size_t oldSize = _finish - _start;  //记录原来的size,避免扩容不能确定_finish
        delete[] _start;

        _start = tmp;
        _finish = _start + oldSize;
        _end_of_storage = _start + n;
    }
}

很多同学看到这段代码的时候可能会认为它没问题,的确,对于内置类型来说它确实是进行了深拷贝,但是对于需要进行深拷贝的自定义类型来说它就有问题了,如下:image-20221201000643954

image-20221201002804642

程序报错的原因如图:当 v 中的元素达到4个再进行插入时,push_back 内部就会调用 reserve 函数进行扩容,而扩容时我们虽然对存放 v1 v2 的空间进行了深拷贝,但是空间里面的内容我们是使用 memcpy 按字节拷贝过来的,这就导致原来的 v 里面的 string 元素和现在 v 里面的元素指向的是同一块空间。

当我们拷贝完毕之后使用 delete[] 释放原空间,而 delete[] 释放空间时对于自定义类型会调用其析构函数,而 v 内部的 string 对象又会去调用自己的析构函数,所以 delete[] 完毕后原来的 v 以及 v 中各个元素指向的空间都被释放了,此时现在的 v 里面的每个元素全部指向已经释放的空间。

从第一张图中我们也可以看到,最后一次 push_back 之后 v 里面的元素全部变红了;最终,当程序结束自动调用析构函数时,就会去析构刚才已经被释放掉的 v 中的各个 string 对象指向的空间,导致同一块空间被析构两次,程序出错。

所以,在 reserve 内部,我们不能使用 memcpy 直接按字节拷贝原空间中的各个元素,因为这些元素可能也指向一块动态开辟的空间,而应该调用每个元素的拷贝构造进行拷贝,如图:image-20221201004838430

具体代码实现如下:

//扩容
void reserve(size_t n)
{
    if (n > capacity())  //reserve 函数不缩容
    {
        T* tmp = new T[n];
        //memcpy(tmp, _start, sizeof(T) * size());  //error

        //memcpy有自定义类型的浅拷贝问题,需要对每个元素使用拷贝构造进行深拷贝
        for (int i = 0; i < size(); i++)
            tmp[i] = _start[i];  //拷贝构造

        size_t oldSize = _finish - _start;  //记录原来的size,避免扩容不能确定_finish
        delete[] _start;

        _start = tmp;
        _finish = _start + oldSize;
        _end_of_storage = _start + n;
    }

image-20221201005431302

注意:有的同学看到这里使用的是赋值运算符就认为这里调用的赋值重载,其实不是的,因为这里完成的是初始化工作,编译器会自动转换为调用拷贝构造函数。

6、模拟 vector 整体代码

在了解了 vector 的核心框架以及解决了上面这几个疑难点之后,剩下的东西就变得很简单了,所以我这里直接给出结果,大家可以根据自己实现的对照一下,如有错误,也欢迎大家指正:

//vector.h
#pragma once
#include <iostream>
#include <assert.h>
#include <string.h>
#include <algorithm>

namespace thj {  //防止命名冲突
	template<class T>
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

	public:
		//-------------------------------------constructor---------------------------------------//
		//无参构造
		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个val构造
		vector(size_t 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个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);
		}

		//拷贝构造 -- 写法1
		//vector(const vector<T>& v)
		//{
		//	T* tmp = new T[v.capacity()];
		//	memcpy(tmp, v._start, sizeof(T) * v.capacity());
		//	_start = tmp;
		//	_finish = _start + v.size();
		//	_end_of_storage = _start + v.capacity();
		//}

		//拷贝构造 -- 写法2
		//vector(const vector<T>& v)
		//	: _start(nullptr)
		//	, _finish(nullptr)
		//	, _end_of_storage(nullptr)
		//{
		//	reserve(v.capacity());
		//	for (size_t i = 0; i < v.size(); i++)
		//		push_back(v[i]);
		//}

		//拷贝构造 -- 现代写法
		vector(const vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			vector<T> tmp(v.begin(), v.end());  //复用构造函数和swap函数
			swap(tmp);
		}

		//析构函数
		~vector() {
			delete[] _start;
			_start = _finish = _end_of_storage = nullptr;
		}

		//赋值重载
		vector<T>& operator=(vector<T> v)  //复用拷贝构造,存在自我赋值的问题,但不影响程序正确性
		{
			swap(v);
			return *this;
		}

		//----------------------------------iterator---------------------------------------//
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

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

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

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

		//扩容
		void reserve(size_t n)
		{
			if (n > capacity())  //reserve 函数不缩容
			{
				T* tmp = new T[n];
				//memcpy(tmp, _start, sizeof(T) * size());  //error

				//memcpy有自定义类型的浅拷贝问题,需要对每个元素使用拷贝构造进行深拷贝
				for (int i = 0; i < size(); i++)
					tmp[i] = _start[i];  //拷贝构造

				size_t oldSize = _finish - _start;  //记录原来的size,避免扩容不能确定_finish
				delete[] _start;

				_start = tmp;
				_finish = _start + oldSize;
				_end_of_storage = _start + n;
			}
		}

		//扩容并初始化
		void resize(size_t n, T x = T())
		{
			if (n > capacity())  //resize 不缩容
			{
				reserve(n);
			}
			if (n > size())
			{
				while (_finish < _start + n)
				{
					*_finish = x;
					++_finish;
				}
			}
			if (n < size())
			{
				_finish = _start + n;
			}
		}
        
		//----------------------------------------element access---------------------------------//
		T& operator[](size_t pos)
		{
			assert(pos < size());  //检查越界
			return _start[pos];
		}

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

		//----------------------------------------modifys-----------------------------------------//
		//尾插
		void push_back(const T& n)
		{
			if (size() == capacity())
			{
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
			}
			*_finish = n;
			++_finish;
		}

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

		//任意位置插入 -- 插入后认为迭代器失效
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);

            //扩容会导致迭代器失效
			if (size() == capacity())
			{
				size_t oldPos = pos - _start;  //记录pos,避免扩容后pos变为野指针
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
				pos = _start + oldPos;  //扩容之后更新pos
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}

			*pos = x;
			++_finish;
			return pos;
		}

		//任意位置删除 -- erase 之后也认为 pos 迭代器失效
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

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

		//交换两个对象
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);  //复用算法库的swap函数
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

		void clear()
		{
			_finish = _start;
		}

	private:
		T* _start;
		T* _finish;
		T* _end_of_storage;
	};
}

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

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

相关文章

[附源码]计算机毕业设计springboot抗疫医疗用品销售平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

智引未来,利尔达成功入选首批“5G应用解决方案供应商推荐名录”名单

近日&#xff0c;5G应用产业方阵&#xff08;5G AIA&#xff09;在“2022年中国5G发展大会5G应用产业发展论坛”发布了“5G应用解决方案供应商推荐名录&#xff08;第一批&#xff09;”入库名单&#xff0c;旨在强化5G应用供需对接&#xff0c;推动5G应用解决方案成熟&#xf…

基于PHP+MySQL共享自行车租赁管理系统的设计与实现

随着环保意识的增加,人们的出行越来越简单便捷,其中共享自行车是现在很多年轻人最热衷的出行方式之一,本系统主要是对共享自行车的信息进行管理。该系统的基本功能包括用户登录,区域信息管理,用户信息管理,用户充值管理,车辆信息管理,租借信息管理,损耗信息管理,统计报表信息,修…

【自然语言处理概述】“海量”文件遍历

【自然语言处理概述】“海量”文件遍历 作者简介&#xff1a;在校大学生一枚&#xff0c;华为云享专家&#xff0c;阿里云专家博主&#xff0c;腾云先锋&#xff08;TDP&#xff09;成员&#xff0c;云曦智划项目总负责人&#xff0c;全国高等学校计算机教学与产业实践资源建设…

有关C++的异常机制

目录 为什么要有异常&#xff1a; 异常的抛出和捕获&#xff1a; 为什么要有异常&#xff1a; 异常在C用于错误处理&#xff0c;C语言中一般使用返回值表示错误&#xff0c;C对于错误处理进行了拓展&#xff0c;统一使用异常机制来处理程序中发生的错误 C的异常处理包括两个部分…

Myeclipse配置tomcat服务器

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 ✨特色专栏&#xff1a;国学周更-心性养成之路…

[附源码]Python计算机毕业设计Django的小说阅读系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

浅谈linux - 线程的基本应用

概述 线程&#xff08;英语&#xff1a;thread&#xff09;是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。 注意 线程和进程之间的区别 1. 线程是执行的基本单位&#xff1b;进程是资源分配的基本单位。 2. 线程共享进程的资源…

【Eureka】【源码+图解】【七】Eureka的下线功能

【Eureka】【源码图解】【六】Eureka的续约功能 目录6. 下线6.1 shutdown()6.2 服务端cancel6.3 同步其他server节点6. 下线 主动下线方式 服务端&#xff1a;/eureka/apps/{application.name}/{instance-id}&#xff0c;以本系列文章的helloworld为例&#xff0c;发送DELETE…

程序员学习 CPU 有什么用?

本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 提问。 前言 大家好&#xff0c;我是小彭。 在上一篇文章里&#xff0c;我们聊到了计算机的冯诺依曼架构&#xff0c;以及计算机的五大部件&#xff1a;控制器、运算器、存储器、输入…

最全面的Spring教程(五)——文件上传与下载

前言 本文为 【SpringMVC教程】文件上传与下载 相关知识&#xff0c;具体将对使用MultipartResolver处理文件上传的步骤&#xff0c;两种文件下载方式&#xff08;直接向response的输出流中写入对应的文件流、使用 ResponseEntity<byte[]>来向前端返回文件&#xff09;等…

老油条表示真干不过,部门新来的00后测试员已把我卷崩溃,想离职了...

在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不是。技术很强的同事&#xff0c;可遇不可求&#xff0c;向他学习还来不及呢。 真正让人反感的&#xff0c;是技术平平&#x…

降价背后,函数计算规格自主选配功能揭秘

作者&#xff1a;吴森梵&#xff08;仰森&#xff09; 在刚刚结束的 2022 杭州 云栖大会上&#xff0c;阿里云宣布函数计算 FC 开启全面降价&#xff0c;vCPU 单价降幅 11% &#xff0c;其他的各个独立计费项最高降幅达 37.5% 。函数计算 FC 全面降价&#xff0c;让 Serverle…

【Pandas数据处理100例】(七十五):Pandas的where()函数使用方法

前言 大家好,我是阿光。 本专栏整理了《Pandas数据分析处理》,内包含了各种常见的数据处理,以及Pandas内置函数的使用方法,帮助我们快速便捷的处理表格数据。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10语言环境:python3.7编译器:PyCharmPandas版本:1.3.5N…

线程篇(JAVA)

&#x1f495;前言&#xff1a;作者是一名正在学习JAVA的初学者&#xff0c;每天分享自己的学习笔记&#xff0c;希望能和大家一起进步成长&#x1f495; 目录 线程&#xff08;重点&#xff09; 第一种线程的实现 第二种线程的实现 线程的执行原理 线程的生命周期 休眠 …

醇酰基转移酶基因对猕猴桃酯生物合成的作用

文章信息 题目&#xff1a;Alcohol acyl transferase genes at a high-flavor intensity locus contribute to ester biosynthesis in kiwifruit 刊名&#xff1a;Plant Physiology 作者&#xff1a;Edwige J F Souleyre et al. 单位&#xff1a;New Zealand Institute for…

工业互联网数据监测预警解决方案

一、工业互联网数据安全趋势 随着“云、大、物、移、智”等新一代信息技术与制造业的融合发展&#xff0c;数字化生产、网络化协同、个性化定制、服务化延伸等生产运营模式逐渐成为常态&#xff0c;工业互联网数据不断走向开放流动。但原本封闭在工业现场的数据上网上云会带来…

论互联网公司的盈利能力

这个月&#xff0c;互联网公司三季度财报基本披露完毕。其中的共同点是都开始降本增效&#xff0c;提升盈利能力&#xff08;或者还在努力扭亏为盈&#xff09;。互联网公司基本是面向C端消费者的&#xff0c;京东创始人刘强东曾提出一个贯穿消费行业的“十节甘蔗”理论&#x…

[附源码]计算机毕业设计springboot健身房预约平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【剧前爆米花--爪哇岛寻宝】面向对象的三大特性——封装、继承以及多态的详细剖析(中——多态)。

作者&#xff1a;困了电视剧 专栏&#xff1a;《JavaSE语法与底层详解》 文章分布&#xff1a;这是一篇关于Java面向对象三大特性——多态的文章&#xff0c;在本篇文章中我会分享多态的一些基础语法以及类在继承时代码的底层逻辑和执行顺序。 目录 多态的定义及实现条件 多态…