【C++】容器篇(一)—— vector 的基本概述以及模拟实现

news2024/9/29 20:05:28

前言:

在之前,我们已经对 string类进行了基本的概述,并且手动的实现了string类中常用的接口函数。本期,我将带领大家学习的是STL库中的一个容器 -- vector 的学习。相比于之前的string类,本期的 vector 相对来说实现起来略微难一点,难点就在于要考虑关于 “迭代器失效”方面和“深浅拷贝”的问题。


文章目录

前言

(一)基本介绍

1、相关定义

2、vector的引入

3、vector的优点

4、vector使用本质

(二)vector的使用

1、vector的定义

 2、vector iterator 的使用

3、vector的空间增长

 4、vector的增删查改

(三)接口函数的介绍

1、Member functions

1️⃣n个val构造

2️⃣迭代区间构造

3️⃣拷贝构造

2、Iterators

1️⃣begin()和end()

2️⃣rbegin()和rend()

3、Capacity

(四)vector模拟实现

1、成员变量

2、成员函数

1️⃣构造函数

2️⃣析构函数

3️⃣拷贝构造

3、迭代器

1️⃣begin() 和 end()

4、容量

1️⃣size()

2️⃣capacity()

3️⃣empty()

4️⃣reserve()

5️⃣resize()

5、元素访问

1️⃣operator[]

6、修改

1️⃣push_back()

2️⃣pop_back()

3️⃣insert()

4️⃣erase()

5️⃣swap()

总结


前言

在正式的学习之前,我们先来看看STL库中的容器有哪些,让大家先混个脸熟,在接下来,对于其他的容器我也会给大家进行相关的讲解。

  • STL库中的容器如下(勾画的接下来我都会给大家讲解):

💨 而对于 map、set等则是要等到之后在给大家讲解,因为要涉及关于 红黑树的相关知识;

💨 而对于像第一个【arry】这个我们十分的熟悉了,就没有必要在进行讲解了,而【deque】和【queue】二者之间也有很多的不同之处;

💨 而对于【bitset】(位集),我们只需简单的了解即可,它主要的功能是提供一些成员函数,使程序员不必通过位运算就能很方便地访问、修改其中的任意一位


(一)基本介绍

1、相关定义

既然要学习vector,那么首先就需要知道到底什么是vector,定义如下:

  • vector是STL库中的一种序列式容器事实上和数组差不多(但它比数组更优越),由于其大小(size)可变,常用于数组大小不可知的情况下来替代数组。
  • 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素 进行访问,和数组一样高效。

2、vector的引入

  1. 一般来说数组不能动态拓展,因此在程序运行的时候不是浪费内存,就是造成越界
  2. 而 vector 正好弥补了这个缺陷,它不像数组那样,它的大小是可以动态改变的,相当于可拓展的数组(动态数组),它的随机访问快,在中间插入和删除慢,但在末端插入和删除快,而且它的大小会被容器自动处理。

3、vector的优点

  1. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效;
  2. 对于其它不在末尾的删除和插入操作,效率更低;
  3. 比起list 和 forward_list 统一的迭代器和引用更好。

4、vector使用本质

本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。

其做法是:分配一个新的数组,然后将全部元素移到这个数组。

就时间而言,这是 一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。


(二)vector的使用

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

1、vector的定义

 2、vector iterator 的使用

3、vector的空间增长

 4、vector的增删查改

大家通过文档不难发现,vector容器跟之前学习的string类的接口很相似。因此,通过之前对string的学习,并且掌握的话对于本次学习的vector学习到掌握是很容易的!!!


(三)接口函数的介绍

接下来,我就将为大家介绍常用的接口以及实现的过程!!!

1、Member functions

对于构造,我们通过阅读文档可以发现,文档给出的构造有四种,分别是:

  • 默认构造
  • n个val构造
  • 迭代区间构造
  • 拷贝构造


接下来,我直接给大家展示相关方法是如何进行函数构造的。 

1️⃣n个val构造

  • 代码如下:
void test1()
{
	//n个val构造
	vector<int>arr(10,1); //ten ints with value 1

	for (int i = 0; i < arr.size(); ++i)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

}

int main()
{
	test1();
	return 0;
}
  • 解释说明:

上述代码我们构造包含 10个元素的容器。每个元素都是 的副本。


2️⃣迭代区间构造

  • 代码如下:
