【C++】vector类的模拟实现

news2024/11/27 10:32:56


Blog’s 主页: 白乐天_ξ( ✿>◡❛)
🌈 个人Motto:他强任他强,清风拂山冈!
🔥 所属专栏:C++深入学习笔记
💫 欢迎来到我的学习笔记!

本篇文章参考博客:【C++】透过STL源码深度剖析及模拟实现vector-CSDN博客

一、框架建立

注意:模板是不能分离到两个文件的,会出现链接错误!

在上一篇文章【链接:】我们就已经知道了迭代器的原貌就是原生指针类型,因此我们也将_start_finish_end_of_storage定义成了三个迭代器类型。

// 定义一个类域
namespace Harper
{
	template<class T>
	class vector 
	{
		// typedef重定义迭代器:
		typedef T* iterator;
		typedef const T* const_iterator;

		// 主要的接口函数:
		// ...

	private:
		// 主要成员函数:
		iteartor _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

二、迭代器

迭代器我们这里实现的是const版本非const版本的,反向版本的迭代器比较复杂,在这里就不实现了。

// 普通对象
iterator begin()
{
    return _start;
}
iterator end()
{
    return _finish;// 这里的end是指数据结束位置,而_end_of_storage是指空间结束位置		}
}

// const对象
const_iterator begin() const
{
    return _start;
}
const_iterator end() const
{
    return _finish;
}

三、容量

3.1 size、capacity

容量相关接口有size()capacity()

// 容量
size_t size()// 数据开始到结束的大小(总长)
{
    return _finish - _start;// ???
}
size_t capacity()
{
    return _end_of_storage - _start;// ???
}

画图示意:

画板

  • size就相当于这个容器的数据个数,即_finish_start两个迭代器之间的距离。在此之前我们已经知道迭代器的底层就是指针,计算两个指针之间的数据个数只需要两个指针相减即可。
  • capacity表示整个容器的容量,即_end_of_storage - _start

3.2 reserve

  • 首先在reserve函数中传入一个size_t类型的参数n,函数开始进行判断:如果传入的n值大于当前的容量(通过capacity()函数获取 ),才会执行扩容逻辑。
  • 在扩容逻辑内部,定义了一个类型为T*的临时指针tmp,使用new T[n]根据类型参数T开辟新的空间。如果原空间的起始指针_start不为空,就使用memcpy函数将元空间的数据(从start开始,拷贝size()T类型大小的数据)拷贝到新空间tmp中,然后释放原空间(delete[] _start)。
  • 最后更新成员变量,将_start指向新空间tmp_finish更新为_start + size()
// 扩容
void reserve(size_t n)
{
    if (n > capacity())
    {
        T* tmp = new T[n];// 开辟新空间给临时指针tmp
        if (_start)// _start不为空时
        {
            // 拷贝数据:从memcpy开始,拷贝size()个数据
            memcpy(tmp, _start, sizeof(T) * size());// 拷贝的数据个数???????
            delete[] _start;// 释放旧空间
        }
        _start = tmp;// 指向新空间
        _finish = _start + size();
        _end_of_storage = _start + n;
    }
}

但是这段代码还存在很多的漏洞!主要是下面的两个方面:

  1. 内存管理方面

    • 浅拷贝与内存泄漏
memcpy(tmp, _start, sizeof(T) * size());delete[] _start;
  • 如果T是复杂对象(如包含指针成员),memcpy执行浅拷贝,只复制指针值。例如,T是一个包含动态分配数组指针的类。
  • 假设T类有一个int*成员指向动态分配的整数数组。当使用memcpy拷贝时,只是复制了这个指针的值,新对象和原对象的这个指针成员会指向同一块内存。
  • 然后执行delete[] _start释放原对象内存,新对象中的指针就成为悬空指针。后续使用这个悬空指针会导致未定义行为,并且原对象管理的数组内存被释放,新对象无法正确管理,造成内存泄漏。
  • 异常安全
T* tmp = new T[n];
  • new T[n]分配内存失败(如系统内存不足),函数没有处理这种情况。
  • 若内存分配失败,函数会直接抛出异常。如果之前已经执行了if (_start)中的部分代码(如memcpy),原空间_start的状态已被改变,会导致数据不一致和潜在资源泄漏。
  1. 逻辑方面

