⭐️从今天开始,我就要给大家介绍STL的内容了,今天我先为大家介绍一下第一号人物——string类,我会先介绍它的一些歌常见接口以及用法,然后再模拟实现它。
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code
目录
🌏了解string类
string类 就是在C语言的字符串做了一些处理,C语言中有一些处理字符串的库函数,但是字符串和库函数是分离的,而string类提供了一些对字符串处理的接口,把这些接口和字符串封装成一个类,更加地方便我们使用和操作。
总结地说:
- string是表示字符串的字符串类
- string类的接口和一些常规容器的接口基本相同,而且添加了一些专门用了操作string的常规操作
- string的底层:typedef basic_string<char, char_traits, allocator> string;
- 不能操作多字节或者变长字符的序列
- 使用时包含sting的头文件,而且要展开std
🌏string类常见的接口
🌲string的几个构造函数
- 无参默认构造函数: string() 构造类的空对象,即空字符串
- 有参构造函数: string(size_t n, char c) 用n个字符c来构造string类对象
- 有参构造函数: string(const char* s) 用C字符串来构造string类对象
- 拷贝构造函数 string(const string& s)
实例演示:
void TestString1()
{
// 构造函数
string s1;// 重点
string s2("hello world");// 用C string来构造string类对象 重点
string s3(s2);// 拷贝构造函数
string s4(10, 'a'); // 用n个字符'a'构造一个类对象 重点
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
代码运行结果如下:
🌲string类的三种遍历方式
- for+operator[]访问
- 迭代器遍历
- 范围for(会被编译器替换成迭代器来遍历)
实例演示:
void TestString2()
{
string s("hello world");
// 3种遍历方式
// 1.for+operator[]
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i];
}
cout << endl;
// 2.迭代器 iterator 不一定是指针,但是像
string::iterator it = s.begin(); // auto it = s.begin();
while (it < s.end())
{
cout << *it;
++it;
}
cout << endl;
// 3.范围for 原理被迭代器替换
for (auto e : s)
{
cout << e;
}
cout << endl;
}
代码运行结果如下:
其中迭代器是属于string类中的一个成员,它可能是指针,但不一定是指针,string类中它其实就是一个指针,但是其他容器就不一定了,后面会介绍。
🌲string类的四种迭代器
按方向分: 有正向迭代器和反向迭代器(iterator和reverse_iterator)分别配合being()、end()和rbegin()、rend()使用
按属性分: 有普通迭代器和const迭代器 (iterator const_iterator | reverse_iterator const_reverse_iterator)
实例演示:
void TestString3()
{
string s("hello world");
string::reverse_iterator rit = s.rbegin();
while (rit < s.rend())
{
cout << *rit;
++rit;
}
cout << endl;
}
代码运行结果如下:
注意: 对于const的类对象,要是有const的迭代器,否则会报错。
🌲string类的大小操作
- size和length 返回字符串的有效长度
- capacity 返回有效空间的大小
- clear 清空有效字符,不改变底层空间的大小
- empty 返回字符串是否为空的状态
- reserve reserve(size_t n = 0); 为字符串预留空间
- resize resize(size_t n, char c = ‘\0’); 改变字符串有效长度,多出的空间用字符c补充
实例演示:
void TestString4()
{
string s("hello world");
cout << s.size() << endl; // 为了与其他容器的接口保持一致
cout << s.length() << endl;
cout << s.capacity() << endl;
cout << "-----------------------" << endl;
s.clear();// 不改变底层空间的大小
cout << s.size() << endl;
cout << s.capacity() << endl;// 不变
cout << "-----------------------" << endl;
// 将有效字符个数增加为10个,多余的用'x'补充
s.resize(10, 'x');
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << "-----------------------" << endl
// 将有效字符个数增加为15个,多余的用'\0'补充
s.resize(15);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << "-----------------------" << endl;
// 将有效字符个数缩小为5个
s.resize(5);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
}
代码运行结果如下:
总结: 对于resize,当字符增多时,多出的空间用使用者给的字符补充,没给就使用缺省参数’\0’赋值,元素增多可能会扩大底层空间大小,元素减小不会堆底层空间大小有影响。
🌲string类的增删查改操作
- push_back 在字符串后尾插字符‘c’
- append 在字符串后尾插一段字符串
- operator+= 在字符串后追加字符串str
- insert 在pos位置插入一个字符或者一段字符串
- erase 从pos位置开始,删去n个字符
实例演示:
void TestString8()
{
string s;
s.push_back('h');
s.push_back('e');
s.push_back('l');
s.push_back('l');
s += 'o';
cout << s << endl;
s.append(" wo");
s += "rld";
cout << s << endl;
}
代码运行结果如下:
-
c_str 返回C格式的字符串
-
find 没找到就返回npos的结果(string::npos = -1)
-
rfind 反向从pos的位置开始寻找
-
substr string substr (size_t pos = 0, size_t len = npos) const 从pos位置开始,截取n个字符
实例演示:
// 分割网址 http(s)+网站名+资源
void SplitUrl(string& url)
{
// https://leetcode-cn.com/problemset/all/
size_t pos1 = url.find(':');
cout << url.substr(0, pos1) << " ";
size_t pos2 = url.find('/', pos1 + 3);
cout << url.substr(pos1 + 3, pos2 - (pos1 + 3)) << " ";
cout << url.substr(pos2, url.size() - pos2) << endl;
}
void TestString7()
{
string s("hello ");
s += "world";// '\0'以空格显示
cout << s << endl;// 使用运算符重载 operator<<
cout << s.c_str() << endl;// 以C语言的方式打印
// basic_string substr(size_type pos = 0,size_type n = npos) const 从pos位置开始打印n个字符
string file1("string.cpp");
size_t pos = file1.find('.');
if (pos != string::npos)
{
cout << file1.substr(pos) << endl;
}
string file2("file.tar.zip");
pos = file2.rfind('.');
if (pos != string::npos)
{
cout << file2.substr(pos) << endl;
}
string url1("http://www.cplusplus.com/search.do?q=npos");
string url2("https://leetcode-cn.com/problemset/all/");
SplitUrl(url1);
SplitUrl(url2);
}
代码运行结果如下:
🌲string类的非成员函数
- operator+ 传值返回加长后的string,尽量少用,因为传值返回会导致深拷贝的效率降低
- operator>> 重载输入运算符
- operator<< 重载输出运算符
- getline 获取一行字符串
- realational operators 大小比较
实例演示:
void TestString9()
{
string s;
cin >> s;
cout << s << endl;
s = s + " world";
cout << s << endl;
}
代码运行结果如下:
🌏string类的模拟实现
🍯string类的成员
string类是字符串类,实现使用了一个C中的字符串,为了可以扩容改变空间大小,我们选择在堆上开辟空间,然后用一个字符指针指向这块空间,和我们之前数据结构中的顺序表实现有类似之处。
private:
char* _str;
size_t _size;// 有效字符个数
size_t _capacity;// 能存储的有效字符的个数 '\0'不是有效字符,是标识结束的字符
public:
static size_t npos;
size_t string::npos = -1;
🍯string类的构造函数和析构函数
- 有参构造函数的实现,缺省值给空字符串,因为要存放’\0’,所以要多开一个空间的大小,’\0’是无效字符,不算有效大小
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];// 因为要存放'\0','\0'是无效字符,不算有效大小
strcpy(_str, str);
}
- 析构函数的实现,清理空间资源
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
🍯深浅拷贝
前面我们就提到过,浅拷贝就是简单的字节序拷贝,为了更好地理解深拷贝和浅拷贝,我来分开为大家讲解:
下面是我们用编译器的浅拷贝实现类对象的拷贝:
string s1("hello world");
string s2(s1);
代码运行结果如下:
编译器直接报错了,为什么会出现这种问题呢?其实我们前面有提到过
所以,为了避免释放两次空间,我们要进行深拷贝,深拷贝就是给新的对象出现开一块空间,这样就不是简单的字节序拷贝了。
代码实现如下:
string(const string& s)
{
// 深拷贝,另外开一块空间
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
代码运行结果:
这次代码就很好地跑起来了。
operator=的实现 与拷贝构造的实现很相似,要进行深拷贝,否则会出问题。
string& operator=(const string& s)
{
if (this != &s)// 防止自己给自己赋值
{
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
🍯string类的访问和迭代器的实现
- operator[]的实现 为了让这块空间的值可以被修改,所以我们选择传引用返回
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
- 迭代器 string类的迭代器的本质其实是char*的别名,就是一个指针的用法,注意其中begin和end不能写其他的单词,不然编译器识别不出迭代器就会导致范围for使用报错
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
- c_str 直接返回底层的字符指针即可
const char* c_str() const
{
return _str;
}
🍯string类的增删操作的实现
reserve 预留出一片空间,我们要开一段新空间,然后把旧空间的数据拷贝到新空间上去,然后释放旧空间,空间都要多开一个,来存放’\0’
void reverse(size_t n)
{
if (n > _capacity)
{
char* newStr = new char[n + 1];
strcpy(newStr, _str);
delete[] _str;
_str = newStr;
_capacity = n;
}
}
push_back 我们以2倍的方式增容,复用reverse的代码
void push_back(char ch)
{
// 扩容
if (_size == _capcaity)
{
size_t newcapacity = _capcaity == 0 ? 2 : _capcaity * 2;
reverse(newcapacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';//注意
}
append
void append(const char* str)
{
size_t len = strlen(str);
// 扩容
if (len + _size > _capacity)
{
size_t newcapacity = len + _size;
reverse(newcapacity);
}
strcpy(_str + _size, str);
_size += len;
}
operator+= 这个函数有两个重载,一个是尾插字符串,一个是尾插字符,我们这里可以分别复用append和push_back的代码
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
insert有两个重载,一个是在pos位置插入一个字符,一个是在pos位置插入一段字符串,同时我们还有判断pos的合理性,为了能够拷贝重叠区间的字符串,我们可以使用memmove库函数来操作,而不是memcpy。
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
// 扩容
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
reverse(newcapacity);
}
memmove(_str + pos + 1, _str + pos, sizeof(char) * (_size - pos));
_str[pos] = ch;
_size++;
_str[_size] = '\0';// 注意
return *this;
}
string& insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
// 扩容
if (len + _size > _capacity)
{
size_t newcapcity = len + _size;
reverse(newcapcity);
}
memmove(_str + pos + len, _str + pos, sizeof(char) * (_size - pos));
// strcpy(_str + pos, s); 最后会把'\0'拷过去
strncpy(_str + pos, s, len);
_size += len;
_str[_size] = '\0';// 注意
return *this;
}
我们还可以思考这样一个问题,我们是不是可以复用insert的两个重载函数实现push_back和append,如下:
void push_back(char ch)
{
insert(_size, ch);
}
void append(const char* str)
{
insert(_size, str);
}
erase 在pos位置删去n个字符
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len >= _size - pos)// 不能写成len+pos>=_size 因为当len是npos时,加上pos之后就变小了
{
_size = pos;
_str[_size] = '\0';
}
else
{
memmove(_str + pos, _str + pos + len, sizeof(char) * (_size - (pos + len)));
_size -= len;
_str[_size] = '\0';
}
return *this;
}
🍯string类的大小操作的模拟实现
size和capacity
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
resize 要考虑改变后的大小是否大于当前空间,考虑是否需要扩容,记得最后加一个’\0’,表示字符串结尾
void resize(size_t n, char ch = '\0')
{
if (n > _capacity)
{
size_t newcapacity = n;
reverse(newcapacity);
}
// _size>=n时不进入循环
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';// 注意
}
🍯string类中find函数的实现
find函数也有两个重载,一个是从pos位置开始寻找一个字符,找到就范围字符位置,没找到就返回npos,还有一个就是从pos位置开始找一段字符串,我们可以使用strstr字符串查找函数,也可以使用KMP算法查找
size_t find(char ch, size_t pos = 0) const
{
if (pos >= _size)
return npos;
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t find(const char* s, size_t pos = 0) const
{
if (pos >= _size)
return npos;
char* p = strstr(_str, s);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
下面是KMP算法的写法
int* GetNext(const char* str)
{
int* next = new int[strlen(str)];
next[0] = -1;
next[1] = 0;
int k = next[1]; // k = next[i-1] str2[i-1]== str2[k1] ==> next[i] = next[i-1] + 1 = k + 1
int i = 2;
while (i < (int)strlen(str))
{
if (k == -1 || str[i - 1] == str[k])
{
next[i] = k + 1;
k = next[i];
++i;
}
else
{
k = next[k];
}
}
return next;
}
int KMP(const char* str1, const char* str2, size_t pos = 0)
{
int len1 = strlen(str1);
int len2 = strlen(str2);
int* next = GetNext(str2);
int i = 0;
int j = (int)pos;
while (i < len1 && j < len2)
{
if (j == -1 || str1[i] == str2[j])
{
++i;
++j;
}
else// i不回退,j回退到特定的位置
{
j = next[j];
}
}
if (j == len2)
{
delete[] next;
return i - j;// 返回匹配的位置的下标
}
return -1;
}
size_t find(const char* s, size_t pos = 0) const
{
if (pos >= _size)
return npos;
size_t index = KMP(_str, s, pos);
if (index != npos)
{
return index;
}
else
{
return -1;
}
}
🍯operator>>和operator<<的实现
这里不需要用到友元函数,因为没有访问私有成员
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();// 把之前的空间清理一下
while (1)
{
char ch;
ch = in.get();
if (ch == ' ' || ch == '\n')
{
break;
}
else
{
s += ch;
}
}
return in;
}
🍯非成员函数
这里比较简单,就不过多介绍
bool operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
bool operator<=(const string& s) const
{
return strcmp(_str, s._str) <= 0;
}
bool operator>(const string& s) const
{
return strcmp(_str, s._str) > 0;
}
bool operator>=(const string& s) const
{
return strcmp(_str, s._str) >= 0;
}
bool operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
bool operator!=(const string& s) const
{
return strcmp(_str, s._str) != 0;
}
🍯string类中拷贝构造和赋值重载的现代写法
我们首先实现一下swap函数
void swap(string& s)
{
::swap(_str, s._str);// ::表示调用全局域的函数
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
我们先看一下现代写法
string(const string& s)
:_str(nullptr)
,_capacity(0)
,_size(0)
{
string tmp(s._str);
swap(tmp);
}
string& operator=(string s)
{
if (this != &s)
{
swap(s);
}
return *this;
}
拷贝构造的现代写法就是利用构造函数构造一个临时对象,然后利用swap把这个临时对象直接换给新的对象,是不是感觉这种方法很酷,很霸道,其实现代写法是简洁的,看起来也很舒服。所以推荐使用现代写法。
🌐总结
以上就是我也介绍的string类的全部内容,喜欢的话,欢迎点赞支持~