C++基础入门:string类深度剖析(万字解析保姆级教程,干货满满)

◆博主名称:少司府
欢迎来到少司府的博客☆*: .。. o(≧▽≦)o .。.:*☆
⭐数据结构系列个人专栏:
⭐C++基础个人专栏:
⭐琢玉成器终有时,笔底生花夺锦归
目录
一、标准库中的string类
1.1 string类
点击此链接可以转跳到官方文档。
在使用string类时,必须包含头文件<string>,以及命名空间std
1.2 string类的实例化

如图,s1不传参数调默认构造,没有赋值;s2传入字符串 hello world ,s3调用拷贝构造s2;s4拷贝构造s2,从下标为6的字符开始,向后拷贝6个字符,不包括\0;s5代表从第一个字符开始向后拷贝5个字符。
1.3 string类的遍历
1.3.1 下标+[]
和数组一样,string类可以利用下标进行遍历。
string s1;
string s2("hello world");
cout << s1 << s2 << endl; //下标+[]
for (int i = 0; i < s2.size(); i++)
{
cout << s2[i] << ' ';
}
cout << endl;
如图,其中size() 是string类的一个接口,返回该对象中有效字符的个数。
1.3.2 迭代器 iterator
1)、任何容器都有自己的迭代器
2)、迭代器使用时需要声明类域
3)、string类中,接口begin() 返回最初位置的迭代器(下标为0的位置);end() 返回最后一个有效字符的下一个位置( \0 的位置)。
4)、可以把迭代器类比为指针,需要 " 解引用 " 才能显示原来的元素
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << ' ';
++it;
}
cout << endl;
链表 list 也有自己的迭代器,如:
list<int> lt = { 1,2,3,4,5 };
list<int>::iterator lit = lt.begin();
while (lit != lt.end())
{
cout << *lit << ' ';
++lit;
}
cout << endl;
此处需要包含头文件<list>。
1.3.3 范围for与auto
auto的使用
1)、auto是一个关键字,auto声明的变量必须由编译器在编译阶段推导得到。
2)、auto声明指针类型时,用auto和auto*没有区别;auto声明引用时,必须用auto&。
3)、在同一行声明多个变量时,必须是相同类型,因为编译器实际是对第一个类型进行推导,然后用推导出来的类型定义其他变量。
4)、auto不能作为函数的参数,但可以作为返回值,谨慎使用
5)、auto不能直接用于声明数组。
int a=10;
auto b=a; // 推导a的类型给b
cout << typeid(b).name() << endl; // 打印b的类型
auto aa=1,bb=2;
auto cc=3,dd=4.0; // error,同一行内,auto只能推导同一类型
范围for
1)、范围for,实际上是用auto来简化代码
2)、auto自动推导类型,自动迭代,自动判断结束,C++11支持,底层就是迭代器
for (auto ch : s2)
{
cout << ch << ' '; //ch就是s2的拷贝,修改ch不改变s2
}
cout << endl;
for (auto& ch : s2) //ch是s2的别名
{
ch += 2;
}
cout << s2 << endl;
如图,上述代码运行结果如下:

1.4 const 迭代器与反向迭代器
反向迭代器是 reverse_iterator ,需要调用的接口改为 rbegin() 和 rend() 。
string::reverse_iterator rit = s2.rbegin();
while (rit != s2.rend())
{
cout << *rit << ' ';
++rit; // ++是倒着走的
}
cout << endl;
const迭代器:
1)、对于用const 修饰的string类对象,需要用到const迭代器
2)、const迭代器和反向迭代器可以合在一起,const反向迭代器
const string s3("hello world");
string::const_iterator cit = s3.cbegin(); // const迭代器,不能修改指向的内容
while (cit != s3.end())
{
cout << *cit << ' ';
++cit;
}
cout << endl;
string::const_reverse_iterator rcit = s3.rbegin();
while (rcit != s3.rend())
{
cout << *rcit << ' ';
++rcit;
}
cout << endl;
如图,分别是const迭代器和const反向迭代器。
其中,C++11规定,cbegin返回对象是const类型迭代器,用begin也可以。
下面是运行结果:

二、capacity 相关接口
2.1 lenth() 与 size()
string s2("hello world");
cout << s2.length() << endl;
cout << s2.size() << endl;
都是返回有效字符的个数,两者没区别,但我们用size,length属于时代产物。
2.2 max_size()
cout << s2.max_size() << endl;
该接口返回 string 能开的最大的空间。
2.3 capacity()
cout << s2.capacity() << endl;
返回 s2 的容量空间,但是不包括\0,所以实际容量比打印出来的多1。
2.4 capacity 容量变化
我们先给一段代码来测试vs下string对象的容量的变化:
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "capacity changed:" << sz << '\n';
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed:" << sz << '\n';
}
}
}
如图,其中 push_back() 是尾插接口,当容量不够的时候,该接口会自动扩容。我们尾插一百次c字符,打印每次变化后的容量和最开始的容量。
结果如图:

