[The road to C++ practice] STL - simulation to achieve vector

foreword

Long time no see, everyone. Today we will simulate and realize the vector container in STL.

simulation

class framework

Because the physical space is continuous, we choose to directly use native pointers as iterators, and the capacity and size of the entire vector container are expressed by subtracting three iterators. I have already explained it in the simulation implementation of string, if you don’t understand it, you can go back and have a look.

namespace teacher
{
    
    
	template<class T>
	class vector
	{
    
    
	public:
		//原生指针作为迭代器
		typedef T* iterator;
		typedef const T* const_iterator;
		//容量和大小
		int capacity() const
		{
    
    
			return _end_of_storage - _start;
		}
		int size() const
		{
    
    
			return _finish - _start;
		}

		//迭代器
		iterator begin()
		{
    
    
			return _start;
		}
		iterator end()
		{
    
    
			return _finish;
		}

		const_iterator begin() const
		{
    
    
			return _start;
		}
		const_iterator end() const
		{
    
    
			return _finish;
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

construction and destruction

Whether it is a constructor or a copy construction, it is necessary to assign values ​​to the elements one by one, so first write a push_back function and call it in other constructions.

In addition, C++ allows functions in a template to use templates.

It should be noted that the life cycle of the anonymous object is only in that line, but if it is modified by const T& x, the life cycle will be extended until the end of x.

vector() {
    
    };
vector(size_t n, const T& val = T())
{
    
    
	reserve(n);
	for (int i = 0; i < n; i++)
	{
    
    
		push_back(val);
	}
}
vector(int n, const T& val = T())
{
    
    
	reserve(n);
	for (int i = 0; i < n; i++)
	{
    
    
		push_back(val);
	}
}

template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
    
    
	while (first != last)
	{
    
    
		push_back(*first);
		++first;
	}
}

vector(const vector<T>& v)
{
    
    
	vector<T> tmp(v.begin(),v.end());
	swap(tmp);
}

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

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


Deep copy and shallow copy problem

When our vector memory has pointers to other locations, a simple byte copy( 值拷贝) will make both vectors point to the same location. This problem is very serious, because if it points to the same location, it means that it needs to be destructed multiple times, which will crash our program, so let's first solve the problem of deep and shallow copying.

In the expansion logic shown in the figure below, we can see that we have re-opened up space for each element of the vector, which can avoid the problem of shallow copying.

void reserve(size_t n)
{
    
    
	size_t sz = size();
	// 容量不够才进行扩容
	if (n > capacity())
	{
    
    
		T* tmp = new T[n];
		//拷贝原来的内容
		if (_start)
		{
    
    
			for (int i = 0; i < sz; i++)
			{
    
    
				tmp[i] = _start[i];
			}
			delete[] _start;
		}
	}
	_start = tmp;
	_finish = _start + sz;
	_end_of_storage = _start + n;
}

But writing like this also has a very serious problem, that is, tmp[i] = _start[i] this step is actually a shallow copy, that is, the problem shown in the figure will appear:
insert image description here
in order to solve this problem, we need to write a deep copy by hand 赋值重载.

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

vector<T> operator=(vector<T> v)
{
    
     
	//v是临时对象,传过来完成了拷贝构造,走了push back的逻辑,因此再交换是开了新空间的。
	swap(v);
	return *this;
}

[ ] overload

Because we are used to the [] of the array to read data, and the memory of the vector is continuous, so we overload []

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

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

insert and erase

The logic is basically the same as the simulation implementation of string, because here is an iterator and there is no need to consider the problem of npos.

Although iterators are returned here, after using these two functions, I suggest you not to use pos anymore, thinking that pos is invalid!

iterator insert(iterator pos, const T& val)
{
    
    
	if (_finish == _end_of_storage)
	{
    
    
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 20);
		pos = _start + len;
		//扩容后更新pos 防止pos失效
	}

	iterator end = _finish - 1;
	while (end >= pos)
	{
    
    
		*(end + 1) = *end;
		--end;
	}
	*pos = val;
	++_finish;
	return pos;
}

iterator erase()
{
    
    
	iterator start = pos + 1;
	while (start != _finish)
	{
    
    
		*(start - 1) = *start;
		++start;
	}
	--_finish;
	return pos;
}

push_pop、empty、resize

These three functions are relatively simple.

bool empty()
{
    
    
	return _start == _finish;
}
void pop_back()
{
    
    
	assert(!empty());
	--_finish;
}

void resize(size_t n,T val = T())
{
    
    
	if (n < size())
	{
    
    
		_finish = _start + n;
	}
	else
	{
    
    
		if (n > capacity())
		{
    
    
			reserve(n);
		}

		while (_finish != _start + n)
		{
    
    
			*_finish = val;
			++_finish;
		}
	}
}

epilogue

The above is the whole content of this article, I hope you have gained something, and we will see you next time

Guess you like

Origin blog.csdn.net/m0_73209194/article/details/129471841