前言std::string是 C 最常用的类之一模拟实现它常被称为my_string或String是理解「资源管理类」的经典练习它同时涉及动态数组、三/五法则、拷贝与移动、异常安全、迭代器失效和 C 字符串接口。写一遍这样的类比背十条规则更能看清所有权是怎么流转的。模拟实现不是要替代std::string。标准库的实现包含短字符串优化small string optimization, SSO、更精细的扩容策略和大量边界处理生产代码应直接使用std::string。本文的目标是讲清一个「能用」的最小实现需要守住哪些不变量以及哪些地方最容易出错。常见的误解是「字符串类只要有一个char*和一个长度就够了」。真正需要维护的是三个量数据指针、当前长度、当前容量并且要保证数据以\0结尾、容量不小于长度、移动后的源对象可以被安全析构。下面按成员、特殊成员函数、扩容与追加、访问接口四部分展开。代码以 C17 为准未在本机编译或运行。一、三个成员与四条不变量一个不带短字符串优化的字符串类通常持有三个成员成员含义说明data_指向堆上的字符数组至少分配capacity_ 1字节size_当前字符数不含结尾的\0size_ capacity_capacity_当前可容纳的字符数不含结尾的\0扩容后只增不减本实现四条不变量必须始终成立data_为nullptr或者指向一块至少capacity_ 1字节的数组。本实现只在「被移动后」出现nullptr。data_[size_] \0保证c_str()返回合法的 C 字符串。size_ capacity_。长度为size_的内容位于data_[0]到data_[size_ - 1]区间之外不参与逻辑。为什么容量要不含结尾的\0这样capacity()与size()使用同一套计数方式比较直观分配时统一多申请一个字节。也有人把容量定义成包含\0只要全类保持一致即可但混用两套定义是 bug 的温床。c_str()返回的指针指向内部缓冲区不转移所有权。只要字符串被修改尤其是触发扩容或销毁这个指针就可能失效调用方不能长期保存它更不能delete[]它。二、特殊成员函数拷贝、移动与赋值默认构造分配一个字节并写入\0长度和容量都为 0。从const char*构造时先strlen求长度再分配len 1字节并连同结尾一起复制。这里必须检查空指针对nullptr调用strlen是未定义行为。拷贝构造做深拷贝按源对象的容量分配新缓冲区复制size_ 1字节。移动构造直接接管指针、长度和容量然后把源对象的三个成员置空。置空之后源对象仍然可以析构delete[] nullptr合法、可以重新赋值但不要读取它的内容。为了让被移动对象也能被安全地拷贝拷贝构造对data_ nullptr做了兜底。拷贝赋值采用「复制并交换」先构造临时副本成功后再交换。如果复制过程中抛出std::bad_alloc原对象完全没变满足强异常安全保证自赋值也自然安全。移动赋值先释放自己的缓冲区再接管源对象的三个成员并置空源对象。析构函数只做delete[] data_。注意必须与new char[...]配对不能用delete也不能用free。// 适用于 C17#include cstddef#include cstdint#include cstring#include ostream#include stdexcept#include utilityclass String {public:String() : data_(new char[1]{\0}), size_(0), capacity_(0) {}String(const char* s) {if (s nullptr) {throw std::invalid_argument(String: null pointer);}size_ std::strlen(s);capacity_ size_;data_ new char[capacity_ 1];std::memcpy(data_, s, size_ 1);}String(const String other): data_(new char[other.capacity_ 1]),size_(other.size_),capacity_(other.capacity_) {if (other.data_ ! nullptr) {std::memcpy(data_, other.data_, size_ 1);} else {data_[0] \0;}}String(String other) noexcept: data_(other.data_), size_(other.size_), capacity_(other.capacity_) {other.data_ nullptr;other.size_ 0;other.capacity_ 0;}String operator(const String other) {if (this ! other) {String tmp(other); // 失败时 *this 不变swap(tmp);}return *this;}String operator(String other) noexcept {if (this ! other) {delete[] data_;data_ other.data_;size_ other.size_;capacity_ other.capacity_;other.data_ nullptr;other.size_ 0;other.capacity_ 0;}return *this;}~String() {delete[] data_;}void swap(String other) noexcept {std::swap(data_, other.data_);std::swap(size_, other.size_);std::swap(capacity_, other.capacity_);}void reserve(std::size_t newCapacity) {if (newCapacity capacity_) {return;}char* newData new char[newCapacity 1];if (data_ ! nullptr) {std::memcpy(newData, data_, size_ 1);} else {newData[0] \0;}delete[] data_;data_ newData;capacity_ newCapacity;}void push_back(char ch) {if (size_ capacity_) {reserve(capacity_ 0 ? 1 : capacity_ * 2);}data_[size_] ch;size_;data_[size_] \0;}String append(const char* s) {if (s nullptr) {throw std::invalid_argument(String::append: null pointer);}const std::size_t len std::strlen(s);// 若 s 指向本对象内部缓冲区扩容后原指针会失效先记下偏移。// 用整数地址比较避免对无关指针做关系比较。const std::uintptr_t base reinterpret_caststd::uintptr_t(data_);const std::uintptr_t addr reinterpret_caststd::uintptr_t(s);const bool inside data_ ! nullptr addr base addr base size_ 1;const std::size_t offset inside ? static_caststd::size_t(addr - base) : 0;reserve(size_ len);if (inside) {s data_ offset;}std::memmove(data_ size_, s, len 1); // 允许源与目标重叠size_ len;return *this;}String operator(const String other) {return append(other.c_str());}String operator(char ch) {push_back(ch);return *this;}char operator[](std::size_t index) { return data_[index]; }const char operator[](std::size_t index) const { return data_[index]; }char at(std::size_t index) {if (index size_) {throw std::out_of_range(String::at);}return data_[index];}const char at(std::size_t index) const {if (index size_) {throw std::out_of_range(String::at);}return data_[index];}const char* c_str() const noexcept {return data_ ! nullptr ? data_ : ;}std::size_t size() const noexcept { return size_; }std::size_t capacity() const noexcept { return capacity_; }bool empty() const noexcept { return size_ 0; }char* begin() noexcept { return data_; }char* end() noexcept { return data_ ! nullptr ? data_ size_ : nullptr; }const char* begin() const noexcept { return data_; }const char* end() const noexcept {return data_ ! nullptr ? data_ size_ : nullptr;}private:char* data_;std::size_t size_;std::size_t capacity_;};inline std::ostream operator(std::ostream os, const String s) {return os s.c_str();}三、扩容、追加与异常安全reserve(n)的行为是「保证容量至少为 n不改变长度」。实现分三步分配新缓冲区、复制旧内容含结尾、释放旧缓冲区并更新成员。如果new抛出前两步没有发生原对象不变因此reserve提供强保证。注意复制的是size_ 1字节结尾的\0也要带过去。push_back在size_ capacity_时扩容。本实现采用「容量为 0 时扩到 1否则翻倍」的策略。翻倍不是标准规定std::string的具体增长策略由实现决定这里只是保证均摊 O(1)。理论上capacity_ * 2可能溢出std::size_t真实实现应加上界检查。append处理了两个容易忽略的问题。第一s可能指向本对象内部例如s.append(s.c_str())如果先扩容再strlen或直接使用旧指针就会读到已释放内存。代码先把指针转成整数地址判断是否落在本缓冲区范围内记下偏移扩容后再把s重新指向新缓冲区。第二源和目标可能重叠因此用memmove而不是memcpy。memmove允许重叠memcpy在重叠时是未定义行为。size_ len也可能溢出。工业级实现会检查上界并抛std::length_error本实现为保持简短没有加读者在自己的代码里应当补上。操作是否可能扩容异常安全迭代器/指针失效push_back是强保证扩容时全部失效append是强保证扩容时全部失效reserve是强保证扩容时全部失效operator[]否不检查边界不失效at否越界抛out_of_range不失效c_str否noexcept修改或析构后失效四、下标、迭代器与 c_str 的边界operator[]不做边界检查越界访问是未定义行为at检查index size_越界抛std::out_of_range。两者都返回引用可以读也可以写但返回的引用在扩容后可能悬垂。不要在持有元素引用的情况下调用push_back或append。begin()/end()返回原始指针作为迭代器。它们只在缓冲区未重新分配时有效任何可能扩容的操作都会让它们失效。被移动后的对象data_为nullptrbegin()和end()都返回空指针此时不能解引用但可以用于「空范围」的判断。c_str()返回内部缓冲区保证以\0结尾。它不分配新内存也不转移所有权调用方只要在字符串被修改或销毁后继续使用这个指针就是悬垂指针。需要长期保存内容时应复制到自己的缓冲区。本实现没有短字符串优化所有非空字符串都至少有一次堆分配也没有实现find、substr、比较运算符等接口。这些不影响对资源管理主线的理解真正使用时请优先选择std::string。常见坑点❌ 构造或追加后忘记写结尾的\0。✅ 每次改变长度后都在data_[size_]写入\0复制时连结尾一起复制。❌ 拷贝赋值先delete[] data_再从other复制自赋值时读到已释放内存。✅ 先判this ! other或先构造副本再交换。❌new char[...]配delete或new配free。✅ 数组形式的new[]必须配delete[]分配与释放方式严格对应。❌ 扩容后继续使用旧的c_str()指针或迭代器。✅ 任何可能扩容的操作之后重新获取指针/迭代器。❌ 移动构造只复制指针不把源对象置空。✅ 接管后立即把源对象的data_置nullptr、size_与capacity_置 0。❌append用memcpy处理可能指向自身的源字符串。✅ 先记录偏移、扩容后重定位并使用允许重叠的memmove。❌ 用operator[]访问index size_的位置并当作有效字符。✅ 下标范围是[0, size_)结尾的\0只供c_str()使用。❌ 把c_str()的返回值保存下来跨过字符串修改继续使用。✅ 需要持久化时复制内容c_str()指针只保证在下一次修改前有效。总结主题关键做法成员data_、size_、capacity_容量不含\0不变量结尾有\0size_ capacity_缓冲区足够拷贝深拷贝按容量分配复制size_ 1字节移动偷指针并置空源对象标noexcept赋值复制并交换保证强异常安全扩容先分配、再复制、后释放失败时原对象不变追加处理自引用与重叠使用memmove访问[]不检查at抛异常指针在扩容后失效模拟string的难点不在字符操作而在「缓冲区何时分配、何时复制、何时释放、何时失效」。把这四条不变量写进每个成员函数再用拷贝并交换和先分配后提交保证异常安全这个类就具备了正确的地基。生产环境仍然使用std::string但这份练习会让你在阅读标准库实现时知道该看什么。