设计思路
使用 ::operator new 分配原始内存,然后通过 placement new 在指定地址构造对象,并显式调用析构函数来销毁对象。这与真实 STL 采用 std::allocator 的原理一致,但代码更直观。
真实 std::vector 的关键特性:
连续内存:元素存储在堆(heap)中,逻辑地址连续。
动态扩容 :当 size == capacity 时分配更大内存(通常 capacity *= 2)。
分离容量与大小 :size 表示实际元素个数,capacity 表示已分配内存能容纳的元素数。
只构造已使用的元素:未使用的内存保持未初始化状态,避免不必要的构造开销。
迭代器 :直接使用指针(T*)即可。
完整代码:
cpp
#include <iostream>
#include <algorithm> // for std::swap
#include <stdexcept> // for std::out_of_range
template <typename T>
class SimpleVector {
private:
T* data; // 指向动态数组的指针
size_t sz; // 当前元素个数
size_t cap; // 当前容量(可容纳元素数)
// 核心工具函数:分配新内存,将旧元素拷贝过去,释放旧内存
void reallocate(size_t new_cap) {
// 1. 分配原始内存(不构造对象)
T* new_data = static_cast<T*>(::operator new(new_cap * sizeof(T)));
// 2. 用拷贝构造将旧元素逐个放置到新内存
for (size_t i = 0; i < sz; ++i) {
new (new_data + i) T(data[i]); // placement new
}
// 3. 析构旧元素(释放对象资源)
for (size_t i = 0; i < sz; ++i) {
data[i].~T();
}
// 4. 释放旧内存
::operator delete(data);
// 5. 更新指针和容量
data = new_data;
cap = new_cap;
}
public:
// ---------- 构造函数 / 析构函数 ----------
SimpleVector() : data(nullptr), sz(0), cap(0) {}
// 拷贝构造
SimpleVector(const SimpleVector& other)
: data(nullptr), sz(other.sz), cap(other.sz) {
if (sz > 0) {
data = static_cast<T*>(::operator new(sz * sizeof(T)));
for (size_t i = 0; i < sz; ++i) {
new (data + i) T(other.data[i]);
}
}
}
// 拷贝赋值(copy-and-swap 惯用法)
SimpleVector& operator=(SimpleVector other) {
swap(*this, other); // 这里需要下面的 friend swap
return *this;
}
// 析构
~SimpleVector() {
clear(); // 析构所有元素
::operator delete(data); // 释放原始内存
}
// ---------- 容量相关 ----------
size_t size() const { return sz; }
size_t capacity() const { return cap; }
bool empty() const { return sz == 0; }
// 预分配容量(若新容量大于当前容量)
void reserve(size_t new_cap) {
if (new_cap > cap) {
reallocate(new_cap);
}
}
// 改变大小(若小于当前大小则截断,否则用 value 填充)
void resize(size_t new_size, const T& value = T()) {
if (new_size < sz) {
// 截断:析构多出的元素
for (size_t i = new_size; i < sz; ++i) {
data[i].~T();
}
sz = new_size;
} else if (new_size > sz) {
reserve(new_size); // 保证容量足够
for (size_t i = sz; i < new_size; ++i) {
new (data + i) T(value); // 构造新元素
}
sz = new_size;
}
}
// ---------- 元素访问 ----------
T& operator[](size_t index) { return data[index]; }
const T& operator[](size_t index) const { return data[index]; }
T& at(size_t index) {
if (index >= sz) throw std::out_of_range("index out of range");
return data[index];
}
const T& at(size_t index) const {
if (index >= sz) throw std::out_of_range("index out of range");
return data[index];
}
// ---------- 修改器 ----------
void push_back(const T& value) {
if (sz == cap) {
size_t new_cap = (cap == 0) ? 1 : cap * 2;
reallocate(new_cap);
}
new (data + sz) T(value); // 在末尾构造
++sz;
}
void pop_back() {
if (empty()) throw std::out_of_range("pop_back on empty vector");
--sz;
data[sz].~T(); // 析构最后一个元素
}
void clear() {
for (size_t i = 0; i < sz; ++i) {
data[i].~T();
}
sz = 0;
}
// ---------- 迭代器(直接使用指针) ----------
T* begin() { return data; }
T* end() { return data + sz; }
const T* begin() const { return data; }
const T* end() const { return data + sz; }
// ---------- 交换(用于拷贝赋值和移动) ----------
friend void swap(SimpleVector& a, SimpleVector& b) noexcept {
using std::swap;
swap(a.data, b.data);
swap(a.sz, b.sz);
swap(a.cap, b.cap);
}
};
关键点解析
1. 内存管理(堆区与逻辑地址)
::operator new 分配的是 原始内存 (未构造对象),返回 void*,我们强转为 T*。这块内存位于进程的 堆区(heap),与你之前学习的进程地址空间完全对应。
使用 placement new new (address) T(args) 在已分配的内存上构造对象,而不是重新申请内存。这避免了对未使用区域调用默认构造函数,提高了效率(对复杂类型尤其重要)。
析构时,我们 显式调用析构函数 data[i].~T() 来释放对象内部资源(如动态分配的成员),然后再用 ::operator delete 释放整个原始内存块。
2. 扩容策略(reallocate)
当 size == capacity 时,触发扩容。新容量通常翻倍(或从 1 开始),这是为了平摊 push_back 的时间复杂度到 O(1)。
reallocate 会:
分配新内存。将旧元素 拷贝构造 到新内存(如果类型支持移动,可进一步优化为移动构造)。析构并释放旧内存。更新指针和容量。
注意 :如果
T的拷贝构造函数可能抛出异常,上述reallocate不是异常安全的(旧数据可能丢失)。真实 STL 会先复制到新内存,成功后再释放旧内存。为简化,此处忽略异常安全,但你可以在此基础上改进。
3. 拷贝赋值(Copy-and-Swap)
operator= 接受参数为 值传递 ,这会在参数构造时调用拷贝构造或移动构造。然后通过 swap 交换当前对象和临时对象,临时对象析构时释放原来的资源。这种写法简洁且自动处理了自赋值和异常安全。
4. 迭代器与边界检查
begin() / end() 返回 T*,这正是连续内存的优势 ------ 指针就是天然迭代器。
operator[] 不检查边界,at() 会抛异常,与标准库行为一致。
下面是测试用例:
cpp
int main() {
SimpleVector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
std::cout << "size = " << v.size() << ", capacity = " << v.capacity() << '\n';
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << ' ';
}
std::cout << '\n';
v.pop_back();
std::cout << "after pop: size = " << v.size() << '\n';
v.resize(5, 0);
std::cout << "after resize(5,0): ";
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << ' ';
}
std::cout << '\n';
try {
std::cout << v.at(10) << '\n';
} catch (const std::out_of_range& e) {
std::cerr << "caught: " << e.what() << '\n';
}
return 0;
}
输出结果:
cpp
size = 3, capacity = 4
1 2 3
after pop: size = 2
after resize(5,0): 1 2 0 0 0
caught: index out of range