C++标准模板库(STL)——string的模拟实现
C++标准模板库(STL)——string的模拟实现
文章目录
在C++编程中,std::string是最常用的容器之一。你是否好奇过它的底层逻辑,为什么它的操作能如此高效?当我们自己动手写一个功能完备的string类时,该怎样实现呢?
接下来我将模拟string类的一些模块的实现逻辑,希望该文章对你深入学习STL容器有所帮助
1、 整体结构
1) 命名空间
我们需要将模拟实现的string封装在namespace ST中,避免与标准库中的std::string冲突
2) 核心成员对象
- std::string以动态字符数组储存字符串,因此我们需要声明_str,让该指针指向一段空间,用这段空间来存放字符。
- 为了兼容C字符串,ST::string字符串的最后一个字符的下一个位置应该放一个字符’\0’,并且’\0’不计入有效字符长度
- 我们用_size来记录当前字符串中有字符的数量(不包含‘\0‘)
- 我们用_capacity来记录当前开辟的空间大小,可以存放多少个字符(不包含‘\0‘)
/string.h
#pragma once
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
namespace ST
{
class string
{
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
2、 string的迭代器
- 迭代器的作用是访问,遍历字符串,其功能类似于指针
- 为什么不直接用char* 指针,非要用iterator?
因为iterator迭代器是C++容器的通用编译工具,iterator可以遍历string、list、vector等等,iterator为访问容器提供了统一的接口,大家都可以使用。如果直接使用char* 遍历,那就只有string能使用,就会和“容器迭代器“的设计思想脱节
- 为什么不直接用const iterator begin();
因为const iterator指迭代器本身不能修改(指针本身不能修改,但是指针指向的内容还可以修改),我们需要的是,迭代器指向的内容不能修改
- 为什么begin() 返回_str,end() 返回_str + _size?
遵循 C++ 迭代器的 “左闭右开” 原则[begin(), end())
class string
{
public:
/将char* 类型重命名成iterator,用指针功能来模拟迭代器
/普通的迭代器,可以访问修改元素
typedef char* iterator;
/常量迭代器,只能读取元素
typedef const char* const_iterator;
iterator begin()
{
return _str;/指向字符串首字符
}
iterator end()
{
return _str + _size;/指向字符串最后一个字符的下一个位置‘\0’
}
const_iterator begin()const
{
return _str;/const修饰this指针,返回的this->_str不可修改
}
const_iterator end()const
{
return _str + _size;/返回只读字符串尾元素
}
3、 构造和析构
1) 构造函数
- 初始化字符串
- 为什么要给构造函数加默认参数str = “”
这样实现了一个构造函数覆盖了空字符串和非空字符串两种场景,减少代码冗余
- 为什么要_size + 1分配内存?
为了兼容C语言的字符串,保证字符串以‘\0’结尾
- 为什么使用memcpy,而不是strcpy(_str,str)?
strcpy遇到’\0’就会停下来,并不会将’\0’拷贝过去 memcpy 明确了拷贝长度(_size + 1),就会将长度内的字符全拷贝过去
/构造函数
string(const char* str = "")/如果不传参,默认为空字符串
:_size(strlen(str))/strlen不包含‘\0’
{
_capacity = _size;
_str = new char[_size + 1];/size+1体现了为‘\0’预留空间
/将str的_size+1个字符串长度,复制到_str
/为什么不使用strcpy(_str,str);
memcpy(_str, str, _size + 1);
}
2) 拷贝构造函数
- 复制一个已经存在的string对象
- 为什么要自己写swap,而非直接用std::swap?
因为直接使用std::swap会触发三次拷贝构造(a拷贝给临时对象、b拷贝给a、临时对象拷贝给b),效率很低
自定义的swap,只会交换_str(指针)、_size、_capacity三个成员,只进行指针交换,不需要分配新内存、拷贝数据,效率很高
- 为什么使用新写法的拷贝构造呢?
新写法不用考虑抛异常问题,而且代码简洁,代码的复用逻辑强
/为啥要自己写swap
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
/拷贝构造函数s2(s1)
/深拷贝,避免浅拷贝让两个对象指向同一块空间导致析构两次
/新写法
string(const string& s)
{
string tmp(s._str);/先用构造函数构造一个临时对象tmp(深拷贝s的内容)
swap(tmp);/再调用自己写的swap,交换当前对象和tmp的资源
}
/传统写法
/string(const string& s)
/{
/ _str = new char[s._capacity + 1];
/ memcpy(_str, s._str, s._size + 1);
/ _size = s._size;
/ _capacity = s._capacity;
/}
3) 赋值运算符重载
将一个string对象的值,赋值给另一个string对象
/赋值运算符重载
/s2=s1
string& operator=(string tmp)/传值参数,自动调用拷贝构造,深拷贝s1的内容到tmp
{
swap(tmp);/交换s2和tmp的资源
return*this;
}
4) 析构函数
释放动态分配的内存,避免内存泄漏
/析构函数
~string()
{
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
代码测试


4、 大小与容量管理
1) size/reserve
size()
- 返回当前字符数
- 为什么返回 _size 而不是计算 strlen (_str) ?
因为_size是类成员。表示字符串的实际字符个数(不含‘\0’),修改字符串时,_size也会同步变化,直接获取很方便,且时间复杂度为O(1)
每一次使用 strlen, 都要遍历字符到‘\0’才会停止,时间复杂度为O(n)
- 为什么返回类型是 size_t ?
size_t是无符号整数类型,专门用来表示大小,长度,只返回大于等于0的数
- 为什么函数声明后加const?
避免类成员对象被函数乱修改,还能确保const 对象可以被调用 (如 const string s = “hello”; )
//返回当前字符串长度
size_t size()const
{
return _size;
}
reserve(n)
- 开辟n大小的空间
- 为什么需要reserve () ?
一次性将需要的空间开好,避免频繁扩容
- 为什么不修改 _siz e?
再次强调 _capacity,是已经开辟的空间大小,表示最多能储存的字符数
_size,表示的是当前实际储存的字符数
reserve()的语义是 “预留容量”,不改变实际字符数,因此仅更新_capacity,不改变_size
/提前扩容
void reserve(size_t n)
{
if (n > _capacity)/当n大于当前容量才扩容
{
char* tmp = new char[n + 1];/新内存包含‘\0’
memcpy(tmp, _str, _size + 1);/拷贝旧字符串,把‘\0’也带上
delete[]_str;
_str = tmp;
_capacity = n;
}
}
2) 常量npos
npos 用来表示“不存在的位置”或“直到字符串末尾”,添加了 const 防止 npos 被修改

