c语言sscanf函数的用法是什么
255
2022-08-23
STL之string学习
写在前面
我们总算是接触到C++的STL了,这是C++模块中很重要的一部分,可以这么说,如果你学C++没有学过STL,那么你的C++很大概率是残缺的,我们学习STL主要学习两个层次,第一个是可以使用,第二个是知道它们的底层,至于更高层次的,现在的我还没有达到,就不和大家分享了。我们先来简单的认识一下STL,有一个大致的了解,今天主要的内容时string。
什么是STL
STL(standard template libaray-标准模板库):是 C++ 标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。我之前和大家分享过数据结构的知识,也和大家用C语言写过,但是它们的可用性有点低,你在写OJ题时,使用C语言,你会发现有的时候还要自己实现数据结构,大佬们也是有这样的困扰,所以出现STL,使用的便是泛型编程,这也是我们前面谈模板的原因。
STL版本
我们学习STL共有三个版本,里面的功能都大致一样。
原始版本 Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,这是最初的STLP. J. 版本 继承HP版本,被微的VS系列使用,代码可读性底SGI版本 继承自HP版 本。被GCC(Linux)采用,可移植性好,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。
STL 组件
STL的六大组件,大家直接看着图片吧,后面的内容都会涉及到,这里大家先来认识一下。
string
我们先来认识一下什么是string,简单来说,string在C语言里我们认识的字符串,在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数 。由于我们比较常用,所以大佬们就把这个给变成了模板类了,某种意义上来说,string不属于STL,它的出现要早的多。
认识string
先看看STL里面关于string的分类,我们可能会感到疑惑,这里怎么会有四种string,我们要学习的是哪一种,SLT不是模板吗,为何这里string看着不像…这些问题我们一个一个解决.
字符编码表
在谈为何会有四种string之前?我们需要谈谈什么是字符编码表.我们都知道在计算机世界中,它们只认识0和1,但是现实世界中,我们是有类似汉语,英语的语言的,我们该如何把现实世界和计算机世界给联系起来呢?这就是字符编码表的作用.最初的字符编码表是由美国提出的,叫做ASCII码,是不支持中文的.
为了编码属于各自国家的字符表,中国编出了GBK,使用的是16位的二进制,但是各国还没有一个统一的标准,这时候国际组织ISO制定了Unicode,也叫万国码.
我们已经知道了字符表的历史,所谓的四种string就是为了支持不同位的字符,是的,我们不是只有char这一个字符类型,我们主要学习的就似乎string,他们四个用法大概差不多,学会了一个,另外几个查查文档就可以了.
int main(){ cout << "string : " << sizeof(char) << endl; cout << "u16string : " << sizeof(char16_t) << endl; cout << "u32string : " << sizeof(char32_t) << endl; cout << "wstring : " << sizeof(wchar_t) << endl; return 0;}
string 是模板码
是的,我们看到的string是typedef过的,也就是它是参数为char类型的模板.
string 的底层
string的底层是一个可以动态开辟的字符数组,这一点大家一定要记住,后面我们模拟实现的时候就是按照这个来的.
int main(){ string s("hello"); return 0;}
我们来真正的看看这个数组,避免出现问题.
使用string
现在我们就可以使用string,我们使用的时候需要包头文件,而且还需要放出std里面的string,少说多做,我们看看就明白了.
#include
构造函数
现在我们就可以正式接触string里面的内容了,注意C98中string里面存在七个构造函数,但是我们不是每一个都常用,这里我来简绍的有四个.
构造函数 | 函数说明 |
string() | 构造一个空字符串 |
string(const string& str) | 拷贝构造,深拷贝 |
string(const char* s) | 通过一个字符串来构造 |
string(size_t n, char c) | 构造一个含有 n 个 c字符的字符串 |
我们现在分别来演示一下.
string()
int main(){ string s; return 0;}
string(const char* s)
int main(){ string s("hello string"); return 0;}
string(size_t n, char c)
int main(){ string s(10,'a'); return 0;}
string(const string& str)
int main(){ string s1("hello"); string s2(s1); return 0;}
长度 & 容量
我们知道了string底层是一个动态开辟的数组,那么string支持了计算我们string对象的有效长度和容量的函数我们直接看看吧.
size() & length()
这两个都是计算string对象的有效长度的,唯一的区别就是函数名不同罢了,length()是最初的方法,我们字符串的有效长度可以形容为length,但是后面的哈希表等就有点说不过去了,所以增加了size()这个函数,为了保持命名的规范性,仅此而已.
int main(){ string s("hello"); cout << s.size() << endl; cout << s.length() << endl; return 0;}
capacity()
计算是当前对象的容量,也就是数组的长度.
int main(){ string s("hello"); cout << s.capacity() << endl; return 0;}
max_size()
这个函数就是我们字符串最大能开多长,一般是没有人用的,这里我就提一下就可以了.
int main(){ string s; cout << s.max_size() << endl; return 0;}
operator[] & at()
string重载了[]这运算符,可以支持下标访问,这里的at()的作用和[]作用是一样的,只不过是早期版本,功能上没有什么区别.
int main(){ string s("hello"); cout << s[1] << endl; cout << s.at(1) << endl; return 0;}
越界访问
我们访问的如果是有效字符的下一个位置,返回的是一个\0,大家现在还不知道底层,但是我们知道空字符串里面是包含一个\0 的,这是为了兼容C语言.
int main(){ string s("hello"); if (s[5] == '\0') { cout << "s[5] == \\0 " << endl; } return 0;}
但是如果我们越界访问了,VS编译器会出现报错.
int main(){ string s("hello"); s[16]; return 0;}
遍历string
遍历string我们存在三种方法,这三种方法也是我们经常用到的。
使用 下标使用 迭代器 这个我们重点谈使用 范围for
### 下标访问
string这个类是重载了[]这个运算符的,再加上string底层是一个数组,它是支持随机访问的,
int main(){ string s("hello"); for (size_t i = 0; i < s.size(); i++) { cout << s[i] << " "; } cout << endl; return 0;}
迭代器访问
或许你还对迭代器有点疑惑,不知道它是什么,这样吧,我简单的说一下,所谓的迭代器就是我们遍历STL最常用的方法.是的,你没有看错,不是所有的STL底层都是连续的空间,也就是说使用下标访问是不具有普遍性的.
string中的迭代器就是一个原生指针,其中迭代器又分为四种.我们来看看吧.
正向迭代器
这里我直接用两种方式来访问,一个是const修饰的,一个可修改的.
下面的是正向迭代器指向的地方,注意end()指向的有效字符的下一个位置.
可以修改迭代器指向的内容
int main(){ string s("hello"); string::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; it++; } cout << endl; return 0;}
如果我们想要修改迭代器指向的内容,这里就可以知道了.
int main(){ string s("hello"); string::iterator it = s.begin(); while (it != s.end()) { (*it)++; cout << *it << " "; it++; } cout << endl; return 0;}
如果我们不想修改迭代器指向的数据,可以直接调用const修饰的迭代器
这个迭代器的主要应用是给那些不想修改的string,我们看看应用.
void func(const string& str){ string::const_iterator it = str.cbegin(); while (it != str.cend()) { cout << *it << " "; it++; } cout << endl;}int main(){ string s("hello"); func(s); return 0;}
反向迭代器
谈完了正向的迭代器,这里就要谈谈什么是反向迭代器,它的作用也是遍历string,只不过是反这遍历的.
这个迭代器是可以修改的,这里就不修改了.
int main(){ string str("hello"); string::reverse_iterator rit = str.rbegin(); while (rit != str.rend()) { cout << *rit << " "; rit++; } cout << endl; return 0;}
现在还有可以反向的const修饰的迭代器,我们用用就可以了,具体的就不谈了.
int main(){ string str("hello"); string::const_reverse_iterator rit = str.crbegin(); while (rit != str.crend()) { cout << *rit << " "; rit++; } cout << endl; return 0;}
范围 for
范围 for就比较简单了,但是有一个缺陷,就是一次肯定遍历完
int main(){ string str("hello"); for (auto ch : str) { cout << ch << " "; } cout << endl; return 0;}
它看着比较高大上,实际上底层也是对迭代器的复用,这里的复用的迭代器是可以修改的那种
push_back()
string支持尾部插入一个字符,如果容量不够,编译器会自动扩容.
int main(){ string s; s.push_back('a'); s.push_back('b'); s.push_back('c'); s.push_back('d'); cout << s << endl; return 0;}
string 扩容机制
我们知道了string的底层是一个动态数组,我们现在想看看它是如何扩容的.
从这里我们可以看出,不同编译器的扩容规则是不一样的,VS下是1.5倍扩容,g++是2倍扩,而且刚开始VS就开辟了15个空间,g++没有开
reserve()
大家都知道,扩容是代价的,有的时候需要再次开辟空间和进行数组的拷贝,如果我们要是知道总共开辟的空间,那么提前开辟好不是更好吗,这就是这个函数的作用.
只改变容量不改变 size,也就是你可以理解为它是只扩容.
注意,reserve()函数开辟的空间不一定和我们要的一样,需要内存对齐的,但是大差不差.
int main(){ string s; s.reserve(100); return 0;}
但是这里就有两个疑问了,如果我们的N大于现在存在的存在的容量会怎么样,小于又会怎么样?这里我们来讨论一下.
N > capacity
这个和上面的一样,会自动扩容,只改变capacity,不改变size.
int main(){ string s("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); int size = s.size(); cout << size << endl; s.reserve(s.capacity() + 20); return 0;}
N < capacity
这种情况如果发生,不会出现任何变化.
int main(){ string s("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); cout << "原始 size " << s.size() << endl; cout << "原始 capacity() " << s.capacity() << endl; s.reserve(15); cout << "size " << s.size() << endl; cout << "capacity() " << s.capacity() << endl; return 0;}
resize()
这个函数的作用从名字上面就可以看出它是重置size,也就是说,我们重新指定尾插数据的地方,如果空间不够,编译器会自动扩容.但是这里有两个函数,本质上是一个缺省函数.
int main(){ string s; cout << "size " << s.size() << endl; cout << "capacity() " << s.capacity() << endl; s.resize(20); cout << "size " << s.size() << endl; cout << "capacity() " << s.capacity() << endl; return 0;}
void resize (size_t n)
这个是重置 N个空间,从开始的那里开始数,数到下标为N的地方,这个及其以后都重置的空间变成\0.这里分为三种情况.这里我们就来讨论两种共情况,第三种就是扩容的情况.
情况一
int main(){ string s; s.push_back('a'); s.push_back('b'); s.push_back('b'); s.push_back('b'); s.push_back('b'); s.resize(2); return 0;}
情况二
int main(){ string s; s.push_back('a'); s.push_back('b'); s.push_back('b'); s.resize(7); return 0;}
这里我就给一个总结,所谓的resize就是把有效元素的最后一个位置的后一个位置给改变,改成下标是N,凡是和原来的有效位置之间的距离,都给我变成\0.
void resize (size_t n, char c)
这个就更加简单了,没有什么可以说的,也符合上面的两种规则,也符合不够的扩容规则.就是把默认的\0改成我们想要的字符,这里就不解释其他的情况了.
int main(){ string s; s.push_back('a'); s.push_back('b'); s.resize(20,'1'); cout << s << endl; return 0;}
clear()
清理有效字符,VS下不会进行缩容,看编译器自己的选择.
int main(){ string s("hello"); s.reserve(100); s.clear(); // 只改变 size 缩容不缩容看编译器 return 0;}
append()
我们发现,push_back尾插是可以的,但是它支持插入一个字符,我们想要插入一个字符串,可不可以,string也提供了这个接口.我们把最长用的的几个个大家简绍一下.
append() | 函数说明 |
string& append (const string& str); | 尾插一个string对象 |
string& append (const char* s); | 尾插一个字符串 |
template string& append (InputIterator first, InputIterator last); | 使用迭代器尾插 |
我直接演示一下这几个函数吧,用法还是比较简单的,这里新增的迭代器尾插是我们没有见识的过的,这里单独用一下.
int main(){ string s("hello"); s.append("qkj"); cout << s << endl; string s2("你好啊"); s.append(so2); cout << s << endl; return 0;}
string& append (InputIterator first, InputIterator last)
这个还是挺容易的,它是尾插first到last的内容,没有什么神奇,到后面几个博客,我们会专门谈谈迭代器.
int main(){ string s("hello"); string s2("你好啊"); s.append(s2.begin(),s2.end()); cout << s << endl; return 0;}
operator+=
相对于上面的append,这个才是我们经常用的,而且更容易理解.
这个我们会经常用.
int main(){ string s("hello"); s += "aaaa"; cout << s << endl; s += 'c'; cout << s << endl; string s2("hahahah"); s += s2; cout << s << endl; return 0;}
insert()
一般情况下,我们是不用这个函数的,都是使用的尾插,但是为了让大家了解一下,这里都给大家拿出来,给大家简绍一下比较常用的.
insert() | 函数说明 |
string& insert (size_t pos, const string& str); | 在pos位置插入一个字符串 |
string& insert (size_t pos, const char* s); | |
iterator insert (iterator p, char c); |
大家会用就可以了.
int main(){ string s1("hello"); string s2(" qkj"); s1.insert(5, s2); cout << s1 << endl; s1.insert(2, "xxxxxx"); cout << s1 << endl; string::iterator it = s1.begin(); s1.insert(it + 2, '1'); cout << s1 << endl; return 0;}
erase()
这个是删除下标从pos往后数len个字符,如果不传字符就删除 npos个,这里的npos是一个静态常量,数据很大.
int main(){ string s1("hello"); s1.erase(1,3); cout << s1 << endl; return 0;}
至于那个传迭代器的就不和大家演示了.
swap()
string里面是存在swap函数的,当然我们std中的那个也可以用,只不过需要发生深拷贝,效率有点低.
int main(){ string s1("hello"); string s2("qkj"); cout << s1 << endl; cout << s2 << endl; s1.swap(s2); cout << s1 << endl; cout << s2 << endl; return 0;}
c_str()
这个函数返回就是这个数组的指针,在工作中,有的是不接容string的指针,这里就要string里面的指针,现在可能还没有什么用处.
int main(){ string s1("hello"); const char* str = s1.c_str(); cout << str << endl; return 0;}
find()
find()是查找string里面是不是有我们想要的字符或者字符串,默认是从下标0开始的,返回的是字符的下标或者是字符串的的头位置,找不的话就是返回npos.
int main(){ string s1("hello"); size_t pos = s1.find('e', 3); // 从 下标 3 开始 if (pos != string::npos) { cout << s1[pos] << endl; } else { cout << "没有找到" << endl; } return 0;}
int main(){ string s1("hello"); size_t pos = s1.find("lo", 2); // 从 下标 3 开始 if (pos != string::npos) { cout << "找到了" << endl; } else { cout << "没有找到" << endl; } return 0;}
还有一个rfind()函数,这个是从string对象后面来查找,这里就不和大家分享了.
substr()
这个可以说字符串截断的函数,从pos位置开始截,len个字符,这里也是一个缺省函数.
int main(){ string s1("hello"); string s2 = s1.substr(1, 2); cout << s2 << endl; return 0;}
getline()
我们都知道,>>运算符遇到空格就结束了,这里对于string也是一样的.
int main(){ string s; cin >> s; cout << s << endl; return 0;}
这个函数不是string特有的,在std里面封者.
int main(){ string s; std::getline(cin,s); cout << s << endl; return 0;}
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~