
OOP 学习笔记(8)——STL 进阶
STL 进阶
string
字符串类
变长字符串
字符串是 char
的数组,但如果无法提前确认字符串长度,就不好处理。
可以使用vector<char>
?实际上使用很不方便,比如输入输出。
因此 STL 提供了更方便的 string
类型。
可以使用简单的加法来拼接:
也能够使用 cin
、cout
来输入输出。
string
类常用构造及转换函数
string
类基本使用方法
使用上与 vector
类似。
访问元素:
查询长度:
清空:
查询是否为空:
迭代访问:
向尾部增加:
当然相比于 vector
,还可以使用 str.length()
获取字符串长度,与 size()
返回值相同。(这里的长度都是真实的字符串长度,比如 "abc"
长度为 3)
string
类三种输入方式
对于同一种输入:
Mike William
Andy William
#
三种方法的读入分别为:
Mike
Mike William
Mike William
Andy William
注意最后一种的结尾是一个换行符。
("Mike William\nAndy William\n"
)
数值类型的转换
除此之外当然还有 stol()
、stoll()
、stof
、stold
、stoul
、stoull
等函数,含义我觉得大家也猜得出来。
这里注意这些函数大多需要 C++11 乃至更新标准。
iostream
输入输出流
回忆:重载输出流运算符
但 istream
、ostream
是什么?
STL 输入输出流
蓝色框是头文件,白色框为类,黄色框为对象,箭头为继承关系,虚线为实例化。
ostream
和 cout
ostream
即为 ouput stream,是 STL 库中所有输出流的基类。
它重载了针对基础类型的输出流运算符(<<
),当然也有包括 string
在内的部分 STL 其他类也重载了输出流。
这种方法统一了输出接口,改善了 C 中输出方式混乱的状况。
(printf()
需要给出输出类型)
cout
是 STL 中内建的 ostream
类对象,它会将数据送到标准输出流 stdout
(一般也就是屏幕上的显示)。
自己实现简单的 ostream
实现原理:<<
运算符为左结合。
先执行 cout << "hello"
调用第二个函数返回 c1
(cout
的引用)。
再执行 c1 << ' '
调用第一个函数返回 c2
(cout
的引用)。
最后执行 c2 << "world"
调用第二个函数。
格式化输出
需要引用:
然后就可以使用:
上述这些突然出现的“东西”都是流操纵算子。
fixed
、scientific
、defaultfloat
、oct
、hex
、dec
等实现方法和 endl
一样,这是标准中定义的。
但有一些流操纵算子在规范里没有规定实现方式,不同编译器实现可能不同,比如 setprecision()
、setw()
、setfill()
等。
所以很难讲一共有多少种实现方式,但按道理来讲,在同一编译器内,应该就是分为不带参数和带参数两种。
流操纵算子:setprecision()
以 setprecision()
为例:
一种可能的实现:
借助辅助类设置成员变量,这种类称为流操纵算子(stream manipulator)。
流操纵算子:endl
C++ 标准中 endl
的声明:
endl
是一个函数,等同于输出 '\n'
,再清空缓冲区 os.flush()
。
可以调用 endl(cout)
。
缓冲区:
- 目的是减少外部读写次数。
- 写文件时,只有清空缓存区或关闭文件才能保证内容正确写入。
但 endl
同时又是流操纵算子,这如何实现?
一种实现方式:
cout
不能复制
注意重载流运算符的方式,全部都采用了引用。
这是因为需要避免复制。
实际上:
也就是禁止复制,只允许移动。
仅能使用一个对象可以:
- 减少复制开销;
- 一个对象对应一个标准输出,符合 OOP 思想;
- 多个对象之间无法同步输出状态。
当然这也会带来问题:全局对象往往引入初始化顺序问题。
其中涉及的单件模式(Singleton Pattern)之后应该会介绍。
文件输入输出流
以文件输入流作为例子,ifstream
是 istream
的子类,功能是从文件中读入数据。
一些基本打开操作:
读入示例:
其他操作:
getline(ifs, str)
,ifstream
是istream
的派生类,因此getline()
仍然有效。get()
用于读取一个字符。ignore(int n = 1, int delim = EOF)
丢弃 n 个字符,或者直至碰到delim
分隔符。peek()
检查下一个字符。putback(char c)
返还一个字符。unget()
返还一个字符。
istream
与 scanf()
为何 C++ 使用流取代了 scanf()
?
scanf()
不友好,不同类型要使用不同的标识符。- 安全性不好。
- 可拓展性差(自定义类)。
- 性能:
scanf()
在运行期间需要对格式字符串进行解析,istream
在编译期间已经解析完毕。
注:实际读入文件 cin
比 scanf
慢,因为默认 cin
与 stdin
总是保持同步,如果使用:
取消同步后,scanf()
速度往往不如 cin
。
stringstream
字符串输入输出流
字符串输入输出流
以输入输出流为作为例子。
stringstream
是 iostream
的子类。
iostream
继承于 istream
和 ostream
。
stringstream
实现了输入输出流双方的接口。
可以发现这是一个多重继承关系。
stringstream
顾名思义:
- 它在对象内部维护了一个
buffer
。 - 使用流输出函数可以将数据写入
buffer
。 - 使用流输入函数可以从
buffer
中读出数据。
一般用于程序内部的字符串操作。
构造方式:
使用示例:
可以连接字符串,也可以将字符串转换为其他类型数据。
配合流操作算子,可以达到格式化输出效果。
获取 stringstream
的 buffer
ss.str()
返回一个 string
对象,内容就是 stringstream
的 buffer
。
但注意 buffer
内容并不是未读取的内容,也就是即便经过了 >>
的部分也会被一起返回(仍然存在 buffer
中)。
实现一个类型转换函数
对于一般的类型:
一种简单的实现方法:
状态位和缓冲区的差别,可以自己查看资料。
clear()
会清除错误标志位、流末位标志,如果不调用可能导致非法输入。
函数对象
排序
给定长度为 n 的数组,如何最快地排序?
std::sort()
引用自 <algorithm>
一般来说是最好的选择。
具体使用:
但如果我们想降序呢?
注意到实际上 sort()
还重载了另一套参数:
一种使用方法:
函数对象
STL 提供了预定义的比较函数——<functional>
头文件。
从小到大:
从大到小:
比较函数为何带括号?因为这是函数对象(仿函数)。
实现
注意 sort()
函数要求:comp
不能修改数据。
一般情况下,comp
也不应该修改自身。
所以一般加上 const
标识符限定。
两种方法对比
此时 sort()
的第三个参数是函数指针。
此时第三个参数是函数对象。
一种 O(n2) 的sort()
实现
值得注意的是,std::sort()
的复杂度实际上是严格的 O(nlogn),其中涉及到快速排序和堆排序算法在此教程暂不涉及。
自定义类型的排序
可以使用重载 <
运算符的方法。
但如果有多种排序方式的需求,则可以定义比较函数(或函数对象)。
STL 与函数对象
STL 中有大量函数用到了函数对象,在 <algorithm>
头文件中有:
for_each()
:对序列进行制定操作。find_if()
:找到满足条件的对象。count_if()
:对满足条件的对象计数。binary_search()
:二分查找满足条件的对象。
并且也有很多预置的函数对象,在 <functional>
头文件中有:
less
:比较a < b
。equal_to
:比较a == b
。greater
:比较a > b
。plus
:返回a + b
。
智能指针与引用计数
指针的销毁
A
,B
对象共享一个 C
对象,C
对象不想交由外部销毁,那如何确定谁来销毁 C
?
这是就可以引入智能指针。
智能指针
shared_ptr
来自 <memory>
库。(从 C++11 开始有)
构造方法:
访问对象:
销毁对象:
p2
,p3
指向同一对象,当两者均出作用域才会被销毁。
引用计数
为什么智能指针能够知道何时销毁对象?
因为其中有引用计数,只有当引用计数归为 0 时,才会销毁对象。
输出为:
1 2 2 1
自己实现引用计数
注意此处并没有实现空指针以及直接调用构造函数的写法,只是实现了存储指针。
其他用法
p.get()
获取裸指针;p.reset()
清除指针并减少引用计数;static_pointer_cast<int>(p)
;dynamic_pointer_cast<Base>(p)
。(基类指针)
注意:
- 不能使用同一个裸指针初始化多个智能指针,会导致产生多个辅助指针。
不能直接使用智能指针维护数组对象。
(所有的实现中都是使用
delete p;
,但是数组却是delete [] p;
)智能指针不总是智能。
输出为:
parent constructing child constructing
因为结束时两个对象的引用次数都是 1,内存泄漏!
解决方法为:
输出为:
parent constructing child constructing parent destructing child destructing
弱引用
weak_ptr
指向对象但不计数。也就是说,在函数结束前,
Parent
的计数为 1,Child
的计数为 2。而
Parent
就可以正常销毁,并使得Child
的计数也减少 1,此时Child
也能正常销毁。
弱引用
弱引用指针的创建:
用法:
实例:
独享共有权
shared_ptr
涉及引用计数,性能较差。
如果要保证一个对象只被一个指针引用,可以使用 unique_ptr
:
智能指针总结
- 优点:
- 只能指针可以帮助管理内存,避免内存泄漏。
- 区分
unique_ptr
和shared_ptr
能够明确语义。 - 在手动维护指针不可行,复制对象开销太大时,智能指针是唯一选择。
- 缺点:
- 性能会受到影响。
- 智能指针不总是智能,需要了解内部原理。
- 需要小心环状结构和数组指针。
lambda 表达与 STL 函数封装
lambda 表达式
可以一行创建函数吗?(别跟我说缩代码)
这时可以引入 lambda 表达式——简便地创建匿名函数的方法。
参数列表和函数体
对应真实函数的参数列表和函数体,较容易理解。
捕获列表
实际上这种捕获方式为按值捕获,在函数声明的一刻,确定捕获变量的值,所以会输出 3。
当然还有按引用捕获,也就是在函数声明的一刻,确定捕获变量的引用。
一般来说有以下常见用法:
[]
:不捕获变量。[a, &b]
:按值捕获a
,引用捕获b
。[this]
:按值捕获this
指针。[=]
:按值捕获所有外部变量。[&]
:按引用捕获所有外部变量。[=, &a]
:按引用捕获a
,其与变量按值捕获。[&, =a]
:按值捕获a
,其余变量按引用捕获。
注意,当按引用捕获时,是可以修改外部变量的值的。
而如果按值捕获,一般而言传入的值不能被修改(非左值),如果想要修改,需要加上 mutable
标识符(当然修改的只是传入的副本,不会影响外界):
返回值
在没有指定的情况下,lambda 表达式会自动推断。
如果想要指定,可以使用:
(-> auto
的用法与一般函数一样需要 C++14)
使用 lambda 表达式的优点
- 更加简洁;
- 只使用一次的函数无需命名,避免污染变量空间;
- 增加相关代码的内聚性。
实际上在前面提到过 sort()
函数的第三个参数中,也可以使用 lambda 表达式以简化代码。
同样也可以配合模板函数(成员函数)使用(编译期进行绑定)。
但如果想要动态绑定,比如:
这就不能用模板在编译期进行绑定。
如何储存不同类型的函数指针和函数对象?
function
类
function
类提供了函数指针与函数对象的封装:
函数对象的组合
比如如何用 STL 库组合出 lessThan3()
函数?
其中:
bind()
是<functional>
库函数。less<int>()
是被绑定的函数。placeholders::_1
:被绑定函数的第一个参数来自新函数的第一个参数。3
:被绑定函数的第二个参数为 3。
其返回值为一个函数对象,功能与 lessThan3()
一致。
同样,甚至可以使用 less
组合出 greater
。
字符串处理与正则表达式
正则表达式
正则表达式是搜索文本时定义的一种规则。
给定一个正则表达式和另一个字符串。
- 匹配整个字符串是否满足条件。
- 查找符合正则表达式的子串。
- 按规则替换字符串的部分。
比如一个正常邮箱地址的通用正则表达式为:
\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}
(看起来长的很不友善,反正我也不会)
基本模式
- 字符代表其本身:
once
匹配句子中所有的once
。 - 转义字符:
\n
表示换行,\t
表示制表符,后面会提到的各种通配符如果需要用到本身,也可以使用前面加一个\
来表示,比如\.
。 - 特殊匹配字符:
^
代表开头,$
代表结尾。^\t
只能匹配到以制表符开头的内容。^bucket$
只能匹配到只含bucket
的内容。
字符簇
- 匹配的单个字符在某个范围中:
[aeiou]
匹配任意一个元音字符。[a-z]
匹配所有单个小写字母。[0-9]
匹配所有单个数字。
- 范围取反:
[^a-z]
匹配所有单个非小写字母。
- 连用:
[a-z][0-9]
:- 匹配所有字母 + 数字的组合,比如
a1
,b9
。 ^[^0-9][0-9]$
:- 用于匹配 2 个长度的内容,且第一个不为数字,第二个为数字。
- 特殊字符:
.
匹配出换行符以外任意字符。\d
等价[0-9]
。\D
等价[^0-9]
。\s
匹配所有空白字符。\S
匹配所有非空白字符。\w
匹配字母、数字、下划线。\W
匹配非字母、数字、下划线。\b
匹配单词边界,或者说它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w
。\B
匹配非单词边界。
重复模式
x{n,m}
代表前面内容出现次数重复 n∼m 次(0≤n≤m):a{4}
匹配aaaa
。a{2,4}
匹配aa
或aaa
或aaaa
。a{2,}
匹配长度大于等于 2 的a
。
- 扩展到字符簇:
[a-z]{5,12}
代表长度为 5∼12 的英文小写字母组合。.{5}
所有长度为 5 的字符,除换行符外。
- 特殊字符:
?
等价{0,1}
。+
等价{1,}
。*
等价{0,}
。
或连接符
匹配模式可以使用 |
进行连接。
(Chapter|Section)[1-9][0-9]?
可以匹配Chapter 1
、Section 10
等。0\d{2}-\d{8}|0\d{3}-\d{7}
可以匹配010-12345678
、0376-2233445
等。
其中使用的 ()
可以改变优先级。
m|food
可以匹配m
或者food
。(m|f)ood
可以匹配mood
或者food
。
一个例子:
((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
可以匹配所有可能的 IP 地址(IPv4)。
分组
以 ()
标识的部分被称作分组:
- 正则表达式匹配后,每个分组的内容将被捕获。
- 用于提取关键信息,例如
version(\d+)
即可捕获版本号。
分组会按顺序标号:
- 0 号永远是匹配的字符串本身。
(a)(pple)
中 0 号为apple
,1 号为a
,2 号为pple
。
使用 (?:pattern)
可以不捕获该分组。
更多内容
- 预查:
- 正向预查
(?=pattern)
(正向肯定预查)、(?!pattern)
(正向否定预查)。 - 反向预查
(?<=pattern)
(反向肯定预查)、(?<!pattern)
(反向否定预查)。 - 预查不会移动匹配指针,也就是其只是一个预先或者后置的匹配。
- 具体可以参见这里。
- 正向预查
- 后向引用:
\b(\w+)\b\s+\1\b
匹配重复两遍的单词,比如go go
或kitty kitty
。- 这里的
\1
即代表需要与第一个分组中内容相同。
- 贪婪与懒惰
- 默认多次重复为贪婪匹配,即匹配次数最多。
- 在重复模式后加
?
可以变为懒惰匹配,即匹配次数最少,比如.*?
。
原生字符串
- 正则表达式往往会有很多
\
,字符串表示时应该写为\\
。 原生字符串可以取消转义,保留字面值。
语法:
R"(str)"
表示str
的字面值。比如:
"\\d+" = R"(\d+)" = \d+
。原生字符串可以换行,比如:
- 结果
str = "Hello\nWorld"
。
正则表达式库 <regex>
正则表达式匹配的三种模式:
- 匹配:
- 询问字符串是否能匹配正则表达式,并捕获响应分组。
regex_match()
。
- 替换:
- 替换字符串中匹配的子串,并替换成相应内容。
regex_replace()
。
- 搜索:
- 搜索字符串中匹配的子串,并捕获相应分组。
regex_search()
。
匹配实例:
输出为:
3 matches
the matches were: [subject] [sub] [ject]
更多用法请自行探索。
Comments: 4
匹配 Email 的那个正则看起来有问题,应该是
\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}
看起来又是 markdown 编辑器出锅了。
还是不对,大括号里{2,14}不能有空格
习惯性打的,,忘记了