目录
一.为什么学习string类?
1.1 C语言的字符串
二. 标准库中的string类
2.1 string类(了解)
2.2 string类成员函数
● string类对象的常见构造
● string类析构函数
● 赋值重载
2.3 string的迭代器
<1>正向迭代器 Iterator
<2> 反向迭代器 reverse_iterator
<3> 迭代器访问const类型容器
2.4 string类的容量操作
2.5 string类的访问修改
<1>元素访问
<2>元素修改
<3>子字符串
2.6 string类的查找
2.7 string类的非成员函数
● 运算符重载+
● 比较运算符重载
● getline(将线从流转换为字符串)
一.为什么学习string类?
1.1 C语言的字符串
C语言中,字符串是以 '\0' 结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合面向对象编程(OOP)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、 快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
string类相比C语言的字符串更安全、更方便、更高效的使用,是面向对象编程的思想体现
二. 标准库中的string类
2.1 string类(了解)
string类的文档介绍
在使用string类时,必须包含头文件#include<string>以及using namespace std;
2.2 string类成员函数
● string类对象的常见构造
<1>string(); 无参构造,空的 string 只有 "\0"
<2>string (const string& str); 拷贝构造函数
<3>string (const string& str, size_t pos, size_t len = npos);
(其他负数也可以起到一样的效果)
拷贝构造,在指定下标 pos 位置拷贝 len 个字符 默认值为 npos 类型为无符号整数类型,因此 npos 为无符号整数最大值,库规定 len 大于字符串长度,则拷贝整个 str 到结束
<4>string (const char* s); 将字符串拷贝给 string 类
<5>string (const char* s, size_t n); 将字符串的前 n 个拷贝给 string 类
<6>string (size_t n, char c); 将 n 个字符 c 拷贝给 string 类
int main()
{
string s1;//无参构造 只有\0
string s11{};//同上 string s1(); 编译器可能认为声明
string s2("hello world");//将括号内字符串拷贝给s2
string s3(s2, 0, 2);//将s2从第0个位置拷贝2个元素给s3
string s4(s2);//拷贝构造 将s2类拷贝给s4
string s5(s2, 5);//拷贝构造 将s2类的前5个字符拷贝给s5
string s6(8, 'a');//拷贝n个字符a给s6
cout << s1 << endl;
cout << s11 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
return 0;
}
● string类析构函数
当字符串生命周期结束,会自动调用析构函数。
● 赋值重载
2.3 string的迭代器
<1>正向迭代器 Iterator
提供了一种通用的(所用)访问容器的方式
string s1("hello");
// it 作用类似指针
string::iterator it = s1.begin();
while (it != s1.end())//end 返回最后一个字符下一个位置 左闭右开
{
*it += 2;//可以修改内容
cout << *it << ' ';
++it;
}
cout << endl;
● begin 返回 指向 string 第一个字符
● end 返回 指向 string 最后一个有效字符下一个位置 \0
<2> 反向迭代器 reverse_iterator
string s1("hello");
// it 作用类似指针
string::reverse_iterator it = s1.rbegin();//反向指向string的最后一个有效字符 视为开始端
auto end = s1.rend();//指向 string的第一个有效字符前一个位置 视为结尾端
while (it != s1.rend())
{
cout << *it << ' ';
++it;
}
cout << endl;
● rbegin
● end
<3> 迭代器访问const类型容器
普通的迭代器是可读可写的,const迭代器访问const修饰的容器只能读不能写(指向的内容)
Tip:const迭代迭代器不是指const 修饰迭代器,因为迭代器本身可以修改,而它指向的内容不能修改!
string s1("hello");
// it 作用类似指针
string::const_iterator it = s1.cbegin(); //也可以写成 begin 相当于权限缩小
while (it != s1.end())
{
/**it += 2;*/
cout << *it << ' ';
++it;
}
cout << endl;
三种访问方式:
string s1("hello world");
cout << s1 << endl;
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i] << ' ';
}
cout << endl;
string::iterator it = s1.begin();
auto it2 = s1.begin();
while (it2 != s1.end())
{
cout << *it2 << ' ';
it2++;
}
cout << endl;
//自动推导类型 自动赋值 自动迭代结束 底层迭代器
//本质拷贝赋值 不能修改原数据 将*it值 赋值给 c
for (auto c : s1)
{
cout << c << ' ';
}
cout << endl;
//引用 可以修改
for (auto& c : s1)
{
cout << c << ' ';
}
cout << endl;
2.4 string类的容量操作
std::string 里内部结构可能优化了,比如 char[] _buff 在这种情况下,短字符串可能直接存储在 std::string 对象的内存空间内,而不是在堆上分配。但是,当字符串增长超出了这个内部空间时,就会触发堆上的内存分配和扩容。
连续尾插测试扩容机制:
int main()
{
string s;
size_t sz = s.capacity();
cout << "capacity changed:" << sz << '\n';
cout << "making a 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';
}
}
return 0;
}
可以看到 由于 _buff 的存在 初始容量为15个字节,再继续尾插之后该编译器出现了 1.5倍的扩容机制以减少将来可能的内存重新分配次数,提高性能。
容量只显示有效字符的个数,内存会比容量多一个空间(存储‘ \0 ’)
● 总结:
<1> size() 与 length() 方法底层实现原理完全相同,引入size() 的原因是为了与其他容器的接
口保持一致, 一般情况下基本都是用size() 。
<2> capacity() 返回有效数据空间大小,不包含 \0 实际需要多开一个空间
<3> clear() 将string中有效字符清空,不改变底层空间大小。
<4> resize(size_t n) 与 resize(size_t n, char c) 都是将字符串中有效字符个数改变到n个 ,不
同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char
c) 用字符c来填充多出的元素空间 。
注意: resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
<5>reserve(size_t res_arg=0) 为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
vs下,有效元素个数小于预留空间大小时,不缩容。
g++下,有效元素个数小于预留空间大小时,缩容。
int main()
{
string s2("hello xxxxxxxxxxxxxxxxxx");
cout << s2.size() << endl;
cout << s2.capacity() << endl << endl;
//给一个小于 size的值
s2.reserve(20); //结果不变
cout << s2.size() << endl;
cout << s2.capacity() << endl << endl;
//给一个 size与 capacit 中间值
s2.reserve(28); //结果不变
cout << s2.size() << endl;
cout << s2.capacity() << endl << endl;
//给一个 大于 capacit 值
s2.reserve(40); //扩容
cout << s2.size() << endl;
cout << s2.capacity() << endl << endl;
s2.clear();//清楚数据 不清容量
cout << s2.size() << endl;
cout << s2.capacity() << endl << endl;
return 0;
}
2.5 string类的访问修改
<1>元素访问
● 运算符重载[ ] 返回pos位置字符的引用
模拟实现:
char& operator[](size_t pos)
{
assert( pos>=0 &&pos < _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos >= 0 && pos < _size);
return _str[pos];
}
● at 获取字符串中的字符
at 函数自动检查 pos 是否是字符串中字符的有效位置(即 pos 是否小于字符串长度),如果不是,则抛出 out_of_range 异常。
<2>元素修改
● push_back 尾部插入一个字符c
● append 在字符串后追加字符串str
● 赋值重载+= 在字符串后追加字符串str
int main()
{
string s2("world");
string s("hello worold");
s.push_back(' ');//单个字符
s.push_back('x');
s.append(" ");
s.append(s2);//字符串
cout << s << endl;
s += ' ';
s += "ccccccc";
cout << s << endl;
return 0;
}
注意:
1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多,一般情况下string类的 += 操作用的比较多,+= 操作不仅可以连接单个字符,还可以连接字符串。
2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
● insert(插入) 在pos或p位置之前插入字符串
● erase(删除)pos位置删除字符串
● replace(替换)
int main()
{
string s("hello worold");
//模拟头插 需要移动数据 效率相对低
s.insert(0, "hello xc ");
cout << s << endl;
//模拟头删 需要移动数据
s.erase(0, 1);
cout << s << endl;
s.erase(s.begin());
cout << s << endl;
//模拟尾删
s.erase(--s.end());
cout << s << endl;
s.erase(s.size() - 1, 1);
cout << s << endl;
//将空格替换
string ss("hello world");
cout << ss << endl;
ss.replace(5, 1, "%%");
cout << ss << endl;
cout << endl;
return 0;
}
<3>子字符串
● c_str(兼容C)返回一个指向字符数组的指针 数组内容即 string 对象
// string 不能直接与 文件操作对接
string file;
cin >> file;
FILE* fout = fopen(file.c_str(),"r");
char ch = fgetc(fout);
while (ch != EOF)
{
cout << ch;
ch= fgetc(fout);
}
fclose(fout);
● data(类似于c_str)
● substr(子串)在str中从pos位置开始,截取n个字符,然后将其返回
2.6 string类的查找
● find(正向) 在字符串里查找字符数据 pos位置开始 n个数据
//将字符串的空格替换成百分号
//法一 移动数据频繁 效率低
string sss("hello world hello bit");
cout << sss << endl;
size_t pos = sss.find(' ');
while (pos != string::npos)
{
sss.replace(pos, 1, "%%");
pos = sss.find(' ',pos+2);
}
cout << sss << endl<<endl;
//法二 构造新串 空间换时间
string tmp;
tmp.reserve(sss.size());
for (auto ch : sss)
{
if (ch == ' ')
tmp += "%%";
else
tmp += ch;
}
cout << tmp << endl;
sss.swap(tmp);// 交换 字符指针
cout << sss << endl << endl;
● rfind(倒着找) 与find 参数一致意义 倒着查找
//找出文件后缀
string s2("test.cpp.zip");
size_t pos2 = s2.rfind('.');
string suffix2 = s2.substr(pos2);
cout << suffix2 << endl;
● find_first_of 在字符串中搜索与其参数中指定的任何字符匹配的第一个字符。
int main()
{
string s3("hello world hello xc");
cout << s3 << endl;
size_t pos3 = s3.find_first_of("abcd");// find_first_of 给定一个需要查找的字符串,一个一个查找,返回索引
while (pos3 != string::npos)
{
s3[pos3] = '*';
pos3 = s3.find_first_of("abcd", pos3 + 1);
}
cout << s3 << endl;
return 0;
}
● find_last_of(倒着找)倒序查找
void SplitFilename(const std::string& str)
{
std::cout << "Splitting: " << str << '\n';
std::size_t found = str.find_last_of("/\\");
std::cout << " path: " << str.substr(0, found) << '\n';
std::cout << " file: " << str.substr(found + 1) << '\n';
}
//文件路径分离
string str1("/usr/bin/man");
string str2("c:\\windows\\winhelp.exe");
SplitFilename(str1);
SplitFilename(str2);
● find_first_not_of 查找字符串中与参数中指定的任何字符都不匹配的第一个字符
● find_last_not_of 倒叙查找字符串中与参数中指定的任何字符都不匹配的第一个字符
2.7 string类的非成员函数
● 运算符重载+
返回一个新构造的字符串对象,其值是lhs中字符的连接,后跟rhs中的字符。
int main()
{
string s1("hello");
string s2 = s1 + " wrold";
string s3 = s1 + s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
return 0;
}
● 比较运算符重载
在字符串对象lhs和rhs之间执行适当的比较操作
● getline(将线从流转换为字符串)
从is中提取字符并将其存储到str中,直到找到分隔符delim
找最后一个单词长度:
#include <iostream>
using namespace std;
int main() {
string str;
//cin >> str;// cin 和 scanf 将空格和换行默认成分割
getline(cin,str);// 无定界默认遇到换行才停止输入
size_t pos = str.rfind(' ');
cout << str.size() - (pos + 1) << endl;
}
可以自定义分隔符
getline(cin,str,"*");//流提取,直到遇到*才停止