第二十六章 手写 STL 组件:从使用者到实现者

← 返回目录

本章目标:亲手实现 vector、string 的核心功能、 unique_ptr、以及一个简单的迭代器。这是检验"是否真懂 C++"的终极练习——理解标准库背后的一切。

26.1 为什么要手写标准库组件

  1. 理解 vector 扩容 / 迭代器 / RAII 的本质
  2. 面试经典题(手写 vector/unique_ptr)
  3. 需要特殊语义时能自己造轮子
  4. 学会读标准库代码(真大神都读源码)

注意:生产代码**永远用标准库**——你写的不会更好, 只会更慢/更多 bug。手写是学习手段,不是替代品。

26.2 手写 MyVector(完整实现见测试文件)

核心设计(和真实 vector 一致):

骨架:

template <typename T>
class MyVector {
public:
    MyVector() = default;
    MyVector(const MyVector& o) { copy_from(o); }
    MyVector(MyVector&& o) noexcept
        : begin_(o.begin_), end_(o.end_), capacity_(o.capacity_) {
        o.begin_ = o.end_ = o.capacity_ = nullptr;
    }
    MyVector& operator=(const MyVector& o) {
        if (this != &o) { clear(); copy_from(o); }
        return *this;
    }
    MyVector& operator=(MyVector&& o) noexcept {
        if (this != &o) {
            clear();                      // 释放旧资源
            begin_ = o.begin_; end_ = o.end_; capacity_ = o.capacity_;
            o.begin_ = o.end_ = o.capacity_ = nullptr;
        }
        return *this;
    }
    ~MyVector() { std::destroy(begin_, end_); delete[] reinterpret_cast<char*>(begin_); }
    void push_back(const T& v) {
        if (end_ == capacity_) grow();
        new (end_) T(v);                  // 定位 new:就地构造
        ++end_;
    }
    T& operator[](std::size_t i) { return begin_[i]; }
    std::size_t size() const { return end_ - begin_; }
    std::size_t capacity() const { return capacity_ - begin_; }
    T* begin() { return begin_; }         // 迭代器 = 裸指针
    T* end() { return end_; }
private:
    void grow() {
        std::size_t old_size = size();                // 先记旧 size(关键!)
        std::size_t new_cap = capacity() == 0 ? 4 : capacity() * 2;
        T* new_begin = reinterpret_cast<T*>(new char[new_cap * sizeof(T)]);
        for (std::size_t i = 0; i < old_size; ++i)
            new (new_begin + i) T(std::move(begin_[i]));  // 移动元素
        std::destroy(begin_, end_);                   // 析构旧元素
        delete[] reinterpret_cast<char*>(begin_);     // 释放旧内存
        begin_ = new_begin;
        end_ = new_begin + old_size;
        capacity_ = new_begin + new_cap;
    }
    void copy_from(const MyVector& o) {
        begin_ = reinterpret_cast<T*>(new char[o.size() * sizeof(T)]);
        for (std::size_t i = 0; i < o.size(); ++i)
            new (begin_ + i) T(o.begin_[i]);
        end_ = begin_ + o.size();
        capacity_ = end_;
    }
    void clear() {
        std::destroy(begin_, end_);
        delete[] reinterpret_cast<char*>(begin_);
        begin_ = end_ = capacity_ = nullptr;
    }
    T* begin_ = nullptr;
    T* end_ = nullptr;
    T* capacity_ = nullptr;
};

关键知识点:

  1. 定位 new:new (ptr) T(args)——在给定内存上构造,不分配
  2. std::destroy:批量调用析构(<memory>)
  3. 内存用 char 数组申请:因为 T 的构造/析构由我们手动控制
  4. 扩容必须 移动 + noexcept(否则拷贝,慢)
  5. 裸内存管理必须"成对":构造多少次,析构多少次

26.3 手写 MyUniquePtr

