从零手写一个面试级 C++ vector:内存模型、拷贝语义与扩容策略全解析

一、为什么我要手写 vector

在 C++ 学习过程中,std::vector 是我们使用频率最高的容器之一。 但**"会用"** 和 "懂它怎么实现",在面试中是两回事。

很多人背过这些结论:

  • vector 底层是连续内存

  • 支持随机访问

  • 扩容一般是 2 倍

  • 插入可能导致迭代器失效

但如果面试官继续追问一句:

"你自己能不能实现一个 vector?"

真正的差距就出现了。

我决定从零开始,完整实现一个具备面试价值的 vector,目标不是复刻 STL 的所有细节,而是做到:

  • 内存模型清晰

  • 拷贝语义正确

  • 接口行为符合 STL 直觉

  • 能在面试中经得起追问


二、整体设计思路

1. vector 的核心结构

标准库中的 vector 本质上只维护了三根指针:

cpp 复制代码
[start, finish)        已使用空间
[start, end_of_storage) 已申请空间

因此我们的 vector 内部结构非常清晰:

cpp 复制代码
T* _start;           // 指向起始位置
T* _finish;          // 指向最后一个元素的下一个
T* _end_of_storage;  // 指向容量末尾

它们之间的关系决定了:

  • size() = _finish - _start

  • capacity() = _end_of_storage - _start

这也是 vector 高效的根本原因。

cpp 复制代码
vector()
    : _start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{}

2. 为什么使用指针而不是下标

使用裸指针有两个优势:

  1. 和 STL 行为一致 : vector 的 iterator 本质就是 T*

  2. 指针运算天然高效: 无需额外封装,插入/删除逻辑清晰

这也是为什么我们定义:

cpp 复制代码
typedef T* iterator;
typedef const T* const_iterator;

三、构造函数与析构:资源管理的第一步

1. 默认构造函数

一个合格的 vector 必须支持:

cpp 复制代码
vector<int> v;
v.push_back(1);

因此默认构造函数必须让指针处于空但安全的状态:

cpp 复制代码
vector()
    : _start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{}

这是后续所有操作的基础。


2. 带参构造函数

构造一个大小为 n、元素初值为 val 的 vector:

cpp 复制代码
vector(size_t n, const T& val = T())
{
    _start = new T[n];
    _finish = _start + n;
    _end_of_storage = _start + n;

    for (size_t i = 0; i < n; ++i)
        _start[i] = val;
}

这里有两个重要点:

  • 容量 = 大小(不做多余扩容)

  • 使用赋值而不是 memcpy,保证对非 POD 类型安全


3. 析构函数

vector 管理的是一整块动态数组,因此析构只需一件事:

cpp 复制代码
~vector()
{
    delete[] _start;
}

这也是 RAII 思想的体现: 资源的申请和释放必须成对出现。


四、拷贝语义:最容易出错的地方

1. 拷贝构造函数(深拷贝)

vector 绝对不能浅拷贝,否则两个对象会共享同一块内存。

正确的做法是:

cpp 复制代码
vector(const vector& x)
{
    size_t n = x.size();
    _start = new T[n];
    for (size_t i = 0; i < n; ++i)
        _start[i] = x._start[i];

    _finish = _start + n;
    _end_of_storage = _finish;
}

这样每个 vector 都拥有独立的内存资源。


2. 赋值运算符:copy-swap 惯用法

赋值是拷贝构造的升级版,推荐使用 copy-swap

cpp 复制代码
vector& operator=(const vector& x)
{
    vector<T> tmp(x);
    swap(tmp);
    return *this;
}

它的好处是:

  • 自动处理自赋值

  • 异常安全

  • 代码简洁、可复用


3. swap 的正确实现

cpp 复制代码
void swap(vector& x)
{
    std::swap(_start, x._start);
    std::swap(_finish, x._finish);
    std::swap(_end_of_storage, x._end_of_storage);
}

注意: swap 一定是"交换",不是覆盖,这是很多初学者会犯的错误。


