Simulation Implementation of vector

Keywords: C++ vector


In the last blog, I introduced the use of vector in detail. In order to better use vector, we also need to carefully understand its underlying principle.
There are three members inside the vector:

membersign
_startIndicates the starting position of the used space
_finishIndicates where used space ends
_end_of_storageIndicates where free space ends

iterator

The storage mode of vector is the same as that of array. It uses continuous space for storage. Unlike array, it is dynamic. Therefore, the iterator of vector must support random access. Because it is a continuous space storage method, the native pointer can be used as an iterator of the vector.

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

		const_iterator begin() const
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}


		const_iterator end() const
		{
			return _finish;
		}

Constructor

There are many constructors:

non-parameter constructor

Parameterless construction indicates that all pointers in the vector are null pointers, and there is no allocated space inside the vector. Then it can be constructed by initializing three members in the vector as null pointers.

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

Constructor with length and initialization value

When creating, put n initialization values into the vector, that is, initialize the space and content of the vector at the beginning.

	vector(int n, const T& val = T())//The second value here is the initialization value. The default value is equivalent to an anonymous object, that is, you can initialize whatever type you are
			: _start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)
		{
			reserve(n);
			//Put in the value to be initialized
			//It can be implemented by pointer or push_back implementation
			while (n--)
			{
				//push_back(val);
				*(_start + n) = val;
				_finish++;
			}
		}

In order to prevent code redundancy during implementation, the reserve function is reused when modifying the space (the implementation of the reserve function is described below), and the initialization traversal assignment or reuse push of the content are assigned_ Back function.

Constructor using iterators

Since vector is stored in continuous space, its iterator can be replaced by native pointer, which has the function of random access. Then initialization through iterators is to traverse this space and put the values of this space into.

		//3. Initialization by iterator
		//Using function templates
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			reserve(last - first);
			while (first != last)
			{
				//push_back(*first);
				first++;
			}
		}

The reason for using the function template is that there are many types of iterators. If you only write a single function and encounter other iterators, you have to write another one. This can be solved through the function template.

copy constructor

The copy constructor is to construct the same vector for a given vector. In order to reduce the redundancy of code, reuse the written functions as much as possible. Then our idea can be as follows: to copy and construct a given vector, firstly, the internal space must be the same, so we can use the reserve function to open up the same space as the given vector; Then traverse and copy the internal elements.

//copy constructor 
//The following operations to open up space and modify member variables are performed in the reserve function
//There is a problem here: normally, if the copy structure is not initialized, the space will be released if the pointer is not null during reserve, and an error will be reported
		vector(const vector<T>& v)
			: _start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)
		{
			reserve(v.capacity());
			//Similarly, memcpy cannot be used for copying here

			//If it is named cbegin, cend will make an error
			for (auto e : v)
				push_back(e);
		}

There are several details to pay attention to:
1. When copying and constructing with the reserve function, null pointer initialization is required for the member variable. This is because the reserve function is called, and the original space will be released when opening up the space. At this time, if there is no initialization, it is a wild pointer, and then the release will make an error.
2. Here, memcpy cannot be used for copying elements, because memcpy copies are shallow copies, and there will be two destructions during vector destructions.

Of course, you can also open up a space for copy construction without using the reserve function.

		vector(const vector<T>& v)
			//: _start(nullptr)
			//, _finish(nullptr)
			//, _endOfStorage(nullptr)
		{
			_start = new T[v.capacity()];
			for (size_t i = 0; i < v.size(); ++i)
				_start[i] = v._start[i];
			_finish = _start + v.size();
			_endOfStorage = _start + v.capacity();
		}

Assignment operator overload

The overloading of assignment operators is mostly written in modern ways

void swap(vector<T>& v)
{
	//Use the domain qualifier to find the global first
	::swap(_start, v._start);
	::swap(_finish, v._finish);
	::swap(_endOfStorage, v._endOfStorage);
}
		
//If it is written in a modern way, v will be destroyed after the exchange. If it is not initialized to a null pointer, the random space will be destroyed. This is wrong
//The exchange function cannot be referenced here, otherwise the original value will be changed
vector<T>& operator=(vector<T> v)//The reason why it can be exchanged is that a deep copy is made when transferring parameters. After the exchange, v it will be destroyed, just releasing the original space
{
	swap(v);//The exchange function here is best implemented by itself. If it is given by the library function, it needs to be copied many times
	return *this;
}

The overloading of assignment operator in modern writing is completed by exchanging two different vectors. In order not to change the value of the original vector, no reference is added when passing parameters. At this time, the compiler will copy and construct a copy, and we will exchange the vector with the current one. After exchange, v it will be automatically destroyed to complete the purpose of copy construction.

be careful:
1. Since the copy structure of modern writing needs to be used and will be released after exchange, the copy structure needs to be initialized here.
2. The exchange function here is best implemented by itself. If it is given by the library function, it needs to be copied many times.

reserve function and resize function

The reserve function is used to change the space capacity of a vector. The reserve function is divided into two aspects. If the changed space capacity is less than the current capacity, the reserve function does nothing and will not change the space capacity; If the changed space capacity is greater than the current capacity, reserve will change the space of the vector to the capacity you want to open up.
When the vector opens space, it needs to pay a price, that is, three steps: reconfiguration, moving data and releasing the original space.