打印出来的容量最开始是15,没有包含\0,实际上是16。
实际上,在vs上大概是这样实现string的:
class string
{
private:
char _buff[16];
char* _str;
int _capacity;
int _size;
};
vs上实现string,不超过16字节存在数组里面,超过就指针指向并扩容
三、reserve 与 clear
3.1 reserve()
string s;
s.reserve(100);
1)、reserve 是保留的意思,语法上是提前开好100个空间,实际比100大,且这100中不包含\0
2)、reserve 不改变size,只修改capacity,如果 n < size ,那么vs下,s.reserve(n) 只能缩减到s.reserve(s.size())。对是否缩容(修改capacity)没有要求,取决于编译器的实现。
3)、vs下不会缩容,例如 n < capacity 的话就不会改变
string s;
size_t sz = s.capacity();
s.reserve(2);
cout << "capacity changed:" << sz << '\n';
s.push_back('c');
s.push_back('c');
s.push_back('c');
cout << s.size() << endl;
如图,一开始没有给值的话,在vs下,capacity 就是底层给定的数组空间,实际上是16,capacity 返回15,因为\0 不算在其中。
3.2 clear()
接口clear是清空数据的作用。
s.clear();
cout << endl << s.size() << ' ' << s.capacity() << endl;
if (s.empty())
cout << "空的\n";
clear清除所有数据但不会删除容量
empty() 是一个判空的接口。
运行结果如图:

四、modifiers 相关接口
4.1 尾插
4.1.1 push_back()
string s("hello world");
s.push_back(' ');
s.push_back('x');
前面讲过,push_back() 用于尾插,直接传入要尾插的字符。
4.1.2 append()
s.append("......");
注意:尾插,push_back只能传单个字符,append可以传字符串
4.1.3 +=
s += ' ';
s += "11111111111";
cout << s << endl;
运算符 += 也是尾插,可以尾插单个字符,也可以是字符串。
4.2 insert()
instert() 可以用于任意位置的插入。
s.insert(s.end(), '*');
s.insert(s.begin(), 'x');
如图,第一个参数是迭代器或下标位置,第二个是字符或字符串。
运行结果如图:

4.3 erase()
s.erase(0, 1);
s.erase(s.end() - 1);
s.erase(6); // 把下标6的位置之后字符全删除
cout << s << endl;
erase 删除,它有多个重构:
1)、第一个参数是删除的下标位置(删除的字符包括第一个参数下标位置),第二个是删除的个数
2)、参数是迭代器位置,删一个
3)、参数是下标,把下标的位置之后字符全删除
4.4 replace()
s.replace(5, 1, "////"); // 把下标5位置开始的1个字符替换成////
cout << s << endl;
replace 是字符串替换,具体用法如图。
上述代码运行结果如图:

4.5 find() 和 rfind()
这两个接口是字符查找函数,可以查找单个字符,也可以查找字符串。
size_t pos1 = s.find('/'); // find寻找字符,找到就返回第一次出现的位置,没找到就返回npso
size_t pos2 = s.find('/', 2); // 从第二字符开始找
size_t pos3 = s.rfind("//");
cout << pos1 << endl;
cout << pos2 << endl;
cout << pos3 << endl;
如图,代码运行结果如图:

4.6 replace() 扩容的测试与优化
while (pos2 != string::npos)
{
s.replace(pos2, 1, "%%"); // 扩容,挪动数据,有效率消耗
pos2 = s.find('/', pos2 + 2);
}
cout << s << endl;
如图,从pos2位置开始的1个字符替换成字符串"%%",这实际上有挪动数据的时间消耗。
可以做如下优化:
string tmp; //优化,以空间换时间
for (auto ch:s)
{
if (ch == '%')
tmp += "//";
else
tmp += ch;
}
cout << tmp << endl;
如图,定义一个tmp对象,找到要替换的字符就利用+=,实际上是尾插。
总的来说,replace、insert、erase 都是性能杀手!
4.7 pop_back()
这是 C++11 提供的尾删接口。
s.pop_back();
cout << s << endl;
代码运行结果:

五、operations 相关接口
5.1 c_str()
c_str 返回的是string对象中底层的字符串指针,在某些只能适配c相关的接口的情况下适合使用。
string s("hello world");
cout << s.c_str() << endl; // c_str 返回s底层字符串的指针
5.2 substr()
substr 是字符串分割函数,有多个重载。
string s1("test.cpp");
size_t pos = s1.rfind('.'); // rfind倒着找
string suffix = s1.substr(pos); // 从pos位置开始(包括pos)返回子串
string suffix1 = s1.substr(pos, 2);
cout << suffix << endl;
cout << suffix1 << endl;
如图,传入只传入一个参数pos,是从pos位置开始截取。传入下标位置、个数是从pos位置开始,截取多少个字符。
代码运行结果:

六、非成员函数
6.1 +、=
+、= 等重载运算符不是string类的成员函数。
string s1("hello");
string s2 = s1 + " world";
string s3 = "world " + s1;
cout << s2 << endl;
cout << s3 << endl;
6.2 getline()
与cin、scanf 不同的是,getline的读取默认是遇到换行才停止,遇到空格不停止。
getline(cin, s3); // getline 默认遇到空格不停止,遇到换行才停止
cout << s3 << endl;
当然,也可以指定停止的字符:
getline(cin, s3, '*'); // 遇到*才停止
cout << s3 << endl;
运行结果如图:

七、string 类的模拟实现
7.1 分文件开发
和之前一样,在模拟实现的string类的时候,分成string.h 文件,string.cpp 文件和 test.cpp 文件。
7.1.1 基本框架

如图,由于是自己实现的string类,需要封装在一个命名空间内。
迭代器可以用typedef 重命名实现,但是真实的迭代器 iterator 不是char*

7.1.2 构造函数
注意默认构造和拷贝构造:

如图是普通构造函数的实现,一个是无参的默认构造,一个是带参的。
如图,注意默认构造 _str 需要指向 \0 ,和标准保持一致。
strcpy() 会把 \0 一起拷贝进去。
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
如图,是拷贝构造的模拟实现。由于 capacity() 不包含\0,因此需要多开一个容量空间。
这是古早的实现方式,有一种现代写法:

如图,调用普通构造实现,将交换函数额外封装一下。
7.1.3 重载运算符 =
// s2 = s1
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
return *this;
}
如图,= 的写法和拷贝构造类似,需要注意的是应该先释放原本的空间,再 _str 指向新开的空间。
下面是赋值运算符的现代写法:
string& operator=(const string& s) // 现代写法
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
如图,利用拷贝构造定义一个中间值tmp,再把tmp和this进行交换。
7.1.4 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
析构函数的实现比较简单,就是释放空间和置空。
7.1.5 c_str()
该接口的实现也比较简单,主要是返回对应的字符串的指针。
const char* c_str()
{
return _str;
}
const char* c_str() const
{
return _str;
}
需要注意的是,我们可以针对普通对象和const对象分别来实现该接口。
7.1.6 重载运算符 []
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
[] 返回的是pos 位置对应的字符,实现两类不同的[] 接口。

如图是给出的测试用例以及结果。
7.2 插入与删除
7.2.1 reserve() 模拟
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
如图,此处 reserve() 的实现与vs的实现风格保持一致,不会缩容。
先定义一个中间变量tmp,开n+1个空间,再利用 strcpy 拷贝原本的元素,释放旧空间,指向新空间。
7.2.2 push_back() 模拟
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
如图,利用三目操作符,调用已经实现好的 reserve() 进行扩容,再进行插入逻辑。
7.2.3 append() 模拟
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
如图,具体的扩容和拷贝的思路和 push_back() 一样。
7.2.4 重载运算符 +=
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
如图,+= 可以调用已经实现好的接口。
7.2.5 insert() 模拟
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 挪动数据
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
}
单个字符插入的实现,如上图所示。先判断是否需要扩容,再处理挪动数据的逻辑。
将pos以及之后的数据向后挪动一个单位,再将数据插入到pos位置,令_size++。
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
memcpy(_str + pos, str, len);
_size += len;
}
如图,这是字符串的插入,也是先判断扩容再挪动数据。
具体的挪动逻辑和单个字符的类似,将pos以及之后的字符串整体向后挪动len个单位,再插入字符串到pos位置。
7.2.6 erase() 模拟
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
// 左闭右开减出来得到剩余字符
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
如图,先判断删除的len个字符是否多于剩余字符,如果是,直接置空。
再不断将后面的数据往前面挪动。
下面给出测试:


7.3 寻找与切割
7.3.1 begin() 和 end()
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
如图是不同版本的begin 和 end ,不用过多赘述。
7.3.2 clear() 模拟
void clear()
{
_str[0] = '\0';
_size = 0;
}
clear() 清理数据如图。
7.3.3 find() 模拟
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str); // 第二个参数是字串
if (ptr == nullptr)
return npos;
return ptr - _str;
}
如图分别是字符查找和字符串查找,两者思路类似。

const size_t string::npos = -1;
如图,npos定义在类里面,在类外面初始化。
7.3.4 substr() 模拟
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
len = _size - pos;
string sub;
for (int i = pos; i < pos + len; i++)
sub += _str[i];
return sub;
}
如图,判断要截取的len是否合理,只能截取pos之后的有限个有效字符。
然后再遍历返回。
样例测试:

7.4 运算符重载
7.4.1 <、>、=等
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);
}
如图,是各类比较大小相关的运算符,只要实现其中两个,其他的就可以通过调用来实现。
7.4.2 <<、>>
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
如图是流插入的重载,参数有一个 ostream 类型的对象。
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch;
//in >> ch; // cin 提取不到空格和换行
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
//s += ch;
//in >> ch;
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
如图是流提取的重载,它需要一个 istream 类型的对象。
在提取之前,先用 clear 清空缓冲区。利用buff来进行缓存,减少扩容产生的消耗。
本期的分享就到这里,如果觉得博主的文章比较对胃口的话,可以点一个小小的关注~
您的三连是我持续更新的动力~
更多推荐



所有评论(0)