【C++杂货铺】探索vector的底层实现

news2024/9/21 12:38:48

在这里插入图片描述

文章目录

  • 一、STL
    • 1.1 什么是STL?
    • 1.2 STL的版本
    • 1.3 STL的六大组件
  • 二、vector的介绍及使用
    • 2.1 vector的介绍
    • 2.2 vector的使用
      • 2.2.1 vector的定义
      • 2.2.2 vector iterator
      • 2.2.3 vector空间增长问题
      • 2.2.4 vector增删查改
    • 2.3 vector\<char\> 可以替代 string 嘛?
  • 三、vector模拟实现
    • 3.1 成员变量
    • 3.2 成员函数
      • 3.2.1 构造函数
      • 3.2.2 拷贝构造
      • 3.2.3 operator=
      • 3.2.4 size
      • 3.2.5 capacity
      • 3.3.6 迭代器相关
      • 3.2.7 reserve(深拷贝问题)
      • 3.2.8 resize
      • 3.2.9 operator[ ]
      • 3.2.10 insert(迭代器失效问题)
      • 3.2.11 erase(迭代器失效问题)
      • 3.2.12 pop_back
  • 四、结语

一、STL

1.1 什么是STL?

STL(standard template libaray-标准模板库):是C++标准库的一部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架

在这里插入图片描述

1.2 STL的版本

  • 原始版本:Alexander Stepanov、Meng Lee在惠普实验室完成的版本,本着开源精神,它们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要像原始版本一样做开源使用。HP版本是所有STL的祖先。

  • P.J版本:由P. J. Plauger开发,继承自HP版本,被微软(Windows Visual C++)采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。

  • RW版本:由Rouge Wage公司开发,继承自HP版本。被C++Builder采用,不能公开或修改,可读性一般。

  • SGI版本:由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程风格上看,阅读性非常高。建议大家在学习STL的过程中,可以参考这个版本的源代码。

1.3 STL的六大组件

在这里插入图片描述

二、vector的介绍及使用

2.1 vector的介绍

  • vector 是表示可变大小数组序列容器。

  • 就像数组一样,vector 也采用连续的存储空间来存储元素。也就意味着可以采用小标对 vector 的元素进行访问,和数组处理一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。

  • 本质讲,vector 使用动态分配数组来存储它的元素。当新元素插入时,为了增加存储空间,这个数组需要被重新分配大小。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价较高的任务,因为每当一个新的元素加入到容器的时候,vector 并不会每次都重新分配大小。

  • vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因此存储空间(容量)比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候实在常数时间复杂度完成的。

  • 因此,vector 占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。

  • 与其它的动态序列容器相比(如:deque、list、forward_list),vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率会比较低。

2.2 vector的使用

vector 学习时一定要学会查看文档:vector的文档介绍,vector 在实际中非常重要,在实际中我们熟悉常用的接口就可以,下面列出了需要我们重点掌握的接口。

2.2.1 vector的定义

构造函数声明接口说明
vector()无参构造
vector(size_type n, const value_type& val = value_type())构造并初始化 n 个 val
vector(const vector& x)拷贝构造
vector(Inputlterator first, Inputiterator last)使用迭代器区间进行初始化构造

小Tips:size_type 表示一个无符号整数类型,value_type 是第一个模板参数,也就是要存储的数据类型。使用迭代器区间的构造函数是函数模板,只要是满足 Input 类型的迭代器都可以使用该构造函数。

int TestVector1()
{
    vector<int> first;                                
    vector<int> second(4, 100);                       
    vector<int> third(second.begin(), second.end());  
    vector<int> fourth(third);                       

    int myints[] = { 16,2,77,29 };
    vector<int> fifth(myints, myints + sizeof(myints) / sizeof(int));

    cout << "The contents of fifth are:";
    for (vector<int>::iterator it = fifth.begin(); it != fifth.end(); ++it)
        cout << ' ' << *it;
    cout << '\n';

    return 0;
}

2.2.2 vector iterator

iterator的使用接口说明
begin + end获取第一个数据位置的 iterator / const_iterator,获取最后一个数据下一个位置的iterator / const_iterator
rbegin + rend获取最后一个数据位置的 reverse_iterator,获取第一个数据前一个位置的 reverse_iterator

在这里插入图片描述

void PrintVector(const vector<int>& v)
{
	// const对象使用const迭代器进行遍历打印
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

2.2.3 vector空间增长问题

容量空间接口说明
size()获取数据个数
capacity()获取容量大小
empty()判断是否为空
resize(size_type n); resize (size_type n, const value_type& val)改变 vector 的 size
reserve(size_type n)改变 vector 的 capacity
  • vs 和 g++ 的扩容机制有所不同,vs 下 capacity 是按照 1.5 倍增长的,g++ 是按照 2 倍增长的。vs 是 PJ 版本 STL,g++ 是 SGI 版本 STL。

  • reserve 只负责开辟空间,如果确定知道需要多少空间,reserve 可以缓解 vector 增容的代价缺陷问题。

  • resize 在开空间的同时还会进行初始化,影响 成员变量 _size。

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

VS 下的结果:
在这里插入图片描述
Linux 下的结果:
在这里插入图片描述
小Tips:如果已经确定 vector 中要存储元素的大概个数,可以提前将空间设置足够,就可以避免边插入边扩容导致效率低下的问题。

void TestVectorExpandOP()
{
    vector<int> v;
    size_t sz = v.capacity();
    v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容
    cout << "making bar 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';
        }
    }
}

2.2.4 vector增删查改

vector 增删查改接口说明
push_back尾插
pop_back尾删
find查找(这个是算法模块实现,不是 vector 的成员接口)
insert在 position 之前插入 val
erase删除 position 位置的数据
swap交换两个 vector 的数据空间
operator[ ]像数组一样访问,通过断言来检查,而 at 是通过抛异常
//经典的错误
void Testerro()
{
    vector<int> v1;
    v1.reserve(10);
    for (size_t i = 0; i < 10; i++)
    {
        v1[i] = i;
    }
}

注意:上面的代码虽然给 v1 提前开了 10 个空间,但是 v1 中的有效元素个数还是 0,即 v1.size() 的返回值是0,这样一来我们就不能直接通过下标去访问 vector 对象中的每一个元素,因为 operator[ ] 实现中的第一步就是检查下标的合理性,防止越界访问,执行 assert(pos < _size),而此时 _size 是 0,就会出错。上面的代码只需要把 reserve 改成 resize 就可以正常运行,因为 resize 会改变 _size 的大小。如果硬要使用 reserve 提前开空间,那么接下来要使用 push_back 来插入数据。

2.3 vector<char> 可以替代 string 嘛?

答案是不可以,虽然他们俩的底层本质上都是动态增长的数组,但是 string 字符串的结尾默认有 \0,可以更好的兼容 C 接口,而 vector<char> 的结尾默认是没有 \0 的,需要我们自己插入。

三、vector模拟实现

在这里插入图片描述

3.1 成员变量

public:
	typedef T* iterator;
	typedef const T* const_iterator;
private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;

3.2 成员函数

3.2.1 构造函数

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

vector(size_t n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	resize(n, val);
}

vector(int n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	resize(n, val);
}

//迭代器区间初始化
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

小Tips:迭代器区间初始化采用的是函数模板,因为它可能使用不同类型的迭代器。其次需要单独提供一个 vector(int n, const T& val = T()),因为迭代器区间初始化采用的是函数模板,如果不单独提供这种构造函数的话,vector<int> v1(10, 1) 这种情况会去走最匹配的,即和迭代器区间初始化函数匹配,而我们希望它走 vector(size_t n, const T& val = T()) 构造函数,但是 10 会被当做 int 型,和 size_t 匹配不上,因此就会去和迭代器区间初始化函数进行匹配,InputIterator 就会被实例化成 int 型,函数中会对 int 型解引用,就会报错,其次逻辑也不符。因此需要针对 int 单独提供一个构造函数。

3.2.2 拷贝构造

//方案一
vector(const vector<T>& V)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	iterator tmp = new T[V.capacity()];
	//memcpy(tmp, V._start, sizeof(T) * V.size());
	for (size_t i = 0; i < V.size(); i++)
	{
		tmp[i] = V._start[i];
	}
	_start = tmp;
	_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.capacity());
	for (auto e : V)
	{
		push_back(e);
	}
}

小Tips:这里设计深拷贝问题,在下文的 reserve 中会提到。

3.2.3 operator=

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

vector<T>& operator=(vector<T> v)//调用拷贝构造函数
{
	swap(v);
	return *this;
}

3.2.4 size

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

3.2.5 capacity

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

3.3.6 迭代器相关

iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

3.2.7 reserve(深拷贝问题)

void reserve(size_t new_capacity)
{
	if (new_capacity > capacity())
	{
		iterator tmp = new T[new_capacity];
		if (_start)//如果原来的_start申请过空间,要先将源空间中的内容拷贝过来
		{
			memcpy(tmp, _start, sizeof(T)*size());
			delete[] _start;
		}

		size_t vsize = size();

		_start = tmp;
		_finish = tmp + vsize;//记得更新_finish
		_end_of_storage = _start + new_capacity;
	}
}