void test()
{
    vector<int>arr(10,1);

	//迭代区间构造
	vector<int>tmp(arr.begin(), arr.end());
	for (int i = 0; i < tmp.size(); ++i)
	{
		cout << tmp[i] << " ";
	}
	cout << endl;

}

int main()
{
	test();
	return 0;
}
  • 解释说明:

 上述通过构造一个容器,其中包含与范围 [first、last)(特别注意迭代区间的范围是左闭右开) 一样多的元素,每个元素都以相同的顺序从该区域中的相应元素构造。

拓展说明:

对于迭代器构造,不知道大家有没有发现文档中给出的描述:

迭代器不应该叫做【iterator】吗,这里怎么叫做【inputiterator】呢?

  1. 这就要说到一个问题了,那就是迭代器是分类型的(说起来很复杂,我简单讲一下)。
  2. 不同的迭代器除了之前说过的正向、反向 这样的使用属性之外,还有相关的特性属性,比如双向、单向等;
  3. 因此在这里就表示你使用时即可传单向,也可以传双向,以及随机等(即可以传任意类型) 

例如以下:

    string str("have a good day");
	vector<int>vv(str.begin(), str.end());
	for (int i = 0; i < vv.size(); ++i)
	{
		cout << vv[i] << " ";
	}
	cout << endl;

输出的即是对应的 ASCll 码值。除此之外,还可以进行【++】或【--】操作。 


3️⃣拷贝构造

  • 代码如下:
void test()
{
	vector<int>arr;  //默认构造
	arr.push_back(1);
	arr.push_back(2);
	arr.push_back(3);
	arr.push_back(4);
	arr.push_back(5);


	//拷贝构造
	vector<int> tmp(arr);
	for (auto e : tmp)
	{
		cout << e << " ";
	}
	cout << endl;
}
  • 解释说明:

复制构造函数,以相同的顺序构造一个容器,其中包含 arr 中每个元素的副本。


2、Iterators

接下来就是关于正向迭代器和反向迭代器的介绍,这个我相信大家在之前的学习中都有了解,我就不过多阐述。

1️⃣begin()和end()

  • 代码如下:
void test()
{
	vector<int>arr;
	arr.push_back(1);
	arr.push_back(2);
	arr.push_back(3);
	arr.push_back(4);
	arr.push_back(5);


	vector<int>::iterator it = arr.begin();
	while (it != arr.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

2️⃣rbegin()和rend()

  • 代码如下:
void test()
{
	vector<int>arr;
	arr.push_back(1);
	arr.push_back(2);
	arr.push_back(3);
	arr.push_back(4);
	arr.push_back(5);

	vector<int>::reverse_iterator rit = arr.rbegin();
	//auto rit = arr.rbegin();

	while (rit != arr.rend())
	{
		cout << *rit << " ";
		rit++;
	}
	cout << endl;
}

3、Capacity

对于容量的相关接口,跟之前学的string是类似的。

这里需要讲明的一点是关于 【capacity】扩容的问题,capacity的代码在 vs 和 g++ 下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。 这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义 的。vs是PJ版本STL,g++是SGI版本STL。

  • 用以下测试代码分别在两种环境下运行后:
// 测试vector的默认扩容机制
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下的结果:

  •  在g++下的结果:

接下来,对于reserve 和 resize,我们在后面的接口函数模拟实现上具体的讲解,以及带大家来理解迭代器失效的问题!!


(四)vector模拟实现

1、成员变量

我这里的实现风格整体来说是按照库里面的实现方式来进行操作的(如果大家有兴趣,可以去库里面看下源码)

  • 首先,我们这里实现的vector是 typedef出来的,具体如下(分为普通版本和const版本):
typedef T* iterator;
typedef const T* const_iterator;
  • 而对于类的成员变量如下: 
private:
		iterator _start;
		iterator _end;
		iterator _end_of_storage;
  • 解释说明:

 ⚔️ _start:表示指向数据的开始位置;

 ⚔️ _end:表示指向数据的结尾位置的下一个位置;

 ⚔️ _end_of_storage:表示的是整个空间的大小。


2、成员函数

1️⃣构造函数

这些都跟之前是一样的,在这里就不多说了,具体如下:

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

2️⃣析构函数


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

3️⃣拷贝构造

  • 现代写法:
        //现代写法
		vector(const vector<T>& arr)
		{
			vector<int> tmp(arr.begin().arr.end());
			swap(tmp);
		}
  •  传统写法:
        vector(const vector<T>& arr)
		{
			_start = new T[arr.capacity()];
			for (size_t i = 0; i < arr.size(); ++i)
			{
				_start[i] = arr._start[i];
			}

			_finish = _start + arr.size();
			_end_of_storage = _start + varrcapacity();
		}

3、迭代器

1️⃣begin() 和 end()

这里我提供了两个版本的,分别是普通版本和const版本的实现。

  • 具体如下:
        iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _end;
		}

		const_iterator begin()const
		{
			return _start;
		}

		const_iterator end()const
		{
			return _end;
		}

对于反向迭代器,我放在之后的学习中再去讲解,因为将会涉及一些其他的东西在里面。


4、容量

1️⃣size()

理解这应该很容易理解,对于有效位数据位的大小,就为【_end】位置与【_start】位置的距离为多少。

  • 具体如下:
        //长度大小
		size_t size()const
		{
			return _end - _start;
		}

2️⃣capacity()

理解对于数据所占空间的大小,就为首元素位置与整体空间大小的差值。

  • 具体如下:
        //空间大小
		size_t capacity()const
		{
			return _end_of_storage - _start;
		}

3️⃣empty()

  • 具体如下:
        bool empty()
		{
			return _start = _end;
		}

4️⃣reserve()

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

  • 代码展示:
void reserve(size_t n)
		{
			//首先先判断是否需要开辟新空间
			if (n > capacity())
			{
				//开新空间
				T* tmp = new T[n];
				if (_start)
				{
					//元素拷贝到新空间
					memcpy(tmp, _start, sizeof(T)*size());
					//释放旧空间
					delete[] _start;
				}


				_start = tmp;
				_end = tmp + size();
				_end_of_storage = tmp + n;
			}
		}

上述模拟实现的vector中的reserve接口中,会使用 memcpy 进行的拷贝,但是大家通过代码有没有发现什么问题呢?

  • 当我们用测试用例去打印结果的时候,会发生程序挂掉了:

  •  我们通过调试去观察是什么原因导致的问题,具体如下:

 现象解释:

  1. 首先,我们知道size() = _end - _start;
  2. 按理说释放之后应该为0,因为 tmp=_start,因此我们可以把上述带入公式带入代码中换掉,我们就不难得出一点,此时代码就为 【tmp + _end - _start(tmp) 】,所以此时最终为0了;
  3. 相当于_start 是一个地址,而最终却是 0 减 一个地址。即_end 还是之前的,而_start 却不是之前的_start 了;
  4. 因此,最好的解决办法就是在事先保留一份最初的 size() 即可。

  • 代码如下:
        void reserve(size_t n)
		{
			//首先先判断是否需要开辟新空间
			if (n > capacity())
			{
				size_t num = size();
				//开新空间
				T* tmp = new T[n];
				if (_start)
				{
					//元素拷贝到新空间
					memcpy(tmp, _start, sizeof(T)*size());
					//释放旧空间
					delete[] _start;
				}

				_start = tmp;
				_end = _start + num;
				_end_of_storage = _start + n;

			}
		}

最终通过上诉我们得出一点:

  1. 如果拷贝的是 自定义类型的元素,memcpy既高效又不会出错;
  2. 但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝

 

结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是 浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

5️⃣resize()

resize在开空间的同时还会进行初始化,影响size。

  • 代码展示:
        void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_end = _start + n;
			}
			else
			{
				if(n > capacity())
					reserve(n);

				while (_end != _start + n)
				{
					_end = val;
					++_end;
				}
			}
		}
  • 注意这里的val初始化时初始化为 T();
  • 当单独使用T()时,当前行结束就会自动调用销毁,而当时用 const  T& val =T() 就可以延长生命周期。

5、元素访问

1️⃣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];
		}

6、修改

1️⃣push_back()


		//尾插
		void push_back(const T& X)
		{
			//先判断空间
			if (_end == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}

			//移动元素
			*_end = X;
			++_end;
		}

2️⃣pop_back()

        void pop_back()
		{
			assert(!empty()); //注意判空
			 --_end;
		}

3️⃣insert()

意思很简单,即在 pos 位置出插入 val 元素即可。但是这里却有一个很坑的点,那就是不小心写出来的代码就会引起迭代器失效的问题。

  • 大家先看以下代码:
        void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _end);

			if (_end == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}

			iterator finish = _end - 1;
			while (finish >= pos)
			{
				*(finish + 1) = *finish;
				--finish;
			}

			*pos = val;
			++_end;
		}

