浏览知识库目录

C++

手写 std::vector

从原始存储、几何扩容和异常回滚出发,实现支持复制移动、迭代器与主要修改操作的动态数组。

手写 std::vector

从原始存储、几何扩容和异常回滚出发,实现支持复制移动、迭代器与主要修改操作的动态数组。

本系列代码使用 C++20 和 oc::handmade 命名空间,目标是解释实现机制、复杂度和工程边界,不是替代标准库。普通容器与缓存核心不内置互斥锁;这不代表 lock-free。


一、学习目标

  • 实现 size/capacity 双边界
  • 理解几何扩容带来的摊还 O(1)
  • 处理自引用插入与搬迁异常

二、前置条件

掌握 allocator_traits、移动语义和强异常保证。

Linux/macOS:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure

Windows PowerShell:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --config Debug
ctest --test-dir build -C Debug --output-on-failure

三、问题与设计选择

保存 data_、size_、capacity_ 和 allocator。扩容先在新缓冲区构造全部对象,成功后一次提交;优先使用 nothrow move,否则复制。

这里刻意保留一条边界:教学实现覆盖构造、复制移动、核心修改、查找和迭代契约,但不复刻标准库全部重载、ABI、constexpr、异构查找或节点句柄。


四、内存布局与核心不变量

data_ 要么为空,要么指向 capacity 个 T 的原始存储;其中前 size 个对象已经构造。

每个修改操作都按“准备资源 → 构造新状态 → 提交连接或指针 → 清理旧状态”的顺序设计。提交点之前发生异常,应保持原对象可继续使用;无法提供强保证时,会在接口说明中明确基本保证。


五、核心实现

void grow_for_one() {
    const size_type next = capacity_ ? capacity_ * 2 : 1;
    pointer fresh = traits::allocate(alloc_, next);
    size_type built = 0;
    try {
        for (; built < size_; ++built)
            traits::construct(alloc_, fresh + built,
                std::move_if_noexcept(data_[built]));
    } catch (...) {
        while (built) traits::destroy(alloc_, fresh + --built);
        traits::deallocate(alloc_, fresh, next);
        throw;
    }
    destroy_and_deallocate();
    data_ = fresh;
    capacity_ = next;
    size_ = built;
}

上面先聚焦最容易写错的核心步骤;若本篇对应一个独立组件,下一节给出统一工程中的完整教学实现。代码没有放入 std 命名空间,避免未定义行为和名称冲突。


六、完整教学实现

下面是统一工程中经过 GCC、Clang、GoogleTest 和 Sanitizer 验证的完整组件。它依赖前序文章已经实现的公共类型以及头文件中的标准库 #include

namespace oc::handmade {

template<class T, class Allocator = std::allocator<T>>
class vector {
    using traits = std::allocator_traits<Allocator>;

    Allocator allocator_{};
    T* data_{};
    std::size_t size_{};
    std::size_t capacity_{};

    void destroy_elements() noexcept {
        while (size_ != 0) traits::destroy(allocator_, data_ + --size_);
    }

    void release() noexcept {
        destroy_elements();
        if (data_) traits::deallocate(allocator_, data_, capacity_);
        data_ = nullptr;
        capacity_ = 0;
    }

    bool refers_to_storage(const T* value) const noexcept {
        return data_ && value >= data_ && value < data_ + size_;
    }

public:
    using value_type = T;
    using size_type = std::size_t;
    using iterator = T*;
    using const_iterator = const T*;

    vector() = default;
    explicit vector(const Allocator& allocator) : allocator_(allocator) {}

    vector(const vector& other)
        : allocator_(traits::select_on_container_copy_construction(other.allocator_)) {
        reserve(other.size_);
        try {
            for (const T& item : other) emplace_back(item);
        } catch (...) {
            release();
            throw;
        }
    }

    vector(vector&& other) noexcept
        : allocator_(std::move(other.allocator_)),
          data_(std::exchange(other.data_, nullptr)),
          size_(std::exchange(other.size_, 0)),
          capacity_(std::exchange(other.capacity_, 0)) {}

    vector& operator=(const vector& other) {
        if (this == &other) return *this;
        vector copy(other);
        swap(copy);
        return *this;
    }

    vector& operator=(vector&& other) noexcept {
        if (this == &other) return *this;
        release();
        allocator_ = std::move(other.allocator_);
        data_ = std::exchange(other.data_, nullptr);
        size_ = std::exchange(other.size_, 0);
        capacity_ = std::exchange(other.capacity_, 0);
        return *this;
    }

    ~vector() { release(); }

    void swap(vector& other) noexcept {
        using std::swap;
        swap(allocator_, other.allocator_);
        swap(data_, other.data_);
        swap(size_, other.size_);
        swap(capacity_, other.capacity_);
    }

