modern cpp learning(二)

容器、智能指针与正则表达式.

容器

常用的容器如下:

  1. std::vector:
    • 动态数组容器,支持随机访问。
    • 可以动态增加或减少容器大小。
    • 适用于需要快速随机访问的场景。
  2. std::list:
    • 双向链表容器,支持高效的插入和删除操作。
    • 不支持随机访问,但在需要频繁插入/删除的场景下性能更好。
    • 适用于需要频繁插入/删除的数据结构,如栈、队列等。
  3. std::deque(双端队列):
    • 双端队列容器,支持在头尾快速插入和删除。
    • 实现上结合了数组和链表的优点。
    • 适用于需要在头尾高效插入/删除的场景。
  4. std::set/std::unordered_set:
    • 有序集合和无序集合容器,自动排序/散列存储元素。
    • 支持高效的查找、插入和删除操作。
    • 适用于需要去重和快速查找的场景。
  5. std::map/std::unordered_map:
    • 关联数组容器,以键-值对的形式存储元素。
    • 有序映射和无序映射,支持高效的查找、插入和删除。
    • 适用于需要快速查找或存储键值对的场景。
  6. std::stack/std::queue:
    • 栈和队列容器,提供先进先出(FIFO)和后进先出(LIFO)的操作。
    • 基于其他容器(如 std::deque)实现。
    • 适用于需要实现栈和队列数据结构的场景。

主要的 STL 算法:

  1. 排序算法:
    • std::sort(): 使用快速排序算法对元素进行排序。
    • std::stable_sort(): 使用稳定排序算法(如归并排序)对元素进行排序。
    • std::partial_sort(): 将前 N 个元素排序,其他元素保持原有顺序。
    • std::nth_element(): 将第 N 个元素放到正确的位置,其他元素的相对顺序不变。
  2. 查找算法:
    • std::find(): 在序列中查找指定元素。
    • std::find_if(): 使用自定义条件在序列中查找元素。
    • std::binary_search(): 在已排序的序列中进行二分查找。
    • std::lower_bound(): 返回指向序列中第一个不小于给定值的元素的迭代器。
    • std::upper_bound(): 返回指向序列中第一个大于给定值的元素的迭代器。
  3. 修改算法:
    • std::transform(): 对序列中的每个元素应用给定的函数。
    • std::replace(): 用新值替换序列中满足条件的元素。
    • std::reverse(): 反转序列中的元素。
    • std::rotate(): 将序列中的元素循环移动指定的距离。
  4. 数值算法:
    • std::accumulate(): 计算序列中元素的累加和。
    • std::inner_product(): 计算两个序列的点积。
    • std::partial_sum(): 计算序列中前 N 个元素的累加和。
  5. 集合算法:
    • std::merge(): 将两个有序序列合并成一个有序序列。
    • std::set_union(): 计算两个集合的并集。
    • std::set_intersection(): 计算两个集合的交集。
    • std::set_difference(): 计算两个集合的差集。

常用头文件

  1. 容器:
    • std::vector: <vector>
    • std::list: <list>
    • std::deque: <deque>
    • std::set: <set>
    • std::unordered_set: <unordered_set>
    • std::map: <map>
    • std::unordered_map: <unordered_map>
    • std::stack: <stack>
    • std::queue: <queue>
  2. 算法:
    • std::sort: <algorithm>
    • std::find: <algorithm>
    • std::accumulate: <numeric>
    • std::transform: <algorithm>
    • std::copy: <algorithm>
  3. 其他常用头文件:
    • <iostream>: 用于输入输出
    • <string>: 用于字符串操作
    • <memory>: 用于智能指针
    • <functional>: 用于函数对象和 lambda 表达式

线性容器

std::array

为什么要使用这个?

  • 节省内存
  • 相比于普通数组更加现代化

std::vector 不同,std::array 对象的大小是固定的,如果容器大小是固定的,那么可以优先考虑使用 std::array 容器。 另外由于 std::vector 是自动扩容的,当存入大量的数据后,并且对容器进行了删除操作, 容器并不会自动归还被删除元素相应的内存,这时候就需要手动运行 shrink_to_fit() 释放这部分内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  std::vector<int> v;
std::cout << "size:" << v.size() << std::endl;
std::cout << "capacity:" << v.capacity() << std::endl;
v.push_back(1);
v.push_back(2);
v.push_back(3);
std::cout << "size:" << v.size() << std::endl; // 输出 3
std::cout << "capacity:" << v.capacity() << std::endl;

v.clear();
std::cout << "size:" << v.size() << std::endl; // 输出 3
std::cout << "capacity:" << v.capacity() << std::endl;
v.shrink_to_fit();
std::cout << "size:" << v.size() << std::endl; // 输出 0
std::cout << "capacity:" << v.capacity() << std::endl;

使用 std::array 能够让代码变得更加“现代化”,而且封装了一些操作函数,比如获取数组大小以及检查是否非空,同时还能够友好的使用标准库中的容器算法,比如 std::sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
std::array<int, 4> arr = {1, 2, 3, 4};

arr.empty(); // 检查容器是否为空
arr.size(); // 返回容纳的元素数

// 迭代器支持
for (auto &i : arr)
{
// ...
}

// 用 lambda 表达式排序
std::sort(arr.begin(), arr.end(), [](int a, int b) {
return b < a;
});

// 数组大小参数必须是常量表达式
constexpr int len = 4;
std::array<int, len> arr = {1, 2, 3, 4};

// 非法,不同于 C 风格数组,std::array 不会自动退化成 T*
// int *arr_p = arr;

当我们开始用上了 std::array 时,难免会遇到要将其兼容 C 风格的接口,这里有三种做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
void foo(int *p, int len) {
return;
}

std::array<int, 4> arr = {1,2,3,4};

// C 风格接口传参
// foo(arr, arr.size()); // 非法, 无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());

// 使用 `std::sort`
std::sort(arr.begin(), arr.end());

std::forward_list

std::forward_list 是一个列表容器,使用方法和 std::list 基本类似

需要知道的是,和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现, 提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点), 也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率

无序容器

我们已经熟知了传统 C++ 中的有序容器 std::map/std::set,这些元素内部通过红黑树进行实现, 插入和搜索的平均复杂度均为 O(log(size))在插入元素时候,会根据 < 操作符比较元素大小并判断元素是否相同, 并选择合适的位置插入到容器中。当对这个容器中的元素进行遍历时,输出结果会按照 < 操作符的顺序来逐个遍历。

而无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant), 在不关心容器内部元素顺序时,能够获得显著的性能提升.

C++11 引入了的两组无序容器分别是:std::unordered_map/std::unordered_multimapstd::unordered_set/std::unordered_multiset,它们的用法和原有的 std::map/std::multimap/std::set/set::multiset 基本类似

无序容器插入和搜索元素的平均复杂度O(1),所以不需要排序时可以考虑使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>
#include <unordered_map>
#include <map>

int main() {
// 两组结构按同样的顺序初始化
std::unordered_map<int, std::string> u = {
{1, "1"},
{3, "3"},
{2, "2"}
};
std::map<int, std::string> v = {
{1, "1"},
{3, "3"},
{2, "2"}
};

// 分别对两组结构进行遍历
std::cout << "std::unordered_map" << std::endl;
for( const auto & n : u)
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";

std::cout << std::endl;
std::cout << "std::map" << std::endl;
for( const auto & n : v)
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
}

元组

纵观传统 C++ 中的容器,除了 std::pair 外, 似乎没有现成的结构能够用来存放不同类型的数据(通常我们会自己定义结构)。 但 std::pair 的缺陷是显而易见的,只能保存两个元素。

核心操作

关于元组的使用有三个核心的函数:

  1. std::make_tuple: 构造元组(装包)
  2. std::get: 获得元组某个位置的值
  3. std::tie: 元组拆包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <tuple>
#include <iostream>

auto get_student(int id)
{
// 返回类型被推断为 std::tuple<double, char, std::string>

if (id == 0)
return std::make_tuple(3.8, 'A', "张三");
if (id == 1)
return std::make_tuple(2.9, 'C', "李四");
if (id == 2)
return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只写 0 会出现推断错误, 编译失败
}

int main()
{
auto student = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';

double gpa;
char grade;
std::string name;

// 元组进行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "成绩: " << grade << ", "
<< "姓名: " << name << '\n';

std::get 除了使用常量获取元组对象外,C++14 增加了使用类型来获取元组中的对象

1
2
3
4
std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8);
std::cout << std::get<std::string>(t) << std::endl;
std::cout << std::get<double>(t) << std::endl; // 非法, 引发编译期错误
std::cout << std::get<3>(t) << std::endl;