五、容量管理:reserve 与 resize

1. reserve:只扩容,不改 size

cpp 复制代码
void reserve(size_t n)
{
    if (n > capacity())
    {
        size_t old_size = size();
        T* tmp = new T[n];
        for (size_t i = 0; i < old_size; ++i)
            tmp[i] = _start[i];

        delete[] _start;
        _start = tmp;
        _finish = _start + old_size;
        _end_of_storage = _start + n;
    }
}

关键点:

  • 只在 n > capacity() 时才扩容

  • 扩容后 size 不变

  • 所有旧 iterator 失效(与 STL 一致)


2. resize:改变 size

cpp 复制代码
void resize(size_t n, T val = T())
{
    if (n > capacity())
        reserve(n);

    if (n > size())
    {
        for (size_t i = size(); i < n; ++i)
            _start[i] = val;
    }

    _finish = _start + n;
}

resize 的行为可以总结为:

  • 变小:逻辑删除

  • 变大:补充新元素


六、元素访问接口

cpp 复制代码
T& front();
T& back();
T& operator[](size_t i);

以及对应的 const 版本:

cpp 复制代码
const T& front() const;
const T& back() const;
const T& operator[](size_t i) const;

这些接口与 STL 行为保持一致:

  • 不做越界检查

  • 由调用者保证前置条件(非空 / 合法下标)


七、push_back 与扩容策略

cpp 复制代码
void push_back(const T& x)
{
    if (_finish == _end_of_storage)
    {
        size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
        reserve(new_cap);
    }

    _start[size()] = x;
    ++_finish;
}

这里体现了 vector 的经典扩容策略:

  • 初始容量:0 → 1

  • 后续扩容:2 倍

这样可以保证 均摊 O(1) 的插入复杂度。


八、insert 与 erase:最考验指针理解的地方

1. insert

cpp 复制代码
iterator insert(iterator pos, const T& val)
{
    if (_finish == _end_of_storage)
    {
        size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
        reserve(new_cap);
    }

    size_t idx = pos - _start;
    for (size_t i = size(); i > idx; --i)
        _start[i] = _start[i - 1];

    _start[idx] = val;
    ++_finish;
    return _start + idx;
}

插入的核心是:

  • 从后往前移动元素

  • 保证不覆盖数据

  • 返回插入位置的 iterator


2. erase

cpp 复制代码
iterator erase(iterator pos)
{
    for (size_t i = pos - _start + 1; i < size(); ++i)
        _start[i - 1] = _start[i];

    --_finish;
    return _start + (pos - _start);
}

删除的本质是:

  • 覆盖删除位置

  • size 减 1

  • 返回删除后的位置


九、迭代器失效规则(面试高频)

通过实现可以清楚得出:

  • reserve所有 iterator 失效

  • insert:插入点及之后 iterator 失效

  • erase:删除点及之后 iterator 失效

这正是 STL 文档中的描述。


十、总结

通过完整实现一个 vector,我最大的收获不是代码本身,而是:

  • 理解了 连续内存模型

  • 真正掌握了 拷贝语义

  • 明白了 为什么 vector 插入慢、访问快

  • 面试时不再害怕被追问 STL 底层

当你能自己写出 vector 时, STL 不再是黑盒,而是朋友。


十一、完整代码

cpp 复制代码
#pragma once
#include<iostream>

namespace Vector
{
    template<class T>
    class vector
    {
    public:
        typedef T* iterator;
        typedef const T* const_iterator;

        vector()
            : _start(nullptr)
            , _finish(nullptr)
            , _end_of_storage(nullptr)
        {
        }

        vector(size_t n, const T& val = T())
        {
            _start = new T[n];
            _finish = _start + n;
            _end_of_storage = _start + n;

            for (size_t i = 0; i < n; ++i)
                _start[i] = val;
        }


        vector(const vector& x)
        {
            size_t n = x.size();
            _start = new T[n];
            for (size_t i = 0; i < n; ++i)
                _start[i] = x._start[i];

            _finish = _start + n;
            _end_of_storage = _finish;
        }