    • size()函数调用
memcpy(tmp, _start, sizeof(T) * size());
  • memcpy操作中使用size()确定拷贝字节数。
  • size()依赖内部状态(如_finish_start关系),在reserve函数改变容器内部结构时(如重新赋值_start之前)调用size()可能得到错误结果。
  • 成员变量更新(空指针异常)
_finish = _start + size();

调试可以发现,_finish的出现了问题,值为0X0000000,那么出错的地方应是在它的前面执行的代码上。调试进入扩容就可以将问题锁定在_finish = _start + size();这一句代码上。

  • 在更新_finish时,_finish = _start + size();可能不正确。
  • 因为size()结果在扩容前后含义或计算方式可能改变,扩容后size()可能未正确更新,导致_finish计算错误,影响后续操作(如push_back依赖_finish的逻辑)。
  • 说明:之前我们使用_finish - _start来计算size(),执行这句话的时候start已经发生变化了,因为我们开辟了一块新空间,但是这是_finish的值还是醉意开始的nullptr,那么size()计算出来的大小即为-_start,此时再和_start去做一个结合,抵消了就是0

开始进行修改:

  1. _finish的修改更新
  • 解决办法一:更新_finish:使用新开辟的空间tmp进行更新,在用tmp去更新_start,这样就不会出现问题了。
_finish = tmp + size();
_start = tmp;
_end_of_storage = _start + n;
  • 解决办法二:我们可以在每次没开始扩容之前我们都可以去事先保存一下这个 size(),后面的更新顺序就不需要发生变动了,在加的时候加上sz即可。
if (n > capacity())
{
	// 先保存一下原先的size()
	size_t sz = size();
	T* tmp = new T[n];		// 开一块新空间
	if (_start)
	{
		memcpy(tmp, _start, sizeof(T) * size());
		delete[] _start;
	}
	_start = tmp;
	_finish = _start + sz;
	_end_of_storage = _start + n;
}
  1. memcpy的修改
memcpy(tmp, _start, sizeof(T) * size());

我们此前已经知道在VS下对于每个string对象的大小都是固定的28Byte,即使是通过不同的构造形式构造出来的对象也是一样的。

在这里就发生了一个浅拷贝问题,导致delete[] _start处发生了一个并发修改问题。

在扩容的时候,我们去开辟了一块新的空间,使用memcpy()函数将数据原封不动地拷贝到另一块空间,再去做一个扩容。因为这个memcpy()原封不动拷贝的问题,就使得新空间和旧空间虽然是两块独立的空间,但是呢每个对象中的_str都和另一个对象指向了那一块同样的空间。

在接下来执行这句代码时,就会先去调用当前对象的析构函数将每一块空间中的内容先清理掉,然后再去调用delete释放掉整块空间。因为没量过对象所指向的空间都是同一块的,是所以在释放的时候就会造成同时修改的问题。

delete[] _start;

总结:vector是深拷贝,但是vector空间上存的对象是string的数组,使用memcpy()导致string对象的浅拷贝。

解决办法:换一个拷贝逻辑即可,不用memcpy了,而是使用下面这种方式来拷贝:

for (size_t i = 0; i < size(); i++)
{
	tmp[i] = _start[i];
}

下面就是完整的实现:

void reserve(size_t n)
{
	if (n > capacity())
	{
		// 先保存一下原先的size()
		size_t sz = size();
		T* tmp = new T[n];// 开一块新空间
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T) * size());
			for (size_t i = 0; i < size(); i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

3.3 push_back接口

  • push_back函数中,接受一个const T&类型的参数x。首先判断_finish是否等于_end_of_storage,如果相等,表示当前空间已满。
  • 若空间已满,计算新的容量newCapacity,如果当前容量为 0,则新容量设为 4,否则新容量为当前容量的 2 倍。然后调用reserve函数进行扩容。
  • 最后将参数x赋值给_finish指向的位置,并将_finish指针后移一位。
void push_back(const T& x)
{
    if (_finish == _end_of_storage)
    {
        size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newCapacity);
    }
    *_finish = x;
    _finish++;
}

push_back函数漏洞:

