嗨喽大家好,时隔许久阿鑫又给大家带来了新的博客,关于vector的模拟实现,下面让我们开始今天的学习吧!
vector的底层实现与模拟
1.关于vector中的插入和删除
2. vector中的拷贝构造和赋值
3.vector的构造函数
4.关于vector中浅拷贝的问题
1.关于vector中的插入和删除
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;
}
size_t capacity()const
{
return _end_of_storage - _start;
}
size_t size()const
{
return _finish - _start;
}
void push_back(const T& x)
{
/*if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
_finish++;*/
insert(_finish, x);
}
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
void pop_back()
{
_finish--;
}
iterator insert(iterator pos, const T& x)
{//扩容会导致迭代器失效
assert(pos >= _start);
assert(pos <= _finish);
int len = pos - _start;
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *(end);
end--;
}
*pos = x;
_finish++;
return pos;
}
void erase(iterator pos)
{
//erase也会导致迭代器失效v
assert(pos >= _start);
assert(pos < _finish);
iterator end = pos;
while (end <= _finish-2)
{
*(end ) = *(end + 1);
++end;
}
_finish--;
}
1.1 尾插和插入时,若需要扩容应该提前保存oldsize的值
我们需要注意的是:size()返回的是_finish-_start的值,但是_start的地址再扩容后已经发生了变化,所以我们需要用oldsize记录下扩容前的对象个数,从而避免调用size()
补充
在C++的std::vector容器中,resize和push_back是两个常用的成员函数但它们有显著的区别:
功能:
push_back:用于在vector的末尾添加一个元素。如果vector的当前大小已经等于其容量(capacity),push_back可能会触发重新分配内存和元素的复制或移动(这通常被称为“重新分配”或“扩容”)。
resize:用于改变vector的大小。如果新的大小大于当前大小,那么会在vector的末尾添加足够多的元素,使其达到新的大小。这些新添加的元素将被初始化为默认值(对于基本类型如int,是0;对于类类型,是调用默认构造函数)。如果新的大小小于当前大小,那么vector会缩短,并丢弃尾部多余的元素。
性能:
push_back:如果vector需要重新分配内存,则性能开销可能较大,因为它涉及到内存的分配、复制或移动以及可能的内存释放。然而,如果vector不需要重新分配内存,则push_back的开销通常很小。
resize:如果新的大小大于当前大小并且需要重新分配内存,则resize的性能开销与push_back在需要重新分配内存时类似。但是,如果resize只是减少vector的大小,那么它通常比连续使用pop_back要快,因为resize只需要一次操作就可以丢弃多个元素。
使用场景:
push_back:当你想要逐个添加元素到vector的末尾时,使用push_back。
resize:当你确切知道你需要多少元素,并希望立即为它们分配空间时,使用resize。这可以避免在后续添加元素时多次重新分配内存。另外,如果你想要快速减少vector的大小,也可以使用resize。
返回值:
push_back:没有返回值(返回类型为void)。
resize:返回一个对vector的引用,允许链式调用。但通常我们不会利用这个特性。
总的来说,push_back和resize在功能、性能和使用场景上都有显著的区别。选择哪一个取决于你的具体需求。
1.2 关于erase和insert中迭代器失效的问题
有些同学想通过传引用的方法,防止外面的实参迭代器失效
注意:若采用传引用的话,由于v1.begin()是传值返回,需要调用拷贝构造,临时对象具有常性,所以pos就不能修改,所以无法完成insert成员函数的内部操作,所以不能利用传引用
string的插入也存在迭代器失效的场景,但大多数时候,string的插入是采用下标,所以平时不太研究string的迭代器失效的场景
在vs下,访问erase后失效的迭代器会直接崩溃,而在Linux下不会直接崩溃
2. vector中的拷贝构造和赋值
//强制编译器生成默认的
vector() = default;
//v1(v3)
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto e:v)
{
push_back(e);
}
}
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)
{
//v3 = v1(this是v3的指针)
this->swap(v);
return *this;
}
3.vector的构造函数
3.1迭代器区间初始化
类模板的成员函数,也可以是函数模板----(支持任意容器的迭代器区间初始化)
//构造函数不能显示的去调用,当一个对象进行实例化的时候
// **编译器会自动调用更匹配的构造函数进行初始化**
// 类模板的成员函数
// 函数模板 -- 目的支持任意容器的迭代器区间初始化
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
void Test_15()
{
vector<int> v3(10,1);
v3.insert(v3.begin() + 3, 10);
for (auto ch : v3)
{
cout << ch << " ";
}
cout << endl;
vector<int> v4(v3.begin()+3, v3.end());
for (auto ch : v4)
{
cout << ch << " ";
}
cout << endl;
string s("hello");
vector<int> v5(s.begin(), s.end());
for (auto ch : v5)
{
cout << ch << " ";
}
cout << endl;
}
3.2 n个val初始化
vector(size_t n, const T& val = T())//匿名对象
{
T* tmp = new T[n];
_start =_finish = tmp;
_end_of_storage = _start + n;
/* reserve(n);*/
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())//给了一个重载的版本,用来解决编译器的匹配问题
{
T* tmp = new T[n];
_start = _finish = tmp;
_end_of_storage = _start + n;
/* reserve(n);*/
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
注意:如果没有第二个重载的版本,当遇到特定情况时,就会发生非法的间接寻址,因为在调用构造时,编译器会选择更对自己口味的构造函数,如果没有重载第二个版本,那么v3的构造就会匹配迭代器区间的初始化,从而造成非法的间接寻址。
3.3 initializer_list的初始化
库系统支持的一个类型initializer_list(和iterator有些类似,都是将某些特定功能封装成一个类)
这个类的对象有两个指针,支持范围for
vector(initializer_list <T> il)//采用列表进行初始化
{
reserve(il.size());
for (auto ch : il)
{
push_back(ch);
}
}
关于花括号的初始化
4.关于vector中浅拷贝的问题
//判断是否扩容
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t oldsize = size();
if (_start)
{
/*for (int i = 0; i < size(); i++)
{
push_back()
}*/
//如果采用上述代码,对内置类型和值拷贝的对象没有影响
for (int i = 0; i < size(); i++)
{
tmp[i] = _start[i];//用的是string的赋值
}
delete[] _start;
}
_start = tmp;
_finish = _start + oldsize;
_end_of_storage = _start + n;
}
}
我们需要注意的是,memcpy对于任意类型的拷贝都是值拷贝