vector的模拟实现

目录

vector各接口总览

构造函数1

构造函数2

 构造函数3

拷贝构造函数

operator=

析构函数

迭代器相关函数

begin和end

容量和大小相关函数

size和capacity

resize和reserve

empty

push_back

pop_back

insert

erase

swap

访问容器相关函数

operator[ ]


vector各接口总览

namespace nxbw
{
	//模拟实现vector
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//默认成员函数
		vector();                                           //构造函数
		vector(size_t n, const T& val);                     //构造函数
		template<class InputIterator>                      
		vector(InputIterator first, InputIterator last);    //构造函数
		vector(const vector<T>& v);                         //拷贝构造函数
		vector<T>& operator=(const vector<T>& v);           //赋值运算符重载函数
		~vector();                                          //析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//容量和大小相关函数
		size_t size()const;
		size_t capacity()const;
		void reserve(size_t n);
		void resize(size_t n, const T& val = T());
		bool empty()const;

		//修改容器内容相关函数
		void push_back(const T& x);
		void pop_back();
		void insert(iterator pos, const T& x);
		iterator erase(iterator pos);
		void swap(vector<T>& v);

		//访问容器相关函数
		T& operator[](size_t i);
		const T& operator[](size_t i)const;

	private:
		iterator _start;        //指向容器的头
		iterator _finish;       //指向有效数据的尾
		iterator _endofstorage; //指向容器的尾
	};
}

注:为了避免和标准库中的元素相冲突,这里我们使用将vector的实现放入命名空间中

在vector中有三个成员变量,如下图所示:

start指向容器的头,finish指向容器有效数据的尾,end_of_storage指向容器的尾

构造函数1

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

构造函数2

vector还支持使用迭代器进行初始化(范围可以是一个区间),因为可以使用一个对象的迭代器对另一个对象赋值,所以迭代器的类型是不确定的,这里需要为这个函数设计一个函数模板,让它可以接收任意类型的迭代器

