news 2026/10/20 13:46:04

C++实现STL容器的示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++实现STL容器的示例

前言


「自己实现一个 STL 容器」是理解 C++ 对象模型和内存管理的最好练习。很多人以为std::vector就是一个会自动扩容的数组,照着写一个new T[n]、delete[]的包装类就完事。真正动手后才会发现:容器必须区分「已分配的内存」和「已构造的对象」,必须在扩容时只搬移有效元素,必须在拷贝构造抛异常时把半成品清理干净,还必须保证移动后的源对象处于可析构状态。


标准库把这套逻辑拆成三层:分配器(allocator)负责申请和释放原始内存,std::allocator_traits负责构造和销毁对象,容器负责维护大小、容量和迭代器。理解这三层分工,比记住vector有多少个成员函数更重要。


本文实现一个简化版的MiniVector<T>,支持默认构造、拷贝、移动、push_back、emplace_back、reserve、resize、at、迭代器遍历。它不追求与std::vector完全兼容,而是把关键机制暴露出来。代码基准 C++17。


一、容器为什么不能直接用 new[]


假设要写一个动态数组,最直觉的做法是:


// 适用于 C++17(错误示范)
template <class T>
class BadArray {
public:
explicit BadArray(std::size_t n) : data_(new T[n]), size_(n) {}
~BadArray() { delete[] data_; }
private:
T* data_;
std::size_t size_;
};

这段代码有三个根本问题:



  1. new T[n]会立即构造 n 个T对象。std::vector的容量可以大于元素个数,多出来的位置必须是「未构造的原始内存」,不能提前构造。对没有默认构造函数的类型,new T[n]直接编译失败。

  2. 无法支持「先分配容量、再逐个构造」的扩容策略。push_back需要在未构造的槽位上用拷贝或移动构造,而不是赋值。

  3. new[]与delete[]配对,但容器的容量和元素个数是分开的,delete[]只能释放整个数组,无法表达「释放 capacity 个槽位」这件事。


因此 STL 容器使用分配器的四个动作:




动作作用对应函数



分配原始内存得到未构造的存储allocate(n)

构造对象在指定地址开始对象生命周期construct(p, args...)

销毁对象结束生命周期,调用析构函数destroy(p)

释放内存归还存储deallocate(p, n)



std::allocator_traits<Allocator>为这些操作提供统一接口:即使分配器没有直接定义construct,traits 也会给出默认实现(通常是 placement new)。这就是容器能适配自定义分配器的原因。


二、大小、容量与迭代器失效


容器要维护三个量:



  • data_:指向分配到的存储首地址;

  • size_:已经构造的元素个数;

  • cap_:存储能容纳的元素个数。


关系始终是size_ <= cap_。[data_, data_ + size_)是有效元素,[data_ + size_, data_ + cap_)是未构造的原始内存,不能解引用。


reserve(n)把容量提升到至少 n,不改变元素个数;resize(n)改变元素个数,必要时扩容并构造新元素。push_back在size_ == cap_时扩容,常见策略是容量翻倍。标准不规定扩容因子,std::vector的具体增长策略因实现而异,本文用翻倍只是示意。


扩容会重新分配存储并把元素搬到新地址,因此所有指向旧存储的指针、引用、迭代器都会失效。这是vector最重要的失效规则,也是reserve存在的意义:提前预留足够容量,避免循环中反复扩容。


三、完整实现


下面的实现把分配、构造、销毁分开,并用 copy-and-swap 提供拷贝赋值的强异常保证。


// 适用于 C++17
#include <cstddef>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>

