目录
一、string类介绍
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
因此在C++中,将字符串和操作字符串的一些函数封装成string字符串类,为了解决传统 C 语言风格字符串的诸多缺陷,并提供更安全、高效、易用的字符串操作方式。
string类总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string底层:是basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
- 不能操作多字节或者变长字符的序列
- 在使用string类时,必须包含#include<string>头文件,为了方便也可以展开std命名空间
二、string类常见的接口
2.1 常见的构造函数
这里主要介绍几个在以后会常用的:
- 无参默认构造函数: string()【重点】
构造类的空对象,即空字符串
- 带参构造函数1:string(size_t n, char c)
用n个字符c来构造string类对象
- 带参构造函数2:string(const char* s)【重点】
用C字符串来构造string类对象
- 带参构造函数3:string(const string& str, size_t pos, size_t len = npos)
截取string对象的一部分来创建string类对象,长度默认从起始位置到字符串的结尾
- 带参构造函数4:string(const char* s, size_t n)
从字符串中截取n个字符来创建string类对象
- 拷贝构造函数:string(const string& s)【重点】
利用实例化的string对象来创建新string类对象
示例:
2.2 对象的容量操作
这里主要介绍几个常用的:
- size() & length()
返回字符串有效字符长度,方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- resize(size_t n) & resize(size_t n, char c)
将字符串中有效字符个数改变到n个。
不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- capacity()
返回有效空间总大小
- reserve(size_t n = 0)
为字符串预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
- clear()
清空有效字符,不改变底层空间的大小。
- empty()
检测字符串释放为空串,是返回true,否则返回false。
示例:
2.3 对象的访问及遍历操作
- operator[]
和访问数组中的元素使用方法一致,下标从0开始,[]解引用。
- begin + end(迭代器)
- rbegin + rend(反向迭代器)
这里的迭代器和反向迭代器在string类中其实是指针的封装,但在其它容器中中可能不是。在 C++ 中引入迭代器Iterator的核心目的是为了统一容器的访问方式,并解耦算法与容器,使得泛型编程更加灵活和安全。
迭代器的种类:
按方向分: 有正向迭代器和反向迭代器,分别支持正序遍历和逆序遍历
按属性分: 有普通迭代器和const迭代器 ,分别作用于普通对象和const对象
- 范围for
之前在【重走C++学习之路】2、C++基础知识讲解过,这是C++11才支持的新语法,支持迭代器的类都可以用范围for进行遍历。
2.4 对象的增删查改操作
这里介绍常用的几个函数接口:
- operator += ()
在字符串后面追加一个字符串,或者C字符串,或者一个字符
- pusb_back(char c)
在字符串后面尾插一个字符
- insert()
在字符串的指定位置插入一个字符串/C字符串/n个字符,还可以指定插入一个字符串的子串。
- erase()
从指定位置开始删除n个字符,默认从起始位置全部删除
- swap(string& str)
交换两个字符串,但一般用非成员函数的swap函数
- pop_back()
删除尾部字符
- c_str()
返回C格式的字符串
- find()
从字符串pos位置开始往后找字符c或者字符串str,返回该字符或者字符串在字符串中的位置,没找到返回npos
- rfind()
与find功能一致,只是反向从pos的位置开始寻找
- substr()
从pos位置开始,截取n个字符
2.5 非成员函数
- operator+
传值返回加长后的string,尽量少用,因为传值返回会导致深拷贝的效率降低
- operator>>
重载输入运算符
- operator<<
重载输出运算符
- getline
从输入流中获取一行字符串
- realational operators
大小比较,重载了一系列的关系运算符
三、string类模拟实现
string类是字符串类,实现使用了一个C中的字符串,为了可以扩容改变空间大小,我们选择在堆上开辟空间,然后用一个字符指针指向这块空间,与动态顺序表类似。在面试中,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
3.1 string类的成员变量
private:
char* _str;
size_t _size; // 有效字符个数(不包括'\0')
size_t _capacity;// 能存储的有效字符的个数(不包括'\0')
3.2 构造函数和析构函数
- 带参构造函数
目标:实现string s1() && string s1("hello world")的形式
string(const char* str = "")
{
if (nullptr == str)
{
assert(false);
return;
}
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1]; // +1是为了存放'\0'
strcpy(_str, str);
}
- 析构函数
~string()
{
delete[] _str; // 由于这里不是自定义类型,没有析构函数
// 需要自己去手动将指针置为空
_str = nullptr;
_size = _capacity = 0;
}
3.3 拷贝构造函数
目标:实现stirng s2(s1)的形式
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
这里需要注意:不能直接将s中str的值赋给_str,这样就是浅拷贝了,两个字符串变量都指向同一块堆空间,在析构的时候就会出现重复释放同一块内存空间,从而导致报错。
拷贝构造函数还有现代写法,利用构造函数开辟空间并拷贝的特点,临时变量充当中间的值,最后交换对象和临时变量达到深拷贝的效果。
string(const string& s)
:_str(nullptr)
{
string tmp(s._str); // 利用s中的字符串创建临时变量
// 这里的字符串和参数中的字符串内容相同地址不同
swap(_str, tmp._str);// 交换之后,临时变量的字符串为空,对象的字符串达到了拷贝的效果
// 且出了函数后,临时变量生命周期结束,不会占用内存
_size = s._size;
_capacity = s._capacity;
}
3.4 operato=函数重载
目标:实现s3 = s2 = s1的形式
为了能连续的赋值,函数的返回值因该是一个stirng类型,并且&来提高效率
传统写法,依次拷贝成员变量,遇到需要资源管理的变量则额外创建空间并进行拷贝(深拷贝),最后释放原有的空间。
string& operator=(const string& s)
{
_size = s._size;
_capacity = s._capacity;
char* tmp = new char[_capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
return *this;
}
现代写法,创建临时变量复用拷贝构造函数(深拷贝),然后交换对象和临时变量,达到赋值的效果,并且对象原来的字符串空间交还给了临时变量后随着函数的结束而析构,也达到了清理资源的效果。
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(_str, tmp._str);
}
return *this;
}
还有一种更简便的方法,利用传值传参的特点,参数是外部变量的临时拷贝,省去了在函数内创建临时变量的步骤,最后交换对象和参数,从而达到了深拷贝赋值,并且对象原来的字符串空间交还给了临时变量后随着函数的结束而析构。
string& operator=(string s)
{
swap(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
3.5 operator[]和迭代器的实现
- operator[]
目标:实现str[i]++的形式
为了能实现++的运算,返回值需要是能被修改的,而且修改后str中的元素也会变,那么就需要传引用返回。考虑到对象会有const属性,因此重载operator[]函数。
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& operator[](size_t i)const
{
assert(i < _size);
return _str[i];
}
- 迭代器iterator
string类的迭代器的本质其实是char*的别名,就是一个指针的用法。需要注意的是:其中begin和end不能写其他的单词,不然编译器识别不出迭代器就会导致范围for使用报错。
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
3.6 增删查操作
1. 插入数据
string对象的插入主要有四个:insert、push_back、append和operator+=,其中insert是有效位置的插入,push_back是在尾部插入字符,append是在尾部追加字符串,operator+=是在尾部加上字符或者字符串。在增加元素的时候需要考虑是否需要扩容,因此我们需要先写一个reserve扩容函数。
- reserve()
void reserve(size_t n)
{
if (n > _capacity)
{
char* newstr = new char[n + 1];
strcpy(newstr, _str);
delete[] _str;
_str = newstr;
_capacity = n;
}
}
- insert()
insert主要有两个,一个是有效位置插入字符,另一个是有效位置插入一个字符串。
string& insert(size_t pos, char ch)
{
assert(pos <= _size); // 等于的时候就相当于在尾部插入
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(newcapacity); // 每次以2倍的速度进行扩容
}
// 需要从尾部开始挪动数据,腾出位置给插入数据
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
//考虑增容
int len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = _size + len;
reserve(newcapacity); // 防止二倍扩容不够,直接扩到能满足的大小
}
//挪动数据
int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
--end;
}
//插入数据
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
- push_back()
void push_back(char ch) //追加单个字符
{
//考虑空间是否满了
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
还有一种简便的写法,复用insert函数来实现:
void push_back(char ch)
{
insert(_size, ch);
}
- append()
void append(const char* s) //追加字符串
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
size_t newcapacity = _size + len;
reserve(newcapacity);
}
strcpy(_str + _size, s);
_size += len;
}
同理,append也可以复用insert实现:
void append(const char* s)
{
insert(_size, s);
}
- operator+=
string& operator+=(char ch)
{
this->push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
this->append(str);
return *this;
}
2. 删除数据
string对象的删除主要有两个:erase和pop_back,其中erase是有效位置的删除,而pop_back是删除尾部数据。
- erase()
string& erase(size_t pos, size_t len = std::string::npos)
{
assert(pos < _size);
if (len >= _size - pos)
{
// 后面没有元素了,直接将pos置为\0
_str[pos] = '\0';
_size = pos;
}
else
{
size_t i = pos + len;
// 将pos+len后面的元素换到pos后面来
while (i <= _size)
{
_str[i - len] = _str[i];
++i;
}
_size -= len;
}
return *this;
}
- pop_back()
void pop_back()
{
assert(_size > 0);
--_size;
_str[_size] = '\0';
}
这里也可以复用erase实现尾删:
void pop_back()
{
erase(_size - 1, 1);
}
3. 查找数据
string对象的查找主要是:find,find要实现查找字符和查找字符串两个功能,并返回它们的起始位置。
size_t find(const char* str, size_t pos = 0)const
{
char* p = strstr(_str, str);
if (p == nullptr)
{
return std::string::npos;
}
else
{
return p - _str;
}
}
size_t find(char ch, size_t pos = 0)
{
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return std::string::npos;
}
3.7 其它函数
- resize()
将字符串的有效大小变为n,需要判断是变小还是变大
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n;++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
- 关系运算符重载
bool operator<(const string& s)
{
int ret = strcmp(_str, s._str);
return ret < 0;
}
bool operator==(const string& s)
{
int ret = strcmp(_str, s._str);
return ret == 0;
}
bool operator<=(const string& s)
{
return *this < s || *this == s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator!=(const string& s)
{
return !(*this == s);
}
- 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;
}
- KMP算法
KMP算法是一种高效的字符串匹配算法,用于在主文本字符串中快速查找子字符串的出现位置。其核心思想是通过预处理模式串,利用部分匹配信息跳过不必要的字符比较,将时间复杂度从暴力匹配的 O(n×m) 优化到 O(n+m)(n为文本长度,m为模式长度)。
#include <vector>
using namespace std;
// 构建部分匹配表 next
vector<int> buildNext(const string& pattern)
{
int m = pattern.size();
vector<int> next(m, 0);
int len = 0; // 当前最长公共前后缀长度
for (int i = 1; i < m; )
{
if (pattern[i] == pattern[len])
{
next[i++] = ++len;
}
else
{
if (len != 0)
len = next[len - 1]; // 回到字串公共前后缀的地方
else
next[i++] = 0;
}
}
return next;
}
// KMP 主算法
int KMPSearch(const string& text, const string& pattern)
{
vector<int> next = buildNext(pattern);
int n = text.size(), m = pattern.size();
int i = 0, j = 0;
while (i < n)
{
if (text[i] == pattern[j])
{
i++;
j++;
if (j == m)
return i - m; // 找到匹配
}
else
{
if (j != 0)
j = next[j - 1];
else
i++;
}
}
return -1; // 未找到
}
结语
string类是在面试中考的比较多的一种类,特别是在这个和字符串打交道的时代,更是需要掌握对其的操作,并且了解了底层才能熟稔于心。下一篇将会介绍STL-vector相关的知识,有兴趣的朋友可以关注一下。