说明:
针对STL每个类的讲解都会分为:接口介绍和模拟实现。接口介绍是为了让大家学会使用,模拟实现是为了让大家更加了解该类。模拟实现并不是为了造出更好的轮子。
这里推荐一个查看C/C++语法的网站:https://cplusplus.com/
还有一个C++官方的网站:https://cppreference.com/不过我不推荐,因为不好找。
后面对每个类的接口介绍并不需要全部记住,只要记住常用的即可,其他需要用的时候可以查。
string类的接口我会讲的比较详细,后面其他类其实都是类似的,就快速过了。
1、string简介
string是C++中的字符串类型,有了它我们可以更加方便的操作字符串。它的基本结构类似下面:
class string
{
public:
char* _str;
size_t _size;
size_t _capacity;
};
_str是个字符指针,_size代表的是字符串中有效字符个数(不包括\0),_capacity是容量代表当前字符指针所指向空间的大小。
这时候你可能认为string并不是类模板,实际上它是类模板,只不过被typedef重命名了。如下图:
我们还发现了有其他类型:
这是因为编码不同,其他类型对应的是其他的编码,string对应的是ASCII编码。这里就不过多赘述,平时我们使用string即可。
2、构造函数
C++98给出了7个构造函数,如下:
来看看如何使用:
string s("hello world");
const char* str = "hello world";
string s1; // 无参构造
string s2(s); // 拷贝构造
string s3(s, 6);
string s4(str);
string s5(str, 5); // str的前五个字符
string s6(10, 'c'); // 用十个字符'c'构造字符串
对每个字符串输出:
关于构造和拷贝构造就不在赘述了。
第三个构造函数:pos代表从pos位置开始,len表示长度。我们发现这里给了个缺省值npos。那么npos是什么?
npos是string类内的一个静态const成员变量,实际上是size_t——无符号整型,而无符号的-1就是整数最大值。那么这里长度就是一个非常大的数。
那么意思就是从串s的下标6开始,长度为无限长,也就是到\0为止。所以构造出来的s3就是"world"。
如果是string s3(s, 6, 3); 代表就是从s的下标6开始的3个字符,那么构造出来的串就是"wor"。只要len+pos>=字符串长度,那就是将字符串后面的所有字符拿来构造。
第四个构造函数:用常量字符串来构造,这个很好理解。
第五个构造函数:用常量字符串的n个字符来构造,这里给的是5,那就是用前五个字符来构造,那么构造出来的s5就是"hello"
第六个构造函数:用n个字符c来构造,这里给的是10,‘c’,所以构造出来了10个c的字符串。
第七个构造函数:这里涉及迭代器,暂时不讲。
3、赋值运算符重载
string s1("hello world");
string s2("hello linux");
string s3("hello c++");
s1 = s2;
s2 = "hello java";
s3 = 'c';
注意:这是赋值运算符重载函数,对象已经被创建出来。
4、迭代器
上面八个函数都是用来获取迭代器的。迭代器是什么?实际上迭代器是一种类似指针的类型,它可能是指针,也可能不是指针,对于目前我们讲的string类,它的迭代器就是char*的指针,但是对于后面要讲的list(双向循环链表),它的迭代器就不是指针了,而是一个自定义类型。
现在我们要遍历输出string字符串,如下:size()这个函数返回的就是string中的有效字符个数(不包括\0)
string s = "hello world";
for (int i = 0; i < s.size(); i++)
{
cout << s[i];
}
上面是一种写法,我们还可以使用迭代器来写:通过类对象::iterator来获取普通迭代器,begin返回的是第一个数据的位置,end返回的最后一个数据的下一个位置。
我们前面讲了范围for,前面是用数组来演示,这里我们可以使用对象:
string s = "hello world";
for (const auto& e : s)
{
cout << e;
}
cout << endl;
范围for看似很牛,但实际上范围for的底层是迭代器,因为string支持了迭代器,所以可以使用范围for,如果不支持迭代器,就无法使用范围for了。下面看vs下范围for和迭代器的对比就知道了:
上面的是对于普通对象的遍历,如果是const对象呢?——使用const_iterator
string s = "hello world";
string::const_iterator cit = s.begin();
while (cit != s.end())
{
cout << *cit;
++cit;
}
cout << endl;
需要注意的是,const_iterator是不能修改值的,而上面的普通迭代器可以修改。
还有反向迭代器reverse_iterator,针对const类型也有const反向迭代器const_reverse_iterator:
还有另外四个函数:cbegin、cend是用来匹配const_iterator的,crbegin、crend是用来匹配const_reverse_iterator的。但是实际上begin、end和rbegin、rend都有重载,所以可以直接使用begin、end。和rbegin、rend如下图:
5、容量相关函数
前面迭代器用过了,size返回的是有效字符的个数(不包括\0)
length:实际上和size一样,返回字符串长度,这里有些渊源,平时直接使用size就好。
max_size:返回string可以开辟的最大容量,不同平台下不一样,所以没有什么价值。
resize:开空间+初始化。
capacity:获取当前字符串的容量大小。
reserve:修改容量——可以理解为扩容,缩容不明确。
clear:清空字符串所有内容。
empty:判断字符串是否为空。
重点来看resize和reserve:
resize是开空间加初始化,reserve是开空间。
resize有两个函数,第一个函数是开至少n的空间,然后后面的空间都初始化为\0,第二个函数是开至少n的空间,并初始化为给定字符c。reserve就是开至少为n的空间。
这里要注意开的空间不同编译器实现不一样,vs下开的空间是大于n的,而g++下测试开的是刚好为n的。所以这里开的空间是大于等于n的。如果给比原来容量小的值是否缩容这个是不一定的,当然一般也不会这么用。
补充:vs下string的扩容是按1.5倍扩容的,linux下g++编译器是按2倍扩容的。
6、元素访问
元素访问这里最直白的就是重载了operator[],可以像数组那样使用。back返回最后一个元素,front最开始的元素。
operator[]支持普通对象和const对象,const对象无法对返回值进行修改。
string s = "hello world";
// 像数组一样使用
cout << s[0] << endl; // h
s[0] = 'a'; // h->a
at函数也支持普通对象和const对象,pos是要访问的下表,跟上面差不多:
string s = "hello world";
cout << s.at(0) << endl; // h
s.at(0) = 'a'; // h->a
s.back(); // 返回'd';
s.front(); // 返回'a'
7、修改类函数
重载了operator+=,这让我们修改字符串会方便,如下:
string s1 = "hello world";
string s2 = "hello c++";
s1 += s2;
s1 += "hhhhhhh";
s1 += 't';
append函数就是在字符串后面添加内容,这里有六个函数,第二个函数类似前面的构造函数,subpos表示开始的下表,sublen表示长度。使用如下:
string s1 = "hello world";
string s2 = "hello c++";
s1.append(s2); // hello worldhello c++
s1.append(s2, 6, 3); // hello worldhello c++c++
s1.append("hhhhhhhhhhh"); // hello worldhello c++c++hhhhhhhhhhh
s1.append("xxxxxxxx", 5); // hello worldhello c++c++hhhhhhhhhhhxxxxx
s1.append(10, 'y'); // hello worldhello c++c++hhhhhhhhhhhxxxxxyyyyyyyyyy
s1.append(s2.begin(), s2.end());// hello worldhello c++c++hhhhhhhhhhhxxxxxyyyyyyyyyyhello c++
这个函数用来在尾部插入一个字符,使用如下:
string s = "hello world";
s.push_back('c'); // hello worldc
这个函数是把字符串清空,替换成新的,使用如下:
string s1 = "hello world";
string s2 = "linux";
s1.assign(s2); // linux
s1.assign(s2, 2, 3); // nux
s1.assign("dasjk"); // dasjk
s1.assign("hello linux", 5); // hello
s1.assign(5, 'x'); // xxxxx
s1.assign(s2.begin(), s2.end()); // linux
这个函数是实现在字符串的任意位置插入字符或字符串的。使用如下:
string s1 = "hello";
string s2 = "xxx";
s1.insert(2, s2); // hexxxllo
s1.insert(0, s2, 1, 2); // xxhexxxllo
s1.insert(0, "axj"); // axjxxhexxxllo
s1.insert(6, 3, 'y'); // axjxxhyyyexxxllo
s1.insert(s1.end(), 2, 'z'); // axjxxhyyyexxxllozz
s1.insert(s1.begin() + 2, 'k'); // axkjxxhyyyexxxllozz
s1.insert(s1.begin(), s2.begin(), s2.end()); // xxxaxkjxxhyyyexxxllozz
这个函数是用来删除,使用如下:
string s = "hello world";
s.erase(5); // hello
s.erase(s.begin()); // ello
s.erase(s.begin() + 2, s.end()); // el
需要注意的是,给的迭代器区间都是左闭右开的,右边都是指向最后一个元素的下一个位置。
replace函数是用来替换区间中的字符串的,这里就举几个例子:
string s = "hello world";
string ss = "hhhh";
s.replace(0, 5, "linux"); // linux world
s.replace(1, 4, ss); // lhhhh world
s.replace(0, 20, 3, 'x'); // xxx
swap函数是用来交换两个对象的成员变量的。
交换后,str1就变成了str2,str2变成了str1.
这个函数就很好理解了,删除最后一个字符。
8、字符串操作函数
这个函数返回c的字符串指针,由于string重载了流插入,所以可以直接用cout输出。也可以用这个函数获取返回值输出:
string s = "hello";
// 以下结果相同,但是含义不同。一个是重载了流插入,另一个是字符指针。
cout << s << endl;
cout << s.c_str() << endl;
当使用cout其实还没有什么作用,但如果用printf输出,这里的c_str函数作用就很大了:
string s = "hello";
printf("%s\n", s.c_str());
find函数用来查找字符串或者字符,找到就返回下标,找不到就返回npos,使用如下:
string s1 = "hello linux";
string s2 = "hello";
int x1 = s1.find(s2, 0); // 从0下表开始找,找到返回下标
cout << x1 << endl; // 输出0
int x2 = s1.find("linux", 0);
cout << x2 << endl; // 输出6
rfind基本上同find,只不过rfind是从后往前找,这里不再赘述。
substr用来截取字符串,并转换为string对象返回。使用如下:
string s = "hello linux";
string ss = s.substr(0, 5);
cout << ss << endl; // 输出hello
9、非成员函数
重载了>>运算符,所以可以直接用cin读取。同时重载了<<可以直接cout输出。另外还有一个getline函数,这个getline函数是用来读取一整行的,包括空格和换行。而我们用cin遇到空格和换行就会停止读取。所以如果要读取带有空格的字符串就要使用getline。
getline函数使用如下: