1 为什么要学习string类
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
2标准库中的string类
可以看出:
1 string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string类是basic_string类模板的一个实例化,使用char作为其字符类型。
4. 不能操作多字节或者变长字符的序列
使用string类的说明
1包含#include 头文件
2 使用using namespace std;
3 string类的常用接口说明
3.1 string类对象的常见构造
① 构造空的string类对象,即空字符串
②拷贝构造函数
③用C-string来构造string类对象
示例
void test1()
{
//构造空的string类
string str;
cout << str << endl;
//用字符串构造string类
string s("hello world");
cout << s << endl;
//拷贝构造
string ss(s);
cout << ss << endl;
}
3.2 析构函数
析构函数会自动调用
3.3赋值重载
可以使用string对象,字符串,以及字符进行赋值
void test10()
{
string s1 ("hello world");
string s2;
//string对象进行赋值
s2 = s1;
cout << s2 << endl;
//字符串进行赋值
s2 = "cppp";
cout << s2 << endl;
//字符进行赋值
s2 = 'c';
cout << s2 << endl;
}
3.4 string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
reserve | 为字符串预留空间 |
resize | 为字符串预留空间并初始化 |
void test2()
{
string s1("hello world");
cout << s1.size() << endl;
cout << s1.length() << endl;
//size 和length都是返回字符串的长度
cout << s1.capacity() << endl;
//返回空间总大小
string s2;
s2.reserve(100);
//开好100个字符空间的大小
string s3;
s3.resize(10, 'c');
//开10个字符空间,并初始化为'c'
}
注:当reserve的参数小于string的底层空间总大小(15)时,reserve不会改变容量大小。
3.5 string类对象的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[ ] | 返回pos位置的字符 |
begin + end | begin返回起始位置,end返回结尾字符的下一个位置 |
rbegin + rend | rbegin返回反向起始位置,end返回反向结尾字符的下一个位置 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
注意:用operator[ ]访问string对象是用下标进行访问,类似于用下标访问数组的方式;而用begin,end,rbegin,rend对string对象进行访问,是通过迭代器进行。迭代器是为STL容器专门打造的一种机制,访问容器中的元素,需要通过迭代器进行,迭代器是像指针一样的类型,所以用法也和指针类似。通过迭代器就可以对它所指向的元素进行相关操作。
operator[ ]
因为operator[ ] 返回的是pos位置的引用,所以支持修改string对象
void test4()
{
string s1 = "hello world";
cout << s1[2] << endl;
//将s1[2]位置的字符修改为'w’
s1[2] = 'w';
cout << s1[2] << endl;
cout << s1 << endl;
int i;
for (i = 0; i < s1.size(); i++)
{
//对s1[i]位置的字符++
s1[i]++;
}
cout << s1 << endl;
}
迭代器
正向迭代器
void test5()
{
string s1 = "hello world";
string::iterator it = s1.begin();
//begin()返回字符串的起始下标
while (it != s1.end())
{
//end()返回字符串最后一个元素的下一个位置,即'\0'
cout << *it ;
++it;
}
}
注意:iterator是迭代器的类型名,在使用时必须指明类域
反向迭代器
顾名思义:倒着遍历字符串
void test6()
{
//反向迭代器
string s1 = "hello world";
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit;
rit++;
}
cout << endl;
}
常量迭代器
常量迭代器不支持修改对象,一般用于常对象或者常对象做形参防止函数内部对对象进行修改
void PrintString(const string& s)//s为常对象
{
//正向迭代
string::const_iterator cit = s.begin();
/*auto cit = s.begin();*///如果cit的类型太长,也可以使用auto关键字自动推导类型
while (cit != s.end())
{
cout << *cit;
cit++;
}
cout << endl;
//反向迭代
string::const_reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit;
rit++;
}
cout << endl;
}
范围for
void test7()
{
string s1("hello world");
for (auto &ch : s1)
{
cout << ch;
ch++;
}
}
范围for在使用起来非常方便,它可以自动判断数组元素的类型和数组的大小,但是只能正向遍历
范围for的底层实际就是迭代器。
3.6 string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符 |
append | 在字符串后追加一个字符串 |
operator+= | 在字符串后追加字符串str |
insert | 在pos位置插入字符或字符串 |
erase | 删除字符串中从pos位置开始并跨越len个字符的部分 |
push_back和 append
void test8()
{
string s1("hello,world");
//s1后面插入'x'
s1.push_back('x');
cout << s1 << endl;
//s1后面插入字符串"hi"
s1.append("hi");
cout << s1 << endl;
}
其中append还支持使用迭代器追加一段区间的字符串
void test8()
{
string s1("hello,world");
string s("this is a demo ");
//在字符串s的后面追加字符,范围是从[s1.begin() s1.end()]
s.append(s1.begin(), s1.end());
cout << s << endl;
//在字符串s的后面追加字符,范围是从[s1.begin()+3 s1.end()-3]
s.append(s1.begin() + 3, s1.end() - 3);
cout << s << endl;
}
operator+=
可以看出,operator+=支持追加字符,也支持追加字符串,还支持追加string类型的对象,所以平常使用的过程中,我们更倾向于使用operator+=来进行追加,其代码可读性也更强
void test9()
{
string s1("hello,world");
string s("!!!");
//追加字符'x'
s1 += 'x';
//追加字符串"你好"
s1 += "你好";
//追加string类型的对象s
s1 += s;
cout << s1 << endl;
}
insert
假设在字符串的每个空格位置前插入###,该如何实现呢?
void test_string1()
{
string s1("I am back");
for (int i = 0; i < s1.size();)
{
//遇到空格就进行插入
if (s1[i] == ' ')
{
s1.insert(i, "###");
//在插入完成后,要改变i,使i指向当前空格的下一个位置,才能继续插入
i += 4;
}
else
{
//没有遇到空格就往后++找空格
i++;
}
}
cout << s1 << endl;
}
补充:如果要将字符串的空格位置替换为###,又该如何实现呢?
构造一个新的字符串news,遍历s,没有遇到空格就把s的字符+=到news后面,遇到空格,+= ###到news的后面
oid test_string4()
{
string s("I am back");
string news;
for (int i = 0; i < s.size();i++)
{
if (s[i] == ' ')
{
news+="###";
}
else
{
news += s[i];
}
}
cout << news << endl;
}
erase
void test_string2()
{
string s("I am back");
for (int i = 0; i < s.size(); i++)
{
if (s[i] == ' ')
{
//将空格删除
s.erase(i,1);
}
}
cout << s << endl;
}
3.7 string类的查找操作
函数名称 | 功能说明 |
---|---|
c_str | 返回c格式的字符串 |
find+npos | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
find ,rfind,substr
void test_string5()
{
string filename1("test.cpp");
//找文件名的后缀
//方法,find()函数找到"."的位置,substr()函数返回字符"."以后的字符串,即为后缀
size_t pos1= filename1.find('.');
if (pos1 != string::npos)
{
string str = filename1.substr(pos1);
cout << str << endl;
}
string filename2("test.cpp.zip.tar");
//找文件名的真实后缀,即最后一个字符'.'对应的后缀
//这时就需要倒着找,使用rfind();
size_t pos2 = filename2.rfind('.');
if (pos2 != string::npos)
{
string ss = filename2.substr(pos2);
cout << ss << endl;
}
}
注: string::npos参数 —— npos 是一个常数,表示无符号整型的最大值,用来表示不存在的位置
使用find+substr分割网址
void test_string6()
{
string url("https://cplusplus.com/reference/string/basic_string/basic_string/");
size_t pos1 = url.find("://");
if (pos1 == string::npos)
{
cout << "非法的url" << endl;
return;
}
string protocol = url.substr(0,pos1);
size_t pos2 = url.find('/', pos1 + 3);
if (pos1 == string::npos)
{
cout << "非法的url" << endl;
return;
}
string domain = url.substr(pos1 + 3, pos2 - pos1 - 3);
string uri = url.substr(pos2+1);
cout << protocol << endl;
cout << domain << endl;
cout << uri << endl;
}
3.8 string类的其它函数
这些函数都是全局函数并没有实现在string类里
其它接口 | 功能说明 |
---|---|
字符串与其它类型进行转换 | stoi,stol,stof,stod等分别是字符串转整形,长整形,单精度浮点型,双精度浮点型 ;to_string是其它类型转为字符串 |
getline | 读取一行字符串,不会因为空格而结束 |
void test_string7()
{
int vali = 999;
double vald = 999.88;
//int转为字符串
string stri = to_string(vali);
//double转为字符串
string strd = to_string(vald);
cout << stri << endl;
cout << strd << endl;
stri = "888";
strd = "888.99";
//字符串转为int
vali = stoi(stri);
//字符串转为double
vald = stod(strd);
cout << vali << endl;
cout << vald << endl;
}
cin在输入字符串的时候,会以空格作为单词的分割,如果输入的字符串里面带有空格,则需要用getline
假设输入"hello world"
如果使用cin输入,则只会输出hello
4 string的模拟实现
4.1 类的定义
namespace zbt
{
class string
{
public:
string()//构造函数
{
}
~string()//析构函数
{
}
private:
char* _str;
size_t _size;//指向有效字符的下一个
size_t _capacity;//区间容量
};
void test1()
{
...
}
}
为了和C++库里的string区分开,我们自己定义一个命名空间,将自己实现的string类以及测试函数都放在里面
4.2 构造函数
string(const char* str = "")//给一个缺省值,如果没有传参,默认为空值""
{
_size = strlen(str);//strlen算出有效字符的个数,赋值给_size
_capacity = _size;
_str = new char[_size + 1];//开空间时要多开一个,存储'\0'
strcpy(_str, str);//将str的内容拷贝到_str中
}
需注意,在初始化_str时,需动态开辟一个和str一样大的空间,再将str的值拷贝到_str中
4.3析构函数
~string()
{
delete[]_str;//delete[]和new[]对应
_str = nullptr;
_size = _capacity = 0;
}
4.4 operator[ ]
char& operator[](size_t pos)
{
assert(pos < _size);//首先判断给的下标要小于_size
return _str[pos];//返回pos位置的值,传引用返回,支持修改string对象
}
const char& operator[](size_t pos)const//const类型,只读,不可修改
{
assert(pos < _size);
return _str[pos];
}
4.5 迭代器
这里我们先实现正向迭代器,string的迭代器底层实际就是指针
typedef char* iterator;//将char*重名为iterator
typedef const char* const_iterator;// const char*重名为const_iterator
iterator begin()//返回字符串的起始下标,即为_str
{
return _str;
}
iterator end()//返回有效字符的下一个位置,即'\0'
{
return _str + _size;//因为是指针,所以起始位置+ _size即为'\0’的位置
}
const_iterator begin()const//常量正向迭代器,加上const即可
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
4.6 拷贝构造函数
这里就会涉及深浅拷贝问题,string的成员变量都为内置类型,如果不自己实现拷贝构造函数,那么编译器会自己默认生成一个浅拷贝的构造函数,假设用s1去拷贝构造s2,s1和s2中的成员变量_str便会指向同一块空间,在执行析构函数的时候,便会对同一块空间释放两次,程序会崩溃掉。深浅拷贝详细讲解请点击这里
那么拷贝构造函数应该实现的是深拷贝,即s1和s2中的成员变量_str指向两块不同的空间,两块空间上内容一样。即给每个对象独立分配资源,保证多个对象之间不会因为共享资源而导致多次释放
既然知道了string的拷贝构造函数是深拷贝,那么具体该如何实现呢?首先是要给s2开辟一块新空间,大小和s1一样,其中s2成员变量中的_size,_capacity应该和s1中的一样,最后再将s1中的字符串内容拷贝给s2。
//s2(s1) s为s1的别名
//传统版本
string(const string& s)
:_str(new char[s._capacity + 1])
, _size(s._size)
, _capacity(s._capacity)
{
strcpy(_str, s._str);
}
对于成员变量的初始化,采用的是初始化列表的方法,函数体内实现拷贝字符串内容。
这里是传统版本的写法,其代码的可读性高。还有一种版本是现代版本,其代码简洁,一起来看看吧
现代版本的核心思想是老板思维,要实现拷贝构造函数,不自己实现,而是去构造出一个临时对象tmp(可以理解为打工人),tmp中的内容和s1一样,最后将s2和tmp交换即可
void swap(string& tmp)
{
//函数里面调用的是库里面的swap函数实现成员变量的交换,并不是我们自己写的swap函数
::swap(_str, tmp._str);
::swap(_size, tmp._size);
::swap(_capacity, tmp._capacity);
}
//s2(s1),s是s1的别名
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);//调用构造函数,构造出临时对象tmp
swap(tmp);//将s2和tmp的内容交换
}
需要对s2的成员变量_str初始化为空,因为当s2和tmp交换后,tmp即为原来的s2,出函数作用域的时候,tmp是局部变量,会调用tmp的析构函数释放tmp,如果_str没有初始化,那就是随机值,会出错。
4.7 赋值运算符重载
(1) 原始写法
实现 s1=s2;函数里面s便是s2的别名
string& operator=(const string& s)
{
if (this != &s)//防止自己给自己赋值
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
首先需要开辟一块数组空间tmp,里面存储s2的内容,既然要将s1的内容改为s2,那么s1原有的内容就应该先释放掉,然后s1里面的内容为之前tmp保存的内容。size和capacity也应该和s2一致。
(2) 现代写法
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);//调用构造函数
// string tmp(s);//调用拷贝构造
swap(tmp);
}
return *this;
}
要实现运算符重载,不自己实现,构造一个tmp对象,里面的内容和s2一样,将s1和tmp进行交换即可。因为上文已经实现了拷贝构造函数,所以这里在构造tmp对象的时候也可以调用拷贝构造函数。
(3)更为简洁的现代写法
//s1=s2,s是s2的一份拷贝,这里让s做tmp完成交换
string& operator=(string s)
{
swap(s);
return *this;
}
这种写法需要注意,形参不能用引用,如果用引用的话,s是s2的别名,s和s1进行交换也就是s2和s1进行交换,会改变s2的值。
4.8 插入操作
4.8.1 push_back
void push_back(char ch)//尾插一个字符
{
if (_size == _capacity)//满了就需要扩容
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
因为_size指向的是有效字符的下一个位置,所以在下标为_size的位置直接插入ch即可,因为是字符串,所以插入完成后要在下一个位置要添上\0
在空间容量满的时候,需要扩容,再来看看扩容的实现吧
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
自己实现扩容,只能实现异地扩。即自己再新开一块空间,将原有的内容拷贝过去,_str指向新开辟的空间,_capacity更新为现有的容量,别忘了原来开辟的空间也要释放掉。
实现了reserve,再来看看和它很像的resize函数吧
resize是开空间并初始化
void resize(size_t n, char ch = '\0')
{
if (n > _size)//初始化字符的个数比原有的有效字符个数多
{
reserve(n);//先调用reserve开好空间
int i;
for ( i = _size; i < n; i++)
{
_str[i] = ch;//将_size之后的字符初始化为ch
}
_str[i] = '\0';
_size = n;
}
else//初始化字符的个数比原有的字符个数少
{//删除数据,不初始化
_str[n] = '\0';
_size = n;
}
}
4.8.2 append
void append(const char* s)//尾插一串字符串
{
size_t len = strlen(s);
if (_size + len > _capacity)//先计算要插入字符的个数,空间不够就继续扩容
{
reserve(_size + len);
}
strcpy(_str + _size, s);//将要插入的字符串s拷贝到原有字符串的后面
_size += len;//更新有效字符的个数
}
注意这里扩容不能像以前一样直接扩二倍,有可能插入的字符串很长,扩二倍容量还是不够,所以需要多少空间就扩多少。
void append(const string &s)//插入string类型的对象
{
append(s._str);//调用尾插字符串的函数
}
4.8.3 operator+=
直接复用之前写好的尾插函数
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
string& operator+=(const string& str)
{
append(str);
return*this;
}
4.9 在任意位置插入
任意位置插入一个字符
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)//把pos位置到结尾的字符统一向后移动一个单位长度
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;//ch放入pos位置
_size++;
return *this;
}
任意位置插入一串字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);//计算出要插入字符的个数
//空间不够就扩容
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end>=pos+len)//pos位置至结尾的字符统一向后移动len个单位长度
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str + pos, str, len);//插入的字符串拷贝过来
_size = _size + len;
return *this;
}
扩容的时候同样也要注意,不能扩二倍,而是需要多少空间就扩多少
4.10 任意位置删除
string& erase(size_t pos, size_t len = npos)
{
//从pos位置开始删除len个字符
if (len == npos || pos + len >= _size)//直接从pos位置删到结尾
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);//pos+len位置之后的字符拷贝到pos位置处
_size = _size - len;
}
return *this;
}
第一种情况,如果从pos位置之后要删除完,那么pos位置直接置为’\0’就好,第二种情况,没有删除完,那么pos+len位置之后的字符串要拷贝到pos位置处
4.11 查找
4.11.1 查找一个字符
size_t find(char ch, size_t pos = 0)const
{
//从pos位置开始查找一个字符
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if(_str[i]==ch)//如果找到了,返回下标
return i;
}
return npos;//没找到,返回npos
}
4.11.2 查找一串字符
size_t find(const char* sub, size_t pos = 0)const
{
assert(sub);
assert(pos < _size);
char* ptr = strstr(_str + pos, sub);//调用strstr函数,看有没有匹配的子串
if (ptr == nullptr)
{
//ptr为空,说明匹配失败,返回npos
return npos;
}
else
return ptr - _str;//找到了,返回子字符串的起始下标
}
查找字符串的时候,调用的是c里面的strstr函数,看从pos位置往后的字符串里面有没有与sub匹配的字符串,有的话ptr就会指向找到的字符串,没有的话指向空。
4.11.3 返回子字符串
string substr(size_t pos = 0, size_t len = npos)const
{
//从pos位置开始,返回len个字符
assert(pos < _size);
size_t reallen = len;//记录返回字符的真实个数
if (len == npos || pos + len > _size)
{
reallen = _size - pos;//真实的个数为总的字符个数减去pos位置之前的个数
}
string sub;
for (int i = 0; i < reallen; i++)
{
sub += _str[pos + i];
}
return sub;
}
在返回子字符串的时候,我们是把从pos位置往后的len个字符+=到sub中,最后返回sub。在访问字符的时候,使用的是operator[ ],那么这就要求下标不能越界,但是有可能返回字符的个数len大于_size,或者从pos位置往后加上len个字符会大于_size,此时就要修改返回字符的真实个数。
4.12 其他接口
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 *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);
}
字符串之间的比较,直接调用strcmp函数即可,实现>和==,其他的复用。
4.13 输入输出
4.13.1 opeerator <<
ostream& operator<<(ostream& out, const string&s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
通过访问下标的方式,输出每一个字符
实现在string类的外面,但是并没有访问类里面的私有成员,所以也不用写为友元函数
4.13.2 operator>>
void clear()
{
_str[0] = '\0';
_size = 0;
}
istream& operator>>(istream& in, string& s)
{
s.clear();//s本身有数据,还要输入,要把之前的数据清空再输入
char ch;
ch = in.get();
const size_t N = 32;
char buff[N];
int i = 0;
//先把从键盘获取到的字符存储在buff数组里面
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)//buff数组满了
{
buff[i] = '\0';
s += buff;//将获取到的一串字符串+=到s上
i = 0;//下标置为0,重复利用buff数组,继续存储接下来的字符
}
ch = in.get();
}
//遇到空格或者换行,一串字符获取结束,将buff数组剩下的字符+=到s上
buff[i] = '\0';
s += buff;
return in;
}
当输入的字符串很长的时候,便会频繁+=,不断扩容,效率变低。所以这里采用先把字符存储在buff数组里面,buff数组满了以后再+=,并且buff数组的空间可以重复使用,会提高效率。
使用get()函数读取字符是因为cin在输入的时候以空格或者换行作为字符串的分割,如果使用cin读字符,便不会获取到空格或者换行,便无法得知字符输入何时结束,而get函数是cin的一个成员函数,它可以读取任意一个字符 。