template <class T, class Allocator = std::allocator<T>>
class MiniVector {
public:
using value_type = T;
using allocator_type = Allocator;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = typename std::allocator_traits<Allocator>::pointer;
using const_pointer = typename std::allocator_traits<Allocator>::const_pointer;
using iterator = pointer;
using const_iterator = const_pointer;

MiniVector() = default;

explicit MiniVector(size_type n) { resize(n); }

MiniVector(const MiniVector& other) : alloc_(other.alloc_) {
if (other.size_ == 0) {
return;
}
data_ = alloc_traits::allocate(alloc_, other.size_);
cap_ = other.size_;
size_type constructed = 0;
try {
for (; constructed < other.size_; ++constructed) {
alloc_traits::construct(alloc_, data_ + constructed,
other.data_[constructed]);
}
size_ = other.size_;
} catch (...) {
for (size_type i = 0; i < constructed; ++i) {
alloc_traits::destroy(alloc_, data_ + i);
}
alloc_traits::deallocate(alloc_, data_, cap_);
data_ = pointer{};
cap_ = 0;
throw;
}
}

MiniVector(MiniVector&& other) noexcept(
std::is_nothrow_move_constructible<Allocator>::value)
: alloc_(std::move(other.alloc_)),
data_(other.data_),
size_(other.size_),
cap_(other.cap_) {
other.data_ = pointer{};
other.size_ = 0;
other.cap_ = 0;
}

MiniVector& operator=(const MiniVector& other) {
if (this != &other) {
MiniVector tmp(other); // 可能抛异常,*this 不受影响
swap(tmp);
}
return *this;
}

MiniVector& operator=(MiniVector&& other) noexcept(
std::is_nothrow_move_assignable<Allocator>::value) {
if (this != &other) {
release();
alloc_ = std::move(other.alloc_);
data_ = other.data_;
size_ = other.size_;
cap_ = other.cap_;
other.data_ = pointer{};
other.size_ = 0;
other.cap_ = 0;
}
return *this;
}

~MiniVector() { release(); }

void swap(MiniVector& other) noexcept {
using std::swap;
swap(data_, other.data_);
swap(size_, other.size_);
swap(cap_, other.cap_);
}

size_type size() const noexcept { return size_; }
size_type capacity() const noexcept { return cap_; }
bool empty() const noexcept { return size_ == 0; }

reference operator[](size_type i) noexcept { return data_[i]; }
const_reference operator[](size_type i) const noexcept { return data_[i]; }

reference at(size_type i) {
if (i >= size_) {
throw std::out_of_range("MiniVector::at");
}
return data_[i];
}

reference front() { return data_[0]; }
reference back() { return data_[size_ - 1]; }

iterator begin() noexcept { return data_; }
iterator end() noexcept { return data_ + size_; }
const_iterator begin() const noexcept { return data_; }
const_iterator end() const noexcept { return data_ + size_; }
const_iterator cbegin() const noexcept { return data_; }
const_iterator cend() const noexcept { return data_ + size_; }

T* data() noexcept { return data_; }
const T* data() const noexcept { return data_; }

void reserve(size_type newCap) {
if (newCap <= cap_) {
return;
}
pointer newData = alloc_traits::allocate(alloc_, newCap);
size_type constructed = 0;
try {
for (; constructed < size_; ++constructed) {
alloc_traits::construct(
alloc_, newData + constructed,
std::move_if_noexcept(data_[constructed]));
}
} catch (...) {
for (size_type i = 0; i < constructed; ++i) {
alloc_traits::destroy(alloc_, newData + i);
}
alloc_traits::deallocate(alloc_, newData, newCap);
throw;
}

for (size_type i = 0; i < size_; ++i) {
alloc_traits::destroy(alloc_, data_ + i);
}
if (data_ != pointer{}) {
alloc_traits::deallocate(alloc_, data_, cap_);
}
data_ = newData;
cap_ = newCap;
}

void resize(size_type n) {
if (n < size_) {
for (size_type i = n; i < size_; ++i) {
alloc_traits::destroy(alloc_, data_ + i);
}
size_ = n;
} else if (n > size_) {
reserve(n);
for (; size_ < n; ++size_) {
alloc_traits::construct(alloc_, data_ + size_);
}
}
}

void clear() noexcept {
for (size_type i = 0; i < size_; ++i) {
alloc_traits::destroy(alloc_, data_ + i);
}
size_ = 0;
}

void push_back(const T& value) {
if (size_ == cap_) {
grow(size_ + 1);
}
alloc_traits::construct(alloc_, data_ + size_, value);
++size_;
}

void push_back(T&& value) {
if (size_ == cap_) {
grow(size_ + 1);
}
alloc_traits::construct(alloc_, data_ + size_, std::move(value));
++size_;
}

template <class... Args>
reference emplace_back(Args&&... args) {
if (size_ == cap_) {
grow(size_ + 1);
}
alloc_traits::construct(alloc_, data_ + size_,
std::forward<Args>(args)...);
++size_;
return data_[size_ - 1];
}

void pop_back() {
--size_;
alloc_traits::destroy(alloc_, data_ + size_);
}

private:
using alloc_traits = std::allocator_traits<Allocator>;

void grow(size_type needed) {
size_type newCap = (cap_ == 0) ? 1 : cap_ * 2;
if (newCap < needed) {
newCap = needed;
}
reserve(newCap);
}

void release() noexcept {
clear();
if (data_ != pointer{}) {
alloc_traits::deallocate(alloc_, data_, cap_);
data_ = pointer{};
cap_ = 0;
}
}

Allocator alloc_{};
pointer data_{};
size_type size_ = 0;
size_type cap_ = 0;
};