//可使用任意寄存器类型
template<class inputiterator>
vector(inputiterator first, inputiterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

 构造函数3

这段构造函数的作用是开辟一个n大小的空间并初始化为val,这里复用reserve来开辟空间,然后使用push_back将val放入空间即可

vector(size_t n, const T& val = T())
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{
	reserve(n);
	for (size_t i = 0; i < n; ++i)
	{
		push_back(val);
	}
}

注意:

1.如果我们对使用所需空间的大小是可知的,可以先使用reserve开辟好空间,避免使用push_back时频繁开辟空间,导致效率降低

2.这个函数还需要开一个函数重载

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

可以观察到该函数只有n的类型与上面函数类型不相同,但是这样是必要的,我们来看一下下面这段构造函数调用会出现什么问题

vector<int> v(20,8) //编译器会优先匹配最合适的函数跟帮它初始化

如果没有以上重载函数,v就会去调用最匹配的构造函数2进行初始化,但是2上会解引用first,int不能解引用(vs编译器就会报错)

拷贝构造函数

传统方法:

vector(const vector<T>& v)
{
	_start = new T[v.capacity()];
	//memcpy不行!!!
    //memcpy(_start, v._start, sizeof(T) * v.size());
	for (size_t i = 0; i < v.size(); ++i)
	{
		_start[i] = v[i];
	}

	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

这里不能使用memcpy,拷贝内置类型或这使用浅拷贝的自定义类型不会出现浅拷贝问题,例如:当拷贝的对象是string类型

使用memcpy对vector进行深拷贝,vector进行深拷贝,但是拷贝的是string数组,对string对象进行的是浅拷贝

浅拷贝的缺点:

1.一个被修改会影响另一个

2.会析构两两次

所以这里不能使用memcpy来进行深拷贝

解决方法:

如果T是string这种深拷贝类型的对象,那么就使用string的赋值重载,对string对象进行深拷贝

for (size_t i = 0; i < v.size(); ++i)
{
	_start[i] = v[i];
}

现代方法:

vector(const vector<T>& v)
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{
	reserve(v._capacity());
	for(auto e : v)
	{
		push_back(e);
	}
}

总结:

如果vector当中存储的元素类型是内置类型(int)或浅拷贝的自定义类型(Date),使用memcpy函数进行进行拷贝构造是没问题的,但如果vector当中存储的元素类型是深拷贝的自定义类型(string),则使用memcpy函数将不能达到我们想要的效果。

operator=

传统方法:

//传统写法
vector<T>& operator=(vector<T> v)
{
	if (*this != &v) //防止自己给自己赋值
	{
		delete[] _start; //释放之前的空间
		_start = new vector<T>[v.capacity()]; //申请和v一样大小的空间

		//进行深拷贝
		for (size_t i = 0; i < v.size(); ++i)
		{
			_start[i] = v[i];
		}

		_finish = _start + v.size(); //指向有效数据的尾部
		_end_of_storage = _start + v.capacity(); //指向整个容器的尾部
	}

	return *this //支持连续赋值
}

现代写法:

首先在右值传参时并没有使用引用传参,因为这样可以间接调用vector的拷贝构造函数,然后将这个拷贝构造出来的容器v与左值进行交换,此时就相当于完成了赋值操作,而容器v会在该函数调用结束时自动析构。

//现代方法
vector<T>& operator=(vector<T> v)
{
	swap(v); 

	return *this;
}

注意: 赋值运算符重载的现代写法也是进行的深拷贝,只不过是调用的vector的拷贝构造函数进行的深拷贝,在赋值运算符重载函数当中仅仅是将深拷贝出来的对象与左值进行了交换而已。

析构函数

~vector()
{
    //判断_start是否为空,为空就没必要析构
	if (_start)
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

迭代器相关函数

vector中的迭代器其实就是容器当中所存储数据类型指针

typedef T* iterator;
typedef const T* const_iterator;

begin和end

	//迭代器相关的函数
	iterator begin()
	{
		return _start;
	}

	iterator end()
	{
		return _finish;
	}

	const_iterator begin() const
	{
		return _start;
	}

	const_iterator end() const
	{
		return _finish;
	}

此时再让我们来看看vector使用迭代器的代码也就一目了然了,实际上就是使用指针遍历容器。

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

现在我们实现了迭代器,实际上也就可以使用范围for遍历容器了,因为编译器在编译时会自动将范围for替换为迭代器的形式。

vector<int> v(5, 3);
//范围for进行遍历
for (auto e : v)
{
	cout << e << " ";
}
cout << endl;

容量和大小相关函数

size和capacity

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

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

resize和reserve

reserve规则:
 1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
 2、当n小于对象当前的capacity时,什么也不做。

reserve函数的实现思路也是很简单的,先判断所给n是否大于当前容器的最大容量(否则无需进行任何操作),操作时直接开辟一块可以容纳n个数据的空间,然后将原容器当中的有效数据拷贝到该空间,之后将原容器存储数据的空间释放,并将新开辟的空间交给该容器维护,最好更新容器当中各个成员变量的值即可。

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

            delete[] _start; //注意:这里需要释放_start这里不释放,后面就没机会释放了
        }        
        
        _start = tmp;  //这里调用的赋值重载,进行的深拷贝
        _finish = _start + size();
        _end_of_storage = _start + n; //这里是增容之后指向的容器尾部
    }
    
} 

这里进行的深拷贝不能使用memcpy进行,这里是申请一个扩容好的动态资源tmp,首先去释放_start的原空间,然后让_start指向tmp新空间

如图:

resize规则:
 1、当n大于当前的size时,将size扩大到n,扩大的数据为val,若val未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。
 2、当n小于当前的size时,将size缩小到n。

根据resize函数的规则,进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可。