运行期索引

如果你仔细思考一下可能就会发现上面代码的问题,std::get<> 依赖一个编译期的常量,所以下面的方式是不合法的:

1
2
int index = 1;
std::get<index>(t);

可以通过constexpr解决这个问题

如果想通过运行期要怎么处理?答案是,使用 std::variant<>(C++ 17 引入),提供给 variant<> 的类型模板参数 可以让一个 variant<> 从而容纳提供的几种类型的变量(在其他语言,例如 Python/JavaScript 等,表现为动态类型):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <variant>
template <size_t n, typename... T>
constexpr std::variant<T...> _tuple_index(const std::tuple<T...>& tpl, size_t i) {
if constexpr (n >= sizeof...(T))
throw std::out_of_range("越界.");
if (i == n)
return std::variant<T...>{ std::in_place_index<n>, std::get<n>(tpl) };
return _tuple_index<(n < sizeof...(T)-1 ? n+1 : 0)>(tpl, i);
}
template <typename... T>
constexpr std::variant<T...> tuple_index(const std::tuple<T...>& tpl, size_t i) {
return _tuple_index<0>(tpl, i);
}
template <typename T0, typename ... Ts>
std::ostream & operator<< (std::ostream & s, std::variant<T0, Ts...> const & v) {
std::visit([&](auto && x){ s << x;}, v);
return s;
}

这样我们就能:

1
2
int i = 1;
std::cout << tuple_index(t, i) << std::endl;

元组合并与遍历

还有一个常见的需求就是合并两个元组,这可以通过 std::tuple_cat 来实现:

1
2
std::tuple t = std::make_tuple(1, 2.0, "3");
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));

马上就能够发现,应该如何快速遍历一个元组?但是我们刚才介绍了如何在运行期通过非常数索引一个 tuple 那么遍历就变得简单了, 首先我们需要知道一个元组的长度,可以:

1
2
3
4
template <typename T>
auto tuple_len(T &tpl) {
return std::tuple_size<T>::value;
}

这样就能够对元组进行迭代了:

1
2
3
4
// 迭代
for(int i = 0; i != tuple_len(new_tuple); ++i)
// 运行期索引
std::cout << tuple_index(new_tuple, i) << std::endl;

智能指针和内存管理

在传统 C++ 中,『记得』手动释放资源,总不是最佳实践。因为我们很有可能就忘记了去释放资源而导致泄露。 所以通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII 资源获取即初始化技术。

凡事都有例外,我们总会有需要将对象在自由存储上分配的需求(堆空间,指针等),在传统 C++ 里我们只好使用 newdelete 去 『记得』对资源进行释放。而 C++11 引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。 这些智能指针包括 std::shared_ptr/std::unique_ptr/std::weak_ptr,使用它们需要包含头文件 <memory>

注意:引用计数不是垃圾回收,引用计数能够尽快收回不再被使用的对象,同时在回收的过程中也不会造成长时间的等待, 更能够清晰明确的表明资源的生命周期。

std::shared_ptr

std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。

但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。

std::make_shared 就能够用来消除显式的使用 new,所以std::make_shared 会分配创建传入参数中的对象, 并返回这个对象类型的std::shared_ptr指针。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <memory>
void foo(std::shared_ptr<int> i) {
(*i)++;
}
int main() {
// auto pointer = new int(10); // illegal, no direct assignment
// Constructed a std::shared_ptr
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl; // 11
// The shared_ptr will be destructed before leaving the scope
return 0;
}

std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数, 并通过use_count()来查看一个对象的引用计数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get(); // 这样不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3

pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = "
<< pointer2.use_count() << std::endl; // pointer2 已 reset; 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = "
<< pointer3.use_count() << std::endl; // pointer3 已 reset; 0

std::unique_ptr

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全

1
std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入std::unique_ptr<int> pointer2 = pointer; // 非法

C++11 没有提供 std::make_unique,可以自行实现:

1
2
3
4
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}

既然是独占,换句话说就是不可复制。但是,我们可以利用 std::move 将其转移给其他的 unique_ptr,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <memory>