使用示例:


// 适用于 C++17
#include <iostream>

int main() {
MiniVector<int> values;
values.reserve(4);

for (int i = 0; i < 5; ++i) {
values.push_back(i * i);
}
values.emplace_back(42);

for (int value : values) {
std::cout << value << ' ';
}
std::cout << "\ncapacity=" << values.capacity()
<< " size=" << values.size() << '\n';
}

按标准语义,reserve(4)后前四个push_back不会扩容,第五个触发扩容;具体输出取决于扩容策略。


四、关键实现细节与差距


为什么reserve里用std::move_if_noexcept。扩容要把旧元素搬到新存储。如果T的移动构造可能抛异常,而T又可拷贝,move_if_noexcept会选择拷贝:拷贝失败时旧元素原封不动,容器满足强异常保证。如果移动是noexcept,则选择移动,避免深拷贝。若类型只能移动且移动可能抛异常,它仍会移动,此时只能提供基本保证。


为什么扩容失败时旧数据还在。reserve先分配新存储,逐个构造新元素;任何一步抛异常,就销毁已构造的新元素、释放新存储并重抛。旧存储和旧元素直到新存储全部构造成功后才销毁。这种「先做新、成功再换」的顺序,是强异常保证的标准手法。


为什么析构和release要分开。release既销毁元素又释放存储,供析构函数和移动赋值复用。clear只销毁元素、不释放存储,容器仍可继续使用,容量不变。


移动后源对象要重置。移动构造和移动赋值都把源对象的data_置空、size_和cap_置 0。否则源对象析构时会释放已经被目标接管的存储,造成重复释放。被移动对象随后处于「有效但未指定」状态,这里我们把它变成空容器,是一种更强的约定。


pop_back不检查空容器。标准容器的pop_back在空容器上是未定义行为,这里保持一致。调用方必须先检查empty()。


operator[]不检查边界。与std::vector一致,越界是 UB;需要检查时用at(),它抛std::out_of_range。


分配器状态被简化。本例假设所有实例使用相等的分配器(默认std::allocator满足)。真正的 STL 实现还要处理propagate_on_container_copy_assignment等 traits,本文不展开。


与 std::vector 的差距


这个MiniVector只覆盖了核心机制,距离标准容器还很远:




维度MiniVectorstd::vector



中间插入/删除不支持insert/erase

分配器传播简化处理完整 traits 支持

异常保证部分强保证明确规定的各级保证

迭代器类型裸指针实现定义的随机访问迭代器

辅助接口少量assign、shrink_to_fit、比较运算符等

调试检查无部分实现有_GLIBCXX_DEBUG等模式



即便如此,实现一遍仍能解释很多平时当成「黑箱」的行为:为什么reserve能避免迭代器失效、为什么push_back的实参引用容器内部元素时不能在扩容后再用、为什么自定义类型需要可移动或可拷贝、为什么vector的析构是线性的。


常见坑点



  1. ❌ 用new T[n]预分配容量,要求T必须可默认构造。


✅ 用allocator::allocate拿原始内存,只在真正插入时construct。



  1. ❌allocate之后用delete[]释放。


✅ 必须用同一个分配器的deallocate(ptr, n),且 n 与分配时一致。



  1. ❌ 只deallocate不destroy,非平凡析构函数不执行。


✅ 先对每个已构造元素调用destroy,再释放存储。



  1. ❌ 扩容后继续使用指向旧存储的指针、引用或迭代器。


✅ 扩容会使它们全部失效;提前reserve,或在扩容后重新获取。



  1. ❌ 拷贝构造中途抛异常,直接往外扔,已构造的部分泄漏。


✅try/catch中销毁已构造元素并释放存储,再throw;重抛。



  1. ❌ 移动构造只搬指针,不把源对象的指针置空。


✅ 源对象重置为空,避免两个对象析构同一块存储。



  1. ❌ 用memcpy搬移非平凡可拷贝类型。


✅ 用移动或拷贝构造;memcpy对含指针、虚表或资源句柄的类型是 UB。



  1. ❌ 认为reserve会改变size(),或认为resize一定重新分配。


✅reserve只改容量;resize改元素个数,容量足够时不分配。


总结




层次职责关键接口



分配器申请/释放原始内存allocate、deallocate

traits构造/销毁对象construct、destroy

容器维护 size/cap、迭代器、异常安全reserve、push_back、resize



实现 STL 容器的核心不是背接口,而是守住三条不变量:size_ <= cap_;只有[0, size_)内是活对象;任何一次扩容都要先保证新存储构造成功,再销毁旧存储。把这三条落实到每一行代码,拷贝、移动、异常、析构的行为就都能推导出来。


真正写生产代码时仍应使用std::vector:它经过充分测试,异常保证、分配器传播和边界情况都有明确规范。自己实现的价值在于获得「可推演」的理解——当你知道push_back背后发生了什么,再看迭代器失效和性能问题,就不会只停留在记忆结论的层面。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 13:45:07

AI Pro用户行为偏好调研:从问卷到LTV提升的工程化方法

1. 项目概述&#xff1a;这不是一次普通问卷&#xff0c;而是对高价值用户行为模式的深度切片“OpenAI Pro 用户偏好调查”——光看标题&#xff0c;很多人第一反应是“又一个平台在收数据”&#xff0c;但作为连续三年深度参与多个AI产品用户研究项目的从业者&#xff0c;我必…

作者头像 李华
网站建设 2026/10/20 13:44:30

AnythingLLM全栈AI应用:从多模型接入到知识库部署的完整拆解

简介&#xff1a;一份围绕AnythingLLM的全栈AI应用解析文档&#xff0c;系统梳理Mintplex Labs出品的AI助手能力&#xff0c;面向需要将多模态、多模型AI引入业务流程的专业人士、开发者、企业IT团队及教研创作者&#xff0c;用于解决知识管理、企业办公、客户支持、内容创作等…

作者头像 李华
网站建设 2026/10/20 13:43:46

STM32驱动ESP8266的AT通信状态机设计与环形缓冲实践

简介&#xff1a;本资源是一套面向嵌入式初学者与物联网项目开发者的STM32与ESP8266串口通信驱动程序&#xff0c;聚焦解决MCU通过AT指令控制Wi-Fi模块实现STA/AP双模联网的核心问题。适用于基于HAL库的STM32F1/F4系列开发&#xff0c;涵盖初始化配置、AT命令收发、响应解析、模…

作者头像 李华
网站建设 2026/10/20 13:43:07

Ray分布式加速医疗模型训练实战:从数据到调参全优化

最近帮某医院影像科的同学做了一次真实的提速改造&#xff0c;场景很朴素&#xff1a;他们手上攒了几万张 CT 和病理切片&#xff0c;想在内部 GPU 服务器上把医学图像分割模型跑起来&#xff0c;结果单卡训练一轮要十几个小时&#xff0c;超参数搜索基本不敢开&#xff0c;数据…

作者头像 李华
网站建设 2026/10/20 13:42:45

122页PPT讲透项目管理:从三边行动到六拍运动的避坑指南

简介&#xff1a;这份华为项目管理金种子初级培训教材面向项目管理初学者、企业基层管理者及希望系统了解华为项目管理方法的职场人&#xff0c;帮助其建立规范的项目管理思维&#xff0c;规避常见误区。教材以122页PPT形式呈现&#xff0c;围绕项目管理基本流程、常用知识、实…

作者头像 李华
网站建设 2026/10/20 13:42:37

基于YOLOv8的安全手套佩戴检测系统:从数据集到部署全流程

车间现场装了一圈摄像头之后&#xff0c;紧接着就遇到了新问题&#xff1a;画面是有了&#xff0c;但靠人盯着屏幕看工人有没有戴手套&#xff0c;根本不现实——十几路画面轮播下来&#xff0c;眼睛都看花了&#xff0c;漏掉的情况还是不少。后来我用YOLOv8搭了一套安全手套佩…

作者头像 李华