5、 元素访问(下标运算符重载)
- 像数组一样访问字符,并进行越界检查
- 重载普通引用和const引用,编译器会根据实例化对象自动匹配对应版本
/可以修改访问
char& operator[](size_t i)
{
assert(i < _size);/避免越界访问
cout << "char& operator[](size_t i)" << endl;
return _str[i];/返回字符引用
}
/只读访问
const char& operator[](size_t i)const
{
assert(i < _size);
cout << "const char& operator[](size_t i)const" << endl;
return _str[i];
}
6、 修改操作(增删查改)
1) 尾部操作(尾插尾删)
push_back
- 尾插一个字符
- 为什么要先检查容量(_size >= _capacity)?
当_size>=_capacity时,已经没有内存空间来插入数据了,为了避免越界插入,所以得检查
/尾插单个字符
void push_back(char ch)
{
if (_size >= _capacity)/检查容量是否足够
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);/调用reserve扩容
}
_str[_size] = ch;/尾插字符
++_size;/更新长度
_str[_size] = '\0';/维护'\0'结尾
}
append
尾插字符串
/尾插字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)/检查总长度是否超过容量
{
/扩容到“2倍原容量”和“当前长度+待插长度”中的较大值
size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
reserve(newcapacity);
}
memcpy(_str + _size, str, len + 1);/拷贝含‘\0’
_size += len;/更新总长度
}
重载+=
- 推荐使用+=,可以直接+=字符或字符串,操作灵活, 尾插字符/字符串
- 语法糖复用已有的函数,避免代码冗余,保证了逻辑一致
- 返回*this,支持了链式操作(s+=’a’+=’b’)
/+=字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
/+=字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
pop_back
尾删一个字符
/尾删一个字符
void pop_back()
{
assert(_size > 0);/检查字符串是否空
--_size;
_str[_size] = '\0';
}
2) 插入与删除
insert
- 在指定pos位置插入字符/字符串
- 为什么从 end = _size + 1开始 “从后往前” 挪动数据?
从后往前挪动数据,可以避免该字符被覆盖,避免数据丢失
_size+1是‘\0’的下一个位置,先把_str[_size]位置的’\0’,挪到_str[_size+1]的位置,再依次向前,直到end=pos,此时原pos到size位置的字符全都向后挪动一位。_str[pos]的位置被空出,可以安全插入ch。
将‘\0’一起挪动,保证了字符串的完整性
/在pos位置插入字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
size_t end = _size + 1;
while (end > pos)
{
/将pos以及pos之后的字符都向后挪动一位,为ch的插入留空间
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;/插入字符
++_size;/更新长度
return *this;
}
- 为什么用for循环拷贝字符串,而非memcpy?
For循环和memcpy的效果一致,但是for逐个字符的拷贝逻辑,更能贴合自定义string处理字符形式
/在pos位置插入字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
/和尾插字符串的开辟空间思路类似
if (_size + len > _capacity)
{
size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
reserve(newcapacity);
}
size_t end = _size + len;/挪动的终点
while (end > pos + len - 1)
{
/将pos以及pos之后的字符向后挪动len位,为str的插入预留空间
_str[end] = _str[end - len];
--end;
}
/拷贝待插入字符串
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;/更新长度
return *this;
}
erase
- 在指定pos位置开始删除len个字符
- 为什么memmove的拷贝长度是_size + 1 - i ?
_size + 1 - i 是 “需要挪动的字节数”, 从_str[i]到_str[_size],包含‘\0’,保证字符串的完整性
/在pos位置开始删除len个字符
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
/要删的数据大于pos后面的字符个数,pos后边全删掉
if (len == npos || len >= (_size - pos))
{
_size = pos;
_str[_size] = '\0';/直接补‘\0’,无需挪动数据,效率O(1)
}
else
{
size_t i = pos + len;
/部分删除,字符直接向前挪动覆盖原数据
memmove(_str + pos, _str + i, _size + 1 - i);
_size -= len;
}
return *this;
}
3) 查找和子串
find
- 从pos位置开始查找字ch/查找子串str
- 字符查找,遍历pos到_size,返回第一个匹配ch的位置,未找到返回npos
- 子串查找,使用字符串匹配算法,从pos开始查找str的完整匹配,返回起始位置
/查找字符
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;/找不到返回npos
}
/查找字符串
size_t find(const char* str, size_t pos = 0)const
{
/strstr是C标准库提供的字符串查找函数,效率高
const char* p1 = strstr(_str + pos, str);
if (p1 == nullptr)
{
return npos;/找不到
}
else
{
return p1 - _str;/找到了返回引索
}
}
substr
提取从pos位置开始的len个字符组成的子串
/提取子串
string substr(size_t pos, size_t len = npos)const
{
if (len == npos || len >= _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
/将提取出的子串储存到新string对象,+=很方便
for (size_t i = 0; i < len; i++)
{
ret += _str[pos + i];
}
return ret;
}
4) 清空字符串
clear
- 清空字符串内容,但保持容量不变
- 为什么_str[0] = '\0’就能实现清空效果?
字符串的本质是以‘\0’结尾的字符序列,strlen等函数看到‘\0’就会停止运算,其他的字符串操作,比如find也会认为这是一个空字符串
- 为什么不处理_capacity?
_capacity表示最多能存储的字符数,清空内容不影响这个属性,还能为清空后的其他函数操作预留空间
/清空字符串
void clear()
{
_str[0] = '\0';/在开头放终止符
_size = 0;/重置长度为0
}
7、 比较运算符重载
逐字符比较 ASCII 值
/ s1 < s2
/ "hello" "hello" -> false
/ "hellox" "hello" -> false
/ "hello" "hellox" -> true
bool operator<(const string& s) const
{
size_t i1 = 0, i2 = 0;
/遍历到较短字符结束
while (i1 < _size && i2 < s._size)
{
/高位小,整体小
if (_str[i1] < s[i2])
{
return true;
}
/高位大,整体大
else if (_str[i1] > s[i2])
{
return false;
}
else
{
++i1;
++i2;
}
}
/如果循环走完了,至少有一个字符已经遍历完了
/ s还有长度,说明s更大,当前字符串更小
/"hello" "hellox" -> true
return i2 < s._size;
}
bool operator==(const string& s) const
{
size_t i1 = 0, i2 = 0;
/遍历到较短字符串结束
while (i1 < _size && i2 < s._size)
{
/找到不相等字符,直接返回false
if (_str[i1] != s[i2])
{
return false;
}
else
{
++i1;
++i2;
}
}
/必须两个字符串都遍历完才相等,长度内容均相等
return i1 == _size && i2 == s._size;
}
/其他运算符(<=、>、>=、!=)均通过operator<和operator==组合而成
/ 避免重复代码
bool operator<=(const string& s) const
{
return *this < s || *this == s;
}
bool operator>(const string& s) const
{
return!(*this <= s);
}
bool operator>=(const string& s) const
{
return!(*this < s);
}
bool operator!=(const string& s) const
{
return !(*this == s);
}
8、 输入输出运算符重载
operator<<
- 输出string类字符串
- 为什么用for循环逐字符输出,而非直接 out << s.c_str()?
s.c_str()会暴露底层_str指针,而逐字符输出通过operator[]访问字符,完全依赖类的公有接口,不泄露底层存储细节,而且operator[]自带越界断言,会更安全
- 为什么返回ostream&,而不是void?
为了链式输出,比如cout << s1 << s2,本质是(cout << s1) << s2
- 参数为什么是const string& s?
传引用避免了拷贝字符串,提高效率。用const修饰符合operator<<只读的性质
/输出
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];/逐个输出字符
}
return out;/返回流对象,支持链式输出(cout<<s1<<s2)
}
operator<<
- 输入string类字符串
- if (in)判断的意义是什么?
处理 流错误 场景,避免后续push_back无效操作
- 为什么用isspace(ch)判断空白字符,而非直接对比’ ‘和’\n’?
isspace是 C 标准库函数,能识别所有空白字符(包括空格’ ‘、换行’\n’、制表符’\t’、回车’\r’等),覆盖更全面
/输入,跳过空格、换行
istream& operator>>(istream& in, string& s)
{
s.clear();/清空原字符串
char ch;
/利用istream的默认行为 —— 自动跳过所有空白字符
/直接读取第一个有效字符
in >> ch;
if (in)
{
s.push_back(ch);/将第一个非空字符插入ch
/继续读取(包括空白字符),并且判断是否为空
while (in.get(ch) && !isspace(ch))
{
s.push_back(ch);
}
}
return in;/返回流对象,支持链式输入(如cin >> s1 >> s2)
}
getline
- 读取整行
- 为什么getline能读取空格,而operator>>不能?
其实是二者的定位不同,operator>>用于读取 “单词”默认跳过空白
而getline用于读取“整行内容” ,用in.get(ch)读取字符,该函数不跳过任何字符
/读取一行(包含空格,到delim为止)
istream& getline(istream& is, string& str, char delim = '\n')
{
str.clear();
char ch;
/逐个读取字符,直到遇到分隔符或流结束
while (is.get(ch))/ 读取包括空白字符在内的所有字符
{
if (ch == delim)/遇到分隔符,停止读取
{
break;
}
str.push_back(ch);
}
return is;
}
9、 检测代码正确性
其实应该写一段代码,就检测一段代码的正确性,为了方便阅读,我将测试用例都写在一起了
#define _CRT_SECURE_NO_WARNINGS 1
/test.pp
#include"string.h"
/构造拷贝构造赋值
void test1()
{
ST::string s1;
ST::string s2("hello");
ST::string s3(s2);
ST::string s4 = "world";
ST::string s5 = s4;
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
cout << "s5:" << s5 << endl;
}
/迭代器遍历
void test2()
{
ST::string s1("hello world");
ST::string::iterator it = s1.begin();
for (it = s1.begin(); it != s1.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
/尾部操作
void test3()
{
ST::string s1;
s1.push_back('a');
s1.append("bcd");
s1 += "hehe";
s1.pop_back();
cout << s1 <<" " << s1[0] << endl;
}
/插入与删除
void test4()
{
ST::string s1("hello");
s1.insert(2, 'X');
s1.insert(5, "AB");
s1.erase(0, 1);
cout << s1 << endl;
}
/查找与子串
void test5()
{
ST::string s1("abcabcabc");
size_t pos1 = s1.find('b');
size_t pos2 = s1.find('b', 3);/从索引3开始找
size_t pos3 = s1.find("abc");
size_t pos4 = s1.find("abc",4);
cout << pos1 <<" " << pos2 <<" " << pos3 <<" " << pos4 << endl;
ST::string s2 = s1.substr(3, 3);
ST::string s3 = s1.substr(6);
cout << s2 << endl;
cout << s3 << endl;
}
/关系运算符
void test6()
{
ST::string s1("haha");
ST::string s2("hehe");
ST::string s3("xixixi");
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;
cout << (s1 < s3) << endl;
}
void test7()
{
ST::string s1("hello world");
cout << s1.size() << endl;
s1.clear();
cout << s1.size() << endl;
s1 += "hehe";
cout <<s1<<" " << s1.size() << endl;
}
/输入输出
void test8()
{
ST::string s1, s2;
/输入hhh hhh,只输出hhh,跳过空格
/cin >> s1;
/cout << s1 << endl;
/输入hhh hhh,输出hhh hhh,不跳过空格
getline(cin, s2);
cout << s2 << endl;
}
int main()
{
/test1();
/test2();
/test3();
/test4();
/test5();
/test6();
/test7();
test8();
return 0;
}
10、完整代码
string.h
#pragma once
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
namespace ST
{
class string
{
public:
//将char* 类型重命名成iterator,用指针功能来模拟迭代器
//普通的迭代器,可以访问修改元素
typedef char* iterator;
//常量迭代器,只能读取元素
typedef const char* const_iterator;
iterator begin()
{
return _str;//指向字符串首字符
}
iterator end()
{
return _str + _size;//指向字符串最后一个字符的下一个位置‘\0’
}
const_iterator begin()const
{
return _str;//const修饰this指针,返回的this->_str不可修改
}
const_iterator end()const
{
return _str + _size;//返回只读字符串尾元素
}
//构造函数
string(const char* str = "")//如果不传参,默认为空字符串
:_size(strlen(str))//strlen不包含‘\0’
{
_capacity = _size;
_str = new char[_size + 1];//_size+1体现了为‘\0’预留空间
//将str的_size+1个字符串长度,复制到_str
//为什么不使用strcpy(_str,str);
memcpy(_str, str, _size + 1);
}
//为啥要自己写swap
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//拷贝构造函数s2(s1)
//深拷贝,避免浅拷贝让两个对象指向同一块空间导致析构两次
//新写法
string(const string& s)
{
string tmp(s._str);//先用构造函数构造一个临时对象tmp(深拷贝s的内容)
swap(tmp);//再调用自己写的swap,交换当前对象和tmp的资源
}
////传统写法
//string(const string& s)
//{
// _str = new char[s._capacity + 1];
// memcpy(_str, s._str, s._size + 1);
// _size = s._size;
// _capacity = s._capacity;
//}
//赋值运算符重载
//s2=s1
string& operator=(string tmp)//传值参数,自动调用拷贝构造,深拷贝s1的内容到tmp
{
swap(tmp);//交换s2和tmp的资源
return*this;
}
//析构函数
~string()
{
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
//返回当前字符串长度
size_t size()const
{
return _size;
}
//提前扩容
void reserve(size_t n)
{
if (n > _capacity)//当n大于当前容量才扩容
{
char* tmp = new char[n + 1];//新内存包含‘\0’
memcpy(tmp, _str, _size + 1);//拷贝旧字符串,把‘\0’也带上
delete[]_str;
_str = tmp;
_capacity = n;
}
}
static const size_t npos;
//可以修改访问
char& operator[](size_t i)
{
assert(i < _size);//避免越界访问
return _str[i];//返回字符引用
}
//只读访问
const char& operator[](size_t i)const
{
assert(i < _size);
return _str[i];
}
//尾插单个字符
void push_back(char ch)
{
if (_size >= _capacity)//检查容量是否足够
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);//调用reserve扩容
}
_str[_size] = ch;//尾插字符
++_size;//更新长度
_str[_size] = '\0';//维护'\0'结尾
}
//尾插字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)//检查总长度是否超过容量
{
//扩容到“2倍原容量”和“当前长度+待插长度”中的较大值
size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
reserve(newcapacity);
}
memcpy(_str + _size, str, len + 1);//拷贝含‘\0’
_size += len;//更新总长度
}
//+=字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//+=字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
//尾删一个字符
void pop_back()
{
assert(_size > 0);//检查字符串是否空
--_size;
_str[_size] = '\0';
}
//在pos位置插入字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
size_t end = _size + 1;
while (end > pos)
{
//将pos以及pos之后的字符都向后挪动一位,为ch的插入留空间
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;//插入字符
++_size;//更新长度
return *this;
}
//在pos位置插入字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
//和尾插字符串的开辟空间思路类似
if (_size + len > _capacity)
{
size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
reserve(newcapacity);
}
size_t end = _size + len;//挪动的终点
while (end > pos + len - 1)
{
//将pos以及pos之后的字符向后挪动len位,为str的插入预留空间
_str[end] = _str[end - len];
--end;
}
//拷贝待插入字符串
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;//更新长度
return *this;
}
//在pos位置开始删除len个字符
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
//要删的数据大于pos后面的字符个数,pos后边全删掉
if (len == npos || len >= (_size - pos))
{
_size = pos;
_str[_size] = '\0';//直接补‘\0’,无需挪动数据,效率O(1)
}
else
{
size_t i = pos + len;
//部分删除,字符直接向前挪动覆盖原数据
memmove(_str + pos, _str + i, _size + 1 - i);
_size -= len;
}
return *this;
}
//查找字符
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;//找不到返回npos
}
//查找字符串
size_t find(const char* str, size_t pos = 0)const
{
//strstr是C标准库提供的字符串查找函数,效率高
const char* p1 = strstr(_str + pos, str);
if (p1 == nullptr)
{
return npos;//找不到
}
else
{
return p1 - _str;//找到了返回引索
}
}
//提取子串
string substr(size_t pos, size_t len = npos)const
{
if (len == npos || len >= _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
//将提取出的子串储存到新string对象,+=很方便
for (size_t i = 0; i < len; i++)
{
ret += _str[pos + i];
}
return ret;
}
//清空字符串
void clear()
{
_str[0] = '\0';//在开头放终止符
_size = 0;//重置长度为0
}
// s1 < s2
// "hello" "hello" -> false
// "hellox" "hello" -> false
// "hello" "hellox" -> true
bool operator<(const string& s) const
{
size_t i1 = 0, i2 = 0;
//遍历到较短字符结束
while (i1 < _size && i2 < s._size)
{
//高位小,整体小
if (_str[i1] < s[i2])
{
return true;
}
//高位大,整体大
else if (_str[i1] > s[i2])
{
return false;
}
else
{
++i1;
++i2;
}
}
//如果循环走完了,至少有一个字符已经遍历完了
// s还有长度,说明s更大,当前字符串更小
//"hello" "hellox" -> true
return i2 < s._size;
}
bool operator==(const string& s) const
{
size_t i1 = 0, i2 = 0;
//遍历到较短字符串结束
while (i1 < _size && i2 < s._size)
{
//找到不相等字符,直接返回false
if (_str[i1] != s[i2])
{
return false;
}
else
{
++i1;
++i2;
}
}
//必须两个字符串都遍历完才相等,长度内容均相等
return i1 == _size && i2 == s._size;
}
//其他运算符(<=、>、>=、!=)均通过operator<和operator==组合而成
// 避免重复代码
bool operator<=(const string& s) const
{
return *this < s || *this == s;
}
bool operator>(const string& s) const
{
return!(*this <= s);
}
bool operator>=(const string& s) const
{
return!(*this < s);
}
bool operator!=(const string& s) const
{
return !(*this == s);
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
//在类外定义npos
const size_t string::npos = -1;
//输出
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];//逐个输出字符
}
return out;//返回流对象,支持链式输出(cout<<s1<<s2)
}
//输入,跳过空格、换行
istream& operator>>(istream& in, string& s)
{
s.clear();//清空原字符串
char ch;
//利用istream的默认行为 —— 自动跳过所有空白字符
//直接读取第一个有效字符
in >> ch;
if (in)
{
s.push_back(ch);//将第一个非空字符插入ch
//继续读取(包括空白字符),并且判断是否为空
while (in.get(ch) && !isspace(ch))
{
s.push_back(ch);
}
}
return in;//返回流对象,支持链式输入(如cin >> s1 >> s2)
}
//读取一行(包含空格,到delim为止)
istream& getline(istream& is, string& str, char delim = '\n')
{
str.clear();
char ch;
// 逐个读取字符,直到遇到分隔符或流结束
while (is.get(ch))// 读取包括空白字符在内的所有字符
{
if (ch == delim)//遇到分隔符,停止读取
{
break;
}
str.push_back(ch);
}
return is;
}
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
//test.pp
#include"string.h"
//构造拷贝构造赋值
void test1()
{
ST::string s1;
ST::string s2("hello");
ST::string s3(s2);
ST::string s4 = "world";
ST::string s5 = s4;
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
cout << "s5:" << s5 << endl;
}
//迭代器遍历
void test2()
{
ST::string s1("hello world");
ST::string::iterator it = s1.begin();
for (it = s1.begin(); it != s1.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
//尾部操作
void test3()
{
ST::string s1;
s1.push_back('a');
s1.append("bcd");
s1 += "hehe";
s1.pop_back();
cout << s1 <<" " << s1[0] << endl;
}
//插入与删除
void test4()
{
ST::string s1("hello");
s1.insert(2, 'X');
s1.insert(5, "AB");
s1.erase(0, 1);
cout << s1 << endl;
}
//查找与子串
void test5()
{
ST::string s1("abcabcabc");
size_t pos1 = s1.find('b');
size_t pos2 = s1.find('b', 3);//从索引3开始找
size_t pos3 = s1.find("abc");
size_t pos4 = s1.find("abc",4);
cout << pos1 <<" " << pos2 <<" " << pos3 <<" " << pos4 << endl;
ST::string s2 = s1.substr(3, 3);
ST::string s3 = s1.substr(6);
cout << s2 << endl;
cout << s3 << endl;
}
//关系运算符
void test6()
{
ST::string s1("haha");
ST::string s2("hehe");
ST::string s3("xixixi");
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;
cout << (s1 < s3) << endl;
}
void test7()
{
ST::string s1("hello world");
cout << s1.size() << endl;
s1.clear();
cout << s1.size() << endl;
s1 += "hehe";
cout <<s1<<" " << s1.size() << endl;
}
//输入输出
void test8()
{
ST::string s1, s2;
//输入hhh hhh,只输出hhh,跳过空格
//cin >> s1;
//cout << s1 << endl;
////输入hhh hhh,输出hhh hhh,不跳过空格
getline(cin, s2);
cout << s2 << endl;
}
int main()
{
//test1();
//test2();
//test3();
//test4();
//test5();
//test6();
//test7();
test8();
return 0;
}
更多推荐


所有评论(0)