06 vector class and Simulation Implementation in STL

Keywords: C++ Container

1, Introduction to vector

1. vector is a sequence container that represents a variable size array.
2. Like an array, vector also uses continuous space to store elements, which also means that the elements of vector can be accessed by subscripts.
3. The difference between vector and ordinary array is that the size of vector can be changed dynamically.
4. When a vector needs to be resized, it allocates a new array, then moves all elements into the array and frees up the original array space.
5. Vector space allocation strategy: vector will allocate some additional space to accommodate possible growth, so the storage space is generally larger than the actual storage space. Different libraries use different strategies to balance the use and reallocation of space, so that the insertion of an element at the end is completed in a constant time complexity.
6. Because vector uses continuous space to store elements, compared with other dynamic sequence containers, vector is more efficient in accessing elements, adding and deleting elements at the end is relatively efficient, and the efficiency of deleting and inserting elements not at the end is relatively low.

It is worth noting that vector is very similar to string in both implementation and usage, but there are differences between them. String class is a dynamic array of characters, because there is an interface c_str, which can be converted into a string of c language. It should end with \ 0, so the string class will end with a \ 0.
Vector < T > is a dynamic array for saving t type, and vector < char > is also a dynamic array for saving characters. However, it will not end with \ 0 and will not save \ 0.

Both of them are essentially a sequential table. The header file iostream does not contain a vector, so the header file < vector > should be added when using

2, Common interface

2.1. Constructor

Default construction

Construct an empty container of a certain type:

explicit vector (const allocator_type& alloc = allocator_type());

The default parameter allocator is used to specify the space configurator to be used. STL provides the default space configurator. We don't care about this parameter unless we implement a space configurator ourselves and want to use the space configurator we write.

vector<int> v; //Construct an empty container of type int

Semi default construction

Construct a container of a certain type containing n Vals (val is 0 by default):

explicit vector (size_type n, const value_type& val = value_type(),
                 const allocator_type& alloc = allocator_type());
vector<int> v2(10, 1); //Construct an int type container with 10 1s

copy construction

vector (const vector& x);

Iterator construction

Use iterator copy to construct a piece of content:

template <class InputIterator>
         vector (InputIterator first, InputIterator last,
                 const allocator_type& alloc = allocator_type());

This method can also be used to copy some content of other containers, because the member function of vector is a template function. Parameters can be passed as long as the type after dereference of iterator is consistent with that after instantiation of vector:

string s("hello world");
vector<char> v5(s.begin(), s.end()); //Copy a section of the content of the constructed string object

2.2. Element access method

[] subscript access

The [] operator is overloaded in vector, so we can also access the elements in the container by means of "subscript + []".

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v(10, 1);
	//Traverse the container with "[] subscript"
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	return 0;
}

Iterator and scope for access

The compiler will automatically replace the range for with an iterator. Vector supports iterators, so we can also use the range for to traverse the vector container:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v(10, 1);
	//Range for
	for (const auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;

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

The essence of the range for is to take out the content of v and copy it to e. if the vector is a built-in type, the consumption is very small, but if its content is a string type, deep copy will occur, resulting in huge consumption. Therefore, it is necessary to reference and pass parameters.

2.3. Spatial growth function

size() and capacity()

The size() function obtains the number of valid elements in the current container, and obtains the maximum capacity of the current container through the capacity() function.

reserve() and resize()

void reserve (size_type n);
void resize (size_type n, value_type val = value_type());

Change the maximum capacity of the container through the restore() function, and the resize() function changes the number of valid elements in the container.

reserve rule:
  1. When the given value is greater than the current capacity of the container, expand the capacity to this value.
  2. Do nothing when the given value is less than the current capacity of the container.
resize rule:
  1. When the given value is greater than the current size of the container, expand the size to the value. The expanded element is the second given value. If it is not given, it is 0 by default.
  2. When the given value is less than the current size of the container, reduce the size to this value.

capacity expansion rules

#include <iostream>
#include <vector>
int main()
{
	size_t sz;
	std::vector<int> foo;
	sz = foo.capacity();
	std::cout << "making foo grow:\n";
	for (int i = 0; i < 100; ++i) {
		foo.push_back(i);
		if (sz != foo.capacity()) {
			sz = foo.capacity();
			std::cout << "capacity changed: " << sz << '\n';
		}
	}
}

When the capacity code is run in vs and g + + respectively, it will be found that the capacity increases by 1.5 times in vs and 2 times in g + +.
This is because vs is the PJ version STL and g + + is the SGI version STL.
The C + + Standard Committee only specifies the name of a container and which interfaces should be provided, and does not specify the underlying implementation method of the interface. Therefore, the underlying implementation method of the interface provided by different compilers may be different, which is the freedom of the implementer.
The reason why the expansion is 1.5 times or 2 times is due to the designer's consideration. The higher the expansion multiple, the lower the expansion frequency, and the larger the space capacity that may be wasted. If we determine how much space we want, we can open up the space in advance through reserve/resize

2.4. Interface for adding, deleting, querying and modifying

push_back and pop_back

For the tail insertion and tail deletion of the container, it is worth noting that the vector does not provide header insertion and header deletion, because the efficiency of header insertion and header deletion is relatively low (moving elements backward), and inset and erase can be used to realize over insertion and header deletion.

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v;
	v.push_back(1); //Trailing element 1
	v.push_back(2); //Trailing element 2
	v.push_back(3); //Trailing element 3
	v.push_back(4); //Trailing element 4

	v.pop_back(); //Tail deletion element
	v.pop_back(); //Tail deletion element
	v.pop_back(); //Tail deletion element
	v.pop_back(); //Tail deletion element
	return 0;
}

