目录
[3.1 默认构造函数](#3.1 默认构造函数)
[3.2 从C字符串构造](#3.2 从C字符串构造)
[3.3 拷贝构造函数](#3.3 拷贝构造函数)
[3.4 析构函数](#3.4 析构函数)
[3.5 赋值运算符重载](#3.5 赋值运算符重载)
[4.1 size(), length(), capacity()](#4.1 size(), length(), capacity())
[4.2 reserve() ------ 手动扩容](#4.2 reserve() —— 手动扩容)
[5.1 operator[] 和 at](#5.1 operator[] 和 at)
[5.2 front() 和 back()](#5.2 front() 和 back())
[5.3 c_str()](#5.3 c_str())
[7.1 push_back 和 pop_back](#7.1 push_back 和 pop_back)
[7.2 append 系列](#7.2 append 系列)
[7.3 operator+= 与 operator+](#7.3 operator+= 与 operator+)
[7.4 insert ------ 在指定位置前插](#7.4 insert —— 在指定位置前插)
[7.5 erase ------ 删除一段字符](#7.5 erase —— 删除一段字符)
[7.6 clear](#7.6 clear)
[8.1 find ------ 查找子串或字符](#8.1 find —— 查找子串或字符)
[8.2 substr ------ 取子串](#8.2 substr —— 取子串)
[10.1 operator>> ------ 读取单词](#10.1 operator>> —— 读取单词)
[10.2 operator<< ------ 输出](#10.2 operator<< —— 输出)
[10.3 getline ------ 读取一行](#10.3 getline —— 读取一行)
引言
本文从零实现一个简化版std::string,深入理解构造、拷贝、扩容、迭代器、增删改查等核心接口的设计细节与注意事项。
一、为什么自己实现String?
C++标准库中的std::string功能强大,但黑盒般的封装让初学者难以理解其内部机制。手动实现一个string类,可以帮你掌握:
-
RAII(资源获取即初始化):构造时分配内存,析构时自动释放。
-
深拷贝与浅拷贝:避免指针悬挂和重复释放。
-
动态扩容策略:如何高效管理容量。
-
运算符重载:让自定义类型像内置类型一样使用。
-
迭代器设计:泛型编程的基础。
本文基于C++11,实现一个名为mine::string的类,涵盖常用接口,并逐段讲解代码背后的设计思想。
二、整体框架与成员变量
cpp
namespace mine {
class string {
private:
char* _str; // 指向动态分配的字符数组,以'\0'结尾
size_t _size; // 有效字符个数(不包括'\0')
size_t _capacity; // 当前可容纳的有效字符数(实际分配_capacity+1字节)
static const size_t npos = -1; // 用于find返回"未找到"或substr表示"直到结尾"
public:
// 接口声明...
};
}
设计约定:
-
_str指向堆空间,始终以'\0'终止,保证C字符串函数可用。 -
_size记录字符串长度,_capacity记录当前已分配的空间(不含终止符)。例如:字符串
"abc",_size=3,_capacity至少为3,实际分配_capacity+1字节。 -
npos是size_t的最大值(-1转换为无符号),用作"未找到"或"直到末尾"的标志。
三、构造与析构(RAII核心)
3.1 默认构造函数
cpp
string() {
_str = new char[1]{'\0'};
_capacity = 0;
_size = 0;
}
讲解:
-
空字符串也需要一个有效的
_str,指向一个只含'\0'的独立堆内存。 -
容量设为0,表示没有额外的有效字符空间。
_str[0]就是终止符。 -
必须使用
new分配,与析构中的delete[]配对。
3.2 从C字符串构造
cpp
string(const char* str) {
assert(str); // 防止传入空指针
size_t len = strlen(str);
_size = _capacity = len;
_str = new char[_capacity + 1]; // +1 给'\0'
strcpy(_str, str);
}
讲解:
-
参数合法性检查:若传入
nullptr,程序直接终止(可通过断言或异常处理)。 -
长度计算使用
strlen,效率O(N)。 -
_capacity = len,注意_capacity不包含终止符。分配内存时需额外多1字节存放'\0'。 -
使用
strcpy拷贝,保证末尾自动添加'\0'。
3.3 拷贝构造函数
cpp
string(const string& s) {
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
讲解:
-
必须深拷贝:不能直接
_str = s._str,否则两个对象指向同一块内存,析构时会重复释放,同时两个对象进行操作时会导致紊乱 -
新分配空间大小依据源对象的
_capacity,保证容量一致。 -
拷贝后两个对象完全独立,互不影响。
3.4 析构函数
cpp
string(const string& s) {
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
讲解:
-
释放堆内存,并将指针置空,避免悬垂指针。
-
即使
_str已经是nullptr,delete[]也是安全的(但这里构造确保非空)。
3.5 赋值运算符重载
cpp
// 从C字符串赋值
string& operator=(const char* str) {
assert(str);
int len = strlen(str);
char* tmp = new char[len + 1];
strcpy(tmp, str); // 先拷贝到新内存
delete[] _str; // 释放旧资源
_str = tmp;
_size = len;
_capacity = len;
return *this;
}
// 从string赋值
string& operator=(const string& s) {
if (&s != this) { // 自我赋值检查
*this = s._str; // 复用上面的C字符串赋值
}
return *this;
}
讲解:
-
异常安全 :先分配新内存并拷贝数据,成功后再释放旧内存。如果
new失败,旧对象保持不变。 -
自我赋值检测避免无谓操作和潜在错误。
-
复用代码:
operator=(const char*)实现具体逻辑,operator=(const string&)调用它,避免重复。
四、容量相关接口
4.1 size(), length(), capacity()
cpp
size_t size() const { return _size; }
size_t length() const { return _size; }
size_t capacity() const { return _capacity; }
讲解:
-
返回类型应为
size_t(无符号整数),与标准库一致。 -
这些函数不修改对象,因此声明为
const成员函数。
4.2 reserve() ------ 手动扩容
cpp
void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
if (_str) strcpy(tmp, _str);
else tmp[0] = '\0';
delete[] _str;
_str = tmp;
_capacity = n;
}
}
讲解:
-
仅当
n > _capacity时才重新分配,否则不做任何事(不缩容)。 -
分配新内存后,拷贝原有数据(如果存在),然后释放旧内存。
-
注意边界:如果当前
_str为nullptr(一般不会发生),则新内存只放一个'\0'。 -
保留原有
_size不变,只调整容量。
五、元素访问接口
5.1 operator[] 和 at
cpp
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
讲解:
-
提供
const和非const两个版本:const对象只能调用const版本,返回常量引用,禁止修改。 -
边界检查使用
assert(调试模式有效),at()可以改为抛出std::out_of_range异常。 -
下标从0开始,
_str[_size]是'\0',不允许访问(除非作为终止符读取)。
5.2 front() 和 back()
cpp
char& front() {
return _str[0];
}
char& back() {
assert(_size > 0);
return _str[_size - 1];
}
讲解:
-
front()即使空字符串也能返回_str[0](值为'\0'),但语义上应禁止。可以加上assert(_size > 0)。 -
back()必须确保字符串非空,否则越界。
5.3 c_str()
cpp
const char* c_str() const {
return _str;
}
讲解:
-
返回内部指针,方便与C字符串函数交互。
-
返回类型为
const char*,防止通过指针修改内部数据。
六、迭代器接口
cpp
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
讲解:
-
迭代器就是原生指针,因为底层是连续数组。
-
begin()指向第一个字符,end()指向最后一个字符的下一个位置(即_str[_size],终止符位置)。 -
范围
for循环会自动调用begin()和end()。
七、修改操作(增、删、改)
7.1 push_back 和 pop_back
cpp
void push_back(char c) {
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = c;
_str[_size] = '\0';
}
void pop_back() {
assert(_size > 0);
_str[--_size] = '\0';
}
讲解:
-
push_back:容量不足时扩容(初始容量为4,之后每次翻倍)。插入字符后记得添加终止符。 -
pop_back:将_size减1,原位置用'\0'覆盖。必须确保非空。
7.2 append 系列
cpp
string& append(const char* s) {
*this += s;
return *this;
}
string& append(const string& s) {
*this += s;
return *this;
}
string& append(const string& str, size_t subpos, size_t sublen) {
assert(subpos <= str._size);
if (sublen == 0) return *this;
if (subpos + sublen > str._size) sublen = str._size - subpos;
while (_capacity < _size + sublen) {
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
for (size_t i = 0; i < sublen; ++i) {
_str[_size + i] = str._str[subpos + i];
}
_size += sublen;
_str[_size] = '\0';
return *this;
}
讲解:
-
前两个重载直接复用
operator+=,简洁。 -
第三个重载用于追加子串:需处理越界(自动截断),然后逐字符拷贝。可优化为
memcpy。
7.3 operator+= 与 operator+
cpp
string string::operator+(const char* str)
{
string tmp=_str;
tmp += str;
return tmp;
}
string string::operator+(const string& s)
{
string tmp=_str;
tmp += s;
return tmp;
}
string& string::operator +=(const char ch)
{
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
return *this;
}
string& string::operator += (const char* str)
{
if (str == nullptr)
{
return *this;
}
int len = strlen(str);
while(_capacity < _size + len)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
return *this;
}
string& string::operator += (const string& s)
{
while(_capacity < _size + s._size)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
strcpy(_str + _size, s._str);
_size += s._size;
return *this;
}
讲解:
-
operator+=修改自身,返回引用,支持链式操作。 -
operator+返回新对象,不改变原对象。实现上先拷贝构造一个临时对象,再追加。 -
注意
operator+应声明为const成员函数(或非成员函数),以便对右值操作。
7.4 insert ------ 在指定位置前插
cpp
string& insert(size_t pos, const char* s) {
assert(pos <= _size);
size_t len = strlen(s);
if (_capacity < _size + len) reserve(/*扩容*/);
// 向后移动数据,腾出空间
for (size_t i = _size; i >= pos; --i) {
_str[i + len] = _str[i];
}
memcpy(_str + pos, s, len);
_size += len;
return *this;
}
讲解:
-
移动数据时必须从后往前拷贝,避免覆盖。如果使用
memmove则更安全。 -
循环中
i的类型应为size_t,注意下溢问题。通常改用memmove一步完成。 -
插入点
pos可以等于_size(尾部插入),此时等同于append。
7.5 erase ------ 删除一段字符
cpp
string& erase(size_t pos = 0, size_t len = npos) {
assert(pos < _size);
if (len == npos || pos + len >= _size) {
_str[pos] = '\0';
_size = pos;
} else {
for (size_t i = pos + len; i <= _size; ++i) {
_str[i - len] = _str[i];
}
_size -= len;
}
return *this;
}
讲解:
-
如果删除长度
len超出尾部,则直接截断至pos位置。 -
否则,将后面的字符向前移动,覆盖被删除区间。
-
移动时包括终止符,保证字符串仍然正确结束。
7.6 clear
cpp
void clear() {
_str[0] = '\0';
_size = 0;
}
讲解 :只需将第一个字符置为终止符,并重置_size,容量不变。
八、查找与子串
8.1 find ------ 查找子串或字符
cpp
size_t find(const char* s, size_t pos = 0) const {
assert(pos <= _size);
const char* p = strstr(_str + pos, s);
return p ? p - _str : npos;
}
size_t find(char ch, size_t pos = 0) const {
for (size_t i = pos; i < _size; ++i) {
if (_str[i] == ch) return i;
}
return npos;
}
讲解:
-
利用C标准库函数
strstr快速查找子串,效率O(N·M)(BF算法)。 -
返回下标,若未找到返回
npos。 -
字符查找手动循环即可。
8.2 substr ------ 取子串
cpp
string substr(size_t pos = 0, size_t len = npos) const {
assert(pos <= _size);
if (len == npos || pos + len > _size) len = _size - pos;
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; ++i) sub += _str[pos + i];
return sub;
}
讲解:
-
先调整
len,避免越界。 -
构造新字符串,提前
reserve避免多次扩容。 -
逐个字符追加(可改用
memcpy优化)。
九、比较运算符(全局函数)
cpp
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
讲解:
-
定义为非成员函数,支持左右操作数隐式转换(例如
"abc" < s)。 -
使用
strcmp比较字典序,符合常规字符串比较规则。
十、输入输出流接口
10.1 operator>> ------ 读取单词
cpp
istream& operator>>(istream& in, string& str) {
str.clear();
char ch;
// 跳过前导空白字符
while ((ch = in.get()) == ' ' || ch == '\n');
// 读取非空白字符
while (ch != ' ' && ch != '\n') {
str += ch;
ch = in.get();
}
return in;
}
讲解:
-
先清空目标字符串。
-
跳过前导空白(空格、换行等),然后读取字符直到遇到空白
10.2 operator<< ------ 输出
cpp
ostream& operator<<(ostream& out, const string& str) {
for (char ch : str) out << ch;
return out;
}
讲解:直接遍历字符串,逐个输出字符,不输出终止符。
10.3 getline ------ 读取一行
cpp
istream& getline(istream& is, string& str) {
str.clear();
char ch;
while ((ch = is.get()) != '\n') {
str += ch;
}
return is;
}
讲解:读取直到换行符,不存储换行符。
十一、总结与改进建议
通过实现这个string类,我们掌握了:
-
动态内存管理的三大件(构造、拷贝构造、赋值、析构)
-
深拷贝的必要性及实现
-
扩容策略与性能权衡
-
运算符重载的常见模式
-
迭代器的本质与用法