文章篇幅较长,越7万余字,建议电脑端访问
本文我们就要来说一说STL中的string类,这也是我们在写C++代码的时候使用最多的
首先要来谈的一点就是为什么要学习string类
string
意为字符串,那回忆一下我们在C语言阶段所学习的字符串,是以'\0'
结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,如果忘记了就再看看 字符串操作函数因此,在C++中专门搞出了一个与字符串相关的类,我们知道C++是面向对象的,我们可以在类内去写各种成员函数来对外提供操作字符串的接口,这个类就是string
类
那这个string有什么实际的应用场景呢,这可多了去了,如果经常刷题的同学应该瞬间能反应过来
字符串转整形数字
字符串相加
string类的文档介绍
basic_string
所实例化出来的那有同学问,下面的这几个【wstring】、【u16string】、【u32string】是什么呢?
对于编码而言我们谈到最多的就是 ASCLL码 ,它的全称叫做【美国信息交换标准代码】,总共包含了像大小写的中英字母、数字、标点符号等等共128个,用来表示老美的的一些文章字符完全足够了。
而对于ASCLL码来说,是存在一个东西叫做映射表,即一个ASCLL码值就对应一个字符
我们可以到VS中来观察一下,其实在内存中所存放的都是ASCLL码,只不过呢在显示的时候却转换为了表中所对应的字符。这个63 64 65 66
其实是十六进制的写法,转换为十进制即为97 98 99 100
,那它们所对应的字符即为a b c d
如果有同学还是不相信的话可以看看下面这个,我给str[0]
这个位置放了数值为97,但是呢其为一个string类的对象,所以里面所存放的都是字符,可以看到在打印出来后的结果就是97所对应的ASCLL码值a
但是呢对于我们的一些中文汉字却是远远不够的,例如说下面这个”比特”
,我通过【sizeof】打印出了其在内存中所占字节数,发现有5B,这是为什么呢?原因就在于汉字的存储规则不是按照ASCLL码来的,而是专属于我国的一套编码字符集叫做GBK。在【GB2312-80】中就存储了很多有关汉字的规则
一般来说一个汉字对应的2个字节,这里的”比特”
是因为最后还有一个\0
最后对这个string类的特性做一个总结:
basic_string
模板类的别名,typedef basic_string string;
⚠ 在使用string类时,必须包含#include
头文件以及using namespace std
对string类大体有个了解后我们就要去学着使用这个类里面的一些函数了
函数名称 | 功能说明 |
---|---|
constructor | 构造函数 |
destructor | 析构函数 |
operator= | 赋值重载 |
① 构造函数
Constructor
就可以看到存在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);
npos
,可能有的同学不清楚这个npos
是什么东西// 拷贝str字符串中从pos位置开始的len个字符
string (const string& str, size_t pos, size_t len = npos);
但是呢,无论这个数值是多少,其实影响都不大,因为我要说明的是即便我们不给出这个参数的话,编译器默认就是从当前的这个【pos】位置开始一直到字符串末尾
substring
指的就是子串的意思,意思即为 从pos位置的len个长度去拷贝字符串的一部分(如果str字符串太短或者len为npos则直接到达字符串的末尾)那现在的话你应该可以明白最后的这个参数我为何没有传递却拷贝到了后面的所有字符
赋值重载
string& operator= (const string& str); // 将一个string对象赋值给到另一个
string& operator= (const char* s); // 将一个字符串赋值给到string对象
string& operator= (char c); // 将一个字符赋值给到string对象
接下去我们来讲一讲有关string类中有关容量的一些操作
函数名称 | 功能说明 |
---|---|
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
maxsize | 返回字符串的最大长度 |
clear | 清空有效字符 |
empty | 检测字符串释放为空串,是返回true,否则返回false |
reverse | 为字符串预留空间 |
resize | 将有效字符的个数该成n个,多出的空间用字符c填充 |
shrink_to_fit | 收缩到合适大小 |
str
没有任何的数据,而VS为我们开出了大小默认为15的空间,其实这里应该是16,只不过最后的\0
也占了一个大小size
的值发生了一个变化。不仅如此,我还打印了一下这个字符串的length
属性,观察到其和size
是一样的string
呢?这里要追溯到STL的一个诞生历史,其实对于string
这个类,是不属于STL的,因为它是在STL之前就已经存在了的,属于C++标准库里的内容string
也是属于标准库的一部分,但是呢二者却没有什么直接的关联。在最早期string
的长度接口是定义为【length】的,到后面STL出了之后才改为了【size】,但是这一块我们需要去考虑到一个历史追溯的问题,所以不能将这个接口给删除,因而我们才看到一模一样的两个接口实现
后续我们在使用到时候直接用【size】即可,因为其他STL库中的接口使用的也是【size】
追究完这个【size】之后,我们再来谈谈这个【capacity】
void TestCapacity()
{
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';
}
}
}
s
中添加字符来扩充他的容量
可以看到Windows下的VS中,扩容的大小是呈现一个1.5倍大小的趋势;可是在Linux平台下呈现的却是2倍大小的趋势
接下去呢我们再来说说这个【maxsize】
void TestMaxsize()
{
string s("hello");
cout << s.size() << endl;
cout << s.max_size() << endl;
}
接下去呢,我们来讲【clear】
void TestClear()
{
string s("hello");
cout << "size: " << s.size() << endl;
cout << "capacity: " << s.capacity() << endl;
s.clear();
cout << "size: " << s.size() << endl;
cout << "capacity: " << s.capacity() << endl;
}
clear
来说,它只会清空字符串中的【size】,但对于【capacity】来说是不会有什么变化的,如果你是这个接口的实现者一定不会去清空这个【capacity】容量的大小,万一后面又需要插入字符呢?此时又需要去进行一个扩容,是非常麻烦的0
,代表false
不空,而被清空之后返回1
,代表true
为空接下去呢我们来讲讲这个【reserve】,可以提前为一个字符串开出指定的空间
可能上面这样还不是很好理解,我们举个例子来理解一下
我们也可以到文档中来看看
不过呢这个文档下面还有一部分,那就是不仅会发生【增容】的情况,也会发生【减容】的情况
s.reserve(10)
来试试是否可以,却发生容量没有发生变化可是,我接下来的操作,可能就会让你惊掉下巴(⊙ˍ⊙)
clear
将字符串中的数据清空后,此时再去使用reserve
的时候容量就发生了变化,回到了最初的那个默认大小reserve(0)
一下看看果然也是这样从以上的种种结果来看可谓真的是【变化多端】鸭,多然是不好掌控
void resize (size_t n);
void resize (size_t n, char c); // 初始化数据为n个c字符
void TestResize()
{
string s("abcdef");
// 开空间
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << "---------------------" << endl;
// 开空间 + 填值初始化
s.resize(200);
cout << s.size() << endl;
cout << s.capacity() << endl;
}
capacity
起作用,size
就是初始化的字符串个数;size
和capacity
会一起发生改变,前者变为传递的参数n,后者则遵循VS下的扩容规则,比给出的参数多一些一样,我们通过解读文档的形式来看看这个函数还有哪些内容
第一点我们刚才已经验证过了,接下去我们再来试试第二点,看看是否真会去做一个删除的操作
上面我们所使用的都是第一个只传递n,不过下面还有一个重载形式可以【初始化数据为n个c字符】,看到最后这里也有说到如果这个c
是特殊的话,就会去以这个c
字符去填充后面多开空间的部分
c
的话,默认会以'\0'
进行填充的,那如果我们制定出字符呢?c
,此刻后面填充的就是我们所指定的那个字符s.resize(100, 'a');
void shrink_to_fit();
capacity
变得适应size
的大小, 确实也有一些作用,不过使用的场景不多,读者了解一下即可函数名称 | 功能说明 |
---|---|
operator[] (重点) | 返回pos位置的字符,const string类对象调用 |
begin + end | begin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | rbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
首先我们来说说这个
operator[]
,相信学习过 类和对象的运算符重载 的同学一定不陌生
operator[]
,我们就可以使用【下标 + [ ]】的形式去访问字符串中的每一个元素void TestOperator()
{
string s("abcdef");
cout << s << endl;
}
for (int i = 0; i < s.size(); i++)
{
cout << s[i] << " ";
}
我们知道,其实这个
string
类的对象会在堆区中开辟出一块数组空间来存放相对应的字符,最后为了和C语言保持一致会在最后面加上一个\0
,那为何这里在打印的时候没有看到呢?
\0
,这是为何呢?\0
for (int i = 0; i < s.size(); i++)
{
s[i]++;
}
s[0]--
,那么在打印的时候看到的结果即为[a]
从上面的种种我们可以看到这个
operator[]
使得字符串可以让我们像数组一样去使用,做增、删、查、改很方便
[]
的形式和下面这样对字符数组的访问是有本质区别的string s("abcdef");
char s2[] = "hello world";
s[1]++; // -> operator[](1)++
s2[1]++; // -> *(s2 + 1)++
s[1]++
在底层是转换为operator[]
的形式;但是对于s[2]++
却是在做一些解引用的操作,这一块看不懂也没关系,但在学习了C语言操作符后我们要知道对于[]
来说其实就是一种解引用的形式【温馨提示】:只能用于string
+ vector
+ deque
,但是链表、树不能用,链表各个结点的空间不连续
string
一样都是连续的,因此我们可以像访问数组一样去访问里面的元素。但是呢,像【链表】、【树】这样的结果,它们的一个个结点在空间中都是都是离散的,无法做到像数组那样去连续访问当然,除了下标 +
[]
的形式,我们还可以通过【at】的形式去做一个访问
void TestAt()
{
string str("abcdef");
for (int i = 0; i < str.size(); i++)
{
cout << str.at(i) << " ";
}
cout << endl;
}
我们再来看看这个const对象
const char& at (size_t pos) const;
void func(const string& s)
{
for (int i = 0; i < s.size(); i++)
{
s.at(i)++;
cout << s.at(i) << " ";
}
}
s
具有常性,是无法修改的对于
oparator[]
和at()
来所,还要再做一个对比,即它们在处理异常这一块
可以看到对于上面的oparator[]
来说若是产生了一个越界访问的话就直接报出【断言错误】了
然后看到对于at()
来说虽然也是弹出了警告框,但是呢这个叫做【抛异常】,之后我们在学习C++异常的时候会讲到的
pos
位置有问题时,就会报出out_of_range
的异常,这也就印证了上面的现象try...catch
的形式。此时我们再运行的话就可以发现此异常被捕获了,而且打印出了异常的信息那接下去呢,我就要来讲讲【迭代器】了,它是我们在学习STL的过程中很重要的一部分内容,让我们对容器的理解能够更上一层楼
begin
和end
it
去保存这个字符串【begin】处的位置,那么在其不断进行后移的过程中,就是在遍历这个字符串,当其到达最后的【end】处时,也就遍历完了,此刻便会停了下来void TestIterator()
{
string s("abcdef");
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
it
取到的是每个元素的位置,那么对于*it
来说即为每个元素string::iterator it = s.begin();
while (it != s.end())
{
(*it)++;
it++;
}
iterator
是像指针一样的类型,有可能是指针,有可能不是指针这边再拓展一点,上面说到迭代器在我们学习STL的过程中起着很大的作用,原因就在于其他的容器都可以使用这种形式来进行遍历
void TestIterator2()
{
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++;
}
}
vector
容器来说,也是可以使用迭代器去做一个遍历的list
也是可以使用迭代器来进行访问的
好,上面仅仅作为拓展,如果读者不懂得话也没关系,下一文就会学习到
【小结】:
好,对上面的内容做一个小结。iterator提供一种统一的方式访问和修改容器
还记得我们在初步认识STL的时候讲到的STL的六大组件,除了【容器】之外最重要的就是【算法】,这里我先简单地介绍几个算法并演示一下
reverse(s.begin(), s.end());
#include
可不要忘记了哦vector
容器来说也是同样适用sort(s.begin(), s.end());
sort
进行排序后原本的乱串变成了有序串
其余容器的这里就不演示了,读者可自己下去试试看,总结一下:算法可以通过迭代器去处理容器中的数据
好,讲完正向迭代器,我们再来说说【反向迭代器】
reverse_iterator rbegin();
reverse_iterator rend();
展示一下代码
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
再来看看结果
好,讲完了正向和反向迭代器后,我们再来拓展地讲一些东西
iterator
和 reverse_iterator
之外,还有const_iterator
和 const_reverse_iterator
,那后面的这两个我们要如何去使用呢?const
做修饰防止权限放大void Func(const string& s)
{
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
}
s
是属于const对象,那么它在调用【begin】的时候返回的就是const迭代器,是【只读】那此时我们若是使用普通迭代去接收的话就是【可读可写】,也算是一个权限放大的问题const_reverse_iterator
才可以,但你是否觉得这样写过于复杂了呢?string::const_reverse_iterator rit = s.rbegin();
auto
的话就可以清楚其可以完成自动类型转换的功能,不需要我们去关心具体的类型,这个关键字我在下面讲到【范围for】的时候还会再提到的,读者可以自行先了解一下auto rit = s.rbegin();
好,我花了很大的篇幅在介绍迭代器之后,我们再来讲讲范围for,这个是C++11才出来的,现在被广泛地使用
auto
就是我们上面所说到过的【自动类型推导】,那这里如果我们不用auto
的话直接使用char
也是可以的void TestRangeFor()
{
string s("abcdef");
for (auto ch : s)
{
cout << ch << " ";
}
cout << endl;
}
*it
的数据给到当前的ch,和迭代器的本质还是类似的ch
在遍历的时候每次只会是当前字符的一份拷贝,那么在循环遍历结束后ch
每一次的变化是不会导致字符串s发生变化的一个类如果不支持迭代器就不支持范围for,因为范围for的底层使用的也是迭代器
stack
类,它就是不支持的只能正着遍历,但是不能倒着
然后再来拓展两个C++11中新接口,看这个字面其实就可以看出【front】取到的是字符串的首字符,而【back】取到的则是字符串的尾字符
void TestBackAndFront()
{
string str("abcdef");
cout << str.front() << " " << str.back() << endl;
}
接下去呢我们来讲讲string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+=() 重点 | 在字符串后追加字符串str |
insert | 在指定位置插入字符或字符串等操作 |
assign | 使用指定的字符串替换原字符串 |
erase | 删除字符串中的一部分 |
replace | 替换指定区间的字符串 |
pop_back | 删除字符串的最后一个字符 |
swap | 收缩到合适大小 |
push_back()
仅能尾插一个字符,其他都是不可以的接下去呢我们再来讲讲【append】这个接口,它在string类中用的还是蛮多的
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 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;
以下是测试结果,读者可以自行对照
对于上面的这两种调用函数的方式,你是否觉得过于麻烦呢?
+=
,这个我们在讲 运算符重载 的时候有提到过。它一共有三个重载形式,分别是拼接一个string
类的对象、一个字符串、一个字符name1 += name2;
name1 += "feng";
name1 += 'g';
可以看出这个 +=
确实是非常地方便,有了它你完全就懒得去用另外的【push_back】、【append】,当然它没有这二者的重载形式这么多,还是要以具体的情景为主
然后呢我们再来看看【insert】这个函数,重载形式也蛮多的
// 在指定位置插入一个string对象
string& insert (size_t pos, const string& str);
// 在指定位置插入一个string对象里的一部分
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);
// 在指定位置插入一个字符串
string& insert (size_t pos, const char* s);
// 在指定位置插入一个字符串的前n个字符
string& insert (size_t pos, const char* s, size_t n);
// 在指定位置插入n个字符
string& insert (size_t pos, size_t n, char c);
// 在指定迭代器的位置插入n个字符
void insert (iterator p, size_t n, char c);
// 在指定迭代器的位置插入一个字符,并且返回一个迭代器的位置
iterator insert (iterator p, char c);
s
的第0个位置插入了一个string的对象“bbb”
,运行起来就看到确实插进去了d
如果读者有看过 C语言版数据结构 的话就可以知道对于上面这些操作来说其底层实现都是需要挪动很多数据的,此时就会造成复杂度的提升,导致算法本身的效率下降。因此【insert】这个接口还是不推荐大家频繁使用
通过上面一步步地演示,相信你对接口函数的重载形式如何去辨析一定有了一个自己的认知与了解,后面就不会讲这么详细了,读者可自己去试着测试看看各个重载示例
讲完【insert】,我们再来瞧瞧【assign】,这个函数读者当做了解,不常用
s
中有多少内容,在执行了【assign】之后就被覆盖成了新的内容。这里的话就演示一下这个了,其余的读者有兴趣可以自己去看看接下去就是【erase】这个接口,用得还是比较多的
npos
我们前面在介绍string类的构造函数时有讲到过,这里就不再做介绍了string& erase (size_t pos = 0, size_t len = npos);
npos
,直接删到结尾不过呢,这里还是要提一句,【erase】这个接口和【insert】一样,在修改原串的时候会造成大量的数据挪动,特别是在头删除的时候,需要挪动
[n - 1]
个数据
接下去这个接口,会让你眼前一惊,因为有非常多的重载类型
“ haha ”
,以下就是替换后的结果“ eeee ”
有【push_back】,那就一定有【pop_back】,不过这是C++11新出来的
接下去我们来看看【swap】这个接口。没错,它可以交换两个字符串
这里再补充一道面试题
面试题01.03.URL化
replace()
接口的话较为合适,但是呢效率却不是很高,下面我介绍一种高效的办法,利用到的是我们上面所讲的+=
,只需要去遍历一下这个字符串即可,然后判断其是否为【空格】即可,如果不是的话就直接拼接过来,如果是空格的话就拼接%20
string replaceSpaces(string S, int length) {
string str = "";
for(int i = 0;i < length; ++i)
{
if(S[i] == ' '){
str += "%20";
}else{
str += S[i];
}
}
return str;
}
然后我们再来看看有关string类对象的其他字符串操作接口
函数名称 | 功能说明 |
---|---|
c_str | 返回C格式字符串 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
find | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
find_first_of | 从前往后找第一个匹配的字符 |
find_last_of | 从后往前找第一个匹配的字符 |
find_first_not_of | 从前往后找第一个不匹配的字符 |
find_last_not_of | 从前往后找第一个不匹配的字符 |
test.cpp
的文件,想要使用C语言中的fopen()
打开它,但是呢却遇到了一些的问题void TestCStr()
{
string str = "test.cpp";
fopen(str, "r");
}
不存在从“std:string"到"const char*”的适当转换函数
,原因就在于我们这个【test.cpp】使用到是string类来进行存储,而如果你去查看 fopen 的文档的话,就可以发现 第一个参数所要传入的是一个字符串,这就是问题所在FILE * fopen ( const char * filename, const char * mode );
此时呢【string】类给我们提供了一个接口函数叫做 c_str
,帮助我们将 string对象 转换为 字符串
FILE* fout = fopen(str.c_str(), "r");
再拓展一个接口叫做【data】,仔细对比一下可以发现二者很类似,因为【data】是很早之前就定义好的接口,而【c_str】则是后面才被定义出来的,现在我们用的一般都是【size_t】
接下去是一个截取子串的接口
pos
然后我们来看看【find】接口,这个接口用的还是比较广泛的,值得学习一下
str
中寻找【def】,返回的位置便是第一次所查找到的位置上面呢我们简单介绍了接口函数【substr】和【find】,现在立马通过具体的情景来使用一下它们
https
,第二个是域名部分legacy.cplusplus.com/reference
,第三个则是资源部分string/string/?kw=string
https://legacy.cplusplus.com/reference/string/string/?kw=string
然后我们就尝试去分割一下这三部分,下面是整体的分割图示
string str("https://legacy.cplusplus.com/reference/string/string/?kw=string");
https
,那么就要找到://
,那么此刻就可以使用到我们前面所学习过的find()
函数,去记录下这个位置。substr()
,起始位置传入0即可,长度的话传入pos1
,在讲解 数据结构之顺序表 的【size】时有说到过 当前位置的下标就是前面的数据个数// 协议
string protocol;
size_t pos1 = str.find("://");
if (pos1 != string::npos)
{
protocol = str.substr(0, pos1);
}
find()
函数,先要确定的就是开始的位置,即这个【legacy】的l
,其距离上一次的pos1
刚好是后移3个的位置,所以我们从pos1 + 3
开始即可,那么要到达的位置就是/
,作为域名的第一次分割线。pos2
这个位置开始取,长度的话直接缺省即可,取到最后面,完全不需要考虑它的长度是多少// 域名 资源名
string domain;
string uri;
size_t pos2 = str.find("/", pos1 + 3);
if (pos2 != string::npos)
{
domain = str.substr(pos1 + 3, pos2 - (pos1 + 3));
uri = str.substr(pos2);
}
最后来看下运行结果,就发现每一块都取出来了
string str("https://www.baidu.com/index.htm");
讲完了【find】,我们再来看看【rfind】
a
最后一次出现的位置就是在下标为4的地方npos
的值npos
的值接下去再来介绍四组接口,它们很类似
find_any_of
才对,不过呢可能是祖师爷在设计的时候突然走神了也说不定void TestFindFirstOf()
{
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_first_of("aeiou");
while (found != string::npos)
{
str[found] = '*';
found = str.find_first_of("aeiou", found + 1);
}
cout << str << endl;
}
aeiou
五个元音字母的字符都会替换成了[*]
。如果你有了解过 strtok() 的话就可以知道上面的代码逻辑和它的实现是存在着异曲同工之妙的看完【find_first_of】,我们再来看看【find_last_of】
void TestFindLastOf()
{
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_last_of("aeiou");
while (found != string::npos)
{
str[found] = '*';
found = str.find_last_of("aeiou", found - 1);
}
cout << str << endl;
}
下面还有两个接口,和上面两个刚好是对立面
not match
,即不匹配的情况void TestFindFirstNotOf()
{
string str("look for non-alphabetic characters...");
size_t found = str.find_first_not_of("abcdefghijklmnopqrstuvwxyz ");
if (found != string::npos)
{
cout << "The first non-alphabetic character is " << str[found];
cout << " at position " << found << '\n';
}
}
str
中寻找26个英文字母 + 空格的时候,第一个找到的位置就是【12】,即为[-]
最后一个【find_last_not_of】,再坚持一下,马上就结束了(ง •_•)ง
void TestFindLastNotOf()
{
string str("look for non-alphabetic characters...");
size_t found = str.find_last_not_of("abcdefghijklmnopqrstuvwxyz ");
if (found != string::npos)
{
cout << "The last non-alphabetic character is " << str[found];
cout << " at position " << found << '\n';
}
}
[.]
,它的位置即为36接下去我们再来看看string类对象的非成员函数重载
函数名称 | 功能说明 |
---|---|
operator+ () | 尽量少用,因为传值返回,导致深拷贝效率低 |
relational operator (重点) | 大小比较 |
operator>>() | 流插入重载 |
operator<<() | 流提取重载 |
getline (重点) | 获取一行字符串 |
接下去我们来说
operator+()
,看到它是否有想起operator+=()
呢,我们来对比辨析一下
operator+=()
就是在后面追加字符串,不过operator+()
起到的是一个拼接的效果但是呢,二者的最本质区别还是在于这个效率问题,对于【+】而言,其底层在实现的时候因为无法对
this指针
本身造成修改,所以我们会通过拷贝构造出一个临时对象,对这个临时对象去做修改后返回,那我们知道返回一个出了作用域就销毁的对象,只能使用传值返回,此时又要发生一个拷贝
因此本接口其实不太推荐读者使用,了解一下即可,尽量还是使用【+=】来得好
接下去的话是一些关系运算符,这个我们在讲【日期类】的时候也是有自己模拟实现过,基本上实现了前面几个的话后面都是可以去做一个复用的,底层这一块如果读者想要深入了解的话就去看看日期类吧
true
,VS中用【1】来表示;反之则返回false
,VS中用【0】来表示下面两个的话我们可以一起说,其实你看到现在的话完全就不需要我说了,因为我们一直在使用这个东西,在对 string对象 进行操作的之后将其打印输出使用的就是重载之后的【流插入】
cin >>
在做输入操作的时候一样,控制台会先去等待我们输入一个值接下去再来说说【getline】,有了它我们可以到缓冲区中拿取一整行的数据
scanf()
,在读取字符串的时候经常是读到空格就结束了,而无法读取到后面的内容然后我去网上找了很多的办法,一共是有以下三种
① 首先的话就是使用一种特殊的格式化输入
scanf("%[^\n]", s1);
gets_s(s1);
③ 第三种乃是通过文件结束符EOF来进行判断,其是直接读取到换行符\n
为止
while ((scanf("%s", s1)) != EOF)
{
printf("%s ", s1);
}
但是呢,在我们学习了getline()
函数后,就不需要这么麻烦了,其可以在缓冲区中读取一整行的数据,而不会遇到空格就截止
对STL中的string类有了一个基本的认识后,本模块,我会带着你从0 ~ 1去模拟一下s库中string的这些接口,当然是比较常用的一些,代码量大概600行左右
bit
的命名空间,此时因为作用域的不同,就不会产生冲突了,如果这一块有点忘记的同学可以再去看看 namespace命名空间namespace bit
{
class string {
public:
//...
private:
size_t _size;
size_t _capacity;
char* _str;
};
}
test3.cpp
中包含一下这个头文件,此时我们才可以在自己实现的类中去调用一些库函数#include
#include
using namespace std;
#include "string.h"
好,首先第一个我们要来讲的就是【构造函数】
_size
和 _capacity
的大小为,然后给字符数组开了一个大小的空间,并且将其初始化为\0
// 无参构造函数
string()
:_size(0)
, _capacity(0)
,_str(new char[1])
{
_str[0] = '\0';
}
bit
中的,那么我们在使用这个类的时候就要使用到 域作用限定符::
bit::string s1;
然后打印一下这个string对象发现是一个空串
_size
的时候先去计算了字符串str的长度,因为_size
取的就是到 \0
为止的有效数据个数(不包含\0),那么【strlen】刚好可以起到这个功能_str
这一块,我们为其开出的空间就是 容量的大小 + 1,最后的话还要在把有效的数据拷贝到这块空间中,使用到的是【strcpy】// 有参构造函数
string(const char* str)
: _size(strlen(str))
, _capacity(_size)
,_str(new char[_capacity + 1])
{
// 最后再将数据拷贝过来
strcpy(_str, str);
}
“”
代表的是一个空的字符串,但是无论怎样,对于一个字符串来说末尾是一定有\0
的,此刻你可以将它带入下面的表达式,发现算出后的结果与前面无参是一样的// 构造函数
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str, _size + 1);
}
memcpy()
,如果有度过 字符串函数与内存函数 一文的话就可以清楚它们的区别在哪里了,对于strcpy()
来说拷贝到\0
就会发生终止而不会拷贝了,这是我在测试一些极端场景的时候考虑到的可以看到换回【strcpy】的时候\0
后面的内容就不会去进行一个拷贝了,不过这里其实体现得不是很明显,我们在下面的 拷贝构造、赋值运算符重载 中会继续提到这个
有同学觉得上面的缺省参数很是奇妙,于是提出能不能写成下面这样
string(const char* str = "\0")
string(const char* str = nullptr)
马上,我们就来聊聊有关【拷贝构造函数】的内容
memcpy()
string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size);
_size = s._size;
_capacity = s._capacity;
}
s1
和 对象s2
中的数据存放在不同的空间中,此时去修改或者是析构的话都不会受到影响下面呢还有一个新的版本,这一块我放到【赋值重载】去进行讲解
// 拷贝构造函数(新版本)
string(const string& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);
// tmp出了当前函数作用域就销毁了,和this做一个交换
this->swap(tmp);
}
对于赋值运算符重载这一块我们知道它也是属于类的默认成员函数,如果我们自己不去写的话类中也会默认地生成一个
s1 = s3
,此时若不去开出一块新空间的话,那么s1
和s3
就会指向一块同一块空间,此时便造成了下面这些问题
s1
所指向的那块空间没人维护了,就造成了内存泄漏的问题s3
里的内容先拷贝到这块空间中来,然后释放掉s1
所指向这块空间中的内容,然后再让s1
指向这块新的空间。那么这个时候,也就达成了我们所要的【深拷贝】,不会让二者去共同维护同一块空间s1
的【_size】和【_capacity】,因为大小和容量都发生了改变
下面是具体的代码,学习过 类的六大天选之子 的同学应该不陌生
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
但是呢,就上面这一种写法并不是最优的,我们来看看下面的这种写法
// 赋值重载(pua版本)
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
this->swap(tmp);
}
return *this;
}
swap()
函数,本来是应该下面讲的,既然这里使用到了,那就在这里讲吧,这个接口我在上面并没有介绍到,但是在讲 C++模版 的时候有提到过库中的这个 swap() 函数,它是一个函数模版,可以 根据模版参数的自动类型推导去交换不同类型的数据swap(string& s)
函数中就去调用了std
标准库中的函数然后交换一个string对象的所有成员变量void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
tmp
,然后再通过swap()
函数去交换当前对象和tmp
的指向,此时s1
就刚好获取到了赋值之后的内容,而tmp
呢则是一个临时对象,出了当前函数的作用域后自动销毁,那么原本s1
所维护的这块空间刚好就会销毁了,也不会造成内存泄漏的问题
那有同学就说:这个妙啊!太妙了!
“PUA”的原理,就是打击你的自尊,摧毁你的独立思考能力,让你觉得自己一无所事,
然后对方趁虚而入,让你产生依赖,让你觉得只有对方才能帮助自己,从而被对方操控。
tmp
对象出了作用域也要销毁的,你手上呢刚好有我想要的东西,那我们换一下吧,此时我得到了我想要的东西,你呢拿到了我的东西,这块地址中的内容刚好就是要销毁的,那tmp
在出了作用域后顺带就销毁了,这也就起到了【一石二鸟】的效果好,我们通过这个调试来观察一下,可以看到就是这个“PUA技术”,很好地达成了我们的目标
但是呢,我觉得上面的这种PUA还不够,还可以再 “精妙” 一些,我们一起来看一下下面这个版本
tmp
便是外面这个对象的一个临时拷贝,我们直接去操作这个对象的时候也可以到达同样的效果// 赋值重载(究极pua版本)
string& operator=(string tmp)
{
this->swap(tmp);
return *this;
}
一样,我们通过调试来看就可以看得很清晰,一开始按F11的时候我们可以看到进入到了拷贝构造函数内部,这个时候其实就是因为传值传参去调用拷贝构造的缘故
此时我们就可以去谈谈在一模块所讲到的这个【新版本的拷贝构造函数】
// 拷贝构造函数(新版本)
string(const string& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);
// tmp出了当前函数作用域就销毁了,和this做一个交换
this->swap(tmp);
}
string(const string& s)
{
string tmp(s._str);
// tmp出了当前函数作用域就销毁了,和this做一个交换
this->swap(tmp);
}
=
的时候,因为传值传参的缘故首先会去调用这个拷贝构造拷贝一份临时对象,但是呢在调试的时候可以发现编译器并没有去对当前对象中的成员变量做一个初始化的工作,在执行swap()
函数后这个没被初始化的对象就交给tmp
来进行维护了,但是呢tmp
在出了作用域之后又要销毁,那么此时在执行析构函数的时候便会出问题了,去释放了一块并没有初始化的空间,一定会出现问题的!
所以我们还是不能去相信编译器所做的一些工作,而是要自己经手去做一些事,避免不必要的麻烦
最后的话就是析构函数这一块,前面在调试的过程中我们已经看到很多遍了,此处不再细述
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
基本的成员函数我们已经讲完了,string对象也构造出来了,接下去我们来访问一下对象里面的内容吧
// 可读可写
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
// 可读不可写
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
size()
函数和流插入我们会在下面讲到const
的话此时这个对象就具有常性了,在调用operator[]
的时候调用的便是 可读不可写 的那一个,所以此刻我们去做一个修改操作的话就会出问题了const bit::string s2("world");
那经过上面的学习我们可以知道,要去遍历访问一个string对象的时候,除了【下标 + []】的形式,我们还可以使用迭代器的形式去做一个遍历
typedef char* iterator;
typedef const char* const_iterator;
_str
所指向的这个位置,而末位的话则是_str + _size
所指向的这个位置iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
auto
关键字进行自动类型推导即可auto cit = s2.begin();
之前我们有讲过,一个类只要支持迭代器的话那一定支持范围for,马上我们来试试看吧
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
for (auto ch : s2)
{
cout << ch << " ";
}
这个方式去遍历的话还是很方便的,必须安利一波✌
下面四个接口我们一起来看看,然后一同测试
_size
即可,因为不会去修改成员变量,所以我们可以加上一个【const成员】size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
_str[0]
这个位置放上一个\0
即可,并且再去修改一下它的_size = 0
即可_size
void clear()
{
_str[0] = '\0';
_size = 0;
}
0 == _size
即可bool empty() const
{
return 0 == _size;
}
然后我们来测试一下
然后我们来看【reserve】扩容
newCapacity
去开),然后再将原本的数据拷贝过来,释放旧空间的数据后让_str指向新空间即可。最后的话不要忘了去更新一下容量大小// 扩容(修改_capacity)
void reserve(size_t newCapacity = 0)
{
// 当新容量大于旧容量的时候,就开空间
if (newCapacity > _capacity)
{
// 1.以给定的容量开出一块新空间
char* tmp = new char[newCapacity + 1];
// 2.将原本的数据先拷贝过来
memcpy(tmp, _str, _size);
// 3.释放旧空间的数据
delete[] _str;
// 4.让_str指向新空间
_str = tmp;
// 5.更新容量大小
_capacity = newCapacity;
}
}
马上来做一个测试
通过调试再去看一下,可以发现_str
的空间确实发生了一个很大的改变
然后我们再来讲讲【resize】,博主觉得下面的这个算法是比较优的,读者可以参考一下
newSize < _size
的话,那我们要选择去删除数据newSize > _size
,但是呢 newSize < _capacity
的话,此时要做的就是新增数据但是呢不去做扩容newSize > _size
的话,我们便要选择去进行扩容了newSize
是否大于_size
,然后在内部又做了一层判断,只有当newSize > _capacity
时,才去执行【reserve】的扩容逻辑newSize
并没有超过容量大小的话我们要做的事情就是去填充数据,这里用到的是一个内存函数【memset】
_str + _size
的位置开始填充;newSize - _size
个;c
newSize <= _size
的话,我们所要做的就是去截取数据,到newSize
为止直接设置一个 \0,然后更新一下当前对象的_size
大小// 改变大小
void resize(size_t newSize, char c = '\0')
{
// 1.当新的_size比旧的_size来得小的话,则进行删除数据
if (newSize > _size)
{
// 只有当新的size比容量还来的大,才去做一个扩容
if (newSize > _capacity)
{
reserve(newSize);
}
// 如果newSize <= _capacity,填充新数据即可
memset(_str + _size, c, newSize - _size);
}
// 如果 newSize <= _size,不考虑扩容和新增数据
_size = newSize;
_str[newSize] = '\0';
}
马上我们就来分类测试一下
resize(8)
,可以看到这里发生了一个数据截断的情况,_size
也相对应地发生了一个变化resize(12)
,这并没有超过其容量值,但是却超出了_size
大小,所以我们要去做一个扩容resize(18)
,此时的话就需要去走一个扩容逻辑了,并且在扩完容之后还要再进一步去填充数据好,接下去我们来讲讲修改器这一块
_size == _capacity
的时候,就要去执行一个扩容的逻辑了,这边的话是运用到了这个三目运算符,若是容量的大小为0的话,默认开个大小为4的空间就可以了;其他的情况都是以2倍的形式去进行扩充_size
指向的就是 \0 的位置,所以就把字符放在这个位置上就可以了,顺带地记得去后移一下这个_size
,再放上一个 \0// 追加一个字符
void push_back(char ch)
{
// 如果数据量大于容量的话,则需要进行扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
_str + _size
的位置(注意拷贝len + 1个,带上最后 \0),最后再把大小_size
给增加一下即可// 追加一个字符串
void append(const char* s)
{
int len = strlen(s); // 获取到待插入字符串的长度
// 若是加上len长度后超出容量大小了,那么就需要扩容
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 将字符串拷贝到末尾的_size位置
memcpy(_str + _size, s, len + 1);
// 大小增加
_size += len;
}
读者一定会觉得上面的函数调用太过于冗余,不过没关系,我们还有【+=】呢
push_back()
接口即可,最后因为【+=】改变的是自身,所以我们return *this
,那么返回一个出了作用域不会销毁的对象,可以采取 引用返回 减少拷贝string& operator+=(char ch)
{
push_back(ch);
return *this;
}
append()
即可string& operator+=(const char* s)
{
append(s);
return *this;
}
立马来测试一下吧
接下去我们就要来实现一下【insert】这个接口了
npos
,它是最大的无符号整数值。但是对于 静态的成员变量 来说我们需要 在类内声明并且在类外进行初始化// 类内声明
static size_t npos;
// 类外初始化
size_t string::npos = -1;
pos
位置插入n个字符void insert(size_t pos, size_t n, char ch)
pos
位置,所以第一步我们就是要去考虑这个pos
位置是否合法assert(pos <= _size);
_size + n
之后的大小大于_capacity
的话那就要调用【reserve】接口去实现一个扩容的逻辑了// 考虑扩容
if (_size + n > _capacity)
{
reserve(_size + n);
}
_size
位置开始,让字符以n个单位地从后往前挪即可,若是从前往后挪的话就会造成覆盖的问题// 挪动数据
size_t end = _size;
while (end >= pos)
{
_str[end + n] = _str[end];
--end;
}
pos == 0
的话,也就是在这个位置开始插入数据,那也就相当于头插,此时需要将全部的数据向后进行挪动,可是呢当这个end
超出pos
的范围时,也就减到了-1,但是呢这个end
的数据类型则是【size_t】,为一个无符号整数,我们知道对于无符号整数来说是不可能为负数的,那么这个时候就会发生一个轮回,变成最大的无符号正数end
在不断减少直至减到0的时候就会突然变成一个很大的数字,这个其实就是npos
的值了,此时就会造成一个死循环,导致程序崩溃end != npos
才对// 挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
--end;
}
_size
的大小即可// 插入n个字符
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
void insert(size_t pos, const char* s)
// 插入字符串
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
void erase(size_t pos, int len = npos)
pos
位置开始去删,删除len个有效长度的字符,那这几个字符就相当于是不要了,但是呢后面的字符串还是要的,所以有的同学就会想到用这个 拼接 的方法去完成读者可以通过下面的算法分解图去思考一下代码该如何书写,我们是从【w】这个位置开始删除长度为3的有效字符
len
很大很大,甚至是最大的无符号整数npos
,或者呢在pos + len
之后的长度超出了当前_size
的大小,此时我们可以直接对pos
之后的字符去做一个截断的操作,让这个位置变成新的_size
下面就是具体的代码展示,对于正常的情况而言,最后呢不要忘记了在覆盖字符后去改变一下这个_size
的大小
// 删除从pos位置开始的len个有效长度字符
void erase(size_t pos, int len = npos)
{
if (len == npos || pos + len > _size)
{
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
pos == 2
的位置开始,删除长度为30的字符,那这个就是pos + len > _size
的情况len == npos
的情况void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
然后再来讲讲有关字符串的一些操作
cout << s << endl;
打印一下,如果你就使用了上面这些代码的话,一定是会报错的,因为流插入运算符<<
和 string类对象并没有对应的重载函数,这一块我后面在讲流插入的时候会提到,报错的同学可以先用下面这种const char* c_str()
{
return _str;
}
<<
是有重载的,所以才可以起到一个很好地匹配cout << s1.c_str() << endl;
_str
,若是在遍历的过程中发现了字符ch
的话就返回这个位置的下标,如果遍历完了还是没有找到的话就返回npos
这个最大的无符号数size_t find(char ch, size_t pos) const
{
assert(pos <= _size);
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
指针 - 指针
的方式即可。那如果没找到的话我们返回【npos】即可size_t find(const char* s, size_t pos) const
{
assert(pos < _size);
char* tmp = strstr(_str, s);
if (tmp)
{
// 指针相减即为距离
return tmp - _str;
}
return npos;
}
那我们立马来测试一下
a
,我们从第0个位置开始找size_t pos = s1.find('a', 0);
上面是去匹配子串,现在我们要将这个子串给取出来,要如何去取呢?
string substr(size_t pos, size_t len = npos)
pos
位置开始所要取的子串长度大于剩余的串长,那最多能取到的有效范围也就是从pos
位置开始的到末尾的_size
结束这段距离,所以当这个所取长度过长的话,我们就要考虑去更新一下取子串长度的有效范围n
作为可取的子串长度,一开始得让其等于传入进来的len
长,因为如果这个所取长度没有超出有效范围的话,我们所用的还是len
n = _size - pos
size_t n = len;
if (len == npos || pos + len > _size)
{
// 就算要取再大的长度,也只能取到pos - _size的位置
n = _size - pos;
}
pos
位置开始取,取【n】个即可,然后追加到这个临时的 string对象 中去,最后呢再将其返回即可,那我们返回一个出了作用域就销毁的临时对象,只能使用【传值返回】,而不能使用【传引用返回】string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;
n
是怎么发生变化的最后的话再来模拟一些【非成员函数重载】,使用到的也是非常多
这里有很多的关系运算符我们来模拟实现一下
① 小于
bool operator<(const string& s)
true
;出现大于的情况就返回false
;如果是相等情况的话则双指针继续向后进行遍历,直接有一个遍历结束位置跳出循环size_t i1 = 0;
size_t i2 = 0;
while (i1 < _size && i2 < s._size)
{
if (_str[i1] < s._str[i2])
{
return true;
}
else if (_str[i1] > s._str[i2])
{
return false;
}
else
{
i1++;
i2++;
}
}
return false
return false
return true
return i1 < _size && i2 == s._size;
return _size < s._size;
不过呢,上面这种方法虽然易懂一些,但是并不精炼
bool operator<(const string& s)
{
int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
return ret == 0 ? _size < s._size : ret < 0;
}
ret == 0
为前提条件,然后比较的便是二者的_size
大小;那如果这个ret != 0
的话我们只需要返回小于0的那种情况即可② 等于
operator==
,这里我们可以使用到的是【逻辑运算符】先去排除掉一部分的情况,因为若是两个对象的_size
都不相同的话,那一定是不会相同的_size
相同的情况下,我们再去使用memcpy()
根据字节去一一比价两个对象中_str
的内容,只有其返回值为0的时候才表示两个对象完全相同bool operator==(const string& s)
{
return _size == s._size && memcmp(_str, s._str, s._size) == 0;
}
那有了上面的【小于】和【等于】之后,下面的我们就可以去做一个复用了,这一块我们在 类的六大天选之子 中讲解日期类的关系运算符重载时有提到过
③ 小于等于
bool operator<=(const string& s)
{
return *this < s || *this == s;
}
② 大于
bool operator>(const string& s)
{
return !(*this <= s);
}
② 大于等于
bool operator>=(const string& s)
{
return !(*this < s);
}
② 不等于
bool operator!=(const string& s)
{
return !(*this == s);
}
立马来测试一下吧
最后的话再来补充两个【流插入】和【流提取】,也是非常地重要
this
所指向的对象默认成为第一个参数的话,我们需要将这个函数实现到类外来,如果要访问类内私有成员的话,就可以使用到【右元】这个东西,不过呢我们不建议使用这个,会破坏类的封装性// 流插入
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
= delete
ostream operator<<(ostream& out, const string& s)
好,那到这里的话,我们是时候来讲讲这个cout << s.c_str()
和 cout << s
的区别了
接下去再来看看这个【>>流提取】
const
,因为我们会去修改这个 sistream& operator>>(istream& in, string& s)
【第一版本】:无法读取 空格 和 换行符
cin >> ch
来将缓冲区内的字符放到【ch】中,接着以换行作为结束读取的标志来不断读取下一个字符并拼接到对象 s 中去istream& operator>>(istream& in, string& s)
{
char ch;
in >> ch;
while (ch != '\n')
{
s += ch;
in >> ch;
}
return in;
}
\n
的时候缓冲区会继续等待字符的输入,而不是结束读取istream
中有一个接口叫做【get】,我们使用它就可以读取到空格和换行符了// 流提取
istream& operator>>(istream& in, string& s)
{
char ch = in.get();
while (ch != '\n') // 以换行作为分隔符
{
s += ch;
ch = in.get();
}
return in;
}
ch = in.get()
读取到了中间的空格,而且在读取到\n
换行符的时候也成功退出了循环clear()
吗,用来清理 string对象 中的数据void clear()
{
_str[0] = '\0';
_size = 0;
}
s += ch
就会去产生频繁扩容的现象,这其实是不好的
那有同学说:那我们在读取数据之前就开出一个很大的数组来不就好了,这样肯定能装得下无需扩容了
1024B
的空间, 剩下的1023B
不是造成了很大的浪费吗?带着上面的这些疑问,我们一起改造一下这个流提取的接口
128
即可char buf[128];
int i = 0;
i
去做一个计数while (ch != '\n') // 以换行作为分隔符
{
buf[i++] = ch;
// ...
ch = in.get();
}
i
是否到达了 127,若是的话就不能再继续读取了,而是要把最后的\0
给手动加上,那这就算是一个完整的字符串了,追加到 string对象 中的即可,最后的话别忘了把i
重置为0,继续下一组数据的读取if (i == 127)
{
buf[i] = '\0';
s += buf;
i = 0;
}
i != 0
的话,即没有到达127,只能说明这一组数据还无法追加到对象中。那我们还要再去做一个手动追加,防止数据丢失if (i != 0)
{
buf[i] = '\0';
s += buf;
}
整体代码如下:
// 流提取
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
char buf[128];
int i = 0;
while (ch != '\n') // 以换行作为分隔符
{
buf[i++] = ch;
// 不能等到128再去判断,要为最后的\0留空间
if (i == 127)
{
buf[i] = '\0';
s += buf;
i = 0;
}
ch = in.get();
}
// 若是有数据且不到127的话,进行倒入
if (i != 0)
{
buf[i] = '\0';
s += buf;
}
return in;
}
最后我们再来测试一下,发现确实扩容的次数大大减少了
难道你认为这样就完了吗?不,还有一点我们没考虑到
【第五版本】:清理字符前多余的空格
get
函数即可,它可以读取到缓冲区中的所有内容// 处理前缓冲区前面的空格或者换行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
整体代码如下:
// 流提取
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
// 处理前缓冲区前面的空格或者换行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
char buf[128];
int i = 0;
while (ch != '\n') // 以换行作为分隔符
{
buf[i++] = ch;
// 不能等到128再去判断,要为最后的\0留空间
if (i == 127)
{
buf[i] = '\0';
s += buf;
i = 0;
}
ch = in.get();
}
// 若是有数据且不到127的话,进行倒入
if (i != 0)
{
buf[i] = '\0';
s += buf;
}
return in;
}
然后再去测试一下上面的两个场景,就发现什么问题了
最后我们再来介绍一个东西叫做【写时拷贝】
所以呢有人就提出了这么一个东西,叫做【写时拷贝】,全称叫做【引用计数 · 写时拷贝】
>= 1
的话,说明这块空间的维护者不止它一个,那么其就不可以去释放掉这块空间,而是将计数器--
,那么此时这个计数器就变成了【1】;接下去当另一个对象再去调用析构函数的时候,发现这个计数器的值是为【1】,表示现在只有它在维护这块空间,其便会去释放掉这块空间当然除了解决析构两次的问题,面对拷贝修改这一块它也做了一些文章
计数器--
那有的同学说:那反正这最后不还是要去做一个深拷贝的,直接深拷贝不就完了,有什么意义呢?
其实读者可以这么来理解
清楚了什么叫做【写时拷贝】,我们现在就来测试一下
首先我们现到Linux平台下去看看
1 #include <iostream>
2 #include <stdio.h>
3 #include <string.h>
4 using namespace std;
5
6 int main(void)
7 {
8 string s1("abc");
9 string s2(s1);
10
11 printf("Show copy\n");
12 printf("%p\n", s1.c_str());
13 printf("%p\n", s2.c_str());
14 cout << "-----------------" << endl;
15
16 s2[0] = 'x';
17 printf("Show modify\n");
18 printf("%p\n", s1.c_str());
19 printf("%p\n", s2.c_str());
20 cout << "-----------------" << endl;
21
22 return 0;
23 }
那可能有同学就会觉得VS还是比较奇怪的,包括我们在前面对各类接口做对比的时候,VS都会去做一些比较反常的事
VS你可以把他当做是一个财大气粗的老板,下面我们再来谈一谈VS对 string对象 这一块的容量设计
string s1("abc");
cout << sizeof(s1) << endl;
可能有读者认为这个对象中一共就三个成员变量,一个指针两个无符号整数,那大小应该就是 12
size_t _size;
size_t _capacity;
char* _str;
“abc”
存到了一个 Buf 数组中,这个数组可容纳的大小为16个字节,虽然下面我圈起来的是【15】,是因为最后还有一个\0
刚才说到这个 Buf数组 只能存放下16个字节的数据,但是当这个数据量变大的时候怎么办呢?
string s2("abcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
_Ptr
所指向的堆空间中去,我们知道向堆中去申请的空间都是很大的,完全就能放得过了。那就可以看到VS这个机制还是蛮不错的总结一下:
_size < 16
的时候,字符串是存放在【Buf数组】中的_size >= 16
的时候,字符串存在【_Ptr】所指向的堆空间中最后来总结一下本文所学习的内容
以上就是本文要介绍的所有内容,感谢您的阅读