insert and erase

//Insert Val in pos position and return the position of val
iterator insert (iterator position, const value_type& val);

//Insert n Vals in pos position
void insert (iterator position, size_type n, const value_type& val);

//Insert an interval at the pos position
template <class InputIterator>
    void insert (iterator position, InputIterator first, InputIterator last);
//Delete the element at the pos position, and the return value is the iterator of the next element at the pos position
iterator erase (iterator position);
//Delete an interval, and the return value is the iterator of the next element of the interval
iterator erase (iterator first, iterator last);

find function

template <class InputIterator, class T>
   InputIterator find (InputIterator first, InputIterator last, const T& val);

The find function has three parameters. The first two parameters determine an iterator interval (left closed and right open), and the third parameter determines the value to be searched.
The find function looks for the first matching element in the given iterator interval and returns its iterator. If it is not found, it returns the given second parameter.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

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); //Gets the iterator for the element with value 2
	
	v.insert(pos, 10); //Insert 10 at position 2

	pos = find(v.begin(), v.end(), 3); //Gets the iterator for the element with value 3
	v.erase(pos); //Delete 3
	return 0;
}

swap

swap function can exchange the data space of two containers to realize the exchange of two containers.

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1(10, 1);
	vector<int> v2(10, 2);

	v1.swap(v2); //Exchange data space of V1 and V2

	return 0;
}

2.5. Iterators

begin and end

The begin() function returns the forward iterator of the first element in the container, and the end() function returns the forward iterator of the last element in the container.

rbegin and rend

The rbegin() function returns the inverse iterator of the last element in the container, and the rend() function returns the inverse iterator of the previous position of the first element in the container.

3, Failure of vector iterator

Example 1:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v;
	for (size_t i = 1; i <= 6; i++)
	{
		v.push_back(i);
	}
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0) //Delete all even numbers in the container
		{
			v.erase(it);
		}
		it++;
	}
	return 0;
}

The iterator accessed a memory space that does not belong to the container, causing the program to crash:

Moreover, when the iterator traverses the elements in the container, it does not judge the 1, 3 and 5 elements.

In addition, when the original space is released, the original iterator points to a space that has been released.

Example 2:

#include <iostream>
#include <vector>
using namespace std;

int main()
{

	vector<int> v(10,0);
	v[1] = 10;
	auto pos = find(v.begin(), v.end(),10);
	v.insert(pos, 0);
	v.erase(pos); 
	return 0;
}

3.1. Iterator failure resolution

This problem can be solved by reassigning the iterator before each use.

For instance 1, we can receive the return value of the erase function (the erase function returns the new location of the next element after deleting the element). And control the logic of the code: continue to judge the element at this position after the element is deleted (because the element at this position has been updated and needs to be judged again).

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v;
	for (size_t i = 1; i <= 6; i++)
	{
		v.push_back(i);
	}
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0) //Delete all even numbers in the container
		{
			it = v.erase(it); //Gets the iterator of the next element after deletion
		}
		else
		{
			it++; //it is an odd number++
		}
	}
	return 0;
}

For instance 2, you can find it again before deleting it.

#include <iostream>
#include <vector>
using namespace std;

int main()
{

	vector<int> v(10,0);
	v[1] = 10;
	auto pos = find(v.begin(), v.end(),10);
	v.insert(pos, 0);
	pos = find(v.begin(), v.end(), 10); //Gets the iterator for the element with value 2
	v.erase(pos); 
	return 0;
}

4, vector simulation implementation

There are three member variables in the vector:

private:
	iterator _start; // Point to the beginning of the data block
	iterator _finish; // Tail pointing to valid data
	iterator _endofStorage; // Tail pointing to storage capacity

There are three member variables in the vector_ start,_ finish,_ endofstorage.
_ start points to the head of the container_ finish points to the end of valid data in the container_ The end of storage points to the end of the entire container.

