【C++世界之string模拟实现】
string介绍
C++ string:在C语言基础上,提供了一个 类的封装,内部通常包含:
一个指向动态内存的 char*(保存字符串内容);
一个记录当前字符串长度的 _size;
一个记录已分配空间大小的 _capacity(一般比 _size 大,用于减少频繁扩容)
string 会 自动管理内存:当字符串变长时,会重新分配更大的空间,并把原来的内容复制过去。
为了效率,string 通常采用 按需扩容:容量不足时,不是只增加 1 个,而是成倍增加(比如翻倍)。
std::string 提供了很多便利的功能:
构造:直接用字面量初始化(string s = “hello”;)。
访问:下标 [],迭代器,at()。
修改:append、insert、erase、replace。
比较:==、!=、< 等重载运算符。
拼接:支持 +、+=。
安全性:自动管理内存,避免手动 new/delete。
string用法及常用函数
string的文档
初始化
int main()
{string s1;string s2("张三");string s3("hello world");string s4(10, '*');string s5(s2);string s6(s3, 6, 5);//区间初始化 6:pos 5:len
}
赋值
int main()
{s1 = s2;s1 = "1111";s1 = '2';
}
增(各种插入)
push_back
string s1("hello");
//尾插一个字符
s1.push_back(' ');
append
//尾插一个字符串
s1.append("world");
//插入一个字符串前几个字符
s1.append("hello world",5); //插入字符串的前五个字符
//插入一个string区间
s1.append(s2,3,6);// pos: 3 len: 6
重载+=
s1 += ' ';
s1 += "world";
s1 += s1;
insert
s1.insert(0, "hello");//pos + 字符串(指定位置插入字符串)s1.insert(0,"hello world",5);// pos + 字符串 + len(插入字符串区间)s1.insert(0,s2);//pos + strings1.insert(0, s2,2,5);//pos + string + string的区间s1.insert(0,10,'x');// pos + n(个数) + chars1.insert(s1.beign(),10,'x'); //迭代器
删
erase
int main()
{string s1("hello world");s1.erase(5, 1);//pos + ncout << s1 << endl;s1.erase(5);//pos -> endcout << s1 << endl;string s2("hello world");s2.erase(0,1);cout << s2 << endl;s2.erase(s2.begin());//迭代器cout << s2 << endl;s2.erase(s2.begin(),s2.end());//全删s2 = '0';cout << s2 << endl;
}
实际效果:
迭代器
介绍:
迭代器(Iterator)就是 用来遍历容器元素的一种对象。
它相当于一个“指针”,但比普通指针更灵活、更安全,能屏蔽底层容器的具体实现方式。
比如 vector、list、map 这些 STL 容器,内部结构不一样,但都能用迭代器来统一遍历。
功能:
输入迭代器:只读,单向移动(常见于 istream_iterator)。
输出迭代器:只写,单向移动(常见于 ostream_iterator)。
前向迭代器:可读可写,单向遍历。
双向迭代器:可读可写,能前进和后退(如 list 的迭代器)。
随机访问迭代器:支持 + - [] < > 等操作(如 vector、deque 的迭代器)。
常见几种迭代器
//iterator
//const_iterator
//reverse_iterator(反向)
//const_reverse_iterator
打印
string::iterator it = s1.begin();while (it != s1.end()){(*it)--;++it;}it = s1.begin();while (it != s1.end()){cout << *it << " ";++it;}
范围for
范围for底层就是迭代器,不过比较简单
打印:
for (auto ch : s1)//for(char ch:s1){cout << ch << " ";}cout << endl;
修改:(需要引用)
for (auto& ch : s1){ch++;}
迭代器+算法容器
反转:
reverse(v.begin(), v.end());reverse(lt.begin(), lt.end());
排序:
sort(s1.begin(),s1.end());
…………
reserve & resize
reserve 开空间
//开空间s1.reserve(100);cout << s1.size() << endl;cout << s1.capacity() << endl;
resize 开空间+初始值
//开空间+填值初始化s1.resize(200,'x');cout << s1.size() << endl;cout << s1.capacity() << endl;//扩容s1.resize(20);//可以删除数据,不缩容cout << s1.size() << endl;cout << s1.capacity() << endl;
上述为基本常用功能·,其他接口自行去文档查找。
模拟实现
前提准备
namespace bit
{
class string
{private:
size_t _size;
size_t _capacity;
char* _str;
static size_t npos;public:
………………
}
size_t string:: npos = -1;
}
构造函数
字符串构造
string(const char* str = "")//相当于\0:_size(strlen(str)),_capacity(_size)
{_str = new char[_capacity + 1];memcpy(_str, str,_size+1);// 如果 只拷贝_size 则 _str[_size] = '\0';
}
我们也可以使用for循环逐个拷贝进去
for (size_t i = 0; i < _size; i++){_str[i] = s[i];}
_str[_size] = '\0';
字符构造
string(const char* str, size_t n):_size(n),_capacity(_size)
{_str = new char[_capacity + 1];memcpy(_str, str, n);_str[_size] = '\0';//注意添加
}
PS1:为什么一个是_size + 1 一个是 _size。
二者差异在memcpy的拷贝区间,因为s自带’\0’,所以拷贝到_size + 1.
但是字符没有‘\0’,所以要手动添加。
PS2: 为何不推荐使用strcpy?
1. 若字符串中包含’\0’字符(虽然这种情况较为少见),拷贝过程会提前终止,导致无法完整复制。
2. 为保持代码一致性及美观性,建议避免使用该函数。
拷贝构造函数
传统写法:
string(const string& str): _size(str._size), _capacity(str._capacity)
{_str = new char[_capacity + 1];memcpy(_str, str._str, _size + 1);
}
现代写法:(不推荐)
string(const string& str):_str(nullptr),_size(0),_capacity(0)
{string tmp(str._str);swap(tmp);
}
PS:
若 _str、_size 和 _capacity 未被初始化,可能导致以下问题:
访问非法内存地址(野指针)
_size 和 _capacity 值异常,引发数组越界
将对象初始化为空状态可确保:
swap 操作时数据始终有效
避免未初始化值带来的风险
重载=
传统写法:
string& operator=(const string& str){if (this == &str) return *this;delete[]_str;_str = new char[str._capacity + 1];memcpy(_str, str._str, str._size + 1);_size = str._size;_capacity = str._capacity;return *this;}
现代写法:
void swap(string& str)
{std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);//不能直接交换,会无限循环
}string& operator=(string str)//形参(传值)拷贝了一个str
{if (this == &str) return *this;swap(str);//this->swap(tmp);return *this;
}
PS1: 为什么不能直接交换(this)和(str)?
由于std::swap在交换this和str时会涉及赋值操作,这会触发重载的=运算符,导致无限递归。
PS2: 为什么要传递形参str?
形参str是实参的副本,通过交换这个副本既能实现赋值目的,又不会影响原始实参的值。
析构函数
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
重载[]
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 char* c_str()const { return _str;}
PS:
采用字符串形式输出,便于使用字符串处理函数。
直接支持打印功能,无需专门重载流输出和流插入操作符。
cout << s.c_str() << endl;
开空间
reserve
void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];memcpy(tmp, _str, n+1);delete[] _str;_str = tmp;_capacity = n;}
}
PS:
调用reserve扩容时,需要先将原有数据拷贝到新空间,且仅当n大于当前_capacity时执行才有实际意义。
resize
void resize(size_t n,char ch = '\0')
{if (n < _size){_size = n;_str[_size] = '\0';}else{reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}
}
PS:使用resize缩小空间时需手动添加’\0’进行截断。
增
push_back
void push_back(char c)
{if (_size == _capacity){//2倍扩容size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(newcapacity);}_str[_size++] = c;_str[_size] = '\0';//插入字符需要手动补充'\0'
}
append
void append(const char* s)
{size_t len = strlen(s);if (_size + len > _capacity){reserve(_size + len);}memcpy(_str + _size, s,len+1);_size += len;
}
void append(const string& str) { append(str.c_str());}
PS:插入流程如下:首先检查空间是否合适,接着通过reserve调整空间大小,最后执行数据插入操作。
+=
string& operator+=(char c)
{push_back(c);return *this;
}string& operator+=(const char* s)
{append(s);return *this;
}string& operator+=(string& str)
{append(str);return *this;
}
PS:实现+=比较简单,主要是函数push_back 和 append 的复用。
insert
void insert(size_t pos,size_t n ,char ch)
{assert(pos <= size());if (size() + n > _capacity){reserve(size() + n);}//挪动数据int end = _size;*** end 也需要是 int 类型for (int i = end; i >=(int)pos; i--){_str[i + n] = _str[i];}//拷贝for (size_t i = 0; i < n; i++){_str[pos + i] = ch;}_size += n;
}
PS:为什么要将pos强制转换为int类型?
size_t 类型始终为非负数。
当i递减到0时,再执行–i操作不会得到-1,
而是会回绕成一个极大的无符号数值(例如18446744073709551615)。
这将导致死循环,并引发内存访问异常。(非常重要) 其他方法我认为比较难以理解故不作解释。
删
erase
void erase(size_t pos, size_t len = npos)
{assert(pos < size());if (len == npos || len + pos >= _size){_size = pos;_str[_size] = '\0';}else{size_t end = pos + len;while (end <= _size){_str[pos++] = _str[end++];}_size -= len;}
}
PS:
检查位置:assert(pos < size()); 确保删除起点合法。
判断删除范围:
如果删到末尾(len == npos 或者超出范围),直接把 _size 截到 pos。
否则就把后面的内容往前搬。
更新 _size:调整字符串长度并补 ‘\0’ 结尾(end==_size 可以添加到’\0’)。
迭代器
begin
end
iterator begin(){ return _str;}iterator end(){ return _str+size();}const_iterator begin()const { return _str;}const_iterator end()const{ return _str+size();}
PS:STL中的end()返回的是容器末尾元素的下一个位置,因此这里使用_str+_size表示。
PS:STL迭代器分为只读迭代器和可读写迭代器两种类型,因此需要区分这两类。
查
find
size_t find(const char* s, size_t pos = 0)const
{const char* ptr = strstr(_str + pos, s);return ptr ? (ptr - _str) : npos;
}size_t find(string& str, size_t pos = 0)const
{return find(str.c_str(), pos);
}
字符串匹配
size_t find(const char* s, size_t pos = 0)const{size_t len1 = _size;size_t len2 = strlen(s);if (len2 == 0) return pos;if (len1 < pos || len1 < len2 + pos) return npos;for (int i = pos; i < len1; i++){size_t j = 0;while (j < len2 && _str[i + j] == s[j])j++;if (j == len2) return i;}
PS:
字串提取
substr
string substr(size_t pos = 0, size_t len = npos) const
{assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);for (size_t i = 0 ; i < n; i++){tmp += _str[pos + i];}return tmp;
}
PS:
检查位置合法:assert(pos < _size);
确定实际截取长度:
如果 len == npos 或超范围,就取 _size - pos。
否则用传入的 len。
创建结果字符串:预留空间 reserve(n)。
逐个字符拷贝:把 _str[pos ~ pos+n-1] 复制进 tmp。
返回子串。
总结
下文是vector的讲解和模拟实现,如果有余力将完成string的其余实现和部分题目。