文章目录
- 前言
- 一、string类对象的构造函数
- string()
- string(const char* s)
- string(size_t n, char c)
- string(const string& s)
- string(const string& str,size_t pos,size_t len = npos)
- 二、string类对象的容量操作
- size与length
- capacity
- capacity返回值比size大
- capacity的扩容机制
- empty
- clear
- shrink_to_fit
- reserve
- 关于reserve与扩容的一些问题
- resize
- 字符串变短(n>size)
- 字符串在容量内变长(capacity>=n>size)
- 字符串修改长度超出容量(n>capcity)
- 三、string类对象的访问
- operator[ ]
- 迭代器(简单介绍)
- 反向迭代器
- 四、string类对象的遍历操作
- for+[ ]
- 迭代器(begin(),end())
- 范围for
- 总结
前言
C语言中,字符串是以’\0’结尾的一些字符的集合(C-string),为了操作方便,C标准库中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
这就是我们为什么学习string类的理由,如果你对前面类和对象的内容了解透彻的话,学习STL按道理来说应该不算太难
另外,我还想说,string类其实是当时发布STL时候的前排兵,说白了就是没什么可供参考,所以你在学习它的时候有时候会感觉很挫很冗余,这就对了,这就是正确的感觉
一、string类对象的构造函数
事先声明,就像我前言说的一样,string设置的很冗余,所以我挑选几个常见的来讲,甚至于有几个我挑出来的也不多见,下文同理
(constructor)函数名称 | 功能说明 |
---|---|
string() (重点) | 默认构造,创建一个空串,这个空串的长度是0 |
string(const char* s) (重点) | 用C-string来构造string类对象 |
string(size_t n, char c) | 通过一个字符来构造,一个字符重复n次 |
string(const string& s) (重点) | 拷贝构造函数 |
string(const string& str,size_t pos,size_t len = npos)(重点) | 从str中pos指向位置先后拷贝len长度字符,两种情况下文给出 |
string()
功能:构造空string类对象,其长度为0
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1;
cout<<s1.length()<<endl; // 0
return 0;
}
string(const char* s)
功能:使用C-string构造string类对象。在非空字符串中,从s指向位置拷贝一份字符串
#include<iostream>
#include<string>
using namespace std;
int main()
{
// 相当简便
string s1("Hello,world!");
cout << s1 << endl; // Hello,world!
// 其实我们还有一种清晰明了的方法
const char* s = "Hello,world!";
string s2(s);
cout << s2 << endl; // Hello,world!
return 0;
}
string(size_t n, char c)
功能:通过一个字符来构造,一个字符重复n次
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1(5,'r');
cout << s1 << endl; // rrrrr
return 0;
}
string(const string& s)
功能:拷贝构造,通过已有对象拷贝构造一个新的对象,这个对象和已有对象在逻辑上是相同的
#include<iostream>
#include<string>
using namespace std;
int main()
{
// 这两种都称为拷贝构造
string s1("Hello,world!");
string s2(s1);
string s3 = s1;
return 0;
}
string(const string& str,size_t pos,size_t len = npos)
功能:从str中pos指向位置先后拷贝len长度字符。出现两种结果:拷贝到str最后一个字符或没有达到最后一个字符完成拷贝
第三个参数len类型为 size_t ,而缺省值 npos == -1 导致了 npos 按补码形式是32个比特位1,而又被当作正数还原为原码,就是 INT_MAX(涉及到编码那块,考验你前面学得扎不扎实的时候到了)。所以对于当没有明确 len 数值,默认是从 pos 位置拷贝字符串到最后一个字符,而如果str已经拷贝到最后一个字符了,那就结束拷贝
这是原文翻译,你英语好的话你自己翻译~
Copies the portion of str that begins at the character position pos and spans len characters (or until the end of str, if either str is too short or if len is string::npos).
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("Hello,world!");
string s2(s1,2,3);
cout << s2 << endl;
return 0;
}
二、string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear (重点) | 清空有效字符 |
reserve (重点) | 为字符串预留空间 |
resize (重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
size与length
你可能会想,这两个方法都是返回字符串有效字符的长度,会不会有什么区别?
int main()
{
string str1("hello world");
cout << str1.size() << endl; // 11
cout << str1.length() << endl; // 11
return 0;
}
答案是没有
那为什么还会有两个同样作用的方法的存在呢?其实就是我说的,string是STL的前排兵,一开始想着要有个返回长度的方法,命名length也很合理,可是后面vector、map、set都是size,为了统一,只好也跟着加了个size
如果你问为什么不把length给删掉,其实你细想,删掉的话,已经用length这个方法写代码的个人、公司是不是又有意见了,所以一般我们只加不改,这就是向前兼容原则
一言以蔽之,length合理,size统一更规范
capacity
功能:返回当前对象所分配的存储空间的大小,一般情况下capacity返回大小中不包含’\0’
int main()
{
string str1("hello world");
cout << str1.size() << endl; // 11
cout << str1.capacity() << endl; // 15
return 0;
}
capacity返回值比size大
std::string在底层上是属于动态数组,数组大小是不固定,根据实际需要进行调正,由于经常性出现频繁插入字符的清空,只存在size情况下,会导致频繁地向系统申请空间,性能降低,其实size和capacity的这种用法我们在之前也见识过
capacity的扩容机制
我们会发现string类还是很智能的,能自动扩容,不需要我们操心,但是我们可以注意下扩容的机制,这其实跟编译器和指标因子的不同而有所差异
VS(msvc):扩容机制是第一次扩容到原来空间的两倍左右,之后则扩容当前空间的1.5倍
g++:扩容机制是以当前空间的两倍
empty
功能:检测字符串是否释放为空,是空返回true,否则返回false
int main()
{
string str1;
if (str1.empty()) // 判断释放为空
cout << "为空" << endl;
else
cout << "非空" << endl;
return 0;
}
clear
功能:清空string有效字符资源,不改变底层空间大小。影响有效元素size,不会影响空间容量大小capacity
int main()
{
string str1("hello world");
cout << str1.size() << endl; // 11
cout << str1.capacity() << endl; // 15
str1.clear(); // 清空有效字符
cout << str1.size() << endl; // 0
cout << str1.capacity() << endl; // 15
return 0;
}
shrink_to_fit
功能:向系统请求字符串缩容到适合大小,但是该函数对于字符串的长度和内容是没有影响的,如果使用后容量并没有发生变化,那么可能字符串对象可能已经使用内存管理策略去避免频繁的内存分配和释放
int main()
{
string str("hello world");
cout << str.size() << endl; // 11
cout << str.capacity() << endl; // 15
cout << endl;
str.resize(100);
cout << str.size() << endl; // 100
cout << str.capacity() << endl; // 111
cout << endl;
str.shrink_to_fit();
cout << str.size() << endl; // 100
cout << str.capacity() << endl; // 111
cout << endl;
return 0;
}
这个方法要少用,甚至其实我觉得都可以不用,因为我们一般认为缩容是释放部分空间从而达到正确大小,可是这是不对的,释放只能整个释放,事实上,正确的流程是,重新开一块空间,拷贝部分内容,进而释放原先的全部空间,损耗极大
reserve
功能:向系统申请预留空间,属于手动扩容
int main()
{
string str1;
cout << str1.capacity() << endl; // 15
str1.reserve(100);
cout << str1.capacity() << endl; // 111
string str2(10, 'x');
cout << str2.capacity() << endl; // 10
str2.reserve(); // 缺省参数为0
cout << str2.capacity() << endl; // 10
return 0;
}
关于reserve与扩容的一些问题
- 既然我们前面说了编译器会自动扩容,为什么还要我们自己手动扩容呢?
理由:扩容是需要付出代价的,如果是异地扩容,付出代价更大,需要进行空间开辟和数据拷贝,如果事先知道所需要的空间大小,使用reverse开辟足够使用的空间,减少频繁对内存的重分配,就算后期出现空间不足,也有自动扩容的机制,不需要担心大小是固定的。虽然自动扩容可以解决容量不足的情况,但是手段扩容可以减少频繁自动扩容的代价,属于一种优化手段 - reverse要求100个字节空间,但却开辟了111个字节空间呢?
理由:在不同编译器下机制是不同的,但是确保了至少满足所需空间。有些编译器开辟多个空间,是对reserve开辟的空间进行了二次开辟,可以灵活调用内存空间分配,在后继需要小空间,避免扩容 - reserve参数部分小于当前空间大小,提出申请空间请求,但是空间大小并没有发生改变
理由:reserve进行扩容必须参数部分比当前空间大,才会改变string的底层空间总大小,否则就是无效扩容
resize
功能:改变字符串的实际长度
这里我们以"hello world"为例子来讲解三种不同情况
字符串变短(n>size)
int main()
{
string str1("hello world"); // 长度为11
cout << str1.size() << endl; // 11
cout << str1.capacity() << endl; // 15
str1.resize(2);
cout << str1 << endl; // he
cout << str1.size() << endl; // 2
cout << str1.capacity() << endl; // 15
return 0;
}
字符串在容量内变长(capacity>=n>size)
int main()
{
string str1("hello world"); // 长度为11
cout << str1.size() << endl; // 11
cout << str1.capacity() << endl; // 15
str1.resize(13);
cout << str1 << endl; // hello world
cout << str1.size() << endl; // 13
cout << str1.capacity() << endl; // 15
return 0;
}
字符串修改长度超出容量(n>capcity)
int main()
{
string str1("hello world"); // 长度为11
cout << str1.size() << endl; // 11
cout << str1.capacity() << endl; // 15
str1.resize(50);
cout << str1 << endl; // hello world
cout << str1.size() << endl; // 50
cout << str1.capacity() << endl; // 63
return 0;
}
当resize修改长度超过capacity,capacity会进行自动扩容。至于最后capacity的值为什么不是50,在reserve中解释了不同编译器扩容机制是不同的
其中resize有两个重载,功能都是将字符串中有效字符个数改变到n个,不同点在于:
resize(size_t n):用’\0’来填充都出的元素空间
resize(size_t n,char c):用字符c来填充多出的元素空间
一样的,相比之下我们鼓励用reserve,提前开好空间,避免频繁扩容
三、string类对象的访问
operator[ ]
我们只要了解第一个operator[ ]就行,其他都是为了规范性
我们不妨来看下这两个重载:
char& operator[ ] (size_t pos); // 读写
const char& operator[ ] (size_t pos) const; // 只读
其实,这里第一个重载体现了引用返回的一大用处:可供修改
// 相当于是自定义类型给数组化了,其实string还是蛮特殊的
// 这点我们在后面的学习会有更深的体会
int main()
{
string str1("hello world");
for (int i = 0 ; i < str1.size() ; i++)
{
// cout << str1.operator[](i) << endl;
cout << str1[i] << endl;
}
const string str2("hello world");
for (int i = 0 ; i < str2.size() ; i++)
{
// str2[i]++; const修饰的话,没有修改的权限
cout << str2[i] << endl;
}
return 0;
}
迭代器(简单介绍)
迭代器(Iterator)是一种用于遍历容器(如列表、字典、集合等)元素的对象,它提供了一种统一的访问容器内部元素的方式,而不必暴露容器的具体实现细节。迭代器通常用于循环结构中,让程序员能够逐个访问容器中的元素,在讲解string类对象的遍历前,我想先简要讲解下这一概念
int main()
{
string str1("hello world");
string::iterator it = str1.begin();
while (it != str1.end()) // 左闭右开
{
cout << *it << endl;
it++;
}
return 0;
}
在string里,你可以暂时把它当成是类似指针的东西
我们有很多种遍历类对象的方式,但是迭代器才是主流。对于链表、树等数据结构,迭代器不在乎底层实现,是通用的遍历容器。迭代器是一种像指针的东西,他可以是指针也可以不是指针,具体还是看不同编译器的底层实现,迭代器有两种类型分别:可读可修改,可读不可修改,但是我们要注意存储迭代器的变量类型应该与容器的迭代器类型相匹配,以确保类型的一致性,避免编译器报错或者意外行为
反向迭代器
定义:string::reverse_iterator
int main()
{
string str1("hello world");
string::reverse_iterator rit = str1.rbegin();
while (rit != str1.rend())
{
cout << *rit << "";
++rit; // 请注意是++
}
cout << endl;
return 0;
}
其实,这个反向迭代器的要求应该很少,我觉得正向的就可以满足所需了
四、string类对象的遍历操作
for+[ ]
前文已讲过这两个方法
// str.operator[ ](size_t pos)
// str[pos]
我前面也说了,这很像数组,所以我们很自然的写出这个遍历
int main()
{
string str("Hello,world!");
for (size_t i = 0; i < str.size(); i++) {
cout << str[i]; // Hello,world!
}
cout << endl;
return 0;
}
迭代器(begin(),end())
前文说,这很像指针,这样一提醒后该怎么遍历,也是很自然而然就能写出来
int main()
{
string str("Hello,world!");
string::iterator it1 = str.begin();
while (it1 != str.end()) {
cout << *it1;
it1++;
}
cout << endl;
return 0;
}
范围for
我们说范围for能够自动遍历所有元素,其实哪有什么自动化,范围for的底层还是迭代器,这点结论大家可自行查看汇编代码得出
int main()
{
string str("Hello,world!");
for (const auto& ch : str) {
cout << ch;
}
cout << endl;
return 0;
}
总结
我真的要花大篇幅来讲string,这是因为其是我们学习STL的第一课,并且就像我在正文一再吐槽的,string设计的是真的冗余