文章目录
- 一、string的默认成员函数
- 1. 构造函数
- 2.赋值重载
- 二、string的常见容量操作
- 1.size、length、capacity
- 2.clear
- 3.empty
- 4. reserve
- 5. resize
- 三、string的访问及遍历操作
- 1. operator[]
- 2. 迭代器
- 3. 范围for
- 四、string的修改操作
- 1. push_back
- 2. append
- 3. operator+=
- 4. c_str
- 五、string的非成员函数重载
- 1. operator+
- 2. operator>>和operator<<
- 3. getline
- 4. relational operators
一、string的默认成员函数
1. 构造函数
我们可以看到在C++98中光构造函数就有7个重载,其中框住的三个是需要重点掌握的,下面分别演示一遍。
string();// 构造一个空字符串
string (const char* s);// 用C-string来构造string类对象
string (const char* s, size_t n);// 用C-string的前n个字符来构造string类对象
string (size_t n, char c);// 生成n个c字符的字符串
string (const string& str);// 利用原先的字符串做拷贝构造
// 拷贝str字符串中从pos位置开始的len个字符
string (const string& str, size_t pos, size_t len = npos);
2.赋值重载
string& operator= (const string& str);// 将一个string对象赋值给到另一个
string& operator= (const char* s);// 将一个字符串赋值给到string对象
string& operator= (char c);// 将一个字符赋值给到string对象
二、string的常见容量操作
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear (重点) | 清空有效字符 |
reserve (重点) | 为字符串预留空间 |
resize (重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
1.size、length、capacity
- 【size】和【length】代表的含义一样都是表示字符串中有几个字符,而【capacity】则表示容量为多少。比如一个教室做多容纳50个学生,现在教室里有20个学生。这里的50就代表是容量【capacity】这里的20就代表【size】和【length】。
- 可以看到s一共有13个字符(其中包含一个空格),所以【size】和【length】的值为13,而capacity的值为15。这里capacity的值是会随之你插入字符的增多自动扩容的。
- 有人会问那【size】和【length】表示的含义是一样的,那有一个不就行了,为什么还要设计出两个来呢?那是因为【size】是STL中的接口,string出现的比STL要早,也就是说刚开始string只有【length】这个接口,但在STL出来之后为了接口的统一,string就又加上了【size】。
下面我们来探究一下【capacity】
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "making s 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';
}
}
}
- 我们可以看到在vs中是1.5倍的扩容逻辑
- 在Linux中是两倍的扩容逻辑
- 这是因为不同平台下这个STL库的内部实现是不一样的,甚至都是在VS中,使用不同版本的VS也会导致扩容大小不同。
2.clear
【clear】的作用就是将字符串清空将【size】改为0,那它还有什么细节呢?我们用下面的函数进行测试。
void Teststring1()
{
// 注意:string类对象支持直接用cin和cout进行输入和输出
string s("hello, bit!!!");
cout <<"clear前size:" << s.size() << endl;
//cout << s.length() << endl;
cout << "clear前capacity:" << s.capacity() << endl;
cout << s << endl;
// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
s.clear();
cout << "clear后size:" << s.size() << endl;
cout << "clear后capacity:" << s.capacity() << endl;
}
- 可见对于clear来说,它只会清空字符串中的【size】原来是13,现在清空了变成了0,但对于【capacity】来说是不会有什么变化的,原来是15现在还是15。如果你是这个接口的实现者一定不会去清空这个【capacity】容量的大小,万一后面又需要插入字符呢?此时又需要去进行一个扩容,是非常麻烦的。
3.empty
【empty】就是判断字符串是否为空,如果为空返回真(1),否则为假(0)。
- 可见一开始不为空所以为假输出0,clear之后为空是真输出1。
4. reserve
【reserve】的功能是提前为一个字符串开好空间。
- 可以看到在vs中我们提前用reserve开了空间之后就不会在进行扩容操作了。但是这里我们发现用的是reserve(100),开了一百个空间,但实际上vs却开了111个空间,而Linux中确实说开100就开100。究其原因还是平台不同底层实现不同。
5. resize
void resize (size_t n);
void resize (size_t n, char c); // 初始化数据为n个c字符
对于reserve来说只会改变capacity,size原来是多少就是多少不改变,但resize是二者都改变,size变成’n’,capacity遵循vs的扩容规则比n多一些。
- 如果 n > 当前的字符串长度,则在末尾插入所需数量的字符以达到n的大小来扩展当前内容,没有指定则填充’\0’。
- 如果n < 当前的字符串长度,则会去进行一个删除的操作,删除第n个字符之后的字符。
第一点上面已经验证,第二点如图,我们发现会出现截断字符的现象。
s.resize(100, 'a');//指定填充的字符为a
三、string的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[] (重点) | 返回pos位置的字符,const string类对象调用 |
begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | rbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
1. operator[]
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
这个接口学过运算符重载的肯定不陌生,本质上就是对[]
进行了重载,因此我们可以使用**下标加[]**的方式进行访问string中的数据。并且由于返回的是char类型的引用,因此我们可以直接通过下标加[]的方式进行修改数据。
在这里普通类型的会调用char& operator[] (size_t pos);,而const string s2会调用const类型的[]重载const char& operator[] (size_t pos) const;
void Teststring3()
{
string s1("hello Bit");
const string s2("Hello Bit");
cout << s1 << " " << s2 << endl;
cout << s1[0] << " " << s2[0] << endl;
s1[0] = 'H';
cout << s1 << endl;
// s2[0] = 'h'; 代码编译失败,因为const类型对象不能修改
}
2. 迭代器
下面我们要说以下迭代器,迭代器呢可以说是STL中很重要的一部分。简单来说迭代器就是用来遍历或访问容器中的数据的,我们暂时可以把迭代器想象成指针,通过指针的++或者–加解引用的方式,我们就可以遍历一个数组,或者访问数组中的元素。当然指针只是迭代器中的一种,迭代器要实现的目的就是通过++或者–能够遍历容器中所有的元素,我们数组是一段连续的空间,可以通过指针加一的方式遍历整个数组,但是如果是链表呢?这种情况下通过对每个指针++的操作就无法实现目的了,因此指针就不适合当迭代器了,我们就得封装新的迭代器。
在这之前呢,我们认识两个最常见的接口函数,即为begin和end
- begin获取一个字符的迭代器
- end获取最后一个字符下一个位置的迭代器
迭代器是是另一种访问string中内容的方式,图示如下,我们使用一个it去保存这个字符串【begin】处的位置,那么在其不断进行后移的过程中,就是在遍历这个字符串,当其到达最后的【end】处时,也就遍历完了,此刻便会停了下来。
void TestIterator()
{
string s("abcdef");
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
- 迭代器也可以像数组那样在遍历的时候修改内部的元素,it取到的是每个元素的位置,那么对于*it来说即为每个元素
void TestIterator()
{
string s("abcdef");
string::iterator it = s.begin();
while (it != s.end())
{
(*it)++;
it++;
}
it = s.begin();
while (it != s.end())
{
cout << *it << endl;
++it;
}
}
begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器,而rbegin和rend恰恰相反,rbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器。
也就是说用begin和end遍历是从前往后遍历,用rbegin和rend就是从后往前遍历。
要注意的地方就是定义要这样string::reverse_iterator rit = s.rbegin();,当然如果记不住这么一长串我们可以直接使用auto来进行自动推导。
void Teststring4()
{
string s("abcdef");
string::reverse_iterator rit = s.rbegin();
// C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
//auto rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << endl;
rit++;
}
}
3. 范围for
这里的auto我们以前介绍过,它可以自动推导类型,这里如果我们不用auto的话直接使用char也是可以的。
void TestRangeFor()
{
string s("abcdef");
for (auto ch : s)
{
cout << ch << " ";
}
cout << endl;
}
范围for的底层其实是迭代器,也就是在底层使用了迭代器进行遍历。如果没有迭代器,那就不能使用范围for。
也可以像迭代器那样在遍历的时候去做一个修改,值得注意的是要在ch后面加个引用,否则是达不到修改的目的的。
四、string的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串str |
c_str(重点) | 返回C格式字符串 |
find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
1. push_back
就是在当前的字符串后面追加一个字符,值得注意的是只能追加一个字符,不能追加多个字符否则会报错。
2. append
这个函数重载了很多,大家可以根据下面的介绍自己操作一下。
string& append (const string& str); // 追加一个string对象
// 追加一个string对象中的指定字符串长度
string& append (const string& str, size_t subpos, size_t sublen);
string& append (const char* s); // 追加一个字符串
string& append (const char* s, size_t n);// 追加字符串中的前n个字符串
string& append (size_t n, char c);// 追加n个字符
string s1("aaaaa");
string s2("bbbbb");
s1.append(s2);
cout << s1 << endl;
s1.append(" ");
s1.append("ccccc");
cout << s1 << endl;
s1.append(" ");
s1.append("hello", 3);
cout << s1 << endl;
s1.append(" ");
s1.append(10, 'e');
cout << s1 << endl;
自已对照一下。
3. operator+=
- 拼接两个string类型的字符串
s1 += s2;
- 拼接一个stirng类型加一个字符串
s1+="world";
3. 拼接一个string类型加一个字符
s1+='w'
4. c_str
它的功能是返回一个c格式的字符串。有的人可能会说,为什么还要转换成C格式的字符串呢?那是因为我们调用某些函数,在传参的时候必须传的是c格式的字符串,不能传string,比如打开文件的fopen函数。
FILE * fopen ( const char * filename, const char * mode );
因此如果我们这样打开这个文件的话就会报错
void TestCStr()
{
string str = "test.cpp";
fopen(str, "r");
}
因此我们应该使用c_str函数进行转换,这个接口存在的意义就是一座桥,将C++和c关联起来。
FILE* fout = fopen(str.c_str(), "r");
五、string的非成员函数重载
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> (重点) | 输入运算符重载 |
operator<< (重点) | 输出运算符重载 |
getline (重点) | 获取一行字符串 |
relational operators (重点) | 大小比较 |
1. operator+
谈起operator+,不知道各位有没有想起operator+=呢?我们可以将二者对比一下。
二者的最本质区别还是在于这个效率问题,对于==operator+==而言,其底层在实现的时候因为无法对this指针本身造成修改,所以我们会通过拷贝构造出一个临时对象,对这个临时对象去做修改后返回,那我们知道返回一个出了作用域就销毁的对象,只能使用传值返回,此时又要发生一个拷贝因此本接口其实不太推荐读者使用,了解一下即可,尽量还是使用operator+= 来得好。
2. operator>>和operator<<
流提取,就是我们使用cin >>输入操作的时候一样,控制台要等待我们输入一个值
流插入,通过去缓冲区中拿取数据,然后将其显示在控制台上。
3. getline
使用getline我们可以从缓冲区获取一整行的数据
无论是C语言中的scanf还是C++中的cin每次遇到空格就会直接结束输入,那有没有一种方法在输入的时候遇到空格不结束遇到换行才结束呢?那就是可以使用getline函数。
第一个参数填cin,第二个填要输入的字符串就行。
4. relational operators
大小比较,这其实就是在底层将==、<、>等等这些运算符进行了重载。
==是将string中的每个字符进行对比,如果所有字符全都一样,那就返回true
<就是将两个字符串中的字符从第一个开始比大小,这里的比大小是按ASCII码进行比较,如果两个串s1和s2,其中s1第一个元素的ASCII码比s2第一个元素的ASCII码小那就返回true否则返回false并停止比较,如果s1和s2第一个字符相同,那就比较第二个字符,以此类推。
简单演示几个
由于ASCII码中b的比a的大,所以输出的是s1<s2。