C++之STL
Posted ElevHe
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++之STL相关的知识,希望对你有一定的参考价值。
面向对象三大特性:封装、继承、多态
封装:复用性高的模块抽象出来,进行整理,作为一个整体,提高了代码的复用性
继承:子类继承父类,把父类中所有的属性和行为都获得,不用再次声明,也提高了代码的复用
多态:一个函数名称有多个接口,由于对象不同,父类指针指向子类对象,对象创建的不同,调用一个接口获得的内容不同,会产生不同的形态
STL 即标准模板库,从广义上分为:容器(container) 算法(algorithm) 迭代(iterator)
容器和算法之间通过迭代器进行无缝连接
STL 六大组件
容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
1、容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据
2、算法:各种常用算法,如sort、find、copy、for_each等
3、迭代器:扮演容器与算法之间的胶合剂
4、仿函数:行为类似函数,可作为算法的某种策略
5、适配器:用来修饰容器或者仿函数或迭代器接口的东西
6、空间配置器:负责空间的配置与管理
一、容器
分为序列式容器和关联式容器
序列式容器:强调值的排序,其中每个元素都有其固定的位置
关联式容器:二叉树结构,各元素间没有严格物理意义上的顺序关系
二、算法
算法分为质变算法与非质变算法
质变算法:运算过程中会更改区间内元素内容。如:拷贝,替换,删除等
非质变算法:运算过程中不改变区间内元素内容。如:查找、遍历、寻找极值等
三、迭代器
提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又不会暴露该容器的内部表达方式
每个容器都有自己的专属迭代器。
迭代器种类:
1.输入迭代器2.输出迭代器3.前向迭代器4.双向迭代器5.随机访问迭代器
常用4、5
四、仿函数
仿函数(Functor)又称为函数对象(Function Object)是一个能行使函数功能的类。
通过重载括号运算符实现
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 在惠普实验室完成的原始版本,这是最初的STL
- P. 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 <iostream>
#include <string>
using std::string;
int main()
std::string s1; // 或者直接放出
string s2;
return 0;
构造函数
现在我们就可以正式接触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&a 以上是关于C++之STL的主要内容,如果未能解决你的问题,请参考以下文章 |