此时,数组中只有4个元素(即数组中有四个元素),当我们想在第三个位置处插入元素0时,我们打印对应的结果,就会发现结果报错:

但是,当我们提前先把 5 也放入数组(即数组中有5个元素),此时再去进行插入操作时,结果又是正确的:

 那么上述是什么问题导致的呢?我们先调试看看:

 解释说明:

  • 通过上述我们不难看出一个问题,程序只走了一步扩容操作,pos 位置此时没有发生变化,而 _start 和 _end 已经发送改变了。
  • 此时这种情况就是一种经典的迭代器失效现象

解决方法:

  • 我们需要更新 pos位置 ,根据相对位置来判断扩容之后处于新空间的位置是多少。

  • 纠正写法:
        void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _end);

			if (_end == _end_of_storage)
			{
				size_t len = pos - _start;  //迭代器失效问题(野指针)
				reserve(capacity() == 0 ? 4 : capacity() * 2);

				pos = _start + len;

			}

			iterator finish = _end - 1;
			while (finish >= pos)
			{
				*(finish + 1) = *finish;
				--finish;
			}

			*pos = val;
			++_end;
		}

但是上述改了之后是不是就 ok l呢?其实并不是的,程序其实还有 bug。

此时,很多小伙伴就会说我都已经更新了pos呀,但是我们经过调试之后发现没有起作用在这里:

 解释说明:

  • 最主要的原因就是这里是值传递,形参的改变是不会改变实参的,所以说外面的pos依旧没有实现更新操作,依旧会失效。

 💨  正确写法是通过返回值去解决问题的,返回的是迭代器,即新插入元素的位置

  • 正确代码:
        iterator  insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _end);

			if (_end == _end_of_storage)
			{
				size_t len = pos - _start;  //迭代器失效问题(野指针)
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;
			}

			iterator finish = _end - 1;
			while (finish >= pos)
			{
				*(finish + 1) = *finish;
				--finish;
			}

			*pos = val;
			++_end;

			return pos;
		}


4️⃣erase()

这里主要的就是从后往前删除的,这点需要大家注意一下。

  • 代码如下:
        void erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _end);

			iterator start = pos + 1;
			while (start != _end)
			{
				*(start - 1) = *start;
				++start;
			}
			--_end;
		}

此时问题又来了,上述这样写会不会也有迭代器失效的问题呢?

 💨  首先,我们简单的测试一下:

  • 从上述结果我们不难发现,经过验证程序是没有问题的。

 💨  接下来,我们在去看看在库中表现如何:

 解释说明:

  1. erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代 器不应该会失效;
  2. 但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是 没有元素的,那么pos就失效了.因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效 了。
  • 如果大家下去验证,发现不管是不是在结束的位置,编译器都会默认程序崩溃。

我们通过调试可以发现,库中默认的检查手段直接就是使得程序发送崩溃了:

 注意:

  • 说明一点的是代码的结果还跟环境有关,大家可以去linux下进行测试看看结果是怎么样的

 💨  与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效:

void TestString()
{
     string s("hello");
     auto it = s.begin();
     // 放开之后代码会崩溃,因为resize到20会string会进行扩容
     // 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了
     // 后序打印时,再访问it指向的空间程序就会崩溃
     //s.resize(20, '!');
     while (it != s.end())
     {
         cout << *it;
         ++it;
     }
     cout << endl;
     it = s.begin();
     while (it != s.end())
     {
         it = s.erase(it);
         // 按照下面方式写,运行时程序会崩溃,因为erase(it)之后
         // it位置的迭代器就失效了
         // s.erase(it); 
         ++it;
     }
}

终上所述,我们认为在erase之后,pos是会发生失效的,因此建议不要访问,行为结果未定义(具体跟编译器有关系)


5️⃣swap()

        void swap(vector<T>&arr)
		{
			swap(_start, arr.start);
			swap(_end, arr.end);
			swap(_end_of_storage, arr._end_of_storage);
		}


总结

到此,关于vector的全部内容便讲解完毕了,接下来我们简单的总结一下本文都学了什么

  1. 首先,在开头我先给大家介绍了关于 STL库中的容器有哪些,让大家初步的认识了各个容器,对其有个基本的印象;
  2. 其次,先给出了关于vector的基本概念介绍,让大家知道vector的作用以及优势;
  3. 接下来,我们通过结合文档内容,带大家去认识了vector中最常用的接口并模拟实现了各个函数接口;
  4. 最后,关于vector最重要的便是关于迭代器失效的问题以及解决方法(重点)需要大家掌握。