  • reserve交互(相关代码行:reserve(newCapacity);*_finish = x;_finish++;
    • push_back调用reserve扩容时,如果reserve因内存分配失败等未正确完成扩容。
    • push_back没有错误处理,继续执行*_finish = x;_finish++;操作,可能导致访问无效内存或破坏容器内部状态。

3.4 修改后的代码(MyContainer类)

#include <iostream>
#include <cstring>

// 假设这是一个简单的模板类表示容器
template <typename T>
class MyContainer 
{
private:
    T* _start;
    T* _finish;
    T* _end_of_storage;

    // 辅助函数,用于正确地拷贝对象
    void copyObjects(T* dest, T* src, size_t num) 
    {
        for (size_t i = 0; i < num; ++i) 
        {
            new(dest + i) T(src[i]); // 使用placement new来正确构造对象
        }
    }
public:
    // 构造函数
    MyContainer() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}

    // 析构函数
    ~MyContainer() 
    {
        clear();
    }

    // 释放容器中的所有对象并释放内存
    void clear() 	
    {
        if (_start) 
        {
            T* cur = _start;
            T* end = _finish;
            for (; cur!= end; ++cur) 
            {
                cur->~T(); // 调用对象的析构函数
            }
            delete[] _start;
            _start = _finish = _end_of_storage = nullptr;
        }
    }

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


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


    void reserve(size_t n)
    {
        if (n > capacity()) 
        {
            T* tmp = nullptr;
            try 
                {
                tmp = new T[n];
            } catch (...) 	
            {
                // 如果内存分配失败,直接返回,不改变容器状态
                return;
            }
            size_t oldSize = size();
            copyObjects(tmp, _start, oldSize);
            clear();
            _start = tmp;
            _finish = _start + oldSize;
            _end_of_storage = _start + n;
        }
    }


    void push_back(const T& x) 
    {
        if (_finish == _end_of_storage) 
        {
            size_t newCapacity = capacity() == 0? 4 : capacity() * 2;
            reserve(newCapacity);
        }
        if (_start) {
            new(_finish) T(x);
            _finish++;
        }
    }
};
  1. reserve函数的修改

    • 内存管理方面
      • 针对浅拷贝和内存泄漏问题,不再使用memcpy,而是使用copyObjects函数。这个函数通过placement new逐个正确地构造新对象,避免了浅拷贝。
      • 对于异常安全问题,使用try - catch块来捕获new T[n]可能抛出的异常。如果内存分配失败,函数直接返回,不改变容器的当前状态。
    • 逻辑错误方面
      • 在计算要拷贝的元素数量时,先保存size()的结果(size_t oldSize = size();),避免了在容器结构改变过程中size()结果可能出现的错误。在更新_finish时,使用保存的旧大小来正确设置新的_finish位置。
  2. push_back函数的修改

    • 在调用reserve后,添加了if (_start)的判断,确保_start不为空(即reserve成功执行)后再进行push_back的操作。这避免了在reserve失败时执行可能导致错误的操作。
// 改成一层模板,实例化,编译器自动推导传入参数的类型
template<class T>
void print_vector(const vector<T>& v)// const对象的迭代器,不能调用非const的成员函数
{
    // 打印输出v的内容
    vector<int>::const_iterator it = v.begin();
    while (it != v.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    for (auto e : v)// 支持了迭代器就支持了范围for
    {
        cout << e << " ";
    }
}

3.5 resize接口

  1. 先分类情况:

    • n < _finish的情况

      • n小于当前容器中的元素个数(即_finish_start之间的距离)时,直接将_finish指针移动到_start + n的位置,这意味着截断容器,使容器中的元素数量变为n
    • n > _finish && n <= _end_of_storagen > _end_of_storage的情况

      • 这两种情况进行了合并处理。首先调用reserve函数检查是否需要扩容。如果n大于当前的_end_of_storage(容器容量),reserve函数会进行扩容操作以满足新的容量需求。
      • 在确保容量足够后(如果需要扩容已经完成扩容),通过循环将val(默认值或者传入的值)赋给从_finish开始到_start + n之间的元素,同时移动_finish指针,直到_finish到达_start + n的位置,从而将容器的元素数量调整为n

画板

void resize(size_t n, const T& val = T())
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		// 先使用reserve()去检查一下是否需要扩容
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			_finish++;
		}
	}
}
  1. 关于默认参数T()
const T& val = T()

功能解释:

  • resize函数的参数const T& val = T()中,T()是一个默认缺省参数。由于形参val的类型是模板参数类型,采用自动推导形式。
  • T()在这里是一个匿名对象,它根据T的类型生成相应的默认值。不能简单地给0作为默认值,因为T的类型不一定是整型,通过T()可以根据不同的类型生成合适的默认值。

四、元素访问

下标 +[]形式:

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

五、修改操作

5.1 push_back接口

  • 扩容在VS编译器下呈现1.5倍的增长趋势,但是在g++编译器下是2倍扩容趋势,在这里扩容使用reserve来实现。
