1. 项目概述为什么我们要亲手实现一个简易的string在C的世界里std::string就像空气和水一样无处不在以至于我们常常忽略了它的复杂性。很多朋友在面试时被问到“string的底层实现”或者在学习STL源码时面对那层层叠叠的模板和内存管理感到一阵头大。我自己在带新人、做技术评审时也发现能熟练使用string的开发者很多但能清晰说出其内部如何管理内存、如何处理拷贝、迭代器失效时机的人却少之又少。这个项目就是带大家从零开始实现一个简化版的MyString。我们的目标不是复刻标准库中那个经过千锤百炼、极度优化、支持各种编码的std::string而是通过“造轮子”的过程深入理解几个核心问题动态内存如何管理拷贝构造和赋值操作符如何避免浅拷贝如何设计一个符合STL风格的接口这个过程远比死记硬背“string的size和capacity区别”要深刻得多。当你亲手处理过内存分配、拷贝、移动这些底层细节后再去看标准库的源码或者去理解其他容器如vector的实现就会有一种豁然开朗的感觉。这不仅是C进阶的必经之路更是夯实基础、写出健壮高效代码的关键。2. 核心设计思路我们的MyString要长什么样在动手写代码之前我们必须先想清楚设计蓝图。一个最基本的字符串类需要哪些核心部件我们参考std::string的常用功能但做大幅简化聚焦于最核心的机制。2.1 数据存储与内存管理策略这是设计的基石。std::string通常采用一种叫做“短字符串优化SSO”的技术对于很短的字符串直接存储在对象内部的缓冲区避免堆内存分配的开销。为了简化我们的MyString先不实现SSO采用更直观的“指针堆内存”模型。核心成员变量设计class MyString { private: char* m_data; // 指向存放字符串内容的堆内存首地址 size_t m_size; // 当前字符串的实际长度不包含结尾的\0 size_t m_capacity; // 当前分配的内存块能容纳的字符数至少为m_size1 };为什么需要三个成员m_data是资源的拥有者指向动态数组。m_size是逻辑长度即strlen的结果。m_capacity是物理容量它总是大于等于m_size 1多出的1用于存放字符串终止符\0。维护m_capacity是为了实现高效的append和push_back操作避免每次添加字符都重新分配内存即“摊还常数时间复杂度”。初始内存分配策略我们设定一个初始容量比如16。当构造一个空字符串时我们并非让m_data为nullptr而是立即分配一块小内存如16字节并将第一个字节设为\0。这样做的好处是后续的append操作可以直接使用这块内存直到它被填满。这是一种非常常见的优化减少了初期频繁分配的开销。2.2 关键接口与STL风格对齐我们的MyString需要提供一组符合直觉的接口让用惯了std::string的人也能轻松上手。主要分为几类构造与析构默认构造、C风格字符串构造、拷贝构造、移动构造、析构函数。容量相关size(),capacity(),empty(),reserve(),clear()。元素访问operator[]重载const和非const版本at()带边界检查c_str(),data()。修改操作append(),push_back(),operator,assign(),insert(),erase()。字符串操作find(),substr(),compare()。迭代器为了支持范围for循环和STL算法我们至少需要实现begin()和end()。在设计时我们要特别注意异常安全和强异常保证。例如在reserve或append中重新分配内存时如果分配失败应保持原字符串不变。这通常通过“先分配新内存拷贝数据再释放旧内存最后更新指针”的顺序来实现。3. 基础骨架与内存管理实现有了设计思路我们开始搭建类的骨架并实现最核心也最容易出错的部分资源管理RAII。3.1 类声明与私有助手函数首先我们给出MyString的类声明并预先声明几个内部使用的私有助手函数。这些函数不暴露给用户但能极大简化公共接口的实现避免代码重复。#include cstring // for strlen, memcpy, etc. #include stdexcept // for std::out_of_range #include algorithm // for std::swap (C11前), std::min class MyString { public: // 类型别名模仿STL using iterator char*; using const_iterator const char*; using size_type size_t; // 1. 构造函数族 MyString(); // 默认构造 explicit MyString(const char* str); // 从C字符串构造 MyString(const MyString other); // 拷贝构造 MyString(MyString other) noexcept; // 移动构造 (C11) // 其他构造如指定数量的字符暂略 // 2. 析构函数 ~MyString(); // 3. 赋值操作符 MyString operator(const MyString other); // 拷贝赋值 MyString operator(MyString other) noexcept; // 移动赋值 (C11) MyString operator(const char* str); // 4. 容量相关 size_type size() const { return m_size; } size_type length() const { return m_size; } // 与size()相同 size_type capacity() const { return m_capacity; } bool empty() const { return m_size 0; } void reserve(size_type new_cap); void clear(); // 5. 元素访问 char operator[](size_type pos); const char operator[](size_type pos) const; char at(size_type pos); const char at(size_type pos) const; const char* c_str() const { return m_data; } const char* data() const { return m_data; } // 6. 修改操作 MyString append(const char* str); MyString append(const MyString str); void push_back(char ch); MyString operator(const char* str) { return append(str); } MyString operator(const MyString str) { return append(str); } // 7. 迭代器 iterator begin() { return m_data; } const_iterator begin() const { return m_data; } const_iterator cbegin() const { return m_data; } iterator end() { return m_data m_size; } const_iterator end() const { return m_data m_size; } const_iterator cend() const { return m_data m_size; } // 8. 字符串操作简化版 size_type find(char ch, size_type pos 0) const; MyString substr(size_type pos, size_type len npos) const; int compare(const MyString other) const; // 静态成员常量模仿std::string::npos static const size_type npos -1; private: char* m_data; size_type m_size; size_type m_capacity; // 私有助手函数 void _init(); // 初始化成员变量为安全状态 void _free(); // 安全释放内存 void _copy_from(const char* str, size_type len); // 从指定字符串拷贝len个字符 void _reallocate(size_type new_cap); // 重新分配内存到指定容量 // 增长策略当需要更多空间时新容量计算规则 size_type _calculate_growth(size_type new_size) const; };3.2 构造、析构与拷贝控制资源管理的核心这部分是C类的灵魂直接决定了类的行为是否正确、高效。默认构造函数与_init助手void MyString::_init() { m_data nullptr; m_size 0; m_capacity 0; } MyString::MyString() { _init(); // 即使为空字符串我们也分配一个最小容量方便后续操作。 reserve(16); // 内部会处理m_data为nullptr的情况 m_data[0] \0; // 确保是空字符串 }_init函数将成员置为安全状态特别是m_data为nullptr。默认构造后我们立即reserve一小块内存这是一种积极的优化避免了第一次push_back时立即分配。从C字符串构造MyString::MyString(const char* str) { _init(); if (str) { size_type len strlen(str); _copy_from(str, len); } else { // 如果传入nullptr我们将其视为空字符串 reserve(16); m_data[0] \0; } }这里必须检查str是否为nullptr因为strlen对nullptr是未定义行为。健壮的实现应该处理这种边界情况。拷贝构造函数深拷贝的典范MyString::MyString(const MyString other) { _init(); _copy_from(other.m_data, other.m_size); }拷贝构造的核心是“深拷贝”。我们不是简单地复制指针那会导致两个对象共享同一块内存析构时重复释放而是分配一块新的、大小足够的内存然后把对方的数据逐个字节拷贝过来。_copy_from函数封装了这个逻辑。移动构造函数C11性能提升的关键MyString::MyString(MyString other) noexcept : m_data(other.m_data), m_size(other.m_size), m_capacity(other.m_capacity) { // “窃取”对方资源 other.m_data nullptr; // 至关重要使other处于可安全析构的状态 other.m_size 0; other.m_capacity 0; }移动构造是C11引入的“资源偷窃”语义。它直接将other内部的指针“抢”过来然后将other置为空状态。这个过程没有内存分配和拷贝效率极高。noexcept关键字告诉编译器这个操作不会抛出异常这对于标准库容器如vector在扩容时选择移动而非拷贝至关重要。析构函数资源释放void MyString::_free() { if (m_data) { delete[] m_data; // 一定要匹配 new char[] m_data nullptr; } } MyString::~MyString() { _free(); }析构函数的职责单一而明确释放对象拥有的资源。_free函数封装了释放逻辑并确保m_data在释放后被置为nullptr防止成为悬垂指针。拷贝赋值运算符处理自赋值MyString MyString::operator(const MyString other) { if (this ! other) { // 1. 防止自赋值 a a // 2. 分配新内存并拷贝数据强异常保证 char* new_data new char[other.m_capacity]; std::memcpy(new_data, other.m_data, other.m_size 1); // 包含\0 // 3. 释放旧资源 _free(); // 4. 接管新资源 m_data new_data; m_size other.m_size; m_capacity other.m_capacity; } return *this; }拷贝赋值运算符是面试高频考点。关键点有四1)自赋值检查a a如果不检查在第2步分配新内存后第3步就把自己的数据释放了导致错误。2)强异常保证先分配新内存并拷贝成功再释放旧内存。这样即使分配失败抛出std::bad_alloc原对象状态也不会改变。3)释放旧资源。4)返回*this以支持链式赋值。移动赋值运算符MyString MyString::operator(MyString other) noexcept { if (this ! other) { _free(); // 释放自己的旧资源 // 接管对方资源 m_data other.m_data; m_size other.m_size; m_capacity other.m_capacity; // 置空对方 other.m_data nullptr; other.m_size 0; other.m_capacity 0; } return *this; }思路与移动构造类似但需要先释放自身原有资源。同样需要检查自赋值虽然移动自身的场景极少但为了一致性。3.3 内存分配策略与reserve实现动态数组的核心在于如何高效地管理容量。std::vector和std::string通常采用指数增长策略例如每次容量不足时新容量 旧容量 * 1.5 或 2。这保证了多次push_back操作的摊还时间复杂度为O(1)。_calculate_growth增长策略MyString::size_type MyString::_calculate_growth(size_type new_size) const { // 常见策略至少翻倍但不超过最大值 const size_type old_cap m_capacity; const size_type max_cap std::numeric_limitssize_type::max(); if (old_cap max_cap - old_cap / 2) { // 已经很大接近上限直接请求所需大小 return new_size; } // 几何增长至少增长50% const size_type geometric old_cap old_cap / 2; if (geometric new_size) { return new_size; // 如果几何增长仍不够则直接满足需求 } return geometric; }这个函数决定了当我们需要更多空间时应该申请多大的新容量。我们采用“至少增长50%”的策略这是一个在内存利用率和性能之间较好的平衡点。_reallocate重新分配void MyString::_reallocate(size_type new_cap) { if (new_cap m_capacity) { return; // 无需重新分配 } // 实际分配的容量需要至少能容纳 new_size 个字符 1个终止符 // 但new_cap参数通常已经考虑了这一点由reserve或append调用者保证 // 为了健壮性我们确保至少为1 size_type alloc_cap (new_cap 0) ? new_cap : 1; char* new_data new char[alloc_cap]; // 可能抛出std::bad_alloc // 拷贝现有数据包括终止符 if (m_data) { std::memcpy(new_data, m_data, m_size 1); } else { // 如果原来没有数据确保新缓冲区以空字符开头 new_data[0] \0; } // 释放旧内存更新成员 delete[] m_data; m_data new_data; m_capacity alloc_cap; // m_size 保持不变 }_reallocate是内存管理的核心。它负责分配新内存、拷贝数据、释放旧内存。注意异常安全new char[...]可能失败如果失败m_data指向的旧内存依然有效满足了强异常保证。reserve公共接口void MyString::reserve(size_type new_cap) { if (new_cap m_capacity) { _reallocate(new_cap); } // 如果 new_cap m_capacity标准规定这是一个非绑定收缩请求。 // 我们的简化实现忽略收缩请求。标准库的shrink_to_fit负责收缩。 }reserve是给用户的承诺“我保证接下来的操作至少不需要重新分配直到字符串长度超过new_cap”。它是一个性能优化工具。_copy_from助手函数void MyString::_copy_from(const char* str, size_type len) { // len 是不包含\0的长度 if (len 0) { reserve(16); m_data[0] \0; m_size 0; return; } // 确保容量足够多分配1字节给终止符 reserve(len); std::memcpy(m_data, str, len); m_data[len] \0; // 手动添加终止符 m_size len; }这个函数被多个构造函数和赋值运算符调用它统一处理了“分配足够内存 - 拷贝数据 - 设置终止符 - 更新大小”这个流程是代码复用的典范。4. 关键成员函数的实现与细节基础框架搭好后我们来实现那些让MyString真正有用的功能。4.1 元素访问operator[]与atchar MyString::operator[](size_type pos) { // 不进行边界检查行为类似数组访问越界是未定义行为(UB) return m_data[pos]; } const char MyString::operator[](size_type pos) const { return m_data[pos]; } char MyString::at(size_type pos) { if (pos m_size) { throw std::out_of_range(MyString::at: pos out of range); } return m_data[pos]; } const char MyString::at(size_type pos) const { if (pos m_size) { throw std::out_of_range(MyString::at: pos out of range); } return m_data[pos]; }这里体现了C标准库的设计哲学operator[]追求效率不检查边界信任程序员at()追求安全进行边界检查如果越界则抛出std::out_of_range异常。我们提供了const和非const版本以支持对常量和非常量对象的访问。4.2 修改操作append与push_backappend是构建字符串的主要手段。MyString MyString::append(const char* str) { if (!str || *str \0) { return *this; // 追加空字符串直接返回 } size_type other_len strlen(str); if (other_len 0) { return *this; } size_type new_size m_size other_len; if (new_size 1 m_capacity) { // 1 for \0 // 需要扩容 size_type new_cap _calculate_growth(new_size 1); // 计算新容量 _reallocate(new_cap); } // 将新字符串拷贝到现有字符串的末尾 std::memcpy(m_data m_size, str, other_len); m_size new_size; m_data[m_size] \0; // 设置新的终止符 return *this; } MyString MyString::append(const MyString str) { // 复用const char*版本避免重复代码 return append(str.m_data); } void MyString::push_back(char ch) { if (m_size 1 m_capacity) { // 需要预留一个位置给\0 reserve(_calculate_growth(m_size 2)); // m_size11 } m_data[m_size] ch; m_size; m_data[m_size] \0; // 始终维护终止符 }append的逻辑是计算新长度 - 检查容量是否足够 - 不够则扩容 - 拷贝数据 - 更新大小和终止符。注意我们总是保证m_data的m_size索引位置是\0这样c_str()才能正常工作。一个关键细节在push_back中我们判断条件是m_size 1 m_capacity。因为m_capacity是包含终止符的总容量。如果当前m_size是5m_capacity是8那么实际用于存储字符的空间是7个索引0-6索引7是终止符。当我们push_back第6个字符时需要占用索引6并把新的终止符放在索引7这刚好够用516 8。当push_back第7个字符时就需要扩容了617 8。这个边界条件需要仔细处理。4.3 字符串操作find,substr,compare这些函数实现了字符串的核心算法。find查找字符MyString::size_type MyString::find(char ch, size_type pos) const { if (pos m_size) { return npos; } const char* result static_castconst char*(std::memchr(m_data pos, ch, m_size - pos)); if (result) { return result - m_data; // 指针相减得到索引 } return npos; }我们使用了C标准库的memchr函数它在指定内存块中查找一个字节。这比手动写循环更高效且代码简洁。注意处理pos参数和npos返回值。substr获取子串MyString MyString::substr(size_type pos, size_type len) const { if (pos m_size) { throw std::out_of_range(MyString::substr: pos out of range); } // 计算实际要拷贝的长度 size_type actual_len std::min(len, m_size - pos); MyString result; if (actual_len 0) { result._copy_from(m_data pos, actual_len); } return result; // 依赖移动语义或RVO返回值优化 }substr需要处理边界如果pos超出范围抛出异常如果请求的长度len超过了从pos到末尾的长度则只拷贝到末尾。这里我们巧妙地复用了内部的_copy_from函数。注意返回值是MyString对象现代C编译器会进行RVO返回值优化或移动构造避免不必要的拷贝。compare比较字符串int MyString::compare(const MyString other) const { // 使用标准库的memcmp进行逐字节比较 int cmp_result std::memcmp(m_data, other.m_data, std::min(m_size, other.m_size)); if (cmp_result ! 0) { return cmp_result; } // 如果公共部分相等则长度长的字符串更大 if (m_size other.m_size) return -1; if (m_size other.m_size) return 1; return 0; // 完全相等 }compare的返回值规则与C标准库的strcmp一致小于0表示*this小于other等于0表示相等大于0表示*this大于other。比较逻辑是先比较公共长度部分如果分出胜负则返回如果公共部分相等则长度更长的字符串更大。4.4clear与shrink_to_fit思路void MyString::clear() { // 逻辑上清空字符串但不释放内存 m_size 0; if (m_data) { m_data[0] \0; } }clear只重置逻辑大小不改变容量。这是为了后续可能的重用避免重复分配内存。一个完整的string类还应该实现shrink_to_fit它请求减少容量以匹配大小。在我们的简化实现中可以留作练习。其思路是如果m_capacity远大于m_size 1比如超过某个阈值则重新分配一块刚好够用的内存拷贝数据释放旧内存。5. 迭代器、运算符重载与完整使用示例为了让我们的MyString用起来更像标准库的string我们需要实现迭代器和一些常用的运算符。5.1 迭代器实现我们在类定义中已经声明了iterator和const_iterator分别是char*和const char*。这是可行的因为char*本身满足随机访问迭代器的所有要求可递增、递减、解引用、相减等。因此begin()和end()的实现非常简单// 已在类内声明 // iterator begin() { return m_data; } // const_iterator begin() const { return m_data; } // iterator end() { return m_data m_size; } // const_iterator end() const { return m_data m_size; }有了迭代器我们的MyString就可以用于范围for循环和许多STL算法MyString str Hello; for (char ch : str) { std::cout ch; } std::reverse(str.begin(), str.end()); // 反转字符串5.2 流操作符重载为了支持像std::cout str和std::cin str这样的操作我们需要重载和运算符。注意这些函数通常是非成员函数。#include iostream #include istream #include ostream // 输出运算符 std::ostream operator(std::ostream os, const MyString str) { if (str.data()) { // 安全起见检查指针 os str.data(); } return os; } // 输入运算符简化版读取一个单词 std::istream operator(std::istream is, MyString str) { str.clear(); // 先清空目标字符串 char ch; // 跳过前导空白字符 while (is.get(ch) std::isspace(static_castunsigned char(ch))) { // 什么也不做只是跳过 } if (!is) { return is; // 输入失败 } // 将第一个非空白字符放回流中因为我们用get()取出来了 is.unget(); // 读取直到遇到空白字符 while (is.get(ch) !std::isspace(static_castunsigned char(ch))) { str.push_back(ch); } return is; }输入运算符的实现相对复杂需要处理空白字符和输入错误。这里实现的是一个简化版本它读取一个“单词”非空白字符序列。更完整的实现还需要考虑缓冲区大小、错误状态等类似于std::getline。5.3 关系运算符重载我们可以基于compare成员函数轻松实现全套关系运算符,!,,,,。这些也通常是非成员函数。bool operator(const MyString lhs, const MyString rhs) { return lhs.compare(rhs) 0; } bool operator!(const MyString lhs, const MyString rhs) { return !(lhs rhs); } bool operator(const MyString lhs, const MyString rhs) { return lhs.compare(rhs) 0; } bool operator(const MyString lhs, const MyString rhs) { return lhs.compare(rhs) 0; } bool operator(const MyString lhs, const MyString rhs) { return rhs lhs; } bool operator(const MyString lhs, const MyString rhs) { return rhs lhs; }注意我们只完整实现了和其他运算符通过这两个推导出来这符合C标准库的惯例可以减少代码重复。5.4 一个完整的使用示例与测试最后我们写一段简单的测试代码验证MyString的核心功能。#include iostream #include cassert void test_my_string() { std::cout 测试 MyString 基础功能 \n; // 1. 默认构造与C字符串构造 MyString s1; assert(s1.empty() s1.size() 0); std::cout s1 (默认构造): \ s1 \\n; MyString s2 Hello; assert(s2.size() 5); assert(s2.compare(Hello) 0); std::cout s2 (C字符串构造): \ s2 \\n; // 2. 拷贝构造与拷贝赋值 MyString s3 s2; // 拷贝构造 assert(s3 s2); s3 World; // 拷贝赋值 (从C字符串) assert(s3 World); std::cout s3 (赋值后): \ s3 \\n; // 3. 移动语义 (C11) MyString s4 std::move(s3); // 移动构造 assert(s4 World); assert(s3.empty() || s3.c_str() nullptr); // 移动后源对象为空 std::cout s4 (从s3移动而来): \ s4 \\n; std::cout s3 (移动后): \ s3 \\n; // 4. 元素访问 assert(s2[0] H); s2[0] h; assert(s2 hello); try { s2.at(10); // 应该抛出异常 assert(false); // 不应该执行到这里 } catch (const std::out_of_range e) { std::cout 异常捕获成功: e.what() \n; } // 5. 修改操作 s2.append( World); assert(s2 hello World); s2.push_back(!); assert(s2 hello World!); std::cout s2 (追加后): \ s2 \\n; // 6. 容量管理 size_t old_cap s2.capacity(); s2.reserve(100); assert(s2.capacity() 100); assert(s2 hello World!); // reserve不应改变内容 std::cout s2 capacity: old_cap - s2.capacity() \n; // 7. 字符串操作 assert(s2.find(W) 6); assert(s2.find(z) MyString::npos); MyString sub s2.substr(6, 5); assert(sub World); std::cout s2.substr(6,5): \ sub \\n; // 8. 迭代器与范围for std::cout 遍历s2: ; for (char ch : s2) { std::cout ch ; } std::cout \n; // 9. 流操作 MyString s5; std::cout 请输入一个单词: ; // 在实际测试中可能需要注释掉因为会阻塞自动化测试 // std::cin s5; // std::cout 你输入的是: s5 \n; std::cout 所有测试通过 \n; } int main() { test_my_string(); return 0; }6. 深入探讨我们的实现与std::string的差距通过以上实现我们完成了一个功能相对完整的简易string类。但它与真正的std::string还有巨大差距理解这些差距正是进阶学习的方向短字符串优化SSO现代std::string实现如GCC的libstdcClang的libc普遍采用SSO。对于短字符串通常15或22字节以内直接将其存储在对象内部的缓冲区而不分配堆内存。这极大地提升了小字符串操作的性能避免了堆分配开销和缓存局部性。实现SSO需要更精巧的内存布局设计通常使用一个union来区分“短字符串模式”和“长字符串模式”。模板化与自定义分配器std::string实际上是std::basic_stringchar的别名。basic_string是一个模板类可以接受不同的字符类型如wchar_t,char16_t,char32_t和字符特性char_traits以及自定义的内存分配器Allocator。这使得它极其灵活。我们的实现写死了char和new/delete。异常安全等级我们实现了基本的强异常保证例如在reserve和赋值中。但标准库的实现需要考虑所有操作在所有地方的异常安全这非常复杂。迭代器失效规则我们的简易实现中任何可能引起内存重新分配的操作如append导致扩容都会使所有迭代器、指针、引用失效。标准库的string有更精确的规定并且SSO的存在使得某些操作可能不会使迭代器失效。更丰富的接口我们只实现了最核心的接口。std::string还有insert,erase,replace,find_first_of,find_last_of,rfind,getline友元函数与数值的转换stoi,to_string等数十个成员函数和非成员函数。性能优化标准库的实现经过了极致的优化会使用平台特定的内存操作指令如memcpy的内联汇编版本、循环展开、避免不必要的拷贝等。7. 常见问题与避坑指南在实现和使用自定义字符串类的过程中我踩过不少坑也见过很多新手容易犯的错误忘记处理拷贝构造和赋值运算符规则三/五这是经典错误。如果你定义了析构函数来释放动态内存那么几乎肯定需要定义拷贝构造函数和拷贝赋值运算符规则三在C11后最好也定义移动构造函数和移动赋值运算符规则五。否则编译器生成的默认版本是浅拷贝会导致双重释放double free或内存泄漏。自赋值问题在拷贝赋值运算符中必须检查if (this ! other)。否则a a这样的操作会先释放自己的内存然后试图从已释放的内存中拷贝数据。异常安全在重新分配内存如reserve时应该先分配新内存、拷贝数据成功后再释放旧内存。如果先释放旧内存再分配新内存一旦分配失败对象就处于无效状态资源已释放但指针未置空违反了强异常保证。空指针与零长度始终要小心处理nullptr。在构造函数中如果传入的C字符串是nullptr是应该抛出异常还是将其视为空字符串标准库的std::string构造函数接受nullptr是未定义行为。在我们的实现中我们选择将其视为空字符串以增强健壮性但这可能与标准库行为不一致。容量与大小的关系时刻记住capacity()返回的是存储空间的总大小包括预留的而size()是当前字符串的实际长度。在push_back或append时判断是否需要扩容的条件是new_size 1 capacity()因为你需要一个位置放新的字符还需要一个位置放终止符\0。终止符\0的管理我们的m_data指向的缓冲区必须始终以\0结尾这样c_str()才能返回一个合法的C风格字符串。在append、push_back、clear、resize等任何改变字符串内容的操作后都必须手动在m_data[m_size]的位置设置\0。迭代器失效要清楚地文档化或意识到哪些操作会使迭代器失效。在我们的实现中任何可能导致_reallocate被调用的操作如append导致扩容都会使之前获取的所有迭代器、指针、引用失效。这是使用动态容器时必须牢记的。亲手实现一遍这个简易的MyString你会对C的RAII、资源管理、拷贝控制、异常安全、迭代器概念有质的理解。下次当你再使用std::string时你看到的将不再是一个黑盒而是一个清晰的内存模型和一系列精心设计的选择。这才是进阶学习的意义所在。