1.vector原理介绍
vector是一种内存连续的、可动态改变自身存储大小的可变数组,其属于C++标准模板库(standard template library,STL),使用vector需包含头文件<vector.h>。
vector内的元素,可类似普通数组一样使用下标直接访问,也可以通过迭代器进行访问。迭代器是一种可以访问容器内所有元素的工具,后面再详细介绍。
vector的容量是可以动态改变的,一般情况下使用vector的步骤是:声明一个vector并指定其元素的数据类型---初始化该vector的容量大小---往该vector增删改查元素---销毁或回收vector内存。在增删改查vector元素的过程中,如果往vector中插入的元素超出了vector的容量(或者达到编译器设置的扩容阈值)时,就会触发vector的扩容操作。vector的扩容操作步骤如下:达到扩容阈值---新开辟一块大小是原vector容量的若干倍的连续内存控件---将原vector的所有数据拷贝至新开辟的内存---让vector指向新的内存---回收原先vector开辟的内存---插入新的元素。从上述步骤可以看到如果频繁触发vector的扩容操作,则会造成显著的时间损耗。
不同系统设置的扩容系数不一样,Windows是1.5倍,Linux是2倍。
2.常用函数
resize()//改变vector容量1
reserve()//改变vector容量2
assign()//改变vector容量3,同时改变容器中的值
size()//返回返回容器中元素个数
capacity()//返回vector开辟的内存大小
begin()//返回头部迭代器
end()//返回尾部+1迭代器
rbegin()//返回逆首部迭代器
rend()//返回逆尾部-1迭代器
front()//返回首个元素
back()//返回尾部元素
push_back()//在末尾添加一个元素
emplace_back()//和push_back()是一样的作用
pop_back()//弹出最后一个元素
empty()//判断是否为空
insert()//在指定位置插入元素
erase()//在指定位置删除元素
clear()//清空容器
①resize()
有下面两个重载函数
//将容器大小变为n,若n小于当前容器的容量,则n后面的元素全部删除,若n大于当前容器的容量
//则在容器末尾添加n-vec.size()个值,并且将值都设为各数据类型的默认值,例如int为0,std::string为空
void resize(size_type n);
//参数n的含义与上述相同,参数val的作用是在n大于当前容器容量时,在容器末尾添加n-vec,size()个值,
//并且将多出来的值均设置为val,当n小于当前容器容量时,多余的元素会被删除,val无效。
void resize(size_type n, const value_type& val = value_type());
②reserve()
reserve本身不会触发vector对元素的任何操作,同时也不会触发vector元素的构造和析构,所以当调用了reserve,如果此时vector无元素,则size()返回值仍为0。reserve的作用主要是在vector即将添加大量元素时预先申请一块内存,尽可能减少因为频繁插入导致触发扩容操作的次数,以及避免因内存分配或回收导致产生过多的内存碎片。
//开辟一块内存,指明系统应该为这个容器预留多大的空间
//如果在reserve之前vector已经有足够的空间容纳元素了,可能reserve不生效
void reserve(size_type n);
③assign()
assign()用于给vector分配元素,并支持替换当前内容,assign支持接收不同类型的参数,例如:
std::list<int> lst = {1, 2, 3, 4, 5};
std::vector<int> vec;
vec.assign(lst.begin(), lst.end());
需要注意,极限情况下,在使用assign()函数时,若使用其他容器的迭代器来初始化vector的元素时,如果迭代器的范围超出了vector容器规定的能申请到的最大内存长度max_len,则会报std::length_error异常。情况较为少见。
④size()
获取vector中元素个数,并非获取vector实际开辟的内存大小。
⑤capacity()
获取vector实际开辟的内存大小
⑥push_back和emplace_back
对于二者的比较,知乎有一篇贼啰嗦的文章进行了讲解:http://【C++ 容器操作】C++高效编程:掌握emplace_back与push_back的使用和机制 - 泡沫o0的文章 - 知乎 https://zhuanlan.zhihu.com/p/682517443。简而言之,这两个都是用来在vector末尾插入元素的函数,二者之间的差异如下:
1)构造方式
push_back接受一个已构造对象作为参数,该对象会被拷贝或移动到容器中。如果传入的是临时对象或右值,C++11及以后版本支持移动语义,可以减少不必要的拷贝开销。
emplace_back接受构造新对象所需的参数,并在容器内直接构造新对象,避免了额外的拷贝或移动操作。这种原地构造的方式通常更为高效。
2)传入参数
push_back:需要传入一个完整的对象,这个对象可以是左值或右值。
emplace_back:需要传入构造对象所需的参数列表,这些参数会直接用于在容器内部构造新对象。
3)性能开销
push_back:如果传入的是左值,会进行一次拷贝操作,
emplace_back:直接在容器内部构造对象,避免了拷贝或移动操作,因此在插入大型对象或频繁插入操作时,emplace_back通常比push_back具有更好的性能。
4)错误定位
由于emplace_back是在容器内构造对象,所以如果出现错误,定位起来比push_back难度要大。
5)支持隐式转换
push_back支持隐式转换,即如果传入的参数类型与容器内部元素的类型不完全匹配,但可以通过隐式转换得到,那么push_back仍然可以正常工作。
emplace_back不支持隐式转换,传入的参数必须能够直接用于构造容器内部元素类型的对象。
3.手动实现一个vector
template<typename T>
class MyVector
{
private:
T* data;
size_t size;
size_t capacity;
void allocateMemory(size_t new_size);
public:
typedef T* iterator;
size_t Size() const { return size; };
size_t Capacity() const { return capacity; };
bool empty();
void resize(size_t new_size);
void resize(size_t new_size, T val);
void push_back(T val);
void insert(iterator iter, T val);
T pop_back();
void erase(iterator iter);//擦除指定位置的数据
T& front();//第一个数
T& back();//最后一个数
T& at(size_t index) const;
//构造函数,包括默认构造函数,拷贝构造函数
MyVector() : data(nullptr), size(0), capacity(0) {};
MyVector(size_t _size);
MyVector(size_t _size, T ele);
MyVector(const MyVector& vec);
//重载操作符
bool operator== (const MyVector& vec) const;
MyVector& operator= (const MyVector& vec);
T& operator[](size_t index);
~MyVector() { delete[] data; data = nullptr; };
};
template <typename T>
bool MyVector<T>::empty()
{
if (size == 0)
{
return true;
}
return false;
}
template<typename T>
void MyVector<T>::allocateMemory(size_t new_capacity)
{
T* new_data = new T[new_capacity];
std::copy(data, data + std::min(size, new_capacity), new_data);
delete[] data;
data = new_data;
capacity = new_capacity;
}
template <typename T>
void MyVector<T>::resize(size_t new_size)
{
if (new_size < 0)
{
return;
}
if (new_size > 1.5 * capacity)
{
allocateMemory(new_size);
}
if (new_size > size)
{
T* new_data = new T[new_size];
memset(new_data, 0, new_size);
memcpy(new_data, data, size);
}
size = new_size;
}
template <typename T>
void MyVector<T>::resize(size_t new_size, T val)
{
if (new_size < 0)
{
return;
}
if (new_size > 1.5 * capacity)
{
allocateMemory(new_size);
}
if (new_size > size)
{
{
//也可以用std::fill实现
// std::fill(data + size, new_size - size, val);
}
{
T* new_data = new T[new_size];
memset(new_data, val, new_size);
memcpy(new_data, data, size);
}
}
size = new_size;
}
template<typename T>
bool MyVector<T>::operator== (const MyVector& vec) const
{
if (size != vec.Size())
{
return false;
}
for (size_t i = 0; i < size; i++)
{
if (data[i] != vec.data[i])
{
return false;
}
}
return true;
}
template<typename T>
MyVector<T>& MyVector<T>::operator=(const MyVector& vec)
{
if (this == &vec)
{
return *this;
}
delete[]data;
data = new T[vec.Capacity()];
capacity = vec.Capacity();
for (size_t i = 0; i < vec.Size(); i++)
{
data[i] = vec.data[i];
}
size = vec.Size();
return *this;
}
template<typename T>
T& MyVector<T>::operator[](size_t index)
{
if (index >= size)
{
return;
}
return data[index];
}
template<typename T>
void MyVector<T>::push_back(T val)
{
if (capacity == 0)
{
capacity = 10;
data = new T[capacity];
}
if (size + 1 >= capacity)
{
allocateMemory(capacity);
}
data[size] = val;
size++;
}
template<typename T>
T MyVector<T>::pop_back()
{
if (size == 0)
{
return;
}
return data[--size];
}
template<typename T>
void MyVector<T>::insert(iterator iter, T val)
{
size_t index = iter - data;
if (capacity == 0)
{
capacity = 10;
data = new T[capacity];
data[0] = val;
}
else if (size + 1 > capacity)
{
allocateMemory(capacity);
}
for (size_t i = size; i > index; i--)
{
data[i] = data[i - 1];
}
data[index] = val;
size++;
}
template<typename T>
void MyVector<T>::erase(iterator iter)
{
size_t index = iter - data;
for (size_t i = index; i < size - 1; ++i)
{
data[i] = data[i + 1];
}
--size;
}
template<typename T>
T& MyVector<T>::front()
{
return data[0];
}
template<typename T>
T& MyVector<T>::back()
{
return data[size - 1];
}
template<typename T>
T& MyVector<T>::at(size_t index) const
{
if (index >= size)
{
return;
}
return data[index];
}
template<typename T>
MyVector<T>::MyVector(size_t _size)
{
if (capacity == 0)
{
capacity = 1.5 * _size;
}
data = new T[_size];
size = _size;
}
template<typename T>
MyVector<T>::MyVector(size_t _size, T val)
{
if (capacity == 0)
{
capacity = 1.5 * _size;
}
data = new T[capacity];
for (size_t i = 0; i < _size; i++)
{
data[i] = val;
}
size = _size;
}
template<typename T>
MyVector<T>::MyVector(const MyVector& vec)
{
size = vec.size;
capacity = vec.capacity;
data = new T[capacity];
for (size_t i = 0; i < size; i++)
{
data[i] = vec.data[i];
}
}
4.迭代器原理
迭代器的作用类似于指针,用于访问容器内的所有元素,同时可以减少容器内部元素相关属性的暴露。一个简易的迭代器实现如下:
template <typename T>
class MyVector<T>::Iterator {
public:
// 构造函数
Iterator(T* data, size_t index) : data_(data), index_(index) {}
// 解引用操作符
T& operator*() {
if (index_ >= size_) {
throw std::out_of_range("Iterator out of range");
}
return data_[index_];
}
// 箭头操作符(可选)
T* operator->() {
return data_;
}
// 前置递增操作符
Iterator& operator++() {
if (index_ >= size_) {
throw std::out_of_range("Iterator out of range");
}
++index_;
return *this;
}
// 后置递增操作符(可选,但通常效率较低)
Iterator operator++(int) {
Iterator temp = *this;
++*this;
return temp;
}
// 比较操作符(用于循环和范围检查)
bool operator==(const Iterator& other) const {
return data_ == other.data_ && index_ == other.index_;
}
bool operator!=(const Iterator& other) const {
return !(*this == other);
}
private:
T* data_; // 指向Vector内部数据的指针
size_t index_; // 当前元素的索引
// 注意:这里我们没有存储size_,因为迭代器本身不知道整个容器的大小
// 但我们可以通过比较index_和容器的size_来检查是否越界
};
对于迭代器,还有其他操作符如+=、-=、+、-等都可以被重载,可以看到重载操作符的时候返回类型有的是iterator,有的是iterator&,这俩返回类型的区别问gpt的解释如下:
返回引用的情况
-
递增(
++
)和递减(--
)操作符:
如果是前缀运算符,即++iter这种,因为是先加后用,所以就应该返回迭代器本身,故这些操作符通常返回迭代器的引用(iterator&
),以便它们可以用于链式调用或作为左值。例如,++it
或--it
后,你通常希望it
保持为有效的迭代器对象,以便可以进一步使用它。如果是后缀运算符,即iter++这种,因为是先用后加,所以返回的是一个临时变量(副本),这个临时变量是一个右值,在这里只能被赋值给等式左边的左值,所以不需要返回迭代器自身的引用。 -
解引用操作符(
*
)和箭头操作符(->
):
解引用操作符(*it
)返回迭代器指向的元素的引用(T&
),而箭头操作符(it->member
)则返回指向元素的指针(T*
),它间接地提供了对元素成员的访问。虽然箭头操作符不直接返回迭代器的引用,但它确实通过返回元素的指针来模拟“引用”的行为。
不返回引用的情况
-
算术操作符(如
+
和-
):
对于支持随机访问的迭代器,算术操作符(如it + n
或it1 - it2
)通常不返回迭代器的引用,而是返回一个新的迭代器对象或迭代器之间的差值(对于减法)。这是因为算术操作通常会产生一个新的迭代器位置,而不是修改现有的迭代器。 -
比较操作符(如
==
、!=
、<
、<=
、>
、>=
):
比较操作符返回布尔值(bool
),而不是迭代器的引用。它们用于比较两个迭代器的位置或状态。
总结
- 递增/递减:返回迭代器的引用,以支持链式调用。
- 解引用/箭头:解引用返回元素的引用,箭头返回元素的指针(间接引用)。
- 算术/比较:返回新的迭代器对象(对于加法)、差值(对于减法)或布尔值(对于比较),而不是迭代器的引用。
这些规则有助于保持迭代器接口的清晰和一致,同时支持各种迭代模式(如前向迭代、双向迭代和随机访问迭代)。在设计自定义迭代器时,遵循这些模式是非常重要的,以便与C++标准库和其他遵循相同约定的代码无缝集成。
如有错误及疏漏请不吝赐教!