void push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);
	}
	*_finish = x;
	_finish++;
}

5.2 insert接口

pos位置插入元素x

void insert(iterator pos, const T& x)
  • 断言检查:
    • 首先assert断言pos为合法的迭代器,即pos_start_finish之间(包含两端)。
    • 这是因为pos是指向容器内部有效空间的迭代器(类似于地址),不同于string类中基于无符号整数的我只表示,这里不可能为0。
  • 扩容逻辑:
    • 如果容器已满(_finish == _end_of_storage),则复用push_back中的扩容逻辑。
    • 按照规则(容量为 0 时新容量设为 4,否则为当前容量的 2 倍)计算新容量并调用reserve函数进行扩容。
  • 数据挪动与插入:
    • 确定要挪动数据的范围,将_finish - 1作为末尾迭代器end。通过循环从后往前将元素依次后移一位(*(end + 1) = *end),直到end到达pos的位置。这样做可以避免覆盖数据。
    • 然后将元素x插入到pos位置(*pos = x),最后将_finish指针向后移动一位,表示容器中的元素数量增加了一个。
void insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);
	// 1.首先考虑扩容逻辑
	if (_finish == _end_of_storage)
	{
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);
	}

	// 2.挪动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = x;
	++_finish;
}

那么在push_back中就可以复用insert接口了。

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

六、默认成员函数

6.1 构造函数

6.1.1 基于resize复用的有参构造函数

  • 自定义vector类中的有参构造函数vector(size_t n, const T& val = T())通过复用resize函数来初始化容器。例如创建Harper::vector<int> v(10, 0);时,构造函数内部调用v.resize(10, 0)
// 有参构造
vector(size_t n, const T& val = T())
{
	resize(n, val);
}
  • 对于vector类中的_start_finish_end_of_storage这三个私有成员变量,它们在定义时被初始化为nullptr,避免内置类型未初始化的问题。

6.1.2 基于迭代器区间的构造函数

  • 原理与实现细节

    • 下面的函数是通过迭代器区间初始化vector的构造函数原型。
template<class InputIterator> vector(InputIterator first, InputIterator last)
  • 举例:
template<class InputIterator>
vector(InputIterator first, InputIterator last) 
{
    while (first != last) 
    {
        push_back(*first);
        ++first;
    }
}
  • 可以用已存在的vector对象结合迭代器区间初始化新的vector对象。
Harper::vector<int> v2(v.begin(), v.end());
  • 也可用于string对象迭代器或数组指针的初始化。
string s("abcdef"); 
Harper::vector<int> v2(s.begin(), s.end());

int a[] = {1, 2, 3, 4}; 
Harper::vector<int> v2(a, a + 4);

6.1.3 构造函数调用歧义及解决

  • 当执行bit::vector<int> v5(10, 1);时,会出现 “非法的间接寻址” 问题。这是因为模板参数自动类型推导时,传入的101int类型,而原有的有参构造函数第一个形参为size_t类型,不会优先匹配该构造函数,而是可能错误匹配到迭代器区间构造函数(其参数为模板类型,匹配度更高)。
  • 通过重载有参构造函数,新增vector(int n, const T& val = T())版本:
vector(int n, const T& val = T()) 
{
    resize(n, val);
}
  • 这样就与原vector(size_t n, const T& val = T())形成重载关系,避免了调用歧义。同时,若要调用size_t类型的构造函数,可在参数后加u,如bit::vector<int> v6(10u, 6);

6.2 拷贝构造函数

  • 最初的拷贝构造函数vector(vector<int>& v)实现中存在浅拷贝问题,在调试时可发现。
  • 最初的代码如下:
vector(vector<int>& v) 
{
    _start = new T[v.capacity()];
    memcpy(tmp, v._start, sizeof(T) * v.size());
    _finish = tmp + v.size();
    _end_of_storage = tmp + v.capacity();
}
  • vector对象存储string数组时,memcpy会导致浅拷贝问题。
  • 正确的深拷贝实现方式是逐个拷贝元素:
vector(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();
}
  • 也可复用reservepush_back接口实现拷贝构造函数:
vector(vector<int>& v) 
{
    // 根据v的capacity()去开出对应的空间
    reserve(v.capacity());
    for (size_t i = 0; i < v.size(); i++) 
    {
        push_back(v[i]);
    }
}

6.3 赋值重载函数

  • 赋值重载函数const vector<T>& operator=(vector<T> v)利用swap接口实现。通过传值传参,先调用拷贝构造函数创建临时对象,然后用swap交换临时对象和当前对象内容。临时对象出作用域后自动销毁。以下是代码:
const vector<T>& operator=(vector<T> v) 
{
    swap(v);
    return *this;
}
  • 在调试时可看到调用赋值重载函数前会先调用拷贝构造函数。

6.4 析构函数

  • 析构函数~vector()用于释放容器占用的空间,将资源归还给操作系统。代码如下:
~vector() 
{
    delete[] _start;
    _start = _finish = _end_of_storage = nullptr;
}

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

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

相关文章

HTML5+CSS+JavaScript剪子石头布游戏

HTML5CSSJavaScript剪子石头布游戏 用HTML5CSSJavaScript剪子石头布游戏实现剪子石头布游戏&#xff0c;游戏有成绩计数&#xff0c;人、机输赢情况&#xff0c;及平局情况。 ✂代表剪刀&#xff0c;▉代表石头&#xff0c;▓ 代表布&#xff0c;给出人机双方的出拳情况 游戏…

对于无人机行业技术、人才、实验环境共享,这事你怎么看?

对于无人机行业技术、人才、实验环境共享这一话题&#xff0c;我认为这是一个具有前瞻性和积极意义的趋势&#xff0c;对于推动无人机行业的健康发展具有重要意义。以下是我对此事的详细看法&#xff1a; 一、技术共享促进创新与发展 1. 加快技术创新&#xff1a;技术共享能够…

web前端面试中拍摄的真实js面试题(真图)

web前端面试中拍摄的真实js面试题&#xff08;真图&#xff09; WechatIMG258.jpeg WechatIMG406.jpeg WechatIMG407.jpeg WechatIMG922.jpeg WechatIMG1063.jpeg © 著作权归作者所有,转载或内容合作请联系作者 喜欢的朋友记得点赞、收藏、关注哦&#xff01;&#xff01;…

散度旋度拉普拉斯算子速记

哈密顿运算符号想象成一个矢量&#xff08;对x偏导&#xff0c;对y偏导&#xff0c;对z偏导&#xff09; 1、旋度就是这个矢量做点乘 2、散度就是矢量做叉乘 3、拉普拉斯算子就是哈密顿算符点乘哈密顿算符

【自然语言处理】(1) --语言转换方法

文章目录 语言转换方法一、统计语言模型1. 词向量转换2. 统计模型问题 二、神经语言模型1. 词向量化2. 维度灾难3. 解决维度灾难4. embedding词嵌入5. Word2Vec技术5.1 连续词袋模型&#xff08;CBOW&#xff09;5.2 跳字模型&#xff08;Skip-gram&#xff09; 总结 语言转换方…

[论文笔记]SGPT: GPT Sentence Embeddings for Semantic Search

引言 解码器Transformer的规模不断壮大&#xff0c;轻松达到千亿级参数。同时由于该规模&#xff0c;基于提示或微调在各种NLP任务上达到SOTA结果。但目前为止解码器Transformer还无法应用在语义搜索或语句嵌入上。 为了简单&#xff0c;下文中以翻译的口吻记录&#xff0c;比…

平台数据分类与聚类实验报告

参考书籍&#xff1a;《数据流挖掘与在线学习算法》 李志杰 1.6.1 实验目的 本书内容以及课程实验主要涉及Java程序设计语言、数据挖掘工具Weka和数据流机器学习平台MOA&#xff0c;因此&#xff0c;需要安装、配置并熟悉实验环境。Java、Weka和MOA都是开源小软件&#xff0…

2024年10月2日历史上的今天大事件早读

1683年10月2日 清朝康熙帝统一台湾 1869年10月2日 印度民族解放运动领袖甘地诞辰 1890年10月2日 中共创始人之一李达诞生 1895年10月2日 天津中西学堂&#xff08;天津大学前身&#xff09;开学 1901年10月2日 郑士良等发起惠州起义 1909年10月2日 京张铁路正式通车 1920…

国外电商系统开发-运维系统功能清单开发

一、最终效果图 二、功能清单 功能 描述 自定义日志绘图 根据Nginx、Apache登录日志文件绘图&#xff0c;绘图数据包括&#xff1a;访问量走势&#xff0c;500错误&#xff0c;200正确百分比等 创建服务器 加入服务器 主机状态自动检查 加入主机到系统后&#xff0c;系统…

【STM32】TCP/IP通信协议(2)--LwIP内存管理

