标准库的 std::string 用起来很方便,但你真的了解它背后的内存管理、深拷贝、写时拷贝、迭代器、动态扩容吗?
为了彻底搞懂这些底层机制,我造了一个自己的 string 类 ------ 取名为 jmp::string。
从构造、析构、拷贝、赋值,到增删改查、迭代器、流输入输出
这篇文章会完整记录我的实现细节、踩过的坑(都已经在代码注释里标注出来了),以及最终跑通测试时的快感。希望也能帮你把 C++ 对象的生命周期和运算符重载看得更通透。
一、基础管理(构造、析构、拷贝、赋值、交换)
|----------|----------------------------------------------------------|
| 构造函数 | string(const char* str = ""):分配足够空间,复制字符串。支持缺省参数(空字符串) |
cpp
string::string(const char* str )//初始化的内容
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size+1];//+1为\0腾出一块空间
strcpy(_str, str);//char * strcpy ( char * destination, const char * source );
}
|----------|--------------------------------|
| 析构函数 | ~string():释放动态内存,指针置空,大小容量归零 |
cpp
string::~string()
{
delete[] _str;//匹配使用
_str = nullptr;
_size = _capacity = 0;//析构函数的作用不能忘
}
|-----------|--------------------------------------------------------|
| 赋值运算符 | operator=(string s):传值(copy-and-swap 惯用法),通过交换实现安全赋值 |
|--------|-------------------------------------------------|
| 交换 | swap(string& s):交换 _str、_size、_capacity |
我这里配合了 内部的swap实现 代码一并贴出
cpp
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& string::operator=(string s)
{
swap(s);
return *this;
}
operator= 利用传值生成临时副本,然后交换,天然处理自赋值且异常安全
二、容量与大小
|----------|------------|
| size() | 返回 _size |
cpp
size_t string::size() const
{
return _size;
}
|---------------------|--------------------------------------|
| reserve(size_t n) | 仅扩容(不缩小),保留原内容,保证至少有 n 容量(不含 \0) |
cpp
void string::reserve(size_t n)//只放大不缩小
{
if (n <= _capacity) return;//只放大不缩小 并不意味着要报错 只是没有强制性;
char* new_str = new char[n + 1];//多给一个空间给'\0'
if (_str)
{
memcpy(new_str, _str, _size + 1); // 拷贝原有内容(包括\0'))
delete[] _str;
}
_str = new_str;//可以采用先移动 再销毁 然后再次指向移动的新位置 从而达到改变this内容的效果
_capacity = n;
}
|-----------|---------------------------------------------|
| clear() | 清空内容,仅将 _size 置 0,并在 _str[0] 置 '\0' |
cpp
void string::clear()
{
_size = 0;
_str[_size] = '\0';
}
三、元素访问与迭代器
|--------------|----------------------------|
| operator[] | 返回指定下标字符的引用(提供 const 版本) |
cpp
char& string::operator[](size_t i)
{
assert(i < _size);
return *(_str + i);
}
const char& string::operator[](size_t i) const
{
assert(i < _size);
return *(_str + i);
}
|---------------------|--------------------------------|
| begin() / end() | 返回 char* 迭代器(普通与 const 版本) |
cpp
string::iterator string::begin()
{
return _str;
}
string::iterator string::end()
{
return _str + _size;//表示最后一个有效数据的后一个位置
}
string::const_iterator string::begin() const
{
return _str;
}
string::const_iterator string::end() const
{
return _str + _size;//表示最后一个有效数据的后一个位置
}
四、修改操作(增、删、改)
|----------------------|----------------------------------|
| push_back(char ch) | 尾部添加字符,容量不足时翻倍扩容(注意已修正内存分配 +1) |
cpp
void string::push_back(char ch)
{
if (_size == _capacity)
{
size_t _newcapacity = (_capacity == 0 ? 4 : _capacity * 2);
char* new_str = new char[_newcapacity+1];
if (_str) //有字符就得拷贝到新的空间内
{
strcpy(new_str, _str); // 拷贝原数据(包括结尾的\0)
delete[] _str;
}
_str = new_str;
_capacity = _newcapacity;
}
_str[_size] = ch;//把\0的位置填上ch
_str[_size + 1] = '\0';//ch后面把\0加上
_size++;
}
|--------------|-------------------------------|
| pop_back() | 删除尾部字符,仅将 _size-- 并置 '\0' |
cpp
void string::pop_back()
{
assert(_size > 0);//删除的话要确保有内容可删 否则断言报错
_size--;
_str[_size] = '\0';
}
|---------------------------|---------------|
| append(const char* str) | 追加 C 字符串,动态扩容 |
cpp
void string::append(const char* str)
{
if (str == nullptr) return;//这些都是细节
size_t len = strlen(str);
if (len == 0) return;//这些都是细节
if (_size + len > _capacity)
{
size_t new_capacity = (_size + len) * 2;// 或简单_size + len
char* new_str = new char[new_capacity + 1];//倘若空间不够的问题要记得考虑
if (_str)
memcpy(new_str, _str, _size);
delete[] _str;
_str = new_str;
_capacity = new_capacity;
}
memcpy(_str + _size, str, len + 1);
_size += len;
}
|-------------------------------------------------------|---------------------------|
| operator+=(char ch) / operator+=(const char* str) | 复用 push_back / append |
cpp
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
|-------------------------------|------------------|
| insert(size_t pos, char ch) | 在指定位置插入字符,后移后续字符 |
cpp
string& string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
reserve(_size + 1);
//memmove(_str + pos + 1,_str + pos,_size - pos + 1);// 后移包括 \0'
//应该从后往前挪动 否则会造成覆盖
for (size_t i = _size; i > pos; i--)
{
_str[i + 1] = _str[i];
}
_str[pos] = ch;
_size++;
return *this;
}
|---------------------------------------|----------------------------|
| insert(size_t pos, const char* str) | 插入 C 字符串,使用 memmove 批量后移 |
cpp
string& string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
reserve(_size + len + 1);//为\0留空间
memmove(_str + pos + len, _str + pos, _size - pos + 1);// 后移包括 \0'
//把_str+pos 后面的所有字符(包括\0) 都从_str + pos + len这个位置开始填充(移动覆盖)
memcpy(_str + pos, str, len);
_size+=len;
return *this;
}
|---------------------------------|-------------------------------------------|
| erase(size_t pos, size_t len) | 删除从 pos 开始的 len 个字符(支持 npos 表示删到末尾) |
cpp
string& string::erase(size_t pos, size_t len)
{
assert(pos <= _size);
if (npos == len || pos + len > _size)
{
len = _size - pos;
}
if (len == 0) return *this;
memmove(_str + pos, _str + pos + len, _size - pos-len + 1);
_size -= len;
return *this;
}
五、查找与子串
|-----------------------------|-----------------------------|
| find(char ch, size_t pos) | 从 pos 开始查找字符,返回下标或 npos |
cpp
size_t string::find(char ch, size_t pos) const
{
assert(pos <= _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
|-------------------------------------|----------------------------------|
| find(const char* str, size_t pos) | 从 pos 开始查找子串,利用 strncmp 逐位比较 |
cpp
size_t string::find(const char* str, size_t pos)const
{
if (str == nullptr) return npos;
assert(pos <= _size);
size_t len = strlen(str);
if (len == 0) return pos;
if (pos + len > _size) return npos;
for (size_t i = pos; i <= _size - len; ++i)
{
if (strncmp(_str + i, str, len) == 0)//比较前n个字符是否相同
{
return i;
}
}
return npos;
}
|----------------------------------|----------------------|
| substr(size_t pos, size_t len) | 提取子串,自动处理长度溢出(截断至末尾) |
cpp
string string::substr(size_t pos, size_t len) const
{
assert(pos <= _size);
size_t count = len;
if (count == npos || pos + count > _size)
{
count = _size - pos;//考虑没有传值或者传值过头的情况 以便正常遍历
}
string new_str;
new_str.reserve(count);
for (size_t i = 0; i < count; i++)
{
new_str += _str[pos+i];
}
return new_str;
}
六、比较运算符(全部内联)
所有比较运算符均基于 strcmp 实现:
==, !=, <, <=, >, >=
cpp
bool jmp::string::operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
bool jmp::string::operator<=(const string& s) const
{
return strcmp(_str, s._str) <= 0;
}
bool jmp::string:: operator>(const string& s) const
{
return strcmp(_str, s._str) > 0;
}
bool jmp::string::operator>=(const string& s) const
{
return strcmp(_str, s._str) >=0;
}
bool jmp::string::operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
bool jmp::string::operator!=(const string& s) const
{
return strcmp(_str, s._str) != 0;
}
七、输入输出流
|--------------|-----------------|
| operator<< | 输出 c_str() 内容 |
|--------------|-----------------------------|
| operator>> | 简单实现(读取单词到临时缓冲区再赋值,生产中应更健壮) |
cpp
ostream& jmp::operator<<(ostream& out, const string& s)
{
out << s.c_str();//输出就直接输出字符串类型即可
return out;
}
istream& jmp::operator>>(istream& in, string& s)
{
// 先清空,然后读入到临时缓冲区,再赋值给 s
// 简单实现:in >> s._str 不安全。可以用 getline 或临时 string
char buf[1024];
in >> buf; // 可能越界
s = buf;
return in;
}
|-----------|---------------------------------|
| getline | 读取一行直到分隔符,使用 is.get(ch) 逐字符添加 |
cpp
istream& jmp::getline(istream& is, string& str, char delim)
{
str.clear();
char ch;
while (is.get(ch))
{
if (ch == delim)
{
break;
}
str.push_back(ch);//每次读取一个字符都尾插上去
}
return is;
}
八、辅助工具
c_str():返回底层 char*,兼容 C 接口。
cpp
const char* string::c_str() const
{
return _str;//本质是为了调用函数来得到_str的内容 因为正常不能访问私有内容
}
swap(x, y) 全局函数:调用成员 swap,通过 ADL 匹配。
这里使用了内部的匹配使用
cpp
void jmp::swap(string& x, string& y)
{
x.swap(y);
}
完结
九、我的gitee仓库
https://gitee.com/jiangmingpeng0716/c-learning-process
这里已经具备一个入门级 string 类的核心功能 其他的 功能暂且不做实现
好的 我的string 就是这样