以上全是全文的基本内容了。感谢大家的阅读与支持!!!

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

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

相关文章

CSAPP复习(三)

CH1: 但是如果问什么时候 得到二进制文件 那就是汇编的时候 CH2 sizeof 的返回值是一个无符号数 然后i-D自动默认是一个无符号数 无符号数不能出现负数 所以出现了一个循环 所以永远不可能循环完成。 CH7链接 什么是静态库&#xff1f;使用静态库的优点是什么&#xff1f; …

在windows内使用virtualbox搭建安卓x86--以及所遇到的问题解决--3

一.ARM兼容包的植入 1.下载arm包: 2.安装arm兼容包 3.验证arm兼容包是否移植成功 二.触屏无效 三.玩游戏卡顿严重 一.ARM兼容包的植入 在AndroidX86系统内大部分应用(国内)并没有适配X86架构&#xff0c;安装基于arm架构的应用会出现报错的情况&#xff0c;如遇到此问题可…

【Linux网络】Linux防火墙

Linux防火墙 一 、Linux包过滤防火墙概述1.1iptables概述1.2netfitel与iptables的关系1.3四表五链1.3.1 四表1.3.2五链1.3.3数据包到达防火墙时&#xff0c;规则表之间的优先顺序**1.3.4规则链之间的匹配顺序** 二、iptables的安装与配置方法2.1iptables的安装2.2iptables的配置…

MySQL数据库基础3-基础查询

文章目录 基础查询(单表)替换查询结果排序筛选分页结果更新表删除数据截断(清空)表聚合函数分组查询 基础查询(单表) 创建表、单行插入、多行插入就不重复介绍了。 替换 当我们的程序每天都会产生大量的数据&#xff0c;而这些数据都是前一天或者再之前的数据更新产生&#…

0基础学习VR全景 平台篇第26章:热点功能-3D物体/空间模型

大家好&#xff0c;欢迎观看蛙色VR官方系列——后台使用课程&#xff01; 本期为大家带来蛙色VR平台&#xff0c;热点功能—3D物体/空间模型操作。 热点&#xff0c;指在全景作品中添加各种类型图标的按钮&#xff0c;引导用户通过按钮产生更多的交互&#xff0c;增加用户的多…

opencv_c++学习(十四)

一、图像直方图的统计与绘制 如果直方图各个数字之间分布比较均衡&#xff0c;则其对比度就相对较低&#xff0c;反之亦然。同时也可以通过直方图可以大致了解图像的亮暗等。 calcHist(const Mat * images, int nimages, const int * channels, lnputArray mask, OutputArray…

【Python Xpath】零基础也能轻松掌握的学习路线与参考资料

Python是一种面向对象的编程语言。Xpath是一种在XML文档中定位信息的方法。XPath是一种语言&#xff0c;可以用于xml和html文档中选择和查找节点。在Python中&#xff0c;我们可以使用xpath来解析html页面&#xff0c;从而提取所需的数据。 Python xpath学习路线&#xff1a; …

C++中的继承、以及赋值兼容转换。

一、继承的概念及定义 继承可以使代码复用&#xff0c;允许在保持原有类特性的基础上进行扩展。 举个例子&#xff1a;就好比我现在要封装老师、学生等这些人的属性和方法&#xff0c;但是这些人都有重复的属性和方法&#xff0c;比如name、age、sex等等&#xff0c;那么我可…

let、const、var的区别,解构赋值,箭头函数

let、const、var的区别 使用var声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象。使用let声明的变量,其作用域为该语句所在的代码块内,不存在变量提升。使用const声明的是常量,在后面出现的代码块中,不能在修改改常量的值。 var let const 函数级作用域 块级…

Go语言核心编程-运算符和进制转换

第 4 章 运算符 4.1 运算符的基本介绍 运算符是一种特殊的符号&#xff0c;用以表示数据的运算、赋值和比较等 运算符是一种特殊的符号&#xff0c;用以表示数据的运算、赋值和比较等 算术运算符赋值运算符比较运算符/关系运算符逻辑运算符位运算符其它运算符 4.2 算术运算符…

体验联网版 ChatGPT:优点和缺点同样明显,还藏着无限可能

