C++面试八股-STL(vector)

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的解释如下:

返回引用的情况

  1. 递增(++)和递减(--)操作符
    如果是前缀运算符,即++iter这种,因为是先加后用,所以就应该返回迭代器本身,故这些操作符通常返回迭代器的引用(iterator&),以便它们可以用于链式调用或作为左值。例如,++it 或 --it 后,你通常希望 it 保持为有效的迭代器对象,以便可以进一步使用它。如果是后缀运算符,即iter++这种,因为是先用后加,所以返回的是一个临时变量(副本),这个临时变量是一个右值,在这里只能被赋值给等式左边的左值,所以不需要返回迭代器自身的引用。

  2. 解引用操作符(*)和箭头操作符(->
    解引用操作符(*it)返回迭代器指向的元素的引用(T&),而箭头操作符(it->member)则返回指向元素的指针(T*),它间接地提供了对元素成员的访问。虽然箭头操作符不直接返回迭代器的引用,但它确实通过返回元素的指针来模拟“引用”的行为。

不返回引用的情况

  1. 算术操作符(如 + 和 -
    对于支持随机访问的迭代器,算术操作符(如 it + n 或 it1 - it2)通常不返回迭代器的引用,而是返回一个新的迭代器对象或迭代器之间的差值(对于减法)。这是因为算术操作通常会产生一个新的迭代器位置,而不是修改现有的迭代器。

  2. 比较操作符(如 ==!=<<=>>=
    比较操作符返回布尔值(bool),而不是迭代器的引用。它们用于比较两个迭代器的位置或状态。

总结

  • 递增/递减:返回迭代器的引用,以支持链式调用。
  • 解引用/箭头:解引用返回元素的引用,箭头返回元素的指针(间接引用)。
  • 算术/比较:返回新的迭代器对象(对于加法)、差值(对于减法)或布尔值(对于比较),而不是迭代器的引用。

这些规则有助于保持迭代器接口的清晰和一致,同时支持各种迭代模式(如前向迭代、双向迭代和随机访问迭代)。在设计自定义迭代器时,遵循这些模式是非常重要的,以便与C++标准库和其他遵循相同约定的代码无缝集成。

如有错误及疏漏请不吝赐教!

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值