template <typename T>
class MyUniquePtr {
public:
    MyUniquePtr() = default;
    explicit MyUniquePtr(T* p) : ptr_(p) {}
    ~MyUniquePtr() { delete ptr_; }
    MyUniquePtr(const MyUniquePtr&) = delete;         // 禁止拷贝
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;
    MyUniquePtr(MyUniquePtr&& o) noexcept : ptr_(o.ptr_) {
        o.ptr_ = nullptr;                              // 转移所有权
    }
    MyUniquePtr& operator=(MyUniquePtr&& o) noexcept {
        if (this != &o) {
            delete ptr_;                               // 释放旧资源
            ptr_ = o.ptr_;
            o.ptr_ = nullptr;
        }
        return *this;
    }
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get() const { return ptr_; }
    explicit operator bool() const { return ptr_ != nullptr; }
    void reset(T* p = nullptr) { delete ptr_; ptr_ = p; }
private:
    T* ptr_ = nullptr;
};

要点:

unique_ptr<T[]> 特化。了解即可。

26.4 手写 MyString(核心思路)

string 的两个灵魂:SSO(短字符串优化)+ 按需堆分配。 简化版(无 SSO):

class MyString {
    char* data_ = nullptr;
    std::size_t size_ = 0;
public:
    MyString() = default;
    MyString(const char* s) { assign(s); }
    MyString(const MyString& o) { assign(o.c_str()); }
    ~MyString() { delete[] data_; }
    // 移动(noexcept!)
    MyString(MyString&& o) noexcept
        : data_(o.data_), size_(o.size_) {
        o.data_ = nullptr; o.size_ = 0;
    }
    void assign(const char* s) {
        delete[] data_;
        size_ = std::strlen(s);
        data_ = new char[size_ + 1];
        std::memcpy(data_, s, size_ + 1);
    }
    const char* c_str() const { return data_ ? data_ : ""; }
    std::size_t size() const { return size_; }
    char operator[](std::size_t i) const { return data_[i]; }
};

真实 std::string 在栈上放一个 16 字节的小缓冲区, 短字符串直接放里面(SSO),长字符串才堆分配。 这就是为什么短字符串操作那么快。

26.5 迭代器接口:让手写容器能用范围 for

只要提供 begin()/end() 返回"支持 ++ * !="的类型, 就能用范围 for 和大部分 STL 算法:

// MyVector 的迭代器就是 T*(begin/end 已实现)
MyVector<int> v;
for (int i = 0; i < 10; ++i) v.push_back(i * i);
for (int x : v) std::println("{}", x);          // 直接可用!
std::ranges::sort(v);                           // 算法也能用!

这就是"迭代器协议":STL 算法只要求迭代器满足特定操作, 不要求类型相同。鸭子类型思想在 C++ 的实现方式。

26.6 完整测试(测试文件)

测试文件会验证:

  1. MyVector:push_back 扩容正确、拷贝/移动语义、

迭代器遍历、和 std::ranges::sort 配合

  1. MyUniquePtr:独占性(编译期验证)、移动转移、RAII 释放
  2. MyString:构造/拷贝/移动/访问
  3. 内存分配计数:验证"构造次数 == 析构次数"(无泄漏)

26.7 陷阱清单(手写容器最容易犯的错)

  1. 忘了调用析构(只释放内存不析构元素 → 资源泄漏)
  2. 定位 new 构造了却忘了对应析构(double-free 或泄漏)
  3. 扩容时用拷贝而不是移动(慢;且必须 noexcept)
  4. 更新 begin_ 后再用旧的 size()/capacity()(顺序错)
  5. 移动后原对象没置空 → 双重释放
  6. 拷贝构造没做深拷贝 → 两个对象共享内存 → 双 delete
  7. 忘了 delete 拷贝操作(unique_ptr 变成"可拷贝"就错了)
  8. 边界:空容器、size=capacity 时 push_back

本章小结

练习题(配套测试:测试_第26章_手写容器.cpp)


  1. 完成 MyVector 并跑通全部测试。
  2. 给 MyVector 加 emplace_back(完美转发)。
  3. 给 MyString 加 SSO(16 字节内嵌缓冲区)。
  4. 给 MyUniquePtr 加 reset/release/swap。
  5. 用分配计数验证你的容器零泄漏。