作为一名长期使用C++进行开发的程序员,vector是我日常工作中最常用的容器之一。它就像是C++标准库中的瑞士军刀,简单却功能强大。vector本质上是一个动态数组,能够自动管理内存,让我们从繁琐的内存分配和释放中解放出来。
在大多数情况下,当我们需要一个能够动态改变大小的数组时,vector都是首选。它提供了随机访问的能力,支持在末尾高效地添加和删除元素,并且能够自动处理内存的分配和释放。不过,要真正用好vector,我们需要深入了解它的内部实现和使用细节。
提示:虽然vector使用起来很方便,但不恰当的使用方式可能导致性能问题甚至程序崩溃。理解其工作原理是高效使用的前提。
在C++中,vector是一个模板类,这意味着我们需要在使用时指定它存储的元素类型。当我们使用vector的迭代器时,必须完整地声明迭代器类型:
cpp复制vector<int>::iterator it; // 正确声明一个int类型vector的迭代器
这种看似繁琐的声明方式其实体现了C++类型安全的特性。模板迭代器必须与容器类型严格匹配,这有助于编译器在编译期发现类型错误。
vector最神奇的特性莫过于它的自动扩容能力。在VS环境下,vector通常以1.5倍的策略进行扩容:
cpp复制vector<int> v;
for(int i=0; i<100; ++i) {
v.push_back(i);
cout << "Size: " << v.size() << " Capacity: " << v.capacity() << endl;
}
运行这段代码,你会看到capacity的变化遵循1.5倍的规律:1, 2, 3, 4, 6, 9, 13, 19...这种增长策略在空间利用和性能之间取得了良好的平衡。
注意:不同编译器的扩容策略可能不同。g++通常使用2倍扩容策略,这也是为什么了解你的开发环境很重要。
很多初学者容易混淆reserve和resize这两个成员函数:
reserve(size_t n): 确保vector至少有n的容量,但不会改变sizeresize(size_t n, value_type val = value_type()): 改变vector的size,必要时会扩容,并用val填充新增元素cpp复制vector<int> v1;
v1.reserve(10); // capacity >= 10, size = 0
vector<int> v2;
v2.resize(10); // size = 10, capacity >= 10, 新增元素初始化为0
特别要注意的是,内置类型如int也有默认构造函数int(),其默认值为0。这在模板编程中尤为重要。
vector内部通常由三个指针(或迭代器)组成:
_begin: 指向数据块的起始位置_finish: 指向最后一个有效元素的下一个位置_end_of_storage: 指向分配的内存块的末尾这种设计使得vector能够高效地管理其内存和元素。size()就是_finish - _begin,而capacity()则是_end_of_storage - _begin。
vector<vector
cpp复制vector<vector<int>> matrix(3, vector<int>(4)); // 3行4列的矩阵
// 填充矩阵
for(int i=0; i<3; ++i) {
for(int j=0; j<4; ++j) {
matrix[i][j] = i * 4 + j;
}
}
编译器会为这种嵌套vector生成专门的模板实例化代码。理解这一点对于调试和性能优化很有帮助。
虽然vector
因此,在处理字符串时,应该优先使用string而不是vector
让我们从vector的基本框架开始:
cpp复制template<class T>
class Vector {
public:
typedef T* iterator;
typedef const T* const_iterator;
// 构造函数和析构函数
Vector();
~Vector();
// 容量相关
size_t size() const { return _finish - _start; }
size_t capacity() const { return _end_of_storage - _start; }
// 迭代器
iterator begin() { return _start; }
iterator end() { return _finish; }
// 元素访问
T& operator[](size_t pos) { return _start[pos]; }
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
这个框架展示了vector最基本的组成部分。注意我们使用了指针作为迭代器类型,这与标准库的实现方式类似。
reserve是vector内存管理的核心函数:
cpp复制void reserve(size_t n) {
if(n > capacity()) {
size_t old_size = size();
T* tmp = new T[n];
// 不能用memcpy,必须逐个构造
for(size_t i=0; i<old_size; ++i) {
tmp[i] = _start[i]; // 调用T的赋值运算符
}
delete[] _start;
_start = tmp;
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
push_back的实现则依赖于reserve:
cpp复制void push_back(const T& val) {
if(_finish == _end_of_storage) {
reserve(capacity() == 0 ? 4 : capacity() * 1.5);
}
*_finish = val;
++_finish;
}
重要提示:在实现reserve时,绝对不能使用memcpy,因为对于非平凡类型,这会导致浅拷贝问题。必须逐个元素进行拷贝构造或赋值。
vector需要提供多种构造函数以满足不同使用场景:
cpp复制// 默认构造
Vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
// 迭代器范围构造
template<class InputIterator>
Vector(InputIterator first, InputIterator last) {
while(first != last) {
push_back(*first);
++first;
}
}
// 拷贝构造
Vector(const Vector<T>& v) {
reserve(v.capacity());
for(const auto& e : v) {
push_back(e);
}
}
迭代器范围构造是一个模板成员函数,这使得vector可以从任何兼容的迭代器范围构造,包括其他容器的迭代器。
resize需要处理多种情况:
cpp复制void resize(size_t n, const T& val = T()) {
if(n < size()) {
_finish = _start + n; // 缩小size
} else if(n > size()) {
reserve(n); // 确保有足够容量
while(_finish != _start + n) {
*_finish = val; // 填充新元素
++_finish;
}
}
}
注意默认参数T(),它会调用T类型的默认构造函数。对于内置类型,这会产生0值。
vector的某些操作会导致迭代器失效,这是一个常见但容易被忽视的问题:
cpp复制vector<int> v = {1,2,3,4};
auto it = v.begin() + 2;
v.insert(v.begin(), 0); // 可能导致扩容
// 此时it可能失效!
在insert实现中,我们必须考虑扩容后迭代器失效的问题:
cpp复制iterator insert(iterator pos, const T& val) {
if(_finish == _end_of_storage) {
size_t len = pos - _start; // 保存相对位置
reserve(capacity() == 0 ? 4 : capacity() * 1.5);
pos = _start + len; // 更新pos
}
iterator end = _finish;
while(end > pos) {
*end = *(end-1);
--end;
}
*pos = val;
++_finish;
return pos;
}
正确的做法是返回新的迭代器,让调用者更新他们的迭代器引用。
在模板编程中,有时需要使用typename来告诉编译器某个名称表示类型:
cpp复制template<class T>
void print(const Vector<T>& v) {
typename Vector<T>::const_iterator it = v.begin();
// ...
}
这里必须使用typename,因为编译器无法确定const_iterator是类型还是静态成员。
赋值运算符需要特别注意避免自赋值和保证异常安全:
cpp复制Vector<T>& operator=(Vector<T> v) {
swap(v); // 利用拷贝构造和swap实现异常安全
return *this;
}
void swap(Vector<T>& v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
这种实现方式利用了拷贝构造函数和swap,既保证了异常安全,又处理了自赋值情况。
频繁扩容是vector性能的主要瓶颈之一。如果事先知道大致需要的元素数量,应该使用reserve:
cpp复制vector<int> v;
v.reserve(1000); // 避免插入过程中的多次扩容
for(int i=0; i<1000; ++i) {
v.push_back(i);
}
vector存储大型对象时,考虑存储指针或使用移动语义:
cpp复制vector<LargeObject> v1; // 可能效率不高
vector<unique_ptr<LargeObject>> v2; // 更高效
vector在中间位置插入或删除元素的效率是O(n),如果需要频繁这类操作,考虑使用list或deque。
C++11引入的emplace_back可以避免临时对象的构造和析构:
cpp复制vector<pair<int, string>> v;
v.emplace_back(1, "test"); // 直接在容器内构造
在实际开发中,我通常会优先考虑vector,除非有明确的理由需要使用其他容器。vector的连续内存特性使得它在现代CPU架构上表现优异,特别是对于遍历操作。