注意:这里需要更新 _finish 和 _end_ofstorage,因为他俩表示的是位置。要更新 _finish,首先要将 size() 保存一下,因为更新 _start 后,_start 指向新空间的开头,而 _finish 指向旧空间的结尾,此时去调用 size(),计算出来的个数是有问题的,因此需要再更新 _start 之前就将原来的元素个数,即 size() 保存一份。

小Tips:上面这种扩容逻辑,当 T 是内置类或者是无需进行深拷贝的自定义类型来说,是完全满足的。但是当 T 是需要进行深拷贝的内置类型时,上面这种扩容方式就会出现大问题。以 vector<string> 为例,即当 T 是 string 的时候。

在这里插入图片描述
如上图所示,如果简单的用 memcpy 将旧空间的数据拷贝到新空间,那么新旧空间中存储的 string 对象指向同一个堆区上的字符串,接着在执行 delete[] _start; 销毁旧空间的时候,由于该 _start 是一个 string* 的指针,所以会先调用 string 的析构函数,将对象中申请的空间释放,即释放 _str 指向的空间,接着再去调用 operator delete 函数释放 string 对象的空间。这样一来,新空间中存储的 string 对象就有问题了,它们的成员变量 _str 指向的空间已经被释放了。这里的问题就出在 memcpy 执行的是浅拷贝。我们可以对上述代码稍作修改即可:

void reserve(size_t new_capacity)
{
	if (new_capacity > capacity())
	{
		iterator tmp = new T[new_capacity];
		if (_start)//如果原来的_start申请过空间,要先将源空间中的内容拷贝过来
		{
			//memcpy(tmp, _start, sizeof(T)*size());
			for (size_t i = 0; i < size(); i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}

		size_t vsize = size();

		_start = tmp;
		_finish = tmp + vsize;//记得更新_finish
		_end_of_storage = _start + new_capacity;
	}
}

修改后执行tmp[i] = _start[i]; 会去调用 string 对象的赋值运算重载,进行深拷贝。

3.2.8 resize

void resize(size_t n, const T& val = T())//缺省参数给的是一个匿名对象
{
	if (n > size())
	{
		//检查容量,扩容
		if (n > capacity())
		{
			reserve(n);
		}

		//开始填数
		iterator it = end();
		while (it < _start + n)
		{
			*it = val;
			it++;
		}

	}

	_finish = _start + n;
}

3.2.9 operator[ ]

T& operator[](size_t pos)//读写版本
{
	assert(pos < size());
	return _start[pos];
}

const T& operator[](size_t pos) const//只读版本
{
	assert(pos < size());
	return _start[pos];
}

3.2.10 insert(迭代器失效问题)

iterator insert(iterator pos, const T& val)
{
	assert(pos >= _start && pos <= _finish);
	size_t rpos = pos - _start;//保存一下pos的相对位置
	//检查容量
	if (_finish + 1 >= _end_of_storage)
	{
		size_t old_capacity = capacity();
		reserve(old_capacity == 0 ? 4 : old_capacity * 2);
	}
	pos = _start + rpos;//更新pos
	//插入数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
	return pos;
}

注意:在进行 insert 的时候,会引发一个著名的问题——迭代器失效。我们希望在 pos 位置插入一个数据,pos 是一个迭代器。在插入数据之前要先检查容量,进行扩容,如果执行了扩容逻辑,_start、_finish、_end_of_storage 都指向了新空间,旧空间已经被释放了,而 pos 指向的还是原来空间中的某个位置,此时 pos 就变成了野指针,再去 pos 指向的位置填入数据,就会造成非法访问。为了避免这个问题,我们可以先保存一下 pos 的相对位置,扩完容之后再去更新 pos。

在这里插入图片描述
小Tips:保存相对位置更新 pos,是 insert 函数内部的解决方式,由于是传值传参,形参的 pos 更新,并不会改变实参的 pos,因此为了解决外部的迭代器失效问题,这里采用返回值的方式,将更新后的 pos 返回。可能会有小伙伴觉得,直接把形参的 pos 变成引用不香嘛?这样对形参的更新就相当于是对实参的更新。想法很好,但是不现实,因为实参很有可能具有常性,例如实参如果用 begin()、end(),他俩都是传值返回,会产生一个临时变量,该临时变量具有常性,如果形参 pos 用引用的话,就需要加 const 进行修饰,但是!但是!!如果用 const 进行修饰,那在函数内部就不能对 pos 进行更新。因此形参 pos 不能用引用。

总结:会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、
push_back等。

3.2.11 erase(迭代器失效问题)

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

注意:erase 删除 pos 位置元素后,pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果 pos 刚好是最后一个元素,删完之后 pos 刚好是 _finish 的位置,而 _finish 位置是没有元素的,那么 pos 就失效了。因此,删除 vector 中任意位置上的元素时,VS 就认为该迭代器失效了(VS 是通过自己重写的 iterator 进行强制检查)。Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。为了解决外部的迭代器失效问题,这里还是采用返回值的方式,返回 pos 下一个位置元素的迭代器。

3.2.12 pop_back

//直接复用即可
void pop_back()
{
	erase(--end());
}

四、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

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

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

相关文章

垃圾识别工具箱、ChatGPT聊天微信小程序、大型商城电商源码

一、识别垃圾分类应用 垃圾识别工具箱微信小程序源码 前端&#xff1a;微信小程序 采用 uni-app 开发框架&#xff0c;uni-app 是一个使用 Vue.js 开发所有前端应用的框架&#xff0c;开发者编写一套代码&#xff0c;可发布到iOS、Android、H5、以及各种小程序&#xff08;微信…

RetNet或成Transformer继承者?通向更快、更强、更经济的基础架构

导读 在计算机科学的发展史上&#xff0c;硬件算力、算法程序与计算数据总是螺旋上升。在硬件相同的条件下&#xff0c;算法的效率和优化程度决定了其利用硬件资源的能力&#xff0c;从而直接影响计算机的算力。因此&#xff0c;为了提升计算机系统带来的综合效益&#xff0c;计…

C++:基础

目录 1.C关键字 2.命名空间 1.命名空间定义 2.命名空间的使用 3.C输入与输出 4.缺省参数 1.缺省参数的概念 2.缺省参数的分类 5.函数重载 1.函数重载概念 2.为什么C支持函数重载&#xff0c;C语言不支持&#xff1f; 6.引用 1.引用的概念 2.引用的特性 3.常引用…

重要通知:9月1日起,微信小程序须完成备案后才可上架

微信官方通知 近日&#xff0c;工信部发布了《工业和信息化部关于开展移动互联网应用程序备案工作的通知》&#xff0c;8月9日&#xff0c;微信公众平台也发布了“关于开展微信小程序备案的通知”&#xff1a; 一、备案必要性 在中华人民共和国境内从事互联网信息服务的移动互…

隐式表达的更进一步:基于NeRF的形状可编辑方法

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 [paper]&#xff1a;https://arxiv.org/pdf/2303.09554 [code]&#xff1a;https://ktertikas.github.io/part_nerf 主要贡献&#xff1a; 设计了 PartNeRF&#xff0c;这是一种新颖的部件感知生成模型&…

idea上利用JDBC连接MySQL数据库(8.1.0版)

1.了解jdbc概念 JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API&#xff0c;可以为多种 关系数据库提供统一访问&#xff0c;它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准&#xff0c;据此可以构建 更高级的工具和接口&#…

MAVEN利器:一文带你了解IDEA中如何使用Maven

前言&#xff1a; 强大的构建工具——Maven。作为Java生态系统中的重要组成部分&#xff0c;Maven为开发人员提供了一种简单而高效的方式来构建、管理和发布Java项目。无论是小型项目还是大型企业级应用&#xff0c;Maven都能帮助开发人员轻松处理依赖管理、编译、测试和部署等…

《Zookeeper》源码分析(二十二)之 客户端核心类

目录 CliCommand数据结构parse()exec() ZooKeeperHostProviderZKClientConfigClientCnxnSocket数据结构构造函数 ClientCnxn数据结构构造函数start() CliCommand 数据结构 CliCommand定义了两个抽象方法&#xff0c;以CreateCommand为例来看下它的parse()和exec()方法。 先看…

c语言每日一练(11)

前言&#xff1a;每日一练系列&#xff0c;每一期都包含5道选择题&#xff0c;2道编程题&#xff0c;博主会尽可能详细地进行讲解&#xff0c;令初学者也能听的清晰。每日一练系列会持续更新&#xff0c;暑假时三天之内必有一更&#xff0c;到了开学之后&#xff0c;将看学业情…

在驱动中创建sysfs接口、procfs接口、debugfs接口

前言 在一些linux开发板中&#xff0c;经常可以看到通过echo的方式来直接控制硬件或者修改驱动&#xff0c;例如&#xff1a; //灯灭 echo 0 >/sys/class/leds/firefly:blue:power/brightness //灯亮 echo 1 >/sys/class/leds/firefly:blue:power/brightness 这是怎么…

老人摔倒智能识别检测算法

老人摔倒智能识别检测算法通过yolov8深度学习算法模型架构&#xff0c;老人摔倒智能识别检测算法能够实时监测老人的活动状态及时发现摔倒事件&#xff0c;系统会立即触发告警&#xff0c;向相关人员发送求助信号&#xff0c;减少延误救援的时间。YOLOv8 算法的核心特性和改动可…

内嵌功能强大、低功耗STM32WB55CEU7、STM32WB55CGU7 射频微控制器 - MCU, 48-UFQFN

一、概述&#xff1a; STM32WB55xx多协议无线和超低功耗器件内嵌功能强大的超低功耗无线电模块&#xff08;符合蓝牙 低功耗SIG规范5.0和IEEE 802.15.4-2011标准&#xff09;。该器件内含专用的Arm Cortex -M0&#xff0c;用于执行所有的底层实时操作。这些器件基于高性能Arm …

分享一种针对uni-app相对通用的抓包方案

PART1&#xff0c;前言 近年来混合开发APP逐渐成为主流的开发模式&#xff0c;与传统的开发模式相比混合开发极大的提升了开发效率&#xff0c;同时跨平台的特性也降低了开发成本&#xff0c;一直以来混合开发被诟病的性能问题随着技术的发展也得到改善。技术的发展往往是一把…

vue3+uni——watch监听props中的数据(组件参数接收与传递defineProps、defineEmits)

案例说明 A页面引用的子组件B A页面 <template><view>//引用组件<serviceOrder change"change" :list"list" :current"type"></serviceOrder></view> </template><script setup>import serviceOrd…

智慧课堂学生行为检测评估算法

智慧课堂学生行为检测评估算法通过yolov5系列图像识别和行为分析&#xff0c;智慧课堂学生行为检测评估算法评估学生的表情、是否交头接耳行为、课堂参与度以及互动质量&#xff0c;并提供相应的反馈和建议。智慧课堂学生行为检测评估算法能够实时监测学生的上课行为&#xff0…

基于Jenkins自动化部署PHP环境---基于rsync部署

基于基于Jenkins自动打包并部署Tomcat环境_学习新鲜事物的博客-CSDN博客环境 准备git仓库 [rootgit ~]# su - git 上一次登录&#xff1a;五 8月 25 15:09:12 CST 2023从 192.168.50.53pts/2 上 [gitgit ~]$ mkdir php.git [gitgit ~]$ cd php.git/ [gitgit php.git]$ git --b…

Heikin-Ashi怎么用,FPmarkets澳福找到3个使用环境

所有赚到钱的交易者都在告诉你Heikin-Ashi是个能赚到钱的交易指标&#xff0c;但是没有一个赚到钱的交易者告诉你如何使用Heikin-Ashi交易指标赚到钱。其实很简单&#xff0c;只要理解Heikin-Ashi的这3个使用环境&#xff0c;如果不好使&#xff0c;FPmarkets澳福帮你账户充值1…

pytest pytest.ini 配置日志输出至文件

创建pytest.ini 文件 [pytest] log_file pytest_log.txt log_file_level INFO log_file_date_format %Y-%m-%d %H:%M:%S log_file_format %(asctime)s | %(filename)s | %(funcName)s | line:%(lineno)d | %(levelname)s | %(message)s import pytest import loggingdef …

基于Hadoop的MapReduce网站日志大数据分析(含预处理MapReduce程序、hdfs、flume、sqoop、hive、mysql、hbase组件、echarts)

需要本项目的可以私信博主&#xff01;&#xff01;&#xff01; 本项目包含&#xff1a;PPT&#xff0c;可视化代码&#xff0c;项目源码&#xff0c;配套Hadoop环境&#xff08;解压可视化&#xff09;&#xff0c;shell脚本&#xff0c;MapReduce代码&#xff0c;文档以及相…

【附安装包】EViews 13.0安装教程|计量经济学|数据处理|建模分析

软件下载 软件&#xff1a;EViews版本&#xff1a;13.0语言&#xff1a;英文大小&#xff1a;369.46M安装环境&#xff1a;Win11/Win10/Win8/Win7硬件要求&#xff1a;CPU2.0GHz 内存4G(或更高&#xff09;下载通道①百度网盘丨64位下载链接&#xff1a;https://pan.baidu.com…