4.1. Constructor

Default construction and semi default construction

vector(int size=0, T val = T())
	: _start(nullptr)
	, _finish(nullptr)
	, _endofStorage(nullptr)
{
	assert(size >= 0);
	if (size>0)
	{
		_start = new T[size];
		if (_start)
		{
			for (size_t i = 0; i < size; ++i)
				_start[i] = val;
		}
		_finish = _start + size;
		_endofStorage = _start + size;
	}
}

Iterator construction

template<class InputIterator> //template function
vector(InputIterator first, InputIterator last)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofStorage(nullptr)
{
	//Insert the data of the iterator interval in [first, last] into the container one by one
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

copy constructor

Traditional writing of copy constructor:

//Modern writing
vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofStorage(nullptr)
{
	reserve(v.capacity()); //Open up the same space as v
	for (auto e : v) //Insert the data in the container v one by one
	{
		push_back(e);
	}
}

//It can also be written in this way
//vector(const vector<T>& v)
//:_start(nullptr)
//, _finish(nullptr)
//, _endofStorage(nullptr)
//{/ / copy structure
//	_start = new T[v.capacity()];
//	size_t size = v.size();
//	if (_start)
//	{
//		For (size_t I = 0; I < size; + + I) / / memcpy cannot be used
//			_start[i] = v._start[i];
//	}
//	_endofStorage = _start + v.capacity();
//	_finish = _start + v.size();
//}

Note that you cannot use memcpy to copy data here, because memcpy is a shallow copy. When the type in the vector is string and other data, you need to use deep copy to complete it (here is the = assignment operator overload of string itself).

Destructor

~vector() {
	if (_start)
		delete[]_start;

	_start = _finish = _endofStorage = nullptr;
}

Assignment operator overloaded function

vector<T>& operator=(vector<T> v) //Copy construct a parameter
{
	swap(v); //Swap the two objects
	return *this; //Continuous assignment is supported
}


//It can also be written in this way
//vector<T>& operator=(const vector<T>& v)
//{
//	If (this! = & V) / / prevent you from assigning values to yourself
//	{
//		delete[] _start; / / free up the original space
//		_start = new T[v.capacity()]; / / open up a space with the same size as container v
//		For (size_t I = 0; I < v.size(); I + +) / / copy the data in container V one by one
//		{
//			_start[i] = v[i];
//		}
//		_finish = _start + v.size(); / / end of valid data in container
//		_endofStorage = _start + v.capacity(); / / end of the entire container
//	}
//	return *this; / / continuous assignment is supported
//}

4.2. Iterator

// The iterator for Vector is a native pointer
typedef T* iterator;
typedef const T* const_iterator;

begin() and end()

iterator begin()
{
	return _start; //Returns the first address of the container
}
iterator end()
{
	return _finish; //Returns the address of the next data of valid data in the container
}
const_iterator begin()const
{
	return _start; //Returns the first address of the container
}
const_iterator end()const
{
	return _finish; //Returns the address of the next data of valid data in the container
}

4.3. Capacity function

size() and capacity()

size_t size()const
{
	return _finish - _start; //Returns the number of valid data in the container
}
size_t capacity()const
{
	return _endofStorage - _start; //Returns the maximum capacity of the current container
}

reserve

void reserve(size_t n)
{
	if (n > capacity()) //Determine whether operation is required
	{
		size_t sz = size(); //To prevent _finish failure, record the number of valid data in the current container first
		T* tmp = new T[n]; //Open up space
		if (_start) //Determine whether it is an empty container
		{
			for (size_t i = 0; i < sz; i++) //Copy the data in the container to tmp one by one
			{
				tmp[i] = _start[i];
			}
			delete[] _start; //Free up the space where the container itself stores data
		}
		_start = tmp; //Submit the data maintained by tmp to _start for maintenance
		_finish = _start + sz; //Tail of valid data of container
		_endofStorage = _start + n; //Tail of container
	}
}

resize

void resize(size_t n, const T& val = T())
{
	if (n < size()) //When n is less than the current size
	{
		_finish = _start + n; //Reduce size to n
	}
	else //When n is greater than the current size
	{
		if (n > capacity()) //Judge whether to increase capacity
		{
			reserve(n);
		}
		while (_finish < _start + n) //Expand size to n
		{
			*_finish = val;
			_finish++;
		}
	}
}

empty

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

4.4. Addition, deletion, query and modification

push_back

//Tail interpolation data
void push_back(const T& x)
{
	if (_finish == _endofstorage) //Judge whether to increase capacity
	{
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //Double capacity
		reserve(newcapacity); //increase capacity
	}
	*_finish = x; //Tail interpolation data
	_finish++; //_The finish pointer moves back
}

pop_back

//Tail deletion data
void pop_back()
{
	assert(!empty()); //Assertion if container is empty
	_finish--; //_The finish pointer moves forward
}

insert

//insert
iterator insert(iterator pos, const T& x) {
	if (_finish == _endofStorage) {
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}
	size_t inpos = pos - _start;//Save a copy of relative position to prevent iterator failure

	pos = _start + inpos;
	iterator last = _finish;
	while (last != pos){//Move back
		*last = *(last - 1);
		last--;
	}
	*pos = x;
	_finish++;
	return pos;//Returns the position where pos points to the newly inserted element
}

erase

//delete
iterator erase(iterator pos) {
	assert(pos >= _start && pos < _finish);
	iterator temp_pos = pos;//Save the original pos location
	while (pos < _finish - 1)//Move the element after pos forward
	{
		*pos = *(pos + 1);
		pos++;
	}
	_finish--;
	return temp_pos;//Return to the original position
}

swap

The swap function is used to exchange the data of two containers. We can directly call the swap function in the library to exchange the member variables of the two containers.

//Exchange data between two containers
void swap(vector<T>& v)
{
	//Exchange member variables in the container
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofStorage, v._endofStorage);
}