//reserve function
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();//Save the length of the original size, otherwise the pointer will be changed after capacity expansion
		//delete[] _start;// Destroy_ Start means that other pointers are destroyed
		iterator tmp = new T[n];
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T)*sz);
			//memcpy copy is a shallow copy, that is, it directly gives the space of the source file to the space of the target file
			//If it is a built-in type or suitable for shallow copy, this is OK
			//If resource management is involved, there will be two releases
			for (size_t i = 0; i < sz; ++i)
				tmp[i] = _start[i];//You can assign values directly. In this way, for some types of deep copy, the assignment overload is a deep copy, and there is no problem at all

			delete[] _start;//Destroy_ start means that other pointers are destroyed

		}
		_start = tmp;
		_finish = _start + sz;
		_endOfStorage = _start + n;
	}
}

The resize function is used to change the size of the current effective space, and can assign a value to the new space.
resize can be implemented in three categories:
The first type is that the changed length is less than the current effective length, so you only need to_ Move the position of fish forward.
The second type is that the changed length is greater than the effective length, but less than the space capacity. At this time, you only need to assign the new length.
The third type is that the changed length is greater than the space capacity, so you need to open up a new space first, and then assign a value in the new length.

It can be found that the first type is to subtract the effective length. If the growth of the second and third types of space is the same as that of the reserve function, the reserve function can be called, and only the new length needs to be assigned.

//resize function
void resize(size_t n, T val = T())
{
	if (n > size())
	{
		reserve(n);
		int len = n - size();
		while (len--)
		{
			*_finish = val;
			_finish++;
		}
	}
	else
	{
		_finish -= (size() - n);
	}
}

Insert function

Tail insertion

The tail of vector is inserted in_ Insert an element at the finish position. Here, only the expansion when the space is full needs to be considered.

//Tail insertion
void push_back(const T& val)
{
	//If there is enough space, expand the capacity
	if (_finish == _endOfStorage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}
	*_finish = val;
	++_finish;
}

insert function

The insert function is to insert at any position. Because the vector is a continuous space, the insertion at any position needs to move the data, and the cost is relatively high.

Insertion of a single element

//1. Insertion of a single element
iterator insert(iterator pos, const T& val)
{
	assert(pos);
	//If there is not enough space, expand the capacity
	if (_finish == _endOfStorage)
	{
		int newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}
	iterator ptr = _finish;
	//Mobile data
	while (ptr != pos)
	{
		*ptr = *(ptr - 1);
		--ptr;
	}
	*pos = val;
	_finish++;
	return pos + 1;
}

Insert multiple elements

//2. Insert multiple elements
void insert(iterator pos, int n, const T& val)
{
	assert(pos);
	//After the expansion, the space changes, so you need to record the coordinates of the original elements
	size_t old_sz = pos - _start;
	iterator new_ptr = _finish + n;//Get the length after inserting multiple elements
	//If it is larger than the capacity, it needs to be expanded
	if (new_ptr > _endOfStorage)
		reserve(new_ptr - _start);
	pos = _start + old_sz;
	iterator ptr = pos;
	//Mobile data
	while (ptr != _finish)
	{
		*(ptr + n) = *ptr;
		++ptr;
	}
	_finish += n; 
	//Put multiple elements from the pos position
	while (n--)
	{
		*pos = val;
		pos++;
	}
}

Insert a section

//3. Insert an interval at a location
template<class InputIterator>
void insert(iterator pos, InputIterator first, InputIterator last)
{
	assert(pos);
	//First, calculate the length to be inserted
	int len = last - first;
	size_t old_sz = pos - _start;
	iterator new_ptr = _finish + len;//Get the length after inserting multiple elements
	//If it is larger than the capacity, it needs to be expanded
	if (new_ptr > _endOfStorage)
		reserve(new_ptr - _start);
	pos = _start + old_sz;
	iterator ptr = pos;
	//Mobile data
	while (ptr != _finish)
	{
		*(ptr + len) = *ptr;
		++ptr;
	}
	_finish += len;
	while (first != last)
	{
		*pos = *first;
		pos++;
		first++;
	}
}

Delete function

Tail deletion

//Tail deletion
void pop_back()
{
	//It should be asserted here that if it is empty, it cannot be deleted
	assert(!empty());
	_finish--;
}

erase function

Delete a single element

//1. Delete a single element
		iterator erase(iterator pos)
		{
			assert(pos);
			iterator ptr = pos + 1;
			while (ptr != _finish)
			{
				*(ptr - 1) = *ptr;
				ptr++;
			}
			_finish--;
			return pos;
		}

Delete an element

//2. Delete an element
void erase(iterator first, iterator last)
{
	int len = last - first;
	iterator ptr = last + 1;
  			while (ptr != _finish)
	{
		*(ptr - len) = *ptr;
		ptr++;
	}

	_finish -= len;
}

Overload [] operator

Overloading the [] operator enables a vector to access elements like an array

//Overload [] operator
T& operator[](size_t n)
{
	return *(_start + n);
}

const T& operator[](size_t n) const
{
	return *(_start + n);
}

Other functions

size() function

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

capacity() function

//capacity function
size_t capacity() const
{
	return _endOfStorage - _start;
}

Destructor

//Destructor
~vector()
{
	if (_start)
		delete[] _start;//Destroy_ start is equivalent to destroying the entire vector
	_start = _finish = _endOfStorage = nullptr;
}

Posted by jenniferG on Sun, 03 Oct 2021 20:58:45 -0700