struct Foo {
Foo() { std::cout << "Foo::Foo" << std::endl; }
~Foo() { std::cout << "Foo::~Foo" << std::endl; }
void foo() { std::cout << "Foo::foo" << std::endl; }
};

void f(const Foo &) {
std::cout << "f(const Foo&)" << std::endl;
}

int main() {
std::unique_ptr<Foo> p1(std::make_unique<Foo>());
// p1 不空, 输出
if (p1) p1->foo();
{
std::unique_ptr<Foo> p2(std::move(p1));
// p2 不空, 输出
f(*p2);
// p2 不空, 输出
if(p2) p2->foo();
// p1 为空, 无输出
if(p1) p1->foo();
p1 = std::move(p2);
// p2 为空, 无输出
if(p2) p2->foo();
std::cout << "p2 被销毁" << std::endl;
}
// p1 不空, 输出
if (p1) p1->foo();
// Foo 的实例会在离开作用域时被销毁
}

使用 std::move(p1)p1 的所有权转移给 p2 时, p1 会变为空指针的原因如下:

  1. std::unique_ptr 是一种独占式的智能指针,这意味着一个 std::unique_ptr 对象只能拥有一个动态分配的对象的所有权。
  2. 当你使用 std::move(p1) 时, p1 的所有权被转移给了 p2。这是因为 std::move 函数将 p1 转换为一个右值引用,从而使所有权从 p1 转移到 p2
  3. 一旦所有权被转移,p1 就不再拥有任何动态分配的对象。因此, p1 被设置为空指针,以确保它不会被用于访问已经被转移所有权的对象。

这种行为是 std::unique_ptr 设计的一部分,旨在确保程序员不会意外地使用已经失去所有权的指针。这样可以帮助我们避免悬空指针和内存泄漏等常见的错误

上面代码可以看出unique_ptr存在作用域,在作用域内如果被转移到其他unque_ptr上离开作用域被销毁,同时原本的在其他作用域内不受影响.

std::weak_ptr

如果你仔细思考 std::shared_ptr 就会发现依然存在着资源无法释放的问题。看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct A;
struct B;

struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A 被销毁" << std::endl;
}
};
struct B {
std::shared_ptr<A> pointer;
~B() {
std::cout << "B 被销毁" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
}

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露

图 5.1

解决这个问题的办法就是使用弱引用指针 std::weak_ptrstd::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加,当换用弱引用时候,最终的释放流程如图.

1
2
3
4
5
6
7
8
9
10
11
struct AA;
struct B;
struct AA {
std::weak_ptr<B> pointer;
~AA() { std::cout << "AA::~AA" << std::endl; }
};

struct B {
std::weak_ptr<AA> pointer;
~B() { std::cout << "B::~B" << std::endl; }
};

图 5.2

std::weak_ptr 不会增加引用计数,使得a,b释放后原本对象没有了引用.

最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。

std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它可以用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true;除此之外,它也可以用于获取指向原始对象的 std::shared_ptr 指针,其 lock() 方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr 指针,进而访问原始对象的资源,否则返回nullptr

1
2
3
4
5
6
7
8
auto aa = std::make_shared<AA>();
auto bb = std::make_shared<B>();
aa->pointer = bb;
bb->pointer = aa;
if (!aa->pointer.expired()) {
auto shared = aa->pointer.lock();
std::cout << "shared:" << shared.use_count() << std::endl;
}

正则表达式

正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求:

  1. 检查一个串是否包含某种形式的子串;
  2. 将匹配的子串替换
  3. 某个串中取出符合条件的子串

检查包含,替换字串,取出字串

正则表达式是由普通字符(例如 a 到 z)以及特殊字符组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。 正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

普通字符

普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

特殊字符

特殊字符是正则表达式里有特殊含义的字符,也是正则表达式的核心匹配语法。参见下表:

特别字符描述
$匹配输入字符串的结尾位置。
(,)标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。
*匹配前面的子表达式零次或多次。
+匹配前面的子表达式一次或多次。
.匹配除换行符 \n 之外的任何单字符。
[标记一个中括号表达式的开始。
?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。
\将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, n 匹配字符 n\n 匹配换行符。序列 \\ 匹配 '\' 字符,而 \( 则匹配 '(' 字符。
^匹配输入字符串的开始位置,除非在方括号表达式中使用,此时它表示不接受该字符集合。
{标记限定符表达式的开始。
``指明两项之间的一个选择

一些正则表达式示例:

  • \s匹配所有空白符,包括换行,\S 非空白符,不包括换行

  • \d+: 匹配一个或多个数字

  • \b 匹配一个单词边界,也就是指单词和空格间的位置.例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’.
  • \w+: 匹配一个或多个字母、数字或下划线字符
  • \s+: 匹配一个或多个空白字符
  • [aeiou]: 匹配任意一个元音字母
  • [a-zA-Z0-9_]+: 匹配一个或多个字母、数字或下划线
  • \b\w+\b: 匹配独立的单词
  • ^[a-z]+$: 匹配全部由小写字母组成的字符串.
匹配解释
(pattern)匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 $0…$9 属性。要匹配圆括号字符,请使用 ‘(‘ 或 ‘)‘。
(?:pattern)匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (\) 来组合一个模式的各个部分是很有用。例如, ‘industr(?:y\ies) 就是一个比 ‘industry\industries’ 更简略的表达式。
(?=pattern)正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,”Windows(?=95\98\NT\2000)”能匹配”Windows2000”中的”Windows”,但不能匹配”Windows3.1”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern)正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如”Windows(?!95\98\NT\2000)”能匹配”Windows3.1”中的”Windows”,但不能匹配”Windows2000”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?<=pattern)反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,”`(?<=9598NT2000)Windows"能匹配"2000Windows"中的"Windows",但不能匹配"3.1Windows"中的"Windows`”。
(?<!pattern)反向否定预查,与正向否定预查类似,只是方向相反。例如”`(?<!9598NT2000)Windows"能匹配"3.1Windows"中的"Windows",但不能匹配"2000Windows"中的"Windows`”。

限定符

限定符用来指定正则表达式的一个给定的组件必须要出现多少次才能满足匹配。见下表:

字符描述
*匹配前面的子表达式零次或多次。例如,foo* 能匹配 fo 以及 foooo* 等价于{0,}
+匹配前面的子表达式一次或多次。例如,foo+ 能匹配 foo 以及 foooo,但不能匹配 fo+ 等价于 {1,}
?匹配前面的子表达式零次或一次。例如,Your(s)? 可以匹配 YourYours 中的Your? 等价于 {0,1}
{n}n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 for 中的 o,但是能匹配 foo 中的两个 o
{n,}n 是一个非负整数。至少匹配 n 次。例如,o{2,} 不能匹配 for 中的 o,但能匹配 foooooo 中的所有 oo{1,} 等价于 o+o{0,} 则等价于 o*
{n,m}mn 均为非负整数,其中 n 小于等于 m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 foooooo 中的前三个 oo{0,1} 等价于 o?。注意,在逗号和两个数之间不能有空格。

std::regex 及其相关

对字符串内容进行匹配的最常见手段就是使用正则表达式。 可惜在传统 C++ 中正则表达式一直没有得到语言层面的支持,没有纳入标准库, 而 C++ 作为一门高性能语言,在后台服务的开发中,对 URL 资源链接进行判断时, 使用正则表达式也是工业界最为成熟的普遍做法。

一般的解决方案就是使用 boost 的正则表达式库。 而 C++11 正式将正则表达式的的处理方法纳入标准库的行列,从语言级上提供了标准的支持, 不再依赖第三方。

C++11 提供的正则表达式库操作 std::string 对象, 模式 std::regex (本质是 std::basic_regex)进行初始化, 通过 std::regex_match 进行匹配, 从而产生 std::smatch (本质是 std::match_results 对象)

1
2
3
4
5
6
std::string fnames[] = {"foot.txt", "bar.txt", "test.txt", "a0.txt"};
std::regex txt_regex("[a-z]+\\.txt");
for (const auto& fname : fnames) {
std::cout << fname << ": " << std::regex_match(fname, txt_regex)
<< std::endl;
}

另一种常用的形式就是依次传入 std::string/std::smatch/std::regex 三个参数, 其中 std::smatch 的本质其实是 std::match_results。 故而在标准库的实现中, std::smatch 被定义为了 std::match_results<std::string::const_iterator>, 也就是一个子串迭代器类型的 match_results。 使用 std::smatch 可以方便的对匹配的结果进行获取,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// std::smatch 的第一个元素匹配整个字符串
// std::smatch 的第二个元素匹配了第一个括号表达式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道