目录
[一. 总述:string类函数风格介绍](#一. 总述:string类函数风格介绍)
[1.1 参数风格](#1.1 参数风格)
[1.2 迭代器里const函数和非const函数](#1.2 迭代器里const函数和非const函数)
[1.3 string类里的隐式类型转换](#1.3 string类里的隐式类型转换)
[二. string类介绍和实现](#二. string类介绍和实现)
[2.1 (默认/拷贝)构造函数和析构函数](#2.1 (默认/拷贝)构造函数和析构函数)
[2.2 一些简单的直接内联的函数](#2.2 一些简单的直接内联的函数)
[2.2.1 迭代器](#2.2.1 迭代器)
[2.2.2 建议内联的函数](#2.2.2 建议内联的函数)
[2.2.3 输入输出函数](#2.2.3 输入输出函数)
[2.2.4 就像原来实现顺序表一样,增删查改起手了](#2.2.4 就像原来实现顺序表一样,增删查改起手了)
[2.增 (operator+=/ append/ push_back)](#2.增 (operator+=/ append/ push_back))
[4. 查 (find / rfind / find_first_of / find_first_not_of)](#4. 查 (find / rfind / find_first_of / find_first_not_of))
[5.插 (insert)](#5.插 (insert))
string类作为c++学习STL入门很必要的一课,突如其来多而杂的知识,有时会把人弄昏,本文来带大家理清一下string类的使用
string类文档
https://cplusplus.com/reference/string/由于编码原因,c++提供了很多的string类兼容各种编码方式,宽字符,窄字符,16位的,32位的都有,本文以最基本的string类举例。

一. 总述:string类函数风格介绍
本文先做一个大体的介绍,再逐个介绍里面的接口
1.1 参数风格
string类提供的接口是很多的,差不多100多个,除了要记住常用函数的的用法,也要记住参数有多少个,顺序是什么,这显然是一个很大的学习成本。但好在string类的风格相对固定,基本看见函数,就知道参数怎么写了。
string类操纵的是字符串,一般提供四种方式
cpp
// 传的参数,越往后,就越是附加,越可以不传(只要能满足完成这个函数的最低要求)
// 比如第一种,只传第一个参数,看文档,有专门的构造函数了,传第一个和第二个,第三个是缺省
// 参数的相对顺序基本不变,掌握一个函数的参数顺序,其他的参数顺序,基本就能推出来了
// 第一种,传 string 类型
string (const string& str, size_t pos, size_t len = npos);
// 第二种,传 char* 类型
string (const char* s, size_t n);
// 第三种,传 char 类型
string (size_t n, char c);
// 第四种, 传 迭代器
template <class InputIterator> string (InputIterator first, InputIterator last);
1.2 迭代器里const函数和非const函数
随便点开一个迭代器,基本都会发现有两个版本,一个const版本,一个非const版本,这是为什么呢?为什么只有迭代器或者 operator[] 才有这种设定?
迭代器类似指针能够访问字符串,我们不想他被修改时用const,可以接受被修改时,不用const,那这跟提供两种接口,有什么关系呢?假如我们传一个const变量来,那显然是无法用非const接收的,你写了,编译器也不会让你过。那我们只留const版本呢?那也不行,有时,你确实有修改变量的需求,所以,一定要非const版本。当然,你模拟实现只要const,也行。
分享一个实际写成员函数时的问题:函数是const函数,但就是定义不了迭代器,就是因为没有用const的迭代器。(一定是要写成员函数才有),下图简化了场景,可见,就是过不去。

1.3 string类里的隐式类型转换
当你写完构造函数时,其他函数只保留 string 类接口就行了,因为你传char*过去,会进行隐式类型转换,转成string类,功能不变。写了也行,因为这样可以减少拷贝构造,提高一点性能。
二. string类介绍和实现
先实现短小的,放在类里,因为放在类里可以内联,对于频繁调用的可以提高一定的性能
2.1 (默认/拷贝)构造函数和析构函数
默认成员函数都是有返回值的,这里有一个好的点就是测试时,用匿名对象,可以简化很多代码。
建议默认构造和带参构造一起写了,也就是用全缺省的构造函数。
cpp
// 默认构造 +
String(const char* s = "")
{
_size = strlen(s);
_capacity = _size <= 15 ? 15 : _size;
_str = new char[_capacity + 1];
strcpy(_str, s);
}
关于构造函数,这里有一些小坑。第一,忘记给'\0'开空间,capacity是有效字符的容量,而不是要开的空间数量;第二,默认构造函数时,没有开空间,直接初始化为nullptr,这样会导致一个问题,打印时,解析空指针的问题。实际上,应该为'\0'开一个空间。总之,不管是传参或者不传参的情况,都不要忘记'\0'.
因为自己申请了空间,所以一定要写析构函数,有析构函数,就一定要写拷贝构造进行深拷贝,否则会面对同一空间释放两次的问题。传值就要调用拷贝构造,因为拷贝构造还没写,所以必须传引用
cpp
~String()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
// 深拷贝
String(const String& str)
{
_size = str._size;
_capacity = str._capacity;
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
其实深拷贝还有一种更现代化的写法,更通用一些,上面那个写法只能用于string类,不通用,关键在strcpy上,你换其他的就不能这样了。在拷贝函数内部,怎么初始化str,就怎么弄个新的,说白了就是复用, 能通过前面写的函数造个新的
cpp
String(const String& s)
: _str(nullptr)
{
String strTmp(s._str);
swap(_str, strTmp._str);
}
// 赋值运算符也是一样的
String& String::operator=(String str)
{
swap(str);
return *this;
}
2.2 一些简单的直接内联的函数
2.2.1 迭代器
cpp
using iterator = char*;
using const_iterator = const char*;
迭代器要先typedef一下,由于数组的特殊性,可以用原生指针进行模拟,写了以后就可以用auto for了。
2.2.2 建议内联的函数
cpp
// 迭代器
iterator begin();const_iterator begin() const;
iterator end();const_iterator end() const;
// 跟容量有关的
size_t size() const;
size_t length() const;
size_t max_size() const;
void clear();
bool empty() const;
// 成员访问
char& operator[] (size_t pos);const char& operator[] (size_t pos) const;
char& at (size_t pos);const char& at (size_t pos) const;
char& back();const char& back() const;
char& front();const char& front() const;
// 字符串操作
const char* c_str() const;
这些函数就是几乎一行代码的事,直接内联。
2.2.3 输入输出函数
cpp
istream& operator>> (istream& is, string& str);
ostream& operator<< (ostream& os, const string& str);
输入输出函数必须重载为全局函数
<< / >>是运算符
(ostream 对象) << (任意类型对象)
成员函数,第一个参数都是this指针,成员函数,就是
(任意类型对象) << (ostream 对象)
s.operator<<(cout); // 完全不符合直觉
s.operator>>(cin);
关于输出,直接拿到用c_str()打印即可,对于输入,有一个问题,假如我要输入一个带空格的字符串,由于空格和'\n'默认是分割符,输入"1234 5",按理说应该只要"1234",但空格是分隔符,实际上"12345"都会读,因此,我们要一个一个读和写
cpp
istream& operator>>(istream& is, String& str)
{
str.clear();
char ch;
while (is.get(ch) && ch != ' ' && ch != '\n')
{
str += ch;
}
return is;
}
2.2.4 就像原来实现顺序表一样,增删查改起手了
1.扩容
要进行增删查改,首先就要先写扩容和缩容的函数。也就是reserve,这个传进去n的时候,现在的空间比n小,就扩容,比n小,就不变(实现简单一些),注意扩容时,我们用的是new/delete,没有用realloc,要自己再复制移动一遍数据。拷贝赋值后,释放旧空间。
cpp
void String::reserve(size_t n)
{
if (n <= _capacity)
{
return;
}
else
{
if (n > _capacity)
{
_capacity = 2 * _capacity < n ? n : 2 * _capacity;
}
char* tmp = new char[_capacity + 1] {'\0'};
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
}
}
2.增 (operator+=/ append/ push_back)
尾插没什么难度,注意最后要补个'\0'
3.删 (erase / pop_back)
尾删也没啥难度,重点是erase,这就涉及到挪动数据的问题了,如果用memmove就会好很多。
4. 查 (find / rfind / find_first_of / find_first_not_of)
遍历一遍,相等了就找到了,返回他的下标,没找到,就返回npos
5.插 (insert)
首先,关于at和[]以及迭代器能访问的地方是不一样的。由图1,迭代器在'\0'处访问就会崩,由图2和3,[]访问'\0'不会,但访问'\0'之外就会,由图4,at访问'\0'也会崩




我们插入字符,就是在pos位置,将包括pos后len个字符,向右挪动,然后再从pos依次插入,这里要注意,size_t逆序遍历的问题,也就是头插的问题,如果头插循环条件>=pos,pos走到0,--后并不是-1,因为是size_t,会变成npos,导致无限循环,用迭代器或者控制好变量的赋值都好说。
cpp
String& String::insert(size_t pos, const String& str)
{
size_t len = str._size;
while (_capacity < _size + len)
{
_capacity = 2 * _capacity + 1;
}
reserve(_capacity);
// 循环变量的看待
size_t end = _size + len - 1;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
for (size_t i = pos; i < len + pos; i++)
{
_str[i] = str._str[i - pos];
}
_size += len;
_str[_size] = '\0';
cout << _str << endl;
return *this;
}
最后就到这里,string类学起来主要是一下子接触太多陌生的东西,但跟背单词一样,每个函数其实并不复杂。