4.5. Element access function

operator[ ]

T& operator[](size_t i)
{
	assert(i < size()); //Detect the legitimacy of Subscripts
	return _start[i]; //Return corresponding data
}
const T& operator[](size_t i)const
{
	assert(i < size()); //Detect the legitimacy of Subscripts
	return _start[i]; //Return corresponding data
}

appendix

namespace hjl
{
	template<class T>
	class vector
	{
	public:
		// The iterator for Vector is a native pointer
		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;
		}
		//Construction and semi default construction
		vector(int size=0, T val = T())
			: _start(nullptr)
			, _finish(nullptr)
			, _endofStorage(nullptr)
		{
			assert(size >= 0);
			if (size>0)
			{
				_start = new T[size];
				if (_start)
				{
					for (size_t i = 0; i < size; ++i)
						_start[i] = val;
				}
				_finish = _start + size;
				_endofStorage = _start + size;
			}
		}
		
		//Iterator construction
		template<class InputIterator> //template function
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofStorage(nullptr)
		{
			//Insert the data of the iterator interval in [first, last] into the container one by one
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

		void swap(vector<T>& x)
		{
			std::swap(_start, x._start);
			std::swap(_finish, x._finish);
			std::swap(_endofStorage, x._endofStorage);
		}
		//copy construction 
		vector(const vector<T>& v)
		    :_start(nullptr)
			,_finish(nullptr)
			,_endofStorage(nullptr){
			_start = new T[v.capacity()];
			size_t size = v.size();
			if (_start)
			{
				for (size_t i = 0; i < size; ++i)
					_start[i] = v._start[i];
			}
			_endofStorage = _start+v.capacity();
			_finish = _start + v.size();
		}

		//Heavy load=
		vector<T>& operator=(vector<T> v) //The compiler automatically calls its copy constructor when it receives the right value
		{
			swap(v); //Swap the two objects
			return *this; //Continuous assignment is supported
		}

		~vector() {
			if (_start)
				delete[]_start;

			_start = _finish = _endofStorage = nullptr;
		}

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

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

		//Overload []
		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];
		}

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

		void resize(size_t n,T val=T()) {//Call the constructor of the default anonymous object
			if (n < size()){
				_finish = _start + n;
			}
			else {
				if (n > capacity()) {
					reserve(n);//When n is larger than capacity, increase capacity first
				}//end if
				while (_finish < _start + n) {
					*_finish = val;
					_finish++;
				}//end while
			}//end else

		}

		//Tail insertion
		void push_back(const T& x) {
			if (_finish == _endofStorage) {
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = x;
			_finish++;
		}

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

		//insert
		iterator insert(iterator pos, const T& x) {
			if (_finish == _endofStorage) {
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			size_t inpos = pos - _start;//Save a copy of relative position to prevent iterator failure

			pos = _start + inpos;
			iterator last = _finish;
			while (last != pos){//Move back
				*last = *(last - 1);
				last--;
			}
			*pos = x;
			_finish++;
			return pos;//Returns the position where pos points to the newly inserted element
		}
		//delete
		iterator erase(iterator pos) {
			assert(pos >= _start && pos < _finish);

			iterator temp_pos = pos;
			while (pos < _finish - 1)//Move the element after pos forward
			{
				*pos = *(pos + 1);
				pos++;
			}
			_finish--;

			return temp_pos;//Return to the original position
		}

	private:
		iterator _start; // Point to the beginning of the data block
		iterator _finish; // Tail pointing to valid data
		iterator _endofStorage; // Tail pointing to storage capacity
	};

}

Posted by andremta on Sat, 06 Nov 2021 15:48:17 -0700