    iterator begin() noexcept { return data_; }
    const_iterator begin() const noexcept { return data_; }
    iterator end() noexcept { return data_ ? data_ + size_ : nullptr; }
    const_iterator end() const noexcept { return data_ ? data_ + size_ : nullptr; }
    T* data() noexcept { return data_; }
    const T* data() const noexcept { return data_; }
    bool empty() const noexcept { return size_ == 0; }
    size_type size() const noexcept { return size_; }
    size_type capacity() const noexcept { return capacity_; }
    T& operator[](size_type index) noexcept { return data_[index]; }
    const T& operator[](size_type index) const noexcept { return data_[index]; }
    T& at(size_type index) {
        if (index >= size_) throw std::out_of_range("oc::handmade::vector::at");
        return data_[index];
    }
    const T& at(size_type index) const {
        if (index >= size_) throw std::out_of_range("oc::handmade::vector::at");
        return data_[index];
    }
    T& front() noexcept { return data_[0]; }
    const T& front() const noexcept { return data_[0]; }
    T& back() noexcept { return data_[size_ - 1]; }
    const T& back() const noexcept { return data_[size_ - 1]; }

    void reserve(size_type requested) {
        if (requested <= capacity_) return;
        T* fresh = traits::allocate(allocator_, requested);
        size_type built = 0;
        try {
            for (; built < size_; ++built) {
                traits::construct(
                    allocator_,
                    fresh + built,
                    std::move_if_noexcept(data_[built])
                );
            }
        } catch (...) {
            while (built) traits::destroy(allocator_, fresh + --built);
            traits::deallocate(allocator_, fresh, requested);
            throw;
        }
        const size_type old_size = size_;
        destroy_elements();
        if (data_) traits::deallocate(allocator_, data_, capacity_);
        data_ = fresh;
        size_ = old_size;
        capacity_ = requested;
    }

    template<class... Args>
    T& emplace_back(Args&&... args) {
        if (size_ == capacity_) {
            T temporary(std::forward<Args>(args)...);
            reserve(capacity_ == 0 ? 1 : capacity_ * 2);
            traits::construct(allocator_, data_ + size_, std::move(temporary));
        } else {
            traits::construct(
                allocator_,
                data_ + size_,
                std::forward<Args>(args)...
            );
        }
        return data_[size_++];
    }

    void push_back(const T& value) {
        if (size_ == capacity_ && refers_to_storage(std::addressof(value))) {
            T copy(value);
            emplace_back(std::move(copy));
        } else {
            emplace_back(value);
        }
    }

    void push_back(T&& value) {
        if (size_ == capacity_ && refers_to_storage(std::addressof(value))) {
            T temporary(std::move(value));
            emplace_back(std::move(temporary));
        } else {
            emplace_back(std::move(value));
        }
    }

    void pop_back() noexcept {
        assert(size_ != 0);
        traits::destroy(allocator_, data_ + --size_);
    }

    iterator erase(iterator position) {
        assert(position >= begin() && position < end());
        for (iterator current = position; current + 1 != end(); ++current)
            *current = std::move(*(current + 1));
        pop_back();
        return position;
    }

    void clear() noexcept { destroy_elements(); }
};

}  // namespace oc::handmade

生产级标准库还要处理完整 allocator 传播、全部重载、ABI、调试迭代器和平台特化;这里保留的是能够独立推导核心数据结构的教学边界。


七、使用示例与输出

预期输出或状态:

依次插入 1、2、3 时容量示例为 1、2、4;元素顺序保持 1 2 3。

示例必须在文章对应的测试目标中实际编译。涉及顺序的输出只依赖接口明确承诺的顺序;无序容器不会把某次桶顺序写成稳定结果。


八、复杂度与失效规则

操作 复杂度 说明
operator[]/back O(1) 不检查边界
push_back 摊还 O(1) 扩容时 O(n)
insert/erase O(n) 移动尾部
reserve O(n) 所有迭代器失效

复杂度中的 O(1) 若标记为“平均”或“摊还”,不能在面试中省略限定词。任何重新分配、节点删除、rehash 或缓存淘汰都必须单独说明迭代器、引用与指针是否失效。


九、异常安全与资源管理

  • 获取资源后立即交给 RAII 对象或明确记录已构造数量。
  • 用户类型构造、复制、移动、比较器和哈希器都可能抛异常。
  • 只有在所有后续步骤不会失败时才修改不可回滚的链接。
  • 析构、释放和关闭路径不得抛异常。
  • 并发包装通过回调在锁内访问,避免返回保护对象的裸引用。

十、常见错误

1. 扩容成功前就覆盖旧指针

扩容成功前就覆盖旧指针会破坏本篇建立的契约。调试时先检查核心不变量,再缩小到触发该状态的最短操作序列。

2. 传入 v[0] 再扩容导致参数引用悬空

传入 v[0] 再扩容导致参数引用悬空会破坏本篇建立的契约。调试时先检查核心不变量,再缩小到触发该状态的最短操作序列。

3. 用 memcpy 搬运任意 T

用 memcpy 搬运任意 T会破坏本篇建立的契约。调试时先检查核心不变量,再缩小到触发该状态的最短操作序列。


十一、面试追问

  1. 为什么扩容倍数通常大于 1?
  2. push_back 在什么情况下不能提供强保证?
  3. reserveresize 的区别是什么?

回答时先说数据结构不变量,再给复杂度,最后说明异常、迭代器或并发边界,通常比背诵结论更有说服力。


十二、练习与自测

  1. 实现 emplace_back
  2. 处理自引用 push_back
  3. 统计不同增长因子的搬迁次数

自测标准:能够不看代码画出内存或节点关系,解释一次成功操作和一次失败回滚,并写出至少一个会击穿错误实现的测试。


十三、官方资料与延伸阅读


上一篇:手写 std::array | 下一篇:手写 std::string