ChatGPT 有点像古希腊的阿喀琉斯&#xff1a;它很强大&#xff0c;却有个致命的弱点——无法联网&#xff0c;这注定了它只能是一个停留在 2021 年的超人。 但很快&#xff0c;我们将等到一个「鱼和熊掌兼得」的时刻。 通过插件集的 Web browsing 功能&#xff0c;ChatGPT 就…

C++每日一练:贝博士的机械零件(补全题目)奇偶排序(巧妙快排)寻因找祖

文章目录 前言一、贝博士的机械零件1、题目2、解法 二、奇偶排序&#xff08;巧用快排&#xff09;三、寻因找祖 前言 今天的题目中寻因找祖最难了&#xff0c;明显的数学题&#xff0c;笔者这数学文盲水平肯定不会&#xff0c;用暴力搞了一波&#xff0c;只有50%。就去考察学…

(二十六)ATP应用测试平台——将一个微服务打包成含skywalking链路追踪的docker镜像

前言 延续前面的章节内容&#xff0c;本节内容我们以ht-atp的springboot应用为例&#xff0c;封装一个包含skywalking链路追踪的微服务docker应用。完成服务调用的链路追踪监控。skywalking采用字节码注入的方式实现代码的无侵入&#xff0c;探针采集数据粒度粗&#xff0c;但…

详解c++STL—容器set/multiset

目录 1、set基本概念 1.1、功能 1.2、本质&#xff1a; 1.3、set和multiset区别 2、set构造和赋值 2.1、功能描述 2.1、构造 2.3、赋值 2.4、示例 3、set大小和交换 3.1、功能描述 3.2、函数原型 3.2、示例 4、set插入和删除 4.1、功能描述 4.2、函数原型 4.3…

YOLO V3 SPP ultralytics 第一节:VOC 的标注文件(xml)转YOLO标注格式(txt)以及如何自定义YOLO数据样本

目录 1. 前言 2. 关于PASCAL VOC数据集xml --> YOLO txt格式 2.1 路径设定 2.2 读取xml 文件的函数 2.3 xml ---> yolo txt 2.4 yolo 的label文件 2.6 结果 2.7 代码 3. 自定义 YOLO 数据集 3.1 预备工作 3.2 打开labelimg 3.3 绘制 代码参考是b站的大佬&…

CNNs: ZFNet之CNN的可视化网络介绍

CNNs: ZFNet之CNN的可视化网络介绍 导言Deconvnet1. Unpooling2. ReLU3. Transpose conv AlexNet网络修改AlexNet Deconv网络介绍特征可视化 导言 上一个内容&#xff0c;我们主要学习了AlexNet网络的实现、超参数对网络结果的影响以及网络中涉及到一些其他的知识点&#xff0…

RedisInsight—Redis可视化工具

到处都是各种redis的可视化工具&#xff0c;但是那个是redis官网推荐的呢。 答案是&#xff1a;RedisInsight。 好用吗&#xff1f;自己看看吧。反正下载是挺烦躁的。 RedisInsight介绍 RedisInsight支持多种操作系统&#xff1a;Linux、Windows和macOS。 RedisInsight支持…

全网最牛最全面的jmeter教程之jmeter对图片验证码的处理

jmeter对图片验证码的处理 在web端的登录接口经常会有图片验证码的输入&#xff0c;而且每次登录时图片验证码都是随机的&#xff1b;当通过jmeter做接口登录的时候要对图片验证码进行识别出图片中的字段&#xff0c;然后再登录接口中使用&#xff1b; 通过jmeter对图片验证码…

【数据结构】C语言实现堆及其应用

二叉树的顺序结构--堆 一、堆的概念与实现堆的概念堆结构定义堆的初始化与销毁堆的插入与向上调整堆的打印、判空、元素个数size、堆顶元素堆的删除与向下调整大根堆与小根堆的写法区别堆的两种建立方式建堆的时间复杂度推导 二、堆的应用堆的应用--topK堆的应用--堆排序优先级…

learn_C_deep_11 (深刻理解整形提升、左移和右移规则、花括号、++和--操作、表达式匹配:贪心算法)

目录 深刻理解整形提升 左移和右移规则 如何理解"丢弃" 一个问题 0x01<<23 的值是多少 花括号 、--操作 表达式匹配&#xff1a;贪心算法 深刻理解整形提升 #include <stdio.h> int main() {char c 0;printf("sizeof(c): %d\n", sizeo…