        ~vector()
        {
            delete[] _start;
        }

        vector& operator= (const vector& x)
        {
            vector<T> tmp(x);
            swap(tmp);
            return *this;
        }

        size_t size() const { return _finish - _start; }
        size_t capacity() const { return _end_of_storage - _start; }

        iterator begin() { return _start; }
        iterator end() { return _finish; }

        const_iterator begin() const { return _start; }
        const_iterator end() const { return _finish; }

        void swap(vector& x)
        {
            std::swap(_start, x._start);
            std::swap(_finish, x._finish);
            std::swap(_end_of_storage, x._end_of_storage);
        }

        bool empty() const
        {
            return _finish == _start;
        }

        void reserve(size_t n)
        {
            if (n > capacity())
            {
                size_t old_size = size();
                T* tmp = new T[n];
                for (size_t i = 0; i < old_size; ++i)
                    tmp[i] = _start[i];

                delete[] _start;
                _start = tmp;

                _finish = _start + old_size;
                _end_of_storage = _start + n;
            }
        }

        void resize(size_t n, T val = T())
        {
            if (n > capacity())
                reserve(n);

            if (n > size())
            {
                for (size_t i = size(); i < n; ++i)
                    _start[i] = val;
            }

            _finish = _start + n;

        }

        T& front() { return _start[0]; }

        T& back() { return _start[size() - 1]; }

        const T& front() const { return _start[0]; }

        const T& back() const { return _start[size() - 1]; }

        void push_back(const T& x)
        {
            if (_finish == _end_of_storage)
            {
                size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
                reserve(new_cap);
            }

            _start[size()] = x;
            _finish++;
        }

        T& operator[](size_t i) { return *(_start + i); }

        const T& operator[](size_t i) const { return *(_start + i); }

        iterator insert(iterator pos, const T& val)
        {
            if (_finish == _end_of_storage)
            {
                size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
                reserve(new_cap);
            }

            size_t idx = pos - _start;
            for (size_t i = size(); i > idx; --i)
                _start[i] = _start[i - 1];

            _start[pos - _start] = val;
            _finish++;

            return _start + (pos - _start);
        }

        void pop_back()
        {
            _finish--;
        }

        iterator erase(iterator pos)
        {
            for (size_t i = pos - _start + 1; i < size(); ++i)
                _start[i - 1] = _start[i];

            _finish--;

            return _start + (pos - _start);
        }

        iterator erase(iterator first, iterator last)
        {
            size_t len = last - first;
            for (size_t i = last - _start; i < size(); ++i)
                _start[i - len] = _start[i];
            _finish -= len;

            return first;
        }

    private:
        iterator _start = nullptr;
        iterator _finish = nullptr;
        iterator _end_of_storage = nullptr;
    };
}
相关推荐
OopspoO2 小时前
C++杂记——构造函数
c++
AlenTech2 小时前
152. 乘积最大子数组 - 力扣(LeetCode)
算法·leetcode·职场和发展
a程序小傲2 小时前
中国邮政Java面试被问:Netty的FastThreadLocal优化原理
java·服务器·开发语言·面试·职场和发展·github·哈希算法
淦。。。。2 小时前
题解:P14013 [POCamp 2023] 送钱 / The Generous Traveler
开发语言·c++·经验分享·学习·其他·娱乐·新浪微博
天赐学c语言2 小时前
1.18 - 滑动窗口最大值 && 子类的指针转换为父类的指针,指针的值是否会改变
数据结构·c++·算法·leecode
是娇娇公主~2 小时前
C++集群聊天服务器(3)—— 项目数据库以及表的设计
服务器·数据库·c++
zephyr053 小时前
C++ STL unordered_set 与 unordered_map 完全指南
开发语言·数据结构·c++
大锦终3 小时前
dfs解决FloodFill 算法
c++·算法·深度优先
一只小bit3 小时前
Qt 事件:覆盖介绍、处理、各种类型及运用全详解
前端·c++·qt·cpp