string叫串,是一个管理字符数组的类,其实就是一个字符数组的顺序表,通过成员函数对字符串进行增、删、查、改。
C++标准库里面的东西都在std这个命名空间中。
int main()
{
string s1;
std:: string s2;
std::string name("xsq");
cout << name << " " << "and" << " ";
name = "lsw";
cout << name << endl;
return 0;
}
1. 为什么学习string类?
1.1 C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数, 但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1.2 两个面试题(暂不做讲解)
字符串转整形数字
字符串相加
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本 都使用string类,很少有人去使用C库中的字符串操作函数。
2. 标准库中的string类
2.1 string类(了解)
string类的文档介绍
1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作 单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信 息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits 和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1. string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string string;
4. 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std;
2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)
1. string类对象的常见构造
学习STL不要想着每个都掌握,不然会很难受,我们掌握最常用的,剩下的我们知道string可以怎么怎么样,用的时候查文档。
int main()
{
string s1; //构造空的string类对象,即空字符串
string s2("hello"); //用C-string来构造string类对象
string s3(10, '*'); //n个字符去初始化
string s3(s2); //拷贝构造
return 0;
}
拷贝一个字符对象的内容,从pos个位置开始,拷贝npos个字符。npos是string这个类里面的一个静态成员变量,这个地方不是-1,反而是整型的最大值。
int main()
{
//字符串大小的比较
string s1("hello world");
string s2(s1, 6, 5);
cout << (s1 == s2) << endl;
cout << (s1 > s2) << endl;
return 0;
}
和C语言一样,比较字符串的大小都是按照ASCI码比较。
int main()
{
string s1("hello world");
string s2(s1, 6);//后面没给参数,表示的是一个很大的值
cout << s2 << endl;
return 0;
}
https://cplusplus.com/reference/string/string/string/ 中https:是网络协议,cplusplus.com是域名
这三个是运算符重载,同时这三个函数又构成函数重载。
int main()
{
string s1;
string s2;
s1 = s2;
s1 = "1111";
s2 = '1';
return 0;
}
字符串的增:
如果我们想尾插一个字符,可以调用上面的成员函数。
如果我们想尾插一个字符串,可以调用上面的成员函数。
int main()
{
string s1("hello");
//尾插一个字符
s1.push_back(' ');
//尾插一个字符串
s1.append("world");
cout << s1 << endl;
return 0;
}
string的好处是空间不够,push_back、append可以扩容。
+=可以+=一个字符,+=可以+=一个字符串,+=可以+=一个string对象。
int main()
{
string s1("hello");
s1 += ' ';
s1 += "world";
cout << s1 << endl;
return 0;
}
本质还是尾插,只是在string类中重载了+=,operator+=一个字符,就是调用push_back, operator+=字符串,就是调用append。
把x转成string对象:
int main()
{
//要求x转成string对象
size_t x = 0;
//cin >> x;
string xstr;
while (x)
{
size_t val = x % 10;
xstr += ('0' + val);
x /= 10;
}
cout << xstr << endl;
return 0;
}
字符是存储的ASCII码,数字1对应的ASCII码不是字符1,需要加上字符'0'才是字符'1'的ASCII码。
.sln是打开项目的额文件。
但是上述的结果不符合我们的需求,我们还需要逆置一下,这里就不做过多讲解。
这里重载了一个运算符operator[],重载了一个[],[]这个运算符本质是一种解引用,是数组来访问它的数据的。重载[]之后string字符串可以当作数组使用。
int main()
{
string s1("hello");
//字符串的修改
for (size_t i = 0; i < s1.size(); i++)
{
s1[i]++;
}
s1[0]--;
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
cout << endl;
return 0;
}
迭代器是如何访问的呢?迭代器是像指针一样的东西,但不一定是指针。迭代器是增加一种访问的方式,迭代器的代码是怎么写的,任何容器的迭代器都是这样的,下节课我们会学string的底层,其实string底层就是指针。
begin对于任何的容器都是第一个位置的迭代器,
下面这两句代码看似相同,实际上底层有很大差别。
s3[1]; //*(s3+1);
s1[1]; //s1.operator[](1);
int main()
{
//string对象的两种实例化方式
string s0;
string s1("hello");
//遍历string的三种方式
cout << s1 << endl;
//下标 + [],用于string和vector
cout << s1.size() << endl;//size()不算字符串末尾的\0
//\0不算有效字符,\0是一个特殊字符,标识字符串结束的特殊字符
//有些地方\0不会显示的打印出来,其实已经访问到了
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
cout << endl;
//迭代器
string::iterator it = s1.begin();
while (it != s1.end())//end返回的是最后一个数据的下一个位置,end指向的是\0
{
(*it)--;
it++;
}//迭代器也可以修改数据
it = s1.begin();
while (it != s1.end())
{
cout << *it;
it++;
}
cout << endl;
//第三种访问string的方式,范围for,自动迭代,自动判断结束
//范围for从底层的角度来说,底层是替换为迭代器
for (auto e : s1)//依次取s1的数据赋值给给变量e,这个变量自动推导出类型char
{
cout << e;//这里的e是*it的拷贝
}
cout << endl;
//其实也可以下面这样写
for (char e : s1)
{
cout << e;
}
cout << endl;
for (auto& e : s1)
{
e++;//这里的e是*it的别名
}
cout << endl;
//根本就没有范围for,实际底层都是迭代器,有了迭代器才有了范围for
//任何容器都支持迭代器,并且用法是类似的
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator vit = v.begin();
while (vit != v.end())
{
cout << *vit;
vit++;
}
cout << endl;
//下面这两个是函数模板
reverse(s1.begin(), s1.end());
reverse(v.begin(), v.end());
//范围for只支持顺着遍历,不支持倒着遍历
for (auto e:s1)
{
cout << e;
}
cout << endl;
for (auto e: v)
{
cout << e;
}
cout << endl;
sort(v.begin(), v.end());
for (auto e : v)
{
cout << e;
}
return 0;
}
总结:iterator提供了一种统一的方式访问和修改容器的数据,这种统一的方式可以去访问简单的数组,稍微复杂的链表、更复杂的树、哈希表等等这样的结构。
是链表、树啥的都可以逆置。
sort要求传的还是迭代器,链表不支持用sort。
迭代器除了提供普通迭代器,还提供了反向迭代器,就是有时候我们需要倒着遍历。
rbegin()在最后一个位置,rend()在第一个数据的前一个位置。范围for不能倒着遍历,只有反向迭代器能倒着遍历。
int main()
{
string s1("hello world");
//有了迭代器后,auto的价值就更明显了
//string::reverse_iterator vit = s1.rbegin();
auto vit = s1.rbegin();
while (vit != s1.rend())
{
//cout << *vit;
//vit++;
//也可以修改数据
*vit += 3;
//cout << *vit;
//vit++;
}
cout << endl;
return 0;
}
//下面的代码编不过
void Func(const string& s)
{
string::iterator vit = s.begin();
while (vit != s.end())
{
cout << *vit;
vit++;
}
}
int main()
{
string s1("hello world");
Func(s1);
return 0;
}
const对象不能用普通的迭代器,要用const迭代器。
这相当于一个权限的放大问题,普通迭代器可以读和写,const迭代器只能读不能写。
//下面这样就可以了
void Func(const string& s)
{
string::const_iterator vit = s.begin();
while (vit != s.end())
{
cout << *vit;
vit++;
}
}
int main()
{
string s1("hello world");
Func(s1);
return 0;
}
int main()
{
//size和length的返回结果是一样的,size的意思是数据个数,length是长度的意思。
string s1("hello world");
cout << s1.size() << endl;
cout << s1.length() << endl;return 0;
}
大家可以发现,string不在容器这个位置上,严格来说,string不属于STL,属于标准库,STL属于标准库的一部分,string属于标准库,string产生的比较早。string最开始定义的接口是length,STL出来以后被迫加了一个size,顺序表、string、甚至链表都可以叫长度。
返回字符串的最大长度。不同的编译器或者不同版本的编译器得到的结果是不一样的,不稳定。
capacity可以看容量。
同样的代码,在Linux下的capacity是11。
clear是清理数据的意思,clear会让size改变为0,不能让capacity改变。
会对我们的容量进行改变:
这里要插入字符串就不会扩容了,空间已经开好了。reserve的应用场景:提前知道要开多少空间,提前把空间开好。
int main()
{
string s2;
s2.reserve(200);
cout << s2.capacity() << endl;
return 0;
}
vs下给出的空间会比提前开的空间大,在Linux下开多少,空间就是多少。
假如reserve100个空间,它可能给的不是100,可能会比100大,而且如果显示的是空间大小是111,它的实际大小会是112,\0也会占用一个空间。如果我们用clear清理数据,再reserve个空间,那么它的容量就会缩小。
reserve是单纯的开空间,也就是对空间进行改变,resize是开空间+填值初始化,初始化的值是"\0"。
空间是不会轻易的缩小,如果缩容也是开辟一块新的空间,把原有的数据部分拷贝到新的空间中,把原来的空间释放掉。
at跟[]的功能是一样的,但是我们平常不太使用,at是早些时候没有支持运算符重载的时候提供的。at如果访问的数据超出会抛异常,[]是断言,这两个读和写都会报错。
int main()
{
string s1("hello world");
s1.at(0) = 'x';
cout << s1 << endl;
return 0;
}
assign是一个赋值的意思,就是我对你覆盖了。
int main()
{
string s1("hello world");
s1.append("sssssss");
cout << s1 << endl;
s1.assign("1111111");
cout << s1 << endl;
return 0;
}
int main()
{
string s1("hello world");
s1.append("sssssss");
cout << s1 << endl;
s1.assign("1111111");
cout << s1 << endl;
s1.insert(0, "hello");
cout << s1 << endl;
s1.insert(0, 10, 'x');
cout << s1 << endl;
s1.insert(s1.begin(), 10, 'y');
cout << s1 << endl;
s1.insert(s1.begin() + 10, 10, 'z');
cout << s1 << endl;
return 0;
}
insert虽然好,但是也不要多用,因为有效率的问题。
int main()
{
string s1("hello world");
s1.erase(5, 1);
cout << s1 << endl;
s1.erase(5);
cout << s1 << endl;
string s2("hello world");
s2.erase(s2.begin());
cout << s2 << endl;
return 0;
}
int main()
{
string s1("hello world hello bit");
s1.replace(6, 5, "xxxxxxxxxxx");
cout << s1 << endl;
//讲s2中的空格替换为20%
string s2("hello world hello bit");
string s3;
for (auto ch : s2)
{
if (ch != ' ')
{
s3 += ch;
}
else
{
s3 += "20%";
}
}
s2 = s3;
cout << s2 << endl;
return 0;
}
int main()
{
string s1("hello world hello bit");
s1.replace(6, 5, "xxxxxxxxxxx");
cout << s1 << endl;
//讲s2中的空格替换为20%
string s2("hello world hello bit");
string s3;
for (auto ch : s2)
{
if (ch != ' ')
{
s3 += ch;
}
else
{
s3 += "20%";
}
}
s2 = s3;
//这两个打印的结果虽然一样,但是第一个调用的是string中重载的流插入和流提取
//第二个是识别成char* ,是调用库里面的对这个的支持
cout << s2 << endl;
cout << s2.c_str() << endl;
return 0;
}
c_str是更好的跟C的一些接口进行配合,C++要兼容C,你需要用某些库,不排除那个库是用C语言写的,比如做个简单的比方,C++里面有些要访问数据库的时候,它会给你提供一些api,方便去访问、连接数据库等等,对于C/C++它没有专门去访问C++的版本,它只提供了一个C的版本,它的这个C版本,如果你是纯C的项目可以用,C++的项目也可以用。