void resize(size_t n, const T& val = T())
{
    if(n < size())
    {
        _finish = _start + n;
    }
    else
    {
        reserve(n); //reserve n小于capacity是不会扩容的

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

注:在C++引入模板之后,对内置类型进行了特殊的处理,内置类型现在有了自己的构造函数

所以我们给模板类型变量初始化可以给匿名类型,它会去调用自己的构造函数进行初始化

empty

empty函数可以直接通过比较容器当中的_start和_finish指针的指向来判断容器是否为空,若所指位置相同,则该容器为空。

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

push_back

要尾插数据首先得判断容器是否已满,若已满则需要先进行增容,然后将数据尾插到_finish指向的位置,再将_finish++即可。

//尾插数据
void push_back(const T& x)
{
	if (_finish == _endofstorage) //判断是否需要增容
	{
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
		reserve(newcapacity); //增容
	}
	*_finish = x; //尾插数据
	_finish++; //_finish指针后移
}

pop_back

尾删数据之前也得先判断容器是否为空,若为空则做断言处理,若不为空则将_finish–即可。

//尾删数据
void pop_back()
{
	assert(!empty()); //容器为空则断言
	_finish--; //_finish指针前移
}

insert

insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。

//pos是一个地址,有效地址的空间不可能为0
iterator insert(iterator pos, const T& x)
{
    //判断pos位置的合理性
	assert(pos >= _start && pos <= _end_of_storage);

	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;

		T* newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
		
		//解决迭代器失效问题(不能访问迭代器我们认为它失效,访问结果是未定义)
		pos = _start + len;
	}

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

	*pos = x;
	++_finish;

	//间接改变pos位置元素的值
	return pos;
}

注意: 若需要增容,则需要在增容前记录pos与_start之间的间隔,然后通过该间隔确定在增容后的容器当中pos的指向,否则pos还指向原来被释放的空间,会导致迭代器失效。

erase

erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。

iterator erase(iterator pos)
{
    //判断pos位置的合理性
	assert(pos >= _start && pos <= _end_of_storage);

	iterator it = pos + 1;
	while (it != _finish)
	{
		*(it - 1) = *it;
		++it;
	}

	--_finish;

	return pos;
}

swap

swap函数用于交换两个容器的数据,我们可以直接调用库当中的swap函数将两个容器当中的各个成员变量进行交换即可。

//交换两个容器的数据
void swap(vector<T>& v)
{
	//交换容器当中的各个成员变量
	::swap(_start, v._start);
	::swap(_finish, v._finish);
	::swap(_endofstorage, v._endofstorage);
}

注意: 在此处调用库当中的swap需要在swap之前加上“::”(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)。

访问容器相关函数

operator[ ]

vector也支持我们使用“下标+[ ]”的方式对容器当中的数据进行访问,实现时直接返回对应位置的数据即可。

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

	return _start[pos];
}

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

	return _start[pos];
}

注意: 重载运算符[ ]时需要重载一个适用于const容器的,因为const容器通过“下标+[ ]”获取到的数据只允许进行读操作,不能对数据进行修改。

引用和提供了关于实现vector的两种方法。其中,引用展示了一个使用reserve和push_back方法的示例,而引用展示了一个使用new和memcpy函数的示例。这两种方法都是常见的实现vector的方式。 在第一种方法中,通过reserve函数可以预留足够的内存空间,然后使用push_back函数逐个将元素添加到vector中。这种方法的好处是可以避免不必要的内存重分配,提高了效率。 而第二种方法使用new操作符在堆上分配内存空间,并使用memcpy函数将已有的vector对象的数据复制到新的内存空间中。通过这种方式,可以实现深拷贝,即两个vector对象拥有独立的内存空间。这种方法的好处是可以在不修改原始vector对象的情况下创建一个新的vector对象。 除了以上两种方法,还可以使用其他方式实现vector类。例如,可以使用动态数组来实现vector的底层数据结构,然后通过成员函数实现vector的各种操作,如增加、删除、查找等。 总结来说,c语言模拟实现vector的关键是动态内存管理和对元素的增删改查操作。可以使用预留空间和逐个添加元素的方式,也可以使用动态数组和复制数据的方式来实现vector类。具体的实现方式可以根据需求和实际情况选择。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++——vector模拟实现](https://blog.csdn.net/weixin_49449676/article/details/126813526)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值