五、LWIP内存管理 1.什么是内存管理&#xff1f; &#xff08;1&#xff09;内存管理&#xff0c;是指软件运行时对计算机内存资源的分配的使用的技术&#xff0c;其主要目的是如何高效、快速的分配&#xff0c;并且在适当的时候释放和回收内存资源&#xff08;就比如C语言当…

前端规范工程-5:Git提交信息规范(commitlint + czg)

前面讲的都是在git提交之前的一些检查流程&#xff0c;然而我们git提交信息的时候&#xff0c;也应该是需要规范的。直接进入主题&#xff1a; 目录 需安装插件清单commitlint 介绍安装配置配置commit-msg钩子提交填写commit信息czg后续方式一&#xff1a;push触动build并上传…

行为设计模式 -观察者模式- JAVA

观察者模式 一.简介二. 案例2.1 抽象主题&#xff08;Subject&#xff09;2.2 具体主题&#xff08;Concrete Subject&#xff09;2.3 抽象观察者&#xff08;Observer&#xff09;2.4 具体观察者&#xff08;Concrete Observer&#xff09;2.5 测试 三. 结论3.1 优缺点3.2 使用…

从零开始讲PCIe(0)——外设与外设总线

一、外设 计算机外设&#xff08;Peripheral&#xff09;是指连接到计算机主机以扩展其功能的外部设备。这些设备可以是输入设备、输出设备、存储设备或通信设备等&#xff0c;外设&#xff08;外围设备&#xff09;通过输入、输出、存储和通信等方式帮助计算机与用户和其他设备…

用Arduino单片机读取PCF8591模数转换器的模拟量并转化为数字输出

PCF8591是一款单芯片&#xff0c;单电源和低功耗8位CMOS数据采集设备。博文[1]对该产品已有介绍&#xff0c;此处不再赘述。但该博文是使用NVIDIA Jetson nano运行python读取输入PCF8591的模拟量的&#xff0c;读取的结果显示在屏幕上&#xff0c;或输出模拟量点亮灯。NVIDIA J…

可解释聚类又“炸出圈”啦!把准3个切入点一路开挂!创新思路一学就会~

可解释聚类是机器学习领域一个非常重要的研究方向&#xff0c;它通过引入解释性强的特征和模型&#xff0c;让我们更直观地理解聚类结果&#xff0c;从而提升聚类分析的准确性和可靠性。 这种方法在一些敏感领域如医疗、金融等非常适用&#xff0c;因为它与传统方法不同&#…

工具模块及项目整体模块框架

文章目录 工具模块logger.hpphelper.hppthreadpool.hpp 核心概念核心API交换机类型持久化⽹络通信消息应答持久化数据管理中心模块虚拟机管理模块交换路由模块消费者管理模块信道管理模块连接管理模块Broker服务器模块消费者管理信道请求模块通信连接模块项⽬模块关系图 工具模…

Oracle SQL语句没有过滤条件,究竟是否会走索引??

答案是&#xff1a;可能走索引也可能不走索引&#xff0c;具体要看列的值可不可为null&#xff0c;Oracle不会为所有列的nullable属性都为Y的sql语句走索引。 例子&#xff1a; create table t as select * from dba_objects; CREATE INDEX ix_t_name ON t(object_id, objec…

MySQL 中的 GTID 复制详解

MySQL 中的 GTID 复制详解 在 MySQL 的复制架构中&#xff0c;GTID&#xff08;Global Transaction Identifier&#xff09;复制是一种重要的技术&#xff0c;它为数据库的复制提供了更强大的功能和更高的可靠性。本文将深入探讨 MySQL 中的 GTID 复制是什么&#xff0c;以及它…

OpenCV计算机视觉库

计算机视觉和图像处理 Tensorflow入门深度神经网络图像分类目标检测图像分割OpenCVPytorchNLP自然语言处理 OpenCV 一、OpenCV简介1.1 简介1.2 OpenCV部署1.3 OpenCV模块 二、OpenCV基本操作2.1 图像的基本操作2.1.1 图像的IO操作2.1.2 绘制几何图像2.1.3 获取并修改图像的像素…

时间相关数据的统计分析(笔记更新中)

对事件相关数据的统计思路做一个笔记 可以用作肿瘤生长曲线&#xff08;Tumor Growth Curve&#xff09;/某一个药物处理后不同时间点表型的获取类型的数据。 总体来说合适的有两类&#xff0c;一类是以ANOVA为基础的方差分析&#xff0c;重复测量资料的方差分析&#xff1b;…