From db172c1e24ad5ad24ef8dbf476a5ed9dce5b969f Mon Sep 17 00:00:00 2001 From: chenhao Date: Mon, 21 Oct 2024 20:38:39 +0800 Subject: [PATCH] Site updated: 2024-10-21 20:38:37 --- .../index.html" | 2 +- .../10/11/C-17-\346\226\260\347\211\271\346\200\247/index.html" | 2 +- .../index.html" | 2 +- "2024/10/12/C-\345\244\232\347\272\277\347\250\213/index.html" | 2 +- "2024/10/12/C-\345\272\223\345\207\275\346\225\260/index.html" | 2 +- "2024/10/12/C-\350\270\251\346\260\264\346\264\274/index.html" | 2 +- "2024/10/12/C-\351\242\204\345\210\266\350\217\234/index.html" | 2 +- "2024/10/18/C-\344\273\200\351\224\246/index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- "2024/10/18/C-\347\232\204\345\244\232\346\200\201/index.html" | 2 +- "2024/10/18/C-\347\232\204\345\270\270\351\207\217/index.html" | 2 +- "2024/10/18/C-\347\232\204\346\265\201/index.html" | 2 +- "2024/10/18/C-\347\232\204\347\273\247\346\211\277/index.html" | 2 +- .../index.html" | 2 +- 2024/10/18/extern-C/index.html | 2 +- .../18/lambda-\350\241\250\350\276\276\345\274\217/index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- 404.html | 2 +- about/index.html | 2 +- archives/2023/12/index.html | 2 +- archives/2023/index.html | 2 +- archives/2024/10/index.html | 2 +- archives/2024/10/page/2/index.html | 2 +- archives/2024/index.html | 2 +- archives/2024/page/2/index.html | 2 +- archives/index.html | 2 +- categories/index.html | 2 +- "categories/\347\274\226\347\250\213/C/index.html" | 2 +- "categories/\347\274\226\347\250\213/C/page/2/index.html" | 2 +- "categories/\347\274\226\347\250\213/index.html" | 2 +- "categories/\347\274\226\347\250\213/page/2/index.html" | 2 +- css/app.css | 2 +- css/comment.css | 2 +- css/mermaid.css | 2 +- index.html | 2 +- js/app.js | 2 +- page/2/index.html | 2 +- tags/CPP11/index.html | 2 +- tags/CPP17/index.html | 2 +- "tags/CPP\346\226\260\347\211\271\346\200\247/index.html" | 2 +- tags/index.html | 2 +- 43 files changed, 43 insertions(+), 43 deletions(-) diff --git "a/2023/12/09/\345\210\251\347\224\250-Obsidian-\346\220\255\345\273\272-PKM/index.html" "b/2023/12/09/\345\210\251\347\224\250-Obsidian-\346\220\255\345\273\272-PKM/index.html" index dfb68db..5018c5a 100644 --- "a/2023/12/09/\345\210\251\347\224\250-Obsidian-\346\220\255\345\273\272-PKM/index.html" +++ "b/2023/12/09/\345\210\251\347\224\250-Obsidian-\346\220\255\345\273\272-PKM/index.html" @@ -1 +1 @@ -利用 Obsidian 搭建 PKM | R1ck's Playground = how

“提高效率最有效的办法,并不是那些让你更自律的手段,而是搭建一个系统,让你这个任务的产出成果,能够复用到下一个任务中,并且将其沉淀为资产,为你带来复利”。

去年在即刻上看到即友关于利用 Obsidian 搭建个人知识体系的介绍,再加上之前看到别的即友也聊到过一些关于 “建立系统复用产出” 的观点,受到了启发,于是自己结合 Obsidian 和 Git 搭建了自己的 PKM(Personal Knowledge Management)。从去年七月中旬(2023.07)开始,把自己以前写的笔记都归纳整理到一起,统一管理和维护。

一下子就打开了我的新世界。

本文从个人体验出发,就 Obsidian 的使用作简要分享:

# 1. 为什么我要搭建 PKM ?

在此之前,我的笔记以各种形式零零散散地散落在世界上的各个角落,飞书云文档、钉钉文档、存放在私人电脑的 markdown 文件、Excel 表格、Typora 文件夹... 这带来的一大问题就是,当我遇到似曾相识的内容需要查阅笔记时,我根本不记得笔记在哪里。

这种经历最令人痛苦的地方在于,当你遇到某个知识点,你模糊地记得你似乎为此查阅过资料、撰写过笔记,但是你在你的各个笔记软件 / 文件里面怎么找呀都找不到,最后只好重新再做一次。

搭 PKM 就很好地解决了这一点,因为我的所有一切都在一个笔记系统下,有就是有,没有就是没有。如果我在我的笔记系统里搜不到,那么我一定没遇到过这个内容,那我就新增 —— 又学到新东西了。如果我在我的笔记系统里搜到了我要找的东西,那么更好,知识被我复用了 —— 又复习巩固了旧知识。

—— 把资源和笔记放在一个专门的笔记系统下统一管理,易于检索的同时,为知识迭代和复用提供了更好的条件。

# 2. 使用感受 ?

首先最直观的,自然而然就是 PKM 的建立,使得我的笔记、知识、经验能够被我多次复用,而不是一次性的。只要是我学习过的、做过笔记的内容,我不会再丢失。即使遗忘,我也能在我的 Obsidian 中快速找到它,它成了我的百科全书。

Obsidian 是一款强大的软件,有强大的社区支持,我的所有笔记在 Obsidian 中被组织得有条不紊。

另外,众所周知,程序员都喜欢折腾,Obsidian 又很适合程序员折腾。

Obsidian 的客制化打开了我参与开源社区的大门。最开始我只是简单地改一改 CSS,做一些比较基础的外观上的客制化。后来随着我笔记的增多,自己也就有了各种各样的需求,于是到官方的插件社区去找插件。然后发现有的插件并不好用,有的有 bug,于是又跑到人家 github 项目上去给作者反馈。如果反馈后问题没被解决,就会自己看它们的源码,想办法自己解决。如果没有插件能够满足我的需求,我就会想办法自己做。

—— 有条不紊地管理笔记的同时,能够有较高自由度的客制化。

# 3. 如何同步?

对于笔记,同步问题始终是令我头疼的一个点。考虑这样的场景:

我在公司上班,对于在实际工作中遇到的问题,撰写相应的笔记,存在公司的电脑中;我在家学习,对于自己感兴趣的领域,撰写相应的笔记和思考,存在我的个人电脑。

我要如何把公司电脑中我的笔记同步到我的个人电脑?我又要如何把我个人电脑的一些笔记同步到公司的电脑?在以前,我真的是用很傻很笨的拷贝方式来实现同步,每个一段时间进行拷贝。

而这一次,对于 Obsidian,我采用的是 git 方案来进行云端同步。本身 Obsidian 是主打本地化的笔记管理,然后我再结合 git 进行云端同步,简直完美。这样,无论是在公司还是在家,维护一个共同的仓库即可,每天手动同步,笔记仓库在 git 上设为私人仓库。

这么做还有一个 “副作用”,就是每次向 github 提交笔记修改时,都能被 github 的热力图跟踪到,于是一段时间下来,你就能很直观地看到你为你的 PKM 所做的努力和贡献。

—— 建立 github 私有仓库进行云端同步,多端维护同一个项目仓库。

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +利用 Obsidian 搭建 PKM | R1ck's Playground = how

“提高效率最有效的办法,并不是那些让你更自律的手段,而是搭建一个系统,让你这个任务的产出成果,能够复用到下一个任务中,并且将其沉淀为资产,为你带来复利”。

去年在即刻上看到即友关于利用 Obsidian 搭建个人知识体系的介绍,再加上之前看到别的即友也聊到过一些关于 “建立系统复用产出” 的观点,受到了启发,于是自己结合 Obsidian 和 Git 搭建了自己的 PKM(Personal Knowledge Management)。从去年七月中旬(2023.07)开始,把自己以前写的笔记都归纳整理到一起,统一管理和维护。

一下子就打开了我的新世界。

本文从个人体验出发,就 Obsidian 的使用作简要分享:

# 1. 为什么我要搭建 PKM ?

在此之前,我的笔记以各种形式零零散散地散落在世界上的各个角落,飞书云文档、钉钉文档、存放在私人电脑的 markdown 文件、Excel 表格、Typora 文件夹... 这带来的一大问题就是,当我遇到似曾相识的内容需要查阅笔记时,我根本不记得笔记在哪里。

这种经历最令人痛苦的地方在于,当你遇到某个知识点,你模糊地记得你似乎为此查阅过资料、撰写过笔记,但是你在你的各个笔记软件 / 文件里面怎么找呀都找不到,最后只好重新再做一次。

搭 PKM 就很好地解决了这一点,因为我的所有一切都在一个笔记系统下,有就是有,没有就是没有。如果我在我的笔记系统里搜不到,那么我一定没遇到过这个内容,那我就新增 —— 又学到新东西了。如果我在我的笔记系统里搜到了我要找的东西,那么更好,知识被我复用了 —— 又复习巩固了旧知识。

—— 把资源和笔记放在一个专门的笔记系统下统一管理,易于检索的同时,为知识迭代和复用提供了更好的条件。

# 2. 使用感受 ?

首先最直观的,自然而然就是 PKM 的建立,使得我的笔记、知识、经验能够被我多次复用,而不是一次性的。只要是我学习过的、做过笔记的内容,我不会再丢失。即使遗忘,我也能在我的 Obsidian 中快速找到它,它成了我的百科全书。

Obsidian 是一款强大的软件,有强大的社区支持,我的所有笔记在 Obsidian 中被组织得有条不紊。

另外,众所周知,程序员都喜欢折腾,Obsidian 又很适合程序员折腾。

Obsidian 的客制化打开了我参与开源社区的大门。最开始我只是简单地改一改 CSS,做一些比较基础的外观上的客制化。后来随着我笔记的增多,自己也就有了各种各样的需求,于是到官方的插件社区去找插件。然后发现有的插件并不好用,有的有 bug,于是又跑到人家 github 项目上去给作者反馈。如果反馈后问题没被解决,就会自己看它们的源码,想办法自己解决。如果没有插件能够满足我的需求,我就会想办法自己做。

—— 有条不紊地管理笔记的同时,能够有较高自由度的客制化。

# 3. 如何同步?

对于笔记,同步问题始终是令我头疼的一个点。考虑这样的场景:

我在公司上班,对于在实际工作中遇到的问题,撰写相应的笔记,存在公司的电脑中;我在家学习,对于自己感兴趣的领域,撰写相应的笔记和思考,存在我的个人电脑。

我要如何把公司电脑中我的笔记同步到我的个人电脑?我又要如何把我个人电脑的一些笔记同步到公司的电脑?在以前,我真的是用很傻很笨的拷贝方式来实现同步,每个一段时间进行拷贝。

而这一次,对于 Obsidian,我采用的是 git 方案来进行云端同步。本身 Obsidian 是主打本地化的笔记管理,然后我再结合 git 进行云端同步,简直完美。这样,无论是在公司还是在家,维护一个共同的仓库即可,每天手动同步,笔记仓库在 git 上设为私人仓库。

这么做还有一个 “副作用”,就是每次向 github 提交笔记修改时,都能被 github 的热力图跟踪到,于是一段时间下来,你就能很直观地看到你为你的 PKM 所做的努力和贡献。

—— 建立 github 私有仓库进行云端同步,多端维护同一个项目仓库。

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/11/C-17-\346\226\260\347\211\271\346\200\247/index.html" "b/2024/10/11/C-17-\346\226\260\347\211\271\346\200\247/index.html" index b635790..b0dc452 100644 --- "a/2024/10/11/C-17-\346\226\260\347\211\271\346\200\247/index.html" +++ "b/2024/10/11/C-17-\346\226\260\347\211\271\346\200\247/index.html" @@ -1 +1 @@ -C++17 新特性 - C++ - 编程 | R1ck's Playground = how

# 1、结构化绑定

#CPP 新特性 #CPP17
C++17 引入了一项非常强大的特性,称为结构化绑定(Structured Bindings),它允许你将复杂的数据结构(如 std::tuple 、数组或用户定义的类型)的成员绑定到命名变量,从而使代码更加清晰和易于理解。结构化绑定的主要目的是让代码更加直观,减少手动提取和命名数据的需要。

以下是结构化绑定的主要特点和用法:
自动解包:结构化绑定允许您将数据结构中的元素自动解包到单独的变量中,而无需手动提取元素。
更具可读性:通过为数据结构的各个成员指定名称,代码变得更加自解释和可读。
适用于多种数据结构:结构化绑定不仅适用于 STL 容器,还适用于用户定义的类型和数组。
更少的样板代码:它减少了编写样板代码的需要,特别是在处理多元组或类似结构的数据时。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用结构化绑定处理用户定义的类型
#include <iostream>
#include <string>

struct Person {
std::string name;
int age;
};

int main() {
Person person = {"Alice", 30};

// 使用结构化绑定
auto [name, age] = person;

std::cout << "姓名: " << name << std::endl;
std::cout << "年龄: " << age << std::endl;

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用结构化绑定处理std::tuple
#include <iostream>
#include <tuple>

int main() {
std::tuple<int, double, std::string> myTuple(42, 3.14, "Hello");

// 使用结构化绑定
auto [intValue, doubleValue, stringValue] = myTuple;

std::cout << "整数值: " << intValue << std::endl;
std::cout << "双精度值: " << doubleValue << std::endl;
std::cout << "字符串值: " << stringValue << std::endl;

return 0;
}

# 2、std::variant

#CPP 新特性 #CPP17
std::variant 是 C++17 引入的标准库类型,用于表示可以取多种类型中的一种的值。它是一种类似于联合(union)的类型,但相比联合更加类型安全,提供了更多的功能。

下面是一个简单的示例,演示了如何使用 std::variant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <variant>
#include <iostream>

int main() {
std::variant<int, double, std::string> myVariant;

myVariant = 42; // 存储整数
std::cout << std::get<int>(myVariant) << std::endl;

myVariant = 3.14; // 存储浮点数
std::cout << std::get<double>(myVariant) << std::endl;

myVariant = "Hello, Variant!"; // 存储字符串
std::cout << std::get<std::string>(myVariant) << std::endl;

return 0;
}

这个例子中, std::variant 可以存储 intdoublestd::string 中的一种类型的值。在运行时,可以根据当前存储的类型使用 std::get 进行访问。

std::variant 具有以下特点:

  1. 多类型支持: std::variant 可以存储多个不同类型的值,但在任意时刻只能包含其中一种类型的值。

  2. 类型安全: 使用 std::variant 可以避免使用联合时可能引起的类型错误。在编译时,编译器会检查对 std::variant 的访问,确保访问的类型是正确的。

  3. 替代联合和指针: 在过去,人们可能使用联合或指针来实现一种变体的效果。但是,这些方法可能不够类型安全,容易引入错误。 std::variant 提供了更好的类型安全性,同时也避免了使用指针带来的一些问题。

  4. 异常安全: std::variant 提供了异常安全的访问方法,避免了在异常情况下的不确定行为。

  5. 访问器函数: std::getstd::get_if 函数用于访问 std::variant 中的值。 std::get 在访问时要求知道值的确切类型,而 std::get_if 则返回指向值的指针,允许进行类型检查。

  6. 访问者模式: std::visit 允许通过访问者模式访问 std::variant 中的值,这是一种在多种类型之间进行分发操作的方法,这样的设计使得可以轻松地在多种类型之间进行分发操作。

对于 variant,可以有这么一些应用场景,比如函数的返回值可能由于条件的不同而返回不同类型,比如求根公式的结果可能有一个根、两个根或者没有根,那么就可以用:

1
using Roots = std::variant<double, std::pair<double, double>, nullptr_t>;

另外一个场景是多态,这里不展开。

# 3、std::string_view

#CPP 新特性 #CPP17
std::string_view 是 C++17 标准引入的一种轻量级字符串视图(String View)类型。它允许你在不拷贝字符串的情况下引用现有的字符串数据。这对于传递字符串参数、字符串操作和提高性能都是有益的。

1
#include <string_view>

std::string_view 本身不拥有字符串的内存,它只是一个对现有字符串数据的引用。因此,在使用期间需要确保原始字符串的生命周期长于字符串视图的生命周期。

std::string_view 提供了一种轻便且高效的方式来处理字符串数据,特别是在函数参数传递和字符串操作时。当你需要引用而非拷贝字符串数据时,可以考虑使用它。

优势:

  1. 参数传递的轻量化
    • 当你需要传递字符串参数但不需要拷贝整个字符串时, std::string_view 是一种很好的选择。这可以避免不必要的内存拷贝开销,对于提高性能和减少内存开销是有益的。
  2. 字符串切片和子串操作
    • std::string_view 提供了方便的子串操作,允许你在不创建新字符串的情况下访问原始字符串的一部分。
      1
      2
      std::string fullString = "C++ String View";
      std::string_view substr = std::string_view(fullString).substr(3, 6);
  3. 性能提升
    • 使用 std::string_view 可以避免不必要的内存分配和拷贝,从而提高程序的性能。特别是在处理大量字符串数据或频繁进行字符串操作时,这种性能提升可能非常显著。

# 4、if 初始化语句

#CPP 新特性 #CPP17
允许在 if 语句中初始化变量。

1
2
3
4
5
if (auto result = someFunction(); result > 0) {
// 处理正结果
} else {
// 处理负结果或零
}

# 5、属性说明符

#CPP 新特性 #CPP17

  1. [[nodiscard]] :这是一个属性,用于告诉编译器,函数的返回值不应被忽略。如果带有 [[nodiscard]] 属性的函数的返回值没有被使用,编译器可能会发出警告。这对于防止程序员忽略有返回值的函数的结果很有用。

  2. [[maybe_unused]] :这是一个属性,用于告诉编译器,标记的实体(例如变量、函数参数、函数等)可能未被使用,但是不应触发警告。这对于编写一些代码,其中某些实体可能在特定情况下未被使用很有用,但不希望因此触发警告。

  3. [[fallthrough]] :这是一个标签,用于在 C++17 中实现 fallthrough 语句。在 switch 语句中,如果你想要有一个意图的 “穿透” 效果,即使不写 break 语句,编译器也不会生成警告。 [[fallthrough]] 标签可以在某个 case 中明确表示穿透到下一个 case ,从而避免编译器生成不必要的警告。

对于 nodiscard,除了可以修饰函数之外,还可以用于修饰类:

在 C++17 及其后的标准中, [[nodiscard]] 属性(attribute)被引入,用于指示特定函数或返回类型的结果不应被忽略。当应用于类时,它会要求所有返回该类类型的对象必须被使用。如果忽略了这样的返回值,编译器会生成一个警告。这对于提示程序员注意某些操作的结果是非常有用的,避免忽略可能的重要结果。

# 目的和优点
  1. 防止资源泄漏:对于表示资源管理的类,如线程池、文件句柄或智能指针,忽略返回值可能导致资源未正确管理。 [[nodiscard]] 可以提醒程序员必须处理这些对象。
  2. 强制检查操作结果:有些操作可能失败,返回一个错误对象或状态对象。如果忽略了这些返回值,程序可能无法正确处理错误情况。
  3. 提高代码安全性:通过强制检查返回值,可以减少因忽略重要操作结果而引入的潜在错误。
# 具体应用场景

使用 [[nodiscard]] 属性的典型场景包括但不限于:

  1. 资源管理类:如线程池、文件操作类、网络连接类等。
  2. 错误处理类:如表示错误状态或异常信息的类。
  3. 智能指针:如自定义的智能指针实现。
# 总结

在类定义中使用 [[nodiscard]] 属性可以帮助程序员避免忽略重要的返回值,从而提高代码的健壮性和安全性。它是一种编译时提示机制,帮助捕捉那些因忽略返回值而可能导致的错误或未预期的行为。在使用 [[nodiscard]] 时,需要确保返回的对象确实需要被使用,并且使用返回值是合理且必要的。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++17 新特性 - C++ - 编程 | R1ck's Playground = how

# 1、结构化绑定

#CPP 新特性 #CPP17
C++17 引入了一项非常强大的特性,称为结构化绑定(Structured Bindings),它允许你将复杂的数据结构(如 std::tuple 、数组或用户定义的类型)的成员绑定到命名变量,从而使代码更加清晰和易于理解。结构化绑定的主要目的是让代码更加直观,减少手动提取和命名数据的需要。

以下是结构化绑定的主要特点和用法:
自动解包:结构化绑定允许您将数据结构中的元素自动解包到单独的变量中,而无需手动提取元素。
更具可读性:通过为数据结构的各个成员指定名称,代码变得更加自解释和可读。
适用于多种数据结构:结构化绑定不仅适用于 STL 容器,还适用于用户定义的类型和数组。
更少的样板代码:它减少了编写样板代码的需要,特别是在处理多元组或类似结构的数据时。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用结构化绑定处理用户定义的类型
#include <iostream>
#include <string>

struct Person {
std::string name;
int age;
};

int main() {
Person person = {"Alice", 30};

// 使用结构化绑定
auto [name, age] = person;

std::cout << "姓名: " << name << std::endl;
std::cout << "年龄: " << age << std::endl;

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用结构化绑定处理std::tuple
#include <iostream>
#include <tuple>

int main() {
std::tuple<int, double, std::string> myTuple(42, 3.14, "Hello");

// 使用结构化绑定
auto [intValue, doubleValue, stringValue] = myTuple;

std::cout << "整数值: " << intValue << std::endl;
std::cout << "双精度值: " << doubleValue << std::endl;
std::cout << "字符串值: " << stringValue << std::endl;

return 0;
}

# 2、std::variant

#CPP 新特性 #CPP17
std::variant 是 C++17 引入的标准库类型,用于表示可以取多种类型中的一种的值。它是一种类似于联合(union)的类型,但相比联合更加类型安全,提供了更多的功能。

下面是一个简单的示例,演示了如何使用 std::variant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <variant>
#include <iostream>

int main() {
std::variant<int, double, std::string> myVariant;

myVariant = 42; // 存储整数
std::cout << std::get<int>(myVariant) << std::endl;

myVariant = 3.14; // 存储浮点数
std::cout << std::get<double>(myVariant) << std::endl;

myVariant = "Hello, Variant!"; // 存储字符串
std::cout << std::get<std::string>(myVariant) << std::endl;

return 0;
}

这个例子中, std::variant 可以存储 intdoublestd::string 中的一种类型的值。在运行时,可以根据当前存储的类型使用 std::get 进行访问。

std::variant 具有以下特点:

  1. 多类型支持: std::variant 可以存储多个不同类型的值,但在任意时刻只能包含其中一种类型的值。

  2. 类型安全: 使用 std::variant 可以避免使用联合时可能引起的类型错误。在编译时,编译器会检查对 std::variant 的访问,确保访问的类型是正确的。

  3. 替代联合和指针: 在过去,人们可能使用联合或指针来实现一种变体的效果。但是,这些方法可能不够类型安全,容易引入错误。 std::variant 提供了更好的类型安全性,同时也避免了使用指针带来的一些问题。

  4. 异常安全: std::variant 提供了异常安全的访问方法,避免了在异常情况下的不确定行为。

  5. 访问器函数: std::getstd::get_if 函数用于访问 std::variant 中的值。 std::get 在访问时要求知道值的确切类型,而 std::get_if 则返回指向值的指针,允许进行类型检查。

  6. 访问者模式: std::visit 允许通过访问者模式访问 std::variant 中的值,这是一种在多种类型之间进行分发操作的方法,这样的设计使得可以轻松地在多种类型之间进行分发操作。

对于 variant,可以有这么一些应用场景,比如函数的返回值可能由于条件的不同而返回不同类型,比如求根公式的结果可能有一个根、两个根或者没有根,那么就可以用:

1
using Roots = std::variant<double, std::pair<double, double>, nullptr_t>;

另外一个场景是多态,这里不展开。

# 3、std::string_view

#CPP 新特性 #CPP17
std::string_view 是 C++17 标准引入的一种轻量级字符串视图(String View)类型。它允许你在不拷贝字符串的情况下引用现有的字符串数据。这对于传递字符串参数、字符串操作和提高性能都是有益的。

1
#include <string_view>

std::string_view 本身不拥有字符串的内存,它只是一个对现有字符串数据的引用。因此,在使用期间需要确保原始字符串的生命周期长于字符串视图的生命周期。

std::string_view 提供了一种轻便且高效的方式来处理字符串数据,特别是在函数参数传递和字符串操作时。当你需要引用而非拷贝字符串数据时,可以考虑使用它。

优势:

  1. 参数传递的轻量化
    • 当你需要传递字符串参数但不需要拷贝整个字符串时, std::string_view 是一种很好的选择。这可以避免不必要的内存拷贝开销,对于提高性能和减少内存开销是有益的。
  2. 字符串切片和子串操作
    • std::string_view 提供了方便的子串操作,允许你在不创建新字符串的情况下访问原始字符串的一部分。
      1
      2
      std::string fullString = "C++ String View";
      std::string_view substr = std::string_view(fullString).substr(3, 6);
  3. 性能提升
    • 使用 std::string_view 可以避免不必要的内存分配和拷贝,从而提高程序的性能。特别是在处理大量字符串数据或频繁进行字符串操作时,这种性能提升可能非常显著。

# 4、if 初始化语句

#CPP 新特性 #CPP17
允许在 if 语句中初始化变量。

1
2
3
4
5
if (auto result = someFunction(); result > 0) {
// 处理正结果
} else {
// 处理负结果或零
}

# 5、属性说明符

#CPP 新特性 #CPP17

  1. [[nodiscard]] :这是一个属性,用于告诉编译器,函数的返回值不应被忽略。如果带有 [[nodiscard]] 属性的函数的返回值没有被使用,编译器可能会发出警告。这对于防止程序员忽略有返回值的函数的结果很有用。

  2. [[maybe_unused]] :这是一个属性,用于告诉编译器,标记的实体(例如变量、函数参数、函数等)可能未被使用,但是不应触发警告。这对于编写一些代码,其中某些实体可能在特定情况下未被使用很有用,但不希望因此触发警告。

  3. [[fallthrough]] :这是一个标签,用于在 C++17 中实现 fallthrough 语句。在 switch 语句中,如果你想要有一个意图的 “穿透” 效果,即使不写 break 语句,编译器也不会生成警告。 [[fallthrough]] 标签可以在某个 case 中明确表示穿透到下一个 case ,从而避免编译器生成不必要的警告。

对于 nodiscard,除了可以修饰函数之外,还可以用于修饰类:

在 C++17 及其后的标准中, [[nodiscard]] 属性(attribute)被引入,用于指示特定函数或返回类型的结果不应被忽略。当应用于类时,它会要求所有返回该类类型的对象必须被使用。如果忽略了这样的返回值,编译器会生成一个警告。这对于提示程序员注意某些操作的结果是非常有用的,避免忽略可能的重要结果。

# 目的和优点
  1. 防止资源泄漏:对于表示资源管理的类,如线程池、文件句柄或智能指针,忽略返回值可能导致资源未正确管理。 [[nodiscard]] 可以提醒程序员必须处理这些对象。
  2. 强制检查操作结果:有些操作可能失败,返回一个错误对象或状态对象。如果忽略了这些返回值,程序可能无法正确处理错误情况。
  3. 提高代码安全性:通过强制检查返回值,可以减少因忽略重要操作结果而引入的潜在错误。
# 具体应用场景

使用 [[nodiscard]] 属性的典型场景包括但不限于:

  1. 资源管理类:如线程池、文件操作类、网络连接类等。
  2. 错误处理类:如表示错误状态或异常信息的类。
  3. 智能指针:如自定义的智能指针实现。
# 总结

在类定义中使用 [[nodiscard]] 属性可以帮助程序员避免忽略重要的返回值,从而提高代码的健壮性和安全性。它是一种编译时提示机制,帮助捕捉那些因忽略返回值而可能导致的错误或未预期的行为。在使用 [[nodiscard]] 时,需要确保返回的对象确实需要被使用,并且使用返回值是合理且必要的。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/11/C-\346\231\272\350\203\275\346\214\207\351\222\210/index.html" "b/2024/10/11/C-\346\231\272\350\203\275\346\214\207\351\222\210/index.html" index b48b319..49f80de 100644 --- "a/2024/10/11/C-\346\231\272\350\203\275\346\214\207\351\222\210/index.html" +++ "b/2024/10/11/C-\346\231\272\350\203\275\346\214\207\351\222\210/index.html" @@ -1 +1 @@ -C++ 智能指针 - C++ - 编程 | R1ck's Playground = how

智能指针是 C++ 中用于管理动态分配的对象生命周期的一种特殊指针。它们提供了自动内存管理和资源释放的机制,避免了手动调用 delete 来释放内存的麻烦和潜在的内存泄漏。

C++ 标准库提供了两种常用的智能指针: std::shared_ptrstd::unique_ptr
(其实还有 std::weak_ptr ,用于破解循环引用的问题)

  1. std::shared_ptr :它是一种共享所有权的智能指针。多个 std::shared_ptr 对象可以同时拥有同一个对象的所有权。它使用引用计数的方式来跟踪对象的引用次数。当最后一个 std::shared_ptr 对象超出作用域或被显式地设置为 nullptr ,引用计数变为零时,对象会自动销毁。这样可以确保对象在没有引用时被正确地释放,避免了内存泄漏。
  2. std::unique_ptr :它是一种独占所有权的智能指针。一个 std::unique_ptr 对象拥有唯一的所有权,不能被其他智能指针共享。它使用了移动语义,因此具有很高的效率。当 std::unique_ptr 对象超出作用域时,或者通过 std::move 转移所有权时,对象会自动销毁。这样可以避免资源的重复释放和悬空指针的问题。

智能指针的好处在于它们提供了自动资源管理,减少了手动管理内存的错误和复杂性。此外,它们还能帮助解决资源泄漏、悬空指针和内存安全等问题。

以下是使用智能指针的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <memory>

int main() {
// 使用 std::shared_ptr
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::shared_ptr<int> anotherSharedPtr = sharedPtr;

// 使用 std::unique_ptr
std::unique_ptr<int> uniquePtr = std::make_unique<int>(42);

return 0;
}

在这个示例中, std::shared_ptrstd::unique_ptr 能够自动管理动态分配的整数对象的生命周期。无需手动释放内存,当 std::shared_ptr 对象超出作用域时,引用计数减少;当 std::unique_ptr 对象超出作用域时,对象自动销毁。

# 底层实现

当我们创建智能指针 shared_ptr 指向一个对象的时候,其构造函数会分配一个空间创建一个管理对象 manager object ,这个管理对象是动态分配的,其包含了一个裸指针,指向我们要管理的对象,还包含了 shared_ptr 的引用计数和 weak_ptr 的引用计数
一开始,当创建一个 shared_ptr 指向一个新创建的对象,显然,sp 的引用计数为 1,wp 的引用计数为 0,然后裸指针指向对象。
正如我们所经常听说的那样,当 sp 引用计数为 0 时,对象会销毁。这里多了一个不为大家所熟知的小细节,就是此时这个管理对象还不会释放,只有等 wp 也减为 0 的时候,才会释放这个管理对象

# 关于智能指针的注意事项

在使用智能指针时,有一些注意事项需要注意:

  1. 避免循环引用:如果使用 std::shared_ptr ,应避免形成循环引用,即两个或多个对象之间相互持有 std::shared_ptr ,导致引用计数无法降为零,从而导致内存泄漏。可以通过使用 std::weak_ptr 打破循环引用来解决这个问题。
  2. 不要使用裸指针和智能指针混合使用:尽量避免在代码中同时使用裸指针和智能指针,因为这可能会导致资源管理的混乱。如果需要使用裸指针,可以使用 std::unique_ptr::get() 方法获取裸指针。
  3. 使用合适的智能指针类型:根据情况选择合适的智能指针类型,例如使用 std::shared_ptr 进行共享所有权的管理,或者使用 std::unique_ptr 进行独占所有权的管理。选择适当的智能指针类型可以提高代码的可读性和安全性。
  4. 只能用智能指针指向用 new 分配的、能够用 delete 释放的对象上
  5. 尽可能地用 make_shared 去代替 new(但它也不是百利而无一害,见下文:[[C++ 智能指针 #make_shared|make_shared]])

# 智能指针作为函数的入口参数和返回参数

智能指针可以作为函数的参数和返回值类型,从而方便地管理动态分配的资源。

以下是一个示例:

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
#include <memory>
#include <iostream>

// 函数参数为智能指针类型
void processObject(std::shared_ptr<int> ptr) {
// 使用智能指针操作对象
std::cout << "Object value: " << *ptr << std::endl;
}

// 函数返回智能指针类型
std::unique_ptr<int> createObject() {
// 创建动态分配的对象,并返回智能指针
return std::make_unique<int>(42);
}

int main() {
// 使用智能指针作为函数参数
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
processObject(sharedPtr);

// 使用智能指针作为函数返回值
std::unique_ptr<int> uniquePtr = createObject();
std::cout << "Object value: " << *uniquePtr << std::endl;

return 0;
}

在这个示例中, processObject 函数接受一个 std::shared_ptr<int> 参数,用于处理动态分配的整数对象。 createObject 函数创建一个动态分配的整数对象,并返回一个 std::unique_ptr<int>

最后,再回答 make_shared 的作用和对象存放位置的问题。 std::make_shared 是一个模板函数,用于创建智能指针对象,它的作用是在一次内存分配中同时创建对象和管理对象的引用计数。由于对象和引用计数信息存储在一块连续的内存中,因此可以提高性能和内存利用率。

至于对象存放的位置, std::make_shared 创建的对象和引用计数信息通常存储在一块连续的内存块中,这个内存块位于堆上。这样的设计可以减少内存碎片化,并提高内存访问的效率。

# weak_ptr

1、初始化:
wp 的初始化默认是空的,不指向任何东西 —— 甚至不指向一个 manager object。
所以你只能通过从 shared_ptr 或现有的 weak_ptr 进行拷贝构造或复制构造然后将 weak_ptr 指向它:

1
2
3
4
5
6
7
shared_ptr<Thing> sp(new Thing);
weak_ptr<Thing> wp1(sp); // construct wp1 from a shared_ptr
weak_ptr<Thing> wp2; // an empty weak_ptr - points to nothing
wp2 = sp; // wp2 now points to the new Thing
weak_ptr<Thing> wp3(wp2); // construct wp3 from a weak_ptr
weak_ptr<Thing> wp4
wp4 = wp2; // wp4 now points to the new Thing.

2、引用
你不能直接用 wp 去引用对象,而是通过一个 sp 来接住 wp.lock() 返回的内容。lock 函数会判断当前 wp 指向的对象是否还存在 / 有效,如果存在,该函数会返回一个该对象的 sp,如果不存在,则返回一个空的 sp:

1
shared_ptr<Thing> sp2 = wp2.lock(); // get shared_ptr from weak_ptr

通常,我们有 3 种方式去引用一个 wp:

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
//第一种方法,通过 sp 和 lock,然后判断 sp 是否为空
void do_it(weak_ptr<Thing> wp){
shared_ptr<Thing> sp = wp.lock(); // get shared_ptr from weak_ptr
if(sp) {
sp->defrangulate(); // tell the Thing to do something
} else {
cout << "The Thing is gone!" << endl;
}
}

//第二种方法,利用 wp 的 expired 函数
bool is_it_there(weak_ptr<Thing> wp) {
if(wp.expired()) {
cout << "The Thing is gone!" << endl;
return false;
}
return true;
}

//第三种方式,利用 wp 构造一个 sp,然后捕获异常
void do_it(weak_ptr<Thing> wp){
shared_ptr<Thing> sp(wp); // construct shared_ptr from weak_ptr
// exception thrown if wp is expired, so if here, sp is good to go
sp->defrangulate(); // tell the Thing to do something
}
...

try {
do_it(wpx);
} catch(bad_weak_ptr&) {
cout << "A Thing (or something else) has disappeared!" << endl;
}

3、特殊使用场景
wp 除了下文提到的解决循环引用的问题之外,还有一个特殊的使用场景就是获取对象的 this 指针。
当一个对象要通过 shared_ptr 的形式获取一个 this 指针以便实现自动的内存管理时,如果我们仅仅通过拷贝 this 指针来构造 sp 的话,由于 this 指针是个裸指针,于是又会生成一个新的管理对象来指向被管理的对象,于是现在有两个 manager object 了。那么最终就很可能造成内存重复释放的错误:

1
2
3
4
5
6
7
void Thing::foo()
{
// we need to transmogrify this object
shared_ptr<Thing> sp_for_this(this); // danger! a second manager object!
transmogrify(sp_for_this);
}
//假设 Thing 对象本身就是用 sp 创建的,那么当其调用 foo 函数时,又将创建一个管理对象

为了解决这个问题,需要借助 std::enabled_shared_from_this 来实现,是一个类模板,将 Thing 继承自它,然后再需要用到 this 指针的地方通过调用 shared_from_this() 这个函数来实现。

enabled_shared_from_this 的底层逻辑大概就是在定义类时,类还有一个 wp 的成员变量指向负责管理该类对象的 manager object,然后每次我们调用 shared_from_this 的时候就是拿到 wp 所指向的这个 manager object 所指向的对象 —— 不会多生成一个管理对象。

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
class Thing : public enable_shared_from_this<Thing> {
public:
void foo();
void defrangulate();
};

void Thing::foo() {
// we need to transmogrify this object
// get a shared_ptr from the weak_ptr in this object
shared_ptr<Thing> sp_this = shared_from_this();
transmogrify(sp_this);
}

void transmogrify(shared_ptr<Thing> ptr) {
ptr->defrangulate();
/* etc. */
}

int main() {
// The following starts a manager object for the Thing and also
// initializes the weak_ptr member that is now part of the Thing.
shared_ptr<Thing> t1(new Thing);
t1->foo();
...
}

# make_shared

当我们创建一个 sp 指向一个新 new 出来的对象时,一种语法是:

1
shared_ptr<Thing> p(new Thing);

在这种情况下,会触发两次内存分配,一次是调用 Thing 的构造函数,分配其内存,还有一次是调用 sp 的构造函数,分配其内存给一个管理对象。而两次内存分配会导致性能 / 效率的降低。

所以,为了优化这个问题,有了 make_shared 这个函数,用来一次分配足够的内存:

1
shared_ptr<Thing> p(make_shared<Thing>()); // only one allocation!

在这种情况下,只会发生一次内存分配,效率提高。

这里有个小问题,为什么 make_shared 也需要传入一个模板参数 Thing 呢?是为了灵活性考虑,这样的话 make_shared 生成的对象就可以是其他类型然后 cast 造型给 sp 使用。比如在继承关系中用于父类、子类的指针指向:

1
shared_ptr<Base> bp(make_shared<Derived1>());

最后,如果 Thing 的构造函数需要传递参数,在 make_shared 后面接着加上即可:

1
shared_ptr<Thing> p (make_shared<Thing>(42, "I'm a Thing!"));

这里提一嘴,虽然说我们用 make_shared 可以优化效率,但是它也不是就完全没有缺点。根据前面 “底层实现” 提到的内容,如果我们用 make_shared 生成的内容,sp 已经减为 0 了,但是 wp 还大于 0,那么整块内存都不会被释放。

# 循环引用

循环引用是指两个或多个对象相互持有 std::shared_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
#include <memory>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> bPtr;

~A() {
std::cout << "A destructor called" << std::endl;
}
};

class B {
public:
std::shared_ptr<A> aPtr;

~B() {
std::cout << "B destructor called" << std::endl;
}
};

int main() {
std::shared_ptr<A> aPtr = std::make_shared<A>();
std::shared_ptr<B> bPtr = std::make_shared<B>();

// 建立循环引用
aPtr->bPtr = bPtr;
bPtr->aPtr = aPtr;

return 0;
}

在上述示例中,我们定义了两个类 AB ,它们分别包含一个 std::shared_ptr 成员变量用于相互引用。在 main 函数中,我们创建了两个对象 aPtrbPtr ,然后通过相互赋值建立了循环引用。

当程序结束时,这两个对象的析构函数并不会被调用,因为它们之间的循环引用导致引用计数无法降为零。这就造成了内存泄漏,因为这些对象所占用的内存无法被释放。

为了解决循环引用问题,C++ 提供了 std::weak_ptrstd::weak_ptr 是一种弱引用,它可以引用 std::shared_ptr 指向的对象,但并不会增加对象的引用计数。通过将其中一个指针使用 std::weak_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
//于是,只需要把上面的其中一个指针声明为weak_ptr即可
#include <iostream>
#include <memory>

class B;  //Forward declaration
class A {
public:
    std::shared_ptr<B> bPtr;
    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

class B {
public:
    // std::shared_ptr<A> aPtr;
    std::weak_ptr<A> aPtr;  //use weak ptr to break the loop

    ~B() {
        std::cout << "B destructor called" << std::endl;
    }
};

int main() {
    while(1) {
        std::shared_ptr<A> aPtr = std::make_shared<A>();
        std::shared_ptr<B> bPtr = std::make_shared<B>();
        // make it loop
        aPtr->bPtr = bPtr;
        bPtr->aPtr = aPtr;
        std::cout << "in while loop..." << std::endl;
    } return 0;
}

std::weak_ptr 并不能直接打破循环引用,而是通过辅助的机制来解决循环引用的问题。它并不增加引用计数,因此即使存在循环引用,对象的引用计数也会降为零,从而触发析构函数的调用。

在前面的示例中,当 aPtrbPtr 的引用计数降为零时,它们的析构函数会被调用,从而释放它们所占用的内存。由于 bPtr 使用的是 std::weak_ptr ,它并不增加 aPtr 的引用计数,因此循环引用并不会阻止 aPtr 的析构。这样就避免了 std::shared_ptr 循环引用导致的内存泄漏问题。

因此,可以说 std::weak_ptr 并非直接打破循环引用,而是通过协助 std::shared_ptr 来实现循环引用的解决方案。它在不增加引用计数的前提下,提供了一种访问被 std::shared_ptr 管理的对象的方式,并且不会阻止对象的销毁。

# unique_ptr

它和 sp 其实差不多了,区别就是 ownership 的独占,which 通过禁用拷贝构造和赋值构造来实现,所以你不能拷贝或赋值一个 up 给另一个 up:

1
2
3
4
unique_ptr<Thing> p1 (new Thing); // p1 owns the Thing
unique_ptr<Thing> p2(p1); // error - copy construction is not allowed.
unique_ptr<Thing> p3; // an empty unique_ptr;
p3 = p1; // error, copy assignment is not allowed

由于禁用拷贝构造,所以如果要把 up 作为函数参数进行传参时,只能 pass by reference,不能 pass by value。

1、所有权转移
而由于 up 对于对象的所有权是独占的,所以有时需要转移所有权,这依赖于移动语义,使用移动构造和移动赋值函数。

其中,我们知道,函数的返回值是右值,所以 up 的这种移动语义可以用于我们在函数中返回一个局部的 up 给外层调用该函数的 up,实现移动:

1
2
3
4
5
6
7
8
9
10
11
12
13
//create a Thing and return a unique_ptr to it:
unique_ptr<Thing> create_Thing() {
unique_ptr<Thing> local_ptr(new Thing);
return local_ptr; // local_ptr will surrender ownership
}

void foo() {
unique_ptr<Thing> p1(create_Thing()); // move ctor from returned rvalue
// p1 now owns the Thing
unique_ptr<Thing> p2; // default ctor'd; owns nothing
p2 = create_Thing(); // move assignment from returned rvalue
// p2 now owns the second Thing
}

2、所有权的显式转移
通过 std::move() 函数来显式地进行所有权的转移:

1
2
3
4
5
6
unique_ptr<Thing> p1(new Thing); // p1 owns the Thing
unique_ptr<Thing> p2; // p2 owns nothing
// invoke move assignment explicitly
p2 = std::move(p1); // now p2 owns it, p1 owns nothing
// invoke move construction explicitly
unique_ptr<Thing> p3(std::move(p2)); // now p3 owns it, p2 and p1 own nothing

另外,前面在讲 sp 的时候讲到了有个函数 make_shared ,而 C++11 居然没有相对应的 make_unique ,直到 C++14 的时候才引入了 make_unique 函数。不过, make_unique 不会有性能上的提升,因为 up 并不会分配管理对象,所以没有额外的内存分配,不会有两次内存分配,就不会有性能低的问题。—— 可能这就是为什么 C++11 引入了 make_shared 却没有同时引入 make_unique 的原因吧。

# shared_ptr

Shared pointer,a class template:

1
2
#include <memory>
template <class T> class shared_ptr;

简单来说,share pointer 具备引用计数、自动释放的特点。多个  shared_ptr  指向同一块内存时,每多一个指针指向该内存,引用数相应自增,反之亦然。当一块内存的  shared_ptr  引用数减少为 0 时,自动释放该内存。

即,我们可以通过智能指针来申请动态内存,其会有一个相应的引用计数(申请时初始值为 1,表明当前这个  shared_ptr  指向该内存)。每当有新的智能指针指向该内存时,引用计数自增。

https://cplusplus.com/reference/memory/shared_ptr/

智能指针的创建:

空智能指针:

1
2
3
std::shared_ptr<int> p1;	//指向 int 类型的智能指针,此时不传入任何实参,为空指针
std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
//空指针的引用计数为 0,此时并未指向任何内存,合情合理

非空智能指针:

1
2
3
4
5
std::shared_ptr<int> p3(new int(10));	//指向一个值为 3 的 int 型的内存
//此时引用计数为 1

//另外,也可以通过 C++11 提供的模板函数来初始化智能指针
std::shared_ptr<int> p4 = std::make_share<int>(10);

构造函数(拷贝构造和移动构造):

1
2
3
4
5
6
7
//拷贝构造函数
std::shared_ptr<int> p5(p4);
std::shared_ptr<int> p6 = p5;

//移动构造
std::shared_ptr<int> p7(std::move(p6));
std::shared_ptr<int> p8 = std::move(p7);

成员方法:

  • swap()
    交换两个相同类型  shared_ptr  的内容

  • reset()
    当该函数没有传入实参时,作用是将当前  shared_ptr  所指内存的引用计数减 1,同时将当前指针对象重置为一个空指针;

当该函数传入一个实参(一个新申请的堆空间)时,则当前指针对象会指向该新申请的堆空间且引用计数初始值为 1,同时,原指向的堆空间(如果有)的引用计数自然而然减 1,如果减 1 后为 0,则释放该内存

  • get()
    获得  shared_ptr  对象内部包含的普通指针

  • use_count()
    返回当前智能指针所指向内存的引用计数值,即所有指向该内存的智能指针的数量

  • unique()
    判断当前智能指针指向的堆内存,是否不再有其他  shared_ptr  指向它

参考链接:
https://cplusplus.com/reference/memory/shared_ptr/reset/
http://c.biancheng.net/view/7898.html
http://c.biancheng.net/view/430.html
https://blog.csdn.net/ff_gogo/article/details/123512482
https://www.cnblogs.com/dream397/p/14620324.html

实际应用:

1
2
3
4
5
6
7
8
9
10
m_dds_ctx.reset(new minieye::DDS::Context(FLAGS_config_json_radar, true));
...
if (radar_topic != nullptr)
{
std::shared_ptr<minieye::DDS::Reader> r(new minieye::DDS::Reader(
m_dds_ctx.get(), radar_topic->topic, OnRecvDDS, this));

m_readers = r;
}
...

# deleter 删除器

对于自定义的类,我们知道存在构造函数和析构函数,析构函数能够在对象销毁时被自动调用,结合智能指针则能够实现自动管理,让其在引用计数减为 0 时自动调用析构函数释放资源。

但是,对于内置的类型,比如 int、float 这种,并不存在任何析构函数,所以无论我们是否使用智能指针,我们都不能很好地调用所谓的 “析构函数” 来释放资源。比如:

1
std::shared_ptr<int> num{new int(5)};

针对这种情况,我们需要用到 deleter。

在智能指针中,可以指定一个删除器,用于在引用计数减为 0 时调用该删除器,进行资源的释放。以 shared_ptr 举例,它的其中两种构造函数如下:

1
2
3
template <class U, class D> shared_ptr(U* p, D del);
template <class D> shared_ptr(nullptr_t p, D del);
#其中 del 是一个删除器

deleter 可以是以下类型之一:

  1. 函数指针:指向一个函数,该函数会在智能指针的资源需要释放时被调用。
  2. 函数对象(Functor):是一个类对象,重载了函数调用运算符 operator() ,可以像函数一样调用。
  3. Lambda 表达式:一种匿名函数,可以用作智能指针的 deleter

于是就有了这种智能指针的构造方式:

1
2
3
4
5
// 使用 Lambda 表达式作为 deleter 的 std::shared_ptr
std::shared_ptr<MyClass> sharedPtr(new MyClass(24), [](MyClass* ptr) {
std::cout << "Lambda deleter is called" << std::endl;
delete ptr;
});

整体示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>
using namespace std;

//Custom deleter function declaration
void my_deleter(int* ptr){
cout << "Deleting integer pointer with value: " << *ptr << endl;
delete ptr;
cout << "Delete Successful.";
}


int main()
{
//int type smart pointer object
shared_ptr<int> ptr {new int{5}, my_deleter};
return 0;
}

大概是这样。

这里需要注意的是,我们的 shared_ptr 的构造函数存在几种形式:

1
2
3
4
5
template< class Y >
explicit shared_ptr( Y* ptr );

template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d );

对于前者,当引用计数值为 0 时会使用 delete ptrdelete[] ptr ,具体取决于 T 是否为数组类型。而对于后者,则是通过传入一个用户自定义的删除器,在引用计数值为 0 自动调用。详细信息见:shared_ptr

但是由此又似乎会得出结论,哪怕我们没有指定删除器,利用原本的 delete 语法也能处理基本类型嘛?再者,当使用智能指针管理基础类型时,真的不能有效释放内存吗?sus

# Q&A

智能指针如何判空?

我们知道,对于裸指针,我们可以直接在 if 条件语句中将其用作条件进行判空,如:

1
2
3
4
5
int* rp = new int(5);

if(rp) {
//...
}

同样的,对于智能指针,也能这么操作。

智能指针可以使用 operator bool()get() 方法来进行空指针判断。以下是两种常用的方法:

  1. 使用 operator bool()

std::shared_ptrstd::unique_ptr 都定义了 operator bool() ,可以用于将智能指针直接用于布尔上下文中进行判空。

1
2
3
4
5
6
7
8
9
std::shared_ptr<int> ptr = std::make_shared<int>(42);

if (ptr) {
// 指针不为空
// 执行代码...
} else {
// 指针为空
// 执行其他代码...
}

  1. 使用 get() 方法:

get() 方法返回底层裸指针,你可以将其与 nullptr 比较来进行判空。

1
2
3
4
5
6
7
8
9
std::shared_ptr<int> ptr = std::make_shared<int>(42);

if (ptr.get() != nullptr) {
// 指针不为空
// 执行代码...
} else {
// 指针为空
// 执行其他代码...
}

不过第二种方法有点没必要,好好的智能指针不用,还把它转化成裸指针,何苦呢

另外,这里的 operator bool() 是操作符重载,具体地:

operator bool() 是一个类型转换运算符,用于将类对象转换为布尔值。在 C++ 中,运算符重载允许程序员自定义类对象在特定上下文中的操作行为,而 operator bool() 是其中一种常见的运算符重载。

运算符重载的语法如下:

1
2
3
4
5
6
7
8
9
class MyClass {
public:
// 类型转换运算符的重载
explicit operator bool() const {
// 返回用于将对象转换为布尔值的逻辑表达式
// 返回 true 表示对象非空,返回 false 表示对象为空
return someCondition; // 替换为实际的逻辑表达式
}
};

在上述示例中, operator bool() 被定义为将 MyClass 类的对象转换为布尔值。注意其中的 explicit 关键字,它表示这个转换是显式的,防止隐式转换。

对于智能指针类(如 std::shared_ptrstd::unique_ptr ), operator bool() 的实现通常是检查底层指针是否为 nullptr 。这样,当智能指针指向有效对象时,转换结果为 true ,指向空对象时,转换结果为 false 。这种设计使得智能指针在条件语句中的使用更加自然,就像原始指针一样。

以下是智能指针 operator bool() 的典型实现:

1
2
3
4
5
6
7
8
9
10
class SmartPtr {
public:
// 类型转换运算符的重载
explicit operator bool() const {
return ptr != nullptr;
}

private:
int* ptr; // 以 int* 为例,实际上可能是其他类型
};

这使得你可以像下面这样使用智能指针:

1
2
3
4
5
6
SmartPtr ptr = ...;  // 通过某种方式初始化
if (ptr) {
// 智能指针不为空,执行代码...
} else {
// 智能指针为空,执行其他代码...
}

这种运算符重载的使用,使得在条件语句中判空更加直观,提高了代码的可读性。

# 相关阅读

stack overflow:如何判断我的 shared_ptr 被谁持有?

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 智能指针 - C++ - 编程 | R1ck's Playground = how

智能指针是 C++ 中用于管理动态分配的对象生命周期的一种特殊指针。它们提供了自动内存管理和资源释放的机制,避免了手动调用 delete 来释放内存的麻烦和潜在的内存泄漏。

C++ 标准库提供了两种常用的智能指针: std::shared_ptrstd::unique_ptr
(其实还有 std::weak_ptr ,用于破解循环引用的问题)

  1. std::shared_ptr :它是一种共享所有权的智能指针。多个 std::shared_ptr 对象可以同时拥有同一个对象的所有权。它使用引用计数的方式来跟踪对象的引用次数。当最后一个 std::shared_ptr 对象超出作用域或被显式地设置为 nullptr ,引用计数变为零时,对象会自动销毁。这样可以确保对象在没有引用时被正确地释放,避免了内存泄漏。
  2. std::unique_ptr :它是一种独占所有权的智能指针。一个 std::unique_ptr 对象拥有唯一的所有权,不能被其他智能指针共享。它使用了移动语义,因此具有很高的效率。当 std::unique_ptr 对象超出作用域时,或者通过 std::move 转移所有权时,对象会自动销毁。这样可以避免资源的重复释放和悬空指针的问题。

智能指针的好处在于它们提供了自动资源管理,减少了手动管理内存的错误和复杂性。此外,它们还能帮助解决资源泄漏、悬空指针和内存安全等问题。

以下是使用智能指针的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <memory>

int main() {
// 使用 std::shared_ptr
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::shared_ptr<int> anotherSharedPtr = sharedPtr;

// 使用 std::unique_ptr
std::unique_ptr<int> uniquePtr = std::make_unique<int>(42);

return 0;
}

在这个示例中, std::shared_ptrstd::unique_ptr 能够自动管理动态分配的整数对象的生命周期。无需手动释放内存,当 std::shared_ptr 对象超出作用域时,引用计数减少;当 std::unique_ptr 对象超出作用域时,对象自动销毁。

# 底层实现

当我们创建智能指针 shared_ptr 指向一个对象的时候,其构造函数会分配一个空间创建一个管理对象 manager object ,这个管理对象是动态分配的,其包含了一个裸指针,指向我们要管理的对象,还包含了 shared_ptr 的引用计数和 weak_ptr 的引用计数
一开始,当创建一个 shared_ptr 指向一个新创建的对象,显然,sp 的引用计数为 1,wp 的引用计数为 0,然后裸指针指向对象。
正如我们所经常听说的那样,当 sp 引用计数为 0 时,对象会销毁。这里多了一个不为大家所熟知的小细节,就是此时这个管理对象还不会释放,只有等 wp 也减为 0 的时候,才会释放这个管理对象

# 关于智能指针的注意事项

在使用智能指针时,有一些注意事项需要注意:

  1. 避免循环引用:如果使用 std::shared_ptr ,应避免形成循环引用,即两个或多个对象之间相互持有 std::shared_ptr ,导致引用计数无法降为零,从而导致内存泄漏。可以通过使用 std::weak_ptr 打破循环引用来解决这个问题。
  2. 不要使用裸指针和智能指针混合使用:尽量避免在代码中同时使用裸指针和智能指针,因为这可能会导致资源管理的混乱。如果需要使用裸指针,可以使用 std::unique_ptr::get() 方法获取裸指针。
  3. 使用合适的智能指针类型:根据情况选择合适的智能指针类型,例如使用 std::shared_ptr 进行共享所有权的管理,或者使用 std::unique_ptr 进行独占所有权的管理。选择适当的智能指针类型可以提高代码的可读性和安全性。
  4. 只能用智能指针指向用 new 分配的、能够用 delete 释放的对象上
  5. 尽可能地用 make_shared 去代替 new(但它也不是百利而无一害,见下文:[[C++ 智能指针 #make_shared|make_shared]])

# 智能指针作为函数的入口参数和返回参数

智能指针可以作为函数的参数和返回值类型,从而方便地管理动态分配的资源。

以下是一个示例:

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
#include <memory>
#include <iostream>

// 函数参数为智能指针类型
void processObject(std::shared_ptr<int> ptr) {
// 使用智能指针操作对象
std::cout << "Object value: " << *ptr << std::endl;
}

// 函数返回智能指针类型
std::unique_ptr<int> createObject() {
// 创建动态分配的对象,并返回智能指针
return std::make_unique<int>(42);
}

int main() {
// 使用智能指针作为函数参数
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
processObject(sharedPtr);

// 使用智能指针作为函数返回值
std::unique_ptr<int> uniquePtr = createObject();
std::cout << "Object value: " << *uniquePtr << std::endl;

return 0;
}

在这个示例中, processObject 函数接受一个 std::shared_ptr<int> 参数,用于处理动态分配的整数对象。 createObject 函数创建一个动态分配的整数对象,并返回一个 std::unique_ptr<int>

最后,再回答 make_shared 的作用和对象存放位置的问题。 std::make_shared 是一个模板函数,用于创建智能指针对象,它的作用是在一次内存分配中同时创建对象和管理对象的引用计数。由于对象和引用计数信息存储在一块连续的内存中,因此可以提高性能和内存利用率。

至于对象存放的位置, std::make_shared 创建的对象和引用计数信息通常存储在一块连续的内存块中,这个内存块位于堆上。这样的设计可以减少内存碎片化,并提高内存访问的效率。

# weak_ptr

1、初始化:
wp 的初始化默认是空的,不指向任何东西 —— 甚至不指向一个 manager object。
所以你只能通过从 shared_ptr 或现有的 weak_ptr 进行拷贝构造或复制构造然后将 weak_ptr 指向它:

1
2
3
4
5
6
7
shared_ptr<Thing> sp(new Thing);
weak_ptr<Thing> wp1(sp); // construct wp1 from a shared_ptr
weak_ptr<Thing> wp2; // an empty weak_ptr - points to nothing
wp2 = sp; // wp2 now points to the new Thing
weak_ptr<Thing> wp3(wp2); // construct wp3 from a weak_ptr
weak_ptr<Thing> wp4
wp4 = wp2; // wp4 now points to the new Thing.

2、引用
你不能直接用 wp 去引用对象,而是通过一个 sp 来接住 wp.lock() 返回的内容。lock 函数会判断当前 wp 指向的对象是否还存在 / 有效,如果存在,该函数会返回一个该对象的 sp,如果不存在,则返回一个空的 sp:

1
shared_ptr<Thing> sp2 = wp2.lock(); // get shared_ptr from weak_ptr

通常,我们有 3 种方式去引用一个 wp:

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
//第一种方法,通过 sp 和 lock,然后判断 sp 是否为空
void do_it(weak_ptr<Thing> wp){
shared_ptr<Thing> sp = wp.lock(); // get shared_ptr from weak_ptr
if(sp) {
sp->defrangulate(); // tell the Thing to do something
} else {
cout << "The Thing is gone!" << endl;
}
}

//第二种方法,利用 wp 的 expired 函数
bool is_it_there(weak_ptr<Thing> wp) {
if(wp.expired()) {
cout << "The Thing is gone!" << endl;
return false;
}
return true;
}

//第三种方式,利用 wp 构造一个 sp,然后捕获异常
void do_it(weak_ptr<Thing> wp){
shared_ptr<Thing> sp(wp); // construct shared_ptr from weak_ptr
// exception thrown if wp is expired, so if here, sp is good to go
sp->defrangulate(); // tell the Thing to do something
}
...

try {
do_it(wpx);
} catch(bad_weak_ptr&) {
cout << "A Thing (or something else) has disappeared!" << endl;
}

3、特殊使用场景
wp 除了下文提到的解决循环引用的问题之外,还有一个特殊的使用场景就是获取对象的 this 指针。
当一个对象要通过 shared_ptr 的形式获取一个 this 指针以便实现自动的内存管理时,如果我们仅仅通过拷贝 this 指针来构造 sp 的话,由于 this 指针是个裸指针,于是又会生成一个新的管理对象来指向被管理的对象,于是现在有两个 manager object 了。那么最终就很可能造成内存重复释放的错误:

1
2
3
4
5
6
7
void Thing::foo()
{
// we need to transmogrify this object
shared_ptr<Thing> sp_for_this(this); // danger! a second manager object!
transmogrify(sp_for_this);
}
//假设 Thing 对象本身就是用 sp 创建的,那么当其调用 foo 函数时,又将创建一个管理对象

为了解决这个问题,需要借助 std::enabled_shared_from_this 来实现,是一个类模板,将 Thing 继承自它,然后再需要用到 this 指针的地方通过调用 shared_from_this() 这个函数来实现。

enabled_shared_from_this 的底层逻辑大概就是在定义类时,类还有一个 wp 的成员变量指向负责管理该类对象的 manager object,然后每次我们调用 shared_from_this 的时候就是拿到 wp 所指向的这个 manager object 所指向的对象 —— 不会多生成一个管理对象。

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
class Thing : public enable_shared_from_this<Thing> {
public:
void foo();
void defrangulate();
};

void Thing::foo() {
// we need to transmogrify this object
// get a shared_ptr from the weak_ptr in this object
shared_ptr<Thing> sp_this = shared_from_this();
transmogrify(sp_this);
}

void transmogrify(shared_ptr<Thing> ptr) {
ptr->defrangulate();
/* etc. */
}

int main() {
// The following starts a manager object for the Thing and also
// initializes the weak_ptr member that is now part of the Thing.
shared_ptr<Thing> t1(new Thing);
t1->foo();
...
}

# make_shared

当我们创建一个 sp 指向一个新 new 出来的对象时,一种语法是:

1
shared_ptr<Thing> p(new Thing);

在这种情况下,会触发两次内存分配,一次是调用 Thing 的构造函数,分配其内存,还有一次是调用 sp 的构造函数,分配其内存给一个管理对象。而两次内存分配会导致性能 / 效率的降低。

所以,为了优化这个问题,有了 make_shared 这个函数,用来一次分配足够的内存:

1
shared_ptr<Thing> p(make_shared<Thing>()); // only one allocation!

在这种情况下,只会发生一次内存分配,效率提高。

这里有个小问题,为什么 make_shared 也需要传入一个模板参数 Thing 呢?是为了灵活性考虑,这样的话 make_shared 生成的对象就可以是其他类型然后 cast 造型给 sp 使用。比如在继承关系中用于父类、子类的指针指向:

1
shared_ptr<Base> bp(make_shared<Derived1>());

最后,如果 Thing 的构造函数需要传递参数,在 make_shared 后面接着加上即可:

1
shared_ptr<Thing> p (make_shared<Thing>(42, "I'm a Thing!"));

这里提一嘴,虽然说我们用 make_shared 可以优化效率,但是它也不是就完全没有缺点。根据前面 “底层实现” 提到的内容,如果我们用 make_shared 生成的内容,sp 已经减为 0 了,但是 wp 还大于 0,那么整块内存都不会被释放。

# 循环引用

循环引用是指两个或多个对象相互持有 std::shared_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
#include <memory>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> bPtr;

~A() {
std::cout << "A destructor called" << std::endl;
}
};

class B {
public:
std::shared_ptr<A> aPtr;

~B() {
std::cout << "B destructor called" << std::endl;
}
};

int main() {
std::shared_ptr<A> aPtr = std::make_shared<A>();
std::shared_ptr<B> bPtr = std::make_shared<B>();

// 建立循环引用
aPtr->bPtr = bPtr;
bPtr->aPtr = aPtr;

return 0;
}

在上述示例中,我们定义了两个类 AB ,它们分别包含一个 std::shared_ptr 成员变量用于相互引用。在 main 函数中,我们创建了两个对象 aPtrbPtr ,然后通过相互赋值建立了循环引用。

当程序结束时,这两个对象的析构函数并不会被调用,因为它们之间的循环引用导致引用计数无法降为零。这就造成了内存泄漏,因为这些对象所占用的内存无法被释放。

为了解决循环引用问题,C++ 提供了 std::weak_ptrstd::weak_ptr 是一种弱引用,它可以引用 std::shared_ptr 指向的对象,但并不会增加对象的引用计数。通过将其中一个指针使用 std::weak_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
//于是,只需要把上面的其中一个指针声明为weak_ptr即可
#include <iostream>
#include <memory>

class B;  //Forward declaration
class A {
public:
    std::shared_ptr<B> bPtr;
    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

class B {
public:
    // std::shared_ptr<A> aPtr;
    std::weak_ptr<A> aPtr;  //use weak ptr to break the loop

    ~B() {
        std::cout << "B destructor called" << std::endl;
    }
};

int main() {
    while(1) {
        std::shared_ptr<A> aPtr = std::make_shared<A>();
        std::shared_ptr<B> bPtr = std::make_shared<B>();
        // make it loop
        aPtr->bPtr = bPtr;
        bPtr->aPtr = aPtr;
        std::cout << "in while loop..." << std::endl;
    } return 0;
}

std::weak_ptr 并不能直接打破循环引用,而是通过辅助的机制来解决循环引用的问题。它并不增加引用计数,因此即使存在循环引用,对象的引用计数也会降为零,从而触发析构函数的调用。

在前面的示例中,当 aPtrbPtr 的引用计数降为零时,它们的析构函数会被调用,从而释放它们所占用的内存。由于 bPtr 使用的是 std::weak_ptr ,它并不增加 aPtr 的引用计数,因此循环引用并不会阻止 aPtr 的析构。这样就避免了 std::shared_ptr 循环引用导致的内存泄漏问题。

因此,可以说 std::weak_ptr 并非直接打破循环引用,而是通过协助 std::shared_ptr 来实现循环引用的解决方案。它在不增加引用计数的前提下,提供了一种访问被 std::shared_ptr 管理的对象的方式,并且不会阻止对象的销毁。

# unique_ptr

它和 sp 其实差不多了,区别就是 ownership 的独占,which 通过禁用拷贝构造和赋值构造来实现,所以你不能拷贝或赋值一个 up 给另一个 up:

1
2
3
4
unique_ptr<Thing> p1 (new Thing); // p1 owns the Thing
unique_ptr<Thing> p2(p1); // error - copy construction is not allowed.
unique_ptr<Thing> p3; // an empty unique_ptr;
p3 = p1; // error, copy assignment is not allowed

由于禁用拷贝构造,所以如果要把 up 作为函数参数进行传参时,只能 pass by reference,不能 pass by value。

1、所有权转移
而由于 up 对于对象的所有权是独占的,所以有时需要转移所有权,这依赖于移动语义,使用移动构造和移动赋值函数。

其中,我们知道,函数的返回值是右值,所以 up 的这种移动语义可以用于我们在函数中返回一个局部的 up 给外层调用该函数的 up,实现移动:

1
2
3
4
5
6
7
8
9
10
11
12
13
//create a Thing and return a unique_ptr to it:
unique_ptr<Thing> create_Thing() {
unique_ptr<Thing> local_ptr(new Thing);
return local_ptr; // local_ptr will surrender ownership
}

void foo() {
unique_ptr<Thing> p1(create_Thing()); // move ctor from returned rvalue
// p1 now owns the Thing
unique_ptr<Thing> p2; // default ctor'd; owns nothing
p2 = create_Thing(); // move assignment from returned rvalue
// p2 now owns the second Thing
}

2、所有权的显式转移
通过 std::move() 函数来显式地进行所有权的转移:

1
2
3
4
5
6
unique_ptr<Thing> p1(new Thing); // p1 owns the Thing
unique_ptr<Thing> p2; // p2 owns nothing
// invoke move assignment explicitly
p2 = std::move(p1); // now p2 owns it, p1 owns nothing
// invoke move construction explicitly
unique_ptr<Thing> p3(std::move(p2)); // now p3 owns it, p2 and p1 own nothing

另外,前面在讲 sp 的时候讲到了有个函数 make_shared ,而 C++11 居然没有相对应的 make_unique ,直到 C++14 的时候才引入了 make_unique 函数。不过, make_unique 不会有性能上的提升,因为 up 并不会分配管理对象,所以没有额外的内存分配,不会有两次内存分配,就不会有性能低的问题。—— 可能这就是为什么 C++11 引入了 make_shared 却没有同时引入 make_unique 的原因吧。

# shared_ptr

Shared pointer,a class template:

1
2
#include <memory>
template <class T> class shared_ptr;

简单来说,share pointer 具备引用计数、自动释放的特点。多个  shared_ptr  指向同一块内存时,每多一个指针指向该内存,引用数相应自增,反之亦然。当一块内存的  shared_ptr  引用数减少为 0 时,自动释放该内存。

即,我们可以通过智能指针来申请动态内存,其会有一个相应的引用计数(申请时初始值为 1,表明当前这个  shared_ptr  指向该内存)。每当有新的智能指针指向该内存时,引用计数自增。

https://cplusplus.com/reference/memory/shared_ptr/

智能指针的创建:

空智能指针:

1
2
3
std::shared_ptr<int> p1;	//指向 int 类型的智能指针,此时不传入任何实参,为空指针
std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
//空指针的引用计数为 0,此时并未指向任何内存,合情合理

非空智能指针:

1
2
3
4
5
std::shared_ptr<int> p3(new int(10));	//指向一个值为 3 的 int 型的内存
//此时引用计数为 1

//另外,也可以通过 C++11 提供的模板函数来初始化智能指针
std::shared_ptr<int> p4 = std::make_share<int>(10);

构造函数(拷贝构造和移动构造):

1
2
3
4
5
6
7
//拷贝构造函数
std::shared_ptr<int> p5(p4);
std::shared_ptr<int> p6 = p5;

//移动构造
std::shared_ptr<int> p7(std::move(p6));
std::shared_ptr<int> p8 = std::move(p7);

成员方法:

  • swap()
    交换两个相同类型  shared_ptr  的内容

  • reset()
    当该函数没有传入实参时,作用是将当前  shared_ptr  所指内存的引用计数减 1,同时将当前指针对象重置为一个空指针;

当该函数传入一个实参(一个新申请的堆空间)时,则当前指针对象会指向该新申请的堆空间且引用计数初始值为 1,同时,原指向的堆空间(如果有)的引用计数自然而然减 1,如果减 1 后为 0,则释放该内存

  • get()
    获得  shared_ptr  对象内部包含的普通指针

  • use_count()
    返回当前智能指针所指向内存的引用计数值,即所有指向该内存的智能指针的数量

  • unique()
    判断当前智能指针指向的堆内存,是否不再有其他  shared_ptr  指向它

参考链接:
https://cplusplus.com/reference/memory/shared_ptr/reset/
http://c.biancheng.net/view/7898.html
http://c.biancheng.net/view/430.html
https://blog.csdn.net/ff_gogo/article/details/123512482
https://www.cnblogs.com/dream397/p/14620324.html

实际应用:

1
2
3
4
5
6
7
8
9
10
m_dds_ctx.reset(new minieye::DDS::Context(FLAGS_config_json_radar, true));
...
if (radar_topic != nullptr)
{
std::shared_ptr<minieye::DDS::Reader> r(new minieye::DDS::Reader(
m_dds_ctx.get(), radar_topic->topic, OnRecvDDS, this));

m_readers = r;
}
...

# deleter 删除器

对于自定义的类,我们知道存在构造函数和析构函数,析构函数能够在对象销毁时被自动调用,结合智能指针则能够实现自动管理,让其在引用计数减为 0 时自动调用析构函数释放资源。

但是,对于内置的类型,比如 int、float 这种,并不存在任何析构函数,所以无论我们是否使用智能指针,我们都不能很好地调用所谓的 “析构函数” 来释放资源。比如:

1
std::shared_ptr<int> num{new int(5)};

针对这种情况,我们需要用到 deleter。

在智能指针中,可以指定一个删除器,用于在引用计数减为 0 时调用该删除器,进行资源的释放。以 shared_ptr 举例,它的其中两种构造函数如下:

1
2
3
template <class U, class D> shared_ptr(U* p, D del);
template <class D> shared_ptr(nullptr_t p, D del);
#其中 del 是一个删除器

deleter 可以是以下类型之一:

  1. 函数指针:指向一个函数,该函数会在智能指针的资源需要释放时被调用。
  2. 函数对象(Functor):是一个类对象,重载了函数调用运算符 operator() ,可以像函数一样调用。
  3. Lambda 表达式:一种匿名函数,可以用作智能指针的 deleter

于是就有了这种智能指针的构造方式:

1
2
3
4
5
// 使用 Lambda 表达式作为 deleter 的 std::shared_ptr
std::shared_ptr<MyClass> sharedPtr(new MyClass(24), [](MyClass* ptr) {
std::cout << "Lambda deleter is called" << std::endl;
delete ptr;
});

整体示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>
using namespace std;

//Custom deleter function declaration
void my_deleter(int* ptr){
cout << "Deleting integer pointer with value: " << *ptr << endl;
delete ptr;
cout << "Delete Successful.";
}


int main()
{
//int type smart pointer object
shared_ptr<int> ptr {new int{5}, my_deleter};
return 0;
}

大概是这样。

这里需要注意的是,我们的 shared_ptr 的构造函数存在几种形式:

1
2
3
4
5
template< class Y >
explicit shared_ptr( Y* ptr );

template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d );

对于前者,当引用计数值为 0 时会使用 delete ptrdelete[] ptr ,具体取决于 T 是否为数组类型。而对于后者,则是通过传入一个用户自定义的删除器,在引用计数值为 0 自动调用。详细信息见:shared_ptr

但是由此又似乎会得出结论,哪怕我们没有指定删除器,利用原本的 delete 语法也能处理基本类型嘛?再者,当使用智能指针管理基础类型时,真的不能有效释放内存吗?sus

# Q&A

智能指针如何判空?

我们知道,对于裸指针,我们可以直接在 if 条件语句中将其用作条件进行判空,如:

1
2
3
4
5
int* rp = new int(5);

if(rp) {
//...
}

同样的,对于智能指针,也能这么操作。

智能指针可以使用 operator bool()get() 方法来进行空指针判断。以下是两种常用的方法:

  1. 使用 operator bool()

std::shared_ptrstd::unique_ptr 都定义了 operator bool() ,可以用于将智能指针直接用于布尔上下文中进行判空。

1
2
3
4
5
6
7
8
9
std::shared_ptr<int> ptr = std::make_shared<int>(42);

if (ptr) {
// 指针不为空
// 执行代码...
} else {
// 指针为空
// 执行其他代码...
}

  1. 使用 get() 方法:

get() 方法返回底层裸指针,你可以将其与 nullptr 比较来进行判空。

1
2
3
4
5
6
7
8
9
std::shared_ptr<int> ptr = std::make_shared<int>(42);

if (ptr.get() != nullptr) {
// 指针不为空
// 执行代码...
} else {
// 指针为空
// 执行其他代码...
}

不过第二种方法有点没必要,好好的智能指针不用,还把它转化成裸指针,何苦呢

另外,这里的 operator bool() 是操作符重载,具体地:

operator bool() 是一个类型转换运算符,用于将类对象转换为布尔值。在 C++ 中,运算符重载允许程序员自定义类对象在特定上下文中的操作行为,而 operator bool() 是其中一种常见的运算符重载。

运算符重载的语法如下:

1
2
3
4
5
6
7
8
9
class MyClass {
public:
// 类型转换运算符的重载
explicit operator bool() const {
// 返回用于将对象转换为布尔值的逻辑表达式
// 返回 true 表示对象非空,返回 false 表示对象为空
return someCondition; // 替换为实际的逻辑表达式
}
};

在上述示例中, operator bool() 被定义为将 MyClass 类的对象转换为布尔值。注意其中的 explicit 关键字,它表示这个转换是显式的,防止隐式转换。

对于智能指针类(如 std::shared_ptrstd::unique_ptr ), operator bool() 的实现通常是检查底层指针是否为 nullptr 。这样,当智能指针指向有效对象时,转换结果为 true ,指向空对象时,转换结果为 false 。这种设计使得智能指针在条件语句中的使用更加自然,就像原始指针一样。

以下是智能指针 operator bool() 的典型实现:

1
2
3
4
5
6
7
8
9
10
class SmartPtr {
public:
// 类型转换运算符的重载
explicit operator bool() const {
return ptr != nullptr;
}

private:
int* ptr; // 以 int* 为例,实际上可能是其他类型
};

这使得你可以像下面这样使用智能指针:

1
2
3
4
5
6
SmartPtr ptr = ...;  // 通过某种方式初始化
if (ptr) {
// 智能指针不为空,执行代码...
} else {
// 智能指针为空,执行其他代码...
}

这种运算符重载的使用,使得在条件语句中判空更加直观,提高了代码的可读性。

# 相关阅读

stack overflow:如何判断我的 shared_ptr 被谁持有?

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/12/C-\345\244\232\347\272\277\347\250\213/index.html" "b/2024/10/12/C-\345\244\232\347\272\277\347\250\213/index.html" index 5c16931..68cbe1b 100644 --- "a/2024/10/12/C-\345\244\232\347\272\277\347\250\213/index.html" +++ "b/2024/10/12/C-\345\244\232\347\272\277\347\250\213/index.html" @@ -1 +1 @@ -C++ 多线程 - C++ - 编程 | R1ck's Playground = how

# 一、Intro

完全无任何线程同步措施,线程会相互竞争打断,本例中,线程 A 的操作无法保证完整性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ThreadA() {
while(1) {
{
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
}
}

void ThreadB() {
while(1) {
{
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

每个线程操作公共资源前先加锁再操作,可以保证对资源的访问不会被打断,但无协调同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ThreadA() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
}
}

void ThreadB() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

加入条件变量进行协同,A 线程在完成处理后通知 B 线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void ThreadA() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // 等待条件满足,即等待线程 A 操作完毕
// 在等待期间,互斥锁会被释放
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

但如果 A 线程的速度很快,A 线程仍然会多次竞争到锁然后多次访问资源、多次 notify 线程 B,尽管线程 B 具备访问公共资源的条件(条件变量成立),但常常会被线程 A 抢占 —— 除非线程 A 通过 sleep 休眠来放弃 CPU。

这里只要让线程 A 稍微休眠仅仅 1 ms,线程 B 就能立刻抢占到 CPU,又加之条件变量的条件满足,故线程 B 得以访问公共资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ThreadA() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // 等待条件满足,即等待线程 A 操作完毕
// 在等待期间,互斥锁会被释放
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

与之相反的是,线程 B 并不会由于频繁调用而持续占用 CPU,因为其调用的 wait 函数会阻塞并等待互斥锁可用,此时其将放弃 CPU。

# 二、常见场景

# 1、A 线程和 B 线程轮流执行,互为条件

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool isThreadAturn = true;


void threadFuncA() {
while(1) {
{
std::unique_lock<std::mutex> lck(mtx);
if(!isThreadAturn) {
cv.wait(lck);
}

std::cout << "in threadFuncA====: 3" << std::endl;
std::cout << "in threadFuncA====: 4" << std::endl;

isThreadAturn = false;
cv.notify_one();
}
} return;
}


void threadFuncB() {
while(1) {
{
std::unique_lock<std::mutex> lck(mtx);
if(isThreadAturn) {
cv.wait(lck);
}

std::cout << "====in threadFuncB: 1" << std::endl;
std::cout << "====in threadFuncB: 2" << std::endl;

isThreadAturn = true;
cv.notify_one();
}
} return;
}


int main() {
std::thread threadA(threadFuncA);
std::thread threadB(threadFuncB);

threadA.join();
threadB.join();

return 0;
}

# 2、A 线程执行一次后,B 线程执行多次,轮流执行
# 3、A 线程执行多次后,B 线程执行一次,轮流执行
# 4、A 线程执行多次后,B 线程执行多次,轮流执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void threadFuncA() {
while(1) {
{
std::unique_lock<std::mutex> lck(mtx);
if(!isThreadAturn) {
cv.wait(lck);
}

std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "in threadFuncA====: 3" << std::endl;
std::cout << "in threadFuncA====: 4" << std::endl;

static int cnt = 0;
cnt++;
if(cnt == 4) {
cnt = 0;
isThreadAturn = false;
cv.notify_one();
}
}
} return;
}

以上三种情况只需要在各自线程中维护一个 cnt 变量即可,只有运行一定次数后再去 notify 其他线程。

# 5、A 线程执行到某个节点后需要等待 B 线程执行到某个节点,然后 A 线程再执行

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
36
37
bool isConditionMeet = false;
void ThreadA() {
{
std::unique_lock<std::mutex> lock(mtx);
if(!isConditionMeet) {
cv.wait(lock);
}

std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
isConditionMeet = false;

getchar();
return;
}

void ThreadB() {
{
std::cout << "== in thread b, some pre process" << std::endl;

std::unique_lock<std::mutex> lock(mtx);
isConditionMeet = true;
cv.notify_one();

cv.wait(lock); // 等待条件满足,即等待线程 A 操作完毕

// 在等待期间,互斥锁会被释放
std::cout << "== Thread B: do somethingB ==" << std::endl;
}

getchar();
return;
}

利用条件变量,维护一个全局的布尔变量,用于控制 A、B 线程的先后运行顺序(先运行 B 线程),然后在需要协同的某个节点开始 wait,等待另一个线程执行完需要的依赖然后 notify。这里其实用两个条件变量更加便于理解且清晰。

# 6、A 线程执行到某个节点后需要等待 B 线程执行到某个节点,然后一起继续执行

B 线程到了相应节点后,notify A 线程,自己则照常继续往下执行即可。
A 线程 wait 等待 B 线程执行到相应的节点然后接收到通知后往下执行。
用到一个 mutex 和一个 condition_variable,可以不需要维护一个全局的布尔变量。

# 三、相关函数

# 1、std::condition_variable

std::condition_variable 是 C++ 标准库中的一种线程同步原语,用于实现线程之间的协作和通信。它通常与互斥锁( std::mutex )一起使用,用于阻塞一个或多个线程,直到另一个线程修改了共享变量(条件)并通知了 condition_variable

想要修改共享变量的线程必须满足以下条件:
1. 获取一个 std::mutex 互斥锁(典型例子是通过 std::lock_guard
2. 当持有锁时修改该共享变量
3. 利用条件变量调用 notify_one 或者 notify_all (可以先释放锁再 notify)

想要等待条件变量的线程必须满足以下条件:
1. 获取用于保护共享变量的 std::unique_lock<std::mutex>
2. 执行以下操作中的任意一个:
检测条件变量是否被 notify
利用条件变量调用 wait 函数(会自动释放锁并挂起线程,直到被 notify 或时间到期或虚假唤醒)
如果条件不满足则继续等待

# a)std::condition_variable::wait

wait 会阻塞当前线程,直到条件变量被 notify(或发生虚假唤醒),有两种重载形式:

1
2
3
4
void wait( std::unique_lock<std::mutex>& lock );

template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate stop_waiting );

当一个条件变量调用 wait 函数时,会自动释放当前已经持有的锁,然后阻塞当前线程,等待条件变量被 notify。当 notify_onenotify_all 被执行后,线程会被解除阻塞。当被解除阻塞后,在 wait 函数退出前,锁会被重新获取

第二种重载形式相较于第一种,多了一个谓词作为条件,其含义为:当条件不满足时,将持续等待。其能够有效避免虚假唤醒。

第二种重载形式等价于:

1
2
3
4
while (!stop_waiting())
{
wait(lock);
}

这意味着:
1. 当第一次调用 wait 时,如果条件为真,不会发生阻塞,不需要 notify 就能继续执行后续代码
2. 一旦阻塞发生,则必须等待 notify 以解除阻塞,光是条件为真不管用
3. 如果被 notify 了,不一定会解除阻塞,需要进一步判断条件是否为真,以防止虚假唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
std::mutex mtx;
std::condition_variable cv;
bool isReady = false;

void ThreadA() {
while(1) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
{
std::lock_guard<std::mutex> lock(mtx);
isReady = true;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return isReady; }); // 等待条件满足
// 在等待期间,互斥锁会被释放
}
}
}

# b)std::condition_variable::wait_for

和 wait 相似,有两个重载版本:

1
2
3
4
5
6
7
8
9
template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time );


template< class Rep, class Period, class Predicate >
bool wait_for( std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time,
Predicate stop_waiting );

与 wait 不同的是, wait_for 允许线程在等待条件一段时间后自动醒来,即使条件尚未满足。线程可以指定一个时间段作为参数,如果在指定时间内条件未满足,线程将自动醒来。

另外,wait_for 具有函数返回值,用于区分是否超时、是否满足条件:
对于第一种重载版本:
返回 std::cv_status::timeout 如果超时了
返回 std::cv_status::no_timeout 如果未超时
对于第二种重载版本:
返回 false 如果超时后条件仍未满足( stop_waiting 仍为 false),否则返回 true

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
std::mutex mtx;
std::condition_variable cv;
bool isReady = false;

void ThreadA() {
while(1) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
{
std::lock_guard<std::mutex> lock(mtx);
isReady = true;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
auto timeout = std::chrono::seconds(1);
std::cv_status status = cv.wait_for(lock, timeout, [] { return isReady; });
if (status == std::cv_status::no_timeout) {
std::cout << "Main thread: Condition is met." << std::endl;
} else {
std::cout << "Main thread: Timeout occurred." << std::endl;
}
}
}
}

# c)std::condition_variable::notify_one and notify_all

唤醒等待在当前条件变量的任意一个线程或所有线程。

# 2、lock_guard

#CPP 新特性 #CPP11
std::lock_guard 是 C++ 标准库中的一个类,用于管理互斥锁(mutex)的自动加锁和解锁操作。它是 C++11 引入的一部分,旨在简化多线程编程中的锁管理,以防止忘记在离开作用域时解锁互斥锁,从而避免死锁等问题

std::lock_guard 具有以下主要特点和用途:

  1. 自动加锁和解锁:当创建 std::lock_guard 对象时,它会自动锁定关联的互斥锁;当 std::lock_guard 对象离开其作用域时(例如,通过函数返回或作用域结束),它会自动释放锁。
  2. 异常安全std::lock_guard 提供了异常安全性,即使在作用域中抛出异常,也会在作用域结束时正确释放锁,从而防止资源泄漏。
  3. 简化代码:使用 std::lock_guard 可以大大简化管理互斥锁的代码,避免了手动加锁和解锁,减少了程序出错的可能性。

1
2
3
4
5
// 示例
{
std::lock_guard<std::mutex> lock(mtx);
//加锁后的一些操作
}//当离开当前作用域,自动解锁

1
2
3
4
5
6
7
8
9
10
// 工程中实际应用
else
{
{
// 设置tick=0,避免泊车切行车时发送旧的结果
std::lock_guard<std::mutex> lk(m_mutex_latest_fusion_res);
m_latest_fusion_res->set_tick(0);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

需要注意的是, std::lock_guard 通常是一个更安全和方便的替代品,而不是手动使用 std::unique_lockstd::lock 函数。它是一种简单的 RAII(资源获取即初始化)包装,可用于管理互斥锁的生命周期。

参考链接:
std: :lock_guard

# 3、unique_lock

std::unique_lock 是 C++ 标准库中的一个模板类,用于管理互斥锁( std::mutex )的生命周期,提供更大的灵活性和功能,以确保在作用域结束时自动释放锁。 std::unique_lock 也是一种用于 RAII(资源获取即初始化)风格的锁管理。

std::unique_lock 具有以下主要特点和用途:

  1. 自动加锁和解锁:当创建 std::unique_lock 对象时,它可以选择是否自动锁定关联的互斥锁,还可以在其作用域结束时自动解锁互斥锁。这提供了更大的灵活性,因为您可以手动控制锁定和解锁的时机
  2. 异常安全std::unique_lock 提供了异常安全性,即使在作用域中抛出异常,也会在作用域结束时正确释放锁,从而防止资源泄漏。
  3. 支持延迟锁定std::unique_lock 允许您在等待条件变量时解锁互斥锁,然后重新锁定互斥锁,以提高效率和减少锁的占用时间。
  4. 手动锁定和解锁:您可以随时手动锁定和解锁互斥锁,以执行更复杂的操作。

从上面这些特点看来,其实 unique_locklock_guard 颇有相似之处,都是 RAII 风格,都有相关的功能。但相比之下, unique_lock 有更大的灵活性,其可以控制加锁解锁的时机,可以手动加锁解锁,可以延迟锁定。二者的具体差异如下:

  1. 灵活性
    • std::lock_guard 具有较低的灵活性,因为它只提供了自动加锁和解锁的功能。一旦创建,它不允许手动解锁或重新锁定互斥锁。这使得它非常适合那些不需要更复杂锁定操作的简单场景。
    • std::unique_lock 具有更高的灵活性,因为它允许您手动锁定和解锁互斥锁。这意味着您可以选择何时锁定和解锁,还可以执行更复杂的操作,如延迟锁定和递归锁定。
  2. 锁定和解锁
    • std::lock_guard 在构造时自动锁定互斥锁,并在作用域结束时自动解锁。没有手动解锁的选项。
    • std::unique_lock 在构造时可以选择是否锁定互斥锁std::defer_lock ),以及在作用域结束时是否自动解锁( std::adopt_lock )。您还可以随时手动解锁或重新锁定互斥锁( lockunlock )。
  3. 延迟锁定和条件变量
    • std::unique_lock 对于支持条件变量的操作非常有用,因为它可以在等待条件变量时解锁互斥锁wait ),然后重新锁定互斥锁。这允许线程在等待条件时不占用锁,提高了效率。
    • std::lock_guard 不支持这种延迟锁定操作。

具体地,对于 unique_lock ,按照其默认的初始化方式,其在创建时自动加锁,离开作用域时自动解锁:

1
2
3
{
std::unique_lock<std::mutex> lck(mtx);
}

如果需要控制使其在创建时不立即 / 自动加锁,则:

1
2
3
{
std::unique_lock<std::mutex> lck(mtx, std::defer_lock);
}

如果需要控制使其在离开作用域时不立即 / 自动解锁,则:

1
2
3
{
std::unique_lock<std::mutex> lck(mtx, std::adopt_lock);
}

这表明在创建该 unique_lock 时 mtx 已经被上锁了,并且在其作用域结束时不要自动解锁。

参考链接:
unique_lock constructor
lock_tag

# 4、scoped_lock

#CPP 新特性 #CPP17
std::scoped_lock 是 C++17 标准引入的一个 RAII 风格的模板类,用于管理多个互斥锁的生命周期。它是 std::lock 的一种更安全和方便的替代品,用于在一个作用域内同时锁定多个互斥锁,以避免死锁和提供异常安全性。

std::scoped_lock 具有以下主要特点和用途:

  1. 同时锁定多个互斥锁std::scoped_lock 允许您在一个作用域内同时锁定多个互斥锁,而不需要手动编写多次锁定的代码。
  2. 自动解锁:与 std::unique_lock 类似, std::scoped_lock 会在其作用域结束时自动解锁所有已锁定的互斥锁,无论是正常退出还是由于抛出异常而退出。
  3. 避免死锁std::scoped_lock 在锁定多个互斥锁时会采用死锁避免算法,以避免死锁。
  4. 异常安全std::scoped_lock 提供了异常安全性,确保即使在作用域中抛出异常,已锁定的互斥锁也会被正确释放。

std::scoped_lockstd::lock_guard 的主要差异在于前者能够管理多个互斥锁,其余都大同小异。
对于两者的差异,可参考:scoped_lock vs lock_guard

scoped_locklock_guard 的区别?

在 C++17 标准中, std::scoped_lockstd::lock_guard 都是用于管理互斥锁(mutex)的锁类型,它们确保在作用域结束时自动释放锁。然而,它们有一些不同之处,主要体现在它们的设计目的和功能上。

std::scoped_lock

  • 功能: std::scoped_lock 主要用于同时锁定多个互斥锁。它提供了一个锁定多个互斥锁的安全机制,防止死锁的发生。
  • 特性:支持锁定多个互斥锁,并按照指定的顺序锁定,防止死锁。
  • 引入: C++17 标准引入。

std::lock_guard

  • 功能: std::lock_guard 用于在单个互斥锁上实现简单的 RAII(资源获取即初始化)风格的锁定。它在构造时锁定互斥锁,在析构时自动释放锁。
  • 特性:只能锁定一个互斥锁,适用于简单的互斥锁管理。
  • 引入: C++11 标准引入。

具体区别

  1. 锁定多个互斥锁:
  • std::scoped_lock 可以同时锁定多个互斥锁,防止死锁。例如:

    1
    2
    std::mutex m1, m2;
    std::scoped_lock lock(m1, m2);

  • std::lock_guard 只能锁定一个互斥锁。

    1
    2
    std::mutex m;
    std::lock_guard<std::mutex> lock(m);

  1. 用法简洁性:
  • 对于单个互斥锁,两者用法几乎一样,但 std::scoped_lock 在 C++17 中更推荐,因为它的名称更具描述性。
  • 对于多个互斥锁, std::scoped_lock 提供了更简洁的语法。

示例代码对比

  1. 使用 std::scoped_lock :

    1
    2
    3
    4
    5
    [[nodiscard]] size_t get_tasks_queued() const
    {
    const std::scoped_lock tasks_lock(tasks_mutex);
    return tasks.size();
    }

  2. 使用 std::lock_guard :

    1
    2
    3
    4
    5
    [[nodiscard]] size_t get_tasks_queued() const
    {
    const std::lock_guard<std::mutex> tasks_lock(tasks_mutex);
    return tasks.size();
    }

在这个具体的例子中,锁定一个互斥锁时,两者的效果是相同的。选择使用哪一个更多的是风格和一致性的考虑。如果代码库已经在使用 std::scoped_lock 来锁定单个或多个互斥锁,那么继续使用 std::scoped_lock 可能更一致。

结论

  • 单个互斥锁: std::lock_guardstd::scoped_lock 都可以使用。 std::scoped_lock 的名称在语义上可能更明确。
  • 多个互斥锁:使用 std::scoped_lock
  • 代码一致性:根据代码库的风格和惯例来选择。C++17 及以后的代码可能更倾向于使用 std::scoped_lock

总的来说:
使用 std::lock_guard 如果需要在整个作用域中管理一个互斥锁
使用 std::scoped_lock 如果需要在整个作用域中管理多个互斥锁(明确多于一个)
使用 std::unique_lock 如果需要在作用域中解锁互斥锁(包括使用条件变量)

# 5、atomic

std::atomic 是 C++ 标准库提供的一组类型和函数,用于支持原子操作。原子操作是一种多线程编程中的同步机制,确保共享数据的并发访问是安全的,避免数据竞争和并发问题。 std::atomic 提供了一些可以在多线程环境中进行原子操作的基本数据类型,如整数、布尔值等。

std::atomic 具有以下特点:

  1. 原子性std::atomic 操作是原子的,要么完全执行,要么不执行。这意味着它们不会被其他线程中断,也不会导致竞态条件。
  2. 不需要互斥锁std::atomic 操作通常不需要显式的互斥锁,因为它们是原子的。这有助于提高多线程程序的性能。
  3. 内存顺序(Memory Order)std::atomic 操作允许您指定内存顺序,以控制操作的顺序和可见性,以满足程序的需求。

常见的 std::atomic 类型包括 std::atomic<int> , std::atomic<bool> , std::atomic<std::shared_ptr<T>> 等,您可以根据需要选择合适的类型。

在多线程语境下, std::atomic 的常见用法包括:

  1. 实现原子计数器std::atomic 可用于实现线程安全的计数器,如统计某个事件发生的次数。
  2. 管理共享标志位std::atomic<bool> 常用于管理共享标志位,用于控制线程的启动、停止或某个状态的切换。
  3. 无锁数据结构std::atomic 用于创建无锁数据结构,如无锁队列、无锁堆栈,以提高多线程程序的性能。
  4. 原子操作函数std::atomic 类型提供了一系列原子操作函数,如 storeloadexchangecompare_exchange 等,用于执行各种原子操作。
  5. 控制并发访问std::atomic 可以用于确保多线程环境中的共享数据的一致性,以避免竞态条件。

简单来说,对于简单的内置变量,可通过  atomic  来进行多线程操作而不用使用锁。

头文件:

1
#include <atomic>

atomic 是个模板,其对于变量的初始化:

1
2
3
4
5
std::atomic<bool> isReady(false);
std::atomic<int> num(0);

std::atomic<int> sum;
std::atomic_init(&sum, 17);

变量取值:

1
int ret = num.load();

变量设值:

1
2
isReady.store(true);
num.store(19);

TBD,更多细节,参考:
https://www.educative.io/answers/what-is-atomic-type-in-cpp
https://stackoverflow.com/questions/31978324/what-exactly-is-stdatomic(回答 2)

# 四、相关概念

# 1、虚假唤醒

虚假唤醒(Spurious Wakeup)是多线程编程中一个重要的概念,指的是在没有收到明确的通知的情况下,等待中的线程会偶尔自发地从休眠状态醒来。这种情况可能发生在使用条件变量( std::condition_variable )等线程同步机制时。

虚假唤醒有以下关键特点和考虑事项:

  1. 无通知情况下醒来:在条件变量的等待期间,线程可能因为某些系统或实现细节而在没有任何明确通知的情况下醒来。这是一个与多线程编程相关的现象,可能是由操作系统、编译器或硬件的特定行为导致的。
  2. 检查条件的必要性:虚假唤醒的发生意味着等待线程必须谨慎处理醒来的情况。因此,在等待条件满足时,线程应该总是在一个循环中检查条件,而不是假设条件一定已满足。
  3. 条件互斥:通常,虚假唤醒会伴随互斥锁的使用。线程在等待前获取互斥锁,然后在等待期间释放它,以确保其他线程能够访问共享资源。虚假唤醒可能会导致等待线程在检查条件前重新获取互斥锁。
  4. 实现相关:虚假唤醒的频率可能因操作系统或编译器的实现而异。有些操作系统 / 编译器可能更容易发生虚假唤醒,而其他可能较少发生。
# 2、概念辨析

以下概念都与多线程编程和并发相关:

  1. 阻塞(Blocking)
    • 阻塞是指线程暂停其执行,等待某种事件的发生,通常是等待某个条件的满足或资源的可用性。在阻塞状态下,线程不会占用 CPU 时间,直到条件满足或资源可用时才会继续执行。
  2. 休眠(Sleeping)
    • 休眠是阻塞的一种形式,线程在休眠状态下会进入一种低功耗状态,以节省系统资源。通常,线程会在休眠一段时间后自动醒来,或者通过外部事件唤醒。
  3. 挂起(Suspending)
    • 挂起是指将线程的执行暂停,使其暂时不可运行。这可以是手动挂起线程,也可以是由操作系统或调度程序执行。线程在挂起状态下不会占用 CPU 时间,需要显式恢复才能继续执行。
  4. 忙等待(Busy-Waiting)
    • 忙等待是一种线程等待条件满足的方式,它通过不断检查条件的变化来等待,而不是进入休眠状态。忙等待会占用大量的 CPU 时间,通常不是一种高效的等待方式,应该避免在需要长时间等待的情况下使用。
  5. 自旋(Spinning)
    • 自旋是忙等待的一种形式,线程在自旋状态下会重复执行某个操作,通常是检查条件是否满足。自旋通常用于需要极短等待时间的情况,以避免进入和退出休眠状态的开销。

这些概念在多线程编程中有不同的用途和场景。选择适当的等待方式取决于具体的需求和性能要求。通常,阻塞和休眠是较为高效的等待方式,而忙等待和自旋适用于某些特定的情况。挂起通常用于需要手动控制线程生命周期的情况。

# 五、关于 wait 的一些实验

对于带条件的 wait,有以下几个实验:

在下面这个实验中,我们让线程 A 先运行,在线程 B 还未执行 notify 之前,线程 A 就跳过了 wait 并打印相关信息。这里我们让 A 线程中的休眠时间尽可能短,使得一旦线程 B notify,线程 A 马上解除阻塞。

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
std::mutex mtx;
std::condition_variable cv;

bool isGood = true;

void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
cv.notify_one();
std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

上面这个实验表明:当调用 cv.wait(lck, [](){ return isGood; }); 时,如果条件满足,则不会阻塞,不需要被 notify 就能直接执行后续代码。

其实就是验证了前文所说的:

当第一次调用 wait 时,如果条件为真,不会发生阻塞,不需要 notify 就能继续执行后续代码

在下面这个实验中,我们去掉了线程 B 中的 notify 动作,只修改 isGood 但不通知。同时,我们更改了线程 A 的休眠时间,让其在第一次执行之后等待足够长的时间以确保线程 B 已经将 isGood 改为 true。

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
void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::seconds(8));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
// std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
// // cv.notify_one();
// std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::cout << "[ThreadB] 2 set isGood to true and dont notify" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

上面这个实验进一步证明:当我们调用带条件的 wait 时,首先会判断条件是否满足,如果满足,则根本不需要 notify 就能往下走。如果把上面线程 A 的休眠时间改成 50 ms,则会产生截然不同的结果:由于休眠时间很短,线程 A 第一次输出打印信息并将 isGood 改为 false 后,由于线程 B 还来不及更改 isGood 的值,线程 A 再次进入作用域中,此时判断 isGood 条件仍为 false,于是阻塞。后续即使线程 B 中更改了 isGood 的值为 true,但由于没有 notify 函数,无法唤醒,线程 A 将永远阻塞在 wait 函数上。

下面这个实验则是在同一个作用域中多次调用 wait,只要条件满足,不阻塞,不需要 notify。

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
void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;

isGood = true;
cv.wait(lck, [](){ return isGood; });
std::cout << "[ThreadA] after second wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
// std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
// // cv.notify_one();
// std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::cout << "[ThreadB] 2 set isGood to true and dont notify" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

在下面这个实验中,我们在线程 B 中并不修改条件使其为真,但却 notify,模拟虚假唤醒。于是显然,线程 A 仍然阻塞在 wait。

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
void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
// isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
cv.notify_one();
std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

综上,带条件的 wait 完全可以视作:

1
2
3
4
while (!stop_waiting())
{
wait(lock);
}

参考链接:
C++ 多线程原语

the End.


更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 多线程 - C++ - 编程 | R1ck's Playground = how

# 一、Intro

完全无任何线程同步措施,线程会相互竞争打断,本例中,线程 A 的操作无法保证完整性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ThreadA() {
while(1) {
{
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
}
}

void ThreadB() {
while(1) {
{
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

每个线程操作公共资源前先加锁再操作,可以保证对资源的访问不会被打断,但无协调同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ThreadA() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
}
}

void ThreadB() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

加入条件变量进行协同,A 线程在完成处理后通知 B 线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void ThreadA() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // 等待条件满足,即等待线程 A 操作完毕
// 在等待期间,互斥锁会被释放
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

但如果 A 线程的速度很快,A 线程仍然会多次竞争到锁然后多次访问资源、多次 notify 线程 B,尽管线程 B 具备访问公共资源的条件(条件变量成立),但常常会被线程 A 抢占 —— 除非线程 A 通过 sleep 休眠来放弃 CPU。

这里只要让线程 A 稍微休眠仅仅 1 ms,线程 B 就能立刻抢占到 CPU,又加之条件变量的条件满足,故线程 B 得以访问公共资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ThreadA() {
while(1) {
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // 等待条件满足,即等待线程 A 操作完毕
// 在等待期间,互斥锁会被释放
std::cout << "== Thread B: do somethingB ==" << std::endl;
}
}
}

与之相反的是,线程 B 并不会由于频繁调用而持续占用 CPU,因为其调用的 wait 函数会阻塞并等待互斥锁可用,此时其将放弃 CPU。

# 二、常见场景

# 1、A 线程和 B 线程轮流执行,互为条件

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool isThreadAturn = true;


void threadFuncA() {
while(1) {
{
std::unique_lock<std::mutex> lck(mtx);
if(!isThreadAturn) {
cv.wait(lck);
}

std::cout << "in threadFuncA====: 3" << std::endl;
std::cout << "in threadFuncA====: 4" << std::endl;

isThreadAturn = false;
cv.notify_one();
}
} return;
}


void threadFuncB() {
while(1) {
{
std::unique_lock<std::mutex> lck(mtx);
if(isThreadAturn) {
cv.wait(lck);
}

std::cout << "====in threadFuncB: 1" << std::endl;
std::cout << "====in threadFuncB: 2" << std::endl;

isThreadAturn = true;
cv.notify_one();
}
} return;
}


int main() {
std::thread threadA(threadFuncA);
std::thread threadB(threadFuncB);

threadA.join();
threadB.join();

return 0;
}

# 2、A 线程执行一次后,B 线程执行多次,轮流执行
# 3、A 线程执行多次后,B 线程执行一次,轮流执行
# 4、A 线程执行多次后,B 线程执行多次,轮流执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void threadFuncA() {
while(1) {
{
std::unique_lock<std::mutex> lck(mtx);
if(!isThreadAturn) {
cv.wait(lck);
}

std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "in threadFuncA====: 3" << std::endl;
std::cout << "in threadFuncA====: 4" << std::endl;

static int cnt = 0;
cnt++;
if(cnt == 4) {
cnt = 0;
isThreadAturn = false;
cv.notify_one();
}
}
} return;
}

以上三种情况只需要在各自线程中维护一个 cnt 变量即可,只有运行一定次数后再去 notify 其他线程。

# 5、A 线程执行到某个节点后需要等待 B 线程执行到某个节点,然后 A 线程再执行

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
36
37
bool isConditionMeet = false;
void ThreadA() {
{
std::unique_lock<std::mutex> lock(mtx);
if(!isConditionMeet) {
cv.wait(lock);
}

std::cout << "===============================" << std::endl;
std::cout << "Thread A: do somethingA 1" << std::endl;
std::cout << "Thread A: do somethingA 2" << std::endl;
std::cout << "Thread A: do somethingA 3" << std::endl;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
isConditionMeet = false;

getchar();
return;
}

void ThreadB() {
{
std::cout << "== in thread b, some pre process" << std::endl;

std::unique_lock<std::mutex> lock(mtx);
isConditionMeet = true;
cv.notify_one();

cv.wait(lock); // 等待条件满足,即等待线程 A 操作完毕

// 在等待期间,互斥锁会被释放
std::cout << "== Thread B: do somethingB ==" << std::endl;
}

getchar();
return;
}

利用条件变量,维护一个全局的布尔变量,用于控制 A、B 线程的先后运行顺序(先运行 B 线程),然后在需要协同的某个节点开始 wait,等待另一个线程执行完需要的依赖然后 notify。这里其实用两个条件变量更加便于理解且清晰。

# 6、A 线程执行到某个节点后需要等待 B 线程执行到某个节点,然后一起继续执行

B 线程到了相应节点后,notify A 线程,自己则照常继续往下执行即可。
A 线程 wait 等待 B 线程执行到相应的节点然后接收到通知后往下执行。
用到一个 mutex 和一个 condition_variable,可以不需要维护一个全局的布尔变量。

# 三、相关函数

# 1、std::condition_variable

std::condition_variable 是 C++ 标准库中的一种线程同步原语,用于实现线程之间的协作和通信。它通常与互斥锁( std::mutex )一起使用,用于阻塞一个或多个线程,直到另一个线程修改了共享变量(条件)并通知了 condition_variable

想要修改共享变量的线程必须满足以下条件:
1. 获取一个 std::mutex 互斥锁(典型例子是通过 std::lock_guard
2. 当持有锁时修改该共享变量
3. 利用条件变量调用 notify_one 或者 notify_all (可以先释放锁再 notify)

想要等待条件变量的线程必须满足以下条件:
1. 获取用于保护共享变量的 std::unique_lock<std::mutex>
2. 执行以下操作中的任意一个:
检测条件变量是否被 notify
利用条件变量调用 wait 函数(会自动释放锁并挂起线程,直到被 notify 或时间到期或虚假唤醒)
如果条件不满足则继续等待

# a)std::condition_variable::wait

wait 会阻塞当前线程,直到条件变量被 notify(或发生虚假唤醒),有两种重载形式:

1
2
3
4
void wait( std::unique_lock<std::mutex>& lock );

template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate stop_waiting );

当一个条件变量调用 wait 函数时,会自动释放当前已经持有的锁,然后阻塞当前线程,等待条件变量被 notify。当 notify_onenotify_all 被执行后,线程会被解除阻塞。当被解除阻塞后,在 wait 函数退出前,锁会被重新获取

第二种重载形式相较于第一种,多了一个谓词作为条件,其含义为:当条件不满足时,将持续等待。其能够有效避免虚假唤醒。

第二种重载形式等价于:

1
2
3
4
while (!stop_waiting())
{
wait(lock);
}

这意味着:
1. 当第一次调用 wait 时,如果条件为真,不会发生阻塞,不需要 notify 就能继续执行后续代码
2. 一旦阻塞发生,则必须等待 notify 以解除阻塞,光是条件为真不管用
3. 如果被 notify 了,不一定会解除阻塞,需要进一步判断条件是否为真,以防止虚假唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
std::mutex mtx;
std::condition_variable cv;
bool isReady = false;

void ThreadA() {
while(1) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
{
std::lock_guard<std::mutex> lock(mtx);
isReady = true;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return isReady; }); // 等待条件满足
// 在等待期间,互斥锁会被释放
}
}
}

# b)std::condition_variable::wait_for

和 wait 相似,有两个重载版本:

1
2
3
4
5
6
7
8
9
template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time );


template< class Rep, class Period, class Predicate >
bool wait_for( std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time,
Predicate stop_waiting );

与 wait 不同的是, wait_for 允许线程在等待条件一段时间后自动醒来,即使条件尚未满足。线程可以指定一个时间段作为参数,如果在指定时间内条件未满足,线程将自动醒来。

另外,wait_for 具有函数返回值,用于区分是否超时、是否满足条件:
对于第一种重载版本:
返回 std::cv_status::timeout 如果超时了
返回 std::cv_status::no_timeout 如果未超时
对于第二种重载版本:
返回 false 如果超时后条件仍未满足( stop_waiting 仍为 false),否则返回 true

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
std::mutex mtx;
std::condition_variable cv;
bool isReady = false;

void ThreadA() {
while(1) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
{
std::lock_guard<std::mutex> lock(mtx);
isReady = true;
}
cv.notify_one(); // 在上面 lock_guard 自动解锁后,在这里通知线程 B
}
}

void ThreadB() {
while(1) {
{
std::unique_lock<std::mutex> lock(mtx);
auto timeout = std::chrono::seconds(1);
std::cv_status status = cv.wait_for(lock, timeout, [] { return isReady; });
if (status == std::cv_status::no_timeout) {
std::cout << "Main thread: Condition is met." << std::endl;
} else {
std::cout << "Main thread: Timeout occurred." << std::endl;
}
}
}
}

# c)std::condition_variable::notify_one and notify_all

唤醒等待在当前条件变量的任意一个线程或所有线程。

# 2、lock_guard

#CPP 新特性 #CPP11
std::lock_guard 是 C++ 标准库中的一个类,用于管理互斥锁(mutex)的自动加锁和解锁操作。它是 C++11 引入的一部分,旨在简化多线程编程中的锁管理,以防止忘记在离开作用域时解锁互斥锁,从而避免死锁等问题

std::lock_guard 具有以下主要特点和用途:

  1. 自动加锁和解锁:当创建 std::lock_guard 对象时,它会自动锁定关联的互斥锁;当 std::lock_guard 对象离开其作用域时(例如,通过函数返回或作用域结束),它会自动释放锁。
  2. 异常安全std::lock_guard 提供了异常安全性,即使在作用域中抛出异常,也会在作用域结束时正确释放锁,从而防止资源泄漏。
  3. 简化代码:使用 std::lock_guard 可以大大简化管理互斥锁的代码,避免了手动加锁和解锁,减少了程序出错的可能性。

1
2
3
4
5
// 示例
{
std::lock_guard<std::mutex> lock(mtx);
//加锁后的一些操作
}//当离开当前作用域,自动解锁

1
2
3
4
5
6
7
8
9
10
// 工程中实际应用
else
{
{
// 设置tick=0,避免泊车切行车时发送旧的结果
std::lock_guard<std::mutex> lk(m_mutex_latest_fusion_res);
m_latest_fusion_res->set_tick(0);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

需要注意的是, std::lock_guard 通常是一个更安全和方便的替代品,而不是手动使用 std::unique_lockstd::lock 函数。它是一种简单的 RAII(资源获取即初始化)包装,可用于管理互斥锁的生命周期。

参考链接:
std: :lock_guard

# 3、unique_lock

std::unique_lock 是 C++ 标准库中的一个模板类,用于管理互斥锁( std::mutex )的生命周期,提供更大的灵活性和功能,以确保在作用域结束时自动释放锁。 std::unique_lock 也是一种用于 RAII(资源获取即初始化)风格的锁管理。

std::unique_lock 具有以下主要特点和用途:

  1. 自动加锁和解锁:当创建 std::unique_lock 对象时,它可以选择是否自动锁定关联的互斥锁,还可以在其作用域结束时自动解锁互斥锁。这提供了更大的灵活性,因为您可以手动控制锁定和解锁的时机
  2. 异常安全std::unique_lock 提供了异常安全性,即使在作用域中抛出异常,也会在作用域结束时正确释放锁,从而防止资源泄漏。
  3. 支持延迟锁定std::unique_lock 允许您在等待条件变量时解锁互斥锁,然后重新锁定互斥锁,以提高效率和减少锁的占用时间。
  4. 手动锁定和解锁:您可以随时手动锁定和解锁互斥锁,以执行更复杂的操作。

从上面这些特点看来,其实 unique_locklock_guard 颇有相似之处,都是 RAII 风格,都有相关的功能。但相比之下, unique_lock 有更大的灵活性,其可以控制加锁解锁的时机,可以手动加锁解锁,可以延迟锁定。二者的具体差异如下:

  1. 灵活性
    • std::lock_guard 具有较低的灵活性,因为它只提供了自动加锁和解锁的功能。一旦创建,它不允许手动解锁或重新锁定互斥锁。这使得它非常适合那些不需要更复杂锁定操作的简单场景。
    • std::unique_lock 具有更高的灵活性,因为它允许您手动锁定和解锁互斥锁。这意味着您可以选择何时锁定和解锁,还可以执行更复杂的操作,如延迟锁定和递归锁定。
  2. 锁定和解锁
    • std::lock_guard 在构造时自动锁定互斥锁,并在作用域结束时自动解锁。没有手动解锁的选项。
    • std::unique_lock 在构造时可以选择是否锁定互斥锁std::defer_lock ),以及在作用域结束时是否自动解锁( std::adopt_lock )。您还可以随时手动解锁或重新锁定互斥锁( lockunlock )。
  3. 延迟锁定和条件变量
    • std::unique_lock 对于支持条件变量的操作非常有用,因为它可以在等待条件变量时解锁互斥锁wait ),然后重新锁定互斥锁。这允许线程在等待条件时不占用锁,提高了效率。
    • std::lock_guard 不支持这种延迟锁定操作。

具体地,对于 unique_lock ,按照其默认的初始化方式,其在创建时自动加锁,离开作用域时自动解锁:

1
2
3
{
std::unique_lock<std::mutex> lck(mtx);
}

如果需要控制使其在创建时不立即 / 自动加锁,则:

1
2
3
{
std::unique_lock<std::mutex> lck(mtx, std::defer_lock);
}

如果需要控制使其在离开作用域时不立即 / 自动解锁,则:

1
2
3
{
std::unique_lock<std::mutex> lck(mtx, std::adopt_lock);
}

这表明在创建该 unique_lock 时 mtx 已经被上锁了,并且在其作用域结束时不要自动解锁。

参考链接:
unique_lock constructor
lock_tag

# 4、scoped_lock

#CPP 新特性 #CPP17
std::scoped_lock 是 C++17 标准引入的一个 RAII 风格的模板类,用于管理多个互斥锁的生命周期。它是 std::lock 的一种更安全和方便的替代品,用于在一个作用域内同时锁定多个互斥锁,以避免死锁和提供异常安全性。

std::scoped_lock 具有以下主要特点和用途:

  1. 同时锁定多个互斥锁std::scoped_lock 允许您在一个作用域内同时锁定多个互斥锁,而不需要手动编写多次锁定的代码。
  2. 自动解锁:与 std::unique_lock 类似, std::scoped_lock 会在其作用域结束时自动解锁所有已锁定的互斥锁,无论是正常退出还是由于抛出异常而退出。
  3. 避免死锁std::scoped_lock 在锁定多个互斥锁时会采用死锁避免算法,以避免死锁。
  4. 异常安全std::scoped_lock 提供了异常安全性,确保即使在作用域中抛出异常,已锁定的互斥锁也会被正确释放。

std::scoped_lockstd::lock_guard 的主要差异在于前者能够管理多个互斥锁,其余都大同小异。
对于两者的差异,可参考:scoped_lock vs lock_guard

scoped_locklock_guard 的区别?

在 C++17 标准中, std::scoped_lockstd::lock_guard 都是用于管理互斥锁(mutex)的锁类型,它们确保在作用域结束时自动释放锁。然而,它们有一些不同之处,主要体现在它们的设计目的和功能上。

std::scoped_lock

  • 功能: std::scoped_lock 主要用于同时锁定多个互斥锁。它提供了一个锁定多个互斥锁的安全机制,防止死锁的发生。
  • 特性:支持锁定多个互斥锁,并按照指定的顺序锁定,防止死锁。
  • 引入: C++17 标准引入。

std::lock_guard

  • 功能: std::lock_guard 用于在单个互斥锁上实现简单的 RAII(资源获取即初始化)风格的锁定。它在构造时锁定互斥锁,在析构时自动释放锁。
  • 特性:只能锁定一个互斥锁,适用于简单的互斥锁管理。
  • 引入: C++11 标准引入。

具体区别

  1. 锁定多个互斥锁:
  • std::scoped_lock 可以同时锁定多个互斥锁,防止死锁。例如:

    1
    2
    std::mutex m1, m2;
    std::scoped_lock lock(m1, m2);

  • std::lock_guard 只能锁定一个互斥锁。

    1
    2
    std::mutex m;
    std::lock_guard<std::mutex> lock(m);

  1. 用法简洁性:
  • 对于单个互斥锁,两者用法几乎一样,但 std::scoped_lock 在 C++17 中更推荐,因为它的名称更具描述性。
  • 对于多个互斥锁, std::scoped_lock 提供了更简洁的语法。

示例代码对比

  1. 使用 std::scoped_lock :

    1
    2
    3
    4
    5
    [[nodiscard]] size_t get_tasks_queued() const
    {
    const std::scoped_lock tasks_lock(tasks_mutex);
    return tasks.size();
    }

  2. 使用 std::lock_guard :

    1
    2
    3
    4
    5
    [[nodiscard]] size_t get_tasks_queued() const
    {
    const std::lock_guard<std::mutex> tasks_lock(tasks_mutex);
    return tasks.size();
    }

在这个具体的例子中,锁定一个互斥锁时,两者的效果是相同的。选择使用哪一个更多的是风格和一致性的考虑。如果代码库已经在使用 std::scoped_lock 来锁定单个或多个互斥锁,那么继续使用 std::scoped_lock 可能更一致。

结论

  • 单个互斥锁: std::lock_guardstd::scoped_lock 都可以使用。 std::scoped_lock 的名称在语义上可能更明确。
  • 多个互斥锁:使用 std::scoped_lock
  • 代码一致性:根据代码库的风格和惯例来选择。C++17 及以后的代码可能更倾向于使用 std::scoped_lock

总的来说:
使用 std::lock_guard 如果需要在整个作用域中管理一个互斥锁
使用 std::scoped_lock 如果需要在整个作用域中管理多个互斥锁(明确多于一个)
使用 std::unique_lock 如果需要在作用域中解锁互斥锁(包括使用条件变量)

# 5、atomic

std::atomic 是 C++ 标准库提供的一组类型和函数,用于支持原子操作。原子操作是一种多线程编程中的同步机制,确保共享数据的并发访问是安全的,避免数据竞争和并发问题。 std::atomic 提供了一些可以在多线程环境中进行原子操作的基本数据类型,如整数、布尔值等。

std::atomic 具有以下特点:

  1. 原子性std::atomic 操作是原子的,要么完全执行,要么不执行。这意味着它们不会被其他线程中断,也不会导致竞态条件。
  2. 不需要互斥锁std::atomic 操作通常不需要显式的互斥锁,因为它们是原子的。这有助于提高多线程程序的性能。
  3. 内存顺序(Memory Order)std::atomic 操作允许您指定内存顺序,以控制操作的顺序和可见性,以满足程序的需求。

常见的 std::atomic 类型包括 std::atomic<int> , std::atomic<bool> , std::atomic<std::shared_ptr<T>> 等,您可以根据需要选择合适的类型。

在多线程语境下, std::atomic 的常见用法包括:

  1. 实现原子计数器std::atomic 可用于实现线程安全的计数器,如统计某个事件发生的次数。
  2. 管理共享标志位std::atomic<bool> 常用于管理共享标志位,用于控制线程的启动、停止或某个状态的切换。
  3. 无锁数据结构std::atomic 用于创建无锁数据结构,如无锁队列、无锁堆栈,以提高多线程程序的性能。
  4. 原子操作函数std::atomic 类型提供了一系列原子操作函数,如 storeloadexchangecompare_exchange 等,用于执行各种原子操作。
  5. 控制并发访问std::atomic 可以用于确保多线程环境中的共享数据的一致性,以避免竞态条件。

简单来说,对于简单的内置变量,可通过  atomic  来进行多线程操作而不用使用锁。

头文件:

1
#include <atomic>

atomic 是个模板,其对于变量的初始化:

1
2
3
4
5
std::atomic<bool> isReady(false);
std::atomic<int> num(0);

std::atomic<int> sum;
std::atomic_init(&sum, 17);

变量取值:

1
int ret = num.load();

变量设值:

1
2
isReady.store(true);
num.store(19);

TBD,更多细节,参考:
https://www.educative.io/answers/what-is-atomic-type-in-cpp
https://stackoverflow.com/questions/31978324/what-exactly-is-stdatomic(回答 2)

# 四、相关概念

# 1、虚假唤醒

虚假唤醒(Spurious Wakeup)是多线程编程中一个重要的概念,指的是在没有收到明确的通知的情况下,等待中的线程会偶尔自发地从休眠状态醒来。这种情况可能发生在使用条件变量( std::condition_variable )等线程同步机制时。

虚假唤醒有以下关键特点和考虑事项:

  1. 无通知情况下醒来:在条件变量的等待期间,线程可能因为某些系统或实现细节而在没有任何明确通知的情况下醒来。这是一个与多线程编程相关的现象,可能是由操作系统、编译器或硬件的特定行为导致的。
  2. 检查条件的必要性:虚假唤醒的发生意味着等待线程必须谨慎处理醒来的情况。因此,在等待条件满足时,线程应该总是在一个循环中检查条件,而不是假设条件一定已满足。
  3. 条件互斥:通常,虚假唤醒会伴随互斥锁的使用。线程在等待前获取互斥锁,然后在等待期间释放它,以确保其他线程能够访问共享资源。虚假唤醒可能会导致等待线程在检查条件前重新获取互斥锁。
  4. 实现相关:虚假唤醒的频率可能因操作系统或编译器的实现而异。有些操作系统 / 编译器可能更容易发生虚假唤醒,而其他可能较少发生。
# 2、概念辨析

以下概念都与多线程编程和并发相关:

  1. 阻塞(Blocking)
    • 阻塞是指线程暂停其执行,等待某种事件的发生,通常是等待某个条件的满足或资源的可用性。在阻塞状态下,线程不会占用 CPU 时间,直到条件满足或资源可用时才会继续执行。
  2. 休眠(Sleeping)
    • 休眠是阻塞的一种形式,线程在休眠状态下会进入一种低功耗状态,以节省系统资源。通常,线程会在休眠一段时间后自动醒来,或者通过外部事件唤醒。
  3. 挂起(Suspending)
    • 挂起是指将线程的执行暂停,使其暂时不可运行。这可以是手动挂起线程,也可以是由操作系统或调度程序执行。线程在挂起状态下不会占用 CPU 时间,需要显式恢复才能继续执行。
  4. 忙等待(Busy-Waiting)
    • 忙等待是一种线程等待条件满足的方式,它通过不断检查条件的变化来等待,而不是进入休眠状态。忙等待会占用大量的 CPU 时间,通常不是一种高效的等待方式,应该避免在需要长时间等待的情况下使用。
  5. 自旋(Spinning)
    • 自旋是忙等待的一种形式,线程在自旋状态下会重复执行某个操作,通常是检查条件是否满足。自旋通常用于需要极短等待时间的情况,以避免进入和退出休眠状态的开销。

这些概念在多线程编程中有不同的用途和场景。选择适当的等待方式取决于具体的需求和性能要求。通常,阻塞和休眠是较为高效的等待方式,而忙等待和自旋适用于某些特定的情况。挂起通常用于需要手动控制线程生命周期的情况。

# 五、关于 wait 的一些实验

对于带条件的 wait,有以下几个实验:

在下面这个实验中,我们让线程 A 先运行,在线程 B 还未执行 notify 之前,线程 A 就跳过了 wait 并打印相关信息。这里我们让 A 线程中的休眠时间尽可能短,使得一旦线程 B notify,线程 A 马上解除阻塞。

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
std::mutex mtx;
std::condition_variable cv;

bool isGood = true;

void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
cv.notify_one();
std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

上面这个实验表明:当调用 cv.wait(lck, [](){ return isGood; }); 时,如果条件满足,则不会阻塞,不需要被 notify 就能直接执行后续代码。

其实就是验证了前文所说的:

当第一次调用 wait 时,如果条件为真,不会发生阻塞,不需要 notify 就能继续执行后续代码

在下面这个实验中,我们去掉了线程 B 中的 notify 动作,只修改 isGood 但不通知。同时,我们更改了线程 A 的休眠时间,让其在第一次执行之后等待足够长的时间以确保线程 B 已经将 isGood 改为 true。

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
void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::seconds(8));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
// std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
// // cv.notify_one();
// std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::cout << "[ThreadB] 2 set isGood to true and dont notify" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

上面这个实验进一步证明:当我们调用带条件的 wait 时,首先会判断条件是否满足,如果满足,则根本不需要 notify 就能往下走。如果把上面线程 A 的休眠时间改成 50 ms,则会产生截然不同的结果:由于休眠时间很短,线程 A 第一次输出打印信息并将 isGood 改为 false 后,由于线程 B 还来不及更改 isGood 的值,线程 A 再次进入作用域中,此时判断 isGood 条件仍为 false,于是阻塞。后续即使线程 B 中更改了 isGood 的值为 true,但由于没有 notify 函数,无法唤醒,线程 A 将永远阻塞在 wait 函数上。

下面这个实验则是在同一个作用域中多次调用 wait,只要条件满足,不阻塞,不需要 notify。

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
void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;

isGood = true;
cv.wait(lck, [](){ return isGood; });
std::cout << "[ThreadA] after second wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
// std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
// // cv.notify_one();
// std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::cout << "[ThreadB] 2 set isGood to true and dont notify" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

在下面这个实验中,我们在线程 B 中并不修改条件使其为真,但却 notify,模拟虚假唤醒。于是显然,线程 A 仍然阻塞在 wait。

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
void ThreadA() {
while(1+1==2) {
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [](){ return isGood; });

std::cout << "[ThreadA] after wait, ThreadA prints" << std::endl;
isGood = false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} return;
}

void ThreadB() {
//sleep long enough to make sure ThreadA runs first
std::this_thread::sleep_for(std::chrono::seconds(5));
while(1+1==2) {
{
std::lock_guard<std::mutex> _(mtx);
// isGood = true;
std::cout << "[ThreadB] 1 do something in ThreadB" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
std::cout << "[ThreadB] 2 before notify ThreadA" << std::endl;
cv.notify_one();
std::cout << "[ThreadB] 3 after notify ThreadA" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} return;
}

综上,带条件的 wait 完全可以视作:

1
2
3
4
while (!stop_waiting())
{
wait(lock);
}

参考链接:
C++ 多线程原语

the End.


更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/12/C-\345\272\223\345\207\275\346\225\260/index.html" "b/2024/10/12/C-\345\272\223\345\207\275\346\225\260/index.html" index b3d13be..643c9d7 100644 --- "a/2024/10/12/C-\345\272\223\345\207\275\346\225\260/index.html" +++ "b/2024/10/12/C-\345\272\223\345\207\275\346\225\260/index.html" @@ -1 +1 @@ -C++ 库函数 - C++ - 编程 | R1ck's Playground = how

记录一些刷题、学习过程中接触到的一些库函数,后续(应该会)持续更新。

# i、std::function

#CPP 新特性 #CPP11
std::function 是 C++11 标准引入的一个功能强大的模板类,它用于包装和管理可调用对象,包括函数指针、函数对象、Lambda 表达式和成员函数指针等。 std::function 允许你以统一的方式存储和调用不同类型的可调用对象。

以下是 std::function 的一些重要特性和用法:

1、创建 std::function 对象:你可以使用 std::function 模板来创建一个对象,该对象可以存储可调用对象,如函数指针、Lambda 表达式或函数对象。

1
std::function<int(int, int)> add = [](int a, int b) { return a + b; };

在上面的示例中,我们创建了一个 std::function 对象 add ,它可以接受两个整数参数并返回它们的和。

2、调用 std::function 对象:你可以像调用函数一样使用 std::function 对象。

1
int result = add(3, 4); // 调用 add 函数

你可以将 std::function 对象当作函数使用,无论它包装的是哪种可调用对象。

3、类型安全std::function 提供了类型安全的方式来管理可调用对象。你可以在编译时检查是否使用了正确的参数类型。

4、空对象检查:你可以检查 std::function 对象是否包含可调用对象,以避免在未初始化的情况下调用它。

1
2
3
if(add) {
int result = add(3, 4);
}

5、多态性std::function 允许你在运行时切换不同的可调用对象,使代码更加灵活

6、存储成员函数指针:你可以存储成员函数指针,并将对象的指针作为第一个参数传递给成员函数。

1
2
3
4
5
6
7
struct MyClass {
int add(int a, int b) { return a + b; }
};

MyClass obj;
std::function<int(MyClass*, int, int)> memberFunction = &MyClass::add;
int result = memberFunction(&obj, 3, 4);

即绑定对象的 this 指针。相关内容可参考:[[C++ 踩水洼 #1、成员函数指针与普通函数指针不同 | 成员函数指针与普通函数指针的差异]]

7、函数参数绑定:你可以使用 std::bind 或 Lambda 表达式将参数绑定到 std::function 对象上,以创建可重用的部分函数。

1
2
std::function<int(int)> addThree = std::bind(add, std::placeholders::_1, 3);
int result = addThree(4); // 结果为 7

std::function 是一种非常有用的工具,特别适用于需要动态调用不同函数或需要将函数作为参数传递的情况。它提供了更高级别的抽象,使得 C++ 中的函数对象和回调更加灵活和可维护。

# i、std::bind

std::bind 是 C++11 引入的函数对象(Function Object)库的一部分,用于创建函数对象(或者叫绑定对象),它可以用来将函数或成员函数与一组参数绑定在一起,创建一个新的可调用对象。 std::bind 提供了一种非常强大的机制,允许你灵活地构建新的函数对象,包括改变参数的顺序、固定某些参数的值,以及创建函数适配器。

std::bind  可以看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表

1
auto newCallable = bind(callable, arg_list);

该形式表达的意思是:当调用 newCallable 时,会调用 callable,并传给它  arg_list  中的参数。

预绑定的参数是以值传递的形式,不预绑定的参数是以引用传递的形式。

后者要用  std::placeholders  (占位符)的形式占位,从  _1  开始,依次递增。

1
2
3
4
if (!m_engine->GetComponent("DDSSimulator")) {
std::thread period_send_thread(std::bind(&DDSOutput::SendFusionPeriodThread, this));
period_send_thread.detach();
}

以下是 std::bind 的一些主要用法和特性:

1、绑定全局函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <functional>
#include <iostream>

void greet(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}

int main() {
auto sayHello = std::bind(greet, "John");
sayHello(); // 输出: Hello, John!
return 0;
}

2、绑定成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <functional>
#include <iostream>

class Greeter {
public:
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
};

int main() {
Greeter greeter;
auto greetFunction = std::bind(&Greeter::greet, &greeter, std::placeholders::_1);
greetFunction("Alice"); // 输出: Hello, Alice!
return 0;
}

3、参数绑定和重排序

1
2
3
4
5
6
7
8
9
10
11
12
#include <functional>
#include <iostream>

void printThreeArgs(int a, int b, int c) {
std::cout << a << ", " << b << ", " << c << std::endl;
}

int main() {
auto printArgs = std::bind(printThreeArgs, std::placeholders::_3, 100, std::placeholders::_1);
printArgs(1, 2, 3); // 输出: 3, 100, 1
return 0;
}

4、创建函数适配器std::bind 允许你创建函数适配器,用于改变函数的行为,比如取反或者应用一元或二元函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <functional>
#include <iostream>

bool isEven(int num) {
return num % 2 == 0;
}

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto isOdd = std::not1(std::bind(isEven, std::placeholders::_1));
// 这里的 std::not1 用于将谓词函数的结果取反,即 true 变为 false,false 变为 true

for (int num : numbers) {
if (isOdd(num)) {
std::cout << num << " is odd." << std::endl;
}
} return 0;
}

5、绑定到引用参数std::bind 还可以用于将参数绑定到引用参数,以允许在调用时改变参数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <functional>
#include <iostream>

void addOne(int& value) {
value += 1;
}

int main() {
int number = 42;
auto increment = std::bind(addOne, std::ref(number));
increment();
std::cout << "Number after increment: " << number << std::endl;
// 输出: Number after increment: 43
return 0;
}

注意,这里的 std::ref 的作用是创建对对象的引用。
从这个例子中我们可以更好地理解 bind 这个函数,它其实就是个包装器,浅封装了另一个可调用对象及其参数。那么,在封装这些参数时就可以指定他们是值绑定还是引用绑定。
默认情况下,bind 传递参数是值绑定,用  std::placeholders  占位符进行占位的参数是引用绑定,即我们前面所提到的:预绑定的参数是以值传递的形式,不预绑定的参数是以引用传递的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void addOne(int& value) {
value += 1;
}

auto increment = std::bind(addOne, std::ref(number));
//等价于
void increment(int& val){
addOne(val);
}

//而如果我们去掉了 std::ref,则
auto increment = std::bind(addOne, number);
//相当于
void increment(int val){
addOne(val);
}

//也就是说,虽然我们底层调用的 addOne 函数是值传递的参数,但是其外部包装的函数入参可以进一步控制

std::bind_frontstd::bind_back 在 bind 的基础上绑定前几个参数或后几个参数:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <functional>

void foo(int a, int b, int c) {
std::cout << a << " " << b << " " << c << std::endl;
}

int main() {
auto bound = std::bind_front(foo, 42, 10);
bound(5); // 输出: 42 10 5
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <functional>

void bar(int a, int b, int c) {
std::cout << a << " " << b << " " << c << std::endl;
}

int main() {
auto bound = std::bind_back(bar, 5, 10);
bound(42); // 输出: 42 5 10
return 0;
}

# i、std::ref

std::ref 是一个标准库函数模板,用于创建一个可以传递引用的包装对象。这在需要将对象的引用传递给其他函数或线程时非常有用,尤其是在标准库函数不支持直接传递引用的情况下(例如, std::thread 构造函数期望的是传值语义)。

std::ref 创建一个 std::reference_wrapper 对象,这个对象持有对传入对象的引用,并且可以像普通引用一样使用。 std::reference_wrapper 可以通过 operator T&() 进行隐式转换,允许引用在需要值的地方被使用。

具体使用场景:

  1. 线程参数传递:当我们使用 std::thread 时,参数通常是按值传递的。如果需要传递引用,就可以使用 std::ref
  2. 函数调用:在标准库中,某些函数接受函数对象(如函数、lambda、或 std::function )时,若需要传递引用,可以使用 std::ref
  3. 容器中存储引用:STL 容器通常存储值,如果需要存储引用,可以使用 std::reference_wrapper

线程参数传递示例:

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
#include <iostream>
#include <thread>
#include <future>
#include <functional> // for std::ref

void addTwoValue(std::promise<int>& prom, int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2));
int result = a + b;
prom.set_value(result);
}

int main() {
std::promise<int> prom;

// 使用 std::ref 来传递引用
std::thread task(addTwoValue, std::ref(prom), 5, 3);
std::cout << "waiting for thread to generate result" << std::endl;

std::future<int> fu = prom.get_future();
int result = fu.get();
std::cout << "getting result " << result << std::endl;

task.join();

return 0;
}

函数调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <functional>

void printValue(int& x) {
std::cout << "Value: " << x << std::endl;
}

int main() {
int a = 10;

// 直接调用
printValue(a);

// 使用 std::ref 传递引用
std::function<void()> func = std::bind(printValue, std::ref(a));
func();

return 0;
}

容器中存储引用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
#include <functional>

int main() {
int a = 10;
int b = 20;

// 创建一个引用包装器的容器
std::vector<std::reference_wrapper<int>> ref_container;
ref_container.push_back(a);
ref_container.push_back(b);

// 修改原变量的值
a = 30;
b = 40;

// 通过引用包装器访问修改后的值
for (auto& ref : ref_container) {
std::cout << ref << std::endl;
}

return 0;
}

std::ref 在需要传递引用但接口不支持的场景下非常有用,特别是在多线程编程中传递引用参数时。通过使用 std::ref 可以避免不必要的拷贝,保持对原对象的引用,从而提高效率和灵活性。

# i、std::pair

std::pair 是 C++ 标准库中的一个模板类,用于将两个值组合在一起,形成一个有序对。 std::pair 提供了一种简单的方式来将两个不同类型的值打包,方便传递和返回多个值。 std::pair 的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <class T1, class T2>
struct pair {
T1 first;
T2 second;

// 构造函数
pair(const T1& x, const T2& y) : first(x), second(y) {}

// 默认构造函数
pair() : first(), second() {}

// 拷贝构造函数
template<class U1, class U2>
pair(const pair<U1, U2>& p) : first(p.first), second(p.second) {}

// 移动构造函数
template<class U1, class U2>
pair(pair<U1, U2>&& p) : first(std::forward<U1>(p.first)), second(std::forward<U2>(p.second)) {}

// 其他成员函数...
};

std::pair 的主要特点包括:

  • firstsecond 成员变量,用于存储两个值。
  • 构造函数,允许通过传递参数进行初始化。
  • 默认构造函数,创建一个默认构造的 pair 对象。
  • 拷贝构造函数,用于从其他 pair 对象进行拷贝构造。
  • 移动构造函数,用于从其他 pair 对象进行移动构造。

例如,使用 std::pair 来创建一个包含整数和浮点数的对象:

1
2
3
4
5
6
7
8
#include <iostream>
#include <utility>

int main() {
std::pair<int, double> myPair(42, 3.14);
std::cout << "Pair: " << myPair.first << ", " << myPair.second << std::endl;
return 0;
}

我们定义的 pair 模板类会有默认构造,其会调用两个成员变量的默认构造。

# i、std::make_pair

std::make_pair 是一个模板函数,用于创建 std::pair 对象。它接受两个参数,并根据这两个参数的类型自动推导出 pair 的模板参数类型。 std::make_pair 的声明如下:

1
2
3
template<class T1, class T2>
constexpr std::pair<typename std::decay<T1>::type, typename std::decay<T2>::type>
make_pair(T1&& t, T2&& u);

其中:

  • T1T2std::pair 的两个模板参数类型。
  • std::decay 是一个模板元函数,用于移除类型的引用和 cv 修饰符,确保类型是纯净的。
  • T1&& tT2&& ustd::make_pair 函数的参数,使用右值引用,允许传递左值或右值。

std::make_pair 的作用是根据传入的参数创建一个 std::pair 对象。通过使用 std::make_pair ,我们可以避免显式指定模板参数类型,使代码更简洁。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <utility>

int main() {
int x = 42;
double y = 3.14;

auto myPair = std::make_pair(x, y);

std::cout << "Pair: " << myPair.first << ", " << myPair.second << std::endl;

return 0;
}

这里, std::make_pair(x, y)xy 包装成一个 std::pair<int, double> 对象,并自动推导出 pair 的模板参数类型。

这里要注意的是,make_pair 在传入两个变量时,会调用传入参数的拷贝构造函数(当然,上面这个例子里面 int 和 double 这种基本类型就没这种说法了)。

1
2
3
4
//假设我们有个类型 A
A a1(5);
A a2(7);
std::make_pair(a1, a2); //这里会发生两次 A 的拷贝构造

核心思想:高效快速判断元素是否存在。

C++ STL(标准模板库)提供了一个二分查找算法,即 std::binary_search 函数。这个函数用于在已排序的序列中查找特定元素

函数签名如下:

1
2
template<class ForwardIt, class T>
bool binary_search(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast 是表示序列范围的迭代器,指定了要进行查找的区间。
  • value 是要查找的目标值。

std::binary_search 函数返回一个布尔值,表示是否找到了目标值。如果找到了,返回 true ;如果未找到,返回 false

这个函数使用二分查找算法,在已排序的序列中进行查找。它会在指定的范围内进行迭代,并根据当前位置的值与目标值的比较结果来决定继续查找的方向。

使用 std::binary_search 函数的前提是,序列必须是已排序的。如果序列未排序,结果是不可预测的。如果需要在未排序的序列中进行查找,可以先使用 std::sort 函数对序列进行排序,然后再使用 std::binary_search 进行查找。

下面是一个使用 std::binary_search 函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int target = 7;

// 使用 std::binary_search 查找目标值
bool found = std::binary_search(numbers.begin(), numbers.end(), target);

if (found) {
std::cout << "Target found in the sequence." << std::endl;
} else {
std::cout << "Target not found in the sequence." << std::endl;
}

return 0;
}

这个示例中,我们创建了一个已排序的整数序列,并使用 std::binary_search 函数查找目标值 7。如果找到了,输出 "Target found in the sequence.";如果未找到,输出 "Target not found in the sequence."。

请注意,对于重复的元素, std::binary_search 只能确定序列中是否存在目标值,而不能确定目标值的具体位置。如果需要确定目标值的位置,可以使用 std::lower_bound 函数或 std::upper_bound 函数。

尽管 std::binary_search 无法提供目标值的具体位置,但它在许多情况下可以提供一个高效的方法来检查存在性,以便根据需要采取进一步的操作。

# i、lower_bound && upper_bound

当我们在已排序的序列中进行查找时,除了使用 std::binary_search 函数之外,还可以使用 std::lower_boundstd::upper_bound 函数来获取目标值在序列中的位置。

std::lower_bound 函数用于在已排序的序列中找到第一个不小于目标值的元素的位置,函数签名如下:

1
2
template<class ForwardIt, class T>
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast 是表示序列范围的迭代器,指定了要进行查找的区间。
  • value 是要查找的目标值。

std::lower_bound 函数返回一个迭代器,指向序列中第一个不小于目标值的元素。如果序列中不存在不小于目标值的元素,返回的迭代器将指向序列的末尾。

std::upper_bound 函数用于在已排序的序列中找到第一个大于目标值的元素的位置,函数签名如下:

1
2
template<class ForwardIt, class T>
ForwardIt upper_bound(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast 是表示序列范围的迭代器,指定了要进行查找的区间。
  • value 是要查找的目标值。

std::upper_bound 函数返回一个迭代器,指向序列中第一个大于目标值的元素。如果序列中不存在大于目标值的元素,返回的迭代器将指向序列的末尾。

下面是一个使用 std::lower_boundstd::upper_bound 函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 5, 7, 8, 9, 10};
int target = 5;

// 使用 std::lower_bound 查找不小于目标值的第一个元素
auto lower = std::lower_bound(numbers.begin(), numbers.end(), target);

// 使用 std::upper_bound 查找大于目标值的第一个元素
auto upper = std::upper_bound(numbers.begin(), numbers.end(), target);

// 输出查找结果
std::cout << "Lower bound: " << std::distance(numbers.begin(), lower) << std::endl;
std::cout << "Upper bound: " << std::distance(numbers.begin(), upper) << std::endl;

return 0;
}

这个示例中,我们创建了一个已排序的整数序列,并使用 std::lower_bound 函数查找不小于目标值 5 的第一个元素,以及使用 std::upper_bound 函数查找大于目标值 5 的第一个元素。我们通过 std::distance 函数计算迭代器与序列起始位置的距离,从而得到目标值在序列中的位置。

请注意, std::lower_boundstd::upper_bound 函数的时间复杂度为 O (log N),其中 N 是序列中的元素数量。这两个函数在查找范围较大的有序序列时具有较高的效率。

1
2
Lower bound: 4
Upper bound: 6

upper_bound 并不能确定某个 target 是否存在数组中,而只能判断是否存在大于 target 的数字。

std::upper_bound 函数返回的是第一个大于目标值的元素的迭代器,而不是判断目标值是否存在于数组中。如果目标值存在于数组中,它将返回大于目标值的第一个元素的迭代器;如果目标值在数组中不存在,它将返回大于目标值的最小元素的迭代器,即序列的上界。

如果你想要确定目标值是否存在于数组中,可以将 std::upper_bound 返回的迭代器与目标值进行比较,如果它指向数组的起始位置或者它的前一个元素与目标值相等,则说明目标值存在于数组中。

# i、partial_sum

在 C++ 标准库中,可以使用 std::partial_sum 函数来求一个序列的前缀和(partial sum)。 std::partial_sum 函数是一个算法,它对输入范围内的元素进行部分求和,并将结果存储在输出序列中。

以下是 std::partial_sum 函数的原型:

1
2
template <class InputIterator, class OutputIterator, class BinaryOperation>
OutputIterator partial_sum(InputIterator first, InputIterator last, OutputIterator result, BinaryOperation binary_op);

其中, InputIteratorOutputIterator 是模板参数,它们分别表示输入序列和输出序列的迭代器类型。 BinaryOperation 是一个函数对象(或函数指针),用于指定计算前缀和的二元操作。 firstlast 分别表示输入序列的起始和结束位置(左闭右开区间)。 result 表示输出序列的起始位置。

std::partial_sum 函数的功能是将位于 [first, last) 范围内的元素从输入序列复制到输出序列,并依次计算部分和。它将结果存储在输出序列中,并返回指向输出序列中最后一个复制元素之后位置的迭代器。

默认情况下, std::partial_sum 使用加法 + 运算符进行前缀和计算。如果要使用其他的二元操作,可以通过传递自定义的函数对象或函数指针 binary_op 来实现。

以下是使用 std::partial_sum 函数求前缀和的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <algorithm>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> prefixSum(numbers.size());

// 使用 std::partial_sum 求前缀和,并直接写入到输出容器
std::partial_sum(numbers.begin(), numbers.end(), prefixSum.begin());

// 打印前缀和序列
for (const auto& num : prefixSum) {
std::cout << num << " ";
} std::cout << std::endl;

return 0;
}

在上面的示例中,我们通过 std::partial_sum 函数计算 numbers 中元素的前缀和,并将结果直接写入到 prefixSum 容器中。在这里,我们在调用 std::partial_sum 时,将 prefixSum.begin() 作为输出序列的起始位置,这样 partial_sum 会将计算得到的前缀和直接写入到 prefixSum 容器中。

# i、priority_queue

使用 priority_queue 需要包含头文件 <queue> ,默认情况下, priority_queue 会按照元素类型的比较函数(默认为 less ,即降序排列)来维护元素的优先级。如果需要按照其他方式来定义优先级(例如升序排列),可以使用自定义的比较函数。

其基本操作同 queue,即 push、top、pop,只是多了排序的特性。(但 pq 只保证顶部元素是有序的,不保证整个队列是有序的。因为它的本质是大小堆,只能保证当前节点比左右子节点大 / 小,而每一次进行 push 或者 pop 进行元素的增删时,都会使得节点进行交换位置,可能打乱顺序。)

示例代码:

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
36
37
38
#include <iostream>
#include <queue>

int main() {
// 默认情况下,创建一个降序的优先级队列
std::priority_queue<int> pq;

// 插入元素
pq.push(10);
pq.push(30);
pq.push(20);

// 访问和删除元素
while (!pq.empty()) {
std::cout << pq.top() << " "; // 访问最大元素
pq.pop(); // 移除最大元素
}

std::cout << std::endl;

// 自定义比较函数,创建一个升序的优先级队列
std::priority_queue<int, std::vector<int>, std::greater<int>> pq_custom;

// 插入元素
pq_custom.push(50);
pq_custom.push(10);
pq_custom.push(30);

// 访问和删除元素
while (!pq_custom.empty()) {
std::cout << pq_custom.top() << " "; // 访问最小元素
pq_custom.pop(); // 移除最小元素
}

std::cout << std::endl;

return 0;
}

# i、accumulate

std::accumulate 是 C++ 标准库中的一个算法,用于对给定范围内的元素进行累积操作。它定义在头文件 <numeric> 中,接口定义如下:

1
2
template <class InputIterator, class T>
T accumulate (InputIterator first, InputIterator last, T init);

  • InputIterator first :表示范围的起始迭代器,指向要累积的元素的第一个元素。
  • InputIterator last :表示范围的结束迭代器,指向要累积的元素的最后一个元素的下一个位置(即不包含在范围内)。
  • T init :表示累积的初始值。

accumulate 函数会从 firstlast 对范围内的元素进行累积操作,将每个元素依次加到 init 上,然后返回最终的累积结果。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>
#include <numeric> // 包含 accumulate 函数

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 使用 accumulate 对 numbers 中的元素进行累积操作,初始值为0
int sum = std::accumulate(numbers.begin(), numbers.end(), 0);

std::cout << "Sum of elements: " << sum << std::endl;

return 0;
}

# i、minmax_element

std::minmax_element 是 C++ 标准库中的一个算法函数,用于在给定范围内查找最小值和最大值的元素,并返回指向这两个元素的迭代器。

以下是 std::minmax_element 函数的原型:

1
2
template <class ForwardIterator>
std::pair<ForwardIterator, ForwardIterator> minmax_element(ForwardIterator first, ForwardIterator last);

ForwardIterator 是模板参数,它表示输入序列的迭代器类型。 firstlast 分别表示输入序列的起始和结束位置(左闭右开区间)。

std::minmax_element 函数的功能是在范围 [first, last) 内查找最小值和最大值的元素,并返回一个 std::pair 对象,其中 first 成员是指向最小值元素的迭代器, second 成员是指向最大值元素的迭代器。

以下是使用 std::minmax_element 函数查找最小值和最大值的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <algorithm>
#include <vector>

int main() {
std::vector<int> numbers = {5, 2, 8, 3, 1, 9, 4, 7, 6};

// 使用 std::minmax_element 查找最小值和最大值
auto minmaxPair = std::minmax_element(numbers.begin(), numbers.end());

// 输出结果
std::cout << "最小值: " << *minmaxPair.first << std::endl;
std::cout << "最大值: " << *minmaxPair.second << std::endl;

return 0;
}

在上面的示例中,我们使用 std::minmax_element 函数在 numbers 中查找最小值和最大值,并将结果存储在 minmaxPair 中。然后通过 minmaxPair.firstminmaxPair.second 分别访问最小值和最大值的元素。

# i、sort

sort 虽然很容易,但是老是忘记一些细节:
sort 默认是从小到大排序,sort 第三个参数为可选参数,可用于传入比较函数。
STL 提供了两个内置的比较函数 less 和 greater,具体使用如下:

1
2
std::sort(nums.begin(), nums.end(), std::less<int>());    //从小到大
std::sort(nums.begin(), nums.end(), std::greater<int>()); //从大到小

当然,第三个参数也可以是自定义函数,比如传入 lambda 表达式。

# i、remove

std::remove 从指定范围内删除指定的值。

它是 C++ 标准库中的一个算法函数,用于从序列中删除指定的元素。它的主要作用是将需要删除的元素移到序列的末尾,然后返回一个指向新范围结尾之后位置的迭代器。它并不会真正地删除元素,而是通过返回新的范围来使得这些元素 “看起来” 被删除了。

其声明如下:

1
2
template <class ForwardIt, class T>
ForwardIt remove(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast :表示要操作的范围, [first, last) 为一个半开区间,包含要进行操作的元素
  • value :要删除的特定值。

返回值是一个迭代器,指向新范围结尾之后的位置。这个迭代器可以用来调整容器的大小,实现元素的 “删除”。

以下是一个使用 std::remove 的示例代码,移除容器中的特定元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 3, 5, 6};

// 使用 std::remove 将所有值为 3 的元素移到末尾
auto newEnd = std::remove(numbers.begin(), numbers.end(), 3);

// 调整容器大小,删除被移到末尾的元素
numbers.erase(newEnd, numbers.end());

// 输出删除元素后的容器
for (const int& num : numbers) {
std::cout << num << " ";
} return 0;
}

# i、erase

在 C++ 中, erase 是用于容器的成员函数,主要用于删除容器中的元素。它在不同类型的容器(如 std::vectorstd::liststd::map 等)中都有不同的用法,但其基本作用是从容器中删除一个或多个元素。以下是关于 erase 函数的一些常见用法:

  1. 删除单个元素:

    • 对于 std::vectorstd::list 等序列容器,可以使用迭代器来删除指定位置的元素。
      1
      2
      3
      std::vector<int> numbers = {1, 2, 3, 4, 5};
      auto it = numbers.begin() + 2; // 删除第三个元素
      numbers.erase(it);
  2. 删除范围内的元素:

    • erase 数可以删除指定范围内的元素。你需要提供一个表示范围的起始迭代器和结束迭代器。
      1
      2
      3
      4
      5
      std::list<int> myList = {1, 2, 3, 4, 5};
      auto start = myList.begin();
      auto end = myList.begin();
      std::advance(end, 3); //迭代器向前移动3个位置——注意这里std::list是双向链表
      myList.erase(start, end);
  3. 删除满足条件的元素:

    • 你可以使用 std::remove_if 算法与 erase 函数一起删除满足某个条件的元素。通常,这在容器中删除特定值或满足某个条件的元素时非常有用。(关于 remove_if 更多细节,见下文)
      1
      2
      std::vector<int> numbers = {1, 2, 3, 4, 5};
      numbers.erase(std::remove_if(numbers.begin(), numbers.end(), [](int x) { return x % 2 == 0; }), numbers.end());
  4. 删除所有元素:

    • 使用 clear 函数可以快速清空容器中的所有元素。
      1
      2
      std::vector<int> numbers = {1, 2, 3, 4, 5};
      numbers.clear(); // 删除所有元素,numbers变为空

需要注意的是, erase 函数可能会导致迭代器失效,因此在使用 erase 后,应谨慎操作迭代器。确保在删除元素后不再使用已失效的迭代器。此外,不同类型的容器可能有不同的参数和用法,因此使用前请查阅相关文档。工程实践中的具体例子可参考:[[SDK 相机断开重连偶现 crash]]

# i、remove_if

和上面的 remove 基本一致,只不过第三个参数由指定某个元素变成了指定某个条件。

std::remove_if 是 C++ 标准库中的一个算法,用于从容器中删除满足特定条件的元素,但它并不会真正地删除元素,而是将这些元素移到容器的末尾,并返回一个指向新的 “逻辑末尾” 位置的迭代器。

函数签名:

1
2
template <class ForwardIt, class UnaryPredicate>
ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p);

参数:
firstlast 是表示范围的迭代器,它们定义了要进行操作的元素的范围。
p 是一个一元谓词(UnaryPredicate),它定义了要删除的条件。

操作:
std::remove_if 会遍历范围 [first, last) 中的元素,对于满足谓词 p 的元素,它会将它们移到范围的末尾,不满足条件的元素保持原位置。这就是为什么它不会真正删除元素,而只是重新排列它们。

返回值:
std::remove_if 返回一个指向新的 “逻辑末尾” 的迭代器,该迭代器之前的元素仍然保持不变,而迭代器之后的元素是被移动的元素,但它们的值是未定义的。这里的逻辑和 remove 函数一样。

由于 remove 之后返回的是新的逻辑尾迭代器,因此可以结合 erase 来使用,形式为 erase(remove_if(...), end)
示例:

1
2
3
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
numbers.erase(std::remove_if(numbers.begin(), numbers.end(), [](int x) { return x % 2 == 0; }), numbers.end());
// 在上述示例中,删除了所有偶数,numbers 现在包含 {1, 3, 5}。

# i、for_each

C++ 中的 for_each 函数是标准库 <algorithm> 头文件中的一个算法,用于对容器中的元素执行某个操作。它通常与函数对象(也称为谓词)一起使用,允许你在容器的每个元素上执行相同的操作。 for_each 函数的一般形式如下:

1
2
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);

  • InputIterator 是一个迭代器,指定了要遍历的容器范围的起始和结束位置。
  • Function 是一个函数对象或谓词,用于定义要在容器的每个元素上执行的操作。
  • firstlast 参数指定了要遍历的范围,通常是容器的起始和结束迭代器。

for_each 函数的主要作用是对容器中的每个元素执行函数对象 f ,即对范围 [first, last) 中的每个元素调用函数对象 f 。这可以用于遍历容器并执行特定的操作,如打印元素、修改元素等。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <algorithm>

void print_element(int element) {
std::cout << element << " ";
}

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 使用for_each函数调用print_element函数打印向量中的元素
std::for_each(numbers.begin(), numbers.end(), print_element);

std::cout << std::endl;

return 0;
}

# i、copy

std::copy 是 C++ 标准库中的一个算法函数,它用于将一个范围内的元素复制到另一个范围中。其基本用法如下:

1
2
3
#include <algorithm>
// ...
std::copy(first1, last1, first2);

  • first1last1 定义了源范围,表示要复制的元素范围是 [first1, last1) (左闭右开区间)。
  • first2 定义了目标范围,表示要将元素复制到的范围的起始位置。

在使用这个函数时,你需要确保源范围 [first1, last1) 和目标范围 [first2, ...) 之间有足够的空间来容纳要复制的元素。

这里假设 ii_iq 的大小足够大,以至于能够容纳 iiDataiqData 中的所有元素。如果 ii_iq 的大小不够,将导致未定义行为,可能引发程序崩溃或产生其他不确定的结果。

# i、replace

std::replace 是 C++ 标准库中的算法之一,用于在指定范围内替换元素的值。它的声明如下:

1
2
template< class ForwardIt, class T >
void replace( ForwardIt first, ForwardIt last, const T& old_value, const T& new_value );

这个算法在指定范围 [first, last) 内搜索所有等于 old_value 的元素,并将它们替换为 new_value

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <algorithm>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};
// 使用 std::replace 将所有值为 2 的元素替换为 0
std::replace(numbers.begin(), numbers.end(), 2, 0);
// 打印替换后的向量
for (int num : numbers) {
std::cout << num << " ";
} return 0;
}

由于是模板函数,所以自然也可以在字符串中使用:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>
#include <algorithm>

int main() {
std::string fileName = "example.file.txt";

// 将所有的 '.' 替换为 '_'
std::replace(fileName.begin(), fileName.end(), '.', '_');

return 0;
}

另外,string 也内置了 replace 函数:(我们的原则是,类内有定义,优先使用类内函数)

1
2
//将字符串 myStr 的末尾 `.bin` 更换成 `.txt`
myStr.replace(myStr.end() - 4, myStr.end(), ".txt");

# i、rfind

在 C++ 中, rfind 是字符串类(如 std::string )的成员函数之一,用于在字符串中从后往前查找指定子字符串(或字符)的位置。它的基本用法如下:

1
size_t rfind(const std::string& str, size_t pos = npos) const;

  • str :要查找的子字符串。
  • pos :可选参数,指定开始查找的位置,默认值是 std::string::npos ,表示从字符串的末尾开始向前查找。

rfind 函数返回指定子字符串在原字符串中最后出现的位置。如果未找到子字符串,它将返回 std::string::npos ,通常表示未找到。

以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>

int main() {
std::string str = "Hello, world! Hello, C++!";
size_t found = str.rfind("Hello");

if (found != std::string::npos) {
std::cout << "Last occurrence of 'Hello' found at position " << found << std::endl;
} else {
std::cout << "'Hello' not found in the string." << std::endl;
}

return 0;
}

在这个示例中, rfind 函数查找字符串中最后出现的 "Hello",并返回其位置。如果找到,它会输出该位置,否则会提示未找到。

注意,这是从字符串开始的偏移量,而不是结束的偏移量。

参考链接:https://en.cppreference.com/w/cpp/string/basic_string/rfind

# i、hardware_concurrency

hardware_concurrency 是 C++ 标准库中的一个函数,属于 <thread> 头文件的一部分。它的主要作用是返回能够并发执行的(物理)线程数量。这通常反映了系统上的处理器核心数量。

下面是一个简单的使用例子:

1
2
3
4
5
6
7
8
#include <iostream>
#include <thread>

int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << "This system can run " << n << " concurrent threads." << std::endl;
return 0;
}

当调用 std::thread::hardware_concurrency() 时,它会返回一个 unsigned int 值,该值表示系统上可以并发运行的最大线程数量。如果无法确定这个数量,它可能返回 0。

需要注意的是,返回的值只是一个建议值,不是硬性规定,具体的并发能力还受到操作系统的线程调度和其他资源限制的影响。

# i、sscanf

1
int sscanf(const char *str, const char *format, ...);

sscanf 用于从字符串中以指定格式读取信息,可视作 scanf 的重定向版本,源头由标准输入转变为字符串。

sscanf() reads its input from the character string pointed to  by str.

例如:

1
2
uint64_t utc_ts, utc_tns, gtc_ts, gtc_tns, data_size, offset;
sscanf(line.c_str(), "%lu %lu %s %lu %lu %lu %lu", &utc_ts, &utc_tns, topic, &data_size, &offset, &gtc_ts, &gtc_tns);

# i、emplace

模板函数,作用是在容器中的 position 位置用 args 这一包数据(可变参数)来原地构造一个元素。

1
template <class... Args>iterator emplace(const_iterator position, Args&&... args);

可以将其视作更优雅的 push_back、insert 方法,举例如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class People {
public:
People()=default;

People(int n, double j) : m_a(n), m_b(j) {
cout << "def constructor" << endl;
}

People(const People &ppl) : m_a(ppl.m_a), m_b(ppl.m_b) {
cout << "cp constructor" << endl;
}

People(const People && ppl) : m_a(ppl.m_a), m_b(ppl.m_b) {
cout << "mv constructor" << endl;
}

People& operator=(const People& ppl) {
cout << "cp assign" << endl;
m_a = ppl.m_a;
m_b = ppl.m_b;
return *this;
}

private:
int m_a;
double m_b;
};

cout << "----------insert func----------" << endl;
vector<People> pplVec1;
pplVec1.insert(pplVec1.end(), People(1, 17.1)); //构造一个临时变量

cout << "----------insert func----------" << endl;
vector<People> pplVec2;
pplVec2.insert(pplVec2.end(), {2, 17.2}); //构造一个临时变量

cout << "----------emplace func----------" << endl;
vector<People> pplVec3;
pplVec3.emplace(pplVec3.end(), 3, 8.2); //无需构造临时变量

输出:
----------insert func----------
def constructor
mv constructor
----------insert func----------
def constructor
mv constructor
----------emplace func----------
def constructor

通过输出信息可知,emplace 少调用了一次拷贝构造。(有移动构造时会用移动构造代替拷贝构造)

注意,当拷贝构造函数和移动构造函数同时存在时,会优先调用移动构造函数。即,emplace 就地构造,省去一次临时变量的创建。其底层实现原理是 可变参数模板 (即...args)和 完美转发 ,这里不展开。 std::vector::emplace_back()  则是另一个显式的函数,不过是在尾部插入。

工作中实际应用场景:

1
2
3
4
5
6
7
8
9
10
template <class FrameVectorType>
typename FrameVectorType::iterator EmplaceMea(FrameVectorType &frame_vector)
{
//FRAMEWORK::NW("DO NOT forget to lock using board mutex");
BeginLock();
typename FrameVectorType::iterator it = frame_vector.emplace_back_itor_noblock();
EndLock();
it->clear();
return it;
}

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 库函数 - C++ - 编程 | R1ck's Playground = how

记录一些刷题、学习过程中接触到的一些库函数,后续(应该会)持续更新。

# i、std::function

#CPP 新特性 #CPP11
std::function 是 C++11 标准引入的一个功能强大的模板类,它用于包装和管理可调用对象,包括函数指针、函数对象、Lambda 表达式和成员函数指针等。 std::function 允许你以统一的方式存储和调用不同类型的可调用对象。

以下是 std::function 的一些重要特性和用法:

1、创建 std::function 对象:你可以使用 std::function 模板来创建一个对象,该对象可以存储可调用对象,如函数指针、Lambda 表达式或函数对象。

1
std::function<int(int, int)> add = [](int a, int b) { return a + b; };

在上面的示例中,我们创建了一个 std::function 对象 add ,它可以接受两个整数参数并返回它们的和。

2、调用 std::function 对象:你可以像调用函数一样使用 std::function 对象。

1
int result = add(3, 4); // 调用 add 函数

你可以将 std::function 对象当作函数使用,无论它包装的是哪种可调用对象。

3、类型安全std::function 提供了类型安全的方式来管理可调用对象。你可以在编译时检查是否使用了正确的参数类型。

4、空对象检查:你可以检查 std::function 对象是否包含可调用对象,以避免在未初始化的情况下调用它。

1
2
3
if(add) {
int result = add(3, 4);
}

5、多态性std::function 允许你在运行时切换不同的可调用对象,使代码更加灵活

6、存储成员函数指针:你可以存储成员函数指针,并将对象的指针作为第一个参数传递给成员函数。

1
2
3
4
5
6
7
struct MyClass {
int add(int a, int b) { return a + b; }
};

MyClass obj;
std::function<int(MyClass*, int, int)> memberFunction = &MyClass::add;
int result = memberFunction(&obj, 3, 4);

即绑定对象的 this 指针。相关内容可参考:[[C++ 踩水洼 #1、成员函数指针与普通函数指针不同 | 成员函数指针与普通函数指针的差异]]

7、函数参数绑定:你可以使用 std::bind 或 Lambda 表达式将参数绑定到 std::function 对象上,以创建可重用的部分函数。

1
2
std::function<int(int)> addThree = std::bind(add, std::placeholders::_1, 3);
int result = addThree(4); // 结果为 7

std::function 是一种非常有用的工具,特别适用于需要动态调用不同函数或需要将函数作为参数传递的情况。它提供了更高级别的抽象,使得 C++ 中的函数对象和回调更加灵活和可维护。

# i、std::bind

std::bind 是 C++11 引入的函数对象(Function Object)库的一部分,用于创建函数对象(或者叫绑定对象),它可以用来将函数或成员函数与一组参数绑定在一起,创建一个新的可调用对象。 std::bind 提供了一种非常强大的机制,允许你灵活地构建新的函数对象,包括改变参数的顺序、固定某些参数的值,以及创建函数适配器。

std::bind  可以看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表

1
auto newCallable = bind(callable, arg_list);

该形式表达的意思是:当调用 newCallable 时,会调用 callable,并传给它  arg_list  中的参数。

预绑定的参数是以值传递的形式,不预绑定的参数是以引用传递的形式。

后者要用  std::placeholders  (占位符)的形式占位,从  _1  开始,依次递增。

1
2
3
4
if (!m_engine->GetComponent("DDSSimulator")) {
std::thread period_send_thread(std::bind(&DDSOutput::SendFusionPeriodThread, this));
period_send_thread.detach();
}

以下是 std::bind 的一些主要用法和特性:

1、绑定全局函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <functional>
#include <iostream>

void greet(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}

int main() {
auto sayHello = std::bind(greet, "John");
sayHello(); // 输出: Hello, John!
return 0;
}

2、绑定成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <functional>
#include <iostream>

class Greeter {
public:
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
};

int main() {
Greeter greeter;
auto greetFunction = std::bind(&Greeter::greet, &greeter, std::placeholders::_1);
greetFunction("Alice"); // 输出: Hello, Alice!
return 0;
}

3、参数绑定和重排序

1
2
3
4
5
6
7
8
9
10
11
12
#include <functional>
#include <iostream>

void printThreeArgs(int a, int b, int c) {
std::cout << a << ", " << b << ", " << c << std::endl;
}

int main() {
auto printArgs = std::bind(printThreeArgs, std::placeholders::_3, 100, std::placeholders::_1);
printArgs(1, 2, 3); // 输出: 3, 100, 1
return 0;
}

4、创建函数适配器std::bind 允许你创建函数适配器,用于改变函数的行为,比如取反或者应用一元或二元函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <functional>
#include <iostream>

bool isEven(int num) {
return num % 2 == 0;
}

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto isOdd = std::not1(std::bind(isEven, std::placeholders::_1));
// 这里的 std::not1 用于将谓词函数的结果取反,即 true 变为 false,false 变为 true

for (int num : numbers) {
if (isOdd(num)) {
std::cout << num << " is odd." << std::endl;
}
} return 0;
}

5、绑定到引用参数std::bind 还可以用于将参数绑定到引用参数,以允许在调用时改变参数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <functional>
#include <iostream>

void addOne(int& value) {
value += 1;
}

int main() {
int number = 42;
auto increment = std::bind(addOne, std::ref(number));
increment();
std::cout << "Number after increment: " << number << std::endl;
// 输出: Number after increment: 43
return 0;
}

注意,这里的 std::ref 的作用是创建对对象的引用。
从这个例子中我们可以更好地理解 bind 这个函数,它其实就是个包装器,浅封装了另一个可调用对象及其参数。那么,在封装这些参数时就可以指定他们是值绑定还是引用绑定。
默认情况下,bind 传递参数是值绑定,用  std::placeholders  占位符进行占位的参数是引用绑定,即我们前面所提到的:预绑定的参数是以值传递的形式,不预绑定的参数是以引用传递的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void addOne(int& value) {
value += 1;
}

auto increment = std::bind(addOne, std::ref(number));
//等价于
void increment(int& val){
addOne(val);
}

//而如果我们去掉了 std::ref,则
auto increment = std::bind(addOne, number);
//相当于
void increment(int val){
addOne(val);
}

//也就是说,虽然我们底层调用的 addOne 函数是值传递的参数,但是其外部包装的函数入参可以进一步控制

std::bind_frontstd::bind_back 在 bind 的基础上绑定前几个参数或后几个参数:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <functional>

void foo(int a, int b, int c) {
std::cout << a << " " << b << " " << c << std::endl;
}

int main() {
auto bound = std::bind_front(foo, 42, 10);
bound(5); // 输出: 42 10 5
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <functional>

void bar(int a, int b, int c) {
std::cout << a << " " << b << " " << c << std::endl;
}

int main() {
auto bound = std::bind_back(bar, 5, 10);
bound(42); // 输出: 42 5 10
return 0;
}

# i、std::ref

std::ref 是一个标准库函数模板,用于创建一个可以传递引用的包装对象。这在需要将对象的引用传递给其他函数或线程时非常有用,尤其是在标准库函数不支持直接传递引用的情况下(例如, std::thread 构造函数期望的是传值语义)。

std::ref 创建一个 std::reference_wrapper 对象,这个对象持有对传入对象的引用,并且可以像普通引用一样使用。 std::reference_wrapper 可以通过 operator T&() 进行隐式转换,允许引用在需要值的地方被使用。

具体使用场景:

  1. 线程参数传递:当我们使用 std::thread 时,参数通常是按值传递的。如果需要传递引用,就可以使用 std::ref
  2. 函数调用:在标准库中,某些函数接受函数对象(如函数、lambda、或 std::function )时,若需要传递引用,可以使用 std::ref
  3. 容器中存储引用:STL 容器通常存储值,如果需要存储引用,可以使用 std::reference_wrapper

线程参数传递示例:

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
#include <iostream>
#include <thread>
#include <future>
#include <functional> // for std::ref

void addTwoValue(std::promise<int>& prom, int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2));
int result = a + b;
prom.set_value(result);
}

int main() {
std::promise<int> prom;

// 使用 std::ref 来传递引用
std::thread task(addTwoValue, std::ref(prom), 5, 3);
std::cout << "waiting for thread to generate result" << std::endl;

std::future<int> fu = prom.get_future();
int result = fu.get();
std::cout << "getting result " << result << std::endl;

task.join();

return 0;
}

函数调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <functional>

void printValue(int& x) {
std::cout << "Value: " << x << std::endl;
}

int main() {
int a = 10;

// 直接调用
printValue(a);

// 使用 std::ref 传递引用
std::function<void()> func = std::bind(printValue, std::ref(a));
func();

return 0;
}

容器中存储引用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
#include <functional>

int main() {
int a = 10;
int b = 20;

// 创建一个引用包装器的容器
std::vector<std::reference_wrapper<int>> ref_container;
ref_container.push_back(a);
ref_container.push_back(b);

// 修改原变量的值
a = 30;
b = 40;

// 通过引用包装器访问修改后的值
for (auto& ref : ref_container) {
std::cout << ref << std::endl;
}

return 0;
}

std::ref 在需要传递引用但接口不支持的场景下非常有用,特别是在多线程编程中传递引用参数时。通过使用 std::ref 可以避免不必要的拷贝,保持对原对象的引用,从而提高效率和灵活性。

# i、std::pair

std::pair 是 C++ 标准库中的一个模板类,用于将两个值组合在一起,形成一个有序对。 std::pair 提供了一种简单的方式来将两个不同类型的值打包,方便传递和返回多个值。 std::pair 的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <class T1, class T2>
struct pair {
T1 first;
T2 second;

// 构造函数
pair(const T1& x, const T2& y) : first(x), second(y) {}

// 默认构造函数
pair() : first(), second() {}

// 拷贝构造函数
template<class U1, class U2>
pair(const pair<U1, U2>& p) : first(p.first), second(p.second) {}

// 移动构造函数
template<class U1, class U2>
pair(pair<U1, U2>&& p) : first(std::forward<U1>(p.first)), second(std::forward<U2>(p.second)) {}

// 其他成员函数...
};

std::pair 的主要特点包括:

  • firstsecond 成员变量,用于存储两个值。
  • 构造函数,允许通过传递参数进行初始化。
  • 默认构造函数,创建一个默认构造的 pair 对象。
  • 拷贝构造函数,用于从其他 pair 对象进行拷贝构造。
  • 移动构造函数,用于从其他 pair 对象进行移动构造。

例如,使用 std::pair 来创建一个包含整数和浮点数的对象:

1
2
3
4
5
6
7
8
#include <iostream>
#include <utility>

int main() {
std::pair<int, double> myPair(42, 3.14);
std::cout << "Pair: " << myPair.first << ", " << myPair.second << std::endl;
return 0;
}

我们定义的 pair 模板类会有默认构造,其会调用两个成员变量的默认构造。

# i、std::make_pair

std::make_pair 是一个模板函数,用于创建 std::pair 对象。它接受两个参数,并根据这两个参数的类型自动推导出 pair 的模板参数类型。 std::make_pair 的声明如下:

1
2
3
template<class T1, class T2>
constexpr std::pair<typename std::decay<T1>::type, typename std::decay<T2>::type>
make_pair(T1&& t, T2&& u);

其中:

  • T1T2std::pair 的两个模板参数类型。
  • std::decay 是一个模板元函数,用于移除类型的引用和 cv 修饰符,确保类型是纯净的。
  • T1&& tT2&& ustd::make_pair 函数的参数,使用右值引用,允许传递左值或右值。

std::make_pair 的作用是根据传入的参数创建一个 std::pair 对象。通过使用 std::make_pair ,我们可以避免显式指定模板参数类型,使代码更简洁。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <utility>

int main() {
int x = 42;
double y = 3.14;

auto myPair = std::make_pair(x, y);

std::cout << "Pair: " << myPair.first << ", " << myPair.second << std::endl;

return 0;
}

这里, std::make_pair(x, y)xy 包装成一个 std::pair<int, double> 对象,并自动推导出 pair 的模板参数类型。

这里要注意的是,make_pair 在传入两个变量时,会调用传入参数的拷贝构造函数(当然,上面这个例子里面 int 和 double 这种基本类型就没这种说法了)。

1
2
3
4
//假设我们有个类型 A
A a1(5);
A a2(7);
std::make_pair(a1, a2); //这里会发生两次 A 的拷贝构造

核心思想:高效快速判断元素是否存在。

C++ STL(标准模板库)提供了一个二分查找算法,即 std::binary_search 函数。这个函数用于在已排序的序列中查找特定元素

函数签名如下:

1
2
template<class ForwardIt, class T>
bool binary_search(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast 是表示序列范围的迭代器,指定了要进行查找的区间。
  • value 是要查找的目标值。

std::binary_search 函数返回一个布尔值,表示是否找到了目标值。如果找到了,返回 true ;如果未找到,返回 false

这个函数使用二分查找算法,在已排序的序列中进行查找。它会在指定的范围内进行迭代,并根据当前位置的值与目标值的比较结果来决定继续查找的方向。

使用 std::binary_search 函数的前提是,序列必须是已排序的。如果序列未排序,结果是不可预测的。如果需要在未排序的序列中进行查找,可以先使用 std::sort 函数对序列进行排序,然后再使用 std::binary_search 进行查找。

下面是一个使用 std::binary_search 函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int target = 7;

// 使用 std::binary_search 查找目标值
bool found = std::binary_search(numbers.begin(), numbers.end(), target);

if (found) {
std::cout << "Target found in the sequence." << std::endl;
} else {
std::cout << "Target not found in the sequence." << std::endl;
}

return 0;
}

这个示例中,我们创建了一个已排序的整数序列,并使用 std::binary_search 函数查找目标值 7。如果找到了,输出 "Target found in the sequence.";如果未找到,输出 "Target not found in the sequence."。

请注意,对于重复的元素, std::binary_search 只能确定序列中是否存在目标值,而不能确定目标值的具体位置。如果需要确定目标值的位置,可以使用 std::lower_bound 函数或 std::upper_bound 函数。

尽管 std::binary_search 无法提供目标值的具体位置,但它在许多情况下可以提供一个高效的方法来检查存在性,以便根据需要采取进一步的操作。

# i、lower_bound && upper_bound

当我们在已排序的序列中进行查找时,除了使用 std::binary_search 函数之外,还可以使用 std::lower_boundstd::upper_bound 函数来获取目标值在序列中的位置。

std::lower_bound 函数用于在已排序的序列中找到第一个不小于目标值的元素的位置,函数签名如下:

1
2
template<class ForwardIt, class T>
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast 是表示序列范围的迭代器,指定了要进行查找的区间。
  • value 是要查找的目标值。

std::lower_bound 函数返回一个迭代器,指向序列中第一个不小于目标值的元素。如果序列中不存在不小于目标值的元素,返回的迭代器将指向序列的末尾。

std::upper_bound 函数用于在已排序的序列中找到第一个大于目标值的元素的位置,函数签名如下:

1
2
template<class ForwardIt, class T>
ForwardIt upper_bound(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast 是表示序列范围的迭代器,指定了要进行查找的区间。
  • value 是要查找的目标值。

std::upper_bound 函数返回一个迭代器,指向序列中第一个大于目标值的元素。如果序列中不存在大于目标值的元素,返回的迭代器将指向序列的末尾。

下面是一个使用 std::lower_boundstd::upper_bound 函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 5, 7, 8, 9, 10};
int target = 5;

// 使用 std::lower_bound 查找不小于目标值的第一个元素
auto lower = std::lower_bound(numbers.begin(), numbers.end(), target);

// 使用 std::upper_bound 查找大于目标值的第一个元素
auto upper = std::upper_bound(numbers.begin(), numbers.end(), target);

// 输出查找结果
std::cout << "Lower bound: " << std::distance(numbers.begin(), lower) << std::endl;
std::cout << "Upper bound: " << std::distance(numbers.begin(), upper) << std::endl;

return 0;
}

这个示例中,我们创建了一个已排序的整数序列,并使用 std::lower_bound 函数查找不小于目标值 5 的第一个元素,以及使用 std::upper_bound 函数查找大于目标值 5 的第一个元素。我们通过 std::distance 函数计算迭代器与序列起始位置的距离,从而得到目标值在序列中的位置。

请注意, std::lower_boundstd::upper_bound 函数的时间复杂度为 O (log N),其中 N 是序列中的元素数量。这两个函数在查找范围较大的有序序列时具有较高的效率。

1
2
Lower bound: 4
Upper bound: 6

upper_bound 并不能确定某个 target 是否存在数组中,而只能判断是否存在大于 target 的数字。

std::upper_bound 函数返回的是第一个大于目标值的元素的迭代器,而不是判断目标值是否存在于数组中。如果目标值存在于数组中,它将返回大于目标值的第一个元素的迭代器;如果目标值在数组中不存在,它将返回大于目标值的最小元素的迭代器,即序列的上界。

如果你想要确定目标值是否存在于数组中,可以将 std::upper_bound 返回的迭代器与目标值进行比较,如果它指向数组的起始位置或者它的前一个元素与目标值相等,则说明目标值存在于数组中。

# i、partial_sum

在 C++ 标准库中,可以使用 std::partial_sum 函数来求一个序列的前缀和(partial sum)。 std::partial_sum 函数是一个算法,它对输入范围内的元素进行部分求和,并将结果存储在输出序列中。

以下是 std::partial_sum 函数的原型:

1
2
template <class InputIterator, class OutputIterator, class BinaryOperation>
OutputIterator partial_sum(InputIterator first, InputIterator last, OutputIterator result, BinaryOperation binary_op);

其中, InputIteratorOutputIterator 是模板参数,它们分别表示输入序列和输出序列的迭代器类型。 BinaryOperation 是一个函数对象(或函数指针),用于指定计算前缀和的二元操作。 firstlast 分别表示输入序列的起始和结束位置(左闭右开区间)。 result 表示输出序列的起始位置。

std::partial_sum 函数的功能是将位于 [first, last) 范围内的元素从输入序列复制到输出序列,并依次计算部分和。它将结果存储在输出序列中,并返回指向输出序列中最后一个复制元素之后位置的迭代器。

默认情况下, std::partial_sum 使用加法 + 运算符进行前缀和计算。如果要使用其他的二元操作,可以通过传递自定义的函数对象或函数指针 binary_op 来实现。

以下是使用 std::partial_sum 函数求前缀和的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <algorithm>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> prefixSum(numbers.size());

// 使用 std::partial_sum 求前缀和,并直接写入到输出容器
std::partial_sum(numbers.begin(), numbers.end(), prefixSum.begin());

// 打印前缀和序列
for (const auto& num : prefixSum) {
std::cout << num << " ";
} std::cout << std::endl;

return 0;
}

在上面的示例中,我们通过 std::partial_sum 函数计算 numbers 中元素的前缀和,并将结果直接写入到 prefixSum 容器中。在这里,我们在调用 std::partial_sum 时,将 prefixSum.begin() 作为输出序列的起始位置,这样 partial_sum 会将计算得到的前缀和直接写入到 prefixSum 容器中。

# i、priority_queue

使用 priority_queue 需要包含头文件 <queue> ,默认情况下, priority_queue 会按照元素类型的比较函数(默认为 less ,即降序排列)来维护元素的优先级。如果需要按照其他方式来定义优先级(例如升序排列),可以使用自定义的比较函数。

其基本操作同 queue,即 push、top、pop,只是多了排序的特性。(但 pq 只保证顶部元素是有序的,不保证整个队列是有序的。因为它的本质是大小堆,只能保证当前节点比左右子节点大 / 小,而每一次进行 push 或者 pop 进行元素的增删时,都会使得节点进行交换位置,可能打乱顺序。)

示例代码:

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
36
37
38
#include <iostream>
#include <queue>

int main() {
// 默认情况下,创建一个降序的优先级队列
std::priority_queue<int> pq;

// 插入元素
pq.push(10);
pq.push(30);
pq.push(20);

// 访问和删除元素
while (!pq.empty()) {
std::cout << pq.top() << " "; // 访问最大元素
pq.pop(); // 移除最大元素
}

std::cout << std::endl;

// 自定义比较函数,创建一个升序的优先级队列
std::priority_queue<int, std::vector<int>, std::greater<int>> pq_custom;

// 插入元素
pq_custom.push(50);
pq_custom.push(10);
pq_custom.push(30);

// 访问和删除元素
while (!pq_custom.empty()) {
std::cout << pq_custom.top() << " "; // 访问最小元素
pq_custom.pop(); // 移除最小元素
}

std::cout << std::endl;

return 0;
}

# i、accumulate

std::accumulate 是 C++ 标准库中的一个算法,用于对给定范围内的元素进行累积操作。它定义在头文件 <numeric> 中,接口定义如下:

1
2
template <class InputIterator, class T>
T accumulate (InputIterator first, InputIterator last, T init);

  • InputIterator first :表示范围的起始迭代器,指向要累积的元素的第一个元素。
  • InputIterator last :表示范围的结束迭代器,指向要累积的元素的最后一个元素的下一个位置(即不包含在范围内)。
  • T init :表示累积的初始值。

accumulate 函数会从 firstlast 对范围内的元素进行累积操作,将每个元素依次加到 init 上,然后返回最终的累积结果。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>
#include <numeric> // 包含 accumulate 函数

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 使用 accumulate 对 numbers 中的元素进行累积操作,初始值为0
int sum = std::accumulate(numbers.begin(), numbers.end(), 0);

std::cout << "Sum of elements: " << sum << std::endl;

return 0;
}

# i、minmax_element

std::minmax_element 是 C++ 标准库中的一个算法函数,用于在给定范围内查找最小值和最大值的元素,并返回指向这两个元素的迭代器。

以下是 std::minmax_element 函数的原型:

1
2
template <class ForwardIterator>
std::pair<ForwardIterator, ForwardIterator> minmax_element(ForwardIterator first, ForwardIterator last);

ForwardIterator 是模板参数,它表示输入序列的迭代器类型。 firstlast 分别表示输入序列的起始和结束位置(左闭右开区间)。

std::minmax_element 函数的功能是在范围 [first, last) 内查找最小值和最大值的元素,并返回一个 std::pair 对象,其中 first 成员是指向最小值元素的迭代器, second 成员是指向最大值元素的迭代器。

以下是使用 std::minmax_element 函数查找最小值和最大值的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <algorithm>
#include <vector>

int main() {
std::vector<int> numbers = {5, 2, 8, 3, 1, 9, 4, 7, 6};

// 使用 std::minmax_element 查找最小值和最大值
auto minmaxPair = std::minmax_element(numbers.begin(), numbers.end());

// 输出结果
std::cout << "最小值: " << *minmaxPair.first << std::endl;
std::cout << "最大值: " << *minmaxPair.second << std::endl;

return 0;
}

在上面的示例中,我们使用 std::minmax_element 函数在 numbers 中查找最小值和最大值,并将结果存储在 minmaxPair 中。然后通过 minmaxPair.firstminmaxPair.second 分别访问最小值和最大值的元素。

# i、sort

sort 虽然很容易,但是老是忘记一些细节:
sort 默认是从小到大排序,sort 第三个参数为可选参数,可用于传入比较函数。
STL 提供了两个内置的比较函数 less 和 greater,具体使用如下:

1
2
std::sort(nums.begin(), nums.end(), std::less<int>());    //从小到大
std::sort(nums.begin(), nums.end(), std::greater<int>()); //从大到小

当然,第三个参数也可以是自定义函数,比如传入 lambda 表达式。

# i、remove

std::remove 从指定范围内删除指定的值。

它是 C++ 标准库中的一个算法函数,用于从序列中删除指定的元素。它的主要作用是将需要删除的元素移到序列的末尾,然后返回一个指向新范围结尾之后位置的迭代器。它并不会真正地删除元素,而是通过返回新的范围来使得这些元素 “看起来” 被删除了。

其声明如下:

1
2
template <class ForwardIt, class T>
ForwardIt remove(ForwardIt first, ForwardIt last, const T& value);

参数说明:

  • firstlast :表示要操作的范围, [first, last) 为一个半开区间,包含要进行操作的元素
  • value :要删除的特定值。

返回值是一个迭代器,指向新范围结尾之后的位置。这个迭代器可以用来调整容器的大小,实现元素的 “删除”。

以下是一个使用 std::remove 的示例代码,移除容器中的特定元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 3, 5, 6};

// 使用 std::remove 将所有值为 3 的元素移到末尾
auto newEnd = std::remove(numbers.begin(), numbers.end(), 3);

// 调整容器大小,删除被移到末尾的元素
numbers.erase(newEnd, numbers.end());

// 输出删除元素后的容器
for (const int& num : numbers) {
std::cout << num << " ";
} return 0;
}

# i、erase

在 C++ 中, erase 是用于容器的成员函数,主要用于删除容器中的元素。它在不同类型的容器(如 std::vectorstd::liststd::map 等)中都有不同的用法,但其基本作用是从容器中删除一个或多个元素。以下是关于 erase 函数的一些常见用法:

  1. 删除单个元素:

    • 对于 std::vectorstd::list 等序列容器,可以使用迭代器来删除指定位置的元素。
      1
      2
      3
      std::vector<int> numbers = {1, 2, 3, 4, 5};
      auto it = numbers.begin() + 2; // 删除第三个元素
      numbers.erase(it);
  2. 删除范围内的元素:

    • erase 数可以删除指定范围内的元素。你需要提供一个表示范围的起始迭代器和结束迭代器。
      1
      2
      3
      4
      5
      std::list<int> myList = {1, 2, 3, 4, 5};
      auto start = myList.begin();
      auto end = myList.begin();
      std::advance(end, 3); //迭代器向前移动3个位置——注意这里std::list是双向链表
      myList.erase(start, end);
  3. 删除满足条件的元素:

    • 你可以使用 std::remove_if 算法与 erase 函数一起删除满足某个条件的元素。通常,这在容器中删除特定值或满足某个条件的元素时非常有用。(关于 remove_if 更多细节,见下文)
      1
      2
      std::vector<int> numbers = {1, 2, 3, 4, 5};
      numbers.erase(std::remove_if(numbers.begin(), numbers.end(), [](int x) { return x % 2 == 0; }), numbers.end());
  4. 删除所有元素:

    • 使用 clear 函数可以快速清空容器中的所有元素。
      1
      2
      std::vector<int> numbers = {1, 2, 3, 4, 5};
      numbers.clear(); // 删除所有元素,numbers变为空

需要注意的是, erase 函数可能会导致迭代器失效,因此在使用 erase 后,应谨慎操作迭代器。确保在删除元素后不再使用已失效的迭代器。此外,不同类型的容器可能有不同的参数和用法,因此使用前请查阅相关文档。工程实践中的具体例子可参考:[[SDK 相机断开重连偶现 crash]]

# i、remove_if

和上面的 remove 基本一致,只不过第三个参数由指定某个元素变成了指定某个条件。

std::remove_if 是 C++ 标准库中的一个算法,用于从容器中删除满足特定条件的元素,但它并不会真正地删除元素,而是将这些元素移到容器的末尾,并返回一个指向新的 “逻辑末尾” 位置的迭代器。

函数签名:

1
2
template <class ForwardIt, class UnaryPredicate>
ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p);

参数:
firstlast 是表示范围的迭代器,它们定义了要进行操作的元素的范围。
p 是一个一元谓词(UnaryPredicate),它定义了要删除的条件。

操作:
std::remove_if 会遍历范围 [first, last) 中的元素,对于满足谓词 p 的元素,它会将它们移到范围的末尾,不满足条件的元素保持原位置。这就是为什么它不会真正删除元素,而只是重新排列它们。

返回值:
std::remove_if 返回一个指向新的 “逻辑末尾” 的迭代器,该迭代器之前的元素仍然保持不变,而迭代器之后的元素是被移动的元素,但它们的值是未定义的。这里的逻辑和 remove 函数一样。

由于 remove 之后返回的是新的逻辑尾迭代器,因此可以结合 erase 来使用,形式为 erase(remove_if(...), end)
示例:

1
2
3
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
numbers.erase(std::remove_if(numbers.begin(), numbers.end(), [](int x) { return x % 2 == 0; }), numbers.end());
// 在上述示例中,删除了所有偶数,numbers 现在包含 {1, 3, 5}。

# i、for_each

C++ 中的 for_each 函数是标准库 <algorithm> 头文件中的一个算法,用于对容器中的元素执行某个操作。它通常与函数对象(也称为谓词)一起使用,允许你在容器的每个元素上执行相同的操作。 for_each 函数的一般形式如下:

1
2
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);

  • InputIterator 是一个迭代器,指定了要遍历的容器范围的起始和结束位置。
  • Function 是一个函数对象或谓词,用于定义要在容器的每个元素上执行的操作。
  • firstlast 参数指定了要遍历的范围,通常是容器的起始和结束迭代器。

for_each 函数的主要作用是对容器中的每个元素执行函数对象 f ,即对范围 [first, last) 中的每个元素调用函数对象 f 。这可以用于遍历容器并执行特定的操作,如打印元素、修改元素等。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <algorithm>

void print_element(int element) {
std::cout << element << " ";
}

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 使用for_each函数调用print_element函数打印向量中的元素
std::for_each(numbers.begin(), numbers.end(), print_element);

std::cout << std::endl;

return 0;
}

# i、copy

std::copy 是 C++ 标准库中的一个算法函数,它用于将一个范围内的元素复制到另一个范围中。其基本用法如下:

1
2
3
#include <algorithm>
// ...
std::copy(first1, last1, first2);

  • first1last1 定义了源范围,表示要复制的元素范围是 [first1, last1) (左闭右开区间)。
  • first2 定义了目标范围,表示要将元素复制到的范围的起始位置。

在使用这个函数时,你需要确保源范围 [first1, last1) 和目标范围 [first2, ...) 之间有足够的空间来容纳要复制的元素。

这里假设 ii_iq 的大小足够大,以至于能够容纳 iiDataiqData 中的所有元素。如果 ii_iq 的大小不够,将导致未定义行为,可能引发程序崩溃或产生其他不确定的结果。

# i、replace

std::replace 是 C++ 标准库中的算法之一,用于在指定范围内替换元素的值。它的声明如下:

1
2
template< class ForwardIt, class T >
void replace( ForwardIt first, ForwardIt last, const T& old_value, const T& new_value );

这个算法在指定范围 [first, last) 内搜索所有等于 old_value 的元素,并将它们替换为 new_value

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <algorithm>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};
// 使用 std::replace 将所有值为 2 的元素替换为 0
std::replace(numbers.begin(), numbers.end(), 2, 0);
// 打印替换后的向量
for (int num : numbers) {
std::cout << num << " ";
} return 0;
}

由于是模板函数,所以自然也可以在字符串中使用:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>
#include <algorithm>

int main() {
std::string fileName = "example.file.txt";

// 将所有的 '.' 替换为 '_'
std::replace(fileName.begin(), fileName.end(), '.', '_');

return 0;
}

另外,string 也内置了 replace 函数:(我们的原则是,类内有定义,优先使用类内函数)

1
2
//将字符串 myStr 的末尾 `.bin` 更换成 `.txt`
myStr.replace(myStr.end() - 4, myStr.end(), ".txt");

# i、rfind

在 C++ 中, rfind 是字符串类(如 std::string )的成员函数之一,用于在字符串中从后往前查找指定子字符串(或字符)的位置。它的基本用法如下:

1
size_t rfind(const std::string& str, size_t pos = npos) const;

  • str :要查找的子字符串。
  • pos :可选参数,指定开始查找的位置,默认值是 std::string::npos ,表示从字符串的末尾开始向前查找。

rfind 函数返回指定子字符串在原字符串中最后出现的位置。如果未找到子字符串,它将返回 std::string::npos ,通常表示未找到。

以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>

int main() {
std::string str = "Hello, world! Hello, C++!";
size_t found = str.rfind("Hello");

if (found != std::string::npos) {
std::cout << "Last occurrence of 'Hello' found at position " << found << std::endl;
} else {
std::cout << "'Hello' not found in the string." << std::endl;
}

return 0;
}

在这个示例中, rfind 函数查找字符串中最后出现的 "Hello",并返回其位置。如果找到,它会输出该位置,否则会提示未找到。

注意,这是从字符串开始的偏移量,而不是结束的偏移量。

参考链接:https://en.cppreference.com/w/cpp/string/basic_string/rfind

# i、hardware_concurrency

hardware_concurrency 是 C++ 标准库中的一个函数,属于 <thread> 头文件的一部分。它的主要作用是返回能够并发执行的(物理)线程数量。这通常反映了系统上的处理器核心数量。

下面是一个简单的使用例子:

1
2
3
4
5
6
7
8
#include <iostream>
#include <thread>

int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << "This system can run " << n << " concurrent threads." << std::endl;
return 0;
}

当调用 std::thread::hardware_concurrency() 时,它会返回一个 unsigned int 值,该值表示系统上可以并发运行的最大线程数量。如果无法确定这个数量,它可能返回 0。

需要注意的是,返回的值只是一个建议值,不是硬性规定,具体的并发能力还受到操作系统的线程调度和其他资源限制的影响。

# i、sscanf

1
int sscanf(const char *str, const char *format, ...);

sscanf 用于从字符串中以指定格式读取信息,可视作 scanf 的重定向版本,源头由标准输入转变为字符串。

sscanf() reads its input from the character string pointed to  by str.

例如:

1
2
uint64_t utc_ts, utc_tns, gtc_ts, gtc_tns, data_size, offset;
sscanf(line.c_str(), "%lu %lu %s %lu %lu %lu %lu", &utc_ts, &utc_tns, topic, &data_size, &offset, &gtc_ts, &gtc_tns);

# i、emplace

模板函数,作用是在容器中的 position 位置用 args 这一包数据(可变参数)来原地构造一个元素。

1
template <class... Args>iterator emplace(const_iterator position, Args&&... args);

可以将其视作更优雅的 push_back、insert 方法,举例如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class People {
public:
People()=default;

People(int n, double j) : m_a(n), m_b(j) {
cout << "def constructor" << endl;
}

People(const People &ppl) : m_a(ppl.m_a), m_b(ppl.m_b) {
cout << "cp constructor" << endl;
}

People(const People && ppl) : m_a(ppl.m_a), m_b(ppl.m_b) {
cout << "mv constructor" << endl;
}

People& operator=(const People& ppl) {
cout << "cp assign" << endl;
m_a = ppl.m_a;
m_b = ppl.m_b;
return *this;
}

private:
int m_a;
double m_b;
};

cout << "----------insert func----------" << endl;
vector<People> pplVec1;
pplVec1.insert(pplVec1.end(), People(1, 17.1)); //构造一个临时变量

cout << "----------insert func----------" << endl;
vector<People> pplVec2;
pplVec2.insert(pplVec2.end(), {2, 17.2}); //构造一个临时变量

cout << "----------emplace func----------" << endl;
vector<People> pplVec3;
pplVec3.emplace(pplVec3.end(), 3, 8.2); //无需构造临时变量

输出:
----------insert func----------
def constructor
mv constructor
----------insert func----------
def constructor
mv constructor
----------emplace func----------
def constructor

通过输出信息可知,emplace 少调用了一次拷贝构造。(有移动构造时会用移动构造代替拷贝构造)

注意,当拷贝构造函数和移动构造函数同时存在时,会优先调用移动构造函数。即,emplace 就地构造,省去一次临时变量的创建。其底层实现原理是 可变参数模板 (即...args)和 完美转发 ,这里不展开。 std::vector::emplace_back()  则是另一个显式的函数,不过是在尾部插入。

工作中实际应用场景:

1
2
3
4
5
6
7
8
9
10
template <class FrameVectorType>
typename FrameVectorType::iterator EmplaceMea(FrameVectorType &frame_vector)
{
//FRAMEWORK::NW("DO NOT forget to lock using board mutex");
BeginLock();
typename FrameVectorType::iterator it = frame_vector.emplace_back_itor_noblock();
EndLock();
it->clear();
return it;
}

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/12/C-\350\270\251\346\260\264\346\264\274/index.html" "b/2024/10/12/C-\350\270\251\346\260\264\346\264\274/index.html" index 5572514..15af592 100644 --- "a/2024/10/12/C-\350\270\251\346\260\264\346\264\274/index.html" +++ "b/2024/10/12/C-\350\270\251\346\260\264\346\264\274/index.html" @@ -1 +1 @@ -C++ 踩水洼 - C++ - 编程 | R1ck's Playground = how

本文用以记录工作过程中 C++ 踩到的一些坑。

# 1、成员函数指针与普通函数指针不同

成员函数需要一个额外的参数(指向类实例的指针 this ),因此不能直接将成员函数传递给普通的函数指针参数。

1
2
3
4
5
6
7
class OtherClass {
public:
typedef function<int(int, int)> callbacktype;
void registerCallBack(callbacktype cb) {
cout << "test: " << cb(2, 4) << endl;
}
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public:
void func1();
int func2(int a, int b);
private:
OtherClass* _otherclass;
};

void MyClass::func1() {
cout << "in func1" << endl;
//wanna set func2 as a callback function into OtherClass
}

int MyClass::func2(int a, int b) {
cout << "in func2" << endl;
return a + b;
}

1
2
3
4
5
6
7
void MyClass::func1() {
cout << "in func1" << endl;
function<int(int, int)> cb = [](int a, int b) {
return a + b;
};
_otherclass->registerCallBack(cb);
}

1
2
3
void MyClass::func1() {
_otherclass->registerCallBack(func2); //error:invalid use of non-static member function
}

1
2
3
4
void MyClass::func1() {
auto cbf = std::bind(&MyClass::func2, this, std::placeholders::_1, std::placeholders::_2);
_otherclass->registerCallBack(cbf);
}

这里解释一下 bind 的作用:

这行代码是使用 C++ 中的 std::bind 函数创建了一个函数对象(function object),并将其分配给了名为 cbf 的变量。这个函数对象的作用是将 MyClass 类中的成员函数 func2 绑定到当前实例( this 指针),并且允许传递两个参数。

在 C++ 中,类的成员函数通常需要一个指向类实例的指针,通常称为 this 指针,以访问该实例的成员变量和其他成员函数。当您在成员函数内部使用 this 关键字时,它指向当前对象的地址。因此,绑定到当前实例意味着创建一个函数对象,该函数对象将包含指向当前对象的 this 指针,以便在后续调用该函数对象时,能够访问当前对象的成员函数和成员变量。

std::bind 函数的第一个参数 &MyClass::func2 是要绑定的成员函数的指针,而第二个参数 this 是指向当前对象的指针。这样,创建的函数对象 cbf 就可以在以后的调用中像普通函数一样使用,而它内部会使用正确的 this 指针来访问当前对象的成员函数 func2 和其他成员。

这种绑定技术非常有用,因为它允许您将成员函数作为回调传递给其他函数或对象,并且在后续调用时,不需要手动传递 this 指针。这可以简化代码并提高代码的可重用性。

1
2
3
4
5
int main() {
MyClass mc;
mc.func1();
return 0;
}

以下是 chatgpt 给出的示例:

成员函数指针和普通函数指针的差异在于成员函数需要额外的参数来指定类的实例,通常是指向类对象的指针,即 this 指针。

在 C++ 中,成员函数指针的类型由类的名称、成员函数名称和函数签名(参数类型和返回类型)决定。例如,对于类 MyClass 的成员函数 void func(int x, int y) ,其成员函数指针的类型为:

1
void (MyClass::*)(int, int)

由于成员函数指针需要额外的参数来传递类的实例,它不能直接传递给普通的函数指针参数。考虑以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public:
void func(int x, int y) {
// ...
}
};

void callFunction(void (*funcPtr)(int, int)) {
// 调用函数指针
funcPtr(10, 20);
}

int main() {
MyClass obj;
callFunction(obj.func); // 错误!成员函数指针不能直接传递给普通函数指针参数
return 0;
}

上面的示例中,我们有一个类 MyClass 和一个普通函数 callFunction ,该函数接受一个普通的函数指针作为参数,并调用它。然后我们在 main 函数中尝试将类的成员函数指针 obj.func 传递给 callFunction ,但这是错误的。因为 obj.func 是成员函数指针,它需要额外的参数 this 来指定类实例,而普通函数指针不支持传递额外的参数。

(其实这里还可以再进一步拓展:为什么明明在传递参数时 “已经指定了具体某个对象”—— 毕竟我们传递的是 obj.func ,为什么还得再多此一举显式地指定?)

为了解决这个问题,C++ 提供了 std::function ,它可以用来包装成员函数指针,并且可以传递额外的参数。修改上面的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <functional>

class MyClass {
public:
void func(int x, int y) {
std::cout << "Result: " << x + y << std::endl;
}
};

void callFunction(std::function<void(MyClass*, int, int)> funcPtr, MyClass* obj, int x, int y) {
// 调用函数指针
funcPtr(obj, x, y);
}

int main() {
MyClass obj;
callFunction(&MyClass::func, &obj, 10, 20); // 正确:使用 std::function 传递成员函数指针和类实例
return 0;
}

在上面的示例中,我们使用 std::function 来包装成员函数指针,并将类实例 obj 作为额外参数传递给了函数指针。这样我们就成功地将成员函数指针传递给了普通函数,并通过 funcPtr 调用了成员函数。注意在 callFunction 中,我们传递了额外的参数 obj、x 和 y 给成员函数指针 funcPtr。(看得出来这种方式很麻烦,不如 bind 来得简洁)

# 2、线程函数未返回导致回收时被阻塞

代码的大致逻辑为:
在主线程中创建某个类 A,调用该类的 start 函数,start 函数会用 pthread_create 创建一个子线程,子线程又会再创建一个子线程。然后主线程阻塞在 getchar 函数,等待按键按下,阻塞结束后调用类 A 的 stop 函数,在该函数中回收前面创建的两个子线程。而在子线程中,循环执行某些内容,直到类 A 的某个 flag 在 stop 中被置零,才跳出 while 循环。(所以我们通过 this 指针传递类 A 的某个 flag)

下面是从项目代码中提取并简化的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include "myClass.h"

int main() {
myClass mc;
mc.start();

getchar();

mc.stop();
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef __MYCLASS_H
#define __MYCLASS_H
#include <iostream>
#include <pthread.h>

class myClass {
public:
myClass(){}
void start();
void stop();
private:
bool _isStart = false;
pthread_t _firstThreadHandler;
pthread_t _secondThreadHandler;
static void* firstThread(void* arg);
static void* secondThread(void* arg);
};

#endif

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
36
37
38
39
40
#include "myClass.h"
#include <unistd.h>

void myClass::start()
{
std::cout << "myClass start!" << std::endl;
_isStart = true;

pthread_create(&_firstThreadHandler, NULL, &firstThread, (void *)this);
}

void myClass::stop()
{
std::cout << "myClass stop!" << std::endl;
_isStart = false;

pthread_join(_firstThreadHandler, nullptr);
_firstThreadHandler = 0;
pthread_join(_secondThreadHandler, nullptr);
_secondThreadHandler = 0;
}

void* myClass::firstThread(void* arg)
{
myClass* this_ptr = (myClass*) arg;
pthread_create(&this_ptr->_secondThreadHandler, NULL, &secondThread, (void *)arg);
while(this_ptr->_isStart) {
usleep(10000);
std::cout << "first" << std::endl;
} return nullptr;
}

void* myClass::secondThread(void* arg)
{
myClass* this_ptr = (myClass*) arg;
while(this_ptr->_isStart) {
usleep(10000);
std::cout << "second" << std::endl;
} return nullptr;
}

上述代码是已经纠正后的正确版本。

而在项目代码中,在修正前遇到的问题是,firstThread 能够正常 join 回收,跳出 while 循环,而 secondThread 却无法正常回收,无法跳出 while 循环。出错的原因是 secondThread 漏了返回,即漏了 return nullptr; 这一句。

但,神奇的是,从现象看就是一直在打印 second ,看似没有跳出 while 循环,但其实已经跳出了,只不过没有 return,那为什么还会一直打印呢?搞不懂。

而且,简化后的示例代码即使漏写 return 语句,也不会出现上述问题。玄学。

# 3、指针作为类内成员变量的初始化问题

对于类内的指针,请务必确保其正确初始化。
要么在定义时直接赋值为 nullptr,要么在类的构造函数中赋值,否则容易出现以下问题:

1
2
3
4
5
6
7
8
{
if(myptr == nullptr) { // 如果我们企图通过指针是否为空来判断是否进行新对象的创建
myptr = new myClass;
} // 那么如果 myptr 没有被初始化赋值,那么其值未定义,是野指针,此时不会走入这个创建逻辑

myptr->callsomefunc(); // 如果此时我们企图利用这个虽然非空但是并不指向对象的野指针调用函数
// 那么我们就会遇到段错误 -- 这里我们没有成功创建对象,于是这里空指针企图调用函数,crash
}

在 C++ 中,未初始化的指针的值是未定义的,它可能是任意值,包括零。这是因为在创建对象时,编译器并不保证为每个成员变量初始化一个特定的值,因此它们的初始状态是不确定的。

即使你没有显式地初始化指针成员变量,它的值也可能每次都是零,这可能是由于编译器或运行时库的一些默认行为。这些默认行为可能会导致未初始化的指针被赋予特定的值,但这并不是标准规定的。

当你修改代码并重新编译后,编译器可能生成了不同的机器代码,或者编译器的优化策略发生了变化,这可能会导致未初始化的指针的值发生变化。在这种情况下,由于未初始化的指针的值本身是未定义的,它可以在不同的编译器、编译选项或运行时环境下表现出不同的行为。

总之,依赖未初始化指针的具体值是一种不好的编程实践,因为它可能导致不可预测的行为。最好的做法是始终显式初始化指针,以确保它具有可预测的初始状态。

# 4、线程函数中访问已销毁的结构体对象中的指针问题

大致场景抽象如下:

我们定义了一个结构体,在结构体中存储了两个指针:

1
2
3
4
struct ThreadParams {
rgbDriver* rgbImpl;
tofDriver* tofImpl;
};

之所以定义这么一个结构体,是因为我们希望同时传递两个指针到一个线程中:

1
2
3
4
5
6
7
8
9
10
ThreadParams threadParams;
threadParams.rgbImpl = _rgb_impl;
threadParams.tofImpl = _tof_impl;

//其中,_rgb_impl 和 _tof_impl 是两个指针

pthread_create(&_mipiTriggerThreadHandle,
NULL,
&mipiTriggerFunc,
static_cast<void*>(pthreadParams));

然后,我们在线程中去获取这两个指针:

1
2
3
4
5
6
7
8
void* MIPI::mipiTriggerFunc(void* arg) {
ThreadParams* params = static_cast<ThreadParams*>(arg);
rgbDriver* rgbImpl = params->rgbImpl;
tofDriver* tofImpl = params->tofImpl;

// 其他业务逻辑...
// ...
}

现在问题来了,即使我们在外面传递的指针是空指针,如:

1
2
3
4
5
6
7
8
ThreadParams threadParams;
threadParams.rgbImpl = nullptr;
threadParams.tofImpl = nullptr;

pthread_create(&_mipiTriggerThreadHandle,
NULL,
&mipiTriggerFunc,
static_cast<void*>(pthreadParams));

我们在线程函数 mipiTriggerFunc 中仍然可能得到非空的指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void* MIPI::mipiTriggerFunc(void* arg) {
ThreadParams* params = static_cast<ThreadParams*>(arg);
rgbDriver* rgbImpl = params->rgbImpl;
tofDriver* tofImpl = params->tofImpl;

if(rgbImpl) {
printf("这一行还是有可能会被打印");
}
if(tofImpl) {
printf("这一行还是有可能会被打印");
}

// 其他业务逻辑...
// ...
}

实际情况就是这两个指针非空,于是造成后续的逻辑出错、混乱,甚至出现段错误,因为后续逻辑试图访问一个野指针的成员变量。

(又或者,即使我们这两个指针都是非空的可用的指针,当我们传递进去之后,在线程函数中得到的仍然会是野指针,同样会导致出错)

之所以造成这个现象,是因为我们传递的是一个临时结构体变量,在线程函数执行过程中,这个临时结构体变量就(可能)被销毁了,导致在线程函数中访问到的是野指针。

所以要解决这个问题也很简单,动态分配内存,使得该结构体对象不会被自动销毁。

1
2
3
4
5
6
ThreadParams* pthreadParams = new ThreadParams;
pthreadParams->rgbImpl = _rgb_impl;
pthreadParams->tofImpl = _tof_impl;

// 记得在线程函数不再使用这两个指针时释放结构体的内存
delete pthreadParams;

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 踩水洼 - C++ - 编程 | R1ck's Playground = how

本文用以记录工作过程中 C++ 踩到的一些坑。

# 1、成员函数指针与普通函数指针不同

成员函数需要一个额外的参数(指向类实例的指针 this ),因此不能直接将成员函数传递给普通的函数指针参数。

1
2
3
4
5
6
7
class OtherClass {
public:
typedef function<int(int, int)> callbacktype;
void registerCallBack(callbacktype cb) {
cout << "test: " << cb(2, 4) << endl;
}
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public:
void func1();
int func2(int a, int b);
private:
OtherClass* _otherclass;
};

void MyClass::func1() {
cout << "in func1" << endl;
//wanna set func2 as a callback function into OtherClass
}

int MyClass::func2(int a, int b) {
cout << "in func2" << endl;
return a + b;
}

1
2
3
4
5
6
7
void MyClass::func1() {
cout << "in func1" << endl;
function<int(int, int)> cb = [](int a, int b) {
return a + b;
};
_otherclass->registerCallBack(cb);
}

1
2
3
void MyClass::func1() {
_otherclass->registerCallBack(func2); //error:invalid use of non-static member function
}

1
2
3
4
void MyClass::func1() {
auto cbf = std::bind(&MyClass::func2, this, std::placeholders::_1, std::placeholders::_2);
_otherclass->registerCallBack(cbf);
}

这里解释一下 bind 的作用:

这行代码是使用 C++ 中的 std::bind 函数创建了一个函数对象(function object),并将其分配给了名为 cbf 的变量。这个函数对象的作用是将 MyClass 类中的成员函数 func2 绑定到当前实例( this 指针),并且允许传递两个参数。

在 C++ 中,类的成员函数通常需要一个指向类实例的指针,通常称为 this 指针,以访问该实例的成员变量和其他成员函数。当您在成员函数内部使用 this 关键字时,它指向当前对象的地址。因此,绑定到当前实例意味着创建一个函数对象,该函数对象将包含指向当前对象的 this 指针,以便在后续调用该函数对象时,能够访问当前对象的成员函数和成员变量。

std::bind 函数的第一个参数 &MyClass::func2 是要绑定的成员函数的指针,而第二个参数 this 是指向当前对象的指针。这样,创建的函数对象 cbf 就可以在以后的调用中像普通函数一样使用,而它内部会使用正确的 this 指针来访问当前对象的成员函数 func2 和其他成员。

这种绑定技术非常有用,因为它允许您将成员函数作为回调传递给其他函数或对象,并且在后续调用时,不需要手动传递 this 指针。这可以简化代码并提高代码的可重用性。

1
2
3
4
5
int main() {
MyClass mc;
mc.func1();
return 0;
}

以下是 chatgpt 给出的示例:

成员函数指针和普通函数指针的差异在于成员函数需要额外的参数来指定类的实例,通常是指向类对象的指针,即 this 指针。

在 C++ 中,成员函数指针的类型由类的名称、成员函数名称和函数签名(参数类型和返回类型)决定。例如,对于类 MyClass 的成员函数 void func(int x, int y) ,其成员函数指针的类型为:

1
void (MyClass::*)(int, int)

由于成员函数指针需要额外的参数来传递类的实例,它不能直接传递给普通的函数指针参数。考虑以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public:
void func(int x, int y) {
// ...
}
};

void callFunction(void (*funcPtr)(int, int)) {
// 调用函数指针
funcPtr(10, 20);
}

int main() {
MyClass obj;
callFunction(obj.func); // 错误!成员函数指针不能直接传递给普通函数指针参数
return 0;
}

上面的示例中,我们有一个类 MyClass 和一个普通函数 callFunction ,该函数接受一个普通的函数指针作为参数,并调用它。然后我们在 main 函数中尝试将类的成员函数指针 obj.func 传递给 callFunction ,但这是错误的。因为 obj.func 是成员函数指针,它需要额外的参数 this 来指定类实例,而普通函数指针不支持传递额外的参数。

(其实这里还可以再进一步拓展:为什么明明在传递参数时 “已经指定了具体某个对象”—— 毕竟我们传递的是 obj.func ,为什么还得再多此一举显式地指定?)

为了解决这个问题,C++ 提供了 std::function ,它可以用来包装成员函数指针,并且可以传递额外的参数。修改上面的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <functional>

class MyClass {
public:
void func(int x, int y) {
std::cout << "Result: " << x + y << std::endl;
}
};

void callFunction(std::function<void(MyClass*, int, int)> funcPtr, MyClass* obj, int x, int y) {
// 调用函数指针
funcPtr(obj, x, y);
}

int main() {
MyClass obj;
callFunction(&MyClass::func, &obj, 10, 20); // 正确:使用 std::function 传递成员函数指针和类实例
return 0;
}

在上面的示例中,我们使用 std::function 来包装成员函数指针,并将类实例 obj 作为额外参数传递给了函数指针。这样我们就成功地将成员函数指针传递给了普通函数,并通过 funcPtr 调用了成员函数。注意在 callFunction 中,我们传递了额外的参数 obj、x 和 y 给成员函数指针 funcPtr。(看得出来这种方式很麻烦,不如 bind 来得简洁)

# 2、线程函数未返回导致回收时被阻塞

代码的大致逻辑为:
在主线程中创建某个类 A,调用该类的 start 函数,start 函数会用 pthread_create 创建一个子线程,子线程又会再创建一个子线程。然后主线程阻塞在 getchar 函数,等待按键按下,阻塞结束后调用类 A 的 stop 函数,在该函数中回收前面创建的两个子线程。而在子线程中,循环执行某些内容,直到类 A 的某个 flag 在 stop 中被置零,才跳出 while 循环。(所以我们通过 this 指针传递类 A 的某个 flag)

下面是从项目代码中提取并简化的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include "myClass.h"

int main() {
myClass mc;
mc.start();

getchar();

mc.stop();
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef __MYCLASS_H
#define __MYCLASS_H
#include <iostream>
#include <pthread.h>

class myClass {
public:
myClass(){}
void start();
void stop();
private:
bool _isStart = false;
pthread_t _firstThreadHandler;
pthread_t _secondThreadHandler;
static void* firstThread(void* arg);
static void* secondThread(void* arg);
};

#endif

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
36
37
38
39
40
#include "myClass.h"
#include <unistd.h>

void myClass::start()
{
std::cout << "myClass start!" << std::endl;
_isStart = true;

pthread_create(&_firstThreadHandler, NULL, &firstThread, (void *)this);
}

void myClass::stop()
{
std::cout << "myClass stop!" << std::endl;
_isStart = false;

pthread_join(_firstThreadHandler, nullptr);
_firstThreadHandler = 0;
pthread_join(_secondThreadHandler, nullptr);
_secondThreadHandler = 0;
}

void* myClass::firstThread(void* arg)
{
myClass* this_ptr = (myClass*) arg;
pthread_create(&this_ptr->_secondThreadHandler, NULL, &secondThread, (void *)arg);
while(this_ptr->_isStart) {
usleep(10000);
std::cout << "first" << std::endl;
} return nullptr;
}

void* myClass::secondThread(void* arg)
{
myClass* this_ptr = (myClass*) arg;
while(this_ptr->_isStart) {
usleep(10000);
std::cout << "second" << std::endl;
} return nullptr;
}

上述代码是已经纠正后的正确版本。

而在项目代码中,在修正前遇到的问题是,firstThread 能够正常 join 回收,跳出 while 循环,而 secondThread 却无法正常回收,无法跳出 while 循环。出错的原因是 secondThread 漏了返回,即漏了 return nullptr; 这一句。

但,神奇的是,从现象看就是一直在打印 second ,看似没有跳出 while 循环,但其实已经跳出了,只不过没有 return,那为什么还会一直打印呢?搞不懂。

而且,简化后的示例代码即使漏写 return 语句,也不会出现上述问题。玄学。

# 3、指针作为类内成员变量的初始化问题

对于类内的指针,请务必确保其正确初始化。
要么在定义时直接赋值为 nullptr,要么在类的构造函数中赋值,否则容易出现以下问题:

1
2
3
4
5
6
7
8
{
if(myptr == nullptr) { // 如果我们企图通过指针是否为空来判断是否进行新对象的创建
myptr = new myClass;
} // 那么如果 myptr 没有被初始化赋值,那么其值未定义,是野指针,此时不会走入这个创建逻辑

myptr->callsomefunc(); // 如果此时我们企图利用这个虽然非空但是并不指向对象的野指针调用函数
// 那么我们就会遇到段错误 -- 这里我们没有成功创建对象,于是这里空指针企图调用函数,crash
}

在 C++ 中,未初始化的指针的值是未定义的,它可能是任意值,包括零。这是因为在创建对象时,编译器并不保证为每个成员变量初始化一个特定的值,因此它们的初始状态是不确定的。

即使你没有显式地初始化指针成员变量,它的值也可能每次都是零,这可能是由于编译器或运行时库的一些默认行为。这些默认行为可能会导致未初始化的指针被赋予特定的值,但这并不是标准规定的。

当你修改代码并重新编译后,编译器可能生成了不同的机器代码,或者编译器的优化策略发生了变化,这可能会导致未初始化的指针的值发生变化。在这种情况下,由于未初始化的指针的值本身是未定义的,它可以在不同的编译器、编译选项或运行时环境下表现出不同的行为。

总之,依赖未初始化指针的具体值是一种不好的编程实践,因为它可能导致不可预测的行为。最好的做法是始终显式初始化指针,以确保它具有可预测的初始状态。

# 4、线程函数中访问已销毁的结构体对象中的指针问题

大致场景抽象如下:

我们定义了一个结构体,在结构体中存储了两个指针:

1
2
3
4
struct ThreadParams {
rgbDriver* rgbImpl;
tofDriver* tofImpl;
};

之所以定义这么一个结构体,是因为我们希望同时传递两个指针到一个线程中:

1
2
3
4
5
6
7
8
9
10
ThreadParams threadParams;
threadParams.rgbImpl = _rgb_impl;
threadParams.tofImpl = _tof_impl;

//其中,_rgb_impl 和 _tof_impl 是两个指针

pthread_create(&_mipiTriggerThreadHandle,
NULL,
&mipiTriggerFunc,
static_cast<void*>(pthreadParams));

然后,我们在线程中去获取这两个指针:

1
2
3
4
5
6
7
8
void* MIPI::mipiTriggerFunc(void* arg) {
ThreadParams* params = static_cast<ThreadParams*>(arg);
rgbDriver* rgbImpl = params->rgbImpl;
tofDriver* tofImpl = params->tofImpl;

// 其他业务逻辑...
// ...
}

现在问题来了,即使我们在外面传递的指针是空指针,如:

1
2
3
4
5
6
7
8
ThreadParams threadParams;
threadParams.rgbImpl = nullptr;
threadParams.tofImpl = nullptr;

pthread_create(&_mipiTriggerThreadHandle,
NULL,
&mipiTriggerFunc,
static_cast<void*>(pthreadParams));

我们在线程函数 mipiTriggerFunc 中仍然可能得到非空的指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void* MIPI::mipiTriggerFunc(void* arg) {
ThreadParams* params = static_cast<ThreadParams*>(arg);
rgbDriver* rgbImpl = params->rgbImpl;
tofDriver* tofImpl = params->tofImpl;

if(rgbImpl) {
printf("这一行还是有可能会被打印");
}
if(tofImpl) {
printf("这一行还是有可能会被打印");
}

// 其他业务逻辑...
// ...
}

实际情况就是这两个指针非空,于是造成后续的逻辑出错、混乱,甚至出现段错误,因为后续逻辑试图访问一个野指针的成员变量。

(又或者,即使我们这两个指针都是非空的可用的指针,当我们传递进去之后,在线程函数中得到的仍然会是野指针,同样会导致出错)

之所以造成这个现象,是因为我们传递的是一个临时结构体变量,在线程函数执行过程中,这个临时结构体变量就(可能)被销毁了,导致在线程函数中访问到的是野指针。

所以要解决这个问题也很简单,动态分配内存,使得该结构体对象不会被自动销毁。

1
2
3
4
5
6
ThreadParams* pthreadParams = new ThreadParams;
pthreadParams->rgbImpl = _rgb_impl;
pthreadParams->tofImpl = _tof_impl;

// 记得在线程函数不再使用这两个指针时释放结构体的内存
delete pthreadParams;

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/12/C-\351\242\204\345\210\266\350\217\234/index.html" "b/2024/10/12/C-\351\242\204\345\210\266\350\217\234/index.html" index d911b9f..8f233c0 100644 --- "a/2024/10/12/C-\351\242\204\345\210\266\350\217\234/index.html" +++ "b/2024/10/12/C-\351\242\204\345\210\266\350\217\234/index.html" @@ -1 +1 @@ -C++ 预制菜 - C++ - 编程 | R1ck's Playground = how

# i、文件流构建字符串

1
2
3
std::ostringstream oss;
oss << v.major << "." << v.minor << "." << v.patch;
string str = oss.str();

# i、文件流分割字符串:

1
2
3
4
5
6
7
//对于给定的以空格分割的字符串“A boy is standing outside”
string word;
stringstream out(str);
vector<string> words;
while(out >> word){
words.push_back(word);
}

关于流的更多细节,见:[[C++ 的流]]

# i、将整型 int 转化为字符串 string :

1
2
//利用函数 to_string,将 int 转化为 string
str += to_string(cur->val);

# i、将字符串 string 转化为整型 int:

1
2
3
stoi(str);
//or
atoi(str.c_str());

# i、string 取子串:

1
2
//注意第二个参数指的是子串的长度,而不是子串尾部的位置
string cur = s.substr(vec[0], vec[1] - vec[0] + 1);

利用这个,我们可以灵活组织新的字符串而不影响原有字符串。场景举例:
当我们想要将某个字符串的后缀进行更换、但不希望影响原有字符串,则可以取子串后拼接:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>

int main() {
std::string encryptedFile = "xxx.bin";

// 创建一个新的字符串,替换 ".bin" 为 ".txt"
std::string decryptedFile = encryptedFile.substr(0, encryptedFile.length() - 4) + ".txt";
// 输出修改后的文件名
std::cout << decryptedFile << std::endl;
return 0;
}

# i、string 查找子串:

1
2
3
4
string str = "Hello, World!";
if (str.find(subStr) != std::string::npos) {
//xxx
}

# i、string 查找前后缀:

1
2
3
4
5
6
7
8
//比如:/home/pcai/how/DeptheyeSDK/PointCloud/3DToF/boards/DepthEyeDual/cameraPairing.conf
size_t lastSlashPos = exePath.rfind('/');
if(lastSlashPos != std::string::npos) {
std::string prefixPath = exePath.substr(0, lastSlashPos);
std::cout << "prefixPath = " << prefixPath << std::endl;
std::string postfixPath = exePath.substr(lastSlashPos + 1);
std::cout << "postfixPath = " << postfixPath << std::endl;
}

# i、string 删除元素:

1
2
3
4
5
string str = " hello world   ";
str.erase(std::remove(str.begin(), str.end(), ' '), str.end());
//这里删除所有 str 中所有的空格
//std::remove 的作用仅仅是把所有的空格移动到字符串末尾,并返回最终有效的尾部 pos
//erase 的作用则是删除从最终有效的末尾到实际末尾的所有空格

# i、C++ 延时

1
2
3
4
5
6
#include <iostream>
#include <thread>
#include <chrono>

std::this_thread::sleep_for(std::chrono::seconds(1));
std::this_thread::sleep_for(std::chrono::milliseconds(500));

# i、partial_sum 求前缀和:

1
2
3
4
#include <algorithm>
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> prefixSum(numbers.size()+1);
std::partial_sum(numbers.begin(), numbers.end(), prefixSum.begin()+1);

# i、Ctrl C 中断函数:

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
36
37
#include <iostream>
#include <csignal>

void ST_HandleSig(int signal) { //处理 Ctrl C 中断
std::cout << "Received signal: " << signal << std::endl;
}

int main() {
std::signal(SIGINT, ST_HandleSig); //注册中断函数

while (true) {
// Your game loop or other code here
} return 0;
}

//============================不太方便的C语言版本================================//
// void ST_HandleSig(int signo)
// {
// if(signo == SIGINT)
// {
// printf("catch Ctrl + C, exit normally\n");
// g_bExit = true;
// }
// }

// int main() {
// //注册中断函数
// struct sigaction sigAction;
// sigAction.sa_handler = ST_HandleSig;
// sigemptyset(&sigAction.sa_mask);
// sigAction.sa_flags = 0;
// sigaction(SIGINT, &sigAction, NULL);

// while (true) {
// // Your game loop or other code here
// } return 0;
// }

# i、读取固定格式数据表

该语法糖可用于读写寄存器,利用 vector 和 tuple:

1
2
3
4
5
6
7
8
9
std::vector<std::tuple<int, int>> reg_table = {
{0x1006, 0x08},
{0x1007, 0x00},
{0x1040, 0x00},
};

for(auto item : reg_table) {
std::cout << std::get<0>(item) << ":" << std::get<1>(item) << std::endl;
}

这里当然也可以在 vector 内部嵌套使用 vector,形成二维数组,但没有 tuple 灵活:

对于这个特定的数据结构,使用 std::vector<std::vector<int>> 存储也是可以的,但使用 std::tuple 更能准确地反映每个元素包含三个不同类型的数据。在这种情况下,每个元素都包含一个地址(整数),一个值 1(整数),和一个值 2(整数)。使用 std::tuple 可以更清晰地表示这个数据结构的含义。

如果使用 std::vector<std::vector<int>> ,则需要确保每个内部 std::vector 都包含三个整数,并且在访问数据时需要小心确保每个内部 std::vector 的大小都是 3。这可能会导致代码更容易出错,并且不如 std::tuple 那样清晰。

综上所述,选择使用 std::tuple 可以提高代码的可读性和清晰度,以及更准确地表示每个元素的结构。

例如:

1
2
3
4
5
6
7
8
9
std::vector<std::tuple<int, char, int>> reg_table = {
{0x1006, 'a', 0x08},
{0x1007, 'g', 0x00},
{0x1040, 'h', 0x00},
};

for(auto item : reg_table) {
std::cout << std::get<0>(item) << ":" << std::get<1>(item) << ":" << std::get<2>(item) << std::endl;
}

对于这种情况,tuple 更方便灵活,可以使用不同的数据类型,而 vector 则会限制每个元素都归属于同一种类型。

对于 tuple 元素的访问,可以通过 get 函数(如上所示),也可以利用 C17 的 [[C17 新特性 #1、结构化绑定 | 结构化绑定]] 语法进行优化:

1
2
3
4
5
6
7
8
std::tuple<int, double, std::string> myTuple(42, 3.14, "Hello");

// 使用结构化绑定
auto [intValue, doubleValue, stringValue] = myTuple;

std::cout << "整数值: " << intValue << std::endl;
std::cout << "双精度值: " << doubleValue << std::endl;
std::cout << "字符串值: " << stringValue << std::endl;

于是前面的代码可以优化成:

1
2
3
4
5
6
7
8
9
10
std::vector<std::tuple<int, char, int>> reg_table = {
{0x1006, 'a', 0x08},
{0x1007, 'g', 0x00},
{0x1040, 'h', 0x00},
};

for(auto item : reg_table) {
auto [addr, ch, val] = item;
std::cout << addr << ":" << ch << ":" << val << std::endl;
}

# i、剥取文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
std::string getFilename(const std::string inputFile) {
std::string fileName = inputFile;
size_t lastSlashPos = inputFile.rfind('/');
size_t lastBackslashPos = inputFile.rfind('\\');
size_t lastSeparatorPos = std::max((lastSlashPos == std::string::npos) ? 0 : lastSlashPos,
(lastBackslashPos == std::string::npos) ? 0 : lastBackslashPos);

if (lastSeparatorPos != 0) {
fileName = inputFile.substr(lastSeparatorPos + 1);
std::cout << "File Name: " << fileName << std::endl;
}
return fileName;
}

这里函数的输入是某个文件的路径,可能的形式为 /path/to/myfolder/file.txt../../file.binfile.txt ,该函数的作用则是剥取最后的文件名。(这里考虑了 \/ 两种文件分隔符形式,注意这里的 \\ 为转义)。

# i、获取可执行程序的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
auto getExePath = [](){
std::string exePath(100, ' ');
if (readlink("/proc/self/exe", const_cast<char*>(exePath.c_str()), exePath.size()) != -1) {
// std::cout << "Executable path: " << exePath << std::endl;
size_t lastSlashPos = exePath.rfind('/');
if(lastSlashPos != std::string::npos) {
std::string prefixPath = exePath.substr(0, lastSlashPos);
return prefixPath;
} else {
std::cout << "split prefixPath error" << std::endl;
return std::string();
}
} else {
std::cout << "getExePath error" << std::endl;
return std::string();
}
};

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 预制菜 - C++ - 编程 | R1ck's Playground = how

# i、文件流构建字符串

1
2
3
std::ostringstream oss;
oss << v.major << "." << v.minor << "." << v.patch;
string str = oss.str();

# i、文件流分割字符串:

1
2
3
4
5
6
7
//对于给定的以空格分割的字符串“A boy is standing outside”
string word;
stringstream out(str);
vector<string> words;
while(out >> word){
words.push_back(word);
}

关于流的更多细节,见:[[C++ 的流]]

# i、将整型 int 转化为字符串 string :

1
2
//利用函数 to_string,将 int 转化为 string
str += to_string(cur->val);

# i、将字符串 string 转化为整型 int:

1
2
3
stoi(str);
//or
atoi(str.c_str());

# i、string 取子串:

1
2
//注意第二个参数指的是子串的长度,而不是子串尾部的位置
string cur = s.substr(vec[0], vec[1] - vec[0] + 1);

利用这个,我们可以灵活组织新的字符串而不影响原有字符串。场景举例:
当我们想要将某个字符串的后缀进行更换、但不希望影响原有字符串,则可以取子串后拼接:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>

int main() {
std::string encryptedFile = "xxx.bin";

// 创建一个新的字符串,替换 ".bin" 为 ".txt"
std::string decryptedFile = encryptedFile.substr(0, encryptedFile.length() - 4) + ".txt";
// 输出修改后的文件名
std::cout << decryptedFile << std::endl;
return 0;
}

# i、string 查找子串:

1
2
3
4
string str = "Hello, World!";
if (str.find(subStr) != std::string::npos) {
//xxx
}

# i、string 查找前后缀:

1
2
3
4
5
6
7
8
//比如:/home/pcai/how/DeptheyeSDK/PointCloud/3DToF/boards/DepthEyeDual/cameraPairing.conf
size_t lastSlashPos = exePath.rfind('/');
if(lastSlashPos != std::string::npos) {
std::string prefixPath = exePath.substr(0, lastSlashPos);
std::cout << "prefixPath = " << prefixPath << std::endl;
std::string postfixPath = exePath.substr(lastSlashPos + 1);
std::cout << "postfixPath = " << postfixPath << std::endl;
}

# i、string 删除元素:

1
2
3
4
5
string str = " hello world   ";
str.erase(std::remove(str.begin(), str.end(), ' '), str.end());
//这里删除所有 str 中所有的空格
//std::remove 的作用仅仅是把所有的空格移动到字符串末尾,并返回最终有效的尾部 pos
//erase 的作用则是删除从最终有效的末尾到实际末尾的所有空格

# i、C++ 延时

1
2
3
4
5
6
#include <iostream>
#include <thread>
#include <chrono>

std::this_thread::sleep_for(std::chrono::seconds(1));
std::this_thread::sleep_for(std::chrono::milliseconds(500));

# i、partial_sum 求前缀和:

1
2
3
4
#include <algorithm>
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> prefixSum(numbers.size()+1);
std::partial_sum(numbers.begin(), numbers.end(), prefixSum.begin()+1);

# i、Ctrl C 中断函数:

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
36
37
#include <iostream>
#include <csignal>

void ST_HandleSig(int signal) { //处理 Ctrl C 中断
std::cout << "Received signal: " << signal << std::endl;
}

int main() {
std::signal(SIGINT, ST_HandleSig); //注册中断函数

while (true) {
// Your game loop or other code here
} return 0;
}

//============================不太方便的C语言版本================================//
// void ST_HandleSig(int signo)
// {
// if(signo == SIGINT)
// {
// printf("catch Ctrl + C, exit normally\n");
// g_bExit = true;
// }
// }

// int main() {
// //注册中断函数
// struct sigaction sigAction;
// sigAction.sa_handler = ST_HandleSig;
// sigemptyset(&sigAction.sa_mask);
// sigAction.sa_flags = 0;
// sigaction(SIGINT, &sigAction, NULL);

// while (true) {
// // Your game loop or other code here
// } return 0;
// }

# i、读取固定格式数据表

该语法糖可用于读写寄存器,利用 vector 和 tuple:

1
2
3
4
5
6
7
8
9
std::vector<std::tuple<int, int>> reg_table = {
{0x1006, 0x08},
{0x1007, 0x00},
{0x1040, 0x00},
};

for(auto item : reg_table) {
std::cout << std::get<0>(item) << ":" << std::get<1>(item) << std::endl;
}

这里当然也可以在 vector 内部嵌套使用 vector,形成二维数组,但没有 tuple 灵活:

对于这个特定的数据结构,使用 std::vector<std::vector<int>> 存储也是可以的,但使用 std::tuple 更能准确地反映每个元素包含三个不同类型的数据。在这种情况下,每个元素都包含一个地址(整数),一个值 1(整数),和一个值 2(整数)。使用 std::tuple 可以更清晰地表示这个数据结构的含义。

如果使用 std::vector<std::vector<int>> ,则需要确保每个内部 std::vector 都包含三个整数,并且在访问数据时需要小心确保每个内部 std::vector 的大小都是 3。这可能会导致代码更容易出错,并且不如 std::tuple 那样清晰。

综上所述,选择使用 std::tuple 可以提高代码的可读性和清晰度,以及更准确地表示每个元素的结构。

例如:

1
2
3
4
5
6
7
8
9
std::vector<std::tuple<int, char, int>> reg_table = {
{0x1006, 'a', 0x08},
{0x1007, 'g', 0x00},
{0x1040, 'h', 0x00},
};

for(auto item : reg_table) {
std::cout << std::get<0>(item) << ":" << std::get<1>(item) << ":" << std::get<2>(item) << std::endl;
}

对于这种情况,tuple 更方便灵活,可以使用不同的数据类型,而 vector 则会限制每个元素都归属于同一种类型。

对于 tuple 元素的访问,可以通过 get 函数(如上所示),也可以利用 C17 的 [[C17 新特性 #1、结构化绑定 | 结构化绑定]] 语法进行优化:

1
2
3
4
5
6
7
8
std::tuple<int, double, std::string> myTuple(42, 3.14, "Hello");

// 使用结构化绑定
auto [intValue, doubleValue, stringValue] = myTuple;

std::cout << "整数值: " << intValue << std::endl;
std::cout << "双精度值: " << doubleValue << std::endl;
std::cout << "字符串值: " << stringValue << std::endl;

于是前面的代码可以优化成:

1
2
3
4
5
6
7
8
9
10
std::vector<std::tuple<int, char, int>> reg_table = {
{0x1006, 'a', 0x08},
{0x1007, 'g', 0x00},
{0x1040, 'h', 0x00},
};

for(auto item : reg_table) {
auto [addr, ch, val] = item;
std::cout << addr << ":" << ch << ":" << val << std::endl;
}

# i、剥取文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
std::string getFilename(const std::string inputFile) {
std::string fileName = inputFile;
size_t lastSlashPos = inputFile.rfind('/');
size_t lastBackslashPos = inputFile.rfind('\\');
size_t lastSeparatorPos = std::max((lastSlashPos == std::string::npos) ? 0 : lastSlashPos,
(lastBackslashPos == std::string::npos) ? 0 : lastBackslashPos);

if (lastSeparatorPos != 0) {
fileName = inputFile.substr(lastSeparatorPos + 1);
std::cout << "File Name: " << fileName << std::endl;
}
return fileName;
}

这里函数的输入是某个文件的路径,可能的形式为 /path/to/myfolder/file.txt../../file.binfile.txt ,该函数的作用则是剥取最后的文件名。(这里考虑了 \/ 两种文件分隔符形式,注意这里的 \\ 为转义)。

# i、获取可执行程序的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
auto getExePath = [](){
std::string exePath(100, ' ');
if (readlink("/proc/self/exe", const_cast<char*>(exePath.c_str()), exePath.size()) != -1) {
// std::cout << "Executable path: " << exePath << std::endl;
size_t lastSlashPos = exePath.rfind('/');
if(lastSlashPos != std::string::npos) {
std::string prefixPath = exePath.substr(0, lastSlashPos);
return prefixPath;
} else {
std::cout << "split prefixPath error" << std::endl;
return std::string();
}
} else {
std::cout << "getExePath error" << std::endl;
return std::string();
}
};

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\344\273\200\351\224\246/index.html" "b/2024/10/18/C-\344\273\200\351\224\246/index.html" index 285f7f6..ba11992 100644 --- "a/2024/10/18/C-\344\273\200\351\224\246/index.html" +++ "b/2024/10/18/C-\344\273\200\351\224\246/index.html" @@ -1 +1 @@ -C++ 什锦 - C++ - 编程 | R1ck's Playground = how

# i、右值引用

# 概述

右值引用是 C++11 引入的一种新的引用类型,用于支持移动语义完美转发。与传统的左值引用(左值引用)不同,右值引用绑定到临时对象将要销毁的对象或者显式转换为右值引用的对象。右值引用的特点是可以接管资源,并实现高效的资源移动操作。

在 C++ 中,我们可以通过在类型名称前添加 && 来声明右值引用。例如, int&& 表示一个右值引用类型的整数。

右值引用的一些关键概念包括:

  1. 左值(Lvalue):左值是指一个具有标识符的、可寻址的对象。它可以出现在赋值表达式的左边或右边,并且具有持久的状态。左值引用(左值的引用)可以绑定到左值。
  2. 右值(Rvalue):右值是指一个临时的、无法寻址的对象。它通常是一个临时表达式的结果,如常量、临时对象、将要销毁的对象等。右值引用可以绑定到右值。
  3. 移动语义(Move Semantics):移动语义是通过右值引用实现的一种特性,允许将资源(如堆内存)从一个对象移动到另一个对象,而不是进行复制。移动语义可以提高性能,避免不必要的内存拷贝和资源分配。
  4. 完美转发(Perfect Forwarding):完美转发是指在函数模板中以原样传递参数,既不进行拷贝也不进行移动,保持其原始类型。通过使用右值引用和模板参数推导,可以实现完美转发,将参数传递给下游函数,保持参数的值类别(左值或右值)和常量性。

右值引用的引入使得 C++ 语言能够更好地处理资源管理和移动语义,避免不必要的数据拷贝,提高程序的效率和性能。它在移动语义、完美转发、智能指针等方面发挥了重要的作用。

# 移动语义

当涉及到资源管理或对象传递时,移动语义完美转发可以提供更高效的操作和灵活性。

移动语义: 移动语义允许将资源(如堆内存)从一个对象移动到另一个对象,而不是进行复制。这可以避免不必要的内存拷贝和资源分配,提高程序的效率。

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
36
37
#include <iostream>
#include <vector>

class MyString {
private:
char* m_data;

public:
MyString(const char* str) {
// 分配内存并复制字符串
size_t length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
std::cout << "Constructor: " << m_data << std::endl;
}

// 移动构造函数
MyString(MyString&& other) noexcept {
// 直接接管资源
m_data = other.m_data;
other.m_data = nullptr;
std::cout << "Move Constructor: " << m_data << std::endl;
}

~MyString() {
delete[] m_data;
}
};

int main() {
MyString str1("Hello"); // 调用构造函数

MyString str2(std::move(str1)); // 调用移动构造函数
// 此时str1不再拥有资源,而是被str2接管了

return 0;
}

在上面的示例中,我们定义了一个简单的字符串类 MyString ,它包含了一个动态分配的字符数组。通过移动构造函数,我们可以直接将资源从一个对象移动到另一个对象,而不需要进行额外的内存拷贝。在 main 函数中,我们创建了 str1str2 两个对象,通过 std::movestr1 的资源移动给了 str2 。这样,资源的所有权从 str1 转移到了 str2 ,并在程序结束时正确释放。

# 完美转发

完美转发允许以原样传递参数,既不进行拷贝也不进行移动,保持其原始类型。 通过使用右值引用模板参数推导,可以实现完美转发,将参数传递给下游函数,保持参数的值类别和常量性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <utility>

void processValue(int& value) {
std::cout << "Lvalue: " << value << std::endl;
}

void processValue(int&& value) {
std::cout << "Rvalue: " << value << std::endl;
}

template <typename T>
void forwardValue(T&& value) {
processValue(std::forward<T>(value));
}

int main() {
int x = 42;
forwardValue(x); // 传递左值,调用 processValue(int&)
forwardValue(123); // 传递右值,调用 processValue(int&&)
forwardValue(std::move(x)); // 传递右值引用,调用 processValue(int&&)
return 0;
}

在上面的示例中,我们定义了两个重载的 processValue 函数,一个接受左值引用,另一个接受右值引用。然后,我们使用模板函数 forwardValue 来实现完美转发。在 forwardValue 函数中,我们使用 std::forward 来保持参数的值类别(左值或右值)和常量性,并将参数传递给下游函数 processValue 。通过完美转发,我们可以在保持原始参数类型的同时,将参数传递给适当的处理函数。

通过移动语义和完美转发,我们可以更高效地管理资源和实现灵活的参数传递。这些特性在处理大型对象、容器元素和函数传参时特别有用,可以避免不必要的拷贝和资源分配,提高程序的性能和效率。

# 追问

1、移动构造函数中的 noexcept 有何作用,是否一定需要?

noexcept 在移动构造函数中的作用是指定该函数是否可能抛出异常。使用 noexcept 关键字可以提供编译器优化的机会,因为它使得在移动构造函数中执行更轻量级的操作,如移动资源的所有权,而无需进行异常处理。这样可以提高程序的性能。然而, noexcept 并不是必需的,你可以选择是否在移动构造函数中使用它,具体取决于你的需求。

2、调用 std::move 后的变量是否不能够再次被使用?

调用 std::move 后的变量仍然可以被使用,但是它的状态会发生变化。 std::move 将变量转换为右值引用,这意味着它可以被移动而不是复制。移动后的变量的状态通常是不确定的,你不应该对其进行操作或访问其值。它通常被用于将资源所有权转移给其他对象,或作为参数传递给接受右值引用的函数。在移动后,你可以重新赋值给它或销毁它。重要的是要记住, std::move 仅仅是改变了变量的类型,而不会对其值进行任何修改

# i、C++ 内存分布

在 C++ 程序中,内存可以划分为以下几个主要区域:

  1. 栈(Stack):
    • 栈位于内存的较高地址部分。
    • 栈用于存储函数的局部变量、函数参数、函数调用信息等。
    • 栈的分配和释放是由编译器自动管理的,具有自动内存管理的特性。
    • 栈的大小在程序运行时是固定的。
  2. 堆(Heap):
    • 堆位于内存的较低地址部分。
    • 堆用于动态分配内存,由程序员手动管理。
    • 堆的分配和释放需要使用特定的函数(如 newdeletemallocfree 等)进行操作。
    • 堆的大小在程序运行时可以动态变化。
  3. 全局区(Global Area):
    • 全局区也称为静态区或数据段。
    • 全局区存储全局变量、静态变量和常量。
    • 全局区在程序运行期间一直存在,直到程序结束。
    • 全局区的大小在编译时确定。
  4. 常量区(Constant Area):
    • 常量区也称为文字常量区或只读数据区。
    • 常量区存储字符串常量和其他常量数据。
    • 常量区的数据是只读的,不能被修改。
    • 常量区在程序运行期间一直存在,直到程序结束。
  5. 代码区(Code Area):
    • 代码区也称为文本区或只读代码区。
    • 代码区存储程序的执行代码。
    • 代码区的数据是只读的,不能被修改。
    • 代码区在程序运行期间一直存在,直到程序结束。

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
+-----------------------+
| 栈 (Stack) |
| |
| |
| |
+-----------------------+
| 堆 (Heap) |
| |
| |
| |
+-----------------------+
| 全局区 (Global) |
| |
| |
| |
+-----------------------+
| 常量区 (Constant) |
| |
| |
| |
+-----------------------+
| 代码区 (Code) |
| |
| |
| |
+-----------------------+

# i、C++ 内存模型

# 概述

C++ 内存模型是描述 C++ 程序在执行过程中,内存访问和操作的规则和保证的规范。它定义了多线程环境下的原子性操作、内存顺序、可见性等行为,以确保多线程程序的正确性和可预测性。

下面是 C++ 内存模型中的几个重要概念:

  1. 原子性(Atomicity):原子操作是不可被中断的单个操作,要么完全执行,要么不执行。C++ 提供了原子操作库(std::atomic),用于在多线程环境下实现原子操作,保证对共享变量的读写操作的原子性。
  2. 内存顺序(Memory Order):内存顺序规定了多个线程对共享变量的读写操作的顺序。C++ 提供了一组枚举类型(如 std::memory_order_relaxedstd::memory_order_acquirestd::memory_order_release 等),用于指定不同的内存顺序要求。
  3. 可见性(Visibility):可见性指的是一个线程对共享变量的修改对其他线程是否可见。C++ 内存模型确保了原子操作的可见性,即一个原子操作对其他线程是立即可见的。
  4. Happens-Before 关系:Happens-Before 关系是 C++ 内存模型中的一个重要概念,它用于描述不同操作之间的顺序关系。如果一个操作 A Happens-Before 于另一个操作 B,那么在多线程环境下,操作 A 的结果对操作 B 是可见的。Happens-Before 关系可以由同步操作(如互斥锁、原子操作等)和特定的内存顺序关系来建立。

C++ 内存模型提供了一系列的规则和保证,以帮助程序员编写正确且具有可移植性的多线程程序。它规定了各种操作之间的可见性和顺序关系,并提供了原子操作和内存顺序的机制,使得程序在多线程环境下能够正确地进行内存访问和操作。

需要注意的是,C++ 内存模型并不是操作系统的内存管理模型,它更关注于程序在多线程环境下的内存访问规则和行为。对于底层的内存管理细节(如页面管理、缓存一致性等),C++ 内存模型通常依赖于操作系统和硬件的支持。

# 内存顺序

当多个线程并发地访问和修改共享变量时,内存顺序定义了这些操作之间的顺序关系。C++ 中的内存顺序通过枚举类型 std::memory_order 来指定。

  1. std::memory_order_relaxed (松散顺序): std::memory_order_relaxed 是最宽松的内存顺序,它不对任何内存操作进行顺序限制。对于使用 std::memory_order_relaxed 的操作,编译器和处理器可以对其进行重排序,且不会对其他线程产生任何顺序上的约束。
  2. std::memory_order_acquire (获取顺序):稍后解释。
  3. std::memory_order_release (释放顺序):稍后解释。

当多个线程并发地读写共享变量时,使用 std::memory_order_acquirestd::memory_order_release 可以确保读写操作之间的顺序关系和可见性。

std::memory_order_acquire 用于读操作,它具有以下特性:

  1. 在使用 std::memory_order_acquire 进行读操作时,该读操作之前的所有读写操作都不会被放置在该读操作之后。这意味着,使用 std::memory_order_acquire 读取的值不会是之前的写操作的过期值。
  2. std::memory_order_acquire 会建立 Happens-Before 关系,使得该读操作之前的写操作对当前线程可见。这意味着,在使用 std::memory_order_acquire 进行读操作后,当前线程能够观察到在该读操作之前发生的写操作所引入的更改。
  3. std::memory_order_acquire 并不会限制该读操作之后的任何操作的顺序,其他线程可以继续并发地读写共享变量。
    相比于 std::memory_order_relaxed ,使用 std::memory_order_acquire 可以提供更强的内存顺序保证,确保了读操作的顺序关系和可见性。

std::memory_order_release 用于写操作,它具有以下特性:

  1. 在使用 std::memory_order_release 进行写操作时,该写操作之后的所有读写操作都不会被放置在该写操作之前。这意味着,使用 std::memory_order_release 写入的值不会影响之后的读写操作的顺序。
  2. std::memory_order_release 会建立 Happens-Before 关系,使得该写操作对其他线程的读操作可见。这意味着,在使用 std::memory_order_release 进行写操作后,其他线程能够观察到该写操作所引入的更改。
  3. std::memory_order_release 并不会限制该写操作之前的任何操作的顺序,其他线程可以继续并发地读写共享变量。

通过使用 std::memory_order_acquirestd::memory_order_release ,可以在多线程环境下实现同步原语,例如互斥锁或读写锁。一个线程在写入共享变量之前,使用 std::memory_order_release 进行写操作,而另一个线程在读取共享变量之前,使用 std::memory_order_acquire 进行读操作。这样可以确保数据的一致性和可见性,防止数据竞争和不确定行为的发生。

需要注意的是, std::memory_order_acquirestd::memory_order_release 是成对使用的。当一个线程使用 std::memory_order_release 顺序进行写操作时,另一个线程使用 std::memory_order_acquire 顺序进行相应的读操作,以建立 Happens-Before 关系。单独使用这两个内存顺序是不够的,要实现正确的同步,需要遵循正确的使用模式和配对操作。

这里需要注意的是, std::memory_order_acquirestd::memory_order_release 是一对配套使用的内存顺序,用于实现同步原语,例如互斥锁、读写锁等。当一个线程使用 std::memory_order_release 顺序进行写操作时,另一个线程使用 std::memory_order_acquire 顺序进行相应的读操作,这样可以建立 Happens-Before 关系,确保数据的正确同步。

std::memory_order_acquirestd::memory_order_release 的主要目的是确保读操作和写操作之间的顺序关系和可见性,并防止编译器和处理器对其进行重排优化。

简单来说,为了确保同步,读用 std::memory_order_acquire ,写用 std::memory_order_release 即可。

# i、全局变量与 static

# 全局变量

全局变量是定义在函数外部、整个源文件都可以访问的变量。它具有全局作用域和静态生存期,意味着它在整个程序的执行过程中都存在,并且可以被程序中的任何函数所使用。

全局变量的特点包括:

  1. 作用域:全局变量的作用域从定义处开始一直延伸到文件的末尾,整个源文件中的任何函数都可以访问这个变量。
  2. 生命周期:全局变量在程序启动时分配内存,在程序结束时释放内存,因此它们的生命周期与整个程序的运行时间一样长。
  3. 默认初始化:如果全局变量没有显式地初始化,那么它们会被默认初始化。
  4. 存储位置:全局变量的存储位置通常位于静态存储区,在程序启动时分配,程序结束时释放。因此,全局变量的内存空间在整个程序的执行过程中都是固定的。
  5. 可见性:全局变量在整个源文件中都是可见的,因此在不同的函数中可以共享数据。但是,过度使用全局变量会导致代码的可读性和维护性降低,因此应该尽量避免滥用全局变量,尤其是在大型项目中。

全局变量的定义和声明一般放在源文件的顶部,在任何函数外部,使用关键字 extern 可以在其他源文件中引用全局变量。虽然全局变量具有方便性,但也容易引起命名冲突和程序设计的复杂性,因此在实际编程中需要谨慎使用。

# 静态全局变量

如果在全局变量的定义前加上 static 关键字进行修饰,则该变量就成为 静态全局变量静态全局变量的作用域仅限于当前文件,不能被其他文件访问。

静态全局变量的特点包括:

  1. 作用域:与全局变量相同,静态全局变量的作用域也从定义处开始一直延伸到文件的末尾,但它只能在当前文件内部访问,其他文件无法访问。
  2. 生命周期:静态全局变量在程序启动时分配内存,在程序结束时释放内存,生命周期与整个程序的执行过程一样长。
  3. 默认初始化:与全局变量相同,如果静态全局变量没有显式地初始化,它们会被默认初始化。
  4. 存储位置:静态全局变量也存储在静态存储区,在程序启动时分配,程序结束时释放,内存空间在整个程序的执行过程中都是固定的。
  5. 可见性:静态全局变量仅在当前文件内可见,不能被其他文件引用,因此不会引起命名冲突。

由于静态全局变量的作用域受限于当前文件,因此它们通常被用于实现文件内部的全局状态或共享数据,同时避免了全局变量可能引发的命名冲突问题。

# 全局变量与静态成员变量

假设我现在有一个 tool.cpp 和 tool.h 文件,其中我定义并实现了 Tool 这个类。
那么,对于某些变量,出于某些共享的需求,我既可以把它设置为全局变量,也可以把它设置成静态成员变量,这两种方式有什么区别?

在 C++ 中,你可以通过将变量设置为全局变量或将其设置为类的静态成员变量来实现共享的需求,但这两种方式有一些区别:

  1. 作用域

    • 全局变量的作用域是整个源文件,其他源文件也可以通过 extern 关键字来引用全局变量。(如果希望其只对 Tool 类可见,可添加 static 关键字使其局限在该 tool.cpp 文件中)
    • 类的静态成员变量的作用域是整个类,可以通过 类名::静态成员变量名 来访问。
  2. 可见性

    • 全局变量的可见性是整个源文件,其他源文件也可以通过 extern 关键字来引用全局变量。
    • 类的静态成员变量的可见性是类的范围内,只有类的成员函数和友元函数可以直接访问静态成员变量。
  3. 命名空间

    • 全局变量属于全局命名空间,可能会导致命名冲突问题,特别是在大型项目中。
    • 类的静态成员变量属于类的命名空间,可以通过类名限定来避免命名冲突。
  4. 初始化

    • 全局变量在整个程序启动时会被初始化,如果没有显式地初始化,则会被默认初始化。
    • 类的静态成员变量需要在类外部进行初始化,可以在类的定义外部进行初始化,也可以在类的实现文件中进行初始化。

综上所述,如果变量需要在多个源文件中共享,则通常选择全局变量;如果变量与类密切相关,并且只需要在类的范围内共享,则选择类的静态成员变量更为合适。
(比如两个源文件要进行线程同步,那么可选择使用全局变量的互斥锁、条件变量。)

# i、静态成员函数

在 C++ 中,静态成员函数是属于类而不是对象实例的函数。它们与类关联,而不是与类的具体实例关联。

# 定义:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
// 静态成员函数
static void staticFunction() {
// 可以访问静态成员和其他静态成员函数
}

// 非静态成员函数
void nonStaticFunction() {
// 可以访问非静态成员和其他成员函数
}
};

# 调用:

1
MyClass::staticFunction(); // 直接通过类名调用,而不需要创建类的实例

# 特点和用途:
  • 与类关联:
    静态成员函数与类关联,而不是与类的具体实例关联。因此,它们不访问任何特定对象的成员,也不能使用非静态成员变量或函数

  • 无 this 指针:
    静态成员函数没有隐含的 this 指针,因为它们不与特定的对象实例相关联。这使得它们在不涉及实例数据的情况下执行更为灵活。(如果涉及,则需要进行绑定)

  • 访问静态成员:
    静态成员函数可以访问类的静态成员变量和其他静态成员函数。这使得它们适用于处理与类而不是与对象实例相关的操作。

  • 全局访问点:
    静态成员函数常用于提供一个全局访问点,例如工厂方法或全局配置设置。

# i、野指针调用类的成员函数

在 C++ 中,当使用野指针(悬空指针)调用类的成员函数时,会发生未定义行为。野指针是指指向已释放的内存或者未初始化的内存的指针。使用野指针访问对象的成员函数可能导致程序崩溃或者产生其他不可预测的结果。

示例代码中的野指针就是将指针 pa 初始化为 nullptr,然后尝试通过它调用类 A 的成员函数 func ()。在这种情况下,由于 pa 指针指向了一个未定义的内存地址(nullptr),调用 func () 会导致未定义行为,这可能会导致程序崩溃。

在编写 C++ 代码时,应该始终确保指针的有效性,避免使用空指针和野指针。在使用指针之前,最好将其初始化为 nullptr,并在使用前进行有效性检查。可以使用条件语句或者智能指针来确保指针指向有效的对象,从而避免悬空指针的问题。

追问:经过我的实践,野指针应该是仍然可以调用到类的成员函数的,只不过其结果是未定义的,是危险的。

实际上,当野指针调用类的成员函数时,确实可以执行函数体内的代码,因为在编译时,编译器只需要知道函数的地址就可以调用它,不会检查指针是否为 nullptr 或者是否指向有效的对象。因此,代码在运行时仍然会执行类的成员函数,但是由于指针是悬空的,因此这样的行为是未定义的,可能会导致程序崩溃或者产生其他不可预测的结果。

虽然野指针调用类的成员函数可能不会立即导致程序崩溃,但这样的行为是非常危险的,可能会导致严重的后果,例如内存访问错误、数据损坏等。因此,应该始终避免使用空指针和野指针,并在使用指针之前进行有效性检查,以确保指针指向有效的对象。

追问:但编译器是怎么知道成员函数的地址的呢?又或者说,类的成员函数存储在哪里?

编译器在编译阶段会为类的成员函数生成代码,并将这些成员函数存储在程序的可执行文件中的代码段(text segment)中。成员函数的地址是由编译器在编译时决定的,通常在编译后的可执行文件中,成员函数会被编译成与类的对象无关的静态函数(static function),这意味着它们不依赖于任何特定的对象实例。

当调用类的成员函数时,实际上是通过类的对象(或类的指针 / 引用)来调用的,编译器会将对象的地址作为隐式的参数传递给成员函数,从而让成员函数知道自己是属于哪个对象的。在函数体内,成员函数可以通过 this 指针来访问类的成员变量和其他成员函数。

由于成员函数存储在代码段中,它们的地址在程序运行时是固定的。当通过类的对象(或指针 / 引用)调用成员函数时,编译器会在运行时使用正确的地址来调用相应的函数。

需要注意的是,对于虚函数(virtual function),编译器会通过虚函数表(vtable)来实现动态绑定,使得在运行时可以根据实际对象的类型来调用正确的虚函数。虚函数表是一个由编译器生成的数据结构,其中存储了每个虚函数的地址。这样,当通过指向基类的指针或引用调用虚函数时,会根据对象的实际类型来查找正确的虚函数地址。

# i、委托构造函数

#CPP 新特性 #CPP11
委托构造函数是 C++11 引入的一个特性,它允许一个构造函数调用同一个类中的另一个构造函数,从而避免代码重复。通过委托构造函数,可以在构造过程中重用已有的构造函数代码。

即,当在一个类中定义多个构造函数,并且其中一些构造函数有共同的初始化逻辑时,委托构造函数可以帮助你避免重复编写相同的初始化代码。

简单举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
public:
// 假设我们在构造函数中都需要做一系列相同的初始化操作
// 比如这里我们简单举例,我们需要计算 x + y 的值

Point() : x(0), y(0) {
// 默认构造函数
int sum = x + y;
}

Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {
// 带参数构造函数
int sum = x + y;
}

Point(int value) : x(value), y(value) {
// 参数为相同值的构造函数
int sum = x + y;
}

private:
int x;
int y;
};

那么上面的代码就是会造成很多冗余,尤其是当 int sum = x + y; 所简化的代码实际上是很多复杂的初始化操作的时候。于是我们就可以用到委托构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {
public:
Point() : Point(0, 0) { // 这里调用另一个构造函数
// 默认构造函数委托给带参数构造函数
}

Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {
// 带参数构造函数
int sum = x + y;
}

Point(int value) : Point(value, value) { // 这里调用另一个构造函数
// 参数为相同值的构造函数委托给带参数构造函数
}

private:
int x;
int y;
};

如上所示,通过委托函数,不再需要每个构造函数都执行 int sum = x + y; 减少了代码的冗余.

委托构造函数指的是主动发起调用的那个构造函数,也就是在构造函数内部通过委托调用另一个构造函数来共享初始化逻辑。被委托调用的构造函数称为目标构造函数。

通过使用委托构造函数,我们避免了重复编写相同的初始化代码。当我们改变构造函数的初始化逻辑时,只需要修改一处,所有委托该逻辑的构造函数都会自动更新。

需要注意以下几点:

  • 委托构造函数的初始化列表中只能调用同一个类中的其他构造函数,而不能调用基类的构造函数或其他类的构造函数。
  • 委托构造函数的参数列表必须与被调用构造函数的参数列表匹配。
  • 委托构造函数只能在成员初始化列表中调用另一个构造函数,不能在函数体中调用
  • 委托构造函数不需要包含构造函数的实际初始化操作,因为它会调用被委托的构造函数来完成初始化。

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
public:
MyClass() : MyClass(0) {
// 委托给其他构造函数
// 可以在委托构造函数中执行一些通用的初始化操作
}

MyClass(int value) : member(value) {
// 实际构造函数,完成特定的初始化操作
}

private:
int member;
};

int main() {
MyClass obj1; // 调用委托构造函数
MyClass obj2(42); // 调用实际构造函数
return 0;
}

委托构造函数的函数体中的代码先执行还是委托构造函数的初始化列表中的目标构造函数先执行?

在委托构造函数中,委托构造函数的初始化列表中的目标构造函数会先执行,然后才会执行委托构造函数的函数体中的代码。

这是因为委托构造函数的目的是将初始化工作委托给另一个构造函数完成,因此首先会执行目标构造函数的初始化列表中的代码,以确保对象的成员变量得到正确的初始化。然后,委托构造函数的函数体中的代码会在目标构造函数初始化之后执行。

以下是一个示例来说明这个顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class MyClass {
public:
MyClass() : MyClass(0) {
std::cout << "委托构造函数的函数体中的代码" << std::endl;
}

MyClass(int value) : member(value) {
std::cout << "目标构造函数的初始化列表中的代码" << std::endl;
}

private:
int member;
};

int main() {
MyClass obj; // 输出顺序:目标构造函数 -> 委托构造函数的函数体
return 0;
}

在上述示例中,对象 obj 的创建过程中,== 首先会执行目标构造函数的初始化列表中的代码,然后执行委托构造函数的函数体中的代码。== 这是因为目标构造函数负责实际的初始化操作,而委托构造函数负责调用目标构造函数并共享初始化逻辑.

总结一下,委托构造函数的好处是提高了代码的可维护性和可读性,避免了重复编写相同的初始化代码,同时确保所有构造函数都使用了一致的初始化逻辑。这对于类的构造函数重用和管理非常有帮助。

实际工程中遇到的委托构造函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class [[nodiscard]] thread_pool
{
public:
thread_pool() : thread_pool(0, [] {}) {}

explicit thread_pool(const concurrency_t num_threads) : thread_pool(num_threads, [] {}) {}

explicit thread_pool(const std::function<void()>& init_task) : thread_pool(0, init_task) {}

thread_pool(const concurrency_t num_threads, const std::function<void()>& init_task) : thread_count(determine_thread_count(num_threads)), threads(std::make_unique<std::thread[]>(determine_thread_count(num_threads)))
{
create_threads(init_task);
}

...
}

# i、using 和 typedef 的区别

#CPP 新特性 #CPP11
using 别名是 C11 标准引入的特性,它提供了更多的灵活性和可读性。使用 using 别名可以轻松地为现有类型创建别名,甚至可以为模板类型创建别名。现代 C 更倾向于使用 using,更加直观明了。

使用 typedef 的方式:

1
2
typedef int MyInt; // 创建 int 的别名 MyInt
typedef std::vector<int> IntVector; // 创建 std::vector<int> 的别名 IntVector

使用 using 的方式:

1
2
using MyInt = int; // 创建 int 的别名 MyInt
using IntVector = std::vector<int>; // 创建 std::vector<int> 的别名 IntVector

using 别名有一些明显的优势:

  1. 可读性更强: using 别名的语法更加自然,易于理解。它直观地表达了创建类型别名的意图。
  2. 支持模板别名: using 别名支持为模板类型创建别名,而 typedef 在处理模板类型时可能会显得复杂。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template <typename T>
    using MyVector = std::vector<T>; // 为 std::vector<T> 创建一个别名 MyVector

    int main() {
    MyVector<int> numbers; // 使用 MyVector 作为 std::vector<int> 的别名
    numbers.push_back(42);
    numbers.push_back(73);

    for(const auto& num : numbers) {
    std::cout << num << " ";
    } std::cout << std::endl;

    return 0;
    }
  3. auto 关联性更强: using 别名与 C++11 中引入的 auto 关联性更强,使得在使用类型推断时更加便利。

特别地,如果是对结构体 struct 的别名,两个关键字的语法分别如下:

1
2
3
4
5
6
7
8
9
typedef struct {
int x;
int y;
} Point;

typedef struct {
float width;
float height;
} Size;

1
2
3
4
5
6
7
8
9
using Point = struct {
int x;
int y;
};

using Size = struct {
float width;
float height;
};

# i、cast 类型转换

在 C++ 中,类型转换(Type Casting)是一种将一个数据类型的值转换为另一个数据类型的过程。C++ 提供了几种不同类型的类型转换,可大致分为隐式转换和显式转换,显式转换又包含:C 风格转换、 static_castdynamic_castconst_castreinterpret_cast

1. static_cast

  • 用途: static_cast 主要用于执行静态类型转换,可以在合理范围内进行类型转换。它用于处理通常是安全的、定义良好的转换。
  • 示例:
    1
    2
    int integer = 42;
    float floatingPoint = static_cast<float>(integer);

2. dynamic_cast

  • 用途: dynamic_cast 主要用于在继承层次结构中执行基类到派生类的安全转换。它在运行时进行类型检查,只有当对象实际上是目标类型的派生类时,才会执行转换。通常与多态相关。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Base { virtual void foo() {} };
    class Derived : public Base { void foo() {} };

    Base* basePtr = new Derived;
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
    // 转换成功,derivedPtr 指向 Derived 类型的对象
    } else {
    // 转换失败,basePtr 不是 Derived 类型的对象
    }

3. const_cast

  • 用途: const_cast 用于去除变量的 const 修饰符,允许修改原本被视为只读的对象。它通常用于修复旧代码或在需要时更改数据的常量属性。
  • 示例:
    1
    2
    3
    const int readOnlyValue = 42;
    int& readWriteReference = const_cast<int&>(readOnlyValue);
    readWriteReference = 100; // 合法,修改了原本的只读变量

4. reinterpret_cast

  • 用途: reinterpret_cast 用于执行低级别的转换,通常用于处理指针和引用之间的转换。这是最不安全的类型转换,因此应谨慎使用。
  • 示例:
    1
    2
    3
    int integerValue = 42;
    int* intPointer = &integerValue;
    double* doublePointer = reinterpret_cast<double*>(intPointer);
# static_cast 和 dynamic_cast 的区别和联系

区别
static_cast

  1. 静态类型转换static_cast 是一种在编译时进行类型转换的操作。它对于已知的、合法的转换非常有用,如整数到浮点数、指针之间的类型转换等。
  2. 非安全性static_cast 不执行运行时类型检查,因此在进行类型转换时,开发者需要确保转换是安全的,否则可能导致未定义的行为。
  3. 用途:主要用于基本的类型转换,如整数之间的转换、指针类型的转换,以及在继承层次结构中进行向上或向下的类型转换。

dynamic_cast

  1. 动态类型转换dynamic_cast 是一种在运行时进行类型转换的操作,主要用于处理继承层次结构中的类型转换。
  2. 安全性dynamic_cast 执行运行时类型检查,确保只有当对象实际上是目标类型的派生类时,才会执行转换。如果类型不匹配,它返回 nullptr (对于指针类型)或抛出 std::bad_cast 异常(对于引用类型)。
  3. 用途:主要用于处理多态性,以安全地进行基类到派生类的类型转换。它在运行时提供了类型检查,可用于确保只有正确类型的对象才会进行转换。

联系

  1. static_castdynamic_cast 都用于类型转换,但它们的用途和时机不同。 static_cast 主要用于基本类型和向上 / 向下的继承层次结构中,而 dynamic_cast 主要用于多态性继承中的安全类型转换。
  2. static_cast 是在编译时执行的,因此更快,但不提供类型检查。 dynamic_cast 是在运行时执行的,提供了类型检查,但可能更慢。
  3. 在多态性和继承的情况下,应优先选择 dynamic_cast 以确保类型安全。 static_cast 更适用于普通的类型转换,如整数之间的转换。
  4. static_castdynamic_cast 都需要开发者谨慎使用,以避免不安全的类型转换和潜在的错误。

在编写代码时,应根据具体情况和安全性需求来选择使用哪种类型转换方式。如果不确定,最好选择更安全的 dynamic_cast

# i、C++ 内部链接

内部链接(internal linkage)是 C++ 语言中的一个概念,用于描述声明或定义的实体在编译单元内部可见,而在其他编译单元中不可见的特性。

具有内部链接的实体只能在其定义所在的编译单元(通常是一个源文件)内部访问,而无法在其他编译单元中进行访问。这样做的目的是将实体限制在特定的作用域内,从而提高代码的模块化性和封装性。

在 C++ 中,可以使用两种方式来实现内部链接:

  1. 未命名命名空间(unnamed namespace):通过将实体放置在未命名命名空间中,可以确保这些实体仅在当前编译单元中可见,而无法在其他编译单元中访问。

    1
    2
    3
    namespace {
    ...
    } // 未命名 namespace

  2. static 关键字:将函数或变量声明为 static,可以使它们具有内部链接。这意味着它们只能在当前编译单元中访问,而不能在其他编译单元中使用。

内部链接是 C++ 编程中常用的技术之一,用于控制实体的可见性和访问范围,有助于提高代码的可维护性和安全性。

# i、extern 关键字

extern 是 C++ 中的一个关键字,用于说明变量或函数的链接性(linkage)和作用域(scope)。它可以用在不同的上下文中,通常用于以下两个方面:

1. 链接性(Linkage):
extern 用于说明一个变量或函数是在其他文件中定义的,而不是当前文件中定义的。它告诉编译器在链接时在其他文件中查找这个变量或函数的定义。在 C++ 中,变量和函数默认情况下具有外部链接性,也就是可以在其他文件中使用。但如果你想明确地指定一个变量或函数为外部链接性,你可以使用 extern 关键字。例如:

1
2
3
4
5
// 声明一个全局变量 x,该变量在其他文件中定义
extern int x;

// 声明一个函数 foo,该函数在其他文件中定义
extern void foo();

这些声明告诉编译器在链接时查找变量 x 和函数 foo 的定义。

特别地,我们会用到 extern C 来告诉编译器使用 C 语言的函数链接性。
extern "C" 是一种用于修改函数链接性和名称修饰的用法,通常与 C 和 C++ 混合编程时使用。它有以下作用:

a. 函数链接性:
在 C++ 中,函数默认会被名称修饰(name-mangling),以便支持函数重载。这导致 C++ 函数的名称在目标文件中不再是原始的函数名。然而,C 语言不使用名称修饰,因此在 C 和 C++ 混合编程时,需要确保 C++ 函数能够与 C 函数进行链接。使用 extern "C" 可以告诉编译器使用 C 语言的函数链接性。
b. 函数名称修饰:
在 C++ 中,函数名称会根据参数的类型和个数进行修饰,以支持函数重载。而 C 语言不支持函数重载,因此函数名称不会被修饰。使用 extern "C" 可以防止 C++ 对函数名称进行修饰,使函数名与 C 语言一致。

下面是一个示例:

1
2
3
4
5
6
7
8
9
// 声明一个C函数
extern "C" {
void c_function(int arg);
}

// C++函数
void cpp_function(int arg) {
// ...
}

在上面的示例中, c_function 声明使用了 extern "C" ,这表示它的链接性和名称修饰与 C 语言兼容。这样,C 和 C++ 代码可以正确链接并一起工作。

2. 作用域(Scope):
extern 也可以用于指示一个变量或函数的作用域是全局的,即它可以在程序的任何地方访问。通常,全局变量和函数默认具有全局作用域,但 extern 可以用于强调这一点。例如:

1
2
// 声明全局变量 y 具有全局作用域
extern int y;

这个声明表示变量 y 具有全局作用域,可以在整个程序中访问。

总之, extern 是一个用于说明变量或函数链接性和作用域的关键字,它在 C++ 中通常用于与其他文件共享变量或函数的定义。

# i、dlopen、dlsym 和 dlclose

通过  dlopen()  函数以指定模式加载指定的动态链接库,并返回一个句柄。

dlsym()  可通过 dlopen()  返回的句柄来调用指定的函数。

通过  dlclose()  来卸载已加载的动态库。

dlopen() 的函数原型:

1
void *dlopen(const char* pathname, int mode);

pathname 是动态库的路径。

mode 是打开方式,有多种,这里列举两个: RTLD_LAZY  执行延迟绑定。仅在执行引用它们的代码时解析符号。如果从未引用该符号,则永远不会解析它(只对函数引用执行延迟绑定。在加载共享对象时,对变量的引用总是立即绑定)。 RTLD_NOW  如果指定了此值,或者环境变量  LD_BIND_NOW  设置为非空字符串,则在  dlopen()  返回之前,将解析共享对象中的所有未定义符号。如果无法执行此操作,则会返回错误。

打开失败时返回 NULL,打开成功则返回该动态库的句柄。

dlsym() 的函数原型:

1
voiddlsym(void* handle, const char* symbol);

该函数的作用是根据动态链接库的操作句柄 (handle) 与符号 (symbol),返回符号对应的地址。这里的符号既可以是函数名,也可以是变量名。于是我们通过这个函数,就可以获取动态库中的函数或变量的地址,就可以调用动态库中的相关函数。

该函数的返回值  void*  指向函数的地址,供调用使用。

dlclose() 就没啥好说的了,卸载相应的动态库。

具体使用示例:

1
2
3
4
void *handle = dlopen(str.c_str(), RTLD_LAZY);
typedef void (*so_config)(int argc, char **argv);
so_config config_func = dynamic_cast<so_config>(dlsym(handle, "Config"));
config_func(ptr_len, ptr_ptr);

dlerror() 是一个用于获取动态链接库错误信息的函数,通常在使用动态链接库时发生错误时调用。该函数位于 <dlfcn.h> 头文件中,是 POSIX 标准的一部分。

当调用动态链接库相关函数(如 dlopen()dlsym()dlclose() 等)失败时,系统会设置一个全局错误状态,并通过 dlerror() 函数返回错误信息。如果前一个动态链接库相关函数调用成功,那么 dlerror() 将返回 NULL。

可以通过如下方式来使用 dlerror() 函数:

  1. 当一个动态链接库相关函数返回 NULL 时,首先调用 dlerror() 函数来确定是否有错误发生。
  2. 如果 dlerror() 返回非 NULL 指针,则表示有错误发生,可以通过返回的字符串来获取错误信息。
  3. 如果 dlerror() 返回 NULL,则表示前一个动态链接库相关函数调用成功,没有错误发生。

通过这种方式,可以在动态链接库加载、符号查找等过程中及时捕获并处理错误,提高程序的健壮性和可靠性。

# i、error 预处理指令

#error 是 C/C++ 预处理器的一条指令,用于在预处理阶段生成编译错误信息。当预处理器遇到 #error 指令时,它会输出指定的错误消息,并终止编译过程。这对于在编译时进行条件检查非常有用,例如版本检查、平台检查或配置检查。

1
2
3
#if (BS_THREAD_POOL_TEST_VERSION_MAJOR != BS_THREAD_POOL_VERSION_MAJOR)
#error The versions do not match. Aborting compilation.
#endif

# i、quiet_NaN

std::numeric_limits<float>::quiet_NaN() 返回表示 “静默 NaN(Not a Number)” 的浮点数。
NaN 是 IEEE 浮点数标准中定义的特殊值,用于表示不是数字的结果,通常用于错误处理或特殊情况下的标记。

这个函数返回的值是一个 float 类型的静默 NaN,它的位表示是特殊的,用于标记不是数字的情况。它不会抛出异常,因为它只是返回一个特定的值,不涉及任何运算或转换。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <limits>

int main() {
// 获取 float 类型的 NaN
float nan = std::numeric_limits<float>::quiet_NaN();

// 检查 NaN 是否等于自身
if (nan != nan) {
std::cout << "nan is not equal to itself" << std::endl;
} else {
std::cout << "nan is equal to itself" << std::endl;
}

return 0;
}

在示例中,我们获取了 float 类型的 NaN,并检查它是否等于自身。
由于 NaN 的特性,它与自身不相等,因此程序输出 "nan is not equal to itself"。

# NaN 的打印

NaN 值是一个特殊的浮点数,它在打印时通常会显示为 "nan" (不区分大小写)。
因此,使用 std::cout 打印 std::numeric_limits<float>::quiet_NaN() 将会输出 "nan"

示例代码:

1
2
3
4
5
6
7
8
#include <iostream>
#include <limits>

int main() {
float nan = std::numeric_limits<float>::quiet_NaN();
std::cout << "NaN value: " << nan << std::endl;
return 0;
}

输出结果将会是:

1
NaN value: nan

# i、unordered_map/unordered_set 存储自定义类型

在使用 unordered_mapunordered_set 存储自定义结构体时,需要注意以下几点:

  1. 哈希函数和相等比较函数:
    默认情况下, unordered_mapunordered_set 需要对键值进行哈希计算和相等比较。对于内置类型,这些操作已经定义好了,但对于自定义结构体,需要提供这两个函数。
  2. 哈希函数:
    必须提供一个自定义的哈希函数来计算自定义结构体的哈希值。你可以通过定义一个结构体或者函数对象来实现,也可以使用 C++11 的 std::hash 模板特化。
  3. 相等比较函数:
    需要提供一个自定义的相等比较函数,以确保 unordered_mapunordered_set 能正确比较两个自定义结构体是否相等。可以通过重载 operator== 或者定义一个函数对象来实现。

以下是一个具体的示例,展示了如何为 testStruct 定义哈希函数和相等比较函数,使其可以用于 unordered_mapunordered_set

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <unordered_map>
#include <unordered_set>

// 定义自定义结构体
struct testStruct {
int a;
float b;

// 重载 operator==,以便在哈希容器中进行比较
bool operator==(const testStruct& other) const {
return a == other.a && b == other.b;
}
};

// 自定义哈希函数
struct testStructHash {
std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
// 组合两个哈希值
return h1 ^ (h2 << 1);
}
};

// 用于测试的 main 函数
int main() {
// 使用自定义结构体作为键的 unordered_map
std::unordered_map<testStruct, std::string, testStructHash> umap;
testStruct ts1 = {1, 1.1f};
testStruct ts2 = {2, 2.2f};

umap[ts1] = "test1";
umap[ts2] = "test2";

for(const auto& pair : umap) {
std::cout << "Key: (" << pair.first.a << ", " << pair.first.b << "), Value: " << pair.second << std::endl;
}

// 使用自定义结构体的 unordered_set
std::unordered_set<testStruct, testStructHash> uset;
uset.insert(ts1);
uset.insert(ts2);

for(const auto& elem : uset) {
std::cout << "Element: (" << elem.a << ", " << elem.b << ")" << std::endl;
}

return 0;
}

在这个示例中,我们做了以下几步:

  1. 定义 testStruct 并重载 operator== : 以便在哈希容器中进行结构体比较。
  2. 定义 testStructHash 结构体:用于计算 testStruct 的哈希值。我们使用了 std::hashintfloat 类型进行哈希计算,并将它们组合起来。
  3. main 函数中测试:使用 unordered_mapunordered_set 存储和访问 testStruct

这样, testStruct 就可以顺利地用于 unordered_mapunordered_set 中。

为什么不使用 lambda 表达式来实现哈希,而是要采用结构体?
为什么说 lambda “可读性和扩展性相对较低”,请你具体举例说明?

用 lambda 表达式定义哈希函数确实可以简洁地实现某些功能,但在某些情况下,其可读性和扩展性相对于结构体方式可能会稍显不足。下面具体举例说明其不足之处:

  • 可读性问题
  1. 代码量较多时可读性下降:
    当哈希计算逻辑复杂时,lambda 表达式可能会变得难以阅读。例如:

1
2
3
4
5
6
auto complexHash = [](const testStruct& ts) -> std::size_t {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
std::size_t h3 = std::hash<int>()(ts.a + static_cast<int>(ts.b * 100));
return h1 ^ (h2 << 1) ^ (h3 << 2);
};

相对于这种复杂的 lambda 表达式,使用结构体来封装会使逻辑更清晰:

1
2
3
4
5
6
7
8
struct testStructHash {
std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
std::size_t h3 = std::hash<int>()(ts.a + static_cast<int>(ts.b * 100));
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};

  • 扩展性问题
  1. 不能直接存储状态:
    lambda 表达式本身不太适合存储状态。如果哈希函数需要存储一些额外的数据,使用结构体会更方便。例如:

1
2
3
4
5
6
7
8
9
10
11
struct testStructHashWithState {
int someState;

testStructHashWithState(int state) : someState(state) {}

std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return (h1 ^ (h2 << 1)) + someState;
}
};

用 lambda 表达式存储状态则相对麻烦:

1
2
3
4
5
6
7
8
int someState = 42;
auto hashFn = [someState](const testStruct& ts) -> std::size_t {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return (h1 ^ (h2 << 1)) + someState;
};

std::unordered_map<testStruct, std::string, decltype(hashFn)> umap(10, hashFn);

  • 模板特化的灵活性

如果需要在模板特化中定义哈希函数,使用结构体会更自然。示例:

1
2
3
4
5
6
7
8
9
10
11
12
namespace std {
template <>
struct hash<testStruct> {
std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return h1 ^ (h2 << 1);
}
};
}

std::unordered_map<testStruct, std::string> umap;

  • 代码复用

结构体的方式便于代码复用和维护。例如,如果哈希计算逻辑需要在多个地方使用,用结构体封装的哈希函数可以直接复用,而不用在每个地方都写一次 lambda 表达式:

1
2
std::unordered_map<testStruct, std::string, testStructHash> umap1;
std::unordered_set<testStruct, testStructHash> uset1;

如果用 lambda 表达式,每个地方都需要定义一次:

1
2
3
4
5
6
7
8
auto hashFn = [](const testStruct& ts) -> std::size_t {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return h1 ^ (h2 << 1);
};

std::unordered_map<testStruct, std::string, decltype(hashFn)> umap1(10, hashFn);
std::unordered_set<testStruct, decltype(hashFn)> uset1(10, hashFn);

  • 总结
    尽管 lambda 表达式在简单情况下非常方便,但当涉及复杂逻辑、状态存储、代码复用以及模板特化时,结构体的方式更具优势。因此,综合考虑可读性和扩展性,结构体定义方式通常是更好的选择。

这里重载 operator== 的作用是什么,为什么不用重载大于 > 或者小于 < ?

在 C++ 中, operator==operator!= 通常用于相等性比较,而 operator<operator> 等用于排序。对于 std::unordered_mapstd::unordered_set 来说,使用哈希表实现,因此需要的是相等性比较而不是排序比较。

std::unordered_mapstd::unordered_set 使用哈希表来存储元素。当我们插入、查找或删除元素时,这些容器会先根据哈希值(由哈希函数计算得出)将元素分配到某个桶中,然后在桶内使用 operator== 来比较元素是否相等。因此,为了在这些容器中使用自定义类型 testStruct ,我们必须重载 operator== 以定义两个 testStruct 对象何时被认为是相等的。

1
2
3
4
5
6
7
8
9
class testStruct {
public:
int a = 0;
float b = 0;

bool operator==(const testStruct& other) const {
return a == other.a && b == other.b;
}
};

  • 为什么不用 operator<operator>

operator<operator> 主要用于排序比较,在需要排序功能的容器如 std::mapstd::set 中会用到。这些容器是基于红黑树等平衡二叉树实现的,因此需要知道两个元素之间的顺序关系。

但是, std::unordered_mapstd::unordered_set基于哈希表实现的,哈希表不需要排序元素,只需要将元素正确地放入桶中并能正确地比较桶内的元素是否相等。因此,在这种情况下,重载 operator<operator> 是没有意义的,哈希表并不会使用这些操作符。

  • 总结
  1. operator== 的作用operator== 用于定义两个 testStruct 对象何时被认为是相等的,这对于哈希表在桶内比较元素是必要的。
  2. 不用 operator<operator> std::unordered_mapstd::unordered_set 基于哈希表实现,不需要排序元素,因此不需要 operator<operator> 。这些操作符主要用于排序容器如 std::mapstd::set
# 标准示范

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include <set>

class MyStruct {
public:
int a = 0;
float b = 0;

// for unordered_map and unordered_set
bool operator==(const MyStruct& other) const {
return a == other.a && b == other.b;
}

// for map and set
bool operator<(const MyStruct& other) const {
return a < other.a && b < other.b;
}
};

// for unordered_map and unordered_set
struct MyHash {
// this should be double const
std::size_t operator()(const MyStruct& ms) const {
std::size_t h1 = std::hash<int>()(ms.a);
std::size_t h2 = std::hash<float>()(ms.b);
return h1 ^ (h2 << 1);
}
};



int main() {
std::unordered_map<MyStruct, int, MyHash> um;
um.clear();

MyStruct aa;
MyStruct bb;

um[aa] = 0;
um[bb] = 1;

std::unordered_set<MyStruct, MyHash> us;

us.insert(aa);
us.insert(bb);

std::set<MyStruct> s;
s.insert(aa);
s.insert(bb);

std::map<MyStruct, int> m;
m[aa] = 0;
m[bb] = 1;

return 0;
}

# i、std::unordered_map 的使用

unordered_map 是 C++ 标准库中的一种关联容器,它提供了一种无序、基于键值对的数据存储结构,具有快速的查找、插入和删除操作。 unordered_map 基于 哈希表 实现,可以在平均情况下以接近 O(1) 的时间复杂度进行元素的查找、插入和删除。

下面是 unordered_map 的一些常见用法:

  1. 创建 unordered_map 对象

    1
    2
    3
    4
    #include <unordered_map>

    std::unordered_map<int, std::string> myMap;
    // 创建一个键为int类型,值为std::string类型的unordered_map对象

  2. 插入元素

    1
    2
    myMap[1] = "value1"; // 插入键值对 <1, "value1">
    myMap.insert({2, "value2"}); // 使用insert方法插入键值对 <2, "value2">

    注意,如果是用 insert 的话,需要构造一个 std::pair 的对象。

  3. 访问元素

    1
    std::string value = myMap[1]; // 访问键为1的值,如果键不存在,则会插入一个默认构造的值

    注意!!!这种方式是存在副作用的,即 “键不存在时创建默认值”。
    如果我们需要通过 map 的 size 来决定代码的逻辑,则应该避免使用这种方式来访问元素,以免对 size 造成影响!

1
2
3
4
auto iter = myMap.find(2); // 使用find方法查找键为2的元素
if(iter != myMap.end()) {
std::string value = iter->second; // 访问找到的值
}

  1. 删除元素

    1
    myMap.erase(1); // 删除键为1的元素

  2. 遍历 unordered_map

    1
    2
    3
    for(const auto& pair : myMap) {
    std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl;
    }

  3. 获取 unordered_map 的大小

    1
    int size = myMap.size(); // 获取unordered_map中键值对的个数

  4. 检查 unordered_map 是否为空

    1
    bool isEmpty = myMap.empty(); // 判断unordered_map是否为空

  5. 清空 unordered_map

    1
    myMap.clear(); // 清空unordered_map中的所有元素

  6. 使用自定义哈希函数
    如果 unordered_map 的键类型不是基本类型(如 intstd::string 等),或者需要自定义哈希函数,可以通过 unordered_map 的第三个模板参数指定自定义的哈希函数,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct MyKey {
    int id;
    std::string name;
    };

    struct MyKeyHash {
    std::size_t operator()(const MyKey& key) const {
    return std::hash<int>()(key.id) ^ (std::hash<std::string>()(key.name) << 1);
    }
    };

    std::unordered_map<MyKey, int, MyKeyHash> myMap; // 使用自定义的哈希函数

unordered_map 提供了丰富的功能,可以用于各种数据存储和处理场景,是 C++ 中非常常用的数据结构之一。


# i、实现一个二分查找算法

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 <iostream>
#include <vector>

// 自定义二分查找函数
template <typename T>
int binarySearch(const std::vector<T>& arr, const T& target) {
int low = 0;
int high = arr.size() - 1;

while(low <= high) {
int mid = low + (high - low) / 2;

if(arr[mid] == target) {
return mid; // 找到目标值,返回索引
} else if (arr[mid] < target) {
low = mid + 1; // 目标值在右半部分,调整区间的下界
} else {
high = mid - 1; // 目标值在左半部分,调整区间的上界
}
} return -1; // 没有找到目标值,返回 -1
}

int main() {
std::vector<int> arr = {2, 4, 6, 8, 10, 12, 14, 16, 18}; // 注意,二分法需要应用于已排序的数组
int target = 12;
int index = binarySearch(arr, target);

if(index != -1) {
std::cout << "Target found at index " << index << std::endl;
} else {
std::cout << "Target not found in the array" << std::endl;
}

return 0;
}

另外,STL 其实有封装二分查找的库,所以其实调个 API 就能实现了。见:[[C++ 库函数 #i、binary_search|binary_search]]

# i、用 C++ 实现 strcpy 函数

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
#include <iostream>

void strcpy(char* dest, const char* src) {
// 检查指针是否为空
if(dest == nullptr || src == nullptr) {
return;
}

// 复制字符串内容直到遇到空字符 '\0'
while(*src != '\0') {
*dest = *src;
dest++;
src++;
}

// 在目标字符串末尾添加空字符 '\0'
*dest = '\0';
}

int main() {
const char* source = "Hello, world!";
char destination[20];

strcpy(destination, source);

std::cout << "Copied string: " << destination << std::endl;

return 0;
}

在上述代码中,strcpy 函数接受两个参数, dest 表示目标字符串的指针, src 表示源字符串的指针。函数通过遍历源字符串的每个字符,逐个将其复制到目标字符串,直到遇到空字符 \0 ,表示字符串的结束。最后,将目标字符串的末尾设置为空字符 \0 ,以确保复制后的字符串正确终止。

# 内存踩踏

当源字符串指针(src)和目标字符串指针(dest)所指向的内存有重叠部分时,使用标准的 strcpy 函数或自定义的 strcpy 函数可能会导致意想不到的结果。

在 C++ 中,按照标准的行为, strcpy 函数不应该用于处理重叠内存区域的字符串复制。这是因为在重叠内存区域中,复制的操作可能会导致数据的不可预测的改变。

具体来说,如果在 strcpy 函数中源字符串和目标字符串的内存区域重叠,复制过程中的操作可能会覆盖尚未被复制的数据,导致数据损坏或不正确的复制结果。

为了处理重叠内存区域的字符串复制,可以使用 memmove 函数,它被设计为可以处理重叠内存区域的内存复制操作。 memmove 函数的实现可以通过使用指针操作来实现内存块的移动。下面是一个用 C++ 编写的简单 memmove 函数的示例:

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
36
37
38
39
40
41
42
43
#include <iostream>

void* memmove(void* dest, const void* src, size_t size) {
// 检查指针是否为空
if (dest == nullptr || src == nullptr) {
return nullptr;
}

// 创建临时缓冲区
char* temp = new char[size];

// 复制源内存块到临时缓冲区
const char* srcPtr = static_cast<const char*>(src);
char* tempPtr = temp;
for(size_t i = 0; i < size; ++i) {
*tempPtr++ = *srcPtr++;
}

// 将临时缓冲区的内容复制到目标内存块
char* destPtr = static_cast<char*>(dest);
tempPtr = temp;
for(size_t i = 0; i < size; ++i) {
*destPtr++ = *tempPtr++;
}

// 释放临时缓冲区
delete[] temp;

return dest;
}

int main() {
char source[] = "Hello, world!";
char destination[20];

std::cout << "Before memmove: " << destination << std::endl;

memmove(destination, source, sizeof(source));

std::cout << "After memmove: " << destination << std::endl;

return 0;
}

在上述代码中, memmove 函数接受三个参数:目标指针( dest )、源指针( src )和要复制的字节数( size )。函数首先创建一个临时缓冲区,然后将源内存块的内容复制到临时缓冲区,最后再将临时缓冲区的内容复制到目标内存块。

请注意,在实际编写代码时,需要考虑更多的边界条件和错误处理,以确保函数的正确性和安全性。此外,C++ 标准库也提供了 std::memmove 函数,它是一个更为优化和健壮的实现,建议在实际使用中使用标准库提供的函数。

可以在不使用额外临时缓冲区的情况下实现 memmove 函数。一种常见的方法是使用指针操作和适当的条件判断来处理重叠内存块的移动。

下面是一个不使用额外临时缓冲区的 memmove 函数的示例实现:

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
36
37
38
39
#include <iostream>

void* memmove(void* dest, const void* src, size_t size) {
// 检查指针是否为空
if(dest == nullptr || src == nullptr) {
return nullptr;
}

char* destPtr = static_cast<char*>(dest);
const char* srcPtr = static_cast<const char*>(src);

// 判断重叠情况
if(srcPtr < destPtr) {
// 从后往前复制
for(size_t i = size; i > 0; --i) {
destPtr[i - 1] = srcPtr[i - 1];
}
} else {
// 从前往后复制
for(size_t i = 0; i < size; ++i) {
destPtr[i] = srcPtr[i];
}
}

return dest;
}

int main() {
char source[] = "Hello, world!";
char destination[20];

std::cout << "Before memmove: " << destination << std::endl;

memmove(destination, source, sizeof(source));

std::cout << "After memmove: " << destination << std::endl;

return 0;
}

在上述代码中, memmove 函数首先判断源内存块和目标内存块的位置关系。如果源指针在目标指针之前,则从后往前逐个复制数据;如果源指针在目标指针之后,则从前往后逐个复制数据。通过这种方式,可以确保正确处理重叠内存块的复制,而无需使用额外的临时缓冲区。

需要注意的是,这种实现方式在处理重叠内存块时可能需要更多的指针操作和条件判断,因此在性能上可能不如使用临时缓冲区的实现方式高效。在实际使用时,根据具体的情况选择适合的实现方式,权衡性能和内存使用的需求。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 什锦 - C++ - 编程 | R1ck's Playground = how

# i、右值引用

# 概述

右值引用是 C++11 引入的一种新的引用类型,用于支持移动语义完美转发。与传统的左值引用(左值引用)不同,右值引用绑定到临时对象将要销毁的对象或者显式转换为右值引用的对象。右值引用的特点是可以接管资源,并实现高效的资源移动操作。

在 C++ 中,我们可以通过在类型名称前添加 && 来声明右值引用。例如, int&& 表示一个右值引用类型的整数。

右值引用的一些关键概念包括:

  1. 左值(Lvalue):左值是指一个具有标识符的、可寻址的对象。它可以出现在赋值表达式的左边或右边,并且具有持久的状态。左值引用(左值的引用)可以绑定到左值。
  2. 右值(Rvalue):右值是指一个临时的、无法寻址的对象。它通常是一个临时表达式的结果,如常量、临时对象、将要销毁的对象等。右值引用可以绑定到右值。
  3. 移动语义(Move Semantics):移动语义是通过右值引用实现的一种特性,允许将资源(如堆内存)从一个对象移动到另一个对象,而不是进行复制。移动语义可以提高性能,避免不必要的内存拷贝和资源分配。
  4. 完美转发(Perfect Forwarding):完美转发是指在函数模板中以原样传递参数,既不进行拷贝也不进行移动,保持其原始类型。通过使用右值引用和模板参数推导,可以实现完美转发,将参数传递给下游函数,保持参数的值类别(左值或右值)和常量性。

右值引用的引入使得 C++ 语言能够更好地处理资源管理和移动语义,避免不必要的数据拷贝,提高程序的效率和性能。它在移动语义、完美转发、智能指针等方面发挥了重要的作用。

# 移动语义

当涉及到资源管理或对象传递时,移动语义完美转发可以提供更高效的操作和灵活性。

移动语义: 移动语义允许将资源(如堆内存)从一个对象移动到另一个对象,而不是进行复制。这可以避免不必要的内存拷贝和资源分配,提高程序的效率。

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
36
37
#include <iostream>
#include <vector>

class MyString {
private:
char* m_data;

public:
MyString(const char* str) {
// 分配内存并复制字符串
size_t length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
std::cout << "Constructor: " << m_data << std::endl;
}

// 移动构造函数
MyString(MyString&& other) noexcept {
// 直接接管资源
m_data = other.m_data;
other.m_data = nullptr;
std::cout << "Move Constructor: " << m_data << std::endl;
}

~MyString() {
delete[] m_data;
}
};

int main() {
MyString str1("Hello"); // 调用构造函数

MyString str2(std::move(str1)); // 调用移动构造函数
// 此时str1不再拥有资源,而是被str2接管了

return 0;
}

在上面的示例中,我们定义了一个简单的字符串类 MyString ,它包含了一个动态分配的字符数组。通过移动构造函数,我们可以直接将资源从一个对象移动到另一个对象,而不需要进行额外的内存拷贝。在 main 函数中,我们创建了 str1str2 两个对象,通过 std::movestr1 的资源移动给了 str2 。这样,资源的所有权从 str1 转移到了 str2 ,并在程序结束时正确释放。

# 完美转发

完美转发允许以原样传递参数,既不进行拷贝也不进行移动,保持其原始类型。 通过使用右值引用模板参数推导,可以实现完美转发,将参数传递给下游函数,保持参数的值类别和常量性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <utility>

void processValue(int& value) {
std::cout << "Lvalue: " << value << std::endl;
}

void processValue(int&& value) {
std::cout << "Rvalue: " << value << std::endl;
}

template <typename T>
void forwardValue(T&& value) {
processValue(std::forward<T>(value));
}

int main() {
int x = 42;
forwardValue(x); // 传递左值,调用 processValue(int&)
forwardValue(123); // 传递右值,调用 processValue(int&&)
forwardValue(std::move(x)); // 传递右值引用,调用 processValue(int&&)
return 0;
}

在上面的示例中,我们定义了两个重载的 processValue 函数,一个接受左值引用,另一个接受右值引用。然后,我们使用模板函数 forwardValue 来实现完美转发。在 forwardValue 函数中,我们使用 std::forward 来保持参数的值类别(左值或右值)和常量性,并将参数传递给下游函数 processValue 。通过完美转发,我们可以在保持原始参数类型的同时,将参数传递给适当的处理函数。

通过移动语义和完美转发,我们可以更高效地管理资源和实现灵活的参数传递。这些特性在处理大型对象、容器元素和函数传参时特别有用,可以避免不必要的拷贝和资源分配,提高程序的性能和效率。

# 追问

1、移动构造函数中的 noexcept 有何作用,是否一定需要?

noexcept 在移动构造函数中的作用是指定该函数是否可能抛出异常。使用 noexcept 关键字可以提供编译器优化的机会,因为它使得在移动构造函数中执行更轻量级的操作,如移动资源的所有权,而无需进行异常处理。这样可以提高程序的性能。然而, noexcept 并不是必需的,你可以选择是否在移动构造函数中使用它,具体取决于你的需求。

2、调用 std::move 后的变量是否不能够再次被使用?

调用 std::move 后的变量仍然可以被使用,但是它的状态会发生变化。 std::move 将变量转换为右值引用,这意味着它可以被移动而不是复制。移动后的变量的状态通常是不确定的,你不应该对其进行操作或访问其值。它通常被用于将资源所有权转移给其他对象,或作为参数传递给接受右值引用的函数。在移动后,你可以重新赋值给它或销毁它。重要的是要记住, std::move 仅仅是改变了变量的类型,而不会对其值进行任何修改

# i、C++ 内存分布

在 C++ 程序中,内存可以划分为以下几个主要区域:

  1. 栈(Stack):
    • 栈位于内存的较高地址部分。
    • 栈用于存储函数的局部变量、函数参数、函数调用信息等。
    • 栈的分配和释放是由编译器自动管理的,具有自动内存管理的特性。
    • 栈的大小在程序运行时是固定的。
  2. 堆(Heap):
    • 堆位于内存的较低地址部分。
    • 堆用于动态分配内存,由程序员手动管理。
    • 堆的分配和释放需要使用特定的函数(如 newdeletemallocfree 等)进行操作。
    • 堆的大小在程序运行时可以动态变化。
  3. 全局区(Global Area):
    • 全局区也称为静态区或数据段。
    • 全局区存储全局变量、静态变量和常量。
    • 全局区在程序运行期间一直存在,直到程序结束。
    • 全局区的大小在编译时确定。
  4. 常量区(Constant Area):
    • 常量区也称为文字常量区或只读数据区。
    • 常量区存储字符串常量和其他常量数据。
    • 常量区的数据是只读的,不能被修改。
    • 常量区在程序运行期间一直存在,直到程序结束。
  5. 代码区(Code Area):
    • 代码区也称为文本区或只读代码区。
    • 代码区存储程序的执行代码。
    • 代码区的数据是只读的,不能被修改。
    • 代码区在程序运行期间一直存在,直到程序结束。

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
+-----------------------+
| 栈 (Stack) |
| |
| |
| |
+-----------------------+
| 堆 (Heap) |
| |
| |
| |
+-----------------------+
| 全局区 (Global) |
| |
| |
| |
+-----------------------+
| 常量区 (Constant) |
| |
| |
| |
+-----------------------+
| 代码区 (Code) |
| |
| |
| |
+-----------------------+

# i、C++ 内存模型

# 概述

C++ 内存模型是描述 C++ 程序在执行过程中,内存访问和操作的规则和保证的规范。它定义了多线程环境下的原子性操作、内存顺序、可见性等行为,以确保多线程程序的正确性和可预测性。

下面是 C++ 内存模型中的几个重要概念:

  1. 原子性(Atomicity):原子操作是不可被中断的单个操作,要么完全执行,要么不执行。C++ 提供了原子操作库(std::atomic),用于在多线程环境下实现原子操作,保证对共享变量的读写操作的原子性。
  2. 内存顺序(Memory Order):内存顺序规定了多个线程对共享变量的读写操作的顺序。C++ 提供了一组枚举类型(如 std::memory_order_relaxedstd::memory_order_acquirestd::memory_order_release 等),用于指定不同的内存顺序要求。
  3. 可见性(Visibility):可见性指的是一个线程对共享变量的修改对其他线程是否可见。C++ 内存模型确保了原子操作的可见性,即一个原子操作对其他线程是立即可见的。
  4. Happens-Before 关系:Happens-Before 关系是 C++ 内存模型中的一个重要概念,它用于描述不同操作之间的顺序关系。如果一个操作 A Happens-Before 于另一个操作 B,那么在多线程环境下,操作 A 的结果对操作 B 是可见的。Happens-Before 关系可以由同步操作(如互斥锁、原子操作等)和特定的内存顺序关系来建立。

C++ 内存模型提供了一系列的规则和保证,以帮助程序员编写正确且具有可移植性的多线程程序。它规定了各种操作之间的可见性和顺序关系,并提供了原子操作和内存顺序的机制,使得程序在多线程环境下能够正确地进行内存访问和操作。

需要注意的是,C++ 内存模型并不是操作系统的内存管理模型,它更关注于程序在多线程环境下的内存访问规则和行为。对于底层的内存管理细节(如页面管理、缓存一致性等),C++ 内存模型通常依赖于操作系统和硬件的支持。

# 内存顺序

当多个线程并发地访问和修改共享变量时,内存顺序定义了这些操作之间的顺序关系。C++ 中的内存顺序通过枚举类型 std::memory_order 来指定。

  1. std::memory_order_relaxed (松散顺序): std::memory_order_relaxed 是最宽松的内存顺序,它不对任何内存操作进行顺序限制。对于使用 std::memory_order_relaxed 的操作,编译器和处理器可以对其进行重排序,且不会对其他线程产生任何顺序上的约束。
  2. std::memory_order_acquire (获取顺序):稍后解释。
  3. std::memory_order_release (释放顺序):稍后解释。

当多个线程并发地读写共享变量时,使用 std::memory_order_acquirestd::memory_order_release 可以确保读写操作之间的顺序关系和可见性。

std::memory_order_acquire 用于读操作,它具有以下特性:

  1. 在使用 std::memory_order_acquire 进行读操作时,该读操作之前的所有读写操作都不会被放置在该读操作之后。这意味着,使用 std::memory_order_acquire 读取的值不会是之前的写操作的过期值。
  2. std::memory_order_acquire 会建立 Happens-Before 关系,使得该读操作之前的写操作对当前线程可见。这意味着,在使用 std::memory_order_acquire 进行读操作后,当前线程能够观察到在该读操作之前发生的写操作所引入的更改。
  3. std::memory_order_acquire 并不会限制该读操作之后的任何操作的顺序,其他线程可以继续并发地读写共享变量。
    相比于 std::memory_order_relaxed ,使用 std::memory_order_acquire 可以提供更强的内存顺序保证,确保了读操作的顺序关系和可见性。

std::memory_order_release 用于写操作,它具有以下特性:

  1. 在使用 std::memory_order_release 进行写操作时,该写操作之后的所有读写操作都不会被放置在该写操作之前。这意味着,使用 std::memory_order_release 写入的值不会影响之后的读写操作的顺序。
  2. std::memory_order_release 会建立 Happens-Before 关系,使得该写操作对其他线程的读操作可见。这意味着,在使用 std::memory_order_release 进行写操作后,其他线程能够观察到该写操作所引入的更改。
  3. std::memory_order_release 并不会限制该写操作之前的任何操作的顺序,其他线程可以继续并发地读写共享变量。

通过使用 std::memory_order_acquirestd::memory_order_release ,可以在多线程环境下实现同步原语,例如互斥锁或读写锁。一个线程在写入共享变量之前,使用 std::memory_order_release 进行写操作,而另一个线程在读取共享变量之前,使用 std::memory_order_acquire 进行读操作。这样可以确保数据的一致性和可见性,防止数据竞争和不确定行为的发生。

需要注意的是, std::memory_order_acquirestd::memory_order_release 是成对使用的。当一个线程使用 std::memory_order_release 顺序进行写操作时,另一个线程使用 std::memory_order_acquire 顺序进行相应的读操作,以建立 Happens-Before 关系。单独使用这两个内存顺序是不够的,要实现正确的同步,需要遵循正确的使用模式和配对操作。

这里需要注意的是, std::memory_order_acquirestd::memory_order_release 是一对配套使用的内存顺序,用于实现同步原语,例如互斥锁、读写锁等。当一个线程使用 std::memory_order_release 顺序进行写操作时,另一个线程使用 std::memory_order_acquire 顺序进行相应的读操作,这样可以建立 Happens-Before 关系,确保数据的正确同步。

std::memory_order_acquirestd::memory_order_release 的主要目的是确保读操作和写操作之间的顺序关系和可见性,并防止编译器和处理器对其进行重排优化。

简单来说,为了确保同步,读用 std::memory_order_acquire ,写用 std::memory_order_release 即可。

# i、全局变量与 static

# 全局变量

全局变量是定义在函数外部、整个源文件都可以访问的变量。它具有全局作用域和静态生存期,意味着它在整个程序的执行过程中都存在,并且可以被程序中的任何函数所使用。

全局变量的特点包括:

  1. 作用域:全局变量的作用域从定义处开始一直延伸到文件的末尾,整个源文件中的任何函数都可以访问这个变量。
  2. 生命周期:全局变量在程序启动时分配内存,在程序结束时释放内存,因此它们的生命周期与整个程序的运行时间一样长。
  3. 默认初始化:如果全局变量没有显式地初始化,那么它们会被默认初始化。
  4. 存储位置:全局变量的存储位置通常位于静态存储区,在程序启动时分配,程序结束时释放。因此,全局变量的内存空间在整个程序的执行过程中都是固定的。
  5. 可见性:全局变量在整个源文件中都是可见的,因此在不同的函数中可以共享数据。但是,过度使用全局变量会导致代码的可读性和维护性降低,因此应该尽量避免滥用全局变量,尤其是在大型项目中。

全局变量的定义和声明一般放在源文件的顶部,在任何函数外部,使用关键字 extern 可以在其他源文件中引用全局变量。虽然全局变量具有方便性,但也容易引起命名冲突和程序设计的复杂性,因此在实际编程中需要谨慎使用。

# 静态全局变量

如果在全局变量的定义前加上 static 关键字进行修饰,则该变量就成为 静态全局变量静态全局变量的作用域仅限于当前文件,不能被其他文件访问。

静态全局变量的特点包括:

  1. 作用域:与全局变量相同,静态全局变量的作用域也从定义处开始一直延伸到文件的末尾,但它只能在当前文件内部访问,其他文件无法访问。
  2. 生命周期:静态全局变量在程序启动时分配内存,在程序结束时释放内存,生命周期与整个程序的执行过程一样长。
  3. 默认初始化:与全局变量相同,如果静态全局变量没有显式地初始化,它们会被默认初始化。
  4. 存储位置:静态全局变量也存储在静态存储区,在程序启动时分配,程序结束时释放,内存空间在整个程序的执行过程中都是固定的。
  5. 可见性:静态全局变量仅在当前文件内可见,不能被其他文件引用,因此不会引起命名冲突。

由于静态全局变量的作用域受限于当前文件,因此它们通常被用于实现文件内部的全局状态或共享数据,同时避免了全局变量可能引发的命名冲突问题。

# 全局变量与静态成员变量

假设我现在有一个 tool.cpp 和 tool.h 文件,其中我定义并实现了 Tool 这个类。
那么,对于某些变量,出于某些共享的需求,我既可以把它设置为全局变量,也可以把它设置成静态成员变量,这两种方式有什么区别?

在 C++ 中,你可以通过将变量设置为全局变量或将其设置为类的静态成员变量来实现共享的需求,但这两种方式有一些区别:

  1. 作用域

    • 全局变量的作用域是整个源文件,其他源文件也可以通过 extern 关键字来引用全局变量。(如果希望其只对 Tool 类可见,可添加 static 关键字使其局限在该 tool.cpp 文件中)
    • 类的静态成员变量的作用域是整个类,可以通过 类名::静态成员变量名 来访问。
  2. 可见性

    • 全局变量的可见性是整个源文件,其他源文件也可以通过 extern 关键字来引用全局变量。
    • 类的静态成员变量的可见性是类的范围内,只有类的成员函数和友元函数可以直接访问静态成员变量。
  3. 命名空间

    • 全局变量属于全局命名空间,可能会导致命名冲突问题,特别是在大型项目中。
    • 类的静态成员变量属于类的命名空间,可以通过类名限定来避免命名冲突。
  4. 初始化

    • 全局变量在整个程序启动时会被初始化,如果没有显式地初始化,则会被默认初始化。
    • 类的静态成员变量需要在类外部进行初始化,可以在类的定义外部进行初始化,也可以在类的实现文件中进行初始化。

综上所述,如果变量需要在多个源文件中共享,则通常选择全局变量;如果变量与类密切相关,并且只需要在类的范围内共享,则选择类的静态成员变量更为合适。
(比如两个源文件要进行线程同步,那么可选择使用全局变量的互斥锁、条件变量。)

# i、静态成员函数

在 C++ 中,静态成员函数是属于类而不是对象实例的函数。它们与类关联,而不是与类的具体实例关联。

# 定义:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
// 静态成员函数
static void staticFunction() {
// 可以访问静态成员和其他静态成员函数
}

// 非静态成员函数
void nonStaticFunction() {
// 可以访问非静态成员和其他成员函数
}
};

# 调用:

1
MyClass::staticFunction(); // 直接通过类名调用,而不需要创建类的实例

# 特点和用途:
  • 与类关联:
    静态成员函数与类关联,而不是与类的具体实例关联。因此,它们不访问任何特定对象的成员,也不能使用非静态成员变量或函数

  • 无 this 指针:
    静态成员函数没有隐含的 this 指针,因为它们不与特定的对象实例相关联。这使得它们在不涉及实例数据的情况下执行更为灵活。(如果涉及,则需要进行绑定)

  • 访问静态成员:
    静态成员函数可以访问类的静态成员变量和其他静态成员函数。这使得它们适用于处理与类而不是与对象实例相关的操作。

  • 全局访问点:
    静态成员函数常用于提供一个全局访问点,例如工厂方法或全局配置设置。

# i、野指针调用类的成员函数

在 C++ 中,当使用野指针(悬空指针)调用类的成员函数时,会发生未定义行为。野指针是指指向已释放的内存或者未初始化的内存的指针。使用野指针访问对象的成员函数可能导致程序崩溃或者产生其他不可预测的结果。

示例代码中的野指针就是将指针 pa 初始化为 nullptr,然后尝试通过它调用类 A 的成员函数 func ()。在这种情况下,由于 pa 指针指向了一个未定义的内存地址(nullptr),调用 func () 会导致未定义行为,这可能会导致程序崩溃。

在编写 C++ 代码时,应该始终确保指针的有效性,避免使用空指针和野指针。在使用指针之前,最好将其初始化为 nullptr,并在使用前进行有效性检查。可以使用条件语句或者智能指针来确保指针指向有效的对象,从而避免悬空指针的问题。

追问:经过我的实践,野指针应该是仍然可以调用到类的成员函数的,只不过其结果是未定义的,是危险的。

实际上,当野指针调用类的成员函数时,确实可以执行函数体内的代码,因为在编译时,编译器只需要知道函数的地址就可以调用它,不会检查指针是否为 nullptr 或者是否指向有效的对象。因此,代码在运行时仍然会执行类的成员函数,但是由于指针是悬空的,因此这样的行为是未定义的,可能会导致程序崩溃或者产生其他不可预测的结果。

虽然野指针调用类的成员函数可能不会立即导致程序崩溃,但这样的行为是非常危险的,可能会导致严重的后果,例如内存访问错误、数据损坏等。因此,应该始终避免使用空指针和野指针,并在使用指针之前进行有效性检查,以确保指针指向有效的对象。

追问:但编译器是怎么知道成员函数的地址的呢?又或者说,类的成员函数存储在哪里?

编译器在编译阶段会为类的成员函数生成代码,并将这些成员函数存储在程序的可执行文件中的代码段(text segment)中。成员函数的地址是由编译器在编译时决定的,通常在编译后的可执行文件中,成员函数会被编译成与类的对象无关的静态函数(static function),这意味着它们不依赖于任何特定的对象实例。

当调用类的成员函数时,实际上是通过类的对象(或类的指针 / 引用)来调用的,编译器会将对象的地址作为隐式的参数传递给成员函数,从而让成员函数知道自己是属于哪个对象的。在函数体内,成员函数可以通过 this 指针来访问类的成员变量和其他成员函数。

由于成员函数存储在代码段中,它们的地址在程序运行时是固定的。当通过类的对象(或指针 / 引用)调用成员函数时,编译器会在运行时使用正确的地址来调用相应的函数。

需要注意的是,对于虚函数(virtual function),编译器会通过虚函数表(vtable)来实现动态绑定,使得在运行时可以根据实际对象的类型来调用正确的虚函数。虚函数表是一个由编译器生成的数据结构,其中存储了每个虚函数的地址。这样,当通过指向基类的指针或引用调用虚函数时,会根据对象的实际类型来查找正确的虚函数地址。

# i、委托构造函数

#CPP 新特性 #CPP11
委托构造函数是 C++11 引入的一个特性,它允许一个构造函数调用同一个类中的另一个构造函数,从而避免代码重复。通过委托构造函数,可以在构造过程中重用已有的构造函数代码。

即,当在一个类中定义多个构造函数,并且其中一些构造函数有共同的初始化逻辑时,委托构造函数可以帮助你避免重复编写相同的初始化代码。

简单举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
public:
// 假设我们在构造函数中都需要做一系列相同的初始化操作
// 比如这里我们简单举例,我们需要计算 x + y 的值

Point() : x(0), y(0) {
// 默认构造函数
int sum = x + y;
}

Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {
// 带参数构造函数
int sum = x + y;
}

Point(int value) : x(value), y(value) {
// 参数为相同值的构造函数
int sum = x + y;
}

private:
int x;
int y;
};

那么上面的代码就是会造成很多冗余,尤其是当 int sum = x + y; 所简化的代码实际上是很多复杂的初始化操作的时候。于是我们就可以用到委托构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {
public:
Point() : Point(0, 0) { // 这里调用另一个构造函数
// 默认构造函数委托给带参数构造函数
}

Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {
// 带参数构造函数
int sum = x + y;
}

Point(int value) : Point(value, value) { // 这里调用另一个构造函数
// 参数为相同值的构造函数委托给带参数构造函数
}

private:
int x;
int y;
};

如上所示,通过委托函数,不再需要每个构造函数都执行 int sum = x + y; 减少了代码的冗余.

委托构造函数指的是主动发起调用的那个构造函数,也就是在构造函数内部通过委托调用另一个构造函数来共享初始化逻辑。被委托调用的构造函数称为目标构造函数。

通过使用委托构造函数,我们避免了重复编写相同的初始化代码。当我们改变构造函数的初始化逻辑时,只需要修改一处,所有委托该逻辑的构造函数都会自动更新。

需要注意以下几点:

  • 委托构造函数的初始化列表中只能调用同一个类中的其他构造函数,而不能调用基类的构造函数或其他类的构造函数。
  • 委托构造函数的参数列表必须与被调用构造函数的参数列表匹配。
  • 委托构造函数只能在成员初始化列表中调用另一个构造函数,不能在函数体中调用
  • 委托构造函数不需要包含构造函数的实际初始化操作,因为它会调用被委托的构造函数来完成初始化。

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
public:
MyClass() : MyClass(0) {
// 委托给其他构造函数
// 可以在委托构造函数中执行一些通用的初始化操作
}

MyClass(int value) : member(value) {
// 实际构造函数,完成特定的初始化操作
}

private:
int member;
};

int main() {
MyClass obj1; // 调用委托构造函数
MyClass obj2(42); // 调用实际构造函数
return 0;
}

委托构造函数的函数体中的代码先执行还是委托构造函数的初始化列表中的目标构造函数先执行?

在委托构造函数中,委托构造函数的初始化列表中的目标构造函数会先执行,然后才会执行委托构造函数的函数体中的代码。

这是因为委托构造函数的目的是将初始化工作委托给另一个构造函数完成,因此首先会执行目标构造函数的初始化列表中的代码,以确保对象的成员变量得到正确的初始化。然后,委托构造函数的函数体中的代码会在目标构造函数初始化之后执行。

以下是一个示例来说明这个顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class MyClass {
public:
MyClass() : MyClass(0) {
std::cout << "委托构造函数的函数体中的代码" << std::endl;
}

MyClass(int value) : member(value) {
std::cout << "目标构造函数的初始化列表中的代码" << std::endl;
}

private:
int member;
};

int main() {
MyClass obj; // 输出顺序:目标构造函数 -> 委托构造函数的函数体
return 0;
}

在上述示例中,对象 obj 的创建过程中,== 首先会执行目标构造函数的初始化列表中的代码,然后执行委托构造函数的函数体中的代码。== 这是因为目标构造函数负责实际的初始化操作,而委托构造函数负责调用目标构造函数并共享初始化逻辑.

总结一下,委托构造函数的好处是提高了代码的可维护性和可读性,避免了重复编写相同的初始化代码,同时确保所有构造函数都使用了一致的初始化逻辑。这对于类的构造函数重用和管理非常有帮助。

实际工程中遇到的委托构造函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class [[nodiscard]] thread_pool
{
public:
thread_pool() : thread_pool(0, [] {}) {}

explicit thread_pool(const concurrency_t num_threads) : thread_pool(num_threads, [] {}) {}

explicit thread_pool(const std::function<void()>& init_task) : thread_pool(0, init_task) {}

thread_pool(const concurrency_t num_threads, const std::function<void()>& init_task) : thread_count(determine_thread_count(num_threads)), threads(std::make_unique<std::thread[]>(determine_thread_count(num_threads)))
{
create_threads(init_task);
}

...
}

# i、using 和 typedef 的区别

#CPP 新特性 #CPP11
using 别名是 C++11 标准引入的特性,它提供了更多的灵活性和可读性。使用 using 别名可以轻松地为现有类型创建别名,甚至可以为模板类型创建别名。现代 C++ 更倾向于使用 using,更加直观明了。

使用 typedef 的方式:

1
2
typedef int MyInt; // 创建 int 的别名 MyInt
typedef std::vector<int> IntVector; // 创建 std::vector<int> 的别名 IntVector

使用 using 的方式:

1
2
using MyInt = int; // 创建 int 的别名 MyInt
using IntVector = std::vector<int>; // 创建 std::vector<int> 的别名 IntVector

using 别名有一些明显的优势:

  1. 可读性更强: using 别名的语法更加自然,易于理解。它直观地表达了创建类型别名的意图。
  2. 支持模板别名: using 别名支持为模板类型创建别名,而 typedef 在处理模板类型时可能会显得复杂。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template <typename T>
    using MyVector = std::vector<T>; // 为 std::vector<T> 创建一个别名 MyVector

    int main() {
    MyVector<int> numbers; // 使用 MyVector 作为 std::vector<int> 的别名
    numbers.push_back(42);
    numbers.push_back(73);

    for(const auto& num : numbers) {
    std::cout << num << " ";
    } std::cout << std::endl;

    return 0;
    }
  3. auto 关联性更强: using 别名与 C++11 中引入的 auto 关联性更强,使得在使用类型推断时更加便利。

特别地,如果是对结构体 struct 的别名,两个关键字的语法分别如下:

1
2
3
4
5
6
7
8
9
typedef struct {
int x;
int y;
} Point;

typedef struct {
float width;
float height;
} Size;

1
2
3
4
5
6
7
8
9
using Point = struct {
int x;
int y;
};

using Size = struct {
float width;
float height;
};

# i、cast 类型转换

在 C++ 中,类型转换(Type Casting)是一种将一个数据类型的值转换为另一个数据类型的过程。C++ 提供了几种不同类型的类型转换,可大致分为隐式转换和显式转换,显式转换又包含:C 风格转换、 static_castdynamic_castconst_castreinterpret_cast

1. static_cast

  • 用途: static_cast 主要用于执行静态类型转换,可以在合理范围内进行类型转换。它用于处理通常是安全的、定义良好的转换。
  • 示例:
    1
    2
    int integer = 42;
    float floatingPoint = static_cast<float>(integer);

2. dynamic_cast

  • 用途: dynamic_cast 主要用于在继承层次结构中执行基类到派生类的安全转换。它在运行时进行类型检查,只有当对象实际上是目标类型的派生类时,才会执行转换。通常与多态相关。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Base { virtual void foo() {} };
    class Derived : public Base { void foo() {} };

    Base* basePtr = new Derived;
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
    // 转换成功,derivedPtr 指向 Derived 类型的对象
    } else {
    // 转换失败,basePtr 不是 Derived 类型的对象
    }

3. const_cast

  • 用途: const_cast 用于去除变量的 const 修饰符,允许修改原本被视为只读的对象。它通常用于修复旧代码或在需要时更改数据的常量属性。
  • 示例:
    1
    2
    3
    const int readOnlyValue = 42;
    int& readWriteReference = const_cast<int&>(readOnlyValue);
    readWriteReference = 100; // 合法,修改了原本的只读变量

4. reinterpret_cast

  • 用途: reinterpret_cast 用于执行低级别的转换,通常用于处理指针和引用之间的转换。这是最不安全的类型转换,因此应谨慎使用。
  • 示例:
    1
    2
    3
    int integerValue = 42;
    int* intPointer = &integerValue;
    double* doublePointer = reinterpret_cast<double*>(intPointer);
# static_cast 和 dynamic_cast 的区别和联系

区别
static_cast

  1. 静态类型转换static_cast 是一种在编译时进行类型转换的操作。它对于已知的、合法的转换非常有用,如整数到浮点数、指针之间的类型转换等。
  2. 非安全性static_cast 不执行运行时类型检查,因此在进行类型转换时,开发者需要确保转换是安全的,否则可能导致未定义的行为。
  3. 用途:主要用于基本的类型转换,如整数之间的转换、指针类型的转换,以及在继承层次结构中进行向上或向下的类型转换。

dynamic_cast

  1. 动态类型转换dynamic_cast 是一种在运行时进行类型转换的操作,主要用于处理继承层次结构中的类型转换。
  2. 安全性dynamic_cast 执行运行时类型检查,确保只有当对象实际上是目标类型的派生类时,才会执行转换。如果类型不匹配,它返回 nullptr (对于指针类型)或抛出 std::bad_cast 异常(对于引用类型)。
  3. 用途:主要用于处理多态性,以安全地进行基类到派生类的类型转换。它在运行时提供了类型检查,可用于确保只有正确类型的对象才会进行转换。

联系

  1. static_castdynamic_cast 都用于类型转换,但它们的用途和时机不同。 static_cast 主要用于基本类型和向上 / 向下的继承层次结构中,而 dynamic_cast 主要用于多态性继承中的安全类型转换。
  2. static_cast 是在编译时执行的,因此更快,但不提供类型检查。 dynamic_cast 是在运行时执行的,提供了类型检查,但可能更慢。
  3. 在多态性和继承的情况下,应优先选择 dynamic_cast 以确保类型安全。 static_cast 更适用于普通的类型转换,如整数之间的转换。
  4. static_castdynamic_cast 都需要开发者谨慎使用,以避免不安全的类型转换和潜在的错误。

在编写代码时,应根据具体情况和安全性需求来选择使用哪种类型转换方式。如果不确定,最好选择更安全的 dynamic_cast

# i、C++ 内部链接

内部链接(internal linkage)是 C++ 语言中的一个概念,用于描述声明或定义的实体在编译单元内部可见,而在其他编译单元中不可见的特性。

具有内部链接的实体只能在其定义所在的编译单元(通常是一个源文件)内部访问,而无法在其他编译单元中进行访问。这样做的目的是将实体限制在特定的作用域内,从而提高代码的模块化性和封装性。

在 C++ 中,可以使用两种方式来实现内部链接:

  1. 未命名命名空间(unnamed namespace):通过将实体放置在未命名命名空间中,可以确保这些实体仅在当前编译单元中可见,而无法在其他编译单元中访问。

    1
    2
    3
    namespace {
    ...
    } // 未命名 namespace

  2. static 关键字:将函数或变量声明为 static,可以使它们具有内部链接。这意味着它们只能在当前编译单元中访问,而不能在其他编译单元中使用。

内部链接是 C++ 编程中常用的技术之一,用于控制实体的可见性和访问范围,有助于提高代码的可维护性和安全性。

# i、extern 关键字

extern 是 C++ 中的一个关键字,用于说明变量或函数的链接性(linkage)和作用域(scope)。它可以用在不同的上下文中,通常用于以下两个方面:

1. 链接性(Linkage):
extern 用于说明一个变量或函数是在其他文件中定义的,而不是当前文件中定义的。它告诉编译器在链接时在其他文件中查找这个变量或函数的定义。在 C++ 中,变量和函数默认情况下具有外部链接性,也就是可以在其他文件中使用。但如果你想明确地指定一个变量或函数为外部链接性,你可以使用 extern 关键字。例如:

1
2
3
4
5
// 声明一个全局变量 x,该变量在其他文件中定义
extern int x;

// 声明一个函数 foo,该函数在其他文件中定义
extern void foo();

这些声明告诉编译器在链接时查找变量 x 和函数 foo 的定义。

特别地,我们会用到 extern C 来告诉编译器使用 C 语言的函数链接性。
extern "C" 是一种用于修改函数链接性和名称修饰的用法,通常与 C 和 C++ 混合编程时使用。它有以下作用:

a. 函数链接性:
在 C++ 中,函数默认会被名称修饰(name-mangling),以便支持函数重载。这导致 C++ 函数的名称在目标文件中不再是原始的函数名。然而,C 语言不使用名称修饰,因此在 C 和 C++ 混合编程时,需要确保 C++ 函数能够与 C 函数进行链接。使用 extern "C" 可以告诉编译器使用 C 语言的函数链接性。
b. 函数名称修饰:
在 C++ 中,函数名称会根据参数的类型和个数进行修饰,以支持函数重载。而 C 语言不支持函数重载,因此函数名称不会被修饰。使用 extern "C" 可以防止 C++ 对函数名称进行修饰,使函数名与 C 语言一致。

下面是一个示例:

1
2
3
4
5
6
7
8
9
// 声明一个C函数
extern "C" {
void c_function(int arg);
}

// C++函数
void cpp_function(int arg) {
// ...
}

在上面的示例中, c_function 声明使用了 extern "C" ,这表示它的链接性和名称修饰与 C 语言兼容。这样,C 和 C++ 代码可以正确链接并一起工作。

2. 作用域(Scope):
extern 也可以用于指示一个变量或函数的作用域是全局的,即它可以在程序的任何地方访问。通常,全局变量和函数默认具有全局作用域,但 extern 可以用于强调这一点。例如:

1
2
// 声明全局变量 y 具有全局作用域
extern int y;

这个声明表示变量 y 具有全局作用域,可以在整个程序中访问。

总之, extern 是一个用于说明变量或函数链接性和作用域的关键字,它在 C++ 中通常用于与其他文件共享变量或函数的定义。

# i、dlopen、dlsym 和 dlclose

通过  dlopen()  函数以指定模式加载指定的动态链接库,并返回一个句柄。

dlsym()  可通过 dlopen()  返回的句柄来调用指定的函数。

通过  dlclose()  来卸载已加载的动态库。

dlopen() 的函数原型:

1
void *dlopen(const char* pathname, int mode);

pathname 是动态库的路径。

mode 是打开方式,有多种,这里列举两个: RTLD_LAZY  执行延迟绑定。仅在执行引用它们的代码时解析符号。如果从未引用该符号,则永远不会解析它(只对函数引用执行延迟绑定。在加载共享对象时,对变量的引用总是立即绑定)。 RTLD_NOW  如果指定了此值,或者环境变量  LD_BIND_NOW  设置为非空字符串,则在  dlopen()  返回之前,将解析共享对象中的所有未定义符号。如果无法执行此操作,则会返回错误。

打开失败时返回 NULL,打开成功则返回该动态库的句柄。

dlsym() 的函数原型:

1
voiddlsym(void* handle, const char* symbol);

该函数的作用是根据动态链接库的操作句柄 (handle) 与符号 (symbol),返回符号对应的地址。这里的符号既可以是函数名,也可以是变量名。于是我们通过这个函数,就可以获取动态库中的函数或变量的地址,就可以调用动态库中的相关函数。

该函数的返回值  void*  指向函数的地址,供调用使用。

dlclose() 就没啥好说的了,卸载相应的动态库。

具体使用示例:

1
2
3
4
void *handle = dlopen(str.c_str(), RTLD_LAZY);
typedef void (*so_config)(int argc, char **argv);
so_config config_func = dynamic_cast<so_config>(dlsym(handle, "Config"));
config_func(ptr_len, ptr_ptr);

dlerror() 是一个用于获取动态链接库错误信息的函数,通常在使用动态链接库时发生错误时调用。该函数位于 <dlfcn.h> 头文件中,是 POSIX 标准的一部分。

当调用动态链接库相关函数(如 dlopen()dlsym()dlclose() 等)失败时,系统会设置一个全局错误状态,并通过 dlerror() 函数返回错误信息。如果前一个动态链接库相关函数调用成功,那么 dlerror() 将返回 NULL。

可以通过如下方式来使用 dlerror() 函数:

  1. 当一个动态链接库相关函数返回 NULL 时,首先调用 dlerror() 函数来确定是否有错误发生。
  2. 如果 dlerror() 返回非 NULL 指针,则表示有错误发生,可以通过返回的字符串来获取错误信息。
  3. 如果 dlerror() 返回 NULL,则表示前一个动态链接库相关函数调用成功,没有错误发生。

通过这种方式,可以在动态链接库加载、符号查找等过程中及时捕获并处理错误,提高程序的健壮性和可靠性。

# i、error 预处理指令

#error 是 C/C++ 预处理器的一条指令,用于在预处理阶段生成编译错误信息。当预处理器遇到 #error 指令时,它会输出指定的错误消息,并终止编译过程。这对于在编译时进行条件检查非常有用,例如版本检查、平台检查或配置检查。

1
2
3
#if (BS_THREAD_POOL_TEST_VERSION_MAJOR != BS_THREAD_POOL_VERSION_MAJOR)
#error The versions do not match. Aborting compilation.
#endif

# i、quiet_NaN

std::numeric_limits<float>::quiet_NaN() 返回表示 “静默 NaN(Not a Number)” 的浮点数。
NaN 是 IEEE 浮点数标准中定义的特殊值,用于表示不是数字的结果,通常用于错误处理或特殊情况下的标记。

这个函数返回的值是一个 float 类型的静默 NaN,它的位表示是特殊的,用于标记不是数字的情况。它不会抛出异常,因为它只是返回一个特定的值,不涉及任何运算或转换。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <limits>

int main() {
// 获取 float 类型的 NaN
float nan = std::numeric_limits<float>::quiet_NaN();

// 检查 NaN 是否等于自身
if (nan != nan) {
std::cout << "nan is not equal to itself" << std::endl;
} else {
std::cout << "nan is equal to itself" << std::endl;
}

return 0;
}

在示例中,我们获取了 float 类型的 NaN,并检查它是否等于自身。
由于 NaN 的特性,它与自身不相等,因此程序输出 "nan is not equal to itself"。

# NaN 的打印

NaN 值是一个特殊的浮点数,它在打印时通常会显示为 "nan" (不区分大小写)。
因此,使用 std::cout 打印 std::numeric_limits<float>::quiet_NaN() 将会输出 "nan"

示例代码:

1
2
3
4
5
6
7
8
#include <iostream>
#include <limits>

int main() {
float nan = std::numeric_limits<float>::quiet_NaN();
std::cout << "NaN value: " << nan << std::endl;
return 0;
}

输出结果将会是:

1
NaN value: nan

# i、unordered_map/unordered_set 存储自定义类型

在使用 unordered_mapunordered_set 存储自定义结构体时,需要注意以下几点:

  1. 哈希函数和相等比较函数:
    默认情况下, unordered_mapunordered_set 需要对键值进行哈希计算和相等比较。对于内置类型,这些操作已经定义好了,但对于自定义结构体,需要提供这两个函数。
  2. 哈希函数:
    必须提供一个自定义的哈希函数来计算自定义结构体的哈希值。你可以通过定义一个结构体或者函数对象来实现,也可以使用 C++11 的 std::hash 模板特化。
  3. 相等比较函数:
    需要提供一个自定义的相等比较函数,以确保 unordered_mapunordered_set 能正确比较两个自定义结构体是否相等。可以通过重载 operator== 或者定义一个函数对象来实现。

以下是一个具体的示例,展示了如何为 testStruct 定义哈希函数和相等比较函数,使其可以用于 unordered_mapunordered_set

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <unordered_map>
#include <unordered_set>

// 定义自定义结构体
struct testStruct {
int a;
float b;

// 重载 operator==,以便在哈希容器中进行比较
bool operator==(const testStruct& other) const {
return a == other.a && b == other.b;
}
};

// 自定义哈希函数
struct testStructHash {
std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
// 组合两个哈希值
return h1 ^ (h2 << 1);
}
};

// 用于测试的 main 函数
int main() {
// 使用自定义结构体作为键的 unordered_map
std::unordered_map<testStruct, std::string, testStructHash> umap;
testStruct ts1 = {1, 1.1f};
testStruct ts2 = {2, 2.2f};

umap[ts1] = "test1";
umap[ts2] = "test2";

for(const auto& pair : umap) {
std::cout << "Key: (" << pair.first.a << ", " << pair.first.b << "), Value: " << pair.second << std::endl;
}

// 使用自定义结构体的 unordered_set
std::unordered_set<testStruct, testStructHash> uset;
uset.insert(ts1);
uset.insert(ts2);

for(const auto& elem : uset) {
std::cout << "Element: (" << elem.a << ", " << elem.b << ")" << std::endl;
}

return 0;
}

在这个示例中,我们做了以下几步:

  1. 定义 testStruct 并重载 operator== : 以便在哈希容器中进行结构体比较。
  2. 定义 testStructHash 结构体:用于计算 testStruct 的哈希值。我们使用了 std::hashintfloat 类型进行哈希计算,并将它们组合起来。
  3. main 函数中测试:使用 unordered_mapunordered_set 存储和访问 testStruct

这样, testStruct 就可以顺利地用于 unordered_mapunordered_set 中。

为什么不使用 lambda 表达式来实现哈希,而是要采用结构体?
为什么说 lambda “可读性和扩展性相对较低”,请你具体举例说明?

用 lambda 表达式定义哈希函数确实可以简洁地实现某些功能,但在某些情况下,其可读性和扩展性相对于结构体方式可能会稍显不足。下面具体举例说明其不足之处:

  • 可读性问题
  1. 代码量较多时可读性下降:
    当哈希计算逻辑复杂时,lambda 表达式可能会变得难以阅读。例如:

1
2
3
4
5
6
auto complexHash = [](const testStruct& ts) -> std::size_t {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
std::size_t h3 = std::hash<int>()(ts.a + static_cast<int>(ts.b * 100));
return h1 ^ (h2 << 1) ^ (h3 << 2);
};

相对于这种复杂的 lambda 表达式,使用结构体来封装会使逻辑更清晰:

1
2
3
4
5
6
7
8
struct testStructHash {
std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
std::size_t h3 = std::hash<int>()(ts.a + static_cast<int>(ts.b * 100));
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};

  • 扩展性问题
  1. 不能直接存储状态:
    lambda 表达式本身不太适合存储状态。如果哈希函数需要存储一些额外的数据,使用结构体会更方便。例如:

1
2
3
4
5
6
7
8
9
10
11
struct testStructHashWithState {
int someState;

testStructHashWithState(int state) : someState(state) {}

std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return (h1 ^ (h2 << 1)) + someState;
}
};

用 lambda 表达式存储状态则相对麻烦:

1
2
3
4
5
6
7
8
int someState = 42;
auto hashFn = [someState](const testStruct& ts) -> std::size_t {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return (h1 ^ (h2 << 1)) + someState;
};

std::unordered_map<testStruct, std::string, decltype(hashFn)> umap(10, hashFn);

  • 模板特化的灵活性

如果需要在模板特化中定义哈希函数,使用结构体会更自然。示例:

1
2
3
4
5
6
7
8
9
10
11
12
namespace std {
template <>
struct hash<testStruct> {
std::size_t operator()(const testStruct& ts) const {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return h1 ^ (h2 << 1);
}
};
}

std::unordered_map<testStruct, std::string> umap;

  • 代码复用

结构体的方式便于代码复用和维护。例如,如果哈希计算逻辑需要在多个地方使用,用结构体封装的哈希函数可以直接复用,而不用在每个地方都写一次 lambda 表达式:

1
2
std::unordered_map<testStruct, std::string, testStructHash> umap1;
std::unordered_set<testStruct, testStructHash> uset1;

如果用 lambda 表达式,每个地方都需要定义一次:

1
2
3
4
5
6
7
8
auto hashFn = [](const testStruct& ts) -> std::size_t {
std::size_t h1 = std::hash<int>()(ts.a);
std::size_t h2 = std::hash<float>()(ts.b);
return h1 ^ (h2 << 1);
};

std::unordered_map<testStruct, std::string, decltype(hashFn)> umap1(10, hashFn);
std::unordered_set<testStruct, decltype(hashFn)> uset1(10, hashFn);

  • 总结
    尽管 lambda 表达式在简单情况下非常方便,但当涉及复杂逻辑、状态存储、代码复用以及模板特化时,结构体的方式更具优势。因此,综合考虑可读性和扩展性,结构体定义方式通常是更好的选择。

这里重载 operator== 的作用是什么,为什么不用重载大于 > 或者小于 < ?

在 C++ 中, operator==operator!= 通常用于相等性比较,而 operator<operator> 等用于排序。对于 std::unordered_mapstd::unordered_set 来说,使用哈希表实现,因此需要的是相等性比较而不是排序比较。

std::unordered_mapstd::unordered_set 使用哈希表来存储元素。当我们插入、查找或删除元素时,这些容器会先根据哈希值(由哈希函数计算得出)将元素分配到某个桶中,然后在桶内使用 operator== 来比较元素是否相等。因此,为了在这些容器中使用自定义类型 testStruct ,我们必须重载 operator== 以定义两个 testStruct 对象何时被认为是相等的。

1
2
3
4
5
6
7
8
9
class testStruct {
public:
int a = 0;
float b = 0;

bool operator==(const testStruct& other) const {
return a == other.a && b == other.b;
}
};

  • 为什么不用 operator<operator>

operator<operator> 主要用于排序比较,在需要排序功能的容器如 std::mapstd::set 中会用到。这些容器是基于红黑树等平衡二叉树实现的,因此需要知道两个元素之间的顺序关系。

但是, std::unordered_mapstd::unordered_set基于哈希表实现的,哈希表不需要排序元素,只需要将元素正确地放入桶中并能正确地比较桶内的元素是否相等。因此,在这种情况下,重载 operator<operator> 是没有意义的,哈希表并不会使用这些操作符。

  • 总结
  1. operator== 的作用operator== 用于定义两个 testStruct 对象何时被认为是相等的,这对于哈希表在桶内比较元素是必要的。
  2. 不用 operator<operator> std::unordered_mapstd::unordered_set 基于哈希表实现,不需要排序元素,因此不需要 operator<operator> 。这些操作符主要用于排序容器如 std::mapstd::set
# 标准示范

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include <set>

class MyStruct {
public:
int a = 0;
float b = 0;

// for unordered_map and unordered_set
bool operator==(const MyStruct& other) const {
return a == other.a && b == other.b;
}

// for map and set
bool operator<(const MyStruct& other) const {
return a < other.a && b < other.b;
}
};

// for unordered_map and unordered_set
struct MyHash {
// this should be double const
std::size_t operator()(const MyStruct& ms) const {
std::size_t h1 = std::hash<int>()(ms.a);
std::size_t h2 = std::hash<float>()(ms.b);
return h1 ^ (h2 << 1);
}
};



int main() {
std::unordered_map<MyStruct, int, MyHash> um;
um.clear();

MyStruct aa;
MyStruct bb;

um[aa] = 0;
um[bb] = 1;

std::unordered_set<MyStruct, MyHash> us;

us.insert(aa);
us.insert(bb);

std::set<MyStruct> s;
s.insert(aa);
s.insert(bb);

std::map<MyStruct, int> m;
m[aa] = 0;
m[bb] = 1;

return 0;
}

# i、std::unordered_map 的使用

unordered_map 是 C++ 标准库中的一种关联容器,它提供了一种无序、基于键值对的数据存储结构,具有快速的查找、插入和删除操作。 unordered_map 基于 哈希表 实现,可以在平均情况下以接近 O(1) 的时间复杂度进行元素的查找、插入和删除。

下面是 unordered_map 的一些常见用法:

  1. 创建 unordered_map 对象

    1
    2
    3
    4
    #include <unordered_map>

    std::unordered_map<int, std::string> myMap;
    // 创建一个键为int类型,值为std::string类型的unordered_map对象

  2. 插入元素

    1
    2
    myMap[1] = "value1"; // 插入键值对 <1, "value1">
    myMap.insert({2, "value2"}); // 使用insert方法插入键值对 <2, "value2">

    注意,如果是用 insert 的话,需要构造一个 std::pair 的对象。

  3. 访问元素

    1
    std::string value = myMap[1]; // 访问键为1的值,如果键不存在,则会插入一个默认构造的值

    注意!!!这种方式是存在副作用的,即 “键不存在时创建默认值”。
    如果我们需要通过 map 的 size 来决定代码的逻辑,则应该避免使用这种方式来访问元素,以免对 size 造成影响!

1
2
3
4
auto iter = myMap.find(2); // 使用find方法查找键为2的元素
if(iter != myMap.end()) {
std::string value = iter->second; // 访问找到的值
}

  1. 删除元素

    1
    myMap.erase(1); // 删除键为1的元素

  2. 遍历 unordered_map

    1
    2
    3
    for(const auto& pair : myMap) {
    std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl;
    }

  3. 获取 unordered_map 的大小

    1
    int size = myMap.size(); // 获取unordered_map中键值对的个数

  4. 检查 unordered_map 是否为空

    1
    bool isEmpty = myMap.empty(); // 判断unordered_map是否为空

  5. 清空 unordered_map

    1
    myMap.clear(); // 清空unordered_map中的所有元素

  6. 使用自定义哈希函数
    如果 unordered_map 的键类型不是基本类型(如 intstd::string 等),或者需要自定义哈希函数,可以通过 unordered_map 的第三个模板参数指定自定义的哈希函数,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct MyKey {
    int id;
    std::string name;
    };

    struct MyKeyHash {
    std::size_t operator()(const MyKey& key) const {
    return std::hash<int>()(key.id) ^ (std::hash<std::string>()(key.name) << 1);
    }
    };

    std::unordered_map<MyKey, int, MyKeyHash> myMap; // 使用自定义的哈希函数

unordered_map 提供了丰富的功能,可以用于各种数据存储和处理场景,是 C++ 中非常常用的数据结构之一。


# i、实现一个二分查找算法

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 <iostream>
#include <vector>

// 自定义二分查找函数
template <typename T>
int binarySearch(const std::vector<T>& arr, const T& target) {
int low = 0;
int high = arr.size() - 1;

while(low <= high) {
int mid = low + (high - low) / 2;

if(arr[mid] == target) {
return mid; // 找到目标值,返回索引
} else if (arr[mid] < target) {
low = mid + 1; // 目标值在右半部分,调整区间的下界
} else {
high = mid - 1; // 目标值在左半部分,调整区间的上界
}
} return -1; // 没有找到目标值,返回 -1
}

int main() {
std::vector<int> arr = {2, 4, 6, 8, 10, 12, 14, 16, 18}; // 注意,二分法需要应用于已排序的数组
int target = 12;
int index = binarySearch(arr, target);

if(index != -1) {
std::cout << "Target found at index " << index << std::endl;
} else {
std::cout << "Target not found in the array" << std::endl;
}

return 0;
}

另外,STL 其实有封装二分查找的库,所以其实调个 API 就能实现了。见:[[C++ 库函数 #i、binary_search|binary_search]]

# i、用 C++ 实现 strcpy 函数

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
#include <iostream>

void strcpy(char* dest, const char* src) {
// 检查指针是否为空
if(dest == nullptr || src == nullptr) {
return;
}

// 复制字符串内容直到遇到空字符 '\0'
while(*src != '\0') {
*dest = *src;
dest++;
src++;
}

// 在目标字符串末尾添加空字符 '\0'
*dest = '\0';
}

int main() {
const char* source = "Hello, world!";
char destination[20];

strcpy(destination, source);

std::cout << "Copied string: " << destination << std::endl;

return 0;
}

在上述代码中,strcpy 函数接受两个参数, dest 表示目标字符串的指针, src 表示源字符串的指针。函数通过遍历源字符串的每个字符,逐个将其复制到目标字符串,直到遇到空字符 \0 ,表示字符串的结束。最后,将目标字符串的末尾设置为空字符 \0 ,以确保复制后的字符串正确终止。

# 内存踩踏

当源字符串指针(src)和目标字符串指针(dest)所指向的内存有重叠部分时,使用标准的 strcpy 函数或自定义的 strcpy 函数可能会导致意想不到的结果。

在 C++ 中,按照标准的行为, strcpy 函数不应该用于处理重叠内存区域的字符串复制。这是因为在重叠内存区域中,复制的操作可能会导致数据的不可预测的改变。

具体来说,如果在 strcpy 函数中源字符串和目标字符串的内存区域重叠,复制过程中的操作可能会覆盖尚未被复制的数据,导致数据损坏或不正确的复制结果。

为了处理重叠内存区域的字符串复制,可以使用 memmove 函数,它被设计为可以处理重叠内存区域的内存复制操作。 memmove 函数的实现可以通过使用指针操作来实现内存块的移动。下面是一个用 C++ 编写的简单 memmove 函数的示例:

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
36
37
38
39
40
41
42
43
#include <iostream>

void* memmove(void* dest, const void* src, size_t size) {
// 检查指针是否为空
if (dest == nullptr || src == nullptr) {
return nullptr;
}

// 创建临时缓冲区
char* temp = new char[size];

// 复制源内存块到临时缓冲区
const char* srcPtr = static_cast<const char*>(src);
char* tempPtr = temp;
for(size_t i = 0; i < size; ++i) {
*tempPtr++ = *srcPtr++;
}

// 将临时缓冲区的内容复制到目标内存块
char* destPtr = static_cast<char*>(dest);
tempPtr = temp;
for(size_t i = 0; i < size; ++i) {
*destPtr++ = *tempPtr++;
}

// 释放临时缓冲区
delete[] temp;

return dest;
}

int main() {
char source[] = "Hello, world!";
char destination[20];

std::cout << "Before memmove: " << destination << std::endl;

memmove(destination, source, sizeof(source));

std::cout << "After memmove: " << destination << std::endl;

return 0;
}

在上述代码中, memmove 函数接受三个参数:目标指针( dest )、源指针( src )和要复制的字节数( size )。函数首先创建一个临时缓冲区,然后将源内存块的内容复制到临时缓冲区,最后再将临时缓冲区的内容复制到目标内存块。

请注意,在实际编写代码时,需要考虑更多的边界条件和错误处理,以确保函数的正确性和安全性。此外,C++ 标准库也提供了 std::memmove 函数,它是一个更为优化和健壮的实现,建议在实际使用中使用标准库提供的函数。

可以在不使用额外临时缓冲区的情况下实现 memmove 函数。一种常见的方法是使用指针操作和适当的条件判断来处理重叠内存块的移动。

下面是一个不使用额外临时缓冲区的 memmove 函数的示例实现:

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
36
37
38
39
#include <iostream>

void* memmove(void* dest, const void* src, size_t size) {
// 检查指针是否为空
if(dest == nullptr || src == nullptr) {
return nullptr;
}

char* destPtr = static_cast<char*>(dest);
const char* srcPtr = static_cast<const char*>(src);

// 判断重叠情况
if(srcPtr < destPtr) {
// 从后往前复制
for(size_t i = size; i > 0; --i) {
destPtr[i - 1] = srcPtr[i - 1];
}
} else {
// 从前往后复制
for(size_t i = 0; i < size; ++i) {
destPtr[i] = srcPtr[i];
}
}

return dest;
}

int main() {
char source[] = "Hello, world!";
char destination[20];

std::cout << "Before memmove: " << destination << std::endl;

memmove(destination, source, sizeof(source));

std::cout << "After memmove: " << destination << std::endl;

return 0;
}

在上述代码中, memmove 函数首先判断源内存块和目标内存块的位置关系。如果源指针在目标指针之前,则从后往前逐个复制数据;如果源指针在目标指针之后,则从前往后逐个复制数据。通过这种方式,可以确保正确处理重叠内存块的复制,而无需使用额外的临时缓冲区。

需要注意的是,这种实现方式在处理重叠内存块时可能需要更多的指针操作和条件判断,因此在性能上可能不如使用临时缓冲区的实现方式高效。在实际使用时,根据具体的情况选择适合的实现方式,权衡性能和内存使用的需求。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\345\274\202\345\270\270\346\234\272\345\210\266/index.html" "b/2024/10/18/C-\345\274\202\345\270\270\346\234\272\345\210\266/index.html" index 8cd6e1c..44ec1d8 100644 --- "a/2024/10/18/C-\345\274\202\345\270\270\346\234\272\345\210\266/index.html" +++ "b/2024/10/18/C-\345\274\202\345\270\270\346\234\272\345\210\266/index.html" @@ -1 +1 @@ -C++ 异常机制 - C++ - 编程 | R1ck's Playground = how

# 一、概述

C++ 异常机制是一种用于处理程序运行时错误和异常情况的机制。它允许在发生异常时抛出异常对象(通过 throw 语句),并在适当的地方捕获并处理这些异常(通过 try-catch 块)。

异常的基本流程如下:

  1. throw 语句:当程序运行到某个错误或异常的情况时,可以使用 throw 语句抛出一个异常对象。异常对象可以是内置类型、自定义类型或标准库中的异常类。
  2. try-catch 块:使用 try 关键字开始一个代码块,该代码块可能会抛出异常。在 try 块中,程序会监视是否有异常被抛出,如果有,就会跳到与之匹配的 catch 块。
  3. catch 块: catch 块用于捕获并处理异常。它指定了需要捕获的异常类型,并在发生异常时执行相应的处理逻辑。可以有多个 catch 块,每个 catch 块捕获不同类型的异常。

下面是一个具体的示例,展示了如何抛出和捕获异常:

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
#include <iostream>

void divideNumbers(int numerator, int denominator) {
if (denominator == 0) {
throw std::runtime_error("Divide by zero exception");
}

int result = numerator / denominator;
std::cout << "Result: " << result << std::endl;
}

int main() {
try {
int numerator, denominator;
std::cout << "Enter numerator: ";
std::cin >> numerator;
std::cout << "Enter denominator: ";
std::cin >> denominator;

divideNumbers(numerator, denominator);
} catch (const std::exception& ex) {
std::cout << "Exception caught: " << ex.what() << std::endl;
}

return 0;
}

在上述代码中, divideNumbers 函数用于计算两个数的除法。如果分母为零,将抛出一个 std::runtime_error 异常,并带有自定义的错误消息。

main 函数中,我们使用 try-catch 块来捕获可能抛出的异常。如果 divideNumbers 函数抛出异常,将跳转到匹配的 catch 块。在本例中,我们使用 const std::exception& 引用来捕获任何类型的异常,并使用 ex.what() 获取异常对象的错误消息。

如果用户在输入分母时输入了零,将会触发异常,然后程序将跳转到 catch 块,并输出相应的错误消息。

这个示例展示了如何使用 throw 语句抛出异常,并在 try-catch 块中捕获和处理异常。通过适当地处理异常,我们可以改善程序的健壮性和容错性。

# 二、异常类

补充说明: 在 C++ 中,可以抛出不同类型的异常,包括标准库中提供的异常类,也可以自定义异常类来满足特定需求。通常,异常类应该从 std::exception 类派生,以便能够通过基类的引用或指针进行统一的异常捕获。

自定义异常类的示例代码如下:

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 <iostream>

class MyException : public std::exception {
public:
MyException(const std::string& message) : message_(message) {}

const char* what() const noexcept override {
return message_.c_str();
}

private:
std::string message_;
};

void processInput(int value) {
if (value < 0) {
throw MyException("Negative value exception");
}

std::cout << "Value: " << value << std::endl;
}

int main() {
try {
int value;
std::cout << "Enter a positive value: ";
std::cin >> value;

processInput(value);
} catch (const std::exception& ex) {
std::cout << "Exception caught: " << ex.what() << std::endl;
}

return 0;
}

在上述代码中,我们自定义了一个名为 MyException 的异常类,它从 std::exception 派生。我们在 processInput 函数中检查输入值,如果值为负数,将抛出 MyException 异常,并传递自定义的错误消息。

main 函数中,我们使用 try-catch 块来捕获可能抛出的异常。如果 processInput 函数抛出 MyException 异常,将跳转到匹配的 catch 块,并输出相应的错误消息。

# 三、异常的好处

C++ 的异常机制提供了一种用于处理异常情况的结构化方式,具有以下几个好处:

  1. 分离正常流程和异常处理逻辑:异常机制允许我们将正常的业务逻辑与异常处理逻辑分离开来,使代码更加清晰和易于维护。我们可以专注于编写正常的代码,而将异常处理部分留给专门的异常处理程序。
  2. 异常处理的集中性:通过异常机制,我们可以将异常处理逻辑集中到一个或几个地方,提高代码的可读性和可维护性。这样,在代码中的每个可能引发异常的地方,我们不需要重复编写相同的异常处理逻辑,而是通过捕获异常并在适当的位置进行处理。
  3. 提供更灵活的错误处理:使用异常机制,我们可以将错误信息传递到适当的异常处理程序中,而不是通过错误码或返回值来传递。这样可以提供更多的上下文信息,并且可以根据具体情况采取不同的处理措施。
  4. 避免混淆错误处理和业务逻辑:使用条件语句来处理错误可能导致代码变得冗长和复杂,因为我们需要在每个可能引发异常的地方进行条件检查。 这样会使代码逻辑变得混乱,难以理解和维护。而异常机制能够将错误处理与业务逻辑分离,提高代码的可读性和可维护性。
  5. 异常的传播性:异常机制提供了异常在调用栈中传播的能力,即在函数调用链的各个层级中,异常可以被捕获和处理。这样可以避免在每个函数中都进行异常处理,只需要在适当的地方进行捕获和处理即可。(可以向上传播)

需要注意的是,异常机制并不适合处理所有类型的错误。对于某些可预测的错误或边界条件,使用条件语句来进行处理可能更加合适。异常应该用于表示意外情况、运行时错误或无法处理的情况。

总结起来,C++ 的异常机制提供了一种结构化的、集中处理异常的方式,使得代码更清晰、更易于维护。它分离了正常流程和异常处理逻辑,并提供了更灵活的错误处理方式,避免了混淆错误处理和业务逻辑。然而,在使用异常机制时,需要合理使用,根据具体情况选择使用异常还是条件语句处理错误。

# 四、异常与条件语句的区别

举例说明:
当处理文件读取异常时,让我们来比较使用异常机制和条件语句的处理方式。假设我们需要读取一个文件的内容,并输出文件的内容到控制台。

使用异常机制的示例代码如下所示:

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
#include <iostream>
#include <fstream>

void readFile(const std::string& filename) {
std::ifstream file(filename);

if(!file) {
throw std::runtime_error("Failed to open file: " + filename);
}

std::string line;
while(std::getline(file, line)) {
std::cout << line << std::endl;
} file.close();
}

int main() {
std::string filename = "example.txt";

try {
readFile(filename);
} catch (const std::exception& e) {
std::cerr << "Exception occurred: " << e.what() << std::endl;
}

return 0;
}

在上述代码中, readFile 函数尝试打开文件并读取其内容。如果文件打开失败,将抛出 std::runtime_error 异常。在 main 函数中,我们使用 try-catch 块来捕获可能抛出的异常,并在异常发生时进行处理。异常处理程序将打印异常信息到标准错误流。

现在,我们来看看使用条件语句的处理方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <fstream>

void readFile(const std::string& filename) {
std::ifstream file(filename);

if(file) {
std::string line;
while(std::getline(file, line)) {
std::cout << line << std::endl;
} file.close();
} else {
std::cerr << "Failed to open file: " << filename << std::endl;
}
}

int main() {
std::string filename = "example.txt";
readFile(filename);
return 0;
}

在这个示例中, readFile 函数尝试打开文件并读取其内容。如果文件打开成功,则将文件内容输出到控制台;如果文件打开失败,则直接在函数内部使用条件语句输出错误信息到标准错误流。

通过对比这两个示例,我们可以看到使用异常机制的好处:

  1. 代码清晰度:使用异常机制可以更清晰地将正常流程和异常处理逻辑分离。异常处理程序专门处理异常情况,而正常流程部分保持简洁
  2. 代码复用:异常处理程序位于 main 函数中的 try-catch 块中,可以在整个程序中重复使用,而不需要在每个可能引发异常的函数中编写重复的错误处理代码。
  3. 异常传播性:如果在 readFile 函数中发生异常,它将在调用栈中传播,直到被 main 函数中的 try-catch 块捕获。这样,我们可以在适当的地方集中处理异常,而不需要在每个函数中进行条件检查。

总结来说,异常机制提供了一种结构化的、集中处理异常的方式,使得代码更加清晰和易于维护。它分离了正常流程和异常处理逻辑,并提供了更灵活的错误处理方式。通过示例代码的对比,我们可以更好地理解使用异常机制的好处。

需要注意的是,在实际编码中,我们应该根据具体情况选择合适的错误处理方式。有些情况下,使用条件语句进行错误处理可能更加简洁和合适。异常机制应该用于表示意外情况、运行时错误或无法处理的情况。

# 五、异常类型

当使用 C++ 异常机制时,我们可以使用各种不同的异常类型来表示不同的错误或异常情况。C++ 标准库提供了一些常见的异常类型,同时我们也可以自定义异常类型来满足特定的需求。

下面是一些常见的 C++ 异常类型及其使用场景:

  1. std::exceptionstd::exception 是 C++ 异常类的基类,用于表示通用的异常情况。它提供了一个 what() 成员函数,返回异常的描述信息。
  2. std::runtime_errorstd::runtime_error 表示在运行时发生的错误,例如无效的参数、资源不可用等。它是从 std::exception 派生而来的,通常用于表示逻辑错误或不可恢复的错误。
  3. std::logic_errorstd::logic_error 表示逻辑错误,例如在代码中违反了逻辑规则、使用了无效的状态等。它也是从 std::exception 派生而来的,通常用于表示可预测且可修复的错误。
  4. std::invalid_argumentstd::invalid_argument 表示传递给函数的参数无效。例如,当函数要求一个有效的参数值,并且传递了一个无效值时,可以抛出该异常。
  5. std::out_of_rangestd::out_of_range 表示索引或迭代器超出了有效范围。例如,在数组、容器或字符串中使用无效的索引时可以抛出该异常。
  6. std::bad_allocstd::bad_alloc 表示内存分配失败。当动态内存分配操作(如 newmalloc )无法满足请求时,可以抛出该异常。

除了上述标准库提供的异常类型,我们还可以自定义异常类型来满足特定的需求。自定义异常类型可以从 std::exception 或其他标准库提供的异常类型派生,以便更好地表示特定的错误或异常情况。

使用适当的异常类型可以增加代码的可读性和可维护性,并提供有关错误或异常的更多上下文信息。当捕获异常时,我们可以根据异常类型进行精确的处理或采取适当的措施,以便正确地处理错误情况。

# 六、异常处理

如果您在代码中抛出了异常却没有相应的异常处理(没有进行 catch 操作),那么异常将会沿着调用栈向上传播,直到遇到能够捕获并处理该异常的地方,或者在程序的顶层时导致程序终止。

具体来说,当一个异常被抛出时,程序会搜索当前的执行上下文(函数调用栈)以查找能够处理该异常的 catch 语句块。如果找不到匹配的 catch 块,异常就会沿着调用栈向上传播,继续搜索调用栈中的下一个执行上下文,直到找到合适的 catch 块或者达到程序的顶层。

如果异常一直没有被捕获,而且在程序的顶层也没有合适的 catch 块来处理该异常,那么程序就会终止运行,并输出一个未捕获异常的错误信息。这通常会包括异常类型、异常发生的位置和相关的调用栈信息,以帮助定位问题。

因此,在编写代码时,建议捕获可能抛出的异常,并进行相应的处理操作,以便更好地控制程序的行为和错误处理。这样可以提高程序的健壮性和可靠性,避免未处理的异常导致程序异常终止。

需要注意的是,在某些特殊情况下,比如在应用程序的顶层或线程的入口点,可以使用类似 try-catch 的结构来捕获并处理未处理的异常,以便进行适当的日志记录和资源清理。

总之,如果抛出的异常没有被捕获,它将沿着调用栈向上传播,直到找到合适的 catch 块或导致程序终止。因此,合理地捕获和处理异常是确保程序稳定性和可靠性的重要步骤。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 异常机制 - C++ - 编程 | R1ck's Playground = how

# 一、概述

C++ 异常机制是一种用于处理程序运行时错误和异常情况的机制。它允许在发生异常时抛出异常对象(通过 throw 语句),并在适当的地方捕获并处理这些异常(通过 try-catch 块)。

异常的基本流程如下:

  1. throw 语句:当程序运行到某个错误或异常的情况时,可以使用 throw 语句抛出一个异常对象。异常对象可以是内置类型、自定义类型或标准库中的异常类。
  2. try-catch 块:使用 try 关键字开始一个代码块,该代码块可能会抛出异常。在 try 块中,程序会监视是否有异常被抛出,如果有,就会跳到与之匹配的 catch 块。
  3. catch 块: catch 块用于捕获并处理异常。它指定了需要捕获的异常类型,并在发生异常时执行相应的处理逻辑。可以有多个 catch 块,每个 catch 块捕获不同类型的异常。

下面是一个具体的示例,展示了如何抛出和捕获异常:

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
#include <iostream>

void divideNumbers(int numerator, int denominator) {
if (denominator == 0) {
throw std::runtime_error("Divide by zero exception");
}

int result = numerator / denominator;
std::cout << "Result: " << result << std::endl;
}

int main() {
try {
int numerator, denominator;
std::cout << "Enter numerator: ";
std::cin >> numerator;
std::cout << "Enter denominator: ";
std::cin >> denominator;

divideNumbers(numerator, denominator);
} catch (const std::exception& ex) {
std::cout << "Exception caught: " << ex.what() << std::endl;
}

return 0;
}

在上述代码中, divideNumbers 函数用于计算两个数的除法。如果分母为零,将抛出一个 std::runtime_error 异常,并带有自定义的错误消息。

main 函数中,我们使用 try-catch 块来捕获可能抛出的异常。如果 divideNumbers 函数抛出异常,将跳转到匹配的 catch 块。在本例中,我们使用 const std::exception& 引用来捕获任何类型的异常,并使用 ex.what() 获取异常对象的错误消息。

如果用户在输入分母时输入了零,将会触发异常,然后程序将跳转到 catch 块,并输出相应的错误消息。

这个示例展示了如何使用 throw 语句抛出异常,并在 try-catch 块中捕获和处理异常。通过适当地处理异常,我们可以改善程序的健壮性和容错性。

# 二、异常类

补充说明: 在 C++ 中,可以抛出不同类型的异常,包括标准库中提供的异常类,也可以自定义异常类来满足特定需求。通常,异常类应该从 std::exception 类派生,以便能够通过基类的引用或指针进行统一的异常捕获。

自定义异常类的示例代码如下:

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 <iostream>

class MyException : public std::exception {
public:
MyException(const std::string& message) : message_(message) {}

const char* what() const noexcept override {
return message_.c_str();
}

private:
std::string message_;
};

void processInput(int value) {
if (value < 0) {
throw MyException("Negative value exception");
}

std::cout << "Value: " << value << std::endl;
}

int main() {
try {
int value;
std::cout << "Enter a positive value: ";
std::cin >> value;

processInput(value);
} catch (const std::exception& ex) {
std::cout << "Exception caught: " << ex.what() << std::endl;
}

return 0;
}

在上述代码中,我们自定义了一个名为 MyException 的异常类,它从 std::exception 派生。我们在 processInput 函数中检查输入值,如果值为负数,将抛出 MyException 异常,并传递自定义的错误消息。

main 函数中,我们使用 try-catch 块来捕获可能抛出的异常。如果 processInput 函数抛出 MyException 异常,将跳转到匹配的 catch 块,并输出相应的错误消息。

# 三、异常的好处

C++ 的异常机制提供了一种用于处理异常情况的结构化方式,具有以下几个好处:

  1. 分离正常流程和异常处理逻辑:异常机制允许我们将正常的业务逻辑与异常处理逻辑分离开来,使代码更加清晰和易于维护。我们可以专注于编写正常的代码,而将异常处理部分留给专门的异常处理程序。
  2. 异常处理的集中性:通过异常机制,我们可以将异常处理逻辑集中到一个或几个地方,提高代码的可读性和可维护性。这样,在代码中的每个可能引发异常的地方,我们不需要重复编写相同的异常处理逻辑,而是通过捕获异常并在适当的位置进行处理。
  3. 提供更灵活的错误处理:使用异常机制,我们可以将错误信息传递到适当的异常处理程序中,而不是通过错误码或返回值来传递。这样可以提供更多的上下文信息,并且可以根据具体情况采取不同的处理措施。
  4. 避免混淆错误处理和业务逻辑:使用条件语句来处理错误可能导致代码变得冗长和复杂,因为我们需要在每个可能引发异常的地方进行条件检查。 这样会使代码逻辑变得混乱,难以理解和维护。而异常机制能够将错误处理与业务逻辑分离,提高代码的可读性和可维护性。
  5. 异常的传播性:异常机制提供了异常在调用栈中传播的能力,即在函数调用链的各个层级中,异常可以被捕获和处理。这样可以避免在每个函数中都进行异常处理,只需要在适当的地方进行捕获和处理即可。(可以向上传播)

需要注意的是,异常机制并不适合处理所有类型的错误。对于某些可预测的错误或边界条件,使用条件语句来进行处理可能更加合适。异常应该用于表示意外情况、运行时错误或无法处理的情况。

总结起来,C++ 的异常机制提供了一种结构化的、集中处理异常的方式,使得代码更清晰、更易于维护。它分离了正常流程和异常处理逻辑,并提供了更灵活的错误处理方式,避免了混淆错误处理和业务逻辑。然而,在使用异常机制时,需要合理使用,根据具体情况选择使用异常还是条件语句处理错误。

# 四、异常与条件语句的区别

举例说明:
当处理文件读取异常时,让我们来比较使用异常机制和条件语句的处理方式。假设我们需要读取一个文件的内容,并输出文件的内容到控制台。

使用异常机制的示例代码如下所示:

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
#include <iostream>
#include <fstream>

void readFile(const std::string& filename) {
std::ifstream file(filename);

if(!file) {
throw std::runtime_error("Failed to open file: " + filename);
}

std::string line;
while(std::getline(file, line)) {
std::cout << line << std::endl;
} file.close();
}

int main() {
std::string filename = "example.txt";

try {
readFile(filename);
} catch (const std::exception& e) {
std::cerr << "Exception occurred: " << e.what() << std::endl;
}

return 0;
}

在上述代码中, readFile 函数尝试打开文件并读取其内容。如果文件打开失败,将抛出 std::runtime_error 异常。在 main 函数中,我们使用 try-catch 块来捕获可能抛出的异常,并在异常发生时进行处理。异常处理程序将打印异常信息到标准错误流。

现在,我们来看看使用条件语句的处理方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <fstream>

void readFile(const std::string& filename) {
std::ifstream file(filename);

if(file) {
std::string line;
while(std::getline(file, line)) {
std::cout << line << std::endl;
} file.close();
} else {
std::cerr << "Failed to open file: " << filename << std::endl;
}
}

int main() {
std::string filename = "example.txt";
readFile(filename);
return 0;
}

在这个示例中, readFile 函数尝试打开文件并读取其内容。如果文件打开成功,则将文件内容输出到控制台;如果文件打开失败,则直接在函数内部使用条件语句输出错误信息到标准错误流。

通过对比这两个示例,我们可以看到使用异常机制的好处:

  1. 代码清晰度:使用异常机制可以更清晰地将正常流程和异常处理逻辑分离。异常处理程序专门处理异常情况,而正常流程部分保持简洁
  2. 代码复用:异常处理程序位于 main 函数中的 try-catch 块中,可以在整个程序中重复使用,而不需要在每个可能引发异常的函数中编写重复的错误处理代码。
  3. 异常传播性:如果在 readFile 函数中发生异常,它将在调用栈中传播,直到被 main 函数中的 try-catch 块捕获。这样,我们可以在适当的地方集中处理异常,而不需要在每个函数中进行条件检查。

总结来说,异常机制提供了一种结构化的、集中处理异常的方式,使得代码更加清晰和易于维护。它分离了正常流程和异常处理逻辑,并提供了更灵活的错误处理方式。通过示例代码的对比,我们可以更好地理解使用异常机制的好处。

需要注意的是,在实际编码中,我们应该根据具体情况选择合适的错误处理方式。有些情况下,使用条件语句进行错误处理可能更加简洁和合适。异常机制应该用于表示意外情况、运行时错误或无法处理的情况。

# 五、异常类型

当使用 C++ 异常机制时,我们可以使用各种不同的异常类型来表示不同的错误或异常情况。C++ 标准库提供了一些常见的异常类型,同时我们也可以自定义异常类型来满足特定的需求。

下面是一些常见的 C++ 异常类型及其使用场景:

  1. std::exceptionstd::exception 是 C++ 异常类的基类,用于表示通用的异常情况。它提供了一个 what() 成员函数,返回异常的描述信息。
  2. std::runtime_errorstd::runtime_error 表示在运行时发生的错误,例如无效的参数、资源不可用等。它是从 std::exception 派生而来的,通常用于表示逻辑错误或不可恢复的错误。
  3. std::logic_errorstd::logic_error 表示逻辑错误,例如在代码中违反了逻辑规则、使用了无效的状态等。它也是从 std::exception 派生而来的,通常用于表示可预测且可修复的错误。
  4. std::invalid_argumentstd::invalid_argument 表示传递给函数的参数无效。例如,当函数要求一个有效的参数值,并且传递了一个无效值时,可以抛出该异常。
  5. std::out_of_rangestd::out_of_range 表示索引或迭代器超出了有效范围。例如,在数组、容器或字符串中使用无效的索引时可以抛出该异常。
  6. std::bad_allocstd::bad_alloc 表示内存分配失败。当动态内存分配操作(如 newmalloc )无法满足请求时,可以抛出该异常。

除了上述标准库提供的异常类型,我们还可以自定义异常类型来满足特定的需求。自定义异常类型可以从 std::exception 或其他标准库提供的异常类型派生,以便更好地表示特定的错误或异常情况。

使用适当的异常类型可以增加代码的可读性和可维护性,并提供有关错误或异常的更多上下文信息。当捕获异常时,我们可以根据异常类型进行精确的处理或采取适当的措施,以便正确地处理错误情况。

# 六、异常处理

如果您在代码中抛出了异常却没有相应的异常处理(没有进行 catch 操作),那么异常将会沿着调用栈向上传播,直到遇到能够捕获并处理该异常的地方,或者在程序的顶层时导致程序终止。

具体来说,当一个异常被抛出时,程序会搜索当前的执行上下文(函数调用栈)以查找能够处理该异常的 catch 语句块。如果找不到匹配的 catch 块,异常就会沿着调用栈向上传播,继续搜索调用栈中的下一个执行上下文,直到找到合适的 catch 块或者达到程序的顶层。

如果异常一直没有被捕获,而且在程序的顶层也没有合适的 catch 块来处理该异常,那么程序就会终止运行,并输出一个未捕获异常的错误信息。这通常会包括异常类型、异常发生的位置和相关的调用栈信息,以帮助定位问题。

因此,在编写代码时,建议捕获可能抛出的异常,并进行相应的处理操作,以便更好地控制程序的行为和错误处理。这样可以提高程序的健壮性和可靠性,避免未处理的异常导致程序异常终止。

需要注意的是,在某些特殊情况下,比如在应用程序的顶层或线程的入口点,可以使用类似 try-catch 的结构来捕获并处理未处理的异常,以便进行适当的日志记录和资源清理。

总之,如果抛出的异常没有被捕获,它将沿着调用栈向上传播,直到找到合适的 catch 块或导致程序终止。因此,合理地捕获和处理异常是确保程序稳定性和可靠性的重要步骤。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\346\226\207\344\273\266\350\257\273\345\206\231/index.html" "b/2024/10/18/C-\346\226\207\344\273\266\350\257\273\345\206\231/index.html" index 37e8278..28fd365 100644 --- "a/2024/10/18/C-\346\226\207\344\273\266\350\257\273\345\206\231/index.html" +++ "b/2024/10/18/C-\346\226\207\344\273\266\350\257\273\345\206\231/index.html" @@ -1 +1 @@ -C++ 文件读写 - C++ - 编程 | R1ck's Playground = how

# 一、C++ 文件 IO 读写

参考教程:C++ 文件和流

在读写文件的时候,每读取或者写入一个字节,磁盘中的文件指针就会向后移动一个字节。可以通过控制指针的位置,以便在我们需要的位置进行读写文件:

tellg()  返回读取文件指针的当前位置
tellp()  返回写入文件指针的当前位置
seekg(指针偏移量)  将读取文件指针移到指定位置
seekg(指针偏移量,参照位置)  将读取文件指针移到指定位置
seekp(指针偏移量)  将写入文件指针移到指定位置
seekp(指针偏移量,参照位置)  将写入文件指针移到指定位置

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
36
37
#include <fstream>
#include <iostream>

using namespace std;

int main()
{
//write file
ofstream outFile;
outFile.open("myfile.txt", ios::out|ios::app); //追加方式打开
// outFile.open("myfile.txt", ios::out|ios::trunc); //截断方式打开

// if(!outFile) {
if(outFile.is_open()) {
cout << "Writing data to myfile.txt" << endl;
outFile.seekp(10); //写指针移位,可选参数 ios::cur、ios::beg、ios::end
for(int i = 0; i < 3; ++i) {
outFile << "data" << i << endl;
} outFile.close();
}

//read file
ifstream inFile;
inFile.open("myfile.txt", ios::in); //读模式打开,ios::in 为默认参数,可不加

if(inFile.is_open()) {
cout << "Reading data from myfile.txt : " << endl;
inFile.seekg(5); //读指针移位,可选参数 ios::cur ios::beg ios::end
while(!inFile.eof()) {
string readStr;
inFile >> readStr;
cout << readStr << endl;
} inFile.close();
}

return 0;
}

1
istream& read(char* s, streamsize n);

作用:Read block of data,从输入流中读取数据然后写入 buffer 中

Extracts n characters from the stream and stores them in the array pointed to by s.

This function simply copies a block of data, without checking its contents nor appending a null character at the end.

If the input sequence runs out of characters to extract (i.e., the end-of-file is reached) before n characters have been successfully read, the array pointed to by s contains all the characters read until that point, and both the eofbit and failbit flags are set for the stream.

具体例子:

1
2
3
4
5
ifstream is(file.c_str(), ios::binary);
msgpack::unpacker unpack_;
unpack_.reserve_buffer(n);
is.read(unpack_.buffer(), n);
unpack_.buffer_consumed(n);

# 二、相关函数

# 2.1 std::ifstream & std::ofstream

std::ifstreamstd::ofstream 是 C++ 标准库中的两个类,用于文件输入和文件输出操作。它们分别用于读取文件和写入文件。

  1. std::ifstream (文件输入流):
    • std::ifstream 是 C++ 标准库中的一个类,用于从文件中读取数据。
    • 它提供了打开文件、读取文件内容以及关闭文件的功能。
    • 可以用于读取文本文件和二进制文件的内容。
    • 通常与文件流操作符(如 >> )一起使用,以从文件中提取数据到程序中。
    • 通常用于读取文本文件中的文本行、数字、配置文件等信息。

以下是一个示例,展示了如何使用 std::ifstream 打开文件并读取其内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fstream>
#include <iostream>

int main() {
std::ifstream inputFile("example.txt");
if (inputFile.is_open()) {
std::string line;
while(std::getline(inputFile, line)) {
std::cout << line << std::endl;
} inputFile.close();
} else {
std::cout << "Unable to open file." << std::endl;
}
return 0;
}

  1. std::ofstream (文件输出流):
    • std::ofstream 是 C++ 标准库中的一个类,用于将数据写入文件。
    • 它提供了打开文件、将数据写入文件以及关闭文件的功能。
    • 可以用于创建文本文件和二进制文件,并将数据写入文件中。
    • 通常与文件流操作符(如 << )一起使用,以将数据从程序写入文件中。
    • 通常用于生成日志、配置文件、数据文件等。

以下是一个示例,展示了如何使用 std::ofstream 创建文件并将数据写入其中:

1
2
3
4
5
6
7
8
9
10
11
12
#include <fstream>

int main() {
std::ofstream outputFile("output.txt");
if (outputFile.is_open()) {
outputFile << "Hello, World!" << std::endl;
outputFile.close();
} else {
std::cout << "Unable to create or open file." << std::endl;
}
return 0;
}

这些类提供了对文件的高级文件输入和输出操作,使得文件处理变得相对容易。在使用它们时,确保在打开文件后检查文件是否成功打开以及在完成操作后关闭文件以确保资源的释放。

常用函数:

1
2
3
4
5
6
7
8
9
is_open()
open()、close()
good()、bad()
fail()
eof()
read()、write()
seekp()、tellp()
seekg()、tellg()
clear()

这些函数基本就都是对文件流进行一些状态判断、数据读写,都是对关联的某个文件进行操作。
这里需要辨析的是 seek 和 tell 函数,p 表示写入位置(put),g 表示读取位置(get),seek 用于写入,tell 用于读取
然后这个 clear 并不是清空文件内容,而是清除之前的流的错误状态标志,以允许继续文件操作
在 open 时 mode 通常为 std::ios::binary | std::ios::instd::ios::binary | std::ios::out

参考链接:
https://en.cppreference.com/w/cpp/io/basic_ifstream
https://en.cppreference.com/w/cpp/io/basic_ofstream

# 2.2 access

在 C++ 中, access 函数用于检查文件系统中文件或目录的访问权限和存在性。它的一般语法如下:

1
2
#include <unistd.h>
int access(const char *pathname, int mode);

  • pathname 是要检查的文件或目录的路径名。
  • mode 是一个整数,用于指定要检查的权限。常见的权限标志包括:
    • F_OK :用于检查文件或目录的存在性。
    • R_OK :用于检查读权限。
    • W_OK :用于检查写权限。
    • X_OK :用于检查执行权限。

access 函数将返回一个整数值,表示检查的结果。如果文件或目录存在且拥有指定的权限,它将返回 0;否则,返回 -1,并设置 errno 全局变量以指示错误类型。

这个函数通常用于在打开文件之前检查文件是否存在并具有所需的权限。例如,你可以使用 access 函数来检查文件是否可读,以避免在试图打开一个不可读的文件时出现错误。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <unistd.h>

int main() {
const char *filename = "example.txt";
if (access(filename, R_OK) == 0) {
std::cout << "File is readable." << std::endl;
// 打开文件并进行读取操作
} else {
std::cerr << "File is not readable or does not exist." << std::endl;
}
return 0;
}

这个示例首先使用 access 函数检查文件的可读性,然后根据结果执行相应的操作。

# 三、读写范例模板

# 3.1 文件读

1
2
3
4
5
6
7
8
9
10
11
12
std::ifstream myFile(filename, std::ios::binary);
if (!myFile.is_open()) {
std::cerr << "cant open file" << std::endl;
}

myFile.seekg(0, std::ios::end);
std::streampos fileSize = myFile.tellg();
myFile.seekg(0, std::ios::beg);

std::vector<char> file_vector(file_size);
myFile.read(file_vector.data(), fileSize);
myFile.close();

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
#include <iostream>
#include <fstream> // std::ifstream
#include <vector> // std::vector

int main() {
std::ifstream input_file("input.png", std::ios::binary);
if(!input_file.is_open()) {
std::cerr << "open input failed" << std::endl;
return 1;
}

input_file.seekg(0, std::ios::end);
std::streampos file_size = input_file.tellg();
input_file.seekg(0, std::ios::beg);

std::vector<char> file_vector(file_size);

input_file.read(file_vector.data(), file_size);
input_file.close();

std::ofstream output_file("output.png", std::ios::binary);
if(output_file.is_open()) {
output_file.write(file_vector.data(), file_size);
output_file.close();
std::cout << "save new output" << std::endl;
}

return 0;
}

# 3.2 文件写

通用模板:

1
2
3
4
5
6
7
8
std::ofstream myFile("filename", std::ios::binary);
if(myFile.is_open()) {
//...文件写操作...

myFile.close();
} else {
std::cout << "open file error" << std::endl;
}

在初始化构造 ofstream 对象时, std::ios::binary 为可选项,决定是否生成二进制文件还是普通文本文件。文件的写操作可以通过 << 运算符对 ofstream 对象进行写入,也可以通过 write 函数进行写入。

1
2
3
4
5
6
7
8
9
10
11
std::ofstream myFile("filename.txt");
if(myFile.is_open()) {
for(int i = 0; i < 640*480; i++) {
myFile << iiiqData[i] << ",";
} myFile << "\n";

myFile.close();
std::cout << "save file done" << std::endl;
} else {
std::cout << "create file fail" << std::endl;
}

如果是通过 write 函数进行数据的写入,通常会将数据指针 cast 成 char* 类型表示字节流,
然后 write(数据指针,数据长度)

1
2
3
4
5
6
7
std::ofstream myFile("iiData.bin", std::ios::binary);
if(myFile.is_open()) {
myFile.write(reinterpret_cast<char*>(&iiData[0]), iiData.size());
myFile.close();
} else {
std::cout << "open file error" << std::endl;
}

1
2
3
4
5
6
7
8
std::ofstream myFile("/home/pcai/Music/temp/raw.bin", std::ios::binary);
if (myFile.is_open()) {
myFile.write(reinterpret_cast<char*>(data), frame_length);
myFile.close();
std::cout << "Data has been written to raw.bin" << std::endl;
} else {
std::cerr << "Unable to open the output file." << std::endl;
}

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 文件读写 - C++ - 编程 | R1ck's Playground = how

# 一、C++ 文件 IO 读写

参考教程:C++ 文件和流

在读写文件的时候,每读取或者写入一个字节,磁盘中的文件指针就会向后移动一个字节。可以通过控制指针的位置,以便在我们需要的位置进行读写文件:

tellg()  返回读取文件指针的当前位置
tellp()  返回写入文件指针的当前位置
seekg(指针偏移量)  将读取文件指针移到指定位置
seekg(指针偏移量,参照位置)  将读取文件指针移到指定位置
seekp(指针偏移量)  将写入文件指针移到指定位置
seekp(指针偏移量,参照位置)  将写入文件指针移到指定位置

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
36
37
#include <fstream>
#include <iostream>

using namespace std;

int main()
{
//write file
ofstream outFile;
outFile.open("myfile.txt", ios::out|ios::app); //追加方式打开
// outFile.open("myfile.txt", ios::out|ios::trunc); //截断方式打开

// if(!outFile) {
if(outFile.is_open()) {
cout << "Writing data to myfile.txt" << endl;
outFile.seekp(10); //写指针移位,可选参数 ios::cur、ios::beg、ios::end
for(int i = 0; i < 3; ++i) {
outFile << "data" << i << endl;
} outFile.close();
}

//read file
ifstream inFile;
inFile.open("myfile.txt", ios::in); //读模式打开,ios::in 为默认参数,可不加

if(inFile.is_open()) {
cout << "Reading data from myfile.txt : " << endl;
inFile.seekg(5); //读指针移位,可选参数 ios::cur ios::beg ios::end
while(!inFile.eof()) {
string readStr;
inFile >> readStr;
cout << readStr << endl;
} inFile.close();
}

return 0;
}

1
istream& read(char* s, streamsize n);

作用:Read block of data,从输入流中读取数据然后写入 buffer 中

Extracts n characters from the stream and stores them in the array pointed to by s.

This function simply copies a block of data, without checking its contents nor appending a null character at the end.

If the input sequence runs out of characters to extract (i.e., the end-of-file is reached) before n characters have been successfully read, the array pointed to by s contains all the characters read until that point, and both the eofbit and failbit flags are set for the stream.

具体例子:

1
2
3
4
5
ifstream is(file.c_str(), ios::binary);
msgpack::unpacker unpack_;
unpack_.reserve_buffer(n);
is.read(unpack_.buffer(), n);
unpack_.buffer_consumed(n);

# 二、相关函数

# 2.1 std::ifstream & std::ofstream

std::ifstreamstd::ofstream 是 C++ 标准库中的两个类,用于文件输入和文件输出操作。它们分别用于读取文件和写入文件。

  1. std::ifstream (文件输入流):
    • std::ifstream 是 C++ 标准库中的一个类,用于从文件中读取数据。
    • 它提供了打开文件、读取文件内容以及关闭文件的功能。
    • 可以用于读取文本文件和二进制文件的内容。
    • 通常与文件流操作符(如 >> )一起使用,以从文件中提取数据到程序中。
    • 通常用于读取文本文件中的文本行、数字、配置文件等信息。

以下是一个示例,展示了如何使用 std::ifstream 打开文件并读取其内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fstream>
#include <iostream>

int main() {
std::ifstream inputFile("example.txt");
if (inputFile.is_open()) {
std::string line;
while(std::getline(inputFile, line)) {
std::cout << line << std::endl;
} inputFile.close();
} else {
std::cout << "Unable to open file." << std::endl;
}
return 0;
}

  1. std::ofstream (文件输出流):
    • std::ofstream 是 C++ 标准库中的一个类,用于将数据写入文件。
    • 它提供了打开文件、将数据写入文件以及关闭文件的功能。
    • 可以用于创建文本文件和二进制文件,并将数据写入文件中。
    • 通常与文件流操作符(如 << )一起使用,以将数据从程序写入文件中。
    • 通常用于生成日志、配置文件、数据文件等。

以下是一个示例,展示了如何使用 std::ofstream 创建文件并将数据写入其中:

1
2
3
4
5
6
7
8
9
10
11
12
#include <fstream>

int main() {
std::ofstream outputFile("output.txt");
if (outputFile.is_open()) {
outputFile << "Hello, World!" << std::endl;
outputFile.close();
} else {
std::cout << "Unable to create or open file." << std::endl;
}
return 0;
}

这些类提供了对文件的高级文件输入和输出操作,使得文件处理变得相对容易。在使用它们时,确保在打开文件后检查文件是否成功打开以及在完成操作后关闭文件以确保资源的释放。

常用函数:

1
2
3
4
5
6
7
8
9
is_open()
open()、close()
good()、bad()
fail()
eof()
read()、write()
seekp()、tellp()
seekg()、tellg()
clear()

这些函数基本就都是对文件流进行一些状态判断、数据读写,都是对关联的某个文件进行操作。
这里需要辨析的是 seek 和 tell 函数,p 表示写入位置(put),g 表示读取位置(get),seek 用于写入,tell 用于读取
然后这个 clear 并不是清空文件内容,而是清除之前的流的错误状态标志,以允许继续文件操作
在 open 时 mode 通常为 std::ios::binary | std::ios::instd::ios::binary | std::ios::out

参考链接:
https://en.cppreference.com/w/cpp/io/basic_ifstream
https://en.cppreference.com/w/cpp/io/basic_ofstream

# 2.2 access

在 C++ 中, access 函数用于检查文件系统中文件或目录的访问权限和存在性。它的一般语法如下:

1
2
#include <unistd.h>
int access(const char *pathname, int mode);

  • pathname 是要检查的文件或目录的路径名。
  • mode 是一个整数,用于指定要检查的权限。常见的权限标志包括:
    • F_OK :用于检查文件或目录的存在性。
    • R_OK :用于检查读权限。
    • W_OK :用于检查写权限。
    • X_OK :用于检查执行权限。

access 函数将返回一个整数值,表示检查的结果。如果文件或目录存在且拥有指定的权限,它将返回 0;否则,返回 -1,并设置 errno 全局变量以指示错误类型。

这个函数通常用于在打开文件之前检查文件是否存在并具有所需的权限。例如,你可以使用 access 函数来检查文件是否可读,以避免在试图打开一个不可读的文件时出现错误。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <unistd.h>

int main() {
const char *filename = "example.txt";
if (access(filename, R_OK) == 0) {
std::cout << "File is readable." << std::endl;
// 打开文件并进行读取操作
} else {
std::cerr << "File is not readable or does not exist." << std::endl;
}
return 0;
}

这个示例首先使用 access 函数检查文件的可读性,然后根据结果执行相应的操作。

# 三、读写范例模板

# 3.1 文件读

1
2
3
4
5
6
7
8
9
10
11
12
std::ifstream myFile(filename, std::ios::binary);
if (!myFile.is_open()) {
std::cerr << "cant open file" << std::endl;
}

myFile.seekg(0, std::ios::end);
std::streampos fileSize = myFile.tellg();
myFile.seekg(0, std::ios::beg);

std::vector<char> file_vector(file_size);
myFile.read(file_vector.data(), fileSize);
myFile.close();

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
#include <iostream>
#include <fstream> // std::ifstream
#include <vector> // std::vector

int main() {
std::ifstream input_file("input.png", std::ios::binary);
if(!input_file.is_open()) {
std::cerr << "open input failed" << std::endl;
return 1;
}

input_file.seekg(0, std::ios::end);
std::streampos file_size = input_file.tellg();
input_file.seekg(0, std::ios::beg);

std::vector<char> file_vector(file_size);

input_file.read(file_vector.data(), file_size);
input_file.close();

std::ofstream output_file("output.png", std::ios::binary);
if(output_file.is_open()) {
output_file.write(file_vector.data(), file_size);
output_file.close();
std::cout << "save new output" << std::endl;
}

return 0;
}

# 3.2 文件写

通用模板:

1
2
3
4
5
6
7
8
std::ofstream myFile("filename", std::ios::binary);
if(myFile.is_open()) {
//...文件写操作...

myFile.close();
} else {
std::cout << "open file error" << std::endl;
}

在初始化构造 ofstream 对象时, std::ios::binary 为可选项,决定是否生成二进制文件还是普通文本文件。文件的写操作可以通过 << 运算符对 ofstream 对象进行写入,也可以通过 write 函数进行写入。

1
2
3
4
5
6
7
8
9
10
11
std::ofstream myFile("filename.txt");
if(myFile.is_open()) {
for(int i = 0; i < 640*480; i++) {
myFile << iiiqData[i] << ",";
} myFile << "\n";

myFile.close();
std::cout << "save file done" << std::endl;
} else {
std::cout << "create file fail" << std::endl;
}

如果是通过 write 函数进行数据的写入,通常会将数据指针 cast 成 char* 类型表示字节流,
然后 write(数据指针,数据长度)

1
2
3
4
5
6
7
std::ofstream myFile("iiData.bin", std::ios::binary);
if(myFile.is_open()) {
myFile.write(reinterpret_cast<char*>(&iiData[0]), iiData.size());
myFile.close();
} else {
std::cout << "open file error" << std::endl;
}

1
2
3
4
5
6
7
8
std::ofstream myFile("/home/pcai/Music/temp/raw.bin", std::ios::binary);
if (myFile.is_open()) {
myFile.write(reinterpret_cast<char*>(data), frame_length);
myFile.close();
std::cout << "Data has been written to raw.bin" << std::endl;
} else {
std::cerr << "Unable to open the output file." << std::endl;
}

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\347\232\204\345\244\232\346\200\201/index.html" "b/2024/10/18/C-\347\232\204\345\244\232\346\200\201/index.html" index 23c8081..97604f6 100644 --- "a/2024/10/18/C-\347\232\204\345\244\232\346\200\201/index.html" +++ "b/2024/10/18/C-\347\232\204\345\244\232\346\200\201/index.html" @@ -1 +1 @@ -C++ 的多态 - C++ - 编程 | R1ck's Playground = how

# 一、概述

C++ 中的多态性(Polymorphism)是面向对象编程的一个关键概念,它允许对象以不同的方式呈现相同的接口。C++ 中的多态性可以分为静态多态性(静态多态)和动态多态性(动态多态),它们有不同的实现方式和特点:

# 1、静态多态性(静态多态)

静态多态性是在编译时(编译期间)解析的多态性,也称为编译时多态性。它是通过函数的重载运算符重载来实现的。
在静态多态性中,编译器在编译时根据函数参数的类型和数量来确定调用哪个函数或运算符。
例如,函数重载允许你定义多个具有相同名称但不同参数列表的函数,编译器会根据调用时的参数类型来选择正确的函数。

1
2
3
4
5
6
7
void print(int num) {
std::cout << "Printing an integer: " << num << std::endl;
}

void print(double num) {
std::cout << "Printing a double: " << num << std::endl;
}

静态多态性的特点是效率高,因为在编译时已经确定了函数的调用,不需要在运行时进行额外的查找和判断。

# 2、动态多态性(动态多态)

动态多态性是在运行时解析的多态性,也称为运行时多态性。它是通过继承虚函数来实现的。
在动态多态性中,子类对象可以以基类的指针或引用来调用虚函数,而实际执行的函数取决于对象的类型。这种机制允许在运行时选择正确的函数,使得程序更灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape" << std::endl;
}
};

class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle" << std::endl;
}
};

在动态多态性中,使用基类指针或引用来调用虚函数时,实际执行的函数取决于对象的类型。这种机制在面向对象编程中非常重要,因为它允许你编写通用的代码,以处理不同子类的对象。

# 3、动态多态示例

但我们常说的多态一般都是指的动态多态

C++ 的多态是面向对象编程的一个重要概念,通过多态性,可以实现基类指针或引用调用子类对象的方法,实现动态绑定,使程序可以根据实际对象的类型来选择正确的方法执行。

在 C++ 中,实现多态性主要依赖于两个机制:虚函数基类指针 / 引用

  1. 虚函数(Virtual Function):在基类中声明虚函数,允许子类对其进行重写。通过在函数声明前加上关键字 virtual ,可以将函数声明为虚函数。虚函数使得基类指针或引用在运行时可以根据实际对象的类型来调用相应的子类函数。基类的虚函数可以在子类中进行重写(override),即子类可以提供自己的实现。通过虚函数,可以实现动态绑定,使得程序在运行时根据对象的实际类型来确定调用的具体函数。
  2. 基类指针 / 引用:基类指针或引用可以指向子类对象,并且可以通过基类指针或引用调用虚函数。这样做的好处是,可以在编译时使用基类指针或引用,而在运行时确定实际调用的函数。当基类指针或引用指向子类对象时,如果该基类中存在虚函数并且被子类重写了,那么调用该函数时会根据对象的实际类型来选择具体的实现。

通过虚函数和基类指针 / 引用的组合使用,可以实现多态性。当程序通过基类指针或引用调用虚函数时,会根据实际对象的类型来选择具体的函数实现,从而实现了多态。这种多态性使得程序具有更高的灵活性和可扩展性,可以处理不同类型的对象,而不需要在编译时明确知道对象的具体类型。

下面是一个简单的示例代码,演示了 C++ 中的多态性:

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 <iostream>

class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape." << std::endl;
}
};

class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};

class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();

shape1->draw(); // 调用 Circle 的 draw 函数
shape2->draw(); // 调用 Rectangle 的 draw 函数

delete shape1;
delete shape2;

return 0;
}

在上述代码中,基类 Shape 声明了虚函数 draw() ,并在两个子类 CircleRectangle 中进行了重写。在 main 函数中,通过基类指针 shape1shape2 分别指向 CircleRectangle 对象,并通过调用虚函数 draw() 实现了多态性。在运行时,根据实际对象的类型来选择调用的具体函数实现。

提问:为什么要用基类指针指向子类对象呢,不能用子类指针去接住 new 出来的子类对象吗?

使用基类指针指向子类对象是为了实现多态性。多态性允许我们通过基类指针或引用来操作不同的子类对象,从而提供更大的灵活性和可扩展性。

当我们使用基类指针指向子类对象时,编译器会将对象的类型信息视为基类类型。这样做的好处是,我们可以在编译时使用基类指针,而在运行时根据实际对象的类型来选择调用相应的函数。这使得我们可以编写通用的代码,处理不同类型的对象,而不需要显式地指定对象的具体类型。

如果我们使用子类指针接收 new 出来的子类对象,那么我们只能访问子类中定义的成员和方法,无法访问基类中定义的成员和方法。 这将导致代码的局限性,不便于处理多个不同类型的对象。而通过使用基类指针,我们可以统一处理不同类型的对象,并且根据实际对象的类型来选择调用相应的函数,实现了多态性。

因此,使用基类指针指向子类对象是为了充分利用多态性的特性,实现代码的灵活性和可扩展性。

# 二、C++ 的动态绑定

当使用基类指针或引用指向派生类对象时,C++ 中的动态绑定(也称为运行时多态)允许根据实际对象的类型来确定要调用的函数,而不仅仅根据指针或引用的类型。

动态绑定通过虚函数虚函数表实现。
在基类中声明一个函数为虚函数时,可以使用 virtual 关键字,如下所示:

1
2
3
4
5
6
class Base {
public:
virtual void foo() {
// 实现基类的 foo 函数
}
};

在派生类中重写基类的虚函数时,可以使用 override 关键字,明确表示对基类虚函数的重写:

1
2
3
4
5
6
class Derived : public Base {
public:
void foo() override {
// 实现派生类的 foo 函数
}
};

当我们通过基类指针或引用调用虚函数时,编译器会根据指针或引用的类型来查找虚函数表,并根据实际对象的类型选择调用相应的函数。这样,在运行时,程序会动态地绑定(根据对象的类型)调用的函数,从而实现了多态性。

例如:

1
2
Base* ptr = new Derived();  // 使用基类指针指向派生类对象
ptr->foo(); // 根据实际对象的类型选择调用 Derived 类的 foo 函数

在上述示例中,虽然 ptr 是基类类型的指针,但由于其指向的是派生类对象,因此在运行时会调用派生类 Derivedfoo 函数。这就是动态绑定的作用。

动态绑定的优点是可以在运行时根据对象的实际类型来决定调用哪个函数,实现了多态性和灵活性。它使得基类指针或引用能够处理不同类型的对象,而不需要显式指定对象的具体类型。这为实现代码的可扩展性和维护性提供了便利。

# 虚函数指针、虚函数表

当一个类中包含虚函数时,编译器会为该类创建一个虚函数表(vtable),其中存储了各个虚函数的地址。虚函数表是一个数组,每个元素对应一个虚函数,它们按照声明顺序排列。

当类被实例化为对象时,该对象会存储一个指向虚函数表的指针,通常称为虚函数指针(vptr)。虚函数指针位于对象的内存布局的开头部分,它指向该对象所属类的虚函数表。

在运行时,当通过基类指针或引用调用虚函数时,实际发生的是通过虚函数指针找到对应的虚函数表,再根据虚函数的索引或偏移量来调用具体的函数。这就是动态绑定的过程。

具体步骤如下:

  1. 当基类对象被创建时,虚函数指针(vptr)被初始化为指向基类的虚函数表。
  2. 当派生类对象被创建时,首先会调用基类的构造函数,此时基类的虚函数指针被初始化为指向基类的虚函数表。然后再调用派生类的构造函数,此时派生类的虚函数指针被初始化为指向派生类的虚函数表。
  3. 当通过基类指针或引用调用虚函数时,首先会根据虚函数指针找到对应的虚函数表。
  4. 在虚函数表中,根据虚函数的索引或偏移量找到要调用的具体函数,并执行该函数。

通过虚函数表和虚函数指针的配合,实现了在运行时根据对象的实际类型动态绑定调用的函数,即动态多态(dynamic polymorphism)。这意味着我们可以通过基类指针或引用调用虚函数,而实际执行的是派生类中的重写函数。

动态绑定的机制使得程序能够根据对象的实际类型选择正确的函数实现,实现了多态性和灵活性,同时提高了代码的可扩展性和维护性。

每个类只有一个虚函数表,它是类的静态成员之一虚函数表存储在内存中的一个只读数据区,通常是在程序的可执行代码段中。

每个对象都包含一个虚函数指针(vptr),该指针指向所属类的虚函数表。虚函数指针位于对象的内存布局的开头部分,一般是对象的头部或前几个字节。当对象被创建时,虚函数指针被初始化为指向所属类的虚函数表。

虚函数表(vtable)存在于类的级别上,每个类只有一个虚函数表。虚函数指针(vptr)则存在于类的每个对象上。每个对象都包含一个虚函数指针,用于指向所属类的虚函数表。

虚函数指针在对象的内存布局中的开头部分,而虚函数表在内存的一个只读数据区,通常是在程序的可执行代码段中。通过虚函数指针,每个对象可以在运行时访问所属类的虚函数表,从而实现动态绑定和多态性的特性。

# 三、相关问题

# i、虚函数与构造函数

构造函数可以是虚函数吗?

在 C++ 中,构造函数不能被声明为虚函数。构造函数的调用是在对象创建时自动发生的,对象的动态类型在构造函数执行期间还没有确定,因此虚函数机制无法应用到构造函数上。因此,构造函数不能是虚函数。

于是乎,进一步地,要避免在构造函数中调用虚函数。如果在构造函数中调用了虚函数,并且在构造函数执行期间这些虚函数被调用,那么这些虚函数的调用将不会按照预期方式派发到子类的实现上,而是会直接调用基类的实现。这可能会导致一些问题和混乱。

原因在于,在对象构造过程中,对象的类型是由其静态类型确定的,而不是动态类型。在构造函数执行期间,对象的动态类型尚未被初始化,因此调用虚函数时只能根据对象的静态类型进行派发,即使对象最终可能会成为某个子类的实例。

由于这种行为可能导致不一致的结果,所以在构造函数中避免调用虚函数是一个良好的实践。如果需要在构造函数中执行某些特定的操作,最好将这些操作设计成非虚函数,并在子类中进行覆盖或重载,以确保正确的行为。

# i、纯虚函数的继承与覆写

提问:在 C++ 中,子类必须实现父类的纯虚函数吗?

在 C++ 中,如果一个类拥有纯虚函数(即在函数声明后面加上 = 0),那么任何派生类必须实现这个纯虚函数,否则派生类也会成为一个抽象类,无法被实例化。(从这个角度上来看,也不是必须,只不过派生类仍然是抽象类罢了,不排除故意设计如此。)

纯虚函数是一种在基类中声明但没有具体实现的虚函数。它的存在要求派生类必须提供自己的实现,使得每个派生类都能为这个函数赋予特定的行为。

以下是简单的例子:

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
#include <iostream>

class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
};

class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};

class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

int main() {
// Shape shape; // 错误!抽象类不能被实例化

Circle circle;
Rectangle rectangle;

circle.draw(); // 输出:Drawing a circle.
rectangle.draw(); // 输出:Drawing a rectangle.

return 0;
}

在上述示例中, Shape 类是一个抽象类,它包含一个纯虚函数 draw() ,因此不能被实例化。 CircleRectangle 类是 Shape 类的派生类,并且它们必须实现 draw() 函数,否则它们也会成为抽象类。在 main() 函数中,我们实例化了 CircleRectangle 对象,并调用了它们的 draw() 函数,分别输出了相应的结果。

# i、纯虚函数的类外实现

提问:C++ 中,类中的某个函数被设为纯虚函数了,它还可以在该类中定义该函数的实现吗?

在 C++ 中,当一个类中的函数被声明为纯虚函数后,意味着该函数没有实际的实现,类被认为是抽象类,无法被实例化。抽象类的主要目的是提供一组接口或协议,规定了派生类必须实现的纯虚函数。

纯虚函数在类的声明中使用 = 0 来表示。例如:

1
2
3
4
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数,没有实际实现
};

因为纯虚函数没有实现,所以抽象类不能创建对象,也不能直接在抽象类中定义该函数的实现。

== 但是!== 如果您希望在抽象类中为纯虚函数提供默认实现,可以在类外部定义纯虚函数的实现。这样做的目的是为了提供一个默认实现,以便在派生类没有实现该函数时,可以调用抽象类中的默认实现。

例如:

1
2
3
4
5
6
7
8
9
10
11
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数,没有实际实现
virtual ~Shape() {}
};

// 在类外部提供纯虚函数的默认实现
void Shape::draw() const {
// 默认实现代码
// ...
}

注意,在派生类中如果实现了纯虚函数,则该函数不再是纯虚函数,派生类可以被实例化。如果派生类没有实现纯虚函数,它仍然是抽象类,无法被实例化。

总结:纯虚函数没有实际的实现,抽象类中不能定义纯虚函数的实现。但可以在类外部为纯虚函数提供默认实现,以便在派生类中提供默认行为。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 的多态 - C++ - 编程 | R1ck's Playground = how

# 一、概述

C++ 中的多态性(Polymorphism)是面向对象编程的一个关键概念,它允许对象以不同的方式呈现相同的接口。C++ 中的多态性可以分为静态多态性(静态多态)和动态多态性(动态多态),它们有不同的实现方式和特点:

# 1、静态多态性(静态多态)

静态多态性是在编译时(编译期间)解析的多态性,也称为编译时多态性。它是通过函数的重载运算符重载来实现的。
在静态多态性中,编译器在编译时根据函数参数的类型和数量来确定调用哪个函数或运算符。
例如,函数重载允许你定义多个具有相同名称但不同参数列表的函数,编译器会根据调用时的参数类型来选择正确的函数。

1
2
3
4
5
6
7
void print(int num) {
std::cout << "Printing an integer: " << num << std::endl;
}

void print(double num) {
std::cout << "Printing a double: " << num << std::endl;
}

静态多态性的特点是效率高,因为在编译时已经确定了函数的调用,不需要在运行时进行额外的查找和判断。

# 2、动态多态性(动态多态)

动态多态性是在运行时解析的多态性,也称为运行时多态性。它是通过继承虚函数来实现的。
在动态多态性中,子类对象可以以基类的指针或引用来调用虚函数,而实际执行的函数取决于对象的类型。这种机制允许在运行时选择正确的函数,使得程序更灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape" << std::endl;
}
};

class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle" << std::endl;
}
};

在动态多态性中,使用基类指针或引用来调用虚函数时,实际执行的函数取决于对象的类型。这种机制在面向对象编程中非常重要,因为它允许你编写通用的代码,以处理不同子类的对象。

# 3、动态多态示例

但我们常说的多态一般都是指的动态多态

C++ 的多态是面向对象编程的一个重要概念,通过多态性,可以实现基类指针或引用调用子类对象的方法,实现动态绑定,使程序可以根据实际对象的类型来选择正确的方法执行。

在 C++ 中,实现多态性主要依赖于两个机制:虚函数基类指针 / 引用

  1. 虚函数(Virtual Function):在基类中声明虚函数,允许子类对其进行重写。通过在函数声明前加上关键字 virtual ,可以将函数声明为虚函数。虚函数使得基类指针或引用在运行时可以根据实际对象的类型来调用相应的子类函数。基类的虚函数可以在子类中进行重写(override),即子类可以提供自己的实现。通过虚函数,可以实现动态绑定,使得程序在运行时根据对象的实际类型来确定调用的具体函数。
  2. 基类指针 / 引用:基类指针或引用可以指向子类对象,并且可以通过基类指针或引用调用虚函数。这样做的好处是,可以在编译时使用基类指针或引用,而在运行时确定实际调用的函数。当基类指针或引用指向子类对象时,如果该基类中存在虚函数并且被子类重写了,那么调用该函数时会根据对象的实际类型来选择具体的实现。

通过虚函数和基类指针 / 引用的组合使用,可以实现多态性。当程序通过基类指针或引用调用虚函数时,会根据实际对象的类型来选择具体的函数实现,从而实现了多态。这种多态性使得程序具有更高的灵活性和可扩展性,可以处理不同类型的对象,而不需要在编译时明确知道对象的具体类型。

下面是一个简单的示例代码,演示了 C++ 中的多态性:

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 <iostream>

class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape." << std::endl;
}
};

class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};

class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();

shape1->draw(); // 调用 Circle 的 draw 函数
shape2->draw(); // 调用 Rectangle 的 draw 函数

delete shape1;
delete shape2;

return 0;
}

在上述代码中,基类 Shape 声明了虚函数 draw() ,并在两个子类 CircleRectangle 中进行了重写。在 main 函数中,通过基类指针 shape1shape2 分别指向 CircleRectangle 对象,并通过调用虚函数 draw() 实现了多态性。在运行时,根据实际对象的类型来选择调用的具体函数实现。

提问:为什么要用基类指针指向子类对象呢,不能用子类指针去接住 new 出来的子类对象吗?

使用基类指针指向子类对象是为了实现多态性。多态性允许我们通过基类指针或引用来操作不同的子类对象,从而提供更大的灵活性和可扩展性。

当我们使用基类指针指向子类对象时,编译器会将对象的类型信息视为基类类型。这样做的好处是,我们可以在编译时使用基类指针,而在运行时根据实际对象的类型来选择调用相应的函数。这使得我们可以编写通用的代码,处理不同类型的对象,而不需要显式地指定对象的具体类型。

如果我们使用子类指针接收 new 出来的子类对象,那么我们只能访问子类中定义的成员和方法,无法访问基类中定义的成员和方法。 这将导致代码的局限性,不便于处理多个不同类型的对象。而通过使用基类指针,我们可以统一处理不同类型的对象,并且根据实际对象的类型来选择调用相应的函数,实现了多态性。

因此,使用基类指针指向子类对象是为了充分利用多态性的特性,实现代码的灵活性和可扩展性。

# 二、C++ 的动态绑定

当使用基类指针或引用指向派生类对象时,C++ 中的动态绑定(也称为运行时多态)允许根据实际对象的类型来确定要调用的函数,而不仅仅根据指针或引用的类型。

动态绑定通过虚函数虚函数表实现。
在基类中声明一个函数为虚函数时,可以使用 virtual 关键字,如下所示:

1
2
3
4
5
6
class Base {
public:
virtual void foo() {
// 实现基类的 foo 函数
}
};

在派生类中重写基类的虚函数时,可以使用 override 关键字,明确表示对基类虚函数的重写:

1
2
3
4
5
6
class Derived : public Base {
public:
void foo() override {
// 实现派生类的 foo 函数
}
};

当我们通过基类指针或引用调用虚函数时,编译器会根据指针或引用的类型来查找虚函数表,并根据实际对象的类型选择调用相应的函数。这样,在运行时,程序会动态地绑定(根据对象的类型)调用的函数,从而实现了多态性。

例如:

1
2
Base* ptr = new Derived();  // 使用基类指针指向派生类对象
ptr->foo(); // 根据实际对象的类型选择调用 Derived 类的 foo 函数

在上述示例中,虽然 ptr 是基类类型的指针,但由于其指向的是派生类对象,因此在运行时会调用派生类 Derivedfoo 函数。这就是动态绑定的作用。

动态绑定的优点是可以在运行时根据对象的实际类型来决定调用哪个函数,实现了多态性和灵活性。它使得基类指针或引用能够处理不同类型的对象,而不需要显式指定对象的具体类型。这为实现代码的可扩展性和维护性提供了便利。

# 虚函数指针、虚函数表

当一个类中包含虚函数时,编译器会为该类创建一个虚函数表(vtable),其中存储了各个虚函数的地址。虚函数表是一个数组,每个元素对应一个虚函数,它们按照声明顺序排列。

当类被实例化为对象时,该对象会存储一个指向虚函数表的指针,通常称为虚函数指针(vptr)。虚函数指针位于对象的内存布局的开头部分,它指向该对象所属类的虚函数表。

在运行时,当通过基类指针或引用调用虚函数时,实际发生的是通过虚函数指针找到对应的虚函数表,再根据虚函数的索引或偏移量来调用具体的函数。这就是动态绑定的过程。

具体步骤如下:

  1. 当基类对象被创建时,虚函数指针(vptr)被初始化为指向基类的虚函数表。
  2. 当派生类对象被创建时,首先会调用基类的构造函数,此时基类的虚函数指针被初始化为指向基类的虚函数表。然后再调用派生类的构造函数,此时派生类的虚函数指针被初始化为指向派生类的虚函数表。
  3. 当通过基类指针或引用调用虚函数时,首先会根据虚函数指针找到对应的虚函数表。
  4. 在虚函数表中,根据虚函数的索引或偏移量找到要调用的具体函数,并执行该函数。

通过虚函数表和虚函数指针的配合,实现了在运行时根据对象的实际类型动态绑定调用的函数,即动态多态(dynamic polymorphism)。这意味着我们可以通过基类指针或引用调用虚函数,而实际执行的是派生类中的重写函数。

动态绑定的机制使得程序能够根据对象的实际类型选择正确的函数实现,实现了多态性和灵活性,同时提高了代码的可扩展性和维护性。

每个类只有一个虚函数表,它是类的静态成员之一虚函数表存储在内存中的一个只读数据区,通常是在程序的可执行代码段中。

每个对象都包含一个虚函数指针(vptr),该指针指向所属类的虚函数表。虚函数指针位于对象的内存布局的开头部分,一般是对象的头部或前几个字节。当对象被创建时,虚函数指针被初始化为指向所属类的虚函数表。

虚函数表(vtable)存在于类的级别上,每个类只有一个虚函数表。虚函数指针(vptr)则存在于类的每个对象上。每个对象都包含一个虚函数指针,用于指向所属类的虚函数表。

虚函数指针在对象的内存布局中的开头部分,而虚函数表在内存的一个只读数据区,通常是在程序的可执行代码段中。通过虚函数指针,每个对象可以在运行时访问所属类的虚函数表,从而实现动态绑定和多态性的特性。

# 三、相关问题

# i、虚函数与构造函数

构造函数可以是虚函数吗?

在 C++ 中,构造函数不能被声明为虚函数。构造函数的调用是在对象创建时自动发生的,对象的动态类型在构造函数执行期间还没有确定,因此虚函数机制无法应用到构造函数上。因此,构造函数不能是虚函数。

于是乎,进一步地,要避免在构造函数中调用虚函数。如果在构造函数中调用了虚函数,并且在构造函数执行期间这些虚函数被调用,那么这些虚函数的调用将不会按照预期方式派发到子类的实现上,而是会直接调用基类的实现。这可能会导致一些问题和混乱。

原因在于,在对象构造过程中,对象的类型是由其静态类型确定的,而不是动态类型。在构造函数执行期间,对象的动态类型尚未被初始化,因此调用虚函数时只能根据对象的静态类型进行派发,即使对象最终可能会成为某个子类的实例。

由于这种行为可能导致不一致的结果,所以在构造函数中避免调用虚函数是一个良好的实践。如果需要在构造函数中执行某些特定的操作,最好将这些操作设计成非虚函数,并在子类中进行覆盖或重载,以确保正确的行为。

# i、纯虚函数的继承与覆写

提问:在 C++ 中,子类必须实现父类的纯虚函数吗?

在 C++ 中,如果一个类拥有纯虚函数(即在函数声明后面加上 = 0),那么任何派生类必须实现这个纯虚函数,否则派生类也会成为一个抽象类,无法被实例化。(从这个角度上来看,也不是必须,只不过派生类仍然是抽象类罢了,不排除故意设计如此。)

纯虚函数是一种在基类中声明但没有具体实现的虚函数。它的存在要求派生类必须提供自己的实现,使得每个派生类都能为这个函数赋予特定的行为。

以下是简单的例子:

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
#include <iostream>

class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
};

class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};

class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

int main() {
// Shape shape; // 错误!抽象类不能被实例化

Circle circle;
Rectangle rectangle;

circle.draw(); // 输出:Drawing a circle.
rectangle.draw(); // 输出:Drawing a rectangle.

return 0;
}

在上述示例中, Shape 类是一个抽象类,它包含一个纯虚函数 draw() ,因此不能被实例化。 CircleRectangle 类是 Shape 类的派生类,并且它们必须实现 draw() 函数,否则它们也会成为抽象类。在 main() 函数中,我们实例化了 CircleRectangle 对象,并调用了它们的 draw() 函数,分别输出了相应的结果。

# i、纯虚函数的类外实现

提问:C++ 中,类中的某个函数被设为纯虚函数了,它还可以在该类中定义该函数的实现吗?

在 C++ 中,当一个类中的函数被声明为纯虚函数后,意味着该函数没有实际的实现,类被认为是抽象类,无法被实例化。抽象类的主要目的是提供一组接口或协议,规定了派生类必须实现的纯虚函数。

纯虚函数在类的声明中使用 = 0 来表示。例如:

1
2
3
4
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数,没有实际实现
};

因为纯虚函数没有实现,所以抽象类不能创建对象,也不能直接在抽象类中定义该函数的实现。

== 但是!== 如果您希望在抽象类中为纯虚函数提供默认实现,可以在类外部定义纯虚函数的实现。这样做的目的是为了提供一个默认实现,以便在派生类没有实现该函数时,可以调用抽象类中的默认实现。

例如:

1
2
3
4
5
6
7
8
9
10
11
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数,没有实际实现
virtual ~Shape() {}
};

// 在类外部提供纯虚函数的默认实现
void Shape::draw() const {
// 默认实现代码
// ...
}

注意,在派生类中如果实现了纯虚函数,则该函数不再是纯虚函数,派生类可以被实例化。如果派生类没有实现纯虚函数,它仍然是抽象类,无法被实例化。

总结:纯虚函数没有实际的实现,抽象类中不能定义纯虚函数的实现。但可以在类外部为纯虚函数提供默认实现,以便在派生类中提供默认行为。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\347\232\204\345\270\270\351\207\217/index.html" "b/2024/10/18/C-\347\232\204\345\270\270\351\207\217/index.html" index e0cfca3..bad0a51 100644 --- "a/2024/10/18/C-\347\232\204\345\270\270\351\207\217/index.html" +++ "b/2024/10/18/C-\347\232\204\345\270\270\351\207\217/index.html" @@ -1 +1 @@ -C++ 的常量 - C++ - 编程 | R1ck's Playground = how

# i、const 和 constexpr 的区别

提问:const 和 constexpr 有什么区别?

回答:
constconstexpr 都用于创建常量,但它们之间有一些重要的区别。

以下是它们的区别以及如何使用它们:

  1. const:
    • const 用于创建只读的常量变量,其值在运行时不能被修改。
    • const 变量必须在声明时初始化,并且可以在运行时计算其值。
    • const 变量可以用于任何数据类型,包括基本数据类型、自定义类、指针等。
    • const 变量的值在编译时或运行时(如果它是一个函数的局部变量)计算。
    • const 变量的值不一定需要在编译时确定,但一旦初始化后就不能更改。
    • const 变量的地址可以传递给函数,但函数不能修改该变量的值。

1
const int myConstVar = 42;

  • constexpr:
    • constexpr 用于创建编译时常量,其值在编译时已经确定,不能在运行时修改。
    • constexpr 变量必须在声明时使用常量表达式初始化,这意味着它的值必须在编译时就能确定,通常是通过常量或字面值来计算。
    • constexpr 变量通常用于基本数据类型,如整数、浮点数等,以及由它们构成的类型。
    • constexpr 变量的值在编译时确定,因此在运行时不存在任何计算开销。
    • constexpr 变量通常用于模板编程、数组大小、枚举、函数的参数等需要在编译时确定值的上下文中。
    • constexpr 变量的地址不能传递给函数,因为它们在运行时不存在。

1
constexpr int myConstexprVar = 42;

总结:

  • 使用 const 创建只读的运行时常量,适用于各种数据类型,值在运行时确定。
  • 使用 constexpr 创建编译时常量,适用于基本数据类型和由它们构成的类型,值在编译时确定。
  • constexpr 变量通常用于需要在编译时确定值的上下文,以提高性能和代码优化。
  • 选择使用哪种关键字取决于你的需求,如果可以在编译时确定值,则首选 constexpr

追问:为什么说 constexpr 变量在运行时不存在?

constexpr 变量在 C++ 中的一个核心概念是编译期常量。这意味着 constexpr 变量的值必须在编译时就能确定,并且这些变量的所有求值都发生在编译时,而不是运行时。因此,通常我们说 constexpr 变量在运行时不存在,这是因为它们的值和用途在编译时已经完全确定,编译器可能会将这些值内联到代码中,而不会真正分配内存存储它们

  1. 编译时常量
    当你声明一个 constexpr 变量时,编译器会在编译阶段计算它的值。与 const 变量不同, constexpr 的值在编译时已经是固定的,不需要等到运行时。

  2. 内存分配优化
    编译器可以对 constexpr 变量进行优化,可能不会在程序的内存中为其分配特定的地址。编译器会直接使用这些变量的值,并在必要的地方进行值替换内联,这意味着这些变量在实际运行的二进制程序中没有内存位置或指针,无法传递其地址。

  3. 编译时求值
    由于 constexpr 变量是在编译时求值的,因此它们的存在只是在编译器的上下文中,而不是在程序运行时的内存空间中。例如:

    1
    constexpr int x = 42;  // x 在编译时被计算为 42

    在这段代码中, x 在编译时已知是 42 ,编译器可以在整个程序中直接使用 42 ,而不是存储一个 x 变量的实际地址。

  4. 地址限制
    由于 constexpr 变量的这种编译时行为,它们的地址无法在运行时确定,因此你不能将 constexpr 变量的地址传递给一个要求运行时地址的函数。例如,以下代码是不允许的:

    1
    2
    constexpr int x = 42;
    int *p = &x; // 错误,不能获取 `constexpr` 变量的地址

    这是因为 constexpr 变量没有在运行时分配地址,它的值已经完全存在于编译时上下文中。

追问:宏定义也是编译时常量吗,它和 constexpr 相比有何优劣?

回答:
#define 宏定义是一种预处理指令,用于在代码中创建符号常量,但它与 constexpr 有一些重要的区别和优劣势:

#define 宏定义:

  • #define 定义的宏是文本替换在编译前进行,而不是创建真正的编译时常量。这意味着它们仅仅是在代码中的文本替换,不会被编译器视为独立的常量。
  • #define 宏没有类型,它们只是纯文本替换。这可能导致类型不一致或错误的使用。
  • 宏的值在编译前无法检查,因此可能会引入难以调试的错误。
  • 宏定义的作用域是整个文件,它们在定义点之后的整个文件中都有效。
  • 由于是文本替换,宏定义可以用于创建各种代码片段,包括函数、语句等。

constexpr 编译时常量:

  • constexpr 用于创建真正的编译时常量,它们在编译时已经确定。这意味着它们具有类型和值,可以进行类型检查和优化。
  • constexpr 变量必须具有明确的类型,并且其值必须在编译时可计算。
  • 编译器可以在编译时检查 constexpr 变量的值,因此可以捕获错误。
  • constexpr 变量的作用域是常规的变量作用域,可以在需要时限制其可见性。
  • constexpr 变量通常用于编写更安全、更可读和更可维护的代码,因为它们提供了类型检查和更好的错误检测。

总结:

  • #define 宏定义是文本替换,不是真正的编译时常量,可能引入类型不一致和难以调试的问题。
  • constexpr 是用于创建编译时常量的关键字,提供了类型检查、编译时错误检测和更好的代码优化。
  • 推荐在 C++ 中使用 constexpr 来创建常量,以提高代码的可维护性和可读性,并避免潜在的问题。

另外,宏定义的作用域限定在单个文件内,它们不会自动影响其他文件。如果你在一个文件中定义了一个宏,其他文件需要包含这个宏的定义才能访问它。通常,宏定义会放在头文件中,然后在多个文件中包含该头文件,以确保多个文件可以共享相同的宏定义。

# i、enum 枚举

C++ 中的枚举(enum)是一种用户自定义的数据类型,用于定义命名的整数常量。枚举提供了一种更可读、更可维护的方式来表示一组相关的命名常量。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

// 定义一个枚举类型
enum Color {
RED, // 默认值为 0
GREEN, // 默认值为 1
BLUE // 默认值为 2
};

int main() {
// 使用枚举类型
Color myColor = GREEN;
// 输出枚举值
std::cout << "Selected color: " << myColor << std::endl;
return 0;
}

注意,在 C++ 中,枚举类型( enum )的值是常量,一旦定义就不能被改变。枚举提供了一种创建命名整数常量集的方式,这些常量的值是固定的,不能在运行时更改

与宏定义 define 的区别:
枚举是具有明确定义类型的常量。枚举类型的值在编译时被检查,并且枚举类型是有限的。
枚举是由编译器处理的,可以提供更多的类型安全性和错误检查。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 的常量 - C++ - 编程 | R1ck's Playground = how

# i、const 和 constexpr 的区别

提问:const 和 constexpr 有什么区别?

回答:
constconstexpr 都用于创建常量,但它们之间有一些重要的区别。

以下是它们的区别以及如何使用它们:

  1. const:
    • const 用于创建只读的常量变量,其值在运行时不能被修改。
    • const 变量必须在声明时初始化,并且可以在运行时计算其值。
    • const 变量可以用于任何数据类型,包括基本数据类型、自定义类、指针等。
    • const 变量的值在编译时或运行时(如果它是一个函数的局部变量)计算。
    • const 变量的值不一定需要在编译时确定,但一旦初始化后就不能更改。
    • const 变量的地址可以传递给函数,但函数不能修改该变量的值。

1
const int myConstVar = 42;

  • constexpr:
    • constexpr 用于创建编译时常量,其值在编译时已经确定,不能在运行时修改。
    • constexpr 变量必须在声明时使用常量表达式初始化,这意味着它的值必须在编译时就能确定,通常是通过常量或字面值来计算。
    • constexpr 变量通常用于基本数据类型,如整数、浮点数等,以及由它们构成的类型。
    • constexpr 变量的值在编译时确定,因此在运行时不存在任何计算开销。
    • constexpr 变量通常用于模板编程、数组大小、枚举、函数的参数等需要在编译时确定值的上下文中。
    • constexpr 变量的地址不能传递给函数,因为它们在运行时不存在。

1
constexpr int myConstexprVar = 42;

总结:

  • 使用 const 创建只读的运行时常量,适用于各种数据类型,值在运行时确定。
  • 使用 constexpr 创建编译时常量,适用于基本数据类型和由它们构成的类型,值在编译时确定。
  • constexpr 变量通常用于需要在编译时确定值的上下文,以提高性能和代码优化。
  • 选择使用哪种关键字取决于你的需求,如果可以在编译时确定值,则首选 constexpr

追问:为什么说 constexpr 变量在运行时不存在?

constexpr 变量在 C++ 中的一个核心概念是编译期常量。这意味着 constexpr 变量的值必须在编译时就能确定,并且这些变量的所有求值都发生在编译时,而不是运行时。因此,通常我们说 constexpr 变量在运行时不存在,这是因为它们的值和用途在编译时已经完全确定,编译器可能会将这些值内联到代码中,而不会真正分配内存存储它们

  1. 编译时常量
    当你声明一个 constexpr 变量时,编译器会在编译阶段计算它的值。与 const 变量不同, constexpr 的值在编译时已经是固定的,不需要等到运行时。

  2. 内存分配优化
    编译器可以对 constexpr 变量进行优化,可能不会在程序的内存中为其分配特定的地址。编译器会直接使用这些变量的值,并在必要的地方进行值替换内联,这意味着这些变量在实际运行的二进制程序中没有内存位置或指针,无法传递其地址。

  3. 编译时求值
    由于 constexpr 变量是在编译时求值的,因此它们的存在只是在编译器的上下文中,而不是在程序运行时的内存空间中。例如:

    1
    constexpr int x = 42;  // x 在编译时被计算为 42

    在这段代码中, x 在编译时已知是 42 ,编译器可以在整个程序中直接使用 42 ,而不是存储一个 x 变量的实际地址。

  4. 地址限制
    由于 constexpr 变量的这种编译时行为,它们的地址无法在运行时确定,因此你不能将 constexpr 变量的地址传递给一个要求运行时地址的函数。例如,以下代码是不允许的:

    1
    2
    constexpr int x = 42;
    int *p = &x; // 错误,不能获取 `constexpr` 变量的地址

    这是因为 constexpr 变量没有在运行时分配地址,它的值已经完全存在于编译时上下文中。

追问:宏定义也是编译时常量吗,它和 constexpr 相比有何优劣?

回答:
#define 宏定义是一种预处理指令,用于在代码中创建符号常量,但它与 constexpr 有一些重要的区别和优劣势:

#define 宏定义:

  • #define 定义的宏是文本替换在编译前进行,而不是创建真正的编译时常量。这意味着它们仅仅是在代码中的文本替换,不会被编译器视为独立的常量。
  • #define 宏没有类型,它们只是纯文本替换。这可能导致类型不一致或错误的使用。
  • 宏的值在编译前无法检查,因此可能会引入难以调试的错误。
  • 宏定义的作用域是整个文件,它们在定义点之后的整个文件中都有效。
  • 由于是文本替换,宏定义可以用于创建各种代码片段,包括函数、语句等。

constexpr 编译时常量:

  • constexpr 用于创建真正的编译时常量,它们在编译时已经确定。这意味着它们具有类型和值,可以进行类型检查和优化。
  • constexpr 变量必须具有明确的类型,并且其值必须在编译时可计算。
  • 编译器可以在编译时检查 constexpr 变量的值,因此可以捕获错误。
  • constexpr 变量的作用域是常规的变量作用域,可以在需要时限制其可见性。
  • constexpr 变量通常用于编写更安全、更可读和更可维护的代码,因为它们提供了类型检查和更好的错误检测。

总结:

  • #define 宏定义是文本替换,不是真正的编译时常量,可能引入类型不一致和难以调试的问题。
  • constexpr 是用于创建编译时常量的关键字,提供了类型检查、编译时错误检测和更好的代码优化。
  • 推荐在 C++ 中使用 constexpr 来创建常量,以提高代码的可维护性和可读性,并避免潜在的问题。

另外,宏定义的作用域限定在单个文件内,它们不会自动影响其他文件。如果你在一个文件中定义了一个宏,其他文件需要包含这个宏的定义才能访问它。通常,宏定义会放在头文件中,然后在多个文件中包含该头文件,以确保多个文件可以共享相同的宏定义。

# i、enum 枚举

C++ 中的枚举(enum)是一种用户自定义的数据类型,用于定义命名的整数常量。枚举提供了一种更可读、更可维护的方式来表示一组相关的命名常量。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

// 定义一个枚举类型
enum Color {
RED, // 默认值为 0
GREEN, // 默认值为 1
BLUE // 默认值为 2
};

int main() {
// 使用枚举类型
Color myColor = GREEN;
// 输出枚举值
std::cout << "Selected color: " << myColor << std::endl;
return 0;
}

注意,在 C++ 中,枚举类型( enum )的值是常量,一旦定义就不能被改变。枚举提供了一种创建命名整数常量集的方式,这些常量的值是固定的,不能在运行时更改

与宏定义 define 的区别:
枚举是具有明确定义类型的常量。枚举类型的值在编译时被检查,并且枚举类型是有限的。
枚举是由编译器处理的,可以提供更多的类型安全性和错误检查。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\347\232\204\346\265\201/index.html" "b/2024/10/18/C-\347\232\204\346\265\201/index.html" index 7866904..dee2a79 100644 --- "a/2024/10/18/C-\347\232\204\346\265\201/index.html" +++ "b/2024/10/18/C-\347\232\204\346\265\201/index.html" @@ -1 +1 @@ -C++ 的流 - C++ - 编程 | R1ck's Playground = how

# 一、概述

C++ 中的流(streams)是用于处理输入和输出的抽象概念,主要用于与设备(如键盘、屏幕、文件等)进行数据交互。流在 C++ 中主要由以下几个方面组成:

# 1. 流的类型

C++ 中有两种主要的流类型:

  • 输入流(Input Stream):用于从输入设备读取数据。最常用的输入流是 std::cin ,它用于从标准输入(通常是键盘)读取数据。

  • 输出流(Output Stream):用于将数据输出到输出设备。最常用的输出流是 std::cout ,它用于将数据输出到标准输出(通常是屏幕)。

# 2. 流的类

在 C++ 中,流是通过类来实现的,主要有以下几个类:

  • std::istream :用于输入流的基类,提供了基本的输入操作。
  • std::ostream :用于输出流的基类,提供了基本的输出操作。
  • std::fstream :用于文件的输入输出操作,既可以读取文件也可以写入文件。

# 3. 基本操作

  • 输入操作
    使用 std::cin 读取数据可以用 >> 操作符。例如:

    1
    2
    3
    int x;
    std::cout << "请输入一个整数:";
    std::cin >> x;

  • 输出操作
    使用 std::cout 输出数据可以用 << 操作符。例如:

    1
    std::cout << "你输入的整数是:" << x << std::endl;

# 4. 文件流

对于文件的读写,使用 std::ifstreamstd::ofstream 类:

  • 读取文件

    1
    2
    3
    4
    5
    6
    7
    std::ifstream infile("data.txt");
    if(infile.is_open()) {
    int value;
    while(infile >> value) {
    std::cout << value << std::endl;
    } infile.close();
    }

  • 写入文件

    1
    2
    3
    4
    5
    std::ofstream outfile("output.txt");
    if(outfile.is_open()) {
    outfile << "Hello, World!" << std::endl;
    outfile.close();
    }

# 5. 流的格式化

C++ 的流可以进行格式化操作,例如控制输出的精度、宽度等:

  • 设置精度

    1
    2
    std::cout.precision(2); // 设置小数精度为2
    std::cout << std::fixed << 3.14159 << std::endl; // 输出为3.14

  • 设置宽度

    1
    std::cout << std::setw(10) << std::setfill('0') << x << std::endl; // 设置输出宽度为10,使用0填充

更多细节,见下文:[[C++ 的流 #i、Input/output manipulators | 输入输出操纵符]]

# 6. 错误处理

流还提供了错误处理的功能,可以通过检查流的状态来确定操作是否成功:

1
2
3
if(std::cin.fail()) {
std::cout << "输入错误!" << std::endl;
}

# 7. 流的优先级

C++ 中的流操作优先级比较高,可以方便地进行链式操作。例如:

1
2
3
std::cout << "请输入一个数字:" << std::endl;
std::cin >> x;
std::cout << "你输入的数字是:" << x << std::endl;

在 C++ 中,流的优先级指的是可以将多个流操作(输入或输出)串联在一起进行的能力。这种方式使得代码更加简洁和易读。

# 二、字符串流

# 1. 什么是 std::stringstream

std::stringstream 是 C++ 标准库中的一个类,位于 <sstream> 头文件中。它允许程序员在内存中创建一个字符串流,支持输入和输出操作。简单来说, std::stringstream 结合了 std::istringstreamstd::ostringstream 的功能,既可以从字符串中读取数据,也可以向字符串中写入数据。

# 2. 基本特性

  • 输入输出:可以使用 >><< 操作符进行输入和输出操作。
  • 字符串存储:可以在内存中动态创建和操作字符串。
  • 格式化能力:可以像其他流一样对数据进行格式化。

# 3. 主要方法

  • 构造函数

    1
    2
    std::stringstream ss; // 默认构造函数
    std::stringstream ss("初始字符串"); // 使用字符串初始化

  • 写入数据
    使用 << 操作符将数据写入流:

    1
    ss << "Hello, " << 42 << "!";

  • 读取数据
    使用 >> 操作符从流中读取数据:

    1
    2
    3
    std::string str;
    int num;
    ss >> str >> num; // 根据空格分隔读取

  • 获取字符串
    使用 .str() 方法获取流中的完整字符串:

    1
    std::string result = ss.str();

  • 清空流
    可以使用 .clear() 方法清空流的状态,并用 .str("") 清空内容:

    1
    2
    ss.str(""); // 清空字符串内容
    ss.clear(); // 清空流状态

注意! 当我们要清空 stringstream 时,不应该调用  clear() ,而是调用  str("")

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <sstream>
using namespace std;

int main()
{
stringstream ss;
ss.write("hello world", 11);
cout << ss.str() << endl;
ss.str("");
cout << ss.str() << endl;
return 0;
}

clear 只是清空流的状态,并不是清空其内容。

# 4. 应用场景

  • 格式化输出:当需要将多个数据格式化为一个字符串时, std::stringstream 非常有用。
  • 数据解析:从格式化的字符串中提取数据,特别适合处理 CSV、JSON 等格式。
  • 临时存储:在不想直接创建临时字符串的情况下,可以使用 std::stringstream 进行中间处理。

# 5. 性能

由于 std::stringstream 在内存中操作字符串,性能通常比频繁的字符串拼接更高效,尤其是在处理大量数据时。

# 三、其他

# i、Input/output manipulators

在 C++ 中,输入 / 输出操纵符(Input/Output Manipulators)是用于格式化输入和输出的特殊函数。它们可以被应用于流操作,以改变输出的格式、对齐方式、小数位数等。通过使用这些操纵符,你可以更精确地控制输出的外观,使得代码更易于阅读和维护。

以下是一些常用的输入 / 输出操纵符:

std::setw(int n)
设置字段宽度。
用于指定下一个输出项的字段宽度为 n 个字符,如果输出的项长度不足 n ,则会使用空格填充。

1
2
3
4
5
6
7
8
#include <iostream>
#include <iomanip>

int main() {
int number = 42;
std::cout << std::setw(10) << number << std::endl; // 输出项宽度为 10
return 0;
}

std::setfill(char c)
设置填充字符。
用于指定填充字符,该字符会被用来填充字段宽度之外的空白位置。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <iomanip>

int main() {
int number = 42;
std::cout << std::setfill('*') << std::setw(10) << number << std::endl; // 使用 '*' 填充
return 0;
}

# $ ./test.exe
# ********42

这个函数可用于 0 的填充,如 16 进制的前置零:0x0A、0x03

std::setprecision(int n)
设置小数位数。
用于控制浮点数的输出精度,指定小数点后的位数为 n

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <iomanip>

int main() {
double value = 3.14159;
std::cout << std::setprecision(3) << value << std::endl; // 输出三位小数
return 0;
}

# $ ./test.exe
# 3.14

std::fixedstd::scientific
设置浮点数输出格式为固定小数点和科学计数法。
可以与 std::setprecision 一起使用,后者用于控制精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <iomanip>

int main() {
double value = 12345.6789;
std::cout << std::fixed << std::setprecision(2) << value << std::endl; // 固定小数点输出
std::cout << std::scientific << std::setprecision(2) << value << std::endl; // 科学计数法输出
return 0;
}


# $ ./test.exe
# 12345.68
# 1.23e+04

std::hexstd::octstd::dec
用于设置整数的输出进制,分别为十六进制、八进制和十进制。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <iomanip>

int main() {
int number = 255;
std::cout << std::hex << number << std::endl; // 十六进制输出
std::cout << std::oct << number << std::endl; // 八进制输出
std::cout << std::dec << number << std::endl; // 十进制输出
return 0;
}

另外,还有一些实用的操纵符:

1
2
3
4
5
6
7
8
9
# 将布尔值用 truefalse 打印
std::boolalpha
std::noboolalpha
# 是否跳过前导空格:仅仅作用于输入流,如 std::cin
std::skipws
std::noskipws
# 是否大写:仅仅作用于浮点数和十六进制整数输出中使用大写字符。
std::uppercase
std::nouppercase

# i、cin.ignore()

ignore()  是 istream 类的成员函数,它的原型是:

1
istream & ignore(int n = 1, int delim = EOF);

此函数的作用是跳过输入流中的 n 个字符,或跳过字符 delim 及其之前的所有字符 —— 哪个条件先满足就按哪个执行。两个参数都有默认值,因此  cin.ignore()  就等效于  cin.ignore(1, EOF) , 即跳过一个字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int main()
{
string str;
cout << "Input: ";
//test input: helloworldnicetomeetyou
cin.ignore(5);
cin.ignore(3, 'o');
cin.ignore(8, 'o');
//多个 ignore 执行,则效果依次叠加
cin >> str;
cout << "Output: " << str << endl;

return 0;
}

通过该函数,能够过滤掉一些冗余信息,便于提取我们想要的信息。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 的流 - C++ - 编程 | R1ck's Playground = how

# 一、概述

C++ 中的流(streams)是用于处理输入和输出的抽象概念,主要用于与设备(如键盘、屏幕、文件等)进行数据交互。流在 C++ 中主要由以下几个方面组成:

# 1. 流的类型

C++ 中有两种主要的流类型:

  • 输入流(Input Stream):用于从输入设备读取数据。最常用的输入流是 std::cin ,它用于从标准输入(通常是键盘)读取数据。

  • 输出流(Output Stream):用于将数据输出到输出设备。最常用的输出流是 std::cout ,它用于将数据输出到标准输出(通常是屏幕)。

# 2. 流的类

在 C++ 中,流是通过类来实现的,主要有以下几个类:

  • std::istream :用于输入流的基类,提供了基本的输入操作。
  • std::ostream :用于输出流的基类,提供了基本的输出操作。
  • std::fstream :用于文件的输入输出操作,既可以读取文件也可以写入文件。

# 3. 基本操作

  • 输入操作
    使用 std::cin 读取数据可以用 >> 操作符。例如:

    1
    2
    3
    int x;
    std::cout << "请输入一个整数:";
    std::cin >> x;

  • 输出操作
    使用 std::cout 输出数据可以用 << 操作符。例如:

    1
    std::cout << "你输入的整数是:" << x << std::endl;

# 4. 文件流

对于文件的读写,使用 std::ifstreamstd::ofstream 类:

  • 读取文件

    1
    2
    3
    4
    5
    6
    7
    std::ifstream infile("data.txt");
    if(infile.is_open()) {
    int value;
    while(infile >> value) {
    std::cout << value << std::endl;
    } infile.close();
    }

  • 写入文件

    1
    2
    3
    4
    5
    std::ofstream outfile("output.txt");
    if(outfile.is_open()) {
    outfile << "Hello, World!" << std::endl;
    outfile.close();
    }

# 5. 流的格式化

C++ 的流可以进行格式化操作,例如控制输出的精度、宽度等:

  • 设置精度

    1
    2
    std::cout.precision(2); // 设置小数精度为2
    std::cout << std::fixed << 3.14159 << std::endl; // 输出为3.14

  • 设置宽度

    1
    std::cout << std::setw(10) << std::setfill('0') << x << std::endl; // 设置输出宽度为10,使用0填充

更多细节,见下文:[[C++ 的流 #i、Input/output manipulators | 输入输出操纵符]]

# 6. 错误处理

流还提供了错误处理的功能,可以通过检查流的状态来确定操作是否成功:

1
2
3
if(std::cin.fail()) {
std::cout << "输入错误!" << std::endl;
}

# 7. 流的优先级

C++ 中的流操作优先级比较高,可以方便地进行链式操作。例如:

1
2
3
std::cout << "请输入一个数字:" << std::endl;
std::cin >> x;
std::cout << "你输入的数字是:" << x << std::endl;

在 C++ 中,流的优先级指的是可以将多个流操作(输入或输出)串联在一起进行的能力。这种方式使得代码更加简洁和易读。

# 二、字符串流

# 1. 什么是 std::stringstream

std::stringstream 是 C++ 标准库中的一个类,位于 <sstream> 头文件中。它允许程序员在内存中创建一个字符串流,支持输入和输出操作。简单来说, std::stringstream 结合了 std::istringstreamstd::ostringstream 的功能,既可以从字符串中读取数据,也可以向字符串中写入数据。

# 2. 基本特性

  • 输入输出:可以使用 >><< 操作符进行输入和输出操作。
  • 字符串存储:可以在内存中动态创建和操作字符串。
  • 格式化能力:可以像其他流一样对数据进行格式化。

# 3. 主要方法

  • 构造函数

    1
    2
    std::stringstream ss; // 默认构造函数
    std::stringstream ss("初始字符串"); // 使用字符串初始化

  • 写入数据
    使用 << 操作符将数据写入流:

    1
    ss << "Hello, " << 42 << "!";

  • 读取数据
    使用 >> 操作符从流中读取数据:

    1
    2
    3
    std::string str;
    int num;
    ss >> str >> num; // 根据空格分隔读取

  • 获取字符串
    使用 .str() 方法获取流中的完整字符串:

    1
    std::string result = ss.str();

  • 清空流
    可以使用 .clear() 方法清空流的状态,并用 .str("") 清空内容:

    1
    2
    ss.str(""); // 清空字符串内容
    ss.clear(); // 清空流状态

注意! 当我们要清空 stringstream 时,不应该调用  clear() ,而是调用  str("")

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <sstream>
using namespace std;

int main()
{
stringstream ss;
ss.write("hello world", 11);
cout << ss.str() << endl;
ss.str("");
cout << ss.str() << endl;
return 0;
}

clear 只是清空流的状态,并不是清空其内容。

# 4. 应用场景

  • 格式化输出:当需要将多个数据格式化为一个字符串时, std::stringstream 非常有用。
  • 数据解析:从格式化的字符串中提取数据,特别适合处理 CSV、JSON 等格式。
  • 临时存储:在不想直接创建临时字符串的情况下,可以使用 std::stringstream 进行中间处理。

# 5. 性能

由于 std::stringstream 在内存中操作字符串,性能通常比频繁的字符串拼接更高效,尤其是在处理大量数据时。

# 三、其他

# i、Input/output manipulators

在 C++ 中,输入 / 输出操纵符(Input/Output Manipulators)是用于格式化输入和输出的特殊函数。它们可以被应用于流操作,以改变输出的格式、对齐方式、小数位数等。通过使用这些操纵符,你可以更精确地控制输出的外观,使得代码更易于阅读和维护。

以下是一些常用的输入 / 输出操纵符:

std::setw(int n)
设置字段宽度。
用于指定下一个输出项的字段宽度为 n 个字符,如果输出的项长度不足 n ,则会使用空格填充。

1
2
3
4
5
6
7
8
#include <iostream>
#include <iomanip>

int main() {
int number = 42;
std::cout << std::setw(10) << number << std::endl; // 输出项宽度为 10
return 0;
}

std::setfill(char c)
设置填充字符。
用于指定填充字符,该字符会被用来填充字段宽度之外的空白位置。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <iomanip>

int main() {
int number = 42;
std::cout << std::setfill('*') << std::setw(10) << number << std::endl; // 使用 '*' 填充
return 0;
}

# $ ./test.exe
# ********42

这个函数可用于 0 的填充,如 16 进制的前置零:0x0A、0x03

std::setprecision(int n)
设置小数位数。
用于控制浮点数的输出精度,指定小数点后的位数为 n

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <iomanip>

int main() {
double value = 3.14159;
std::cout << std::setprecision(3) << value << std::endl; // 输出三位小数
return 0;
}

# $ ./test.exe
# 3.14

std::fixedstd::scientific
设置浮点数输出格式为固定小数点和科学计数法。
可以与 std::setprecision 一起使用,后者用于控制精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <iomanip>

int main() {
double value = 12345.6789;
std::cout << std::fixed << std::setprecision(2) << value << std::endl; // 固定小数点输出
std::cout << std::scientific << std::setprecision(2) << value << std::endl; // 科学计数法输出
return 0;
}


# $ ./test.exe
# 12345.68
# 1.23e+04

std::hexstd::octstd::dec
用于设置整数的输出进制,分别为十六进制、八进制和十进制。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <iomanip>

int main() {
int number = 255;
std::cout << std::hex << number << std::endl; // 十六进制输出
std::cout << std::oct << number << std::endl; // 八进制输出
std::cout << std::dec << number << std::endl; // 十进制输出
return 0;
}

另外,还有一些实用的操纵符:

1
2
3
4
5
6
7
8
9
# 将布尔值用 truefalse 打印
std::boolalpha
std::noboolalpha
# 是否跳过前导空格:仅仅作用于输入流,如 std::cin
std::skipws
std::noskipws
# 是否大写:仅仅作用于浮点数和十六进制整数输出中使用大写字符。
std::uppercase
std::nouppercase

# i、cin.ignore()

ignore()  是 istream 类的成员函数,它的原型是:

1
istream & ignore(int n = 1, int delim = EOF);

此函数的作用是跳过输入流中的 n 个字符,或跳过字符 delim 及其之前的所有字符 —— 哪个条件先满足就按哪个执行。两个参数都有默认值,因此  cin.ignore()  就等效于  cin.ignore(1, EOF) , 即跳过一个字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int main()
{
string str;
cout << "Input: ";
//test input: helloworldnicetomeetyou
cin.ignore(5);
cin.ignore(3, 'o');
cin.ignore(8, 'o');
//多个 ignore 执行,则效果依次叠加
cin >> str;
cout << "Output: " << str << endl;

return 0;
}

通过该函数,能够过滤掉一些冗余信息,便于提取我们想要的信息。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\347\232\204\347\273\247\346\211\277/index.html" "b/2024/10/18/C-\347\232\204\347\273\247\346\211\277/index.html" index 5b5735e..3909011 100644 --- "a/2024/10/18/C-\347\232\204\347\273\247\346\211\277/index.html" +++ "b/2024/10/18/C-\347\232\204\347\273\247\346\211\277/index.html" @@ -1 +1 @@ -C++ 的继承 - C++ - 编程 | R1ck's Playground = how

# i、基本概念

# 继承关系中的构造函数

在 C++ 的继承关系中,派生类的构造函数必须在其初始化列表中调用基类的构造函数来构造基类部分

在派生类的构造函数中,构造基类部分的步骤是隐式的,但必须确保基类的构造函数被正确调用,以初始化基类的成员。这是因为派生类不仅包含自己的成员,还继承了基类的成员,这些成员需要在构造过程中得到适当的初始化。

构造函数调用基类构造函数的方式是在派生类构造函数的初始化列表中使用基类的构造函数,并提供适当的参数。

以下是一个示例:

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
class Base {
public:
Base(int value) : baseValue(value) {
// ...
}

// ...
private:
int baseValue;
};

class Derived : public Base {
public:
Derived(int baseValue, int derivedValue)
: Base(baseValue), derivedValue(derivedValue) {
// ...
}

// ...
private:
int derivedValue;
};

int main() {
Derived d(42, 24);
return 0;
}

另外,如果是在多重继承的情况下,派生类继承了多个基类。基类构造函数的调用顺序是按照派生类中基类的声明顺序进行的,而不是按照派生类构造函数初始化列表中的顺序。

追问:抽象父类的构造函数能否在子类的构造函数初始化列表中被调用?

首先,抽象类它是不能被实例化的。抽象类中包含纯虚函数,意味着派生类必须实现这些纯虚函数,否则派生类也会成为抽象类,无法实例化。

但是,初始化列表可以在派生类的构造函数中调用基类的构造函数,即使基类是抽象类(包含纯虚函数)。这样做是合法的,并不会导致抽象类的实例化。

初始化列表的目的是在派生类构造函数中初始化基类的成员。对于抽象类来说,由于它不能被实例化,派生类必须在其构造函数中调用基类的构造函数来初始化基类的成员。

# 多重继承

多重继承是面向对象编程中的一个概念,它允许一个类继承多个父类的特性和行为。在多重继承中,一个子类可以同时拥有多个父类的属性和方法。

在 C++ 中,多重继承的语法如下:

1
2
3
class DerivedClass : public BaseClass1, public BaseClass2, ... {
// 子类的成员变量和成员函数
};

  • DerivedClass 是子类,它将继承多个父类的特性。
  • BaseClass1 , BaseClass2 , 等等,是父类或基类,它们定义了子类可以继承的属性和方法。
  • public 关键字表明继承是公共的,子类可以访问父类的公共成员。

多重继承的特点:

  1. 多样性和复用性: 多重继承允许一个类从多个父类中继承不同的特性,从而增加了类的多样性和复用性。这有助于构建更灵活的类层次结构。
  2. "Diamond Problem": 多重继承可能引发潜在的问题,其中多个父类中包含具有相同名称的成员,导致命名冲突。这种情况通常称为 "Diamond Problem",即菱形继承。为了解决这个问题,C++ 引入了虚拟继承。
  3. 虚拟继承: C++ 提供了虚拟继承,它允许通过关键字 virtual 来避免菱形继承问题。使用虚拟继承,只会创建一个共享的基类实例,从而解决了冲突。

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
class Animal {
public:
void eat() { /* 实现吃的行为 */ }
};

class Bird {
public:
void fly() { /* 实现飞翔的行为 */ }
};

class BirdWithLegs {
public:
void walk() { /* 实现行走的行为 */ }
};

class Sparrow : public Animal, public Bird, public BirdWithLegs {
// Sparrow 类继承了 Animal、Bird 和 BirdWithLegs 的行为
};

int main() {
Sparrow sparrow;
sparrow.eat(); // Sparrow 可以调用 eat(),它继承自 Animal
sparrow.fly(); // Sparrow 可以调用 fly(),它继承自 Bird
sparrow.walk(); // Sparrow 可以调用 walk(),它继承自 BirdWithLegs
return 0;
}

上述示例展示了多重继承的概念, Sparrow 类继承了 AnimalBirdBirdWithLegs 类的行为,拥有多个父类的特性。

# 菱形继承

"菱形问题"(Diamond Problem)是一种多重继承中的命名冲突和二义性的问题,通常出现在支持多重继承的编程语言中,如 C++。这个问题的名称来自于继承关系图的形状,它看起来像一个菱形。

在菱形问题中,有一个基类(父类)A,它有两个子类(派生类)B 和 C,分别继承自 A。此外,还有一个类 D,它同时继承自 B 和 C。
这会导致一个菱形继承结构。菱形继承的问题主要表现在 D 类中有两份 A 类的数据成员拷贝。

具体问题如下:

  1. 数据冗余D 类继承了两份 A 类的成员,即 B 继承了一份 AC 也继承了一份 A 。这意味着 D 类实际上有两份 A::value 变量。
  2. 二义性问题:在 D 类中,当你访问 A 类的成员(比如 value ),编译器不知道你是想通过 B 继承的那份 A 还是通过 C 继承的那份 A 。这种情况下,你需要显式指定是哪一条继承路径,比如 D::B::valueD::C::value
    (同样地,如果派生类 BC 都重写了基类 A 中的某个函数,而子类 D 通过多重继承同时继承了 BC ,那么在 D 类中调用这个函数时,会产生 二义性 问题。因为编译器不知道应该调用 B 中的实现,还是 C 中的实现。)

以下是一个示例,说明了菱形问题:

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
class A {
public:
int value;
void foo() {
std::cout << "A's foo" << std::endl;
}
};

class B : public A {
public:
void foo() {
std::cout << "B's foo" << std::endl;
}
};

class C : public A {
public:
void foo() {
std::cout << "C's foo" << std::endl;
}
};

class D : public B, public C {
};

int main() {
D d;
d.value; // 这里将会导致二义性
d.foo(); // 这里将会导致二义性,因为 D 继承了 B 和 C,它们都有不同的 foo() 函数实现。
return 0;
}

在上述示例中,类 D 继承了类 B 和类 C,它们都重载了基类 A 的 foo() 函数。当在类 D 的实例上调用 foo() 函数时,编译器无法确定应该使用哪个版本的 foo() 函数,因此会导致二义性。

要解决这个问题,可以采用虚继承。(注意:虚继承只能解决数据冗余的问题,虚继承本身并不能直接解决这种函数重写的二义性问题)

# 虚继承

C++ 的虚继承是一种解决 "菱形问题"(Diamond Problem)的机制,它允许在多重继承关系中通过虚基类来消除二义性。
虚继承通过使用虚基类(virtual base class)来确保在继承层次结构中只存在一个共享的基类实例,而不会出现多次实例化,从而避免二义性。

在虚继承中,如果一个类作为虚基类,它的派生类(继承它的类)将共享同一个基类子对象,而不是创建多个独立的基类子对象。这就确保了无论多少次继承,只有一个共享的虚基类子对象。这有助于解决菱形问题,避免出现二义性。

以下是一个示例,说明虚继承的用法:

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
class A {
public:
int dataA;
};

class B : public virtual A {
public:
int dataB;
};

class C : public virtual A {
public:
int dataC;
};

class D : public B, public C {
public:
int dataD;
};

int main() {
D d;
d.dataA = 10; // 可以直接访问共享的虚基类A的成员
d.dataB = 20;
d.dataC = 30;
d.dataD = 40;
return 0;
}

在上述示例中,类 B 和类 C 都通过虚继承继承了虚基类 A。这确保了在类 D 中只存在一个共享的虚基类 A 的实例。通过虚继承,可以避免菱形问题,使继承层次结构更清晰,并减少二义性。

虚继承的主要作用是解决菱形继承中的数据的冗余和二义性问题,即确保基类的成员在最终派生类中只有一份。但虚继承本身并不能直接解决这种函数重写的二义性问题。解决二义性问题的关键依然在于你如何设计和调用派生类中的函数。

那么如何解决函数菱形继承中的函数二义性问题呢?

方案一:显式调用
如果你希望在 D 中调用 B::show() 或者 C::show() ,可以通过显式指定调用路径来解决这个问题。例如:

1
2
3
D d;
d.B::show(); // 调用 B::show()
d.C::show(); // 调用 C::show()

方案二:再次覆写
如果希望 D 类自动决定调用哪个父类的函数实现,可以在 D 中重写 show() 函数,从而显式选择调用 BC 的实现。例如:

1
2
3
4
5
6
class D : public B, public C {
public:
void show() override {
B::show(); // 选择调用 B::show()
}
};

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 的继承 - C++ - 编程 | R1ck's Playground = how

# i、基本概念

# 继承关系中的构造函数

在 C++ 的继承关系中,派生类的构造函数必须在其初始化列表中调用基类的构造函数来构造基类部分

在派生类的构造函数中,构造基类部分的步骤是隐式的,但必须确保基类的构造函数被正确调用,以初始化基类的成员。这是因为派生类不仅包含自己的成员,还继承了基类的成员,这些成员需要在构造过程中得到适当的初始化。

构造函数调用基类构造函数的方式是在派生类构造函数的初始化列表中使用基类的构造函数,并提供适当的参数。

以下是一个示例:

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
class Base {
public:
Base(int value) : baseValue(value) {
// ...
}

// ...
private:
int baseValue;
};

class Derived : public Base {
public:
Derived(int baseValue, int derivedValue)
: Base(baseValue), derivedValue(derivedValue) {
// ...
}

// ...
private:
int derivedValue;
};

int main() {
Derived d(42, 24);
return 0;
}

另外,如果是在多重继承的情况下,派生类继承了多个基类。基类构造函数的调用顺序是按照派生类中基类的声明顺序进行的,而不是按照派生类构造函数初始化列表中的顺序。

追问:抽象父类的构造函数能否在子类的构造函数初始化列表中被调用?

首先,抽象类它是不能被实例化的。抽象类中包含纯虚函数,意味着派生类必须实现这些纯虚函数,否则派生类也会成为抽象类,无法实例化。

但是,初始化列表可以在派生类的构造函数中调用基类的构造函数,即使基类是抽象类(包含纯虚函数)。这样做是合法的,并不会导致抽象类的实例化。

初始化列表的目的是在派生类构造函数中初始化基类的成员。对于抽象类来说,由于它不能被实例化,派生类必须在其构造函数中调用基类的构造函数来初始化基类的成员。

# 多重继承

多重继承是面向对象编程中的一个概念,它允许一个类继承多个父类的特性和行为。在多重继承中,一个子类可以同时拥有多个父类的属性和方法。

在 C++ 中,多重继承的语法如下:

1
2
3
class DerivedClass : public BaseClass1, public BaseClass2, ... {
// 子类的成员变量和成员函数
};

  • DerivedClass 是子类,它将继承多个父类的特性。
  • BaseClass1 , BaseClass2 , 等等,是父类或基类,它们定义了子类可以继承的属性和方法。
  • public 关键字表明继承是公共的,子类可以访问父类的公共成员。

多重继承的特点:

  1. 多样性和复用性: 多重继承允许一个类从多个父类中继承不同的特性,从而增加了类的多样性和复用性。这有助于构建更灵活的类层次结构。
  2. "Diamond Problem": 多重继承可能引发潜在的问题,其中多个父类中包含具有相同名称的成员,导致命名冲突。这种情况通常称为 "Diamond Problem",即菱形继承。为了解决这个问题,C++ 引入了虚拟继承。
  3. 虚拟继承: C++ 提供了虚拟继承,它允许通过关键字 virtual 来避免菱形继承问题。使用虚拟继承,只会创建一个共享的基类实例,从而解决了冲突。

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
class Animal {
public:
void eat() { /* 实现吃的行为 */ }
};

class Bird {
public:
void fly() { /* 实现飞翔的行为 */ }
};

class BirdWithLegs {
public:
void walk() { /* 实现行走的行为 */ }
};

class Sparrow : public Animal, public Bird, public BirdWithLegs {
// Sparrow 类继承了 Animal、Bird 和 BirdWithLegs 的行为
};

int main() {
Sparrow sparrow;
sparrow.eat(); // Sparrow 可以调用 eat(),它继承自 Animal
sparrow.fly(); // Sparrow 可以调用 fly(),它继承自 Bird
sparrow.walk(); // Sparrow 可以调用 walk(),它继承自 BirdWithLegs
return 0;
}

上述示例展示了多重继承的概念, Sparrow 类继承了 AnimalBirdBirdWithLegs 类的行为,拥有多个父类的特性。

# 菱形继承

"菱形问题"(Diamond Problem)是一种多重继承中的命名冲突和二义性的问题,通常出现在支持多重继承的编程语言中,如 C++。这个问题的名称来自于继承关系图的形状,它看起来像一个菱形。

在菱形问题中,有一个基类(父类)A,它有两个子类(派生类)B 和 C,分别继承自 A。此外,还有一个类 D,它同时继承自 B 和 C。
这会导致一个菱形继承结构。菱形继承的问题主要表现在 D 类中有两份 A 类的数据成员拷贝。

具体问题如下:

  1. 数据冗余D 类继承了两份 A 类的成员,即 B 继承了一份 AC 也继承了一份 A 。这意味着 D 类实际上有两份 A::value 变量。
  2. 二义性问题:在 D 类中,当你访问 A 类的成员(比如 value ),编译器不知道你是想通过 B 继承的那份 A 还是通过 C 继承的那份 A 。这种情况下,你需要显式指定是哪一条继承路径,比如 D::B::valueD::C::value
    (同样地,如果派生类 BC 都重写了基类 A 中的某个函数,而子类 D 通过多重继承同时继承了 BC ,那么在 D 类中调用这个函数时,会产生 二义性 问题。因为编译器不知道应该调用 B 中的实现,还是 C 中的实现。)

以下是一个示例,说明了菱形问题:

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
class A {
public:
int value;
void foo() {
std::cout << "A's foo" << std::endl;
}
};

class B : public A {
public:
void foo() {
std::cout << "B's foo" << std::endl;
}
};

class C : public A {
public:
void foo() {
std::cout << "C's foo" << std::endl;
}
};

class D : public B, public C {
};

int main() {
D d;
d.value; // 这里将会导致二义性
d.foo(); // 这里将会导致二义性,因为 D 继承了 B 和 C,它们都有不同的 foo() 函数实现。
return 0;
}

在上述示例中,类 D 继承了类 B 和类 C,它们都重载了基类 A 的 foo() 函数。当在类 D 的实例上调用 foo() 函数时,编译器无法确定应该使用哪个版本的 foo() 函数,因此会导致二义性。

要解决这个问题,可以采用虚继承。(注意:虚继承只能解决数据冗余的问题,虚继承本身并不能直接解决这种函数重写的二义性问题)

# 虚继承

C++ 的虚继承是一种解决 "菱形问题"(Diamond Problem)的机制,它允许在多重继承关系中通过虚基类来消除二义性。
虚继承通过使用虚基类(virtual base class)来确保在继承层次结构中只存在一个共享的基类实例,而不会出现多次实例化,从而避免二义性。

在虚继承中,如果一个类作为虚基类,它的派生类(继承它的类)将共享同一个基类子对象,而不是创建多个独立的基类子对象。这就确保了无论多少次继承,只有一个共享的虚基类子对象。这有助于解决菱形问题,避免出现二义性。

以下是一个示例,说明虚继承的用法:

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
class A {
public:
int dataA;
};

class B : public virtual A {
public:
int dataB;
};

class C : public virtual A {
public:
int dataC;
};

class D : public B, public C {
public:
int dataD;
};

int main() {
D d;
d.dataA = 10; // 可以直接访问共享的虚基类A的成员
d.dataB = 20;
d.dataC = 30;
d.dataD = 40;
return 0;
}

在上述示例中,类 B 和类 C 都通过虚继承继承了虚基类 A。这确保了在类 D 中只存在一个共享的虚基类 A 的实例。通过虚继承,可以避免菱形问题,使继承层次结构更清晰,并减少二义性。

虚继承的主要作用是解决菱形继承中的数据的冗余和二义性问题,即确保基类的成员在最终派生类中只有一份。但虚继承本身并不能直接解决这种函数重写的二义性问题。解决二义性问题的关键依然在于你如何设计和调用派生类中的函数。

那么如何解决函数菱形继承中的函数二义性问题呢?

方案一:显式调用
如果你希望在 D 中调用 B::show() 或者 C::show() ,可以通过显式指定调用路径来解决这个问题。例如:

1
2
3
D d;
d.B::show(); // 调用 B::show()
d.C::show(); // 调用 C::show()

方案二:再次覆写
如果希望 D 类自动决定调用哪个父类的函数实现,可以在 D 中重写 show() 函数,从而显式选择调用 BC 的实现。例如:

1
2
3
4
5
6
class D : public B, public C {
public:
void show() override {
B::show(); // 选择调用 B::show()
}
};

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/C-\347\261\273\344\270\216\346\250\241\346\235\277/index.html" "b/2024/10/18/C-\347\261\273\344\270\216\346\250\241\346\235\277/index.html" index 8d63e5c..bf364c5 100644 --- "a/2024/10/18/C-\347\261\273\344\270\216\346\250\241\346\235\277/index.html" +++ "b/2024/10/18/C-\347\261\273\344\270\216\346\250\241\346\235\277/index.html" @@ -1 +1 @@ -C++ 类与模板 - C++ - 编程 | R1ck's Playground = how

# i、C++ 抽象类

# ii、什么是抽象类

在 C++ 中,抽象类是一个特殊的类,它包含了至少一个纯虚函数(pure virtual function)。纯虚函数是在函数声明后面加上 "= 0" 的虚函数,表示该函数没有实际的实现,而是由派生类来实现。

抽象类的主要特点是它不能被实例化,即无法创建抽象类的对象。这是因为抽象类中包含了纯虚函数,这些函数没有实际的实现,无法直接调用。因此,抽象类的作用是定义一组接口或协议,规定了派生类必须实现的纯虚函数。

抽象类用于建立类的继承关系和接口规范,它充当了一种抽象的模板,要求派生类必须实现其纯虚函数,从而才能创建对象并调用这些函数。

抽象类的声明示例:

1
2
3
4
5
6
7
8
9
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数,没有实际实现
virtual double area() const = 0; // 另一个纯虚函数
// 可以包含普通的虚函数和非虚函数,也可以有数据成员
};

// 无法实例化抽象类,下面的代码会导致编译错误
// Shape shape;

派生类可以从抽象类继承,并必须实现抽象类中的所有纯虚函数才能成为非抽象类。

抽象类在面向对象编程中有重要的作用,它提供了一种接口和规范的定义方式,促使派生类遵循约定,并具有更好的可扩展性和可维护性。

# ii、C++ “接口”

提问:C++ 中,据我所知,有抽象类的概念,我想知道 C++ 中有 “接口” 的概念吗,它和抽象类是什么关系?

回答:
在 C++ 中,虽然没有像 Java 或 C# 中那样明确的 "接口"(interface)关键字,但是我们可以通过抽象类来模拟接口的概念。

在其他面向对象编程语言中,接口是一种特殊的抽象类,它只包含纯虚函数(没有数据成员和非虚函数),而且类中的所有成员函数都是纯虚函数。接口定义了一组公共的方法签名,用于规范一组类的行为,但不包含具体的实现。类可以通过继承接口来实现这些方法,从而遵循了接口的规范。

在 C++ 中,我们可以使用抽象类来模拟接口。通过将所有成员函数都声明为纯虚函数,我们可以使抽象类成为一个接口。类似于接口,抽象类也不能被实例化,而只能用作其他类的基类,要求派生类必须实现其纯虚函数。

# i、C++ 嵌套类

在 C++ 中,类内部嵌套类(Nested Class)是一种定义在另一个类的内部的类。嵌套类可以像普通类一样拥有成员变量、成员函数、构造函数和析构函数,但它们的作用范围限定在包含它们的外部类中。嵌套类可以分为两种类型:静态嵌套类非静态嵌套类

1. 静态嵌套类(Static Nested Class): 静态嵌套类是一个定义在外部类的作用域内的内部类。它的定义不依赖于外部类的对象,可以直接通过外部类名访问。静态嵌套类可以拥有自己的静态成员和非静态成员,但它们不会继承外部类的非静态成员。

1
2
3
4
5
6
7
8
9
class OuterClass {
public:
// OuterClass members

static class StaticNestedClass {
public:
// StaticNestedClass members
};
};

2. 非静态嵌套类(Inner Class): 非静态嵌套类是一个定义在外部类的作用域内的内部类,但它的每个对象都与外部类的特定对象相关联。非静态嵌套类可以访问外部类的所有成员,包括私有成员。非静态嵌套类的定义必须在外部类的作用域内,否则编译器无法理解它们。

1
2
3
4
5
6
7
class OuterClass {
public:
class InnerClass {
public:
// InnerClass members
};
};

嵌套类的一个常见用途是将相关的类定义组织在一起,以减少全局命名冲突,并提高代码的模块化性。此外,嵌套类可以访问外部类的私有成员,从而实现一些封装和数据隐藏的效果。

需要注意的是,嵌套类的定义和使用需要遵循 C++ 的语法规则,尤其是在访问嵌套类的成员时,需要使用正确的作用域限定符。

提问:嵌套类的作用是什么,和使用继承关系有什么区别?

嵌套类的作用在于将相关的类定义组织在一起,以提高代码的模块化性和可读性。它可以将具有紧密关联的功能和数据封装在一起,减少全局命名冲突,并在逻辑上将相关的类组织在一起。嵌套类还可以访问外部类的私有成员,从而实现一些封装和数据隐藏的效果。

与继承关系相比,嵌套类具有以下区别和优势:
1. 组织性和可读性: 嵌套类更适合在代码中组织相关的类定义,使代码更易读、易理解。继承关系更多用于实现类之间的层次关系。
2. 权限控制: 嵌套类可以访问外部类的私有成员,但继承关系只能通过继承来获得基类的受保护和公有成员。
3. 独立性: 嵌套类是独立于外部类的,每个嵌套类的对象与外部类对象无关。而继承关系会导致子类和父类之间的紧密耦合。
4. 名称空间: 嵌套类可以有更短的名称,因为它们的名称是相对于外部类的。继承关系中的子类名称需要保留父类的名称。

总之,嵌套类适用于将相关的类定义组织在一起,实现更模块化的代码结构,而继承关系适用于实现类之间的层次关系和共享行为。

提问:嵌套类只能在外部类中实例化吗,还是在整个程序中都可以使用?

嵌套类并不仅限于在外部类中实例化,它们可以在整个程序中使用,就像独立的类一样。嵌套类的可见性和使用范围取决于其访问修饰符(public、protected、private)以及外部类的访问修饰符。

在外部类的作用域中,嵌套类的访问权限和普通类的访问权限一样。例如,如果嵌套类声明为 public ,则它可以在外部类的作用域外被其他代码访问和实例化。如果嵌套类声明为 private ,则只有外部类的成员函数内部才能实例化和访问嵌套类。

而在外部类的作用域外,要使用嵌套类,需要通过外部类名来限定。嵌套类的名称是相对于外部类的,因此需要使用 外部类名::嵌套类名 的方式来访问。

以下是一个示例,演示了如何在外部类作用域外使用嵌套类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Outer {
public:
class Nested {
public:
void NestedFunction() {
// 嵌套类的成员函数
}
};
};

int main() {
Outer::Nested nestedObj; // 在外部类作用域外实例化嵌套类对象
nestedObj.NestedFunction(); // 调用嵌套类的成员函数

return 0;
}

在上述示例中, NestedOuter 的嵌套类,但它可以在 main 函数中实例化和使用。需要注意的是,嵌套类的可见性和使用范围仍然受到外部类的访问修饰符的影响。

提问:嵌套类可以任意访问外部类的所有成员数据吗?

嵌套类可以访问外部类的所有成员数据,包括私有成员数据。这是因为嵌套类在外部类的作用域内,被视为外部类的一个成员,所以可以访问外部类的所有成员,不论其是公有、保护还是私有。

以下是一个示例,演示了嵌套类如何访问外部类的成员数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Outer {
private:
int privateMember;

public:
class Nested {
public:
void AccessOuterPrivate(Outer& outerObj) {
// 嵌套类可以访问外部类的私有成员
int data = outerObj.privateMember;
// 在这里使用外部类的数据进行操作
}
};
};

在上述示例中, Nested 嵌套类可以通过外部类对象的引用来访问外部类的私有成员变量 privateMember

需要注意的是,虽然嵌套类可以访问外部类的所有成员数据,但这也增加了代码的紧密性和依赖性。嵌套类和外部类之间的耦合度会增加,可能会影响代码的维护和扩展。因此,在使用嵌套类时,需要谨慎考虑类之间的关系,避免不必要的耦合。

提问:嵌套类和组合关系有什么区别?

嵌套类和组合关系是两种不同的概念,它们在面向对象编程中具有不同的含义和用途。

  1. 嵌套类(Nested Class)
    • 嵌套类是在一个类的内部定义的类,也称为内部类。
    • 嵌套类可以访问外部类的成员,包括私有成员。
    • 嵌套类可以在外部类的作用域内创建实例。
    • 嵌套类可以用来实现一种逻辑上的内部封装,将与外部类相关的功能封装在一起,但也增加了类之间的紧密性。
    • 嵌套类通常用于辅助外部类,但并不意味着嵌套类是外部类的组成部分。
  2. 组合关系
    • 组合是一种对象关系,它描述了一个类(整体)由其他类(部分)组成的关系。
    • 在组合关系中,一个类的对象包含了其他类的对象作为其成员变量。
    • 组合关系强调了整体与部分之间的关系,整体对象负责管理和控制部分对象的生命周期。
    • 组合关系通常用于表示更复杂的对象结构,通过将不同的对象组合在一起实现更高层次的功能。

总的来说,嵌套类是一个类的内部定义的类,用于实现逻辑上的封装,而组合关系描述了整体与部分之间的关系,用于构建更复杂的对象结构。在选择使用嵌套类还是组合关系时,需要根据具体的需求和设计来决定。

提问:但嵌套类不也是描述了一个类作为其他类的组成成分吗?

嵌套类确实也可以看作是一种类的组成关系,但是与常规的组合关系还是有一些区别。

嵌套类确实是一个类作为另一个类的一部分的情况,但是它通常用于在一个类的内部定义一个辅助的、相对独立的类,以实现某种特定的功能。它并不是强调整体与部分之间的关系,而更强调内部的逻辑结构。例如,在外部类内部定义的嵌套类通常是为了实现外部类的某个方面的功能,它们的存在并不一定是整体与部分的强关系

组合关系则更强调整体与部分之间的紧密关系,其中一个类的实例包含了另一个类的实例。组合关系用于构建复杂的对象结构,强调整体对象和部分对象之间的依赖和合作关系,通常整体对象负责管理部分对象的生命周期。

虽然嵌套类和组合关系都涉及类与类之间的关系,但它们的重点和用途略有不同。嵌套类更侧重于内部逻辑结构和功能的划分,而组合关系更强调整体与部分之间的关系和依赖。所以,嵌套类虽然可以看作是一种类的组成关系,但在实际使用中,它的重点和目的与通常的组合关系有所不同。

提问:那么,外部类中定义了一个嵌套类,是否同时在外部类中定义 / 实例化了一个嵌套类的对象,还是说仅仅只是定义了嵌套类?

在外部类中定义了一个嵌套类,并不会自动同时在外部类中实例化一个嵌套类的对象。定义嵌套类只是在外部类的作用域中引入了一个内部类的定义,你需要显式地实例化嵌套类的对象才能在外部类中使用

嵌套类的定义只是为了在外部类内部创建一个命名空间,以便可以在外部类的作用域中引用嵌套类的名称。要在外部类中使用嵌套类,你需要通过外部类的实例来创建嵌套类的对象。这个对象并不是自动创建的,而是需要你显式地进行实例化。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class OuterClass {
public:
class NestedClass {
public:
void NestedMethod() {
// ...
}
};

void OuterMethod() {
NestedClass nestedObject; // 实例化嵌套类的对象
nestedObject.NestedMethod(); // 调用嵌套类的方法
}
};

int main() {
OuterClass outerObject;
outerObject.OuterMethod();

// 下面这行代码会报错,因为 NestedClass 不在 main 的作用域内
// NestedClass nestedObject;

return 0;
}

在这个示例中,虽然在 OuterClass 中定义了 NestedClass ,但要在 main 函数中使用 NestedClass ,你需要通过 OuterClass 的实例来创建 NestedClass 的对象。直接在 main 函数中创建 NestedClass 的对象会导致编译错误,因为 NestedClass 的作用域仅限于 OuterClass 内部。

# i、C++ 模板类

# ii、什么是模板类

C++ 的模板类(Template Class)是一种通用的类定义,可以用来生成特定类型的类。模板类在类的定义中使用了模板参数,使得类的行为和属性能够根据不同的类型进行定制。模板类可以使代码更加通用和可复用,因为它可以为不同的数据类型生成相似的类,而不需要重复编写相似的代码。

模板类的定义通常如下所示:

1
2
3
4
template <typename T>
class MyTemplateClass {
// 类的成员和方法定义,可以使用类型 T
};

在上面的例子中, typename T 是一个模板参数,代表一个通用的类型。在使用模板类时,你可以为 T 指定具体的类型,然后编译器会根据指定的类型生成相应的类。

# ii、模板类的继承

关于模板类的继承,模板类可以像普通类一样继承其他类,但有一些注意事项:

  1. 模板类可以继承普通类:可以创建一个继承自普通类的模板类。这样,模板类会继承基类的属性和方法,但模板参数仍然可以在子类中使用。
  2. 模板类可以继承模板类:模板类也可以继承其他模板类,但需要确保模板参数的匹配。即使基类和派生类的模板参数不同,它们仍然被认为是不同的类。

下面是一个简单的示例,展示了模板类的继承:

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
template <typename T>
class Base {
public:
T value;

Base(T val) : value(val) {}

void printValue() {
cout << "Value: " << value << endl;
}
};

template <typename T>
class Derived : public Base<T> {
public:
Derived(T val) : Base<T>(val) {}

void printDoubledValue() {
cout << "Doubled Value: " << this->value * 2 << endl;
}
};

int main() {
Derived<int> derivedObj(5);
derivedObj.printValue();
derivedObj.printDoubledValue();

return 0;
}

模板类继承模板类时,派生类的模板参数必须和基类的模板参数一致吗?

不一定,但派生类模板的模板参数受基类模板的模板参数影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
class Base
{
public:
T _a;
public:
Base(T n):_a(n) {}
T get() const { return _a; }
};

template<typename T, typename U>
class Derive:public Base<U>
{
public:
U _b;
public:
Derive(T t, U u):Base<T>(t), _b(u) {}
U sum() const { return _b + U(Base::get()); }
};

参考链接:
https://www.54benniao.com/a/fleu4w.html

# i、模板特化

在 C++ 中,模板特化(Template Specialization)是一种机制,允许你为模板类或函数的特定参数类型提供特定的实现。特化的主要目的是让你可以根据不同的类型提供特定的行为,而不是使用通用的模板实现。

有两种主要类型的特化:类模板特化和函数模板特化。

1. 类模板特化
类模板特化用于为特定的数据类型提供特定的类定义。它通常用于为模板类的成员变量或成员函数提供特定的实现。例如,你可以为一个模板类提供特定于某种类型的成员函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
class MyTemplateClass {
public:
void DoSomething() {
// 通用实现
}
};

template <>
class MyTemplateClass<int> {
public:
void DoSomething() {
// 针对 int 类型的特化实现
}
};

2. 函数模板特化
函数模板特化用于为特定的数据类型提供特定的函数实现。它通常用于为模板函数提供特定的行为。例如,你可以为一个模板函数提供特定于某种类型的实现。

1
2
3
4
5
6
7
8
9
template <typename T>
T Add(T a, T b) {
return a + b;
}

template <>
int Add<int>(int a, int b) {
return a * b; // 针对 int 类型的特化实现
}

模板特化的语法通常包括在模板声明之后使用 template <> 关键字,然后提供特化的参数类型,随后是特定的实现。特化可以用于为不同的数据类型提供不同的逻辑,以满足特定的需求。

需要注意的是,特化不仅可以用于提供不同的实现,还可以用于解决特定类型的边界条件和优化,以提高程序的性能和灵活性。特化是 C++ 中强大的工具,用于在泛型编程中处理不同数据类型的情况。

# i、函数模板

一般形式:

1
2
3
4
5
template <typename type>
return_type funcname(parameter list)
{
// 函数的主体
}

其中,模板参数可以出现在返回值或传参列表中,但不一定。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}

template <class T>
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}

另外,在调用模板函数时,网上的教程举的例子一般都是隐式转换,即传参时并没有显式声明模板参数的类型,让编译器自动推导。

但也可以直接显式声明(有些情况下编译器无法隐式推导)。此时调用格式为:

1
函数名<typename>(函数传参);

例如:

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
36
37
38
39
40
41
42
43
44
45
46
47
template <typename InjectInfo, typename Module>
void write_bin(const string & data_type)
{
for (auto &info : dict[data_type])
{
// 获取bin文件中的data_size和offset
char buf[info.data_size + 1];
parse.old_bin_file[data_type]->seekg(info.offset);
parse.old_bin_file[data_type]->read(buf, info.data_size);

// 组成inject格式
InjectInfo iInfo;
iInfo.info = shared_ptr<Module>(new Module());;
iInfo.info->ParseFromArray(buf, info.data_size);
iInfo.utc_tick = info.utc_tick;
iInfo.gtc_tick = info.gtc_tick;
std::shared_ptr<InjectInfo> _inject(new InjectInfo(iInfo));
vector<shared_ptr<InjectInfo> > _inject_v;
_inject_v.emplace_back(_inject);

// 用msgpack序列化然后写入新的bin文件
msgpack::sbuffer b;
msgpack::packer<msgpack::sbuffer> p(&b);
p.pack(_inject_v);
parse.new_bin_file[old_2_new_bin[data_type]]->write(b.data(), b.size());
}
}

void write_log()
{
string data_type;
data_type = "ObjectPerceptionObjectList";
write_bin<VehicleInjectInfo, perception::ObjectList>(data_type);
data_type = "roadmarkingfront";
write_bin<LaneInjectInfo, perception::Roadmarking>(data_type);
data_type = "radar";
write_bin<RadarInjectInfo, perception::Radar>(data_type);
data_type = "corner_radar";
write_bin<RadarInjectInfo, perception::Radar>(data_type);
data_type = "rcfusion";
write_bin<ResultInfo, perception::RCFusion>(data_type);
data_type = "rcfusion_hmi";
write_bin<ResultInfo, perception::RCFusion>(data_type);
data_type = "vehicle_signal";
write_bin<DriveInjectInfo, minieye::VehicleSignal>(data_type);
return ;
}

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +C++ 类与模板 - C++ - 编程 | R1ck's Playground = how

# i、C++ 抽象类

# ii、什么是抽象类

在 C++ 中,抽象类是一个特殊的类,它包含了至少一个纯虚函数(pure virtual function)。纯虚函数是在函数声明后面加上 "= 0" 的虚函数,表示该函数没有实际的实现,而是由派生类来实现。

抽象类的主要特点是它不能被实例化,即无法创建抽象类的对象。这是因为抽象类中包含了纯虚函数,这些函数没有实际的实现,无法直接调用。因此,抽象类的作用是定义一组接口或协议,规定了派生类必须实现的纯虚函数。

抽象类用于建立类的继承关系和接口规范,它充当了一种抽象的模板,要求派生类必须实现其纯虚函数,从而才能创建对象并调用这些函数。

抽象类的声明示例:

1
2
3
4
5
6
7
8
9
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数,没有实际实现
virtual double area() const = 0; // 另一个纯虚函数
// 可以包含普通的虚函数和非虚函数,也可以有数据成员
};

// 无法实例化抽象类,下面的代码会导致编译错误
// Shape shape;

派生类可以从抽象类继承,并必须实现抽象类中的所有纯虚函数才能成为非抽象类。

抽象类在面向对象编程中有重要的作用,它提供了一种接口和规范的定义方式,促使派生类遵循约定,并具有更好的可扩展性和可维护性。

# ii、C++ “接口”

提问:C++ 中,据我所知,有抽象类的概念,我想知道 C++ 中有 “接口” 的概念吗,它和抽象类是什么关系?

回答:
在 C++ 中,虽然没有像 Java 或 C# 中那样明确的 "接口"(interface)关键字,但是我们可以通过抽象类来模拟接口的概念。

在其他面向对象编程语言中,接口是一种特殊的抽象类,它只包含纯虚函数(没有数据成员和非虚函数),而且类中的所有成员函数都是纯虚函数。接口定义了一组公共的方法签名,用于规范一组类的行为,但不包含具体的实现。类可以通过继承接口来实现这些方法,从而遵循了接口的规范。

在 C++ 中,我们可以使用抽象类来模拟接口。通过将所有成员函数都声明为纯虚函数,我们可以使抽象类成为一个接口。类似于接口,抽象类也不能被实例化,而只能用作其他类的基类,要求派生类必须实现其纯虚函数。

# i、C++ 嵌套类

在 C++ 中,类内部嵌套类(Nested Class)是一种定义在另一个类的内部的类。嵌套类可以像普通类一样拥有成员变量、成员函数、构造函数和析构函数,但它们的作用范围限定在包含它们的外部类中。嵌套类可以分为两种类型:静态嵌套类非静态嵌套类

1. 静态嵌套类(Static Nested Class): 静态嵌套类是一个定义在外部类的作用域内的内部类。它的定义不依赖于外部类的对象,可以直接通过外部类名访问。静态嵌套类可以拥有自己的静态成员和非静态成员,但它们不会继承外部类的非静态成员。

1
2
3
4
5
6
7
8
9
class OuterClass {
public:
// OuterClass members

static class StaticNestedClass {
public:
// StaticNestedClass members
};
};

2. 非静态嵌套类(Inner Class): 非静态嵌套类是一个定义在外部类的作用域内的内部类,但它的每个对象都与外部类的特定对象相关联。非静态嵌套类可以访问外部类的所有成员,包括私有成员。非静态嵌套类的定义必须在外部类的作用域内,否则编译器无法理解它们。

1
2
3
4
5
6
7
class OuterClass {
public:
class InnerClass {
public:
// InnerClass members
};
};

嵌套类的一个常见用途是将相关的类定义组织在一起,以减少全局命名冲突,并提高代码的模块化性。此外,嵌套类可以访问外部类的私有成员,从而实现一些封装和数据隐藏的效果。

需要注意的是,嵌套类的定义和使用需要遵循 C++ 的语法规则,尤其是在访问嵌套类的成员时,需要使用正确的作用域限定符。

提问:嵌套类的作用是什么,和使用继承关系有什么区别?

嵌套类的作用在于将相关的类定义组织在一起,以提高代码的模块化性和可读性。它可以将具有紧密关联的功能和数据封装在一起,减少全局命名冲突,并在逻辑上将相关的类组织在一起。嵌套类还可以访问外部类的私有成员,从而实现一些封装和数据隐藏的效果。

与继承关系相比,嵌套类具有以下区别和优势:
1. 组织性和可读性: 嵌套类更适合在代码中组织相关的类定义,使代码更易读、易理解。继承关系更多用于实现类之间的层次关系。
2. 权限控制: 嵌套类可以访问外部类的私有成员,但继承关系只能通过继承来获得基类的受保护和公有成员。
3. 独立性: 嵌套类是独立于外部类的,每个嵌套类的对象与外部类对象无关。而继承关系会导致子类和父类之间的紧密耦合。
4. 名称空间: 嵌套类可以有更短的名称,因为它们的名称是相对于外部类的。继承关系中的子类名称需要保留父类的名称。

总之,嵌套类适用于将相关的类定义组织在一起,实现更模块化的代码结构,而继承关系适用于实现类之间的层次关系和共享行为。

提问:嵌套类只能在外部类中实例化吗,还是在整个程序中都可以使用?

嵌套类并不仅限于在外部类中实例化,它们可以在整个程序中使用,就像独立的类一样。嵌套类的可见性和使用范围取决于其访问修饰符(public、protected、private)以及外部类的访问修饰符。

在外部类的作用域中,嵌套类的访问权限和普通类的访问权限一样。例如,如果嵌套类声明为 public ,则它可以在外部类的作用域外被其他代码访问和实例化。如果嵌套类声明为 private ,则只有外部类的成员函数内部才能实例化和访问嵌套类。

而在外部类的作用域外,要使用嵌套类,需要通过外部类名来限定。嵌套类的名称是相对于外部类的,因此需要使用 外部类名::嵌套类名 的方式来访问。

以下是一个示例,演示了如何在外部类作用域外使用嵌套类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Outer {
public:
class Nested {
public:
void NestedFunction() {
// 嵌套类的成员函数
}
};
};

int main() {
Outer::Nested nestedObj; // 在外部类作用域外实例化嵌套类对象
nestedObj.NestedFunction(); // 调用嵌套类的成员函数

return 0;
}

在上述示例中, NestedOuter 的嵌套类,但它可以在 main 函数中实例化和使用。需要注意的是,嵌套类的可见性和使用范围仍然受到外部类的访问修饰符的影响。

提问:嵌套类可以任意访问外部类的所有成员数据吗?

嵌套类可以访问外部类的所有成员数据,包括私有成员数据。这是因为嵌套类在外部类的作用域内,被视为外部类的一个成员,所以可以访问外部类的所有成员,不论其是公有、保护还是私有。

以下是一个示例,演示了嵌套类如何访问外部类的成员数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Outer {
private:
int privateMember;

public:
class Nested {
public:
void AccessOuterPrivate(Outer& outerObj) {
// 嵌套类可以访问外部类的私有成员
int data = outerObj.privateMember;
// 在这里使用外部类的数据进行操作
}
};
};

在上述示例中, Nested 嵌套类可以通过外部类对象的引用来访问外部类的私有成员变量 privateMember

需要注意的是,虽然嵌套类可以访问外部类的所有成员数据,但这也增加了代码的紧密性和依赖性。嵌套类和外部类之间的耦合度会增加,可能会影响代码的维护和扩展。因此,在使用嵌套类时,需要谨慎考虑类之间的关系,避免不必要的耦合。

提问:嵌套类和组合关系有什么区别?

嵌套类和组合关系是两种不同的概念,它们在面向对象编程中具有不同的含义和用途。

  1. 嵌套类(Nested Class)
    • 嵌套类是在一个类的内部定义的类,也称为内部类。
    • 嵌套类可以访问外部类的成员,包括私有成员。
    • 嵌套类可以在外部类的作用域内创建实例。
    • 嵌套类可以用来实现一种逻辑上的内部封装,将与外部类相关的功能封装在一起,但也增加了类之间的紧密性。
    • 嵌套类通常用于辅助外部类,但并不意味着嵌套类是外部类的组成部分。
  2. 组合关系
    • 组合是一种对象关系,它描述了一个类(整体)由其他类(部分)组成的关系。
    • 在组合关系中,一个类的对象包含了其他类的对象作为其成员变量。
    • 组合关系强调了整体与部分之间的关系,整体对象负责管理和控制部分对象的生命周期。
    • 组合关系通常用于表示更复杂的对象结构,通过将不同的对象组合在一起实现更高层次的功能。

总的来说,嵌套类是一个类的内部定义的类,用于实现逻辑上的封装,而组合关系描述了整体与部分之间的关系,用于构建更复杂的对象结构。在选择使用嵌套类还是组合关系时,需要根据具体的需求和设计来决定。

提问:但嵌套类不也是描述了一个类作为其他类的组成成分吗?

嵌套类确实也可以看作是一种类的组成关系,但是与常规的组合关系还是有一些区别。

嵌套类确实是一个类作为另一个类的一部分的情况,但是它通常用于在一个类的内部定义一个辅助的、相对独立的类,以实现某种特定的功能。它并不是强调整体与部分之间的关系,而更强调内部的逻辑结构。例如,在外部类内部定义的嵌套类通常是为了实现外部类的某个方面的功能,它们的存在并不一定是整体与部分的强关系

组合关系则更强调整体与部分之间的紧密关系,其中一个类的实例包含了另一个类的实例。组合关系用于构建复杂的对象结构,强调整体对象和部分对象之间的依赖和合作关系,通常整体对象负责管理部分对象的生命周期。

虽然嵌套类和组合关系都涉及类与类之间的关系,但它们的重点和用途略有不同。嵌套类更侧重于内部逻辑结构和功能的划分,而组合关系更强调整体与部分之间的关系和依赖。所以,嵌套类虽然可以看作是一种类的组成关系,但在实际使用中,它的重点和目的与通常的组合关系有所不同。

提问:那么,外部类中定义了一个嵌套类,是否同时在外部类中定义 / 实例化了一个嵌套类的对象,还是说仅仅只是定义了嵌套类?

在外部类中定义了一个嵌套类,并不会自动同时在外部类中实例化一个嵌套类的对象。定义嵌套类只是在外部类的作用域中引入了一个内部类的定义,你需要显式地实例化嵌套类的对象才能在外部类中使用

嵌套类的定义只是为了在外部类内部创建一个命名空间,以便可以在外部类的作用域中引用嵌套类的名称。要在外部类中使用嵌套类,你需要通过外部类的实例来创建嵌套类的对象。这个对象并不是自动创建的,而是需要你显式地进行实例化。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class OuterClass {
public:
class NestedClass {
public:
void NestedMethod() {
// ...
}
};

void OuterMethod() {
NestedClass nestedObject; // 实例化嵌套类的对象
nestedObject.NestedMethod(); // 调用嵌套类的方法
}
};

int main() {
OuterClass outerObject;
outerObject.OuterMethod();

// 下面这行代码会报错,因为 NestedClass 不在 main 的作用域内
// NestedClass nestedObject;

return 0;
}

在这个示例中,虽然在 OuterClass 中定义了 NestedClass ,但要在 main 函数中使用 NestedClass ,你需要通过 OuterClass 的实例来创建 NestedClass 的对象。直接在 main 函数中创建 NestedClass 的对象会导致编译错误,因为 NestedClass 的作用域仅限于 OuterClass 内部。

# i、C++ 模板类

# ii、什么是模板类

C++ 的模板类(Template Class)是一种通用的类定义,可以用来生成特定类型的类。模板类在类的定义中使用了模板参数,使得类的行为和属性能够根据不同的类型进行定制。模板类可以使代码更加通用和可复用,因为它可以为不同的数据类型生成相似的类,而不需要重复编写相似的代码。

模板类的定义通常如下所示:

1
2
3
4
template <typename T>
class MyTemplateClass {
// 类的成员和方法定义,可以使用类型 T
};

在上面的例子中, typename T 是一个模板参数,代表一个通用的类型。在使用模板类时,你可以为 T 指定具体的类型,然后编译器会根据指定的类型生成相应的类。

# ii、模板类的继承

关于模板类的继承,模板类可以像普通类一样继承其他类,但有一些注意事项:

  1. 模板类可以继承普通类:可以创建一个继承自普通类的模板类。这样,模板类会继承基类的属性和方法,但模板参数仍然可以在子类中使用。
  2. 模板类可以继承模板类:模板类也可以继承其他模板类,但需要确保模板参数的匹配。即使基类和派生类的模板参数不同,它们仍然被认为是不同的类。

下面是一个简单的示例,展示了模板类的继承:

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
template <typename T>
class Base {
public:
T value;

Base(T val) : value(val) {}

void printValue() {
cout << "Value: " << value << endl;
}
};

template <typename T>
class Derived : public Base<T> {
public:
Derived(T val) : Base<T>(val) {}

void printDoubledValue() {
cout << "Doubled Value: " << this->value * 2 << endl;
}
};

int main() {
Derived<int> derivedObj(5);
derivedObj.printValue();
derivedObj.printDoubledValue();

return 0;
}

模板类继承模板类时,派生类的模板参数必须和基类的模板参数一致吗?

不一定,但派生类模板的模板参数受基类模板的模板参数影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
class Base
{
public:
T _a;
public:
Base(T n):_a(n) {}
T get() const { return _a; }
};

template<typename T, typename U>
class Derive:public Base<U>
{
public:
U _b;
public:
Derive(T t, U u):Base<T>(t), _b(u) {}
U sum() const { return _b + U(Base::get()); }
};

参考链接:
https://www.54benniao.com/a/fleu4w.html

# i、模板特化

在 C++ 中,模板特化(Template Specialization)是一种机制,允许你为模板类或函数的特定参数类型提供特定的实现。特化的主要目的是让你可以根据不同的类型提供特定的行为,而不是使用通用的模板实现。

有两种主要类型的特化:类模板特化和函数模板特化。

1. 类模板特化
类模板特化用于为特定的数据类型提供特定的类定义。它通常用于为模板类的成员变量或成员函数提供特定的实现。例如,你可以为一个模板类提供特定于某种类型的成员函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
class MyTemplateClass {
public:
void DoSomething() {
// 通用实现
}
};

template <>
class MyTemplateClass<int> {
public:
void DoSomething() {
// 针对 int 类型的特化实现
}
};

2. 函数模板特化
函数模板特化用于为特定的数据类型提供特定的函数实现。它通常用于为模板函数提供特定的行为。例如,你可以为一个模板函数提供特定于某种类型的实现。

1
2
3
4
5
6
7
8
9
template <typename T>
T Add(T a, T b) {
return a + b;
}

template <>
int Add<int>(int a, int b) {
return a * b; // 针对 int 类型的特化实现
}

模板特化的语法通常包括在模板声明之后使用 template <> 关键字,然后提供特化的参数类型,随后是特定的实现。特化可以用于为不同的数据类型提供不同的逻辑,以满足特定的需求。

需要注意的是,特化不仅可以用于提供不同的实现,还可以用于解决特定类型的边界条件和优化,以提高程序的性能和灵活性。特化是 C++ 中强大的工具,用于在泛型编程中处理不同数据类型的情况。

# i、函数模板

一般形式:

1
2
3
4
5
template <typename type>
return_type funcname(parameter list)
{
// 函数的主体
}

其中,模板参数可以出现在返回值或传参列表中,但不一定。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}

template <class T>
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}

另外,在调用模板函数时,网上的教程举的例子一般都是隐式转换,即传参时并没有显式声明模板参数的类型,让编译器自动推导。

但也可以直接显式声明(有些情况下编译器无法隐式推导)。此时调用格式为:

1
函数名<typename>(函数传参);

例如:

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
36
37
38
39
40
41
42
43
44
45
46
47
template <typename InjectInfo, typename Module>
void write_bin(const string & data_type)
{
for (auto &info : dict[data_type])
{
// 获取bin文件中的data_size和offset
char buf[info.data_size + 1];
parse.old_bin_file[data_type]->seekg(info.offset);
parse.old_bin_file[data_type]->read(buf, info.data_size);

// 组成inject格式
InjectInfo iInfo;
iInfo.info = shared_ptr<Module>(new Module());;
iInfo.info->ParseFromArray(buf, info.data_size);
iInfo.utc_tick = info.utc_tick;
iInfo.gtc_tick = info.gtc_tick;
std::shared_ptr<InjectInfo> _inject(new InjectInfo(iInfo));
vector<shared_ptr<InjectInfo> > _inject_v;
_inject_v.emplace_back(_inject);

// 用msgpack序列化然后写入新的bin文件
msgpack::sbuffer b;
msgpack::packer<msgpack::sbuffer> p(&b);
p.pack(_inject_v);
parse.new_bin_file[old_2_new_bin[data_type]]->write(b.data(), b.size());
}
}

void write_log()
{
string data_type;
data_type = "ObjectPerceptionObjectList";
write_bin<VehicleInjectInfo, perception::ObjectList>(data_type);
data_type = "roadmarkingfront";
write_bin<LaneInjectInfo, perception::Roadmarking>(data_type);
data_type = "radar";
write_bin<RadarInjectInfo, perception::Radar>(data_type);
data_type = "corner_radar";
write_bin<RadarInjectInfo, perception::Radar>(data_type);
data_type = "rcfusion";
write_bin<ResultInfo, perception::RCFusion>(data_type);
data_type = "rcfusion_hmi";
write_bin<ResultInfo, perception::RCFusion>(data_type);
data_type = "vehicle_signal";
write_bin<DriveInjectInfo, minieye::VehicleSignal>(data_type);
return ;
}

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/2024/10/18/extern-C/index.html b/2024/10/18/extern-C/index.html index 7ac9311..c55aebc 100644 --- a/2024/10/18/extern-C/index.html +++ b/2024/10/18/extern-C/index.html @@ -1 +1 @@ -extern C - C++ - 编程 | R1ck's Playground = how

extern "C" 是 C++ 中用于与 C 语言进行互操作的重要关键字。它主要用于告诉编译器在链接阶段如何处理函数或变量名。下面是详细介绍 extern "C" 的背景、用途、以及使用方法:

# 1. 背景:

  • 在 C 和 C++ 中,函数和变量的符号是通过名字来链接的,但 C++ 使用了 "名字修饰"(Name Mangling)来支持函数重载等特性。这种修饰会导致编译器生成的函数名不再是原始的函数名,而是带有类名、参数类型等信息的复杂字符串。
  • 例如,C++ 中一个函数 int add(int, int) 可能会被编译器修饰成 _Z3addii 或其他复杂的符号,而 C 语言中相同的函数名则是简单的 add
  • 由于 C 语言不支持函数重载和名字修饰,如果在 C++ 代码中直接调用 C 语言的函数,编译器会找不到匹配的符号,导致链接错误。

# 2. extern "C" 的用途:

  • 消除名字修饰:
    extern "C" 告诉编译器不要对函数进行名字修饰,而是保持 C 语言的链接方式,这样 C++ 代码就能正确链接 C 语言代码中的符号。
  • 函数声明:
    当在 C++ 中声明 C 语言函数时使用 extern "C" ,以确保链接器找到正确的函数符号。
  • 用于 C++ 与 C 语言混合编程:
    尤其是在大型项目或跨语言项目中,经常需要 C++ 调用 C 语言的库,或者反过来,这时就需要 extern "C" 来管理符号。

# 3. 使用方法:

  • 单个函数声明:

    1
    2
    // 这是在 C++ 代码中声明一个 C 语言的函数
    extern "C" void myCFunction(int arg);

    这样做会告诉编译器 myCFunction 是一个 C 语言函数,编译器将不会对它的名字进行修饰。

  • 多函数声明:

    1
    2
    3
    4
    extern "C" {
    void func1(int);
    int func2(double);
    }

    使用大括号可以一次性声明多个 C 函数,使它们都以 C 语言的链接方式处理。

  • C 语言头文件的引用:

    1
    2
    3
    4
    5
    // C 语言头文件可能没有使用 extern "C"
    // 需要在 C++ 代码中做处理
    extern "C" {
    #include "my_c_library.h"
    }

    这确保了所有在 my_c_library.h 中声明的函数在 C++ 中被正确识别为 C 语言函数。

# 4. 注意事项:

  • 对变量的作用:
    extern "C" 也可以用于变量声明,确保变量名在链接时不被修饰。
  • 不能用于类:
    由于 C 语言不支持类,因此不能对类使用 extern "C"
  • 多次声明:
    在多个地方对同一函数使用 extern "C" 是安全的,但在定义时需要确保只有一个链接的定义。
  • 与 C++ 函数混合:
    extern "C" 不能消除 C++ 特性,比如函数重载、默认参数等。这些特性在 C 语言中是不支持的。

# 5. 示例:

  • 假设有一个 C 语言函数实现如下:
    1
    2
    3
    4
    5
    6
    // my_functions.c
    #include <stdio.h>

    void greet(const char* name) {
    printf("Hello, %s!\n", name);
    }
  • 在 C++ 中调用这个函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // main.cpp
    extern "C" {
    void greet(const char* name);
    }

    int main() {
    greet("World");
    return 0;
    }
  • 这段代码编译时不会出现链接错误,因为 greet 函数在 C++ 中被正确识别为 C 语言函数。

extern "C" 是在 C++ 和 C 混合编程中不可或缺的工具,确保了两种语言可以无缝协作。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +extern C - C++ - 编程 | R1ck's Playground = how

extern "C" 是 C++ 中用于与 C 语言进行互操作的重要关键字。它主要用于告诉编译器在链接阶段如何处理函数或变量名。下面是详细介绍 extern "C" 的背景、用途、以及使用方法:

# 1. 背景:

  • 在 C 和 C++ 中,函数和变量的符号是通过名字来链接的,但 C++ 使用了 "名字修饰"(Name Mangling)来支持函数重载等特性。这种修饰会导致编译器生成的函数名不再是原始的函数名,而是带有类名、参数类型等信息的复杂字符串。
  • 例如,C++ 中一个函数 int add(int, int) 可能会被编译器修饰成 _Z3addii 或其他复杂的符号,而 C 语言中相同的函数名则是简单的 add
  • 由于 C 语言不支持函数重载和名字修饰,如果在 C++ 代码中直接调用 C 语言的函数,编译器会找不到匹配的符号,导致链接错误。

# 2. extern "C" 的用途:

  • 消除名字修饰:
    extern "C" 告诉编译器不要对函数进行名字修饰,而是保持 C 语言的链接方式,这样 C++ 代码就能正确链接 C 语言代码中的符号。
  • 函数声明:
    当在 C++ 中声明 C 语言函数时使用 extern "C" ,以确保链接器找到正确的函数符号。
  • 用于 C++ 与 C 语言混合编程:
    尤其是在大型项目或跨语言项目中,经常需要 C++ 调用 C 语言的库,或者反过来,这时就需要 extern "C" 来管理符号。

# 3. 使用方法:

  • 单个函数声明:

    1
    2
    // 这是在 C++ 代码中声明一个 C 语言的函数
    extern "C" void myCFunction(int arg);

    这样做会告诉编译器 myCFunction 是一个 C 语言函数,编译器将不会对它的名字进行修饰。

  • 多函数声明:

    1
    2
    3
    4
    extern "C" {
    void func1(int);
    int func2(double);
    }

    使用大括号可以一次性声明多个 C 函数,使它们都以 C 语言的链接方式处理。

  • C 语言头文件的引用:

    1
    2
    3
    4
    5
    // C 语言头文件可能没有使用 extern "C"
    // 需要在 C++ 代码中做处理
    extern "C" {
    #include "my_c_library.h"
    }

    这确保了所有在 my_c_library.h 中声明的函数在 C++ 中被正确识别为 C 语言函数。

# 4. 注意事项:

  • 对变量的作用:
    extern "C" 也可以用于变量声明,确保变量名在链接时不被修饰。
  • 不能用于类:
    由于 C 语言不支持类,因此不能对类使用 extern "C"
  • 多次声明:
    在多个地方对同一函数使用 extern "C" 是安全的,但在定义时需要确保只有一个链接的定义。
  • 与 C++ 函数混合:
    extern "C" 不能消除 C++ 特性,比如函数重载、默认参数等。这些特性在 C 语言中是不支持的。

# 5. 示例:

  • 假设有一个 C 语言函数实现如下:
    1
    2
    3
    4
    5
    6
    // my_functions.c
    #include <stdio.h>

    void greet(const char* name) {
    printf("Hello, %s!\n", name);
    }
  • 在 C++ 中调用这个函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // main.cpp
    extern "C" {
    void greet(const char* name);
    }

    int main() {
    greet("World");
    return 0;
    }
  • 这段代码编译时不会出现链接错误,因为 greet 函数在 C++ 中被正确识别为 C 语言函数。

extern "C" 是在 C++ 和 C 混合编程中不可或缺的工具,确保了两种语言可以无缝协作。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/18/lambda-\350\241\250\350\276\276\345\274\217/index.html" "b/2024/10/18/lambda-\350\241\250\350\276\276\345\274\217/index.html" index 2268d9b..065a174 100644 --- "a/2024/10/18/lambda-\350\241\250\350\276\276\345\274\217/index.html" +++ "b/2024/10/18/lambda-\350\241\250\350\276\276\345\274\217/index.html" @@ -1 +1 @@ -lambda 表达式 - C++ - 编程 | R1ck's Playground = how

# i、lambda 表达式 vs 函数

在 C++ 中,定义 lambda 表达式和定义普通函数有以下几个区别和好处:

  1. 简洁性:相对于显式定义一个命名函数,lambda 表达式更加简洁。您可以在需要的地方直接定义 lambda 表达式,而无需显式命名函数。
  2. 匿名性:lambda 表达式是匿名的,它不需要命名,并且可以直接作为参数传递给其他函数或算法。
  3. 上下文捕获:lambda 表达式可以捕获外部作用域中的变量,包括局部变量、函数参数等。这使得它们能够在函数对象中保持状态,并在需要时访问和修改这些变量。
  4. 类型推导:lambda 表达式可以通过类型推导来自动推断其参数类型和返回类型。这使得编写 lambda 表达式更加方便,无需显式指定类型。
  5. 内联函数:编译器通常会将 lambda 表达式视为内联函数,这意味着它们的代码将在使用它们的地方直接插入,而不会引入额外的函数调用开销。
  6. 便于函数对象的创建:lambda 表达式本质上是一种函数对象的创建方式。它们可以被存储在变量中,作为函数参数传递,或者作为函数的返回值返回。

总体而言,lambda 表达式提供了一种更为简洁和灵活的定义函数对象的方式。它们在需要临时性的、简单的函数行为时非常有用,避免了显式定义命名函数的繁琐过程,并且通过上下文捕获和内联函数的特性,使得代码更加紧凑和可读。此外,lambda 表达式与 STL(标准模板库)算法的结合使用可以极大地简化代码,并提高代码的可读性和可维护性。

以上是 gpt 提供的答案,个人而言,我觉得最大的好处似乎也就是匿名性和间接性,使得我可以随时在任何地方生成一个函数对象进行调用。

# i、lambda 表达式对变量的捕获

捕获(Capture)是指在 lambda 表达式中访问并绑定外部作用域中的变量。Lambda 表达式可以捕获局部变量、函数参数、全局变量或静态成员变量等。通过捕获,lambda 表达式可以在其函数体中引用这些变量,并且可以在函数对象的生命周期内保持对这些变量的状态。

在 C++ 中,有两种主要的捕获方式:

  1. 值捕获(Value Capture):通过值捕获,lambda 表达式可以在创建时复制并绑定外部变量的值。这意味着 lambda 函数体中使用的变量将保留捕获时的值,即使外部作用域的变量发生改变也不会影响 lambda 表达式内部的值。
    例如:
    1
    2
    3
    4
    5
    int x = 5;
    auto lambda = [x]() {
    // 使用值捕获的变量x
    // ...
    };
  2. 引用捕获(Reference Capture):通过引用捕获,lambda 表达式可以在创建时绑定外部变量的引用。这意味着 lambda 函数体中使用的变量将与外部作用域中的变量共享相同的存储位置,对变量的修改会影响到外部作用域的值。
    例如:
    1
    2
    3
    4
    5
    int y = 10;
    auto lambda = [&y]() {
    // 使用引用捕获的变量y
    // ...
    };

在 lambda 表达式中,可以同时使用值捕获和引用捕获,并对不同的变量使用不同的捕获方式。此外,还可以使用捕获列表(Capture List)来指定需要捕获的变量以及捕获方式。

捕获使得 lambda 表达式可以在函数对象内部访问和操作外部作用域中的变量。通过捕获,lambda 函数对象可以维持对这些变量的状态,并在其生命周期内使用和修改它们。这使得 lambda 表达式在需要访问外部状态的回调函数、算法和并行编程等场景中非常有用。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +lambda 表达式 - C++ - 编程 | R1ck's Playground = how

# i、lambda 表达式 vs 函数

在 C++ 中,定义 lambda 表达式和定义普通函数有以下几个区别和好处:

  1. 简洁性:相对于显式定义一个命名函数,lambda 表达式更加简洁。您可以在需要的地方直接定义 lambda 表达式,而无需显式命名函数。
  2. 匿名性:lambda 表达式是匿名的,它不需要命名,并且可以直接作为参数传递给其他函数或算法。
  3. 上下文捕获:lambda 表达式可以捕获外部作用域中的变量,包括局部变量、函数参数等。这使得它们能够在函数对象中保持状态,并在需要时访问和修改这些变量。
  4. 类型推导:lambda 表达式可以通过类型推导来自动推断其参数类型和返回类型。这使得编写 lambda 表达式更加方便,无需显式指定类型。
  5. 内联函数:编译器通常会将 lambda 表达式视为内联函数,这意味着它们的代码将在使用它们的地方直接插入,而不会引入额外的函数调用开销。
  6. 便于函数对象的创建:lambda 表达式本质上是一种函数对象的创建方式。它们可以被存储在变量中,作为函数参数传递,或者作为函数的返回值返回。

总体而言,lambda 表达式提供了一种更为简洁和灵活的定义函数对象的方式。它们在需要临时性的、简单的函数行为时非常有用,避免了显式定义命名函数的繁琐过程,并且通过上下文捕获和内联函数的特性,使得代码更加紧凑和可读。此外,lambda 表达式与 STL(标准模板库)算法的结合使用可以极大地简化代码,并提高代码的可读性和可维护性。

以上是 gpt 提供的答案,个人而言,我觉得最大的好处似乎也就是匿名性和间接性,使得我可以随时在任何地方生成一个函数对象进行调用。

# i、lambda 表达式对变量的捕获

捕获(Capture)是指在 lambda 表达式中访问并绑定外部作用域中的变量。Lambda 表达式可以捕获局部变量、函数参数、全局变量或静态成员变量等。通过捕获,lambda 表达式可以在其函数体中引用这些变量,并且可以在函数对象的生命周期内保持对这些变量的状态。

在 C++ 中,有两种主要的捕获方式:

  1. 值捕获(Value Capture):通过值捕获,lambda 表达式可以在创建时复制并绑定外部变量的值。这意味着 lambda 函数体中使用的变量将保留捕获时的值,即使外部作用域的变量发生改变也不会影响 lambda 表达式内部的值。
    例如:
    1
    2
    3
    4
    5
    int x = 5;
    auto lambda = [x]() {
    // 使用值捕获的变量x
    // ...
    };
  2. 引用捕获(Reference Capture):通过引用捕获,lambda 表达式可以在创建时绑定外部变量的引用。这意味着 lambda 函数体中使用的变量将与外部作用域中的变量共享相同的存储位置,对变量的修改会影响到外部作用域的值。
    例如:
    1
    2
    3
    4
    5
    int y = 10;
    auto lambda = [&y]() {
    // 使用引用捕获的变量y
    // ...
    };

在 lambda 表达式中,可以同时使用值捕获和引用捕获,并对不同的变量使用不同的捕获方式。此外,还可以使用捕获列表(Capture List)来指定需要捕获的变量以及捕获方式。

捕获使得 lambda 表达式可以在函数对象内部访问和操作外部作用域中的变量。通过捕获,lambda 函数对象可以维持对这些变量的状态,并在其生命周期内使用和修改它们。这使得 lambda 表达式在需要访问外部状态的回调函数、算法和并行编程等场景中非常有用。

更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/21/\343\200\212Google-C-Style-Guide\343\200\213note/index.html" "b/2024/10/21/\343\200\212Google-C-Style-Guide\343\200\213note/index.html" index da7287f..6df84a4 100644 --- "a/2024/10/21/\343\200\212Google-C-Style-Guide\343\200\213note/index.html" +++ "b/2024/10/21/\343\200\212Google-C-Style-Guide\343\200\213note/index.html" @@ -1 +1 @@ -《Google C++ Style Guide》note - C++ - 编程 | R1ck's Playground = how

参考链接:Google C++ Style Guide

# 头文件

  • 每个 .cpp 源文件都应该有一个相对应的 .h 头文件
  • 每个头文件都应该有头文件守卫,守卫格式统一为: _<PROJECT>_<PATH>_<FILE>_H_
  • 对于头文件中对于 inline 内联函数的定义,要求函数体在十行以内,且不包括循环、switch 语句。
  • 头文件的 include 顺序:相关头文件、C 系统头文件、C++ 头文件、其他库头文件、项目头文件
    (这里的 “相关头文件” 指的是与该源文件同名的头文件)

# 作用域

  • 除了少数例外,将代码放置在命名空间中,以减少命名冲突

  • 不要使用 using namespace xxx 来导入命名空间中的内容(会污染命名空间)

    1
    2
    // Forbidden -- This pollutes the namespace.
    using namespace foo;

  • 当源文件中的定义不需要被其他文件所引用时,通过未命名命名空间或 static 来赋予其 [[C++ 什锦 #i、C++ 内部链接 | 内部链接性]]:

    1
    2
    3
    namespace {
    ...
    } // namespace

  • 用注释来结束命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // In the .h file
    namespace mynamespace {

    // All declarations are within the namespace scope.
    // Notice the lack of indentation.
    class MyClass {
    public:
    ...
    void Foo();
    };

    } // namespace mynamespace

# 对于局部变量
  • 将其放在尽可能小的范围内,并在声明中初始化变量。

  • 定义局部变量时尽可能地靠近其第一次被使用的地方,便于读者找到定义并了解其初始值

    1
    2
    3
    4
    5
    6
    7
    int jobs = NumJobs();
    // More code...
    f(jobs); // Bad -- declaration separate from use.


    int jobs = NumJobs();
    f(jobs); // Good -- declaration immediately (or closely) followed by use.

  • 局部变量应该在定义时初始化,而不是先定义后初始化:

    1
    2
    3
    4
    5
    int i;
    i = f(); // Bad -- initialization separate from declaration.


    int i = f(); // Good -- declaration has initialization.

1
2
3
4
5
6
std::vector<int> v;
v.push_back(1); // Prefer initializing using brace initialization.
v.push_back(2);


std::vector<int> v = {1, 2}; // Good -- v starts initialized.

#

  • 避免在类的构造函数中调用虚函数,详见 [[C++ 的多态 #i、虚函数与构造函数 | 虚函数与构造函数]]
  • 避免隐式转换,尽可能地使用 explicit 关键字来修饰单一参数的函数(拷贝构造和移动构造除外)
  • 只对携带数据的被动对象( passive objects )使用 struct ,其他一律使用 class
  • 尽可能地考虑组合,而不是继承。当使用继承时,优先公有继承
  • 对于纯抽象类,其派生类的继承属于 “接口继承”,其他情况则都是 “实现继承”。
  • 强烈不建议多实现继承
  • 类的成员变量应该是私有的

其中,"passive objects" 指的是那些仅仅用于存储数据而不包含任何行为的对象。换句话说,它们是纯粹的数据结构,用于在程序中传递数据,而不负责执行任何操作或行为。通常,这些对象仅包含公共数据成员和对这些数据成员进行初始化、访问和设置的方法,而不包含任何其他的逻辑或操作。

这种对象通常用于数据传递和数据存储,而不涉及到具体的行为。它们被设计为尽可能简单和轻量,只提供了对数据的简单操作,例如读取和写入。因此,当文本中提到 "passive objects" 时,意思是将结构体(struct)仅用于这种数据容器的情况。

另外,在 C++ 中,“多实现继承” 指的是一个类同时从多个基类继承属性和方法,可能会导致 [[C++ 的继承 #菱形继承 | 菱形继承]] 问题。

# 函数

  • 对于函数的输出,优先使用返回值进行传递,而不是使用输出参数
  • 对于函数的返回值,优先返回值(return by value),而不是返回引用(return by reference)
  • 避免返回裸指针,除非它可以为 nullptr
  • 对于函数的输入参数,应该采用值传递或者 const 的引用传递(pass by value, or pass by reference of const)
  • 对于函数的输出参数和输入输出参数,应该采用引用传递
  • 对于函数参数的顺序,优先输入参数,然后是输出参数
  • 函数体尽可能短小(40 行左右),一个函数就应该集中做某件事情

一个函数应该避免依赖某个引用参数在整个函数调用期间的存活(Avoid defining functions that require a reference parameter to outlive the call.)即,某个引用参数的存活与否,不应该影响该函数的正常运作,其不会因为某个引用参数在函数调用过程中不再存活而出错。

对于函数的返回值,存在两种返回语法,一种是普通的、常见的,返回类型出现在函数名之前;而另一种则是在函数名之前用 auto 进行占位,而真正的返回值则尾随函数的参数列表(这种语法我们称之为 Trailing Return Type Syntax ):

1
auto foo(int x) -> int;

对于 Trailing Return Type Syntax ,其于普通的返回形式不同之处在于,这种情况下返回类型存在于函数体的作用域中,则对于以下情况能够有较好的表现:

1
2
3
4
5
6
7
//普通的“前置”返回
template <typename T, typename U>
decltype(declval<T&>() + declval<U&>()) add(T t, U u);

//“后置”返回
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u);

总得来说,在大多数情况下,我们优先考虑普通的 “前置” 返回,而当我们在处理涉及 lambda 表达式时,或者当将函数的返回类型后置能够带来更好的阅读体验时,我们采用 “后置” 的 Trailing Return Type Syntax

# 所有权与智能指针

  • 首选使用 std::unique_ptr 来明确所有权转移
  • 转移所有权比 “借用” 指针或引用更方便,它减少了用户之间对于内存对象的生存期的协同。
    (所有权明确了谁改对内存的生存期负责,明确了谁该最终释放内存,而单纯地指针拷贝、借用则会混淆这一点)

如果动态分配是必要的,最好将所有权闲置 / 保留在分配它的代码中。此后,如果其他代码需要访问对象,考虑向其传递一个副本,或者传递一个指针或引用而不转移所有权。(我猜这样做的目的是,将所有权保留在原先分配它的内存的位置,让原 own 负责维护其生存期,进而避免所有权的混乱。)

# C++ 特性

# 类型转换
  • 使用 C++ 风格的类型转换( static_castconst_castreinterpret_cast )而不是 C 风格的类型转换
  • 不要使用 C 风格的转换(例如 (int)x ) 除非转换的目标类型是 void
  • 当且仅当 T 是一个类类型时使用 T(x) 这种转换形式
  • 使用大括号初始化来转换算术类型(例如 int64_t{x}
  • static_cast 进行类类型的向上、向下转换
  • const_cast 来移除 const 限定词
  • reinterpret_cast 来进行不安全的类型转换,当且仅当你知道你在干什么的时候使用
# 其他
  • 优先使用前置形式的自增、自减运算符 ++-- ,(即 ++i--i )除非你需要用到后置自增 / 自减的结果。
  • 优先使用 using 语法而不是 typedef 语法进行别名
  • 对于空指针,使用 nullptr ,对于空字符(串),使用 \0
  • 优先考虑使用 sizeof (变量名) 而不是 sizeof (类型名)
    原因是 sizeof (变量名) 会随着类型的改变而进行适当的更新
    1
    2
    3
    4
    MyStruct data;
    memset(&data, 0, sizeof(data)); //good

    memset(&data, 0, sizeof(MyStruct)); //bad
# const 限定
  • 在 API 中,尽可能地(合理)使用 const,用于修饰类函数、函数传参
  • 对于以值传递的函数参数,加 const 修饰无意义,因为它不会影响到调用者传入的数据
  • 对于 const 修饰符,使用前置的书写形式,即 const int* foo
# 无符号整型
  • 尽可能地不要使用无符号整型(unsigned integer)诸如 uint32_t ,除非有一个合理有效的理由:用于表示位图
  • 不要因为 “一个数字不能出现负值” 这样的原因而使用无符号整型
# 宏定义
  • 避免定义宏,禁止使用宏定义 C++ API
  • 对于常量,用 const 变量代替,对于缩写,用引用代替,总之,尽可能避免使用宏定义
# 类型推导
  • 当且仅当类型推导使得代码变得更清晰和安全的情况下,使用类型推导
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    std::unique_ptr<WidgetWithBellsAndWhistles> widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
    absl::flat_hash_map<std::string, std::unique_ptr<WidgetWithBellsAndWhistles>>::const_iterator
    it = my_map_.find(key);
    std::array<int, 6> numbers = {4, 8, 15, 16, 23, 42};


    //it's ok and better below:
    auto widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
    auto it = my_map_.find(key);
    std::array numbers = {4, 8, 15, 16, 23, 42};

# 命名

  • 减少使用缩写,命名的描述性(详细程度)应该与该命名在作用域的可见性成正比
    (例如,在一个只有 5 行的代码中,变量名 n 可以被接受,但如果是在一个大段代码的类中,则 n 有点显得过于模糊了)

一些 “常见的” 缩写可以被接受,比如用 i 来表示迭代(iteration)次数,用 T 来表示模板参数(template parameter)

# 文件名的命名规范

全小写,单词之间以下划线( _ )或破折号( - )进行连接,优先考虑下划线( _

1
2
my_useful_class.cpp
my_useful_class.h

# 类型名的命名规范

以大写字母开头,每个单词首字母大写,不带任何下划线:

1
2
3
4
5
6
7
8
9
10
11
12
13
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;

// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;

// enums
enum class UrlTableError { ...

这里我们所说的类型,包括了 classes、structs、type aliases、enums 和 type template parameters

# 变量名的命名规范

采用蛇形命名法,全小写,每个单词之间用下划线( _ )进行连接
特殊地,对于类内的类成员变量,在变量名的前面加一个前缀的下划线( _
(在谷歌命名规范中,它们采用后缀下划线,但我 prefer 前缀下划线)

1
2
3
4
5
6
7
8
std::string table_name;

class TableInfo {
...
private:
std::string _table_name;
static Pool<TableInfo>* _pool;
};

# 常量名的命名规范

对于用 const 和 constexpr 描述的变量,或其值在程序的整个生存期保持固定不变的变量,采用 “全大写 + 蛇形命名” 的形式:
(在谷歌规范中,用一个小写的前缀 “k” 开头,然后其余每个单词首字母大写。个人不赞同不喜欢这种风格):

1
2
3
4
5
constexpr uint32_t MAX_FRAME_WIDTH = 640;
constexpr uint32_t MAX_FRAME_HEIGHT = 480;
constexpr uint16_t MAX_PHASE = 4095;
constexpr int RGB_FRAME_WIDTH = 1920;
constexpr int RGB_FRAME_HEIGHT = 1080;

谷歌风格常量:

1
2
3
const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24; // Android 8.0.0
//特殊地,对于无法用过首字母大写进行分隔的情况,允许使用下划线,如上面的数字 8.0.0 的情况

# 枚举类型的命名规范

枚举类型本质上也是一种常量,所以其命名规则同常量名,小写字母 “k”+ 每个单词首字母大写。

# 命名空间的命名规范

在谷歌规范中,其对命名空间的约定为:全小写,单词以下划线分隔。
但我不喜欢这种风格,更偏向于同类型名,以大写字母开头,每个单词首字母大写,不带任何下划线

# 注释

  • 好的代码自我阐释。

  • 函数定义时最好在函数前添加注释,用于形容函数的作用以及用法

  • 对于函数的参数,如果是指针类型,可以备注其是否允许为空指针,以及如果是会怎样

  • 对于类内成员,可以添加注释形容某个变量的默认值 / 初始化值的取值原因 or 含义:

    1
    2
    3
    4
    private:
    // Used to bounds-check table accesses. -1 means
    // that we don't yet know how many entries the table has.
    int num_total_entries_;

  • 对于 TODO 类型的注释,其用于描述临时解决方案、短期解决方案、足够好但不够完美的方案。

  • TODO 类型的注释以全大写的 “TODO” 开头,其后跟随 bug ID、姓名、邮箱地址以及问题描述。

  • TODO 最好包含一个 “什么时候做某事” 的截止日期或具体的事件(时机)

# 格式

# 基本规范
  • 对于每行代码的长度,尽量不超过 80 个字符(不要有单行过长的代码)
  • 尽可能避免使用非 ASCII 的字符(包括但不限于中文字符(中文注释))
  • 采用 4 个空格作为缩进,不使用 tabs,或者在编辑器中将 tab 设置为输出 4 个空格
    (原文采用的是 2 个空格作为缩进,但我不喜欢)
  • 每行代码行末不应该尾随多余的空格,对于行末的注释,间隔两个空格之后开始,注释内容与 // 之间留一个空格
    1
    int i = 0;  // Two spaces before end-of-line comments.
# 对于函数定义
  • 返回值与函数名处于同一行,函数参数尽可能也与函数名处于同一行。
  • 左花括号处于一行的行末,与前面的内容之间存在一个空格。
  • 如果参数过多,则可适当分行,第一个参数保留在与函数名同一行的位置,后续参数则与第一个参数对齐。
    1
    2
    3
    4
    ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
    DoSomething();
    ...
    }

由于这种换行方式,为了避免换行的函数参数与函数体内容混淆,则在这种情况下,把花括号另起一行:

1
2
3
4
5
6
7
ReturnType ClassName::ReallyLongFunctionName(Type par_name1,
Type par_name2,
Type par_name3)
{
DoSomething();
...
}

# 对于函数调用

与函数定义的情况基本一致,如果函数传参过多,则换行,第一个参数与函数名同一行,其余参数换行后与第一个参数对齐:

1
2
bool result = DoSomething(averyveryveryverylongargument1,
argument2, argument3, argument4);

# 对于分支与循环语句
  • if、while 和 for 之后紧跟左圆括号,不留空格
  • else、else if 前后各留一个空格
    (这里与原文的 preference 略有偏差,我 prefer 在 if、while、for 和条件之间不希望有空格)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    if(condition) {
    DoOneThing();
    DoAnotherThing();
    } else if (int a = f(); a != 3) { //分号之后要有空格隔开
    DoAThirdThing(a);
    } else {
    DoNothing();
    }

    // Good - the same rules apply to loops.
    while(condition) {
    RepeatAThing();
    }

    // Good - the same rules apply to loops.
    do {
    RepeatAThing();
    } while (condition);

    // Good - the same rules apply to loops.
    for(int i = 0; i < 10; ++i) { //分号之后要有空格隔开
    RepeatAThing();
    }

以下情况不被鼓励:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(condition)
foo;
else {
bar;
}


if(x) DoThis();
else DoThat();


if(condition)
DoSomething();


if(x == kFoo) { return new Foo(); } //仅当我们定义 getter 或 setter 函数时鼓励这种写法

# 对于空循环

1
2
3
4
5
while(condition) {}  // Good - `{}` indicates no logic.
while(condition) {
// Comments are okay, too
}
while(condition) continue; // Good - `continue` indicates no logic.

以下情况不被鼓励:

1
while(condition);  // Bad - looks like part of `do-while` loop.

# 对于指针和引用

采用 “尾随空格” 的形式,即变量类型紧跟星号 * 或引用符 & ,之前不含空格,其后再与变量名之间填充一个空格:
(特殊地,在 <> 表示的模板类或类型转换中,指针或引用不需要任何空格)

1
2
3
4
5
char* c;
const std::string& str;
int* GetPointer();
std::vector<char*> // Note no space between '*' and '>'
y = static_cast<char*>(x);

另外,当定义指针和引用时,为了避免混淆和误导,不允许在单行定义多个变量:

1
2
3
//以下情况被强烈禁止
int x, *y; // Disallowed - no & or * in multiple declaration
int* x, *y; // Disallowed - no & or * in multiple declaration; inconsistent spacing

# 对于条件

对于条件语句和循环语句中的条件表达式,如果存在多个条件,则可适当进行换行,每行以条件的逻辑运算符(如 &&|| )结尾,(大致上与函数定义的逻辑一致):

1
2
3
4
5
6
if(this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one)
{
...
}

# 对于预处理指令

预处理指令顶格书写,不允许缩进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Good - directives at beginning of line
if (lopsided_score) {
#if DISASTER_PENDING // Correct -- Starts at beginning of line
DropEverything();
NotifyClient();
#endif
BackToNormal();
}


// Bad - indented directives
if (lopsided_score) {
#if DISASTER_PENDING // Wrong! The "#if" should be at beginning of line
DropEverything();
#endif // Wrong! Do not indent "#endif"
BackToNormal();
}

# 对于类内的格式

采用 public、protected、private 的顺序进行代码书写,这三个关键字不缩进(谷歌规范则缩进一个空格,我不喜欢)

# 对于命名空间

其内容不应该 / 不需要因此而缩进一级:

1
2
3
4
5
6
7
namespace {

void foo() { // Correct. No extra indentation within namespace.
...
}

} // namespace

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +《Google C++ Style Guide》note - C++ - 编程 | R1ck's Playground = how

参考链接:Google C++ Style Guide

# 头文件

  • 每个 .cpp 源文件都应该有一个相对应的 .h 头文件
  • 每个头文件都应该有头文件守卫,守卫格式统一为: _<PROJECT>_<PATH>_<FILE>_H_
  • 对于头文件中对于 inline 内联函数的定义,要求函数体在十行以内,且不包括循环、switch 语句。
  • 头文件的 include 顺序:相关头文件、C 系统头文件、C++ 头文件、其他库头文件、项目头文件
    (这里的 “相关头文件” 指的是与该源文件同名的头文件)

# 作用域

  • 除了少数例外,将代码放置在命名空间中,以减少命名冲突

  • 不要使用 using namespace xxx 来导入命名空间中的内容(会污染命名空间)

    1
    2
    // Forbidden -- This pollutes the namespace.
    using namespace foo;

  • 当源文件中的定义不需要被其他文件所引用时,通过未命名命名空间或 static 来赋予其内部链接性

    1
    2
    3
    namespace {
    ...
    } // namespace

  • 用注释来结束命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // In the .h file
    namespace mynamespace {

    // All declarations are within the namespace scope.
    // Notice the lack of indentation.
    class MyClass {
    public:
    ...
    void Foo();
    };

    } // namespace mynamespace

# 对于局部变量
  • 将其放在尽可能小的范围内,并在声明中初始化变量。

  • 定义局部变量时尽可能地靠近其第一次被使用的地方,便于读者找到定义并了解其初始值

    1
    2
    3
    4
    5
    6
    7
    int jobs = NumJobs();
    // More code...
    f(jobs); // Bad -- declaration separate from use.


    int jobs = NumJobs();
    f(jobs); // Good -- declaration immediately (or closely) followed by use.

  • 局部变量应该在定义时初始化,而不是先定义后初始化:

    1
    2
    3
    4
    5
    int i;
    i = f(); // Bad -- initialization separate from declaration.


    int i = f(); // Good -- declaration has initialization.

1
2
3
4
5
6
std::vector<int> v;
v.push_back(1); // Prefer initializing using brace initialization.
v.push_back(2);


std::vector<int> v = {1, 2}; // Good -- v starts initialized.

#

  • 避免在类的构造函数中调用虚函数,详见虚函数与构造函数
  • 避免隐式转换,尽可能地使用 explicit 关键字来修饰单一参数的函数(拷贝构造和移动构造除外)
  • 只对携带数据的被动对象( passive objects )使用 struct ,其他一律使用 class
  • 尽可能地考虑组合,而不是继承。当使用继承时,优先公有继承
  • 对于纯抽象类,其派生类的继承属于 “接口继承”,其他情况则都是 “实现继承”。
  • 强烈不建议多实现继承
  • 类的成员变量应该是私有的

其中,"passive objects" 指的是那些仅仅用于存储数据而不包含任何行为的对象。换句话说,它们是纯粹的数据结构,用于在程序中传递数据,而不负责执行任何操作或行为。通常,这些对象仅包含公共数据成员和对这些数据成员进行初始化、访问和设置的方法,而不包含任何其他的逻辑或操作。

这种对象通常用于数据传递和数据存储,而不涉及到具体的行为。它们被设计为尽可能简单和轻量,只提供了对数据的简单操作,例如读取和写入。因此,当文本中提到 "passive objects" 时,意思是将结构体(struct)仅用于这种数据容器的情况。

另外,在 C++ 中,“多实现继承” 指的是一个类同时从多个基类继承属性和方法,可能会导致 菱形继承 问题。

# 函数

  • 对于函数的输出,优先使用返回值进行传递,而不是使用输出参数
  • 对于函数的返回值,优先返回值(return by value),而不是返回引用(return by reference)
  • 避免返回裸指针,除非它可以为 nullptr
  • 对于函数的输入参数,应该采用值传递或者 const 的引用传递(pass by value, or pass by reference of const)
  • 对于函数的输出参数和输入输出参数,应该采用引用传递
  • 对于函数参数的顺序,优先输入参数,然后是输出参数
  • 函数体尽可能短小(40 行左右),一个函数就应该集中做某件事情

一个函数应该避免依赖某个引用参数在整个函数调用期间的存活(Avoid defining functions that require a reference parameter to outlive the call.)即,某个引用参数的存活与否,不应该影响该函数的正常运作,其不会因为某个引用参数在函数调用过程中不再存活而出错。

对于函数的返回值,存在两种返回语法,一种是普通的、常见的,返回类型出现在函数名之前;而另一种则是在函数名之前用 auto 进行占位,而真正的返回值则尾随函数的参数列表(这种语法我们称之为 Trailing Return Type Syntax ):

1
auto foo(int x) -> int;

对于 Trailing Return Type Syntax ,其于普通的返回形式不同之处在于,这种情况下返回类型存在于函数体的作用域中,则对于以下情况能够有较好的表现:

1
2
3
4
5
6
7
//普通的“前置”返回
template <typename T, typename U>
decltype(declval<T&>() + declval<U&>()) add(T t, U u);

//“后置”返回
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u);

总得来说,在大多数情况下,我们优先考虑普通的 “前置” 返回,而当我们在处理涉及 lambda 表达式时,或者当将函数的返回类型后置能够带来更好的阅读体验时,我们采用 “后置” 的 Trailing Return Type Syntax

# 所有权与智能指针

  • 首选使用 std::unique_ptr 来明确所有权转移
  • 转移所有权比 “借用” 指针或引用更方便,它减少了用户之间对于内存对象的生存期的协同。
    (所有权明确了谁改对内存的生存期负责,明确了谁该最终释放内存,而单纯地指针拷贝、借用则会混淆这一点)

如果动态分配是必要的,最好将所有权闲置 / 保留在分配它的代码中。此后,如果其他代码需要访问对象,考虑向其传递一个副本,或者传递一个指针或引用而不转移所有权。(我猜这样做的目的是,将所有权保留在原先分配它的内存的位置,让原 own 负责维护其生存期,进而避免所有权的混乱。)

# C++ 特性

# 类型转换
  • 使用 C++ 风格的类型转换( static_castconst_castreinterpret_cast )而不是 C 风格的类型转换
  • 不要使用 C 风格的转换(例如 (int)x ) 除非转换的目标类型是 void
  • 当且仅当 T 是一个类类型时使用 T(x) 这种转换形式
  • 使用大括号初始化来转换算术类型(例如 int64_t{x}
  • static_cast 进行类类型的向上、向下转换
  • const_cast 来移除 const 限定词
  • reinterpret_cast 来进行不安全的类型转换,当且仅当你知道你在干什么的时候使用
# 其他
  • 优先使用前置形式的自增、自减运算符 ++-- ,(即 ++i--i )除非你需要用到后置自增 / 自减的结果。
  • 优先使用 using 语法而不是 typedef 语法进行别名
  • 对于空指针,使用 nullptr ,对于空字符(串),使用 \0
  • 优先考虑使用 sizeof (变量名) 而不是 sizeof (类型名)
    原因是 sizeof (变量名) 会随着类型的改变而进行适当的更新
    1
    2
    3
    4
    MyStruct data;
    memset(&data, 0, sizeof(data)); //good

    memset(&data, 0, sizeof(MyStruct)); //bad
# const 限定
  • 在 API 中,尽可能地(合理)使用 const,用于修饰类函数、函数传参
  • 对于以值传递的函数参数,加 const 修饰无意义,因为它不会影响到调用者传入的数据
  • 对于 const 修饰符,使用前置的书写形式,即 const int* foo
# 无符号整型
  • 尽可能地不要使用无符号整型(unsigned integer)诸如 uint32_t ,除非有一个合理有效的理由:用于表示位图
  • 不要因为 “一个数字不能出现负值” 这样的原因而使用无符号整型
# 宏定义
  • 避免定义宏,禁止使用宏定义 C++ API
  • 对于常量,用 const 变量代替,对于缩写,用引用代替,总之,尽可能避免使用宏定义
# 类型推导
  • 当且仅当类型推导使得代码变得更清晰和安全的情况下,使用类型推导
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    std::unique_ptr<WidgetWithBellsAndWhistles> widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
    absl::flat_hash_map<std::string, std::unique_ptr<WidgetWithBellsAndWhistles>>::const_iterator
    it = my_map_.find(key);
    std::array<int, 6> numbers = {4, 8, 15, 16, 23, 42};


    //it's ok and better below:
    auto widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
    auto it = my_map_.find(key);
    std::array numbers = {4, 8, 15, 16, 23, 42};

# 命名

  • 减少使用缩写,命名的描述性(详细程度)应该与该命名在作用域的可见性成正比
    (例如,在一个只有 5 行的代码中,变量名 n 可以被接受,但如果是在一个大段代码的类中,则 n 有点显得过于模糊了)

一些 “常见的” 缩写可以被接受,比如用 i 来表示迭代(iteration)次数,用 T 来表示模板参数(template parameter)

# 文件名的命名规范

全小写,单词之间以下划线( _ )或破折号( - )进行连接,优先考虑下划线( _

1
2
my_useful_class.cpp
my_useful_class.h

# 类型名的命名规范

以大写字母开头,每个单词首字母大写,不带任何下划线:

1
2
3
4
5
6
7
8
9
10
11
12
13
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;

// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;

// enums
enum class UrlTableError { ...

这里我们所说的类型,包括了 classes、structs、type aliases、enums 和 type template parameters

# 变量名的命名规范

采用蛇形命名法,全小写,每个单词之间用下划线( _ )进行连接
特殊地,对于类内的类成员变量,在变量名的前面加一个前缀的下划线( _
(在谷歌命名规范中,它们采用后缀下划线,但我 prefer 前缀下划线)

1
2
3
4
5
6
7
8
std::string table_name;

class TableInfo {
...
private:
std::string _table_name;
static Pool<TableInfo>* _pool;
};

# 常量名的命名规范

对于用 const 和 constexpr 描述的变量,或其值在程序的整个生存期保持固定不变的变量,采用 “全大写 + 蛇形命名” 的形式:
(在谷歌规范中,用一个小写的前缀 “k” 开头,然后其余每个单词首字母大写。个人不赞同不喜欢这种风格):

1
2
3
4
5
constexpr uint32_t MAX_FRAME_WIDTH = 640;
constexpr uint32_t MAX_FRAME_HEIGHT = 480;
constexpr uint16_t MAX_PHASE = 4095;
constexpr int RGB_FRAME_WIDTH = 1920;
constexpr int RGB_FRAME_HEIGHT = 1080;

谷歌风格常量:

1
2
3
const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24; // Android 8.0.0
//特殊地,对于无法用过首字母大写进行分隔的情况,允许使用下划线,如上面的数字 8.0.0 的情况

# 枚举类型的命名规范

枚举类型本质上也是一种常量,所以其命名规则同常量名,小写字母 “k”+ 每个单词首字母大写。

# 命名空间的命名规范

在谷歌规范中,其对命名空间的约定为:全小写,单词以下划线分隔。
但我不喜欢这种风格,更偏向于同类型名,以大写字母开头,每个单词首字母大写,不带任何下划线

# 注释

  • 好的代码自我阐释。

  • 函数定义时最好在函数前添加注释,用于形容函数的作用以及用法

  • 对于函数的参数,如果是指针类型,可以备注其是否允许为空指针,以及如果是会怎样

  • 对于类内成员,可以添加注释形容某个变量的默认值 / 初始化值的取值原因 or 含义:

    1
    2
    3
    4
    private:
    // Used to bounds-check table accesses. -1 means
    // that we don't yet know how many entries the table has.
    int num_total_entries_;

  • 对于 TODO 类型的注释,其用于描述临时解决方案、短期解决方案、足够好但不够完美的方案。

  • TODO 类型的注释以全大写的 “TODO” 开头,其后跟随 bug ID、姓名、邮箱地址以及问题描述。

  • TODO 最好包含一个 “什么时候做某事” 的截止日期或具体的事件(时机)

# 格式

# 基本规范
  • 对于每行代码的长度,尽量不超过 80 个字符(不要有单行过长的代码)
  • 尽可能避免使用非 ASCII 的字符(包括但不限于中文字符(中文注释))
  • 采用 4 个空格作为缩进,不使用 tabs,或者在编辑器中将 tab 设置为输出 4 个空格
    (原文采用的是 2 个空格作为缩进,但我不喜欢)
  • 每行代码行末不应该尾随多余的空格,对于行末的注释,间隔两个空格之后开始,注释内容与 // 之间留一个空格
    1
    int i = 0;  // Two spaces before end-of-line comments.
# 对于函数定义
  • 返回值与函数名处于同一行,函数参数尽可能也与函数名处于同一行。
  • 左花括号处于一行的行末,与前面的内容之间存在一个空格。
  • 如果参数过多,则可适当分行,第一个参数保留在与函数名同一行的位置,后续参数则与第一个参数对齐。
    1
    2
    3
    4
    ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
    DoSomething();
    ...
    }

由于这种换行方式,为了避免换行的函数参数与函数体内容混淆,则在这种情况下,把花括号另起一行:

1
2
3
4
5
6
7
ReturnType ClassName::ReallyLongFunctionName(Type par_name1,
Type par_name2,
Type par_name3)
{
DoSomething();
...
}

# 对于函数调用

与函数定义的情况基本一致,如果函数传参过多,则换行,第一个参数与函数名同一行,其余参数换行后与第一个参数对齐:

1
2
bool result = DoSomething(averyveryveryverylongargument1,
argument2, argument3, argument4);

# 对于分支与循环语句
  • if、while 和 for 之后紧跟左圆括号,不留空格
  • else、else if 前后各留一个空格
    (这里与原文的 preference 略有偏差,我 prefer 在 if、while、for 和条件之间不希望有空格)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    if(condition) {
    DoOneThing();
    DoAnotherThing();
    } else if (int a = f(); a != 3) { //分号之后要有空格隔开
    DoAThirdThing(a);
    } else {
    DoNothing();
    }

    // Good - the same rules apply to loops.
    while(condition) {
    RepeatAThing();
    }

    // Good - the same rules apply to loops.
    do {
    RepeatAThing();
    } while (condition);

    // Good - the same rules apply to loops.
    for(int i = 0; i < 10; ++i) { //分号之后要有空格隔开
    RepeatAThing();
    }

以下情况不被鼓励:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(condition)
foo;
else {
bar;
}


if(x) DoThis();
else DoThat();


if(condition)
DoSomething();


if(x == kFoo) { return new Foo(); } //仅当我们定义 getter 或 setter 函数时鼓励这种写法

# 对于空循环

1
2
3
4
5
while(condition) {}  // Good - `{}` indicates no logic.
while(condition) {
// Comments are okay, too
}
while(condition) continue; // Good - `continue` indicates no logic.

以下情况不被鼓励:

1
while(condition);  // Bad - looks like part of `do-while` loop.

# 对于指针和引用

采用 “尾随空格” 的形式,即变量类型紧跟星号 * 或引用符 & ,之前不含空格,其后再与变量名之间填充一个空格:
(特殊地,在 <> 表示的模板类或类型转换中,指针或引用不需要任何空格)

1
2
3
4
5
char* c;
const std::string& str;
int* GetPointer();
std::vector<char*> // Note no space between '*' and '>'
y = static_cast<char*>(x);

另外,当定义指针和引用时,为了避免混淆和误导,不允许在单行定义多个变量:

1
2
3
//以下情况被强烈禁止
int x, *y; // Disallowed - no & or * in multiple declaration
int* x, *y; // Disallowed - no & or * in multiple declaration; inconsistent spacing

# 对于条件

对于条件语句和循环语句中的条件表达式,如果存在多个条件,则可适当进行换行,每行以条件的逻辑运算符(如 &&|| )结尾,(大致上与函数定义的逻辑一致):

1
2
3
4
5
6
if(this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one)
{
...
}

# 对于预处理指令

预处理指令顶格书写,不允许缩进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Good - directives at beginning of line
if (lopsided_score) {
#if DISASTER_PENDING // Correct -- Starts at beginning of line
DropEverything();
NotifyClient();
#endif
BackToNormal();
}


// Bad - indented directives
if (lopsided_score) {
#if DISASTER_PENDING // Wrong! The "#if" should be at beginning of line
DropEverything();
#endif // Wrong! Do not indent "#endif"
BackToNormal();
}

# 对于类内的格式

采用 public、protected、private 的顺序进行代码书写,这三个关键字不缩进(谷歌规范则缩进一个空格,我不喜欢)

# 对于命名空间

其内容不应该 / 不需要因此而缩进一级:

1
2
3
4
5
6
7
namespace {

void foo() { // Correct. No extra indentation within namespace.
...
}

} // namespace

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/2024/10/21/\345\217\257\345\217\230\345\217\202\346\225\260\346\250\241\346\235\277/index.html" "b/2024/10/21/\345\217\257\345\217\230\345\217\202\346\225\260\346\250\241\346\235\277/index.html" index c6c028f..8ddfa74 100644 --- "a/2024/10/21/\345\217\257\345\217\230\345\217\202\346\225\260\346\250\241\346\235\277/index.html" +++ "b/2024/10/21/\345\217\257\345\217\230\345\217\202\346\225\260\346\250\241\346\235\277/index.html" @@ -1 +1 @@ -可变参数模板 - C++ - 编程 | R1ck's Playground = how

# 一、可变参数模板

#CPP11

# 1. 概述

C++ 的可变参数模板(Variadic Templates)是一种强大的特性,使得模板能够接受可变数量的参数。这一特性引入于 C++11,允许程序员创建更加灵活和通用的代码。

可变参数模板使得函数模板和类模板能够接受任意数量的模板参数,无论是类型参数还是非类型参数。它们为泛型编程提供了更大的灵活性。

# 2. 基本语法

可以通过使用省略号 ... 来定义参数包。

  • 函数模板

    1
    2
    3
    4
    template<typename... Args>
    void func(Args... args) {
    // 处理参数
    }

  • 类模板:同理。

    1
    2
    3
    4
    5
    6
    7
    template<typename... Args>
    class MyClass {
    public:
    MyClass(Args... args) {
    // 构造函数处理
    }
    };

# 3. 参数展开

展开参数包(pack expansion)是将模板参数包展开成单个参数序列。

在函数模板或类模板中,可以使用参数包展开来处理传入的参数。展开的方式通常使用以下语法:

  • 在函数参数中展开:直接使用 args... 。(见下文 [[可变参数模板 #5. 递归模板 | 递归模板]] 的举例)
  • 在函数体中展开:使用 (..., expression) 的形式。(即,使用折叠表达式进行展开)

1
2
3
4
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // 使用折叠表达式展开参数包
}

# 4. 示例

以下是一个简单的示例,演示如何使用模板参数包:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

template<typename... Args>
void print(Args... args) {
// 逐个打印参数
(std::cout << ... << args) << std::endl; // 展开并输出
}

int main() {
print(1, 2.5, "Hello", 'A'); // 可以传入不同类型的参数
return 0;
}

# 5. 递归模板

可以通过递归模板实现对参数包的处理。例如,计算参数包中所有数值的和:

1
2
3
4
5
6
7
8
9
template<typename T>
T sum(T value) {
return value; // 基础情况
}

template<typename T, typename... Args>
T sum(T value, Args... args) {
return value + sum(args...); // 递归展开
}

# 6. 使用非类型模板参数

除了类型参数,模板参数包也可以使用非类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<int... Values>
class IntPack {
public:
static void print() {
((std::cout << Values << ' '), ...); // 输出所有值
std::cout << std::endl;
}
};

int main() {
IntPack<1, 2, 3>::print(); // 输出 1 2 3
return 0;
}

# 7. 结合其他特性

模板参数包可以与其他 C++ 特性结合使用,如 SFINAE(Substitution Failure Is Not An Error)和类型推导,使得编写更加复杂的泛型代码变得可能。

# 二、折叠表达式

#CPP17

# 1. 基本概念

折叠表达式(Fold Expression)是 C++17 引入的一种特性,用于简化对可变参数模板的操作,尤其是在处理参数包时。它允许你对参数包中的元素进行简单的折叠运算(例如加法、乘法、逻辑运算等),减少了编写递归模板的需要。

折叠表达式是通过指定一个二元操作符(如 +*&& 等),对参数包中的所有元素进行运算的简洁方式。

# 2. 语法

折叠表达式有四种主要形式:

  1. 一元右折叠(Unary Right Fold): ( ... op pack )
  2. 一元左折叠(Unary Left Fold): ( pack op ... )
  3. 二元右折叠(Binary Right Fold): ( init op ... op pack )
  4. 二元左折叠(Binary Left Fold): ( pack op ... op init )

具体地,有:

  1. 一元右折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (args + ...); // 等价于 (((args1 + args2) + args3) + ...)
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

  2. 一元左折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (... + args); // 等价于 (... + (args1 + (args2 + (args3 + ...))))
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

  3. 二元右折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (0 + ... + args); // 初始化值为0,等价于 (0 + ((args1 + args2) + args3) + ...)
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

  4. 二元左折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (args + ... + 0); // 初始化值为0,等价于 (... + (args1 + (args2 + (args3 + ... + 0))))
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

# 三、示例代码与解析

结合模板参数包和折叠表达式,我们可以编写出非常简洁和强大的模板函数。

例如,一个同时支持不同数据类型的打印函数:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n'; // 使用折叠表达式展开参数包
}

int main() {
print(1, 2, 3); // 输出: 1 2 3
print("Hello", " ", "world!"); // 输出: Hello world!
}

这里我们逐行解释上面的模板函数 print,并详细说明模板参数包和折叠表达式的工作原理。

  1. 模板声明

    1
    template<typename... Args>

    这一行代码声明了一个模板,其中 Args 是一个模板参数包。 typename... Args 表示 Args 可以接受任意数量的模板参数。

  2. 函数定义

    1
    void print(Args... args) {

这一行定义了一个名为 print 的函数。 Args... args 表示 args 是一个函数参数包,对应于模板参数包 Args 。每个 Args 中的类型对应于一个 args 中的参数。

  1. 折叠表达式
    1
    (std::cout << ... << args) << '\n';

    这一行是函数体,使用了折叠表达式来输出所有参数,并在末尾输出一个换行符。
更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +可变参数模板 - C++ - 编程 | R1ck's Playground = how

# 一、可变参数模板

#CPP11

# 1. 概述

C++ 的可变参数模板(Variadic Templates)是一种强大的特性,使得模板能够接受可变数量的参数。这一特性引入于 C++11,允许程序员创建更加灵活和通用的代码。

可变参数模板使得函数模板和类模板能够接受任意数量的模板参数,无论是类型参数还是非类型参数。它们为泛型编程提供了更大的灵活性。

# 2. 基本语法

可以通过使用省略号 ... 来定义参数包。

  • 函数模板

    1
    2
    3
    4
    template<typename... Args>
    void func(Args... args) {
    // 处理参数
    }

  • 类模板:同理。

    1
    2
    3
    4
    5
    6
    7
    template<typename... Args>
    class MyClass {
    public:
    MyClass(Args... args) {
    // 构造函数处理
    }
    };

# 3. 参数展开

展开参数包(pack expansion)是将模板参数包展开成单个参数序列。

在函数模板或类模板中,可以使用参数包展开来处理传入的参数。展开的方式通常使用以下语法:

  • 在函数参数中展开:直接使用 args... 。(见下文 [[可变参数模板 #5. 递归模板 | 递归模板]] 的举例)
  • 在函数体中展开:使用 (..., expression) 的形式。(即,使用折叠表达式进行展开)

1
2
3
4
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // 使用折叠表达式展开参数包
}

# 4. 示例

以下是一个简单的示例,演示如何使用模板参数包:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

template<typename... Args>
void print(Args... args) {
// 逐个打印参数
(std::cout << ... << args) << std::endl; // 展开并输出
}

int main() {
print(1, 2.5, "Hello", 'A'); // 可以传入不同类型的参数
return 0;
}

# 5. 递归模板

可以通过递归模板实现对参数包的处理。例如,计算参数包中所有数值的和:

1
2
3
4
5
6
7
8
9
template<typename T>
T sum(T value) {
return value; // 基础情况
}

template<typename T, typename... Args>
T sum(T value, Args... args) {
return value + sum(args...); // 递归展开
}

# 6. 使用非类型模板参数

除了类型参数,模板参数包也可以使用非类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<int... Values>
class IntPack {
public:
static void print() {
((std::cout << Values << ' '), ...); // 输出所有值
std::cout << std::endl;
}
};

int main() {
IntPack<1, 2, 3>::print(); // 输出 1 2 3
return 0;
}

# 7. 结合其他特性

模板参数包可以与其他 C++ 特性结合使用,如 SFINAE(Substitution Failure Is Not An Error)和类型推导,使得编写更加复杂的泛型代码变得可能。

# 二、折叠表达式

#CPP17

# 1. 基本概念

折叠表达式(Fold Expression)是 C++17 引入的一种特性,用于简化对可变参数模板的操作,尤其是在处理参数包时。它允许你对参数包中的元素进行简单的折叠运算(例如加法、乘法、逻辑运算等),减少了编写递归模板的需要。

折叠表达式是通过指定一个二元操作符(如 +*&& 等),对参数包中的所有元素进行运算的简洁方式。

# 2. 语法

折叠表达式有四种主要形式:

  1. 一元右折叠(Unary Right Fold): ( ... op pack )
  2. 一元左折叠(Unary Left Fold): ( pack op ... )
  3. 二元右折叠(Binary Right Fold): ( init op ... op pack )
  4. 二元左折叠(Binary Left Fold): ( pack op ... op init )

具体地,有:

  1. 一元右折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (args + ...); // 等价于 (((args1 + args2) + args3) + ...)
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

  2. 一元左折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (... + args); // 等价于 (... + (args1 + (args2 + (args3 + ...))))
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

  3. 二元右折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (0 + ... + args); // 初始化值为0,等价于 (0 + ((args1 + args2) + args3) + ...)
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

  4. 二元左折叠

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename... Args>
    auto sum(Args... args) {
    return (args + ... + 0); // 初始化值为0,等价于 (... + (args1 + (args2 + (args3 + ... + 0))))
    }

    int main() {
    std::cout << sum(1, 2, 3, 4); // 输出: 10
    }

# 三、示例代码与解析

结合模板参数包和折叠表达式,我们可以编写出非常简洁和强大的模板函数。

例如,一个同时支持不同数据类型的打印函数:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n'; // 使用折叠表达式展开参数包
}

int main() {
print(1, 2, 3); // 输出: 1 2 3
print("Hello", " ", "world!"); // 输出: Hello world!
}

这里我们逐行解释上面的模板函数 print,并详细说明模板参数包和折叠表达式的工作原理。

  1. 模板声明

    1
    template<typename... Args>

    这一行代码声明了一个模板,其中 Args 是一个模板参数包。 typename... Args 表示 Args 可以接受任意数量的模板参数。

  2. 函数定义

    1
    void print(Args... args) {

这一行定义了一个名为 print 的函数。 Args... args 表示 args 是一个函数参数包,对应于模板参数包 Args 。每个 Args 中的类型对应于一个 args 中的参数。

  1. 折叠表达式
    1
    (std::cout << ... << args) << '\n';

    这一行是函数体,使用了折叠表达式来输出所有参数,并在末尾输出一个换行符。
更新于

请我喝杯咖啡吧~

Rick how 微信支付

微信支付

Rick how 支付宝

支付宝

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/404.html b/404.html index a341f36..aec4d39 100644 --- a/404.html +++ b/404.html @@ -1 +1 @@ -不见啦 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +不见啦 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/about/index.html b/about/index.html index f09e1b0..1c72275 100644 --- a/about/index.html +++ b/about/index.html @@ -1,4 +1,4 @@ -my BIO | R1ck's Playground = how

# 关于本站

#

一个助我唤起前世记忆的疗愈中心。
+my BIO | R1ck's Playground = how

# 关于本站

#

一个助我唤起前世记忆的疗愈中心。
                                                                         —— 阿 how 的精神病房
 

#

截至目前为止,作者仍然无法明确本站建立的目的和意义 —— “先上线再说吧” :)
 本站的笔记均来节选自我的 PKM,由于对于本站的定位尚不清晰,后续或许会将这些笔记移除,且看且珍惜
diff --git a/archives/2023/12/index.html b/archives/2023/12/index.html
index 79328f1..f89a424 100644
--- a/archives/2023/12/index.html
+++ b/archives/2023/12/index.html
@@ -1 +1 @@
-2023 年 / 12 月 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +2023 年 / 12 月 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/archives/2023/index.html b/archives/2023/index.html index 1a41bf5..5e9726d 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -1 +1 @@ -2023 年 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +2023 年 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/archives/2024/10/index.html b/archives/2024/10/index.html index c64af32..d3ee93c 100644 --- a/archives/2024/10/index.html +++ b/archives/2024/10/index.html @@ -1 +1 @@ -2024 年 / 10 月 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +2024 年 / 10 月 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/archives/2024/10/page/2/index.html b/archives/2024/10/page/2/index.html index ffbdf71..6f4ff80 100644 --- a/archives/2024/10/page/2/index.html +++ b/archives/2024/10/page/2/index.html @@ -1 +1 @@ -2024 年 / 10 月 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +2024 年 / 10 月 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/archives/2024/index.html b/archives/2024/index.html index c3bc80b..205fcd2 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -1 +1 @@ -2024 年 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +2024 年 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/archives/2024/page/2/index.html b/archives/2024/page/2/index.html index dacc0b7..2ce0c75 100644 --- a/archives/2024/page/2/index.html +++ b/archives/2024/page/2/index.html @@ -1 +1 @@ -2024 年 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +2024 年 - 归档 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/archives/index.html b/archives/index.html index 6cc1728..5a071e0 100644 --- a/archives/index.html +++ b/archives/index.html @@ -1 +1 @@ -归档 | R1ck's Playground = how

首页 / 目前共计 19 篇文章。 嗯.. 继续努力。

2024 年/10 月 ( 18 )

2023 年/12 月 ( 1 )

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +归档 | R1ck's Playground = how

首页 / 目前共计 19 篇文章。 嗯.. 继续努力。

2024 年/10 月 ( 18 )

2023 年/12 月 ( 1 )

170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/categories/index.html b/categories/index.html index e76e10f..2998013 100644 --- a/categories/index.html +++ b/categories/index.html @@ -1 +1 @@ -全部分类 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +全部分类 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213/C/index.html" "b/categories/\347\274\226\347\250\213/C/index.html" index 3ca9ddf..75758ea 100644 --- "a/categories/\347\274\226\347\250\213/C/index.html" +++ "b/categories/\347\274\226\347\250\213/C/index.html" @@ -1 +1 @@ -分类:C++ | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +分类:C++ | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213/C/page/2/index.html" "b/categories/\347\274\226\347\250\213/C/page/2/index.html" index fc68311..930b204 100644 --- "a/categories/\347\274\226\347\250\213/C/page/2/index.html" +++ "b/categories/\347\274\226\347\250\213/C/page/2/index.html" @@ -1 +1 @@ -分类:C++ | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +分类:C++ | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213/index.html" "b/categories/\347\274\226\347\250\213/index.html" index 9a83b54..ccbde67 100644 --- "a/categories/\347\274\226\347\250\213/index.html" +++ "b/categories/\347\274\226\347\250\213/index.html" @@ -1 +1 @@ -分类:编程 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +分类:编程 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213/page/2/index.html" "b/categories/\347\274\226\347\250\213/page/2/index.html" index ab2ed68..d266f26 100644 --- "a/categories/\347\274\226\347\250\213/page/2/index.html" +++ "b/categories/\347\274\226\347\250\213/page/2/index.html" @@ -1 +1 @@ -分类:编程 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +分类:编程 | R1ck's Playground = how
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/css/app.css b/css/app.css index 93a69bf..d4beab9 100644 --- a/css/app.css +++ b/css/app.css @@ -1,3 +1,3 @@ -/* build time:Mon Oct 21 2024 20:10:22 GMT+0800 (中国标准时间)*/ +/* build time:Mon Oct 21 2024 20:38:37 GMT+0800 (中国标准时间)*/ #nav .menu .item:not(.title) a::before{content:"";position:absolute;width:0;height:.1875rem;bottom:0;border-radius:.125rem;left:50%;transform:translateX(-50%);background-color:currentColor}#nav .menu .submenu .item.active,#nav .menu .submenu .item:hover,#search-pagination .current .page-number,#sidebar .tab .item.active,#sidebar .tab .item:hover,.overview .menu .item.active,.pagination .next:hover,.pagination .page-number.current,.pagination .page-number:hover,.pagination .prev:hover{color:var(--grey-0);background-image:linear-gradient(to right,var(--color-pink) 0,var(--color-orange) 100%);box-shadow:0 0 .75rem var(--color-pink-a3)}#loading,#neko{position:fixed;left:0;right:0;top:0;bottom:0;z-index:9999}:root{--grey-0:#fff;--grey-1:#fdfdfd;--grey-2:#f7f7f7;--grey-3:#eff2f3;--grey-4:#ccc;--grey-5:#999;--grey-6:#666;--grey-7:#333;--grey-8:#222;--grey-9:#000;--grey-1-a0:rgba(253,253,253,0);--grey-1-a7:rgba(253,253,253,0.7);--grey-1-a5:rgba(253,253,253,0.5);--grey-1-a3:rgba(253,253,253,0.3);--grey-9-a1:rgba(0,0,0,0.1);--grey-9-a5:rgba(0,0,0,0.5);--grey-2-a0:rgba(247,247,247,0);--color-pink-light:#ffe6fa;--color-cyan-light:#e3fdf5;--color-red:#e9546b;--color-pink:#ed6ea0;--color-orange:#ec8c69;--color-yellow:#eab700;--color-green:#0a7426;--color-aqua:#3e999f;--color-blue:#38a1db;--color-purple:#9d5b8b;--color-grey:#869194;--color-red-a1:rgba(233,84,107,0.1);--color-red-a3:rgba(233,84,107,0.3);--color-pink-a3:rgba(237,110,160,0.3);--color-pink-light-a3:rgba(255,230,250,0.3);--color-pink-light-a5:rgba(255,230,250,0.5);--color-pink-light-a7:rgba(255,230,250,0.7);--body-bg-shadow:var(--grey-2);--box-bg-shadow:var(--grey-9-a1);--text-color:var(--grey-7);--header-text-color:var(--grey-0);--primary-color:var(--color-red);--nav-bg:linear-gradient(-225deg, var(--color-cyan-light) 0, var(--color-pink-light) 100%)}.primary{--note-border:#cda0c7;--note-bg:#fdf8ff;--note-text:#8a51c0;--note-hover:#935aca}.info{--note-border:#8fa4dc;--note-bg:#f1f9ff;--note-text:#1d4974;--note-hover:#1d5fa0}.success{--note-border:#a3c293;--note-bg:#fcfff5;--note-text:#2c662d;--note-hover:#3b883c}.warning{--note-border:#c9ba9b;--note-bg:#fffbeb;--note-text:#947600;--note-hover:#ccb045}.danger{--note-border:#f4b3c1;--note-bg:#fff2f5;--note-text:#cc0f35;--note-hover:#f14668}[data-theme=dark]:root{--grey-0:#222;--grey-1:#21252b;--grey-2:#363636;--grey-3:#444;--grey-4:#666;--grey-5:#aaa;--grey-6:#ccc;--grey-7:#ddd;--grey-8:#eee;--grey-9:#f7f7f7;--grey-1-a7:rgba(34,34,34,0.7);--grey-1-a5:rgba(34,34,34,0.5);--grey-1-a3:rgba(34,34,34,0.3);--grey-1-a0:rgba(34,34,34,0);--grey-9-a1:rgba(51,51,51,0.1);--grey-2-a0:rgba(54,54,54,0);--color-pink-light:#322d31;--color-cyan-light:#2d3230;--color-red:rgba(237,118,137,0.9);--color-pink:rgba(241,139,179,0.8);--color-orange:rgba(240,163,135,0.8);--color-yellow:#ffe175;--color-green:#86c59d;--color-aqua:#97d3d6;--color-blue:#9cd0ed;--color-purple:#cfacc5;--color-grey:#c3c8ca;--body-bg-shadow:#000;--box-bg-shadow:#000;--text-color:var(--grey-5);--header-text-color:var(--grey-9)}[data-theme=dark] .primary{--note-border:rgba(123,96,119,0.8);--note-bg:rgba(50,49,50,0.8);--note-text:rgba(161,116,205,0.8);--note-hover:rgba(117,72,161,0.8)}[data-theme=dark] .info{--note-border:rgba(85,98,132,0.8);--note-bg:rgba(48,49,50,0.8);--note-text:rgba(109,164,219,0.8);--note-hover:rgba(39,127,214,0.8)}[data-theme=dark] .success{--note-border:rgba(97,116,88,0.8);--note-bg:rgba(50,50,48,0.8);--note-text:rgba(128,200,129,0.8);--note-hover:rgba(41,95,42,0.8)}[data-theme=dark] .warning{--note-border:rgba(120,111,93,0.8);--note-bg:rgba(50,50,46,0.8);--note-text:rgba(220,176,0,0.8);--note-hover:rgba(163,140,55,0.8)}[data-theme=dark] .danger{--note-border:rgba(146,107,115,0.8);--note-bg:rgba(50,48,48,0.8);--note-text:rgba(239,38,79,0.8);--note-hover:rgba(168,49,72,0.8)}[data-theme=dark] .index.wrap .card .message .btn,[data-theme=dark] .overview .menu .item,[data-theme=dark] .sidebar .tab li,[data-theme=dark] [data-background-image],[data-theme=dark] img{transition:all .2s ease-in-out 0s;opacity:.75}[data-theme=dark] .index.wrap .card .message .btn:hover,[data-theme=dark] .overview .menu .item:hover,[data-theme=dark] .sidebar .tab li:hover,[data-theme=dark] [data-background-image]:hover,[data-theme=dark] img:hover{opacity:.9}[data-theme=dark] #imgs::before{background-color:rgba(0,0,0,.5)}.red{color:var(--color-red)}.pink{color:var(--color-pink)}.orange{color:var(--color-orange)}.yellow{color:var(--color-yellow)}.green{color:var(--color-green)}.aqua{color:var(--color-aqua)}.blue{color:var(--color-blue)}.purple{color:var(--color-purple)}.grey{color:var(--color-grey)}.i-cloud:before{content:"\e659"}.i-bilibili:before{content:"\e652"}.i-volume-off:before,.player-info .volume.off::before{content:"\e61e"}.i-volume-on:before,.player-info .volume.on::before{content:"\e62c"}.i-circle-play:before,.player-info .play-pause::before{content:"\e647"}.i-forward:before,.player-info .forward::before{content:"\e648"}.i-backward:before,.player-info .backward::before{content:"\e649"}.i-circle-pause:before,.playing .player-info .play-pause::before{content:"\e64a"}.i-loop:before,.player-info .mode.loop::before{content:"\e64b"}.i-order:before,.player-info .mode.order::before{content:"\e64c"}.i-random:before,.player-info .mode.random::before{content:"\e64d"}.i-douban:before{content:"\e75f"}.i-linux:before{content:"\f1e8"}.i-opera:before{content:"\f205"}.i-qq:before{content:"\f216"}.i-safari:before{content:"\f229"}.i-snapchat-ghost:before{content:"\f234"}.i-weixin:before{content:"\f262"}.i-windows:before{content:"\f266"}.i-stars:before{content:"\e8c4"}.i-apple:before{content:"\e600"}.i-blackberry:before{content:"\e601"}.i-centos:before{content:"\e602"}.i-fedora:before{content:"\e603"}.i-redhat:before{content:"\e604"}.i-ubuntu:before{content:"\e605"}.i-suse:before{content:"\e606"}.i-mobile-alt:before{content:"\f052"}.i-paw:before{content:"\f06b"}.i-android:before{content:"\f161"}.i-chrome:before{content:"\f178"}.i-edge:before{content:"\f195"}.i-firefox:before{content:"\f1a1"}.i-internet-explorer:before{content:"\f1d4"}.i-markdown:before{content:"\f1eb"}.i-smile:before{content:"\f2a5"}.i-preview:before{content:"\e901"}#copyright .license::before,.i-share:before{content:"\e61b"}#copyright .link::before,.i-link-circle:before{content:"\e67b"}#copyright .author::before,.i-person:before{content:"\e69d"}.i-sun:before{content:"\e6d1"}.i-moon:before{content:"\e71e"}.i-compress:before{content:"\ef82"}.i-expand:before{content:"\efb4"}.i-align-justify:before{content:"\ef13"}.i-align-left:before{content:"\ef14"}.i-eye:before{content:"\efb8"}.i-pen:before{content:"\f071"}.i-clock:before{content:"\ef75"}.i-flag:before{content:"\e680"}.i-at:before{content:"\e619"}.i-file:before{content:"\e68d"}.i-clipboard:before{content:"\e651"}.i-feather:before{content:"\efbd"}#tool .player .music::before,.i-music:before{content:"\f059"}#tool.playing .player .play-pause::before,.i-pause:before{content:"\f06a"}.i-comments:before{content:"\ef7f"}#tool .player .play-pause::before,.i-play:before,.player-info ol>li.current::before{content:"\f07f"}.i-calendar-check:before{content:"\ef5b"}.i-angle-up:before{content:"\ef1b"}.i-facebook:before{content:"\f19d"}.i-instagram:before{content:"\f1d3"}.i-skype:before{content:"\f231"}.i-stack-overflow:before{content:"\f239"}.i-youtube:before{content:"\f274"}.i-list-alt:before{content:"\e6c1"}.i-star:before,.page .body h3 .anchor::before{content:"\f0d4"}.i-link-alt:before{content:"\f037"}.i-paper-plane:before{content:"\f063"}.i-user:before{content:"\f2dd"}.i-link:before{content:"\e8fc"}.i-angle-down:before,details[open] summary::before{content:"\ef1a"}.i-calendar:before{content:"\e812"}#sidebar .tab .item.overview::before,.i-home:before{content:"\e8ed"}.i-magic:before{content:"\f03e"}.i-sakura:before,.page .body h1 .anchor::before{content:"\e695"}.i-tag:before{content:"\e759"}.i-angle-left:before{content:"\ef19"}.i-arrow-circle-right:before,.md .note.default::before{content:"\ef23"}.i-check-circle:before,.md .note.success::before,.md ul li.task-list-item input[type=checkbox]:checked+label::before{content:"\ef66"}.i-exclamation-circle:before,.md .note.warning::before{content:"\efb5"}.i-info-circle:before,.md .note.info::before{content:"\f02b"}.i-minus-circle:before,.md .note.danger::before{content:"\f050"}.i-plus-circle:before,.md .note.primary::before{content:"\f082"}.i-file-word:before{content:"\f299"}.i-check:before,.md ol>li.quiz.show.true::after,.md ol>li.quiz>ul.options li.right::after{content:"\ef65"}.i-times:before,.md ol>li.quiz.show.false::after,.md ol>li.quiz>ul.options li.wrong::after{content:"\f109"}#sidebar .tab .item.contents::before,.i-list-ol:before{content:"\f039"}.i-archive:before{content:"\ef1c"}.i-angle-right:before,details summary::before{content:"\ef1f"}.i-arrow-down:before{content:"\ef25"}.i-arrow-up:before,.tabs .show-btn::before{content:"\ef2a"}.i-chart-area:before{content:"\ef64"}.i-chevron-left:before{content:"\ef6e"}.i-chevron-right:before{content:"\ef6f"}.i-coffee:before{content:"\ef7a"}.i-envelope:before{content:"\efae"}.i-external-link-alt:before{content:"\efb6"}.i-heart:before{content:"\f013"}.i-heartbeat:before,.page .body h2 .anchor::before{content:"\f017"}.i-search:before{content:"\f0a8"}#sidebar .tab .item.related::before,.i-sitemap:before{content:"\f0bd"}.i-tags:before{content:"\f0f3"}.i-th:before{content:"\f0fc"}.i-thumbtack:before{content:"\f107"}.i-times-circle:before,.md ul li.task-list-item input[type=checkbox]+label::before{content:"\f10a"}.i-creative-commons:before{content:"\f17e"}.i-github:before{content:"\f1b4"}.i-twitter:before{content:"\f24d"}.i-weibo:before{content:"\f261"}.i-address-card:before{content:"\f278"}.i-zhihu:before{content:"\e765"}.i-cloud-music:before{content:"\e76a"}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background:0 0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:.0625rem dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-0.125rem;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}::-webkit-scrollbar{width:.3125rem;height:.3125rem;background:0 0}::-webkit-scrollbar-track{border-radius:.3125rem;background:0 0}::-webkit-scrollbar-track-piece{background:0 0}::-webkit-scrollbar-button{background:0 0;height:0}::-webkit-scrollbar-thumb{border-radius:1.25rem;background-clip:padding-box;background-color:var(--grey-3);background-image:linear-gradient(45deg,var(--grey-1-a5) 25%,transparent 0,transparent 50%,var(--grey-1-a5) 0,var(--grey-1-a5) 75%,transparent 0,transparent)}body.loaded::-webkit-scrollbar{width:.5rem;height:.5rem}body.loaded::-webkit-scrollbar-thumb{background-color:var(--color-pink-light)}.page .body h1 .anchor::before,.rotate{animation:rotate 6s linear infinite}.beat,.page .body h2 .anchor::before{animation:beat 1.33s ease-in-out infinite}.flash,.page .body h3 .anchor::before{animation:flash 6s cubic-bezier(.22,.61,.36,1) infinite}.overview .author:hover .image,.ribbon a:hover,.shake{animation:shake 1s}.fade-in,.tabs .tab,.tip,details[open]>div{animation:fadeIn .5s}#tool.affix .player-info.show.hide,.fade-out,.tip.hide{animation:fadeOut .3s}.code-container .show-btn .ic,.up-down{animation:UpDown 2s infinite}.code-container .show-btn.open .ic,.down-up{animation:DownUp 2s infinite}.md ol>li.quiz.show .note,.md ol>li.quiz.show blockquote,.slide{animation:slide .5s}#nav .menu .submenu,.slide-up-in{animation:slideUpIn .3s}.cards .item.show,.segments>.item.show,.slide-up-big-in,body.loaded #main .wrap,body.loaded #sidebar .panel.active{animation:slideUpBigIn .5s}#tool.affix .player-info.show,.slide-right-in{animation:slideRightIn .3s}.slide-left-in{animation:slideLeftIn .3s}.overview .menu .item .submenu,.slide-down-in,body.loaded #brand .pjax{animation:slideDownIn .3s}.blur,.lozaded{animation:blur .8s ease-in-out forwards}.elastic,.highlight.fullscreen{animation:elastic 1s}@keyframes rotate{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes rotating{from{transform:rotate(720deg)}to{transform:none}}@keyframes rotate-needle-pause{0%{transform:rotateZ(-35deg)}100%{transform:rotateZ(-60deg)}}@keyframes rotate-needle-resume{0%{transform:rotateZ(-60deg)}100%{transform:rotateZ(-35deg)}}@keyframes beat{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes shake{from,to{transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{transform:translate3d(-10px,0,0)}20%,40%,60%,80%{transform:translate3d(10px,0,0)}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes blur{0%{filter:blur(10px)}to{filter:blur(0)}}@keyframes blur-dark{0%{filter:blur(10px) brightness(.9)}to{filter:blur(0) brightness(.9)}}@keyframes UpDown{0%,100%{opacity:.8;transform:translateY(10px)}50%{opacity:.4;transform:translateY(0)}}@keyframes DownUp{0%,100%{opacity:.8;transform:rotate(180deg) translateY(0)}50%{opacity:.4;transform:rotate(180deg) translateY(-10px)}}@keyframes slide{0%{opacity:0;transform:scaleY(0)}100%{opacity:1;transform:scaleY(1)}}@keyframes slideRightIn{0%{opacity:0;transform:translateX(50%)}to{opacity:1;transform:translateX(0)}}@keyframes slideLeftIn{0%{opacity:0;transform:translateX(-50%)}to{opacity:1;transform:translateX(0)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes slideUpBigIn{0%{opacity:0;transform:translateY(80px)}100%{opacity:1;transform:translateY(0)}}@keyframes slideDownIn{0%{opacity:0;transform:translateY(-18px)}100%{opacity:1;transform:translateY(0)}}@keyframes elastic{0%{transform:scale(0)}55%{transform:scale(1)}70%{transform:scale(.98)}100%{transform:scale(1)}}::-moz-selection{background:var(--color-orange);color:var(--grey-2)}::selection{background:var(--color-orange);color:var(--grey-2)}*,:after,:before{box-sizing:border-box;margin:0;padding:0}body,html{height:100%}body{background:linear-gradient(to top,var(--body-bg-shadow) 0,var(--grey-1) 20%) no-repeat bottom;color:var(--text-color);font-family:Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;font-size:1em;line-height:2;-webkit-font-smoothing:antialiased;overflow:hidden}body.loaded{overflow:auto}body.loaded #sidebar .panel.active{display:block}@media (max-width:991px){body{padding-left:0!important;padding-right:0!important}}body.fullscreen{overflow:hidden}body.fullscreen #sidebar>.inner,body.fullscreen #tool{z-index:0}h1,h2,h3,h4,h5,h6{font-family:Mulish,'Noto Serif SC','Noto Serif JP',Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;font-weight:700;line-height:1.5;margin:1.25rem 0 .9375rem}h1.title,h2.title,h3.title,h4.title,h5.title,h6.title{font-family:Mulish,'Noto Serif JP','Noto Serif SC',Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}a{border:none;color:currentColor;outline:0;text-decoration:none;overflow-wrap:break-word;word-wrap:break-word;transition:all .2s ease-in-out 0s;cursor:pointer}a:hover{border-bottom-color:var(--color-blue);color:var(--color-blue)}a::after,a::before{transition:all .4s ease-in-out 0s}a,div,li{-webkit-tap-highlight-color:transparent}li{list-style:none}iframe,img,video{display:block;margin-left:auto;margin-right:auto;max-width:100%}hr{background-image:repeating-linear-gradient(-45deg,var(--grey-3),var(--grey-4) .25rem,transparent .25rem,transparent .5rem);border:none;height:.125rem;margin:1.5rem 0}blockquote{border-left:.25rem solid var(--grey-4);color:var(--grey-6);margin:1.25rem 0;padding:.625rem 1.25rem}blockquote cite::before{content:'-';padding:0 .3125rem}dt{font-weight:700}dd{margin:0;padding:0}input,textarea{color:var(--text-color)}@font-face{font-family:ic;src:url("//at.alicdn.com/t/font_1832207_igi8uaupcus.eot");src:url("//at.alicdn.com/t/font_1832207_igi8uaupcus.eot?#iefix") format('embedded-opentype'),url("//at.alicdn.com/t/font_1832207_igi8uaupcus.woff2") format('woff2'),url("//at.alicdn.com/t/font_1832207_igi8uaupcus.woff") format('woff'),url("//at.alicdn.com/t/font_1832207_igi8uaupcus.ttf") format('truetype'),url("//at.alicdn.com/t/font_1832207_igi8uaupcus.svg#ic") format('svg')}.ic{font-family:ic!important;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;width:1.25em;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.ic em{font-size:0}.ic-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.table-container{overflow:auto}.table-container table{border-collapse:collapse;border-spacing:0;font-size:.875em;margin:0 0 1.25rem 0;width:100%;overflow:auto}.table-container table tbody tr:nth-of-type(2n){background:var(--grey-0)}.table-container table tbody tr:hover{background:var(--grey-2)}.table-container table caption,.table-container table td,.table-container table th{font-weight:400;padding:.5rem;text-align:left;vertical-align:middle}.table-container table td,.table-container table th{border:.0625rem solid var(--grey-3);border-bottom:.1875rem solid var(--grey-3)}.table-container table th{font-weight:700;padding-bottom:.625rem;text-align:center}.table-container table td{border-bottom-width:.0625rem}.table-container table td .highlight:last-child,.table-container table td p:last-child,.table-container table td pre:last-child{margin:0}.button{display:inline-block;cursor:pointer;touch-action:manipulation;text-align:center;text-decoration:none;vertical-align:middle;white-space:nowrap;border-radius:.3rem;border:.0625rem solid var(--grey-3);color:var(--grey-6);font-size:.875em;font-weight:400;line-height:1.5;background:0 0;margin-bottom:0;min-height:1em;padding:.5em 1.25em;-webkit-user-select:none;-moz-user-select:none;user-select:none;outline:0;will-change:auto;transition:all .2s ease-in-out 0s}.button+.button{margin-left:1.25em}.button:active,.button:hover{color:var(--grey-0);background-color:var(--primary-color);border-color:var(--primary-color)}.button:disabled{border-color:var(--grey-4);color:var(--grey-4);background-color:var(--grey-2);cursor:not-allowed}.button .ic{text-align:left;width:1.285714285714286em}.toggle{line-height:0;cursor:pointer}.toggle .line{background:var(--header-text-color);display:inline-block;height:.125rem;left:0;position:relative;border-radius:.0625rem;top:0;transition:all .4s;vertical-align:top;width:100%;box-shadow:0 0 .5rem rgba(0,0,0,.5)}.toggle .line:not(:first-child){margin-top:.1875rem}.toggle.toggle-arrow .toggle-line-first{left:50%;top:.125rem;transform:rotate(45deg);width:50%}.toggle.toggle-arrow .toggle-line-middle{left:.125rem;width:90%}.toggle.toggle-arrow .toggle-line-last{left:50%;top:-.125rem;transform:rotate(-45deg);width:50%}.toggle.close .line:first-child{transform:rotate(-45deg);top:.3125rem}.toggle.close .line:nth-child(2){opacity:0}.toggle.close .line:nth-child(3){transform:rotate(45deg);top:-.3125rem}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:0 .5rem;padding:0 .75rem;position:relative;border-radius:.3125rem}@media (max-width:767px){.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 .3125rem}}.pagination{width:100%;padding:1.25rem 3.125rem;text-align:center;display:inline-block;color:var(--grey-5)}@media (max-width:767px){.pagination{padding:1.25rem .625rem}}.pagination .inner{width:auto;border-radius:.9375rem}.pagination .next,.pagination .page-number,.pagination .prev{transition:all .2s ease-in-out 0s}.pagination .space{margin:0;padding:0}.pagination .prev{margin-left:0}.pagination .next{margin-right:0}#search-pagination .current .page-number:hover,.pagination .page-number.current:hover{box-shadow:0 0 .3125rem var(--primary-color)}.tip{position:fixed;background:var(--grey-9-a5);color:#fff;top:50%;left:50%;z-index:9999;padding:.625rem 1rem;border-radius:.625rem;transform:translate(-50%,-50%);text-align:center;font-size:.875em;-webkit-backdrop-filter:blur(0.625rem);backdrop-filter:blur(0.625rem)}.ribbon{display:inline-block;align-self:flex-start;position:relative;padding:0 1rem 0 2rem;border-radius:0 .3rem .3rem 0;background-image:linear-gradient(to right,var(--color-orange) 0,var(--color-pink) 100%);color:var(--grey-0)}.ribbon::after{content:"";position:absolute;top:100%;left:0;width:0;height:0;background-color:transparent;border-style:solid;border-width:0 1rem 1rem 0;border-color:transparent;border-right-color:var(--color-orange);filter:brightness(.9)}.ribbon a{display:block;margin:0;text-align:center;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ribbon a:hover{color:currentColor}.divider{margin:1rem 0;line-height:1;height:0;font-weight:700;text-transform:uppercase;letter-spacing:.05rem;-webkit-user-select:none;-moz-user-select:none;user-select:none;color:var(--grey-4);display:table;white-space:nowrap;height:auto;line-height:1;text-align:center}.divider::after,.divider::before{content:'';display:table-cell;position:relative;top:50%;width:50%;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaAAAAACCAYAAACuTHuKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyFpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1OThBRDY4OUNDMTYxMUU0OUE3NUVGOEJDMzMzMjE2NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1OThBRDY4QUNDMTYxMUU0OUE3NUVGOEJDMzMzMjE2NyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjU5OEFENjg3Q0MxNjExRTQ5QTc1RUY4QkMzMzMyMTY3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjU5OEFENjg4Q0MxNjExRTQ5QTc1RUY4QkMzMzMyMTY3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+VU513gAAADVJREFUeNrs0DENACAQBDBIWLGBJQby/mUcJn5sJXQmOQMAAAAAAJqt+2prAAAAAACg2xdgANk6BEVuJgyMAAAAAElFTkSuQmCC")}.divider::before{background-position:right 1rem top 50%}.divider::after{background-position:left 1rem top 50%}#container{min-height:100%;min-width:100%;position:relative;display:flex;flex-direction:column}.inner{margin:0 auto;width:100%}main{background:linear-gradient(to top,var(--body-bg-shadow) 0,var(--grey-1) 20%) no-repeat bottom}main>.inner{width:calc(100% - .625rem);align-items:flex-start;display:flex;justify-content:space-between;flex-direction:row-reverse}@media (min-width:1200px){main>.inner{width:72.5rem}}@media (min-width:1600px){main>.inner{width:73%}}#main{background:linear-gradient(to top,var(--grey-0) 0,var(--grey-1) 20%) no-repeat top;box-shadow:0 1.25rem 1rem .3125rem var(--body-bg-shadow);width:calc(100% - 15.75rem);min-height:37.5rem}#main .cat{margin-top:10rem}#main .wrap{position:relative;padding:1.25rem}#main .wrap:first-child{margin-bottom:1.25rem}@media (max-width:991px){#main{width:100%}#main .wrap{padding:.625rem}}@media (max-width:767px){#main .wrap{padding:.5rem}}#header{margin:0 auto;position:relative;width:100%;height:50vh;text-shadow:0 .2rem .3rem rgba(0,0,0,.5);color:var(--header-text-color)}#header a:hover{color:currentColor}#imgs{display:block;position:fixed;top:0;left:0;width:100%;height:70vh;min-height:25rem;z-index:-9;background-color:#363636}#imgs img{width:100%;height:100%;position:absolute;top:0;left:0;-o-object-fit:cover;object-fit:cover}#imgs .item{width:100%;height:100%;position:absolute;top:0;left:0;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;opacity:0;z-index:0;animation:imageAnimation 36s linear infinite 0s;backface-visibility:hidden;transform-style:preserve-3d}#imgs .item:nth-child(2){animation-delay:6s}#imgs .item:nth-child(3){animation-delay:12s}#imgs .item:nth-child(4){animation-delay:18s}#imgs .item:nth-child(5){animation-delay:24s}#imgs .item:nth-child(6){animation-delay:30s}#imgs .item:nth-child(7){animation-delay:36s}#imgs::before{content:'';display:block;position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.2);z-index:1;transition:all .2s ease-in-out 0s}@keyframes imageAnimation{0%{opacity:0;animation-timing-function:ease-in}2%{opacity:1}8%{opacity:1;transform:scale(1.05);animation-timing-function:ease-out}17%{opacity:1;transform:scale(1.1)}25%{opacity:0;transform:scale(1.1)}100%{opacity:0}}#nav{position:fixed;z-index:9;width:100%;height:3.125rem;transition:all .2s ease-in-out 0s}#nav.up{transform:translateY(0)}#nav.down{transform:translateY(-100%)}#nav.show{background:var(--nav-bg);box-shadow:.1rem .1rem .2rem var(--grey-9-a1);text-shadow:0 0 .0625rem var(--grey-9-a1);color:var(--text-color)}#nav.show .line{background:var(--text-color);box-shadow:0 0 .0625rem var(--grey-9-a1)}#nav.show .item.active>a,#nav.show .item.expand>a{color:var(--color-aqua);opacity:1}#nav.show .menu .submenu{background-color:var(--grey-1)}#nav.show .menu .submenu .item.active a{color:var(--grey-0);opacity:1}#nav .inner{height:100%;display:flex;width:calc(100% - .625rem);flex-wrap:nowrap}@media (min-width:1200px){#nav .inner{width:72.5rem}}@media (min-width:1600px){#nav .inner{width:73%}}#nav .toggle{display:none}@media (max-width:991px){#nav .toggle{display:flex;flex-direction:column;justify-content:center;align-items:center}}#nav .toggle .lines{padding:1.25rem;width:1.375rem;box-sizing:unset}#nav .menu{padding:.625rem 0;margin:0;width:100%}#nav .menu .item{display:inline-block;position:relative;padding:0 .625rem;letter-spacing:.0625rem;text-align:center}@media (max-width:767px){#nav .menu .item{display:none}#nav .menu .item.title{display:block}}#nav .menu .item .ic{margin-right:.5rem}#nav .menu .item:not(.title) a{display:block;font-size:1em}#nav .menu .item.active:not(.dropdown) a::before,#nav .menu .item:not(.dropdown):hover a::before{width:70%}#nav .menu .submenu{display:none;position:absolute;margin-top:.5rem;padding:0;width:-moz-max-content;width:max-content;background-color:var(--grey-9-a5);box-shadow:0 .3125rem 1.25rem -.25rem var(--grey-9-a1);border-radius:.625rem 0}#nav .menu .submenu::before{position:absolute;top:-1.25rem;left:0;width:100%;height:2.5rem;content:''}#nav .menu .submenu:hover{display:block}#nav .menu .submenu .item{display:block}#nav .menu .submenu .item:first-child{border-radius:.625rem 0 0 0}#nav .menu .submenu .item:last-child{border-radius:0 0 .625rem 0}#nav .menu .submenu .item a{display:inline-block;padding:.3rem .7rem;width:100%;text-shadow:none}#nav .menu .submenu .item a::before{content:none}#nav .menu .submenu .item:hover a{transform:translateX(.3rem)}#nav .menu .submenu .item.active a,#nav .menu .submenu .item:hover a{opacity:1}#nav .menu .item.dropdown>a::after{content:"";display:inline-block;margin-left:.3rem;vertical-align:middle;border:.3rem solid transparent;border-top-color:currentColor;border-bottom:0}#nav .menu .item.dropdown:hover .submenu{display:block}#nav .right{display:inline-flex;align-items:center;justify-content:center}#nav .right .item{padding:.625rem .5rem;cursor:pointer}#nav .right .i-sun{font-size:1.125em}#brand{position:fixed;padding:3rem 5rem 0;text-align:center;width:100%;height:50vh;min-height:10rem}#brand.affix{z-index:-1}#brand,#brand .pjax{display:flex;flex-direction:column;justify-content:center;align-items:center}#brand .artboard{font-family:'Fredericka the Great',Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;font-size:3.5em;line-height:1.2}#brand h1{font-size:2.5em;letter-spacing:.125rem}#brand .artboard+h1{margin:.625rem 0}#brand .sticky{font-size:.75em;display:inline-block;transform:rotate(30deg);backface-visibility:hidden}#brand .meta{display:flex;font-size:.875em;margin:0}#brand .meta .item+.item{margin-left:.625rem}@media (min-width:768px) and (max-width:991px){#brand .meta{font-size:.8125em}}@media (max-width:767px){#brand{padding:3rem .5rem 0}#brand h1{font-size:1.5em}#brand .meta{font-size:.75em}#brand .meta .text{display:none}}@media (max-width:413px){#brand .artboard{font-size:2.5em}}#tool{position:fixed;right:1rem;top:50vh;z-index:9}@media (max-width:991px){#tool{right:1rem;left:auto}}#tool .item{display:none;width:1.875rem;height:1.875rem;opacity:.5;cursor:pointer;align-items:center;justify-content:center;flex-direction:column;transition:all .2s ease-in-out 0s}#tool .item:hover{opacity:.9}#tool .player{display:inline-flex;flex-direction:row-reverse;font-size:1.5em;width:auto;height:auto}#tool .player>.btn{font-family:ic;font-weight:400;width:1.875rem;text-align:center}#tool .player>.btn+.btn{margin-right:.625rem}#tool .player .music{display:none}#tool .player-info{border-radius:.5rem;border:.0625rem solid var(--grey-2);box-shadow:0 .625rem 1.875rem -.9375rem var(--box-bg-shadow);background:var(--grey-1-a7);-webkit-backdrop-filter:blur(0.625rem);backdrop-filter:blur(0.625rem);position:fixed;display:none;bottom:0;overflow:hidden;right:3.5rem;width:50vw;z-index:9;max-width:25rem}@media (max-width:767px){#tool .player-info{right:2.5rem}}#tool .player-info .nav{font-size:.75em;height:2.1875rem}#tool .back-to-top span{font-size:.75em}#tool.affix{text-shadow:none;top:auto;box-shadow:0 0 .5rem rgba(0,0,0,.1);background:var(--grey-1-a3);border-radius:.3125rem;right:0;bottom:0}#tool.affix .item{display:flex;color:var(--primary-color)}#tool.affix .player{font-size:1em;flex-direction:column-reverse}#tool.affix .player>.btn+.btn{margin-right:0}#tool.affix .player .music{display:block}#tool.affix .player-info{right:2.5rem}@media (min-width:1200px){#tool.affix .player-info{right:3.5rem}}@media (max-width:991px){#tool.affix .player-info{right:2.5rem;left:auto}}#tool.affix .player-info.show{display:block}#tool.affix .back-to-top{align-items:center;justify-content:center;height:auto;padding:.3125rem 0 0}#tool.affix .contents{display:none}@media (max-width:991px){#tool.affix .contents{display:flex}}@media (min-width:1200px){#tool.affix{right:1rem;bottom:1rem}}@media (max-width:991px){#tool.affix{right:0;bottom:0;left:auto}}.waves{width:100%;height:15vh;margin-bottom:-.6875rem;min-height:3.125rem;max-height:9.375rem;position:relative}@media (max-width:767px){.waves{height:10vh}}.parallax>use{animation:wave 25s cubic-bezier(.55,.5,.45,.5) infinite}.parallax>use:first-child{animation-delay:-2s;animation-duration:7s;fill:var(--grey-1-a7)}.parallax>use:nth-child(2){animation-delay:-3s;animation-duration:10s;fill:var(--grey-1-a5)}.parallax>use:nth-child(3){animation-delay:-4s;animation-duration:13s;fill:var(--grey-1-a3)}.parallax>use:nth-child(4){animation-delay:-5s;animation-duration:20s;fill:var(--grey-1)}@keyframes wave{0%{transform:translate3d(-90px,0,0)}100%{transform:translate3d(85px,0,0)}}#sidebar{position:static;width:15rem;top:0;bottom:0;transition:all .2s ease-in-out 0s}@media (max-width:991px){#sidebar{display:none;position:fixed;right:0;background:var(--grey-1);box-shadow:0 .375rem .9375rem .3125rem rgba(0,0,0,.2);z-index:99}}#sidebar.affix>.inner{position:fixed;top:0}#sidebar.affix .panels{padding-top:3.625rem;height:100vh}#sidebar>.inner{position:relative;width:15rem;color:var(--grey-6);text-align:center;display:flex;justify-content:space-around;align-items:flex-start;flex-wrap:wrap;z-index:1}.panels{padding:4.6875rem 0 2rem;width:100%;overflow:hidden}.panels .inner{overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch;width:auto;height:100%}.panels .inner::-webkit-scrollbar{display:none}.panels .panel{display:none;padding:.875rem .9375rem 2rem}.dimmer{display:none}@media (max-width:991px){.dimmer{background:#000;height:100%;left:100%;opacity:0;top:0;width:100%;z-index:9;transition:opacity 1s}#sidebar.on+.dimmer{position:fixed;display:block;opacity:.3;transform:translateX(-100%)}}.overview .author .image{border:.0625rem solid var(--body-bg-shadow);display:block;margin:0 auto;max-width:10rem;padding:.125rem;box-shadow:0 0 1rem .625rem var(--body-bg-shadow);border-radius:50%}.overview .author .name{color:var(--grey-7);font-weight:400;margin:.3125rem 0 0;text-align:center}.overview .author .description{color:var(--grey-5);font-size:1em;margin-top:.3125rem;text-align:center}@keyframes shake{0%{transform:scale(1)}10%,20%{transform:scale(.9) rotate(3deg)}30%,50%,70%,90%{transform:scale(1.1) rotate(-3deg)}40%,60%,80%{transform:scale(1.1) rotate(3deg)}100%{transform:scale(1)}}.overview .menu{padding:1.25rem;margin:0;background-color:transparent}.overview .menu .item{border-radius:.9375rem;margin-bottom:.625rem;display:block;color:var(--grey-5);transition:all .2s ease-in-out 0s}.overview .menu .item a{color:inherit;display:block;line-height:3}.overview .menu .item .submenu{display:none;padding:0}.overview .menu .item:hover{background-color:rgba(0,0,0,.1);color:inherit}.overview .menu .item:hover .submenu{display:block}.overview .menu .item .ic{margin-right:.625rem}.overview .menu .item.active:hover{box-shadow:0 0 .75rem var(--color-pink);color:var(--grey-0)}.overview .menu .item.active .item{color:currentColor}.overview .menu .item.expand{background-color:rgba(0,0,0,.05)}.overview .menu .item.expand .submenu{display:block}.social{margin-top:.9375rem;text-align:center}.social .item{display:inline-block;width:1.875rem;height:1.875rem;line-height:1.875rem;text-align:center;position:relative;overflow:hidden;border-radius:38%}.social .item i{font-size:1.4em;vertical-align:middle;transform:scale(.8)}.social .item::before{top:90%;left:-110%;content:"";width:120%;height:120%;position:absolute;transform:rotate(45deg)}.social .item i,.social .item::before{transition:all .35s cubic-bezier(.31, -.105, .43, 1.59) 0s}.social .item:focus::before,.social .item:hover::before{top:-10%;left:-10%}.social .item.github::before{background-color:#3b5998}.social .item.github i{color:#3b5998}.social .item.about::before{background-color:#3b5998}.social .item.about i{color:#3b5998}.social .item.email::before{background-color:#3b5998}.social .item.email i{color:#3b5998}.social .item:focus i,.social .item:hover i{color:var(--grey-0);transform:scale(1)}#quick{display:none;align-items:center;flex-wrap:wrap;width:15rem;margin:0;padding:0;position:fixed;bottom:.125rem}#quick li{width:25%;min-height:1.875rem;transition:all .2s ease-in-out 0s}#quick li i{cursor:pointer}#quick li a{width:100%;display:block}#quick li:hover{color:var(--primary-color)}#quick li.percent{display:block;background:var(--primary-color);width:0;min-height:.125rem}#sidebar.affix #quick,#sidebar.on #quick{display:flex}#sidebar .tab{position:absolute;display:inline-flex;padding:1.875rem 0 .625rem;margin:0;min-height:1.875rem}#sidebar .tab .item{cursor:pointer;display:inline-flex;font-size:.875em;padding:.3125rem .9375rem;color:var(--grey-5);border-radius:.625rem;text-align:center;text-decoration:none;background-color:rgba(0,0,0,.08);transition:all .2s ease-out 0s}#sidebar .tab .item:nth-child(2){margin:auto .625rem}#sidebar .tab .item span{display:none;word-break:keep-all}#sidebar .tab .item.active span{display:inherit}#sidebar .tab .item.active:hover{box-shadow:0 0 .75rem var(--color-pink)}#sidebar .tab .item::before{font-family:ic;font-weight:400}#sidebar .tab .item.active::before{margin-right:.3125rem}#sidebar.affix .tab{padding-top:.625rem}.contents ol{padding:0 .125rem .3125rem .625rem;text-align:left}.contents ol>ol{padding-left:0}.contents .toc-item{font-size:1em;line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.contents .toc-child{display:none}.contents .active>.toc-child{display:block}.contents .current>.toc-child{display:block}.contents .current>.toc-child>.toc-item{display:block}.contents .active>a{color:var(--primary-color)}.contents .current>a{color:var(--primary-color)}.contents .current>a:hover{color:var(--primary-color)}.related{font-size:.875em}.related ul{padding:0 .125rem .3125rem 1.25rem;text-align:left}.related ul li{position:relative;line-height:1.8;padding-bottom:.625rem}.related ul li a{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;width:100%;display:inline-block}.related ul li.active a{color:var(--primary-color)}.related ul li::before{content:"";width:.5rem;height:.5rem;background:var(--primary-color);box-sizing:unset;left:-1.25rem;top:.3125rem;border-radius:100%;position:absolute;border:.1875rem solid var(--grey-1);z-index:1;transition:all .2s ease-in-out 0s}.related ul li:hover::before{background:var(--color-blue)}.related ul li:not(:last-child)::after{content:"";height:100%;width:.125rem;background:var(--color-red-a3);position:absolute;left:-.875rem;top:.5rem}.state{display:flex;justify-content:center;line-height:1.4;margin-top:.625rem;overflow:hidden;text-align:center;white-space:nowrap}.state .item{padding:0 .9375rem}.state .item:not(:first-child){border-left:.0625rem solid var(--grey-4)}.state .item a{border-bottom:none}.state .item .count{display:block;font-size:1.25em;font-weight:600;text-align:center}.state .item .name{color:inherit;font-size:.875em}#footer{color:var(--grey-5);font-size:.875em;background:var(--body-bg-shadow)}#footer .inner{margin:0 auto;width:calc(100% - .625rem);position:relative;padding-left:16.25rem}@media (min-width:1200px){#footer .inner{width:72.5rem}}@media (min-width:1600px){#footer .inner{width:73%}}@media (max-width:991px){#footer .inner{padding-left:0;padding-right:0;width:auto}}.status{width:100%;text-align:center;margin-top:2rem}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5rem}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:pink;display:inline-block;margin:0 .3125rem 0 .125rem}.powered-by,.theme-info{display:inline-block;margin-bottom:.625rem}@media (max-width:567px){.post.block{padding:initial!important}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{padding-left:1.25rem}.md .note h1,.md .note h2,.md .note h3,.md .note h4,.md .note h5,.md .note h6,.md .tabs .tab-content .tab-pane h1,.md .tabs .tab-content .tab-pane h2,.md .tabs .tab-content .tab-pane h3,.md .tabs .tab-content .tab-pane h4,.md .tabs .tab-content .tab-pane h5,.md .tabs .tab-content .tab-pane h6{margin:0 .3125rem}.md img,.md video{margin-bottom:.625rem!important}.md .tabs .tab-content .tab-pane{padding:.625rem .625rem 0 .625rem!important}.post .eof{margin:2.5rem auto 1.25rem!important}}.post.block{padding:0 2rem}.md{font-family:Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;overflow-wrap:break-word;word-wrap:break-word}.md .exturl .ic{font-size:.875em;margin-left:.25rem}.md .button{margin-top:2.5rem;text-align:center}.breadcrumb{display:inline-flex;font-size:.8125em;align-items:center;margin:1.25rem 0;flex-wrap:wrap}.breadcrumb .ic{margin:0 .125rem;color:var(--grey-4)}.breadcrumb .ic:first-child{margin-left:0;margin-right:.3125rem}.breadcrumb span{white-space:pre}.breadcrumb span.current{background-color:var(--color-red-a1);border-radius:.625rem;padding:0 .625rem;transition:all .2s ease-in-out 0s}.breadcrumb span.current span{white-space:normal}.breadcrumb span.current a{color:var(--primary-color)}.breadcrumb span.current:hover{background-color:var(--color-red-a3)}.post header{font-size:1.125em;margin-bottom:.625rem}.post header .title{font-size:1.5em;margin:initial;text-align:center;overflow-wrap:break-word;word-wrap:break-word;padding-bottom:.625rem}.post header .link{display:inline-block;position:relative;vertical-align:top}.post header .link .i-link-alt{font-size:.875em;margin-left:.3125rem}.post header .meta{text-align:center;border-top:.0625rem dashed var(--grey-9-a1);font-family:Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}.post .meta{color:var(--grey-5);font-size:.75em;text-align:right}.post .meta .item{display:inline-block;margin-right:.625rem}.post .meta .icon{margin-right:.1875rem}@media (max-width:991px){.post .meta .icon{display:inline-block}}.post-nav{display:flex;margin-bottom:2.5rem}@media (max-width:767px){.post-nav{flex-direction:column}}.post-nav .item{width:50%}@media (max-width:767px){.post-nav .item{width:100%}}.post-nav .item a{display:flex;flex-direction:column;height:100%;color:var(--header-text-color);padding:1.25rem 2.5rem;background-size:cover;position:relative}.post-nav .item a::before{content:"";position:absolute;width:100%;height:100%;background:linear-gradient(135deg,#434343,#000);opacity:.5;transition:all .2s ease-in-out 0s;z-index:-1;top:0;left:0}.post-nav .item a:hover::before{opacity:.4}.post-nav .item span{font-size:.8125em}.post-nav .item.left h3,.post-nav .item.left span{align-self:flex-start}.post-nav .item.right h3,.post-nav .item.right span{align-self:flex-end;text-align:right}.md .links:last-child,.md .tabs:last-child,.md blockquote:last-child,.md img:last-child,.md p:last-child,.md pre:last-child,.md table:last-child{margin-bottom:0}.md dd ol,.md dd ul,.md ol ol,.md ol ul,.md ul ol,.md ul ul{margin-top:0}.md a{color:var(--primary-color)}.md a:hover{color:var(--color-blue)}.md h1{font-size:1.5em}.md h2{font-size:1.375em}.md h3{font-size:1.25em}.md h4{font-size:1.125em}.md h5{font-size:1em}.md h6{font-size:.875em}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{position:relative;padding-top:.625rem}.md h1:hover .anchor::after,.md h1:hover .anchor::before,.md h2:hover .anchor::after,.md h2:hover .anchor::before,.md h3:hover .anchor::after,.md h3:hover .anchor::before,.md h4:hover .anchor::after,.md h4:hover .anchor::before,.md h5:hover .anchor::after,.md h5:hover .anchor::before,.md h6:hover .anchor::after,.md h6:hover .anchor::before{color:var(--primary-color)}.md h1::after{content:"";display:block;box-sizing:unset;width:100%;height:.0625rem;background:var(--grey-3);padding-right:1.25rem;margin-left:-1.25rem;margin-top:.3125rem}.md .anchor{border-bottom-style:none;color:var(--grey-4);float:right;margin-left:.625rem}.md .anchor:hover{color:inherit}.md .anchor::after,.md .anchor::before{color:var(--grey-4);position:absolute;font-weight:400;transition:all .2s ease-out 0s}.md .active .anchor::after,.md .active .anchor::before{color:var(--primary-color)}.md p{margin:0 0 .8rem}.md blockquote{font-size:90%;background-color:var(--grey-2);margin:1.25rem 0;border-radius:.1875rem}.md blockquote ul{margin:.625rem 0!important}.md blockquote ul>li::before{width:.375rem!important;height:.375rem!important;top:.6875rem!important}.md blockquote ol:last-child,.md blockquote p:last-child,.md blockquote ul:last-child{margin-bottom:0!important}.md>blockquote{border-left-color:var(--primary-color)}.md iframe{margin-bottom:1.25rem}.md .image-info{display:block;text-align:center;font-size:.8125em;color:var(--grey-4)}.md .video-container{height:0;margin-bottom:1.25rem;overflow:hidden;padding-top:75%;position:relative;width:100%}.md .video-container embed,.md .video-container iframe,.md .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.md .kbd,.md kbd{background-color:var(--grey-1);background-image:linear-gradient(var(--grey-2),var(--grey-0),var(--grey-2));border:.0625rem solid var(--grey-4);border-radius:.2rem;box-shadow:.1rem .1rem .2rem var(--grey-9-a1);font-family:inherit;padding:.1rem .3rem;white-space:nowrap}.md mark{background-color:#dbfdad}.md ins{--line-color:var(--note-hover, var(--primary-color));text-decoration:none;border-bottom:.125rem solid var(--line-color)}.md ins.wavy{-webkit-text-decoration-style:wavy;text-decoration-style:wavy;-webkit-text-decoration-line:underline;text-decoration-line:underline;-webkit-text-decoration-color:var(--line-color);text-decoration-color:var(--line-color);border-bottom:none}.md ins.dot{border-bottom:.2rem dotted var(--line-color)}.md s{color:var(--grey-5);-webkit-text-decoration-color:var(--note-hover,var(--grey-5));text-decoration-color:var(--note-hover,var(--grey-5))}.md ruby{padding:0 .3125rem}.md .katex-display{overflow-x:scroll;overflow-y:hidden}.md .spoiler:not(.bulr){background-color:var(--text-color);color:var(--text-color);text-shadow:none;transition:color .3s;padding:0 .1875rem}.md .spoiler:not(.bulr):hover{color:#fff}.md .bulr{text-shadow:rgba(0,0,0,.7) 0 0 .625rem;color:transparent}.md .rainbow{background-image:linear-gradient(to left,#ff4500,orange,gold,#90ee90,#0ff,#1e90ff,#9370db,#ff69b4,#ff4500);background-size:110vw;-webkit-background-clip:text;background-clip:text;color:transparent;animation:rainbow 60s linear infinite}.article .md .anchor::after,.article .md .anchor::before{content:"H";font-family:Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;left:-1.875rem;top:1rem;width:1.25rem;height:1.5625rem;text-align:right;visibility:visible;font-size:80%}@media (max-width:567px){.article .md .anchor::after,.article .md .anchor::before{left:-.625rem}}.article .md .anchor::after{font-size:50%;left:-1.375rem;line-height:3}@media (max-width:567px){.article .md .anchor::after{left:-.1875rem}}.article .md h1 .anchor::after{content:" 1 "}.article .md h2 .anchor::after{content:" 2 "}.article .md h3 .anchor::after{content:" 3 "}.article .md h4 .anchor::after{content:" 4 "}.article .md h5 .anchor::after{content:" 5 "}.article .md h6 .anchor::after{content:" 6 "}@keyframes rainbow{to{background-position:-2000vw}}.rtl.body a,.rtl.body h1,.rtl.body h2,.rtl.body h3,.rtl.body h4,.rtl.body h5,.rtl.body h6,.rtl.body li,.rtl.body ol,.rtl.body p,.rtl.body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.title{font-family:UKIJ Ekran}.post footer::before{content:"";width:100%;height:.0625rem;background:var(--grey-3);display:block;margin:1.25rem auto 0}.post .tags{text-align:left;margin-top:.625rem;font-size:.75em}.post .tags a{display:inline-block;position:relative;padding:0 .3125rem;border-radius:.3125rem;background:var(--note-bg);color:var(--note-text)}.post .tags a:not(:last-child){margin-right:.625rem}.post .tags a:before{content:'';position:absolute;bottom:0;height:100%;width:0;right:0;background:var(--color-red-a1);border-radius:.25rem;transition:all .2s ease-in-out 0s}.post .tags a:hover{color:var(--primary-color)}.post .tags a:hover:before{width:104%;left:-2%}.reward{margin:1.25rem auto;padding:.625rem 0;text-align:center}.reward button{background:var(--primary-color);border:0;border-radius:.3125rem;color:var(--grey-0);cursor:pointer;line-height:2;outline:0;padding:0 .9375rem;vertical-align:text-top}.reward button:hover{background:var(--primary-color)}.reward button i{margin-right:.3125rem}.reward p{font-size:.8125em;color:var(--grey-5);margin:0}#qr{padding-top:1.25rem;display:none}#qr a{border:0}#qr img{display:inline-block;max-width:100%}#qr p{text-align:center}#copyright{background:var(--grey-2);padding:1rem 2rem;position:relative;font-size:.75em;border-radius:.625rem;color:var(--grey-6)}#copyright li::before{font-family:ic;font-weight:400;color:var(--grey-5);margin-right:.3125rem;font-size:1.1rem;line-height:.75rem;vertical-align:-.0667rem}.cards .item,.segments>.item{position:relative;color:inherit;width:calc(50% - 2rem);min-width:calc(50% - 2rem);height:14rem;margin:1rem;opacity:0}.cards .item.show,.segments>.item.show{opacity:1}@media (max-width:767px){.cards .item,.segments>.item{width:calc(100% - 1rem)!important;min-width:calc(100% - 1rem)!important;margin:1rem .5rem!important}}.index.wrap .btn{position:absolute;bottom:0;right:0;padding:.3rem 1rem;border-radius:1rem 0;color:var(--grey-0);background-image:linear-gradient(to right,var(--color-pink) 0,var(--color-orange) 100%)}.index.wrap .btn::before{position:absolute;display:block;content:'';height:calc(100% - 1rem);width:calc(100% - 1rem);border-radius:5rem;left:.5rem;top:.8rem;box-shadow:0 0 .6rem .6rem var(--color-pink-a3);background-color:var(--color-pink-a3)}.index.wrap .btn:hover{transform:translateZ(2.5rem)}.index.wrap .btn:hover::before{transform:translateZ(-2.5rem)}.index.wrap .active .btn{transform-style:preserve-3d;transform:translateZ(2rem);backface-visibility:hidden}.index.wrap .active .btn::before{transform-style:preserve-3d;transform:translateZ(-2rem);backface-visibility:hidden}.index.wrap .meta{font-size:.8125em;color:var(--grey-5)}.index.wrap .meta .ic{margin-right:.0625rem}.index.wrap .meta .item+.item{margin-left:.625rem}.index.wrap .meta.footer{position:absolute;bottom:.5rem;max-width:calc(100% - 7rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;justify-content:flex-start}.index.wrap .meta.footer span{margin-right:.5rem}.cards{display:flex;margin:0 auto;align-items:center;flex-wrap:wrap;justify-content:space-between}.cards.hide .item{display:none}.cards .item{perspective:62.5rem}.cards .item.show{display:block!important}.cards .item .cover,.cards .item .info{position:absolute;display:flex;flex-direction:column;justify-content:center;align-items:center;border-radius:.375rem;height:100%;width:100%;backface-visibility:hidden;transform-style:preserve-3d;transition:ease-in-out .6s}.cards .item .cover{background-position:center;background-size:cover;padding:.5rem 1rem;font-size:1em;color:var(--header-text-color);overflow:hidden;transform:rotateY(0)}.cards .item .cover .title{margin:0;white-space:normal;text-align:center}.cards .item .cover::before{position:absolute;display:block;content:'';top:0;left:0;right:0;bottom:0;background-image:linear-gradient(135deg,#434343 0,#000 100%);opacity:.25;z-index:-1}.cards .item .cover span{font-size:.75em;position:absolute;right:.9375rem;top:.625rem;padding:0 .3125rem;border-radius:.3125rem;box-shadow:0 0 .3125rem .0625rem rgba(0,0,0,.6);background:rgba(0,0,0,.5)}.cards .item .info{background-color:var(--grey-0);transform:rotateY(-180deg);padding:1rem 1.5rem 4rem;justify-content:space-between}@media (max-width:767px){.cards .item .info{padding:1rem 1rem 4rem}}.cards .item .info .ribbon{left:-2.5rem;margin-bottom:.8rem;max-width:calc(100% + 2rem)}@media (max-width:767px){.cards .item .info .ribbon{left:-2rem}}.cards .item:nth-child(2n) .info{transform:rotateY(180deg)}.cards .item.active .cover{transform:rotateY(180deg)}.cards .item.active .info{transform:rotateY(0);box-shadow:0 0 2rem var(--box-bg-shadow)}.cards .item:nth-child(2n).active .cover{transform:rotateY(-180deg)}.cards .item .title .ic{font-size:80%}.cards .item ul.posts{display:flex;flex-wrap:wrap;justify-content:space-between;align-content:baseline;min-height:5rem;overflow:hidden}.cards .item ul.posts li{width:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--primary-color)}.segments{display:flex;flex-direction:column;justify-content:center;align-items:center}.segments>.item{display:flex;border-radius:.625rem;width:calc(100% - 2rem);min-width:calc(100% - 2rem);border-radius:.5rem;box-shadow:0 .625rem 1.875rem -.9375rem var(--box-bg-shadow);transition:all .2s ease-in-out 0s}@media (max-width:767px){.segments>.item{flex-direction:column;height:-moz-fit-content;height:fit-content;max-height:-moz-fit-content;max-height:fit-content}}.segments>.item:hover{box-shadow:0 0 2rem var(--box-bg-shadow)}.segments>.item:hover .cover img{transform:scale(1.05) rotate(1deg)}.segments .cover{width:50%;margin-right:1.5rem;-webkit-clip-path:polygon(0 0,92% 0%,100% 100%,0% 100%);clip-path:polygon(0 0,92% 0%,100% 100%,0% 100%);border-radius:.625rem 0 0 .625rem;overflow:hidden}@media (max-width:767px){.segments .cover{width:100%;height:14rem;margin:auto;-webkit-clip-path:polygon(0 0,100% 0,100% 92%,0 100%);clip-path:polygon(0 0,100% 0,100% 92%,0 100%);border-radius:.625rem .625rem 0 0}}.segments .cover img{-o-object-fit:cover;object-fit:cover;width:100%;height:100%;transition:all .2s ease-in-out 0s}.segments .info{position:relative;width:50%;padding:1rem 1.5rem 3rem 0;perspective:62.5rem}@media (max-width:767px){.segments .info{width:100%;height:14rem;padding:0 1rem 3rem}}.segments .info .meta{display:flex;justify-content:flex-end;margin:0}@media (max-width:767px){.segments .info .meta .item:not(:first-child){display:none}}.segments .info h3{text-overflow:ellipsis;overflow:hidden;white-space:nowrap;margin:.625rem 0;color:var(--primary-color)}.segments .info .excerpt{overflow:hidden;font-size:.875em;max-height:5rem;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;text-overflow:ellipsis}.segments>.item:nth-child(2n){flex-direction:row-reverse}@media (max-width:767px){.segments>.item:nth-child(2n){flex-direction:column}}.segments>.item:nth-child(2n) .cover{margin-right:auto;margin-left:1.5rem;-webkit-clip-path:polygon(0 0%,100% 0%,100% 100%,8% 100%);clip-path:polygon(0 0%,100% 0%,100% 100%,8% 100%);border-radius:0 .625rem .625rem 0}@media (max-width:767px){.segments>.item:nth-child(2n) .cover{width:100%;margin:auto;-webkit-clip-path:polygon(0 0,100% 0,100% 100%,0 92%);clip-path:polygon(0 0,100% 0,100% 100%,0 92%);border-radius:.625rem .625rem 0 0}}.segments>.item:nth-child(2n) .info{padding:1rem 0 3rem 1.5rem}@media (max-width:767px){.segments>.item:nth-child(2n) .info{padding:0 1rem 3rem}}.segments>.item:nth-child(2n) .info .meta{justify-content:flex-start}.segments>.item:nth-child(2n) .btn{left:0;right:auto;border-radius:0 1rem;background-image:linear-gradient(to right,var(--color-orange) 0,var(--color-pink) 100%)}.segments>.item:nth-child(2n) .meta.footer{right:.5rem;justify-content:flex-start}.segments>.item:nth-child(2n):hover .cover img{transform:scale(1.05) rotate(-1deg)}.collapse small{color:var(--grey-4);margin:auto .3125rem}.collapse .item{position:relative;padding:1.25rem 1.875rem;margin:0}.collapse .item::before{content:"";position:absolute;z-index:1;transition:all .2s ease-in-out 0s;box-sizing:unset;top:1.9rem;left:0;width:.6rem;height:.6rem;border:.15rem solid var(--primary-color);border-radius:50%;background:var(--grey-1)}.collapse .item:not(:last-child):not(.title)::after{content:"";position:absolute;top:1.9rem;bottom:-1.9rem;left:.35rem;border-left:.2rem solid var(--color-red-a3)}.collapse .item:hover::before{border-color:var(--color-blue)}.collapse .item.header::after{border-left-style:dashed!important}.collapse .item.header .cheers{display:block}.collapse .item.section::before{width:.4rem;height:.4rem;margin-left:.1rem}.collapse .item.header a,.collapse .item.section a,.collapse .item.title a{border-bottom:.0625rem dashed var(--grey-4)}.collapse .item.header:hover a,.collapse .item.section:hover a,.collapse .item.title:hover a{border-bottom-color:var(--color-blue)}.collapse .item.normal{display:flex;flex-wrap:wrap;align-items:center;padding:.625rem 1.875rem}.collapse .item.normal::before{top:1.4rem;width:.2rem;height:.2rem;margin-left:.2rem;background:var(--primary-color)}.collapse .item.normal:hover::before{background:var(--color-blue)}.collapse .item.normal .meta{display:inline;font-size:.75em;margin-right:.625rem}.collapse .item.normal .meta time{color:var(--grey-4)}.collapse .item.normal .title{display:inline}.collapse .item.normal .title a{color:var(--primary-color)}.collapse .item.normal .title a:hover{color:var(--color-blue)}.collapse .item.normal .title .i-link-alt{font-size:.875em;margin-left:.3125rem}.tag.cloud{text-align:center}.tag.cloud a{display:inline-block;margin:.625rem}.tag.cloud a:hover{color:var(--primary-color)!important}.page .notfound{width:18.75rem;height:22.625rem;background:url("../images/404.png") no-repeat center bottom;text-align:center;margin:6.25rem auto}.page .anchor::before{font-family:ic;font-weight:400;color:var(--grey-4);position:absolute;left:-1.5625rem;margin-top:.25rem;visibility:visible}@media (max-width:567px){.page .anchor::before{left:-.1875rem}}.page .body h1 .anchor::before{font-size:.875em;margin-top:.15rem;color:pink;left:-1.875rem}@media (max-width:567px){.page .body h1 .anchor::before{left:-.3125rem}}.page .body h2 .anchor::before{font-size:.8125em;color:var(--color-pink)}.page .body h3 .anchor::before{font-size:.75em;color:var(--color-orange)}.pace{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pace-inactive{display:none}.pace .pace-progress{background:var(--primary-color);position:fixed;z-index:2000;top:0;right:100%;width:100%;height:.125rem}#loading{background-color:var(--grey-1);display:none}.cat{position:relative;display:block;width:15em;height:100%;font-size:10px;margin:auto;animation:2.74s linear infinite loading-cat}.cat *{box-sizing:content-box}.cat .body,.cat .foot,.cat .head,.cat .paw{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto;border-radius:50%;width:15em;height:15em}.cat .body{background-image:radial-gradient(transparent 0,transparent 35%,#383c4b 35%,#383c4b 39%,#eda65d 39%,#eda65d 46%,#f2c089 46%,#f2c089 60%,#eda65d 60%,#eda65d 67%,#383c4b 67%,#383c4b 100%)}.cat .foot:before,.cat .head:before{background-image:radial-gradient(transparent 0,transparent 35%,#383c4b 35%,#383c4b 39%,#eda65d 39%,#eda65d 67%,#383c4b 67%,#383c4b 100%)}.cat .head:before{content:'';width:100%;height:100%;position:absolute;border-radius:50%;clip-path:polygon(100% 20%,50% 50%,70% -10%);-webkit-clip-path:polygon(100% 20%,50% 50%,70% -10%)}.cat .head:after{content:'';width:4.125em;height:2.5em;position:absolute;top:.8125em;right:3.9375em;background-image:linear-gradient(var(--grey-1) 65%,transparent 65%),radial-gradient(var(--grey-1) 51%,#383c4b 55%,#383c4b 68%,transparent 70%);transform:rotate(-66deg)}.cat .head .face{width:5em;height:3.75em;left:9.0625em;top:1.8125em;position:absolute;transform:rotate(-47deg);background:radial-gradient(circle,#f2c089 0,#f2c089 23%,transparent 23%) -.1875em 1.0625em no-repeat,radial-gradient(circle,#383c4b 0,#383c4b 6%,transparent 6%) .75em -.75em no-repeat,radial-gradient(circle,#383c4b 0,#383c4b 6%,transparent 6%) -.75em -.75em no-repeat,radial-gradient(#eda65d 0,#eda65d 15%,transparent 15%) 0 -.6875em no-repeat,radial-gradient(circle,transparent 5%,#383c4b 5%,#383c4b 10%,transparent 10%) -.1875em -.3125em no-repeat,radial-gradient(circle,transparent 5%,#383c4b 5%,#383c4b 10%,transparent 10%) .1875em -.3125em no-repeat,radial-gradient(circle,#eda65d 45%,transparent 45%) 0 -.1875em,linear-gradient(transparent 35%,#383c4b 35%,#383c4b 41%,transparent 41%,transparent 44%,#383c4b 44%,#383c4b 50%,transparent 50%,transparent 53%,#383c4b 53%,#383c4b 59%,transparent 59%)}.cat .foot:after,.cat .foot:before{content:'';width:100%;height:100%;position:absolute}.cat .foot:before{border-radius:50%;clip-path:polygon(50% 50%,0% 50%,0% 25%);-webkit-clip-path:polygon(50% 50%,0% 50%,0% 25%)}.cat .foot .tummy-end{width:1.5em;height:1.5em;position:absolute;border-radius:50%;background-color:#f2c089;left:1.1875em;top:6.5625em}.cat .foot .bottom{width:2.1875em;height:.9375em;position:absolute;top:4.875em;left:.75em;border:.375em solid #383c4b;border-bottom:0;border-radius:50%;transform:rotate(21deg);background:#eda65d}.cat .foot:after,.cat .hands,.cat .legs{width:.625em;height:1.5625em;position:absolute;border:.375em solid #383c4b;background-color:#eda65d}.cat .hands{border-top:0;border-radius:0 0 .75em .75em}.cat .hands.left{top:4.3em;left:13.1875em;transform:rotate(-20deg)}.cat .hands.right{top:5.125em;left:10.975em;transform:rotate(-25deg)}.cat .legs{border-bottom:0;border-radius:.75em .75em 0 0}.cat .legs.left{top:4.0625em;left:3.125em;transform:rotate(25deg)}.cat .legs.right{top:3.3125em;left:.75em;transform:rotate(22deg)}.cat .foot:after{width:.9em;height:2.5em;top:2.625em;left:2.5em;z-index:-1;transform:rotate(25deg);background-color:#c6823b;border-bottom:0;border-radius:.75em .75em 0 0}.cat .body{animation:2.74s linear infinite body}.cat .foot{animation:2.74s linear infinite foot}.cat:hover{animation-play-state:paused}.cat:hover .body,.cat:hover .foot{animation-play-state:paused}.cat:active{animation-play-state:running}.cat:active .body,.cat:active .foot{animation-play-state:running}@keyframes body{0%{-webkit-clip-path:polygon(50% 50%,0% 50%,0% 100%,100% 100%,100% 20%);clip-path:polygon(50% 50%,0% 50%,0% 100%,100% 100%,100% 20%)}10%{-webkit-clip-path:polygon(50% 50%,30% 120%,50% 100%,100% 100%,100% 20%);clip-path:polygon(50% 50%,30% 120%,50% 100%,100% 100%,100% 20%)}20%{-webkit-clip-path:polygon(50% 50%,100% 90%,120% 90%,100% 100%,100% 20%);clip-path:polygon(50% 50%,100% 90%,120% 90%,100% 100%,100% 20%)}40%{-webkit-clip-path:polygon(50% 50%,100% 45%,120% 45%,120% 50%,100% 20%);clip-path:polygon(50% 50%,100% 45%,120% 45%,120% 50%,100% 20%)}50%{-webkit-clip-path:polygon(50% 50%,100% 45%,120% 45%,120% 50%,100% 20%);clip-path:polygon(50% 50%,100% 45%,120% 45%,120% 50%,100% 20%)}65%{-webkit-clip-path:polygon(50% 50%,100% 65%,120% 65%,120% 50%,100% 20%);clip-path:polygon(50% 50%,100% 65%,120% 65%,120% 50%,100% 20%)}80%{-webkit-clip-path:polygon(50% 50%,75% 130%,120% 65%,120% 50%,100% 20%);clip-path:polygon(50% 50%,75% 130%,120% 65%,120% 50%,100% 20%)}90%{-webkit-clip-path:polygon(50% 50%,-20% 110%,50% 120%,100% 100%,100% 20%);clip-path:polygon(50% 50%,-20% 110%,50% 120%,100% 100%,100% 20%)}100%{-webkit-clip-path:polygon(50% 50%,0% 50%,0% 100%,100% 100%,100% 20%);clip-path:polygon(50% 50%,0% 50%,0% 100%,100% 100%,100% 20%)}}@keyframes loading-cat{0%{transform:rotate(0)}10%{transform:rotate(-80deg)}20%{transform:rotate(-180deg)}40%{transform:rotate(-245deg)}50%{transform:rotate(-250deg)}68%{transform:rotate(-300deg)}90%{transform:rotate(-560deg)}100%{transform:rotate(-720deg)}}@keyframes foot{0%{transform:rotate(-10deg)}10%{transform:rotate(-100deg)}20%{transform:rotate(-145deg)}35%{transform:rotate(-190deg)}50%{transform:rotate(-195deg)}70%{transform:rotate(-165deg)}100%{transform:rotate(-10deg)}}#search{position:fixed;background:var(--nav-bg);left:0;top:0;width:100%;height:100%;padding:1.25rem;z-index:999;display:none}#search>.inner{border-radius:0;height:100%;margin:0 auto;width:43.75rem;text-shadow:none}@media (max-width:767px){#search>.inner{width:100%}}#search>.inner .close-btn,#search>.inner .icon{color:var(--grey-5);font-size:1.125rem;padding:0 .625rem}#search>.inner .close-btn{cursor:pointer}#search>.inner .close-btn:hover i{color:var(--grey-7)}#search>.inner .header{display:flex;background:var(--grey-1-a5);border-radius:3rem;padding:.5rem 1.5rem;margin-bottom:1.25rem;border:.0625rem solid var(--grey-5);font-size:1.125em;align-items:center}#search>.inner .search-input-container{flex-grow:1}#search>.inner .search-input-container form{padding:.125rem}#search>.inner .search-input{background:0 0;border:0;outline:0;width:100%}#search>.inner .search-input::-webkit-search-cancel-button{display:none}#search .results{height:calc(100% - 6.25rem);padding:1.875rem 1.875rem .3125rem;border-radius:.3125rem;background:var(--grey-1-a7) url("../images/search.png") no-repeat bottom right;color:var(--text-color)}#search .results .inner{position:relative;height:100%;overflow:hidden}#search .results hr{margin:.625rem 0}.algolia-powered{float:right;background:url("../images/algolia_logo.svg") no-repeat;display:inline-block;height:1.125rem;width:4.25rem;margin:.5rem auto}#search-hits{overflow-y:scroll;height:calc(100% - 8.125rem)}#search-hits ol{padding:0}#search-hits .item{margin:.9375rem 0}#search-hits .item a{border-bottom:.0625rem dashed var(--grey-4);display:block;transition:all .2s ease-in-out 0s}#search-hits .item span{font-size:70%;display:block}#search-hits .item span i{color:var(--grey-4);margin:0 .3125rem}#search-pagination ul{padding:0;margin:1.25rem 0}#search-pagination .pagination{opacity:1;padding:0}#search-pagination .pagination-item{display:inline-block}#search-pagination .page-number{transition:all .2s ease-in-out 0s}#search-pagination .current .page-number{cursor:default}#search-pagination .disabled-item{color:var(--grey-4);cursor:default}#search-pagination .disabled-item .page-number:hover{color:var(--grey-4);background:0 0;box-shadow:none}#neko{display:none;background:linear-gradient(to top,#fddb92 0,#d1fdff 80%)}#neko .planet{position:fixed;left:-50%;top:-50%;width:200%;height:200%;animation:rotate 2s cubic-bezier(.7,0,0,1);transform-origin:center bottom}#neko:before{content:"";position:absolute;left:0;right:0;top:0;bottom:0;opacity:0;background:linear-gradient(to top,#30cfd0 0,#330867 100%);transition:2s ease all}#neko .moon,#neko .sun{position:absolute;border-radius:100%;left:55%;top:32%}#neko .sun{height:40px;width:40px;background:#ffee94;box-shadow:0 0 40px #ffee94;opacity:1}#neko .moon{height:24px;width:24px;background:#eee;box-shadow:0 0 20px #fff;opacity:0}#neko .body{display:block;position:absolute;bottom:-20px;height:140px;width:135px;left:50%;margin-left:-100px;background:#777;transition:all .25s ease-in-out;animation:slideUpBigIn 1s}#neko .body:after,#neko .body:before{position:absolute;content:"";width:0;height:0;border-bottom:20px solid #777;top:-20px;transition:all .25s ease-in-out}#neko .body:before{border-left:0 solid transparent;border-right:30px solid transparent;left:0}#neko .body:after{border-right:0px solid transparent;border-left:30px solid transparent;right:0}#neko .body .eyes{display:block;position:absolute;background:#ffee94;height:40px;width:40px;border-radius:100%;bottom:80px}#neko .body .eyes.left{left:12px}#neko .body .eyes.right{right:12px}#neko .body .eyes .pupil,#neko .body .nose{display:block;position:relative;background:#ffb399;border-radius:100%;margin:0 auto}#neko .body .eyes .pupil{height:100%;width:5px;transition:width 1s .5s ease-in-out}#neko .body .nose{top:45px;height:10px;width:10px}#neko.dark:before{opacity:1}#neko.dark .sun{opacity:0}#neko.dark .moon{opacity:1}#neko.dark .body{background:#444}#neko.dark .body:before{border-bottom:20px solid #444}#neko.dark .body:after{border-bottom:20px solid #444}#neko.dark .body .eyes .pupil{height:90%;width:34px;margin:5% auto}.widgets{display:flex;z-index:1;background:var(--body-bg-shadow);justify-content:space-around}.widgets>div{width:calc(50% - 2rem);padding:1rem}@media (max-width:767px){.widgets{flex-direction:column-reverse}.widgets>div{width:calc(100% - 1rem)!important}}.widgets ul{counter-reset:counter}.widgets .item{padding:.5rem 0 .5rem 2rem;border-bottom:.0625rem dashed var(--grey-4);position:relative}.widgets .item::before{counter-increment:counter;content:counter(counter);position:absolute;left:0;font-size:1.5em;color:var(--grey-4);line-height:1.2;text-align:right;width:1em}.widgets .item .breadcrumb,.widgets .item span{display:block;text-overflow:ellipsis;overflow:hidden;max-height:2rem;white-space:nowrap}.widgets .item .breadcrumb{margin:0;display:flex;max-height:1.2rem}.katex,.katex-display{-webkit-user-select:all;-moz-user-select:all;user-select:all}.medium-zoom-overlay{z-index:9998}.medium-zoom-image--opened{z-index:9999}.operation{color:var(--grey-5);cursor:pointer;position:absolute;padding:.125rem .375rem;right:.125rem;top:.45rem;font-size:.8125em}.operation span{transition:all .2s ease-in-out 0s;margin:0 .3125rem}.operation span:hover{color:var(--grey-7)}.highlight{background:var(--grey-2);color:var(--grey-7);line-height:1.6;margin:1.25rem auto}.code-container,code,pre{font-family:Inconsolata,consolas,Menlo,-apple-system,"PingFang SC","Microsoft YaHei";font-size:1em}:not(td)>pre{background:var(--grey-1);border:.0625rem solid var(--grey-3);border-radius:.3125rem;padding:1rem;margin-bottom:.8rem;line-break:anywhere;white-space:break-spaces}:not(td)>pre code{background:0 0;color:currentColor}:not(pre)>code{color:var(--primary-color);border-radius:.3rem;border:.0625rem solid rgba(0,0,0,.1);background-color:var(--grey-0);padding:.2rem .3rem;overflow-wrap:break-word;word-wrap:break-word}.highlight{position:relative;overflow:hidden;border-radius:.5rem;box-shadow:0 .3125rem .625rem -.125rem var(--grey-9-a1)}.highlight ::-moz-selection{background:var(--grey-4)}.highlight ::selection{background:var(--grey-4)}.highlight::before{content:" ";position:absolute;border-radius:50%;background:#fc625d;width:.75rem;height:.75rem;left:.75rem;top:.8125rem;box-shadow:1.25rem 0 #fdbc40,2.5rem 0 #35cd4b}.highlight figcaption{color:var(--grey-4);display:inline-flex;font-size:.875em;font-weight:700;padding:0 6rem 0 5rem;min-height:2.5rem;width:100%;text-align:center;align-items:center;justify-content:space-between;background-color:var(--grey-3);margin-bottom:.625rem}.highlight figcaption::before{content:attr(data-lang);text-transform:Capitalize}.highlight figcaption a,.highlight figcaption span{display:block;margin-left:.625rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.highlight figcaption a{color:var(--grey-5)}.highlight figcaption a:hover{color:var(--grey-6)}.highlight.fullscreen{position:fixed;top:0;left:0;width:100%;height:100%;min-width:100%;z-index:9999;margin:0;border-radius:0;overflow-y:scroll;overflow-x:hidden}.highlight.fullscreen .show-btn{position:fixed}.highlight.breakline .code-container table{line-break:anywhere;white-space:break-spaces}.highlight.breakline .code-container table pre{white-space:break-spaces}.code-container{overflow-x:scroll;overflow-y:hidden}.code-container::after{content:"";display:block;height:.625rem;width:100%}.code-container table{white-space:pre;border-spacing:0;width:100%}.code-container tr{background-color:inherit}.code-container tr:hover td{background-color:var(--grey-3)}.code-container td{position:relative;padding:unset;vertical-align:unset;border:unset;transition:all .2s ease-in-out 0s}.code-container td pre::before{content:" "}.code-container td:first-child{position:absolute;background:var(--grey-2);text-align:right;overflow-x:visible!important;overflow-y:hidden;left:0;width:2.5rem;padding-right:.9375rem;color:var(--color-grey);z-index:1}.code-container td:nth-child(2){padding-left:3rem}.code-container td:last-child{padding-right:.9375rem}.code-container td[data-num]::before{content:attr(data-num)}.code-container td[data-command]::before{content:attr(data-command) " ";color:var(--grey-5);display:block;letter-spacing:-1px;padding-right:.5rem;text-align:right;white-space:nowrap}.code-container td[data-command]+td{padding-left:.5rem;border-left:1px solid var(--grey-4)}.code-container .marked td{background-color:var(--grey-3)}.code-container .marked:hover td:last-child{background-color:var(--color-red-a1)}.code-container .show-btn{position:absolute;cursor:pointer;left:0;bottom:0;width:100%;height:2.875rem;text-align:center;color:var(--text-color);background-image:linear-gradient(to bottom,var(--grey-2-a0) 0,var(--grey-2) 80%);z-index:1}.code-container .show-btn::after{content:"";display:block;width:100%;height:1rem;background:var(--grey-2)}.code-container .show-btn .ic{margin-top:1rem}.code-container .show-btn.open{background:0 0;bottom:.5rem}.code-container .show-btn.open::after{display:none}.code-container .deletion{background:var(--color-pink-light)}.code-container .addition{background:var(--color-cyan-light)}.code-container .meta{color:var(--color-yellow);-moz-user-select:none;-webkit-user-select:none;user-select:none}.code-container .comment,.code-container .punctuation{color:var(--grey-5)}.code-container .attribute,.code-container .css .class,.code-container .css .id,.code-container .css .pseudo,.code-container .function,.code-container .html .doctype,.code-container .name,.code-container .regexp,.code-container .ruby .constant,.code-container .tag,.code-container .variable,.code-container .xml .doctype,.code-container .xml .pi,.code-container .xml .tag .title{color:var(--color-red)}.code-container .built_in,.code-container .builtin-name,.code-container .command,.code-container .constant,.code-container .literal,.code-container .number,.code-container .params,.code-container .preprocessor{color:var(--color-orange)}.code-container .css .rules .attribute,.code-container .formula,.code-container .header,.code-container .inheritance,.code-container .ruby .class .title,.code-container .ruby .symbol,.code-container .special,.code-container .string,.code-container .symbol,.code-container .value,.code-container .xml .cdata{color:var(--color-green)}.code-container .css .hexcolor,.code-container .namespace,.code-container .title{color:var(--color-aqua)}.code-container .class-name,.code-container .coffeescript .title,.code-container .javascript .title,.code-container .perl .sub,.code-container .python .decorator,.code-container .python .title,.code-container .ruby .function .title,.code-container .ruby .title .keyword{color:var(--color-blue)}.code-container .javascript .function,.code-container .keyword{color:var(--color-purple)}.md li{position:relative;margin:.2rem 0}.md li:before{transition:all .2s ease-in-out 0s}.md li p{margin:0}.md dl,.md ol,.md ul{margin:.5em 0 .5em;padding:.1em .2em .1em 1.4em}.md dl dt::before,.md ul>li::before{content:"";position:absolute;width:.4em;height:.4em;background:var(--primary-color);border-radius:50%;top:.85em;left:-1em}.md dl dt:hover::before,.md ul>li:hover::before{background:var(--color-pink)}.md ol:not([start]){counter-reset:counter}.md ol>li::before{counter-increment:counter;content:counter(counter);position:absolute;width:1.4em;height:1.4em;border-radius:50%;text-align:center;font-size:.8em;line-height:1.4;top:.5em;left:-1.8em;background:var(--primary-color);color:var(--grey-1);cursor:pointer}.md ol>li:hover::before{color:var(--grey-1);background:var(--color-pink)}.md dl dt{position:relative}.md dl dd{padding-left:.9375em}.md ul li.nodot::before{width:auto;height:auto;background:0 0!important;border:none!important;position:relative;top:0!important;left:0!important}.md ul li.task-list-item::before{width:auto;height:auto;background:0 0!important;border:none!important;position:relative;top:0!important;left:0!important}.md ul li.task-list-item input[type=checkbox]{display:none}.md ul li.task-list-item input[type=checkbox]+label::before{font-family:ic;font-weight:400;color:var(--grey-4);display:inline-block;margin-right:.625em}.md ul li.task-list-item input[type=checkbox]:checked+label::before{color:var(--note-text,var(--primary-color))}.md li ul>li::before{background:var(--grey-1);border:1px solid var(--primary-color)}.md dd ol>li::before,.md li ol>li::before{content:counter(counter) ".";background:0 0;color:var(--primary-color);font-size:1em;line-height:1;width:auto}.md dd ol>li:hover::before,.md li ol>li:hover::before{background:0 0;color:var(--color-pink)}.md ol>li.quiz.show.true::before,.md ol>li.quiz>ul.options li.right::before{color:var(--color-green)}.md ol>li.quiz.show.true::after,.md ol>li.quiz>ul.options li.right::after{color:var(--color-green);animation:fadeIn .5s}.md ol>li.quiz.show.false::before,.md ol>li.quiz>ul.options li.wrong::before{color:var(--color-red)}.md ol>li.quiz.show.false::after,.md ol>li.quiz>ul.options li.wrong::after{color:var(--color-red);animation:fadeIn .5s}.md ol>li.quiz{margin:.625rem 0}.md ol>li.quiz::before{counter-increment:counter;content:counter(counter) ".";position:absolute;color:var(--primary-color);text-align:right;width:2rem;left:-2.2rem;height:auto;background:0 0!important;border:none!important;font-size:1em}.md ol>li.quiz>p{margin:0;cursor:pointer}.md ol>li.quiz>p:first-child::before{content:"[" attr(data-type) "]";font-size:.75em;color:var(--grey-4);margin:auto .3125rem}.md ol>li.quiz.mistake>p:first-child::before{color:var(--primary-color)}.md ol>li.quiz .gap{display:inline-block;min-width:2.5rem;text-align:center;padding:0 .625rem;text-indent:-624.9375rem}.md ol>li.quiz .gap::after{display:block;content:"";background:var(--text-color);width:calc(100% + 1.25rem);height:.0625rem;margin-bottom:-.0625rem;margin-left:-.625rem}.md ol>li.quiz.show.fill .gap{text-indent:0}.md ol>li.quiz::after,.md ol>li.quiz::before{transition:all .2s ease-in-out 0s}.md ol>li.quiz blockquote{display:none}.md ol>li.quiz blockquote .mistake{display:block;border:.1875rem dashed var(--grey-4);padding:.625rem 1.25rem;background:var(--grey-3);margin:.3125rem}.md ol>li.quiz blockquote .mistake::before{display:block;content:"[" attr(data-type) "]";font-size:.8125em;color:var(--grey-4)}.md ol>li.quiz .note{display:none;padding:0;margin:0;background:0 0;border:none;border-radius:inherit;color:unset;font-size:inherit;filter:unset}.md ol>li.quiz.show .note,.md ol>li.quiz.show blockquote{display:block}.md ol>li.quiz ul.options{padding-inline-start:0.625rem}.md ol>li.quiz ul.options li::before{color:var(--color-grey);margin-right:.625rem;transition:all .2s ease-in-out 0s;width:auto;height:auto;background:0 0!important;border:none!important;position:relative;top:0!important;left:0!important}.md ol>li.quiz ul.options li:first-child::before{content:"A."}.md ol>li.quiz ul.options li:nth-child(2)::before{content:"B."}.md ol>li.quiz ul.options li:nth-child(3)::before{content:"C."}.md ol>li.quiz ul.options li:nth-child(4)::before{content:"D."}.md ol>li.quiz ul.options li:nth-child(5)::before{content:"E."}.md ol>li.quiz>ul.options li{cursor:pointer}.md ol>li.quiz>ul.options li::after{font-family:ic;font-weight:400;position:absolute;left:-1.5625rem;top:0}.md ol>li.quiz.show.false::before,.md ol>li.quiz.show.true::before{visibility:hidden}.md ol>li.quiz.show.false::after,.md ol>li.quiz.show.true::after{font-family:ic;font-weight:400;position:absolute;left:-1.2rem;top:.25rem}.links{display:flex;flex-wrap:wrap;font-size:.9rem}.links .item{position:relative;display:inline-flex;justify-self:center;align-items:center;line-height:1.5;width:calc(50% - 2rem);padding:.5rem 1rem;margin:1rem;transition:all .2s ease-in-out 0s;border-radius:.5rem;border:.0625rem solid var(--grey-2);box-shadow:0 .625rem 1.875rem -.9375rem var(--box-bg-shadow);--bg-color:var(--block-color, #666)}.links .item .image{display:block;width:4rem;height:4rem;border-radius:.9375rem;background:var(--grey-1) center no-repeat;background-size:contain;flex-shrink:0;border:none}.links .item .info{padding-left:1rem}.links .item .title{margin:.5rem 0;font-family:Mulish,'Noto Serif JP','Noto Serif SC',Mulish,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;font-weight:700;color:var(--bg-color);border:none}.links .item .title .ic{display:none}.links .item .desc{font-size:.75em;margin:.5rem 0}.links .item:hover{color:var(--header-text-color);background-color:var(--bg-color);box-shadow:0 .125rem 1.25rem var(--bg-color);border-color:var(--bg-color)}.links .item:hover .image{mix-blend-mode:normal;background-color:var(--bg-color)}.links .item:hover .title{color:var(--header-text-color)}@media (max-width:767px){.links{font-size:1em}.links .item{width:calc(100% - 2rem)}}.md .note{border-radius:.1875rem;margin:1rem 0;padding:1rem;position:relative;background:var(--note-bg,var(--grey-2));color:var(--grey-6);border-left:.25rem solid var(--note-border,var(--grey-4));font-size:.875em;padding-left:2.5rem;--primary-color:var(--note-text)}.md .note::before{position:absolute;left:.5rem;top:calc(50% - 1.5rem);font-family:ic;font-weight:400;font-size:1.5rem;color:var(--note-text,var(--grey-6))}.md .note.no-icon{padding-left:1rem}.md .note.no-icon::before{content:none}::-webkit-details-marker{display:none}details>summary:first-of-type{list-style-type:none}details summary{outline:0;cursor:pointer;padding:.625rem;background:var(--note-bg,none);border-radius:.25rem;transition:all .4s ease}details summary::before{font-family:ic;font-weight:400;color:var(--grey-4);margin-right:.5rem}details>div{padding:.625rem}details.danger,details.info,details.primary,details.success,details.warning{margin:0 0 .8rem}details.danger summary,details.info summary,details.primary summary,details.success summary,details.warning summary{color:var(--note-text)}details.danger summary::before,details.info summary::before,details.primary summary::before,details.success summary::before,details.warning summary::before{color:var(--note-text)}details[open]{background:var(--note-bg,none);border-radius:.25rem;color:var(--grey-6)}details[open] summary{background:var(--note-hover,var(--grey-2));border-radius:.25rem}details[open] summary::before{color:var(--primary-color)}details[open]>div{margin:0 0 .8rem}details[open].danger,details[open].info,details[open].primary,details[open].success,details[open].warning{color:var(--note-text)}details[open].danger>summary,details[open].info>summary,details[open].primary>summary,details[open].success>summary,details[open].warning>summary{color:#fff;border-radius:.25rem .25rem 0 0}details[open].danger>summary::before,details[open].info>summary::before,details[open].primary>summary::before,details[open].success>summary::before,details[open].warning>summary::before{color:#fff}span.label{display:inline;border-radius:.3rem;border:.0625rem solid;padding:.2rem .3rem;font-family:Inconsolata,consolas,Menlo,-apple-system,"PingFang SC","Microsoft YaHei";font-size:1em;background:var(--note-bg,var(--grey-2));border-color:var(--note-border,var(--grey-4));color:var(--note-text,var(--grey-6))}.tabs{display:block;position:relative}.tabs .nav{overflow:hidden;border-bottom:.0625rem solid var(--grey-1-a7);height:2.6875rem}.tabs .nav ul{display:flex;padding:0;white-space:nowrap;overflow-x:auto}.tabs .nav li{position:relative;cursor:pointer;border:none;display:inline-block;padding:.3125rem 1.25rem;margin:0}.tabs .nav li::before{content:"";position:absolute;left:50%;right:50%;top:auto;bottom:0;transition:all .2s ease-in-out;width:auto;height:auto;background:0 0;border-radius:0;border-bottom:.125rem solid transparent}.tabs .nav li.active::before{left:0;right:0;border-bottom-color:var(--note-hover,var(--primary-color))}.tabs .tab{display:none}.tabs .tab.active{display:block}.tabs .show-btn{position:absolute;cursor:pointer;right:0;bottom:0;width:2.875rem;height:2.875rem;text-align:center;color:var(--grey-4);z-index:1}.tabs .show-btn::before{font-family:ic;font-weight:400}.md .tabs{margin:0 0 2rem;border-radius:.5rem;border:.0625rem solid var(--grey-2);box-shadow:0 .625rem 1.875rem -.9375rem var(--box-bg-shadow)}.md .tabs .tab{padding:1.25rem}.md .note h2,.md .note h3,.md .note h4,.md .note h5,.md .note h6,.md details h2,.md details h3,.md details h4,.md details h5,.md details h6{margin-top:.1875rem;border-bottom:initial;margin-bottom:0;padding-top:0}.md .note p,.md details p{line-height:1.8}.md .note a:not(.btn),.md details a:not(.btn){color:var(--note-hover);border-bottom:.0625rem dashed var(--note-text)}.md .note a:not(.btn):hover,.md details a:not(.btn):hover{color:var(--note-text);border-bottom-color:var(--note-text)}.md .note ul li::before,.md details ul li::before{background:var(--note-text)}.md .note ol li:before,.md details ol li:before{background:var(--note-text,var(--primary-color))!important;width:.8rem;height:.8rem;font-size:.45rem;line-height:.8rem;top:.4rem;left:-1.2rem}.media-container{position:relative;border-radius:.5rem;border:.0625rem solid var(--grey-2);box-shadow:0 .625rem 1.875rem -.9375rem var(--box-bg-shadow);overflow:hidden;margin-bottom:2rem}.player-info{color:var(--text-color);font-size:1em;min-width:16.25rem}.player-info .tabs{border:none;box-shadow:none}.player-info .tabs .show-btn{display:none}.player-info .tabs .tab{padding:0}.player-info .controller{font-family:ic;font-weight:400;cursor:pointer;font-size:1.25em;display:flex;justify-content:space-around;align-items:center;text-align:center}.player-info .controller .btn{color:var(--grey-6);width:18%}.player-info .controller .btn:hover{color:var(--color-pink)}.player-info .play-pause{font-size:130%}.player-info .volume{position:relative}.player-info .volume .bar{position:absolute;height:.25rem;background:var(--color-pink-a3);bottom:0;left:0;transition:all .2s ease-out 0s}.player-info .volume.off .bar{height:0}.player-info .playlist{border-top:.125rem solid var(--grey-9-a1)}.player-info ol{font-size:.8125em;padding:.3125rem 0;margin:.625rem 0 0;height:12.5rem;overflow-x:scroll;counter-reset:counter;position:relative}.player-info ol::-webkit-scrollbar{width:.1875rem;height:.1875rem}.player-info ol>li{display:flex;padding:.3125rem .9375rem .3125rem 1.5625rem;cursor:pointer;transition:all .2s ease-in-out 0s;height:2rem;overflow:hidden}.player-info ol>li.error{opacity:.5;-webkit-text-decoration-line:line-through;text-decoration-line:line-through}.player-info ol>li::before{height:auto;background:0 0!important;border:none!important;position:relative;top:0!important;left:0!important;font-size:inherit;line-height:inherit;margin-left:-1.25rem;width:1.875rem;counter-increment:counter;content:counter(counter);text-align:right;padding-right:.3125rem;color:var(--grey-5)}.player-info ol>li .info{display:block;width:100%}.player-info ol>li .info span:nth-child(2){float:right;margin-left:.625rem;color:var(--grey-5)}.player-info ol>li.current{color:var(--primary-color);position:relative}.player-info ol>li.current::before{font-family:ic;font-weight:400;color:currentColor}.player-info ol>li.current .progress .bar{position:absolute;height:100%;background:var(--color-pink-a3);top:0;left:0;transition:all .2s ease-out 0s}.player-info ol>li.current .progress::before{content:attr(data-ptime) " / " attr(data-dtime);color:var(--grey-5);position:absolute;right:0;padding:0 .3rem}.player-info ol>li.current .progress.seeking::before{color:currentColor}.player-info ol>li.current .info{padding-right:5rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.player-info ol>li.current .info span:nth-child(2){display:none}.player-info ol>li:hover{background-color:var(--grey-1-a7)}.player-info ol>li:hover::before{color:var(--primary-color)}.video{border-radius:0 0 .5rem .5rem}.audio .preview{display:flex;align-items:center;padding:1.25rem 1.25rem 0}@media (max-width:767px){.audio .preview{padding:1.25rem .625rem 0;flex-direction:column}}.audio .preview .info{display:flex;flex-direction:column;margin-left:.625rem;padding:.625rem;text-overflow:ellipsis;overflow:hidden;width:100%}@media (max-width:767px){.audio .preview .info{margin-left:0}}.audio .preview .info h4{margin:0;text-overflow:ellipsis;max-height:3rem;padding:0;overflow:hidden}.audio .preview .info span{font-size:.75em}.audio .preview .lrc{max-height:4rem;margin-top:.3125rem;font-size:.75em;position:relative;text-align:center;overflow:hidden}.audio .preview .lrc .inner{width:100%;transition:all .5s ease-out}.audio .preview .lrc p{font-size:.75rem;color:var(--grey-6);line-height:1rem!important;height:1rem!important;padding:0!important;margin:0!important;transition:all .5s ease-out;opacity:.3;overflow:hidden}.audio .preview .lrc p.current{opacity:1;overflow:visible;height:auto!important;min-height:1rem}.audio .cover{position:relative;flex-shrink:0;cursor:pointer}.audio .cover::before{content:"";position:absolute;z-index:1;background:url("../images/play_needle.png") no-repeat center/contain;width:3.4375rem;height:5.1875rem;top:-1.5625rem;left:-1.5625rem;transform:rotateZ(-60deg);animation:rotate-needle-pause .5s 1 normal linear forwards;transform-origin:0.625rem 0.625rem}.audio .cover .disc{animation:rotate 20s linear infinite;animation-play-state:paused;padding:1.5rem;max-height:144px}.audio .cover .disc::after{content:"";position:absolute;background:url("../images/play_disc.png") no-repeat center/contain;z-index:1;width:100%;height:100%;top:0;left:0}.audio .cover img{-o-object-fit:contain;object-fit:contain;width:6rem;height:6rem;border-radius:50%}.audio.playing .cover::before{animation:rotate-needle-resume .5s 1 normal linear forwards}.audio.playing .cover .disc{animation-play-state:running} /* rebuild by hrmmi */ \ No newline at end of file diff --git a/css/comment.css b/css/comment.css index e3af4d9..9a64fcd 100644 --- a/css/comment.css +++ b/css/comment.css @@ -1,3 +1,3 @@ -/* build time:Mon Oct 21 2024 20:10:22 GMT+0800 (中国标准时间)*/ +/* build time:Mon Oct 21 2024 20:38:37 GMT+0800 (中国标准时间)*/ .v.wrap{animation:none;opacity:0}.v .lozaded{animation:none}.v input,.v textarea{border:none;outline:0;background:0 0;font-size:.75em;transition:all .25s ease}.v a{color:var(--color-aqua)}.v a:hover{color:var(--primary-color)}.v .text-center{text-align:center}.v .text-right{text-align:right}.v .float-right{float:right!important}.v .pd5{padding:5px}.v .pd10{padding:10px}.v .emoji,.v .vemoji{max-height:1.5rem;display:inline;vertical-align:middle}.v .submitting{margin:1em 0}.v .form{border-radius:.5rem;border:.0625rem solid var(--grey-2);box-shadow:0 .625rem 1.875rem -.9375rem var(--box-bg-shadow);margin-bottom:.625rem;position:relative;padding:.625rem}.v .form>.inner .auth-section{display:flex;padding:.3em .6em}@media (max-width:567px){.v .form>.inner{flex-wrap:wrap}}.v .form>.inner .input{flex:1 1 27%;width:27%}@media (max-width:567px){.v .form>.inner .input{flex:1 1 100%;padding-right:0;width:100%}}.v .form>.inner .input input{padding:.625rem .3125rem;width:100%;border-bottom:.0625rem dashed var(--grey-4)}.v .form>.inner .input input:focus{border-bottom-color:var(--primary-color)}.v .form>.inner .textarea{padding:.3em .6em}.v .form>.inner .veditor{width:100%;min-height:8.75em;font-size:.875em;line-height:1.75;resize:vertical}.v .form>.inner .btn-group{padding:.625rem 0;display:flex}.v .form>.inner .btn-group .i-markdown{font-size:.75em}.v .form>.inner .btn-group .left{width:30%}.v .form>.inner .btn-group .right{width:70%;text-align:right}.v .form>.inner .vextra-area{margin:.4em 0 .4em .5em}.v .form>.inner .preview-box{padding:.4375rem 1.25rem;font-size:.8125em;border-radius:.5rem;border:.0625rem solid var(--grey-2);box-shadow:0 .625rem 1.875rem -.9375rem var(--box-bg-shadow)}.v .form>.inner .trigger-section{display:none}.v .form .smile-body img{display:inline-block;width:auto;height:3rem;cursor:pointer;padding:.5rem;border-radius:.25rem;transition:box-shadow .2s ease-out,background .2s ease-out}.v .form .smile-body img:hover,.v .form .smile-body li.active img{background:var(--grey-2);box-shadow:1px 1px 2px var(--grey-1),-1px -1px 2px var(--grey-4),inset 8px 8px 16px var(--grey-1),inset -8px -8px 16px var(--grey-3)}.v .form .smile-body .smile-icons{border-radius:.5rem;margin:.3rem 0;min-height:10rem;max-height:10rem;overflow:auto;background:var(--grey-2);padding:.5rem}.v .form .smile-body .smile-bar li{display:inline-block;margin-right:.25rem}.v .form .smile-body .smile-bar img{width:2.8rem;height:2.8rem;background:var(--grey-2);-o-object-fit:contain;object-fit:contain}.v .form .btn{cursor:pointer;display:inline-block;margin:.5rem;overflow:hidden;vertical-align:middle}.v .form .btn.actived .ic{color:var(--primary-color)}.v .form .btn .ic{color:var(--grey-6);transition:all .25s ease}.v .form .btn .ic:hover{color:var(--color-blue)}.v .form .cancel-reply{position:absolute;right:0;top:0;z-index:1}.v .form .vsubmit{font-size:.875em}.v .msg{position:absolute;background:var(--grey-1-a5);width:100%;height:100%;left:0;top:0}.v .msg .alert{padding:3em 0 0 0}.v .msg .alert .text{color:var(--grey-8);padding:15px}@media (max-width:767px){.v .msg .alert{padding:8em 0}.v .msg .alert .text{padding:10px}}.v .info{padding:.3125rem;font-weight:600;font-size:1.25em}.v .info .col{display:inline-block;padding:0 20px;position:relative;background:0 0;z-index:2}.v .info .col .count{font-size:1.375rem;font-weight:inherit}.v .next .more{cursor:pointer;margin:1em 0}.v .list{width:100%}.v .item{word-break:break-all;padding-top:1.25em}.v .item .avatar{width:3.125em;height:3.125em;float:left;border-radius:50%;margin-right:.7525em;border:.0625rem solid var(--grey-3);padding:.125em}.v .item:hover .avatar{animation:.8s ease-out 0s 1 normal both running shake}.v .item .main{overflow:hidden;padding-bottom:.5em;border-bottom:.0625rem dashed var(--grey-3)}.v .item .head,.v .item .meta{color:var(--grey-5);line-height:1.8}.v .item .name{margin-right:.875rem}.v .item .system,.v .item .tag,.v .item .time{display:inline-block;font-size:.75em}@media (max-width:567px){.v .item .system span{display:none}}.v .item .tag{border-radius:.125rem;color:var(--grey-1);padding:0 .3125rem;margin-right:.5rem;background-color:var(--color-grey)}.v .item .tag.master{background-color:var(--color-orange)}.v .item .tag.friend{background-color:var(--color-aqua)}.v .item .tag.investor{background-color:var(--color-pink)}.v .item .at{color:var(--primary-color)}.v .item .at:hover{color:var(--color-aqua)}.v .item .meta{line-height:1}.v .item .meta .at{float:right}.v .item .content{margin-bottom:.75em;padding:.625em}.v .item .content>.inner{font-size:.875em;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;line-height:2;position:relative}.v .item .content>.inner.expand{cursor:pointer;max-height:8em;overflow:hidden}.v .item .content>.inner.expand::before{display:block;content:"";position:absolute;width:100%;left:0;top:0;bottom:3.15em;background:linear-gradient(180deg,var(--grey-1-a0),var(--grey-1));z-index:999}.v .item .content>.inner.expand::after{display:block;content:attr(data-expand);text-align:center;position:absolute;width:100%;height:3.15em;line-height:3.15em;left:0;bottom:0;z-index:999;background:var(--grey-1)}.v .item .content .form{margin-top:1rem}.v .item:last-child .main{border-bottom:none}.v .item .children{padding-left:1em;border-left:.0625rem dashed var(--grey-3)}.v .item .children .avatar{width:2.225em;height:2.225em}.v .empty{padding:20px;text-align:center}.v .spinner{margin:10px auto;width:50px;height:30px;text-align:center;font-size:10px}.v .spinner>div{background-color:var(--primary-color);height:100%;width:6px;margin-right:3px;display:inline-block;animation:sk-stretchdelay 1.2s infinite ease-in-out}.v .spinner .r2{animation-delay:-1.1s}.v .spinner .r3{animation-delay:-1s}.v .spinner .r4{animation-delay:-.9s}.v .spinner .r5{animation-delay:-.8s}@keyframes sk-stretchdelay{0%,100%,40%{transform:scaleY(.4)}20%{transform:scaleY(1)}}.hljs-comment,.hljs-quote{color:var(--grey-5);font-style:italic}.hljs-keyword,.hljs-meta,.hljs-selector-tag,.hljs-subst{color:var(--grey-7);font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:var(--color-green)}.hljs-doctag,.hljs-string{color:var(--color-red)}.hljs-section,.hljs-selector-id,.hljs-title{color:var(--color-pink);font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:var(--color-grey);font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:var(--color-orange);font-weight:400}.hljs-link,.hljs-regexp{color:var(--color-green)}.hljs-bullet,.hljs-symbol{color:var(--color-purple)}.hljs-built_in,.hljs-builtin-name{color:var(--color-aqua)}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} /* rebuild by hrmmi */ \ No newline at end of file diff --git a/css/mermaid.css b/css/mermaid.css index 6d16dc3..952af3f 100644 --- a/css/mermaid.css +++ b/css/mermaid.css @@ -1,3 +1,3 @@ -/* build time:Mon Oct 21 2024 20:10:22 GMT+0800 (中国标准时间)*/ +/* build time:Mon Oct 21 2024 20:38:37 GMT+0800 (中国标准时间)*/ pre.graphviz,pre.mermaid{background:0 0;padding:0;border:none}pre>svg{display:block;margin:0 auto}pre.graphviz{white-space:normal;max-width:calc(100% - 3.125rem)}pre.graphviz svg>g>polygon{fill:var(--grey-1)}.mermaid.graph svg{max-height:18.75rem}.mermaid .label{font-family:var(--mermaid-font-family);color:var(--grey-7);font-size:.875em}.mermaid .label text{fill:var(--text-color)}.mermaid .node circle,.mermaid .node ellipse,.mermaid .node path,.mermaid .node polygon,.mermaid .node rect{fill:var(--grey-1);stroke:var(--text-color);stroke-width:.0625rem}.mermaid .node .label{text-align:center}.mermaid .node.clickable{cursor:pointer}.mermaid .arrowheadPath{fill:var(--text-color)}.mermaid .edgePath .path{stroke:var(--text-color)}.mermaid .flowchart-link{stroke:var(--text-color);fill:none}.mermaid .edgeLabel{background-color:var(--grey-2);text-align:center}.mermaid .edgeLabel rect{opacity:.8;fill:var(--grey-2)!important}.mermaid .cluster rect{fill:var(--grey-2);stroke:var(--text-color);stroke-width:.0625rem}.mermaid .cluster text{fill:var(--text-color)}.mermaid div.mermaidTooltip{position:absolute;text-align:center;max-width:12.5rem;padding:.125rem;font-family:var(--mermaid-font-family);font-size:.75rem;background:var(--grey-1);border:.0625rem solid var(--text-color);border-radius:.125rem;pointer-events:none;z-index:99}.mermaid .actor{stroke:var(--text-color);fill:var(--grey-1)}.mermaid text.actor{fill:var(--text-color);stroke:none}.mermaid .actor-line{stroke:var(--text-color)}.mermaid .messageLine0{stroke-width:1.5;stroke-dasharray:'2 2';stroke:var(--text-color)}.mermaid .messageLine1{stroke-width:1.5;stroke-dasharray:'2 2';stroke:var(--text-color)}.mermaid #arrowhead{fill:var(--text-color)}.mermaid .sequenceNumber{fill:#fff}.mermaid #sequencenumber{fill:var(--text-color)}.mermaid #crosshead path{fill:var(--text-color)!important;stroke:var(--text-color)!important}.mermaid .messageText{fill:var(--text-color);stroke:none}.mermaid .labelBox{stroke:var(--text-color);fill:var(--grey-3)}.mermaid .labelText{fill:var(--text-color);stroke:none}.mermaid .loopText{fill:var(--text-color);stroke:none}.mermaid .loopLine{stroke-width:2;stroke-dasharray:'2 2';stroke:var(--text-color)}.mermaid .note{stroke:var(--grey-4);fill:var(--grey-3)}.mermaid .noteText{fill:#000;stroke:none;font-family:var(--mermaid-font-family);font-size:.875rem}.mermaid .activation0{fill:#f4f4f4;stroke:#666}.mermaid .activation1{fill:#f4f4f4;stroke:#666}.mermaid .activation2{fill:#f4f4f4;stroke:#666}.mermaid .mermaid-main-font{font-family:var(--mermaid-font-family)}.mermaid .section{stroke:none;opacity:.2}.mermaid .section0{fill:var(--grey-1)}.mermaid .section2{fill:var(--grey-1)}.mermaid .section1,.mermaid .section3{fill:var(--grey-0);opacity:.2}.mermaid .sectionTitle0{fill:var(--text-color)}.mermaid .sectionTitle1{fill:var(--text-color)}.mermaid .sectionTitle2{fill:var(--text-color)}.mermaid .sectionTitle3{fill:var(--text-color)}.mermaid .sectionTitle{text-anchor:start;font-size:.6875rem;text-height:0.875rem;font-family:var(--mermaid-font-family)}.mermaid .grid .tick{stroke:var(--text-color);opacity:.8;shape-rendering:crispEdges}.mermaid .grid .tick text{font-family:var(--mermaid-font-family)}.mermaid .grid path{stroke-width:0}.mermaid .today{fill:none;stroke:var(--primary-color);stroke-width:.125rem}.mermaid .task{stroke-width:2}.mermaid .taskText{text-anchor:middle;font-family:var(--mermaid-font-family)}.mermaid .taskText:not([font-size]){font-size:.6875rem}.mermaid .taskTextOutsideRight{fill:var(--text-color);text-anchor:start;font-size:.6875rem;font-family:var(--mermaid-font-family)}.mermaid .taskTextOutsideLeft{fill:var(--text-color);text-anchor:end;font-size:.6875rem}.mermaid .task.clickable{cursor:pointer}.mermaid .taskText.clickable{cursor:pointer;fill:#003163!important;font-weight:700}.mermaid .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163!important;font-weight:700}.mermaid .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163!important;font-weight:700}.mermaid .taskText0,.mermaid .taskText1,.mermaid .taskText2,.mermaid .taskText3{fill:var(--grey-1)}.mermaid .task0,.mermaid .task1,.mermaid .task2,.mermaid .task3{fill:var(--text-color);stroke:var(--text-color)}.mermaid .taskTextOutside0,.mermaid .taskTextOutside2{fill:var(--color-blue)}.mermaid .taskTextOutside1,.mermaid .taskTextOutside3{fill:var(--color-purple)}.mermaid .active0,.mermaid .active1,.mermaid .active2,.mermaid .active3{fill:var(--grey-1);stroke:var(--text-color)}.mermaid .activeText0,.mermaid .activeText1,.mermaid .activeText2,.mermaid .activeText3{fill:var(--text-color)!important}.mermaid .done0,.mermaid .done1,.mermaid .done2,.mermaid .done3{stroke:var(--color-green);fill:var(--grey-1);stroke-width:2}.mermaid .doneText0,.mermaid .doneText1,.mermaid .doneText2,.mermaid .doneText3{fill:var(--text-color)!important}.mermaid .crit0,.mermaid .crit1,.mermaid .crit2,.mermaid .crit3{stroke:var(--primary-color);fill:var(--primary-color);stroke-width:2}.mermaid .activeCrit0,.mermaid .activeCrit1,.mermaid .activeCrit2,.mermaid .activeCrit3{stroke:var(--primary-color);fill:var(--grey-1);stroke-width:2}.mermaid .doneCrit0,.mermaid .doneCrit1,.mermaid .doneCrit2,.mermaid .doneCrit3{stroke:var(--text-color);fill:var(--text-color);stroke-width:2;cursor:pointer;shape-rendering:crispEdges}.mermaid .milestone{transform:rotate(45deg) scale(.8,.8)}.mermaid .milestoneText{font-style:italic}.mermaid .doneCritText0,.mermaid .doneCritText1,.mermaid .doneCritText2,.mermaid .doneCritText3{fill:var(--color-green)!important}.mermaid .activeCritText0,.mermaid .activeCritText1,.mermaid .activeCritText2,.mermaid .activeCritText3{fill:var(--text-color)!important}.mermaid .titleText{text-anchor:middle;font-size:1.125rem;fill:var(--text-color);font-family:var(--mermaid-font-family)}.mermaid g.classGroup text{fill:var(--text-color);stroke:none;font-family:var(--mermaid-font-family);font-size:.625rem}.mermaid g.classGroup text .title{font-weight:bolder}.mermaid g.clickable{cursor:pointer}.mermaid g.classGroup rect{fill:var(--grey-1);stroke:var(--text-color)}.mermaid g.classGroup line{stroke:var(--text-color);stroke-width:1}.mermaid .classLabel .box{stroke:none;stroke-width:0;fill:var(--grey-1);opacity:.5}.mermaid .classLabel .label{fill:var(--text-color);font-size:.625rem}.mermaid .relation{stroke:var(--text-color);stroke-width:1;fill:none}.mermaid .dashed-line{stroke-dasharray:3}@mixin composition{fill:var(--text-color);stroke:var(--text-color);stroke-width:1}@include composition;@include composition;@mixin aggregation{fill:var(--grey-1);stroke:var(--text-color);stroke-width:1}@include aggregation;@include aggregation;@include composition;@include composition;@include composition;@include composition;.mermaid .branch-label,.mermaid .commit-id,.mermaid .commit-msg{fill:#d3d3d3;color:#d3d3d3;font-family:var(--mermaid-font-family)}.mermaid .pieTitleText{text-anchor:middle;font-size:1.5625rem;fill:$taskTextDarkColor;font-family:var(--mermaid-font-family)}.mermaid .slice{font-family:var(--mermaid-font-family)}.mermaid g.stateGroup text{fill:var(--text-color);stroke:none;font-size:.625rem;font-family:var(--mermaid-font-family)}.mermaid g.stateGroup text{fill:var(--text-color);stroke:none;font-size:.625rem}.mermaid g.stateGroup .state-title{font-weight:bolder;fill:#000}.mermaid g.stateGroup rect{fill:var(--grey-1);stroke:var(--text-color)}.mermaid g.stateGroup line{stroke:var(--text-color);stroke-width:1}.mermaid .transition{stroke:var(--text-color);stroke-width:1;fill:none}.mermaid .stateGroup .composit{fill:#fff;border-bottom:.0625rem}.mermaid .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:.0625rem}.mermaid .state-note{stroke:var(--grey-4);fill:var(--grey-3)}.mermaid .state-note text{fill:#000;stroke:none;font-size:.625rem}.mermaid .stateLabel .box{stroke:none;stroke-width:0;fill:var(--grey-1);opacity:.5}.mermaid .stateLabel text{fill:#000;font-size:.625rem;font-weight:700;font-family:var(--mermaid-font-family)}.mermaid .node circle.state-start{fill:#000;stroke:#000}.mermaid .node circle.state-end{fill:#000;stroke:#fff;stroke-width:1.5}.mermaid #statediagram-barbEnd{fill:var(--text-color)}.mermaid .statediagram-cluster rect{fill:var(--grey-1);stroke:var(--text-color);stroke-width:.0625rem}.mermaid .statediagram-cluster rect.outer{rx:0.3125rem;ry:0.3125rem}.mermaid .statediagram-state .divider{stroke:var(--text-color)}.mermaid .statediagram-state .title-state{rx:0.3125rem;ry:0.3125rem}.mermaid .statediagram-cluster.statediagram-cluster .inner{fill:#fff}.mermaid .statediagram-cluster.statediagram-cluster-alt .inner{fill:#e0e0e0}.mermaid .statediagram-cluster .inner{rx:0;ry:0}.mermaid .statediagram-state rect.basic{rx:0.3125rem;ry:0.3125rem}.mermaid .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#efefef}.mermaid .note-edge{stroke-dasharray:5}.mermaid .statediagram-note rect{stroke:var(--grey-4);fill:var(--grey-3);stroke-width:.0625rem;rx:0;ry:0}:root{--mermaid-font-family:"trebuchet ms",verdana,arial} /* rebuild by hrmmi */ \ No newline at end of file diff --git a/index.html b/index.html index 9e14aeb..4bb5b0c 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -R1ck's Playground = how

文章列表

9.2k 8 分钟

参考链接:Google C++ Style Guide # 头文件 每个 .cpp 源文件都应该有一个相对应的 .h 头文件 每个头文件都应该有头文件守卫,守卫格式统一为: _&lt;PROJECT&gt;_&lt;PATH&gt;_&lt;FILE&gt;_H_ 对于头文件中对于 inline 内联函数的定义,要求函数体在十行以内,且不包括循环、switch 语句。 头文件的 include 顺序:相关头文件、C 系统头文件、C++ 头文件、其他库头文件、项目头文件 (这里的 “相关头文件” 指的是与该源文件同名的头文件) #...
3.4k 3 分钟

# 一、可变参数模板 #CPP11 # 1. 概述 C++ 的可变参数模板(Variadic Templates)是一种强大的特性,使得模板能够接受可变数量的参数。这一特性引入于 C++11,允许程序员创建更加灵活和通用的代码。 可变参数模板使得函数模板和类模板能够接受任意数量的模板参数,无论是类型参数还是非类型参数。它们为泛型编程提供了更大的灵活性。 # 2. 基本语法 可以通过使用省略号 ... 来定义参数包。 函数模板: 1234template&lt;typename... Args&gt;void func(Args... args) &#123; //...
1.4k 1 分钟

# i、lambda 表达式 vs 函数 在 C++ 中,定义 lambda 表达式和定义普通函数有以下几个区别和好处: 简洁性:相对于显式定义一个命名函数,lambda 表达式更加简洁。您可以在需要的地方直接定义 lambda 表达式,而无需显式命名函数。 匿名性:lambda 表达式是匿名的,它不需要命名,并且可以直接作为参数传递给其他函数或算法。 上下文捕获:lambda 表达式可以捕获外部作用域中的变量,包括局部变量、函数参数等。这使得它们能够在函数对象中保持状态,并在需要时访问和修改这些变量。 类型推导:lambda 表达式可以通过类型推导来自动推断其参数类型和返回类型。这使得编写...
1.6k 1 分钟

extern &quot;C&quot; 是 C++ 中用于与 C 语言进行互操作的重要关键字。它主要用于告诉编译器在链接阶段如何处理函数或变量名。下面是详细介绍 extern &quot;C&quot; 的背景、用途、以及使用方法: # 1. 背景: 在 C 和 C++ 中,函数和变量的符号是通过名字来链接的,但 C++ 使用了 &quot;名字修饰&quot;(Name Mangling)来支持函数重载等特性。这种修饰会导致编译器生成的函数名不再是原始的函数名,而是带有类名、参数类型等信息的复杂字符串。 例如,C++ 中一个函数 int...
6.5k 6 分钟

# 一、概述 C++ 异常机制是一种用于处理程序运行时错误和异常情况的机制。它允许在发生异常时抛出异常对象(通过 throw 语句),并在适当的地方捕获并处理这些异常(通过 try-catch 块)。 异常的基本流程如下: throw 语句:当程序运行到某个错误或异常的情况时,可以使用 throw 语句抛出一个异常对象。异常对象可以是内置类型、自定义类型或标准库中的异常类。 try-catch 块:使用 try 关键字开始一个代码块,该代码块可能会抛出异常。在 try 块中,程序会监视是否有异常被抛出,如果有,就会跳到与之匹配的 catch 块。 catch 块: catch...
6.9k 6 分钟

# 一、C++ 文件 IO 读写 参考教程:C++ 文件和流 在读写文件的时候,每读取或者写入一个字节,磁盘中的文件指针就会向后移动一个字节。可以通过控制指针的位置,以便在我们需要的位置进行读写文件: tellg()  返回读取文件指针的当前位置 tellp()  返回写入文件指针的当前位置 seekg(指针偏移量)  将读取文件指针移到指定位置 seekg(指针偏移量,参照位置)  将读取文件指针移到指定位置 seekp(指针偏移量)  将写入文件指针移到指定位置 seekp(指针偏移量,参照位置)...
33k 30 分钟

# i、右值引用 # 概述 右值引用是 C++11 引入的一种新的引用类型,用于支持移动语义和完美转发。与传统的左值引用(左值引用)不同,右值引用绑定到临时对象、将要销毁的对象或者显式转换为右值引用的对象。右值引用的特点是可以接管资源,并实现高效的资源移动操作。 在 C++ 中,我们可以通过在类型名称前添加 &amp;&amp; 来声明右值引用。例如, int&amp;&amp;...
9.5k 9 分钟

# i、C++ 抽象类 # ii、什么是抽象类 在 C++ 中,抽象类是一个特殊的类,它包含了至少一个纯虚函数(pure virtual function)。纯虚函数是在函数声明后面加上 &quot;= 0&quot;...
5.4k 5 分钟

# 一、概述 C++ 中的流(streams)是用于处理输入和输出的抽象概念,主要用于与设备(如键盘、屏幕、文件等)进行数据交互。流在 C++ 中主要由以下几个方面组成: # 1. 流的类型 C++ 中有两种主要的流类型: 输入流(Input Stream):用于从输入设备读取数据。最常用的输入流是 std::cin ,它用于从标准输入(通常是键盘)读取数据。 输出流(Output Stream):用于将数据输出到输出设备。最常用的输出流是 std::cout ,它用于将数据输出到标准输出(通常是屏幕)。 # 2. 流的类 在 C++...
4.3k 4 分钟

# i、基本概念 # 继承关系中的构造函数 在 C++ 的继承关系中,派生类的构造函数必须在其初始化列表中调用基类的构造函数来构造基类部分。 在派生类的构造函数中,构造基类部分的步骤是隐式的,但必须确保基类的构造函数被正确调用,以初始化基类的成员。这是因为派生类不仅包含自己的成员,还继承了基类的成员,这些成员需要在构造过程中得到适当的初始化。 构造函数调用基类构造函数的方式是在派生类构造函数的初始化列表中使用基类的构造函数,并提供适当的参数。 以下是一个示例: 123456789101112131415161718192021222324252627class Base...
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file +R1ck's Playground = how

文章列表

9.1k 8 分钟

参考链接:Google C++ Style Guide # 头文件 每个 .cpp 源文件都应该有一个相对应的 .h 头文件 每个头文件都应该有头文件守卫,守卫格式统一为: _&lt;PROJECT&gt;_&lt;PATH&gt;_&lt;FILE&gt;_H_ 对于头文件中对于 inline 内联函数的定义,要求函数体在十行以内,且不包括循环、switch 语句。 头文件的 include 顺序:相关头文件、C 系统头文件、C++ 头文件、其他库头文件、项目头文件 (这里的 “相关头文件” 指的是与该源文件同名的头文件) #...
3.4k 3 分钟

# 一、可变参数模板 #CPP11 # 1. 概述 C++ 的可变参数模板(Variadic Templates)是一种强大的特性,使得模板能够接受可变数量的参数。这一特性引入于 C++11,允许程序员创建更加灵活和通用的代码。 可变参数模板使得函数模板和类模板能够接受任意数量的模板参数,无论是类型参数还是非类型参数。它们为泛型编程提供了更大的灵活性。 # 2. 基本语法 可以通过使用省略号 ... 来定义参数包。 函数模板: 1234template&lt;typename... Args&gt;void func(Args... args) &#123; //...
1.4k 1 分钟

# i、lambda 表达式 vs 函数 在 C++ 中,定义 lambda 表达式和定义普通函数有以下几个区别和好处: 简洁性:相对于显式定义一个命名函数,lambda 表达式更加简洁。您可以在需要的地方直接定义 lambda 表达式,而无需显式命名函数。 匿名性:lambda 表达式是匿名的,它不需要命名,并且可以直接作为参数传递给其他函数或算法。 上下文捕获:lambda 表达式可以捕获外部作用域中的变量,包括局部变量、函数参数等。这使得它们能够在函数对象中保持状态,并在需要时访问和修改这些变量。 类型推导:lambda 表达式可以通过类型推导来自动推断其参数类型和返回类型。这使得编写...
1.6k 1 分钟

extern &quot;C&quot; 是 C++ 中用于与 C 语言进行互操作的重要关键字。它主要用于告诉编译器在链接阶段如何处理函数或变量名。下面是详细介绍 extern &quot;C&quot; 的背景、用途、以及使用方法: # 1. 背景: 在 C 和 C++ 中,函数和变量的符号是通过名字来链接的,但 C++ 使用了 &quot;名字修饰&quot;(Name Mangling)来支持函数重载等特性。这种修饰会导致编译器生成的函数名不再是原始的函数名,而是带有类名、参数类型等信息的复杂字符串。 例如,C++ 中一个函数 int...
6.5k 6 分钟

# 一、概述 C++ 异常机制是一种用于处理程序运行时错误和异常情况的机制。它允许在发生异常时抛出异常对象(通过 throw 语句),并在适当的地方捕获并处理这些异常(通过 try-catch 块)。 异常的基本流程如下: throw 语句:当程序运行到某个错误或异常的情况时,可以使用 throw 语句抛出一个异常对象。异常对象可以是内置类型、自定义类型或标准库中的异常类。 try-catch 块:使用 try 关键字开始一个代码块,该代码块可能会抛出异常。在 try 块中,程序会监视是否有异常被抛出,如果有,就会跳到与之匹配的 catch 块。 catch 块: catch...
6.9k 6 分钟

# 一、C++ 文件 IO 读写 参考教程:C++ 文件和流 在读写文件的时候,每读取或者写入一个字节,磁盘中的文件指针就会向后移动一个字节。可以通过控制指针的位置,以便在我们需要的位置进行读写文件: tellg()  返回读取文件指针的当前位置 tellp()  返回写入文件指针的当前位置 seekg(指针偏移量)  将读取文件指针移到指定位置 seekg(指针偏移量,参照位置)  将读取文件指针移到指定位置 seekp(指针偏移量)  将写入文件指针移到指定位置 seekp(指针偏移量,参照位置)...
33k 30 分钟

# i、右值引用 # 概述 右值引用是 C++11 引入的一种新的引用类型,用于支持移动语义和完美转发。与传统的左值引用(左值引用)不同,右值引用绑定到临时对象、将要销毁的对象或者显式转换为右值引用的对象。右值引用的特点是可以接管资源,并实现高效的资源移动操作。 在 C++ 中,我们可以通过在类型名称前添加 &amp;&amp; 来声明右值引用。例如, int&amp;&amp;...
9.5k 9 分钟

# i、C++ 抽象类 # ii、什么是抽象类 在 C++ 中,抽象类是一个特殊的类,它包含了至少一个纯虚函数(pure virtual function)。纯虚函数是在函数声明后面加上 &quot;= 0&quot;...
5.4k 5 分钟

# 一、概述 C++ 中的流(streams)是用于处理输入和输出的抽象概念,主要用于与设备(如键盘、屏幕、文件等)进行数据交互。流在 C++ 中主要由以下几个方面组成: # 1. 流的类型 C++ 中有两种主要的流类型: 输入流(Input Stream):用于从输入设备读取数据。最常用的输入流是 std::cin ,它用于从标准输入(通常是键盘)读取数据。 输出流(Output Stream):用于将数据输出到输出设备。最常用的输出流是 std::cout ,它用于将数据输出到标准输出(通常是屏幕)。 # 2. 流的类 在 C++...
4.3k 4 分钟

# i、基本概念 # 继承关系中的构造函数 在 C++ 的继承关系中,派生类的构造函数必须在其初始化列表中调用基类的构造函数来构造基类部分。 在派生类的构造函数中,构造基类部分的步骤是隐式的,但必须确保基类的构造函数被正确调用,以初始化基类的成员。这是因为派生类不仅包含自己的成员,还继承了基类的成员,这些成员需要在构造过程中得到适当的初始化。 构造函数调用基类构造函数的方式是在派生类构造函数的初始化列表中使用基类的构造函数,并提供适当的参数。 以下是一个示例: 123456789101112131415161718192021222324252627class Base...
170k 字 2:35
基于 Hexo & Theme.Shoka
\ No newline at end of file diff --git a/js/app.js b/js/app.js index 8eec0df..710de78 100644 --- a/js/app.js +++ b/js/app.js @@ -1,4 +1,4 @@ -// build time:Mon Oct 21 2024 20:10:21 GMT+0800 (中国标准时间) +// build time:Mon Oct 21 2024 20:38:35 GMT+0800 (中国标准时间) var e={version:"0.2.5",hostname:"https://how2051.github.io",root:"/",statics:"/",favicon:{normal:"images/favicon.ico",hidden:"images/failure.ico"},darkmode:false,auto_scroll:true,js:{valine:"gh/amehime/MiniValine@4.2.2-beta10/dist/MiniValine.min.js",chart:"npm/frappe-charts@1.5.0/dist/frappe-charts.min.iife.min.js",copy_tex:"npm/katex@0.12.0/dist/contrib/copy-tex.min.js",fancybox:"combine/npm/jquery@3.5.1/dist/jquery.min.js,npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js,npm/justifiedGallery@3.8.1/dist/js/jquery.justifiedGallery.min.js"},css:{valine:"css/comment.css",katex:"npm/katex@0.12.0/dist/katex.min.css",mermaid:"css/mermaid.css",fancybox:"combine/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css,npm/justifiedGallery@3.8.1/dist/css/justifiedGallery.min.css"},loader:{start:false,"switch":false},search:{appID:null,apiKey:null,indexName:null,hits:{per_page:10}},valine:{appId:null,appKey:null,placeholder:"ヽ(○´∀`)ノ♪",avatar:"mp",pageSize:10,lang:"en",visitor:true,NoRecordIP:false,serverURLs:null,powerMode:true,tagMeta:{visitor:"新朋友",master:"主人",friend:"小伙伴",investor:"金主粑粑"},tagColor:{master:"var(--color-orange)",friend:"var(--color-aqua)",investor:"var(--color-pink)"},tagMember:{master:null,friend:null,investor:null}},quicklink:{timeout:3e3,priority:true},fireworks:["rgba(255,182,185,.9)","rgba(250,227,217,.9)","rgba(187,222,214,.9)","rgba(138,198,209,.9)"]};const t=function(e,t){return Math.floor(Math.random()*(t-e+1))+e};const a=function(){return u("main > .inner").offsetHeight};const n=function(e,t,a){if(a){t()}else{var n=document.createElement("script");n.onload=n.onreadystatechange=function(e,a){if(a||!n.readyState||/loaded|complete/.test(n.readyState)){n.onload=n.onreadystatechange=null;n=undefined;if(!a&&t)setTimeout(t,0)}};n.src=e;document.head.appendChild(n)}};const i=function(t,a){var n=e[t][a];if(n.indexOf("npm")>-1||n.indexOf("gh")>-1||n.indexOf("combine")>-1)return"//cdn.jsdelivr.net/"+n;if(n.indexOf("http")>-1)return n;return m+n};const r=function(e,t,a){if(LOCAL[e]){n(i("js",e),t||function(){window[e]=true},a||window[e])}};const s=function(e,t){if(window["css"+e])return;if(LOCAL[e]){document.head.createChild("link",{rel:"stylesheet",href:i("css",e)});window["css"+e]=true}};const o=function(e){var t=e.text||e.textContent||e.innerHTML||"";var a=e.parentNode;a.removeChild(e);var n=document.createElement("script");if(e.id){n.id=e.id}if(e.className){n.className=e.className}if(e.type){n.type=e.type}if(e.src){n.src=e.src;n.async=false}if(e.dataset.pjax!==undefined){n.dataset.pjax=""}if(t!==""){n.appendChild(document.createTextNode(t))}a.appendChild(n)};const c=function(e,t,a){var n={targets:typeof t=="number"?e.parentNode:document.scrollingElement,duration:500,easing:"easeInOutQuad",scrollTop:t||(typeof e=="number"?e:e?e.top()+document.documentElement.scrollTop-R:0),complete:function(){a&&a()}};anime(n)};const l=function(e,t,a){var n={};var i="none";switch(t){case 0:n={opacity:[1,0]};break;case 1:n={opacity:[0,1]};i="block";break;case"bounceUpIn":n={begin:function(t){e.display("block")},translateY:[{value:-60,duration:200},{value:10,duration:200},{value:-5,duration:200},{value:0,duration:200}],opacity:[0,1]};i="block";break;case"shrinkIn":n={begin:function(t){e.display("block")},scale:[{value:1.1,duration:300},{value:1,duration:200}],opacity:1};i="block";break;case"slideRightIn":n={begin:function(t){e.display("block")},translateX:[100,0],opacity:[0,1]};i="block";break;case"slideRightOut":n={translateX:[0,100],opacity:[1,0]};break;default:n=t;i=t.display;break}anime(Object.assign({targets:e,duration:200,easing:"linear"},n)).finished.then(function(){e.display(i);a&&a()})};const d={get:function(e){return localStorage.getItem(e)},set:function(e,t){localStorage.setItem(e,t);return t},del:function(e){localStorage.removeItem(e)}};const u=function(e,t){t=t||document;if(e.indexOf("#")===0){return t.getElementById(e.replace("#",""))}return t.querySelector(e)};u.all=function(e,t){t=t||document;return t.querySelectorAll(e)};u.each=function(e,t,a){return u.all(e,a).forEach(t)};Object.assign(HTMLElement.prototype,{createChild:function(e,t,a){var n=document.createElement(e);Object.assign(n,t);switch(a){case"after":this.insertAfter(n);break;case"replace":this.innerHTML="";default:this.appendChild(n)}return n},wrap:function(e){var t=document.createElement("div");Object.assign(t,e);this.parentNode.insertBefore(t,this);this.parentNode.removeChild(this);t.appendChild(this)},height:function(e){if(e){this.style.height=typeof e=="number"?e+"rem":e}return this.getBoundingClientRect().height},width:function(e){if(e){this.style.width=typeof e=="number"?e+"rem":e}return this.getBoundingClientRect().width},top:function(){return this.getBoundingClientRect().top},left:function(){return this.getBoundingClientRect().left},attr:function(e,t){if(t===null){return this.removeAttribute(e)}if(t){this.setAttribute(e,t);return this}else{return this.getAttribute(e)}},insertAfter:function(e){var t=this.parentNode;if(t.lastChild==this){t.appendChild(e)}else{t.insertBefore(e,this.nextSibling)}},display:function(e){if(e==null){return this.style.display}else{this.style.display=e;return this}},child:function(e){return u(e,this)},find:function(e){return u.all(e,this)},_class:function(e,t,a){var n=t.indexOf(" ")?t.split(" "):[t];var i=this;n.forEach(function(t){if(e=="toggle"){i.classList.toggle(t,a)}else{i.classList[e](t)}})},addClass:function(e){this._class("add",e);return this},removeClass:function(e){this._class("remove",e);return this},toggleClass:function(e,t){this._class("toggle",e,t);return this},hasClass:function(e){return this.classList.contains(e)}});var f=null;const h=/mobile/i.test(window.navigator.userAgent);const p=function(e,t){var a={type:"audio",mode:"random",btns:["play-pause","music"],controls:["mode","backward","play-pause","forward","volume"],events:{"play-pause":function(t){if(i.paused){e.player.play()}else{e.player.pause()}},music:function(e){if(r.el.hasClass("show")){r.hide()}else{r.el.addClass("show");s.scroll().title()}}}},n={random:function(e){return Math.floor(Math.random()*e)},parse:function(e){var t=[];[["music.163.com.*song.*id=(\\d+)","netease","song"],["music.163.com.*album.*id=(\\d+)","netease","album"],["music.163.com.*artist.*id=(\\d+)","netease","artist"],["music.163.com.*playlist.*id=(\\d+)","netease","playlist"],["music.163.com.*discover/toplist.*id=(\\d+)","netease","playlist"],["y.qq.com.*song/(\\w+).html","tencent","song"],["y.qq.com.*album/(\\w+).html","tencent","album"],["y.qq.com.*singer/(\\w+).html","tencent","artist"],["y.qq.com.*playsquare/(\\w+).html","tencent","playlist"],["y.qq.com.*playlist/(\\w+).html","tencent","playlist"],["xiami.com.*song/(\\w+)","xiami","song"],["xiami.com.*album/(\\w+)","xiami","album"],["xiami.com.*artist/(\\w+)","xiami","artist"],["xiami.com.*collect/(\\w+)","xiami","playlist"]].forEach(function(a){var n=new RegExp(a[0]);var i=n.exec(e);if(i!==null){t=[a[1],a[2],i[1]]}});return t},fetch:function(e){var t=[];return new Promise(function(a,i){e.forEach(function(e){var i=n.parse(e);if(i[0]){var r=JSON.stringify(i);var s=d.get(r);if(s){t.push.apply(t,JSON.parse(s));a(t)}else{fetch("https://api.i-meto.com/meting/api?server="+i[0]+"&type="+i[1]+"&id="+i[2]+"&r="+Math.random()).then(function(e){return e.json()}).then(function(e){d.set(r,JSON.stringify(e));t.push.apply(t,e);a(t)}).catch(function(e){})}}else{t.push(e);a(t)}})})},secondToTime:function(e){var t=function(e){return isNaN(e)?"00":e<10?"0"+e:""+e};var a=Math.floor(e/3600);var n=Math.floor((e-a*3600)/60);var i=Math.floor(e-a*3600-n*60);return(a>0?[a,n,i]:[n,i]).map(t).join(":")},nameMap:{dragStart:h?"touchstart":"mousedown",dragMove:h?"touchmove":"mousemove",dragEnd:h?"touchend":"mouseup"}},i=null;e.player={_id:n.random(999999),group:true,load:function(e){var t="";var a=this;if(e&&e.length>0){if(this.options.rawList!==e){this.options.rawList=e;s.clear()}}else{t="none";this.pause()}for(var n in g.el){g.el[n].display(t)}return this},fetch:function(){var e=this;return new Promise(function(t,a){if(s.data.length>0){t()}else{if(e.options.rawList){var i=[];e.options.rawList.forEach(function(t,a){i.push(new Promise(function(i,r){var o=a;var c;if(!t.list){o=0;e.group=false;c=[t]}else{e.group=true;c=t.list}n.fetch(c).then(function(e){s.add(o,e);i()})}))});Promise.all(i).then(function(){t(true)})}}}).then(function(t){if(t){s.create();m.create();e.mode()}})},mode:function(){var e=s.data.length;if(!e||s.errnum==e)return;var t=m.step=="next"?1:-1;var a=function(){var a=s.index+t;if(a>e||a<0){a=m.step=="next"?0:e-1}s.index=a};var i=function(){var t=n.random(e);if(s.index!==t){s.index=t}else{a()}};switch(this.options.mode){case"random":i();break;case"order":a();break;case"loop":if(m.step)a();if(s.index==-1)i();break}this.init()},"switch":function(e){if(typeof e=="number"&&e!=s.index&&s.current()&&!s.current().error){s.index=e;this.init()}},init:function(){var e=s.current();if(!e||e["error"]){this.mode();return}var t=false;if(!i.paused){t=true;this.stop()}i.attr("src",e.url);i.attr("title",e.name+" - "+e.artist);this.volume(d.get("_PlayerVolume")||"0.7");this.muted(d.get("_PlayerMuted"));p.create();if(this.options.type=="audio")l.create();if(t==true){this.play()}},play:function(){f&&f.player.pause();if(s.current().error){this.mode();return}var e=this;i.play().then(function(){s.scroll()}).catch(function(e){})},pause:function(){i.pause();document.title=y},stop:function(){i.pause();i.currentTime=0;document.title=y},seek:function(e){e=Math.max(e,0);e=Math.min(e,i.duration);i.currentTime=e;p.update(e/i.duration)},muted:function(e){if(e=="muted"){i.muted=e;d.set("_PlayerMuted",e);m.update(0)}else{d.del("_PlayerMuted");i.muted=false;m.update(i.volume)}},volume:function(e){if(!isNaN(e)){m.update(e);d.set("_PlayerVolume",e);i.volume=e}},mini:function(){r.hide()}};var r={el:null,create:function(){if(this.el)return;this.el=e.createChild("div",{className:"player-info",innerHTML:(e.player.options.type=="audio"?'
':"")+'
'},"after");l.el=this.el.child(".preview");s.el=this.el.child(".playlist");m.el=this.el.child(".controller")},hide:function(){var e=this.el;e.addClass("hide");window.setTimeout(function(){e.removeClass("show hide")},300)}};var s={el:null,data:[],index:-1,errnum:0,add:function(e,t){var a=this;t.forEach(function(t,n){t.group=e;t.name=t.name||t.title||"Meida name";t.artist=t.artist||t.author||"Anonymous";t.cover=t.cover||t.pic;t.type=t.type||"normal";a.data.push(t)})},clear:function(){this.data=[];this.el.innerHTML="";if(this.index!==-1){this.index=-1;e.player.fetch()}},create:function(){var t=this.el;this.data.map(function(a,n){if(a.el)return;var r="list-"+e.player._id+"-"+a.group;var o=u("#"+r);if(!o){o=t.createChild("div",{id:r,className:e.player.group?"tab":"",innerHTML:"
    "});if(e.player.group){o.attr("data-title",e.player.options.rawList[a.group]["title"]).attr("data-id",e.player._id)}}a.el=o.child("ol").createChild("li",{title:a.name+" - "+a.artist,innerHTML:''+a.name+""+a.artist+"",onclick:function(t){var a=t.currentTarget;if(s.index===n&&p.el){if(i.paused){e.player.play()}else{e.player.seek(i.duration*p.percent(t,a))}return}e.player.switch(n);e.player.play()}});return a});he()},current:function(){return this.data[this.index]},scroll:function(){var e=this.current();var t=this.el.child("li.active");t&&t.removeClass("active");var a=this.el.child(".tab.active");a&&a.removeClass("active");t=this.el.find(".nav li")[e.group];t&&t.addClass("active");a=this.el.find(".tab")[e.group];a&&a.addClass("active");c(e.el,e.el.offsetTop);return this},title:function(){if(i.paused)return;var e=this.current();document.title="Now Playing..."+e["name"]+" - "+e["artist"]+" | "+y},error:function(){var e=this.current();e.el.removeClass("current").addClass("error");e.error=true;this.errnum++}};var o={el:null,data:null,index:0,create:function(e){var t=s.index;var a=this;var n=s.current().lrc;var i=function(n){if(t!==s.index)return;a.data=a.parse(n);var i="";a.data.forEach(function(e,t){i+=""+e[1]+"

    "});a.el=e.createChild("div",{className:"inner",innerHTML:i},"replace");a.index=0};if(n.startsWith("http"))this.fetch(n,i);else i(n)},update:function(e){if(!this.data)return;if(this.index>this.data.length-1||e=this.data[this.index+1][0])){for(var t=0;t=this.data[t][0]&&(!this.data[t+1]||e/g,"").replace(/^\s+|\s+$/g,"");if(r){const o=r.length;for(var c=0;c
    '+'

    '+t.name+"

    "+t.artist+""+'
    ';this.el.child(".cover").addEventListener("click",e.player.options.events["play-pause"]);o.create(this.el.child(".lrc"))}};var p={el:null,bar:null,create:function(){var e=s.current().el;if(e){if(this.el){this.el.parentNode.removeClass("current").removeEventListener(n.nameMap.dragStart,this.drag);this.el.remove()}this.el=e.createChild("div",{className:"progress"});this.el.attr("data-dtime",n.secondToTime(0));this.bar=this.el.createChild("div",{className:"bar"});e.addClass("current");e.addEventListener(n.nameMap.dragStart,this.drag);s.scroll()}},update:function(e){this.bar.width(Math.floor(e*100)+"%");this.el.attr("data-ptime",n.secondToTime(e*i.duration))},seeking:function(e){if(e)this.el.addClass("seeking");else this.el.removeClass("seeking")},percent:function(e,t){var a=((e.clientX||e.changedTouches[0].clientX)-t.left())/t.width();a=Math.max(a,0);return Math.min(a,1)},drag:function(t){t.preventDefault();var a=s.current().el;var r=function(e){e.preventDefault();var t=p.percent(e,a);p.update(t);o.update(t*i.duration)};var c=function(t){t.preventDefault();a.removeEventListener(n.nameMap.dragEnd,c);a.removeEventListener(n.nameMap.dragMove,r);var s=p.percent(t,a);p.update(s);e.player.seek(s*i.duration);i.disableTimeupdate=false;p.seeking(false)};i.disableTimeupdate=true;p.seeking(true);a.addEventListener(n.nameMap.dragMove,r);a.addEventListener(n.nameMap.dragEnd,c)}};var m={el:null,btns:{},step:"next",create:function(){if(!e.player.options.controls)return;var t=this;e.player.options.controls.forEach(function(a){if(t.btns[a])return;var r={onclick:function(n){t.events[a]?t.events[a](n):e.player.options.events[a](n)}};switch(a){case"volume":r.className=" "+(i.muted?"off":"on");r.innerHTML='
    ';r["on"+n.nameMap.dragStart]=t.events["volume"];r.onclick=null;break;case"mode":r.className=" "+e.player.options.mode;break;default:r.className="";break}r.className=a+r.className+" btn";t.btns[a]=t.el.createChild("div",r)});t.btns["volume"].bar=t.btns["volume"].child(".bar")},events:{mode:function(t){switch(e.player.options.mode){case"loop":e.player.options.mode="random";break;case"random":e.player.options.mode="order";break;default:e.player.options.mode="loop"}m.btns["mode"].className="mode "+e.player.options.mode+" btn";d.set("_PlayerMode",e.player.options.mode)},volume:function(t){t.preventDefault();var a=t.currentTarget;var r=false;var s=function(t){t.preventDefault();e.player.volume(m.percent(t,a));r=true};var o=function(t){t.preventDefault();a.removeEventListener(n.nameMap.dragEnd,o);a.removeEventListener(n.nameMap.dragMove,s);if(r){e.player.muted();e.player.volume(m.percent(t,a))}else{if(i.muted){e.player.muted();e.player.volume(i.volume)}else{e.player.muted("muted");m.update(0)}}};a.addEventListener(n.nameMap.dragMove,s);a.addEventListener(n.nameMap.dragEnd,o)},backward:function(t){m.step="prev";e.player.mode()},forward:function(t){m.step="next";e.player.mode()}},update:function(e){m.btns["volume"].className="volume "+(!i.muted&&e>0?"on":"off")+" btn";m.btns["volume"].bar.width(Math.floor(e*100)+"%")},percent:function(e,t){var a=((e.clientX||e.changedTouches[0].clientX)-t.left())/t.width();a=Math.max(a,0);return Math.min(a,1)}};var v={onerror:function(){s.error();e.player.mode()},ondurationchange:function(){if(i.duration!==1){p.el.attr("data-dtime",n.secondToTime(i.duration))}},onloadedmetadata:function(){e.player.seek(0);p.el.attr("data-dtime",n.secondToTime(i.duration))},onplay:function(){e.parentNode.addClass("playing");J(this.attr("title"));f=e},onpause:function(){e.parentNode.removeClass("playing");f=null},ontimeupdate:function(){if(!this.disableTimeupdate){p.update(this.currentTime/this.duration);o.update(this.currentTime)}},onended:function(t){e.player.mode();e.player.play()}};var g={el:{},create:function(){if(!e.player.options.btns)return;var t=this;e.player.options.btns.forEach(function(a){if(t.el[a])return;t.el[a]=e.createChild("div",{className:a+" btn",onclick:function(t){e.player.fetch().then(function(){e.player.options.events[a](t)})}})})}};var b=function(t){if(e.player.created)return;e.player.options=Object.assign(a,t);e.player.options.mode=d.get("_PlayerMode")||e.player.options.mode;g.create();i=e.createChild(e.player.options.type,v);r.create();e.parentNode.addClass(e.player.options.type);e.player.created=true};b(t);return e};var m=e.statics.indexOf("//")>0?e.statics:e.root;var v={x:"undefined",y:"undefined"};var g=0;var y,b;const w=document.getElementsByTagName("body")[0];const C=document.documentElement;const x=u("#container");const k=u("#loading");const L=u("#nav");const E=u("#header");const T=L.child(".toggle");const M=u("#quick");const N=u("#sidebar");const O=u("#brand");var j=u("#tool"),I,H,A,q;var S=u("#search");var R,_,P;var D=window.innerHeight;var B=window.innerWidth;var W=0,z=window.location.href;var Y;const F=lozad("img, [data-background-image]",{loaded:function(e){e.addClass("lozaded")}});const X={timer:null,lock:false,show:function(){clearTimeout(this.timer);document.body.removeClass("loaded");k.attr("style","display:block");X.lock=false},hide:function(t){if(!e.loader.start)t=-1;this.timer=setTimeout(this.vanish,t||3e3)},vanish:function(){if(X.lock)return;if(e.loader.start)l(k,0);document.body.addClass("loaded");X.lock=true}};const U=function(e){var t=u(".theme .ic");if(e=="dark"){C.attr("data-theme",e);t.removeClass("i-sun");t.addClass("i-moon")}else{C.attr("data-theme",null);t.removeClass("i-moon");t.addClass("i-sun")}};const V=function(e){if(C.attr("data-theme")=="dark")e="#222";u('meta[name="theme-color"]').attr("content",e)};const G=function(){window.matchMedia("(prefers-color-scheme: dark)").addListener(function(e){if(e.matches){U("dark")}else{U()}});var t=d.get("theme");if(t){U(t)}else{if(e.darkmode){U("dark")}}u(".theme").addEventListener("click",function(e){var t=e.currentTarget.child(".ic");var a=w.createChild("div",{id:"neko",innerHTML:'
    '});var n=function(){l(a,{delay:2500,opacity:0},function(){w.removeChild(a)})};if(t.hasClass("i-sun")){var i=function(){a.addClass("dark");U("dark");d.set("theme","dark");n()}}else{a.addClass("dark");var i=function(){a.removeClass("dark");U();d.set("theme","light");n()}}l(a,1,function(){setTimeout(i,210)})})};const $=function(){document.addEventListener("visibilitychange",function(){switch(document.visibilityState){case"hidden":u('[rel="icon"]').attr("href",m+e.favicon.hidden);document.title=LOCAL.favicon.hide;if(e.loader.switch)X.show();clearTimeout(b);break;case"visible":u('[rel="icon"]').attr("href",m+e.favicon.normal);document.title=LOCAL.favicon.show;if(e.loader.switch)X.hide(1e3);b=setTimeout(function(){document.title=y},2e3);break}})};const J=function(e){if(!e)return;var t=w.createChild("div",{innerHTML:e,className:"tip"});setTimeout(function(){t.addClass("hide");setTimeout(function(){w.removeChild(t)},300)},3e3)};const K=function(e){R=L.height();_=E.height();P=_+u("#waves").height();if(B!=window.innerWidth)ae(null,1);D=window.innerHeight;B=window.innerWidth;N.child(".panels").height(D+"px")};const Q=function(e){var t=window.innerHeight;var n=a();var i=n>t?n-t:document.body.scrollHeight-t;var r=window.pageYOffset>_;var s=window.pageYOffset>0;if(r){V("#FFF")}else{V("#222")}L.toggleClass("show",r);j.toggleClass("affix",s);O.toggleClass("affix",s);N.toggleClass("affix",window.pageYOffset>P&&document.body.offsetWidth>991);if(typeof v.y=="undefined"){v.y=window.pageYOffset}g=v.y-window.pageYOffset;if(g<0){L.removeClass("up");L.toggleClass("down",r)}else if(g>0){L.removeClass("down");L.toggleClass("up",r)}else{}v.y=window.pageYOffset;var o=Math.round(Math.min(100*window.pageYOffset/i,100))+"%";H.child("span").innerText=o;u(".percent").width(o)};const Z=function(){if(e.auto_scroll)d.set(z,v.y)};const ee=function(t){var a=window.location.hash;var n=null;if(W){d.del(z);return}if(a)n=u(decodeURI(a));else{n=e.auto_scroll?parseInt(d.get(z)):0}if(n){c(n);W=1}if(t&&a&&!W){c(n);W=1}};const te=function(e,t){var a=w.createChild("textarea",{style:{top:window.scrollY+"px",position:"absolute",opacity:"0"},readOnly:true,value:e});const n=document.getSelection();const i=n.rangeCount>0?n.getRangeAt(0):false;a.select();a.setSelectionRange(0,e.length);a.readOnly=false;var r=document.execCommand("copy");t&&t(r);a.blur();if(i){n.removeAllRanges();n.addRange(i)}w.removeChild(a)};const ae=function(e,t){if(N.hasClass("on")){N.removeClass("on");T.removeClass("close");if(t){N.style=""}else{l(N,"slideRightOut")}}else{if(t){N.style=""}else{l(N,"slideRightIn",function(){N.addClass("on");T.addClass("close")})}}};const ne=function(){var e=N.child(".inner");var t=N.find(".panel");if(N.child(".tab")){e.removeChild(N.child(".tab"))}var a=document.createElement("ul"),n="active";a.className="tab";["contents","related","overview"].forEach(function(e){var t=N.child(".panel."+e);if(t.innerHTML.replace(/(^\s*)|(\s*$)/g,"").length<1){if(e=="contents"){q.display("none")}return}if(e=="contents"){q.display("")}var i=document.createElement("li");var r=document.createElement("span");var s=document.createTextNode(t.attr("data-title"));r.appendChild(s);i.appendChild(r);i.addClass(e+" item");if(n){t.addClass(n);i.addClass(n)}else{t.removeClass("active")}i.addEventListener("click",function(e){var t=event.currentTarget;if(t.hasClass("active"))return;N.find(".tab .item").forEach(function(e){e.removeClass("active")});N.find(".panel").forEach(function(e){e.removeClass("active")});N.child(".panel."+t.className.replace(" item","")).addClass("active");t.addClass("active")});a.appendChild(i);n=""});if(a.childNodes.length>1){e.insertBefore(a,e.childNodes[0]);N.child(".panels").style.paddingTop=""}else{N.child(".panels").style.paddingTop=".625rem"}};const ie=function(){var t=u.all(".contents li");if(t.length<1){return}var a=Array.prototype.slice.call(t)||[];var n=null;a=a.map(function(t,a){var i=t.child("a.toc-link");var s=u(decodeURI(i.attr("href")));if(!s)return;var o=s.child("a.anchor");var l=function(e){e.preventDefault();var t=u(decodeURI(e.currentTarget.attr("href")));n=a;c(t,null,function(){r(a);n=null})};i.addEventListener("click",l);o&&o.addEventListener("click",function(t){l(t);te(e.hostname+"/"+LOCAL.path+t.currentTarget.attr("href"))});return s});var i=N.child(".contents.panel");var r=function(e,n){var r=t[e];if(!r)return;if(r.hasClass("current")){return}u.each(".toc .active",function(e){e&&e.removeClass("active current")});a.forEach(function(e){e&&e.removeClass("active")});r.addClass("active current");a[e]&&a[e].addClass("active");var s=r.parentNode;while(!s.matches(".contents")){if(s.matches("li")){s.addClass("active");var o=u(s.child("a.toc-link").attr("href"));if(o){o.addClass("active")}}s=s.parentNode}if(getComputedStyle(N).display!="none"&&i.hasClass("active")){c(i,r.offsetTop-i.offsetHeight/4)}};var s=function(e){var t=0;var n=e[t];if(n.boundingClientRect.top>0){t=a.indexOf(n.target);return t===0?0:t-1}for(;t0){t.target.addClass("show");e.unobserve(t.target)}}})},{root:null,threshold:[.3]});u.each(".index.wrap article.item, .index.wrap section.item",function(t){e.observe(t)});u(".index.wrap .item:first-child").addClass("show")}u.each(".cards .item",function(e,t){["mouseenter","touchstart"].forEach(function(t){e.addEventListener(t,function(t){if(u(".cards .item.active")){u(".cards .item.active").removeClass("active")}e.addClass("active")})});["mouseleave"].forEach(function(t){e.addEventListener(t,function(t){e.removeClass("active")})})})};const de=function(){u.each("span.exturl",function(e){var t=document.createElement("a");t.href=decodeURIComponent(atob(e.dataset.url).split("").map(function(e){return"%"+("00"+e.charCodeAt(0).toString(16)).slice(-2)}).join(""));t.rel="noopener external nofollow noreferrer";t.target="_blank";t.className=e.className;t.title=e.title||e.innerText;t.innerHTML=e.innerHTML;if(e.dataset.backgroundImage){t.dataset.backgroundImage=e.dataset.backgroundImage}e.parentNode.replaceChild(t,e)})};const ue=function(e){if(u(e+" .md img")){s("fancybox");r("fancybox",function(){var t=jQuery.noConflict();u.each(e+" p.gallery",function(e){var t=document.createElement("div");t.className="gallery";t.attr("data-height",e.attr("data-height")||220);t.innerHTML=e.innerHTML.replace(/
    /g,"");e.parentNode.insertBefore(t,e);e.remove()});u.each(e+" .md img:not(.emoji):not(.vemoji)",function(e){var a=t(e);var n,i="image-info";if(!a.is("a img")){var r=a.attr("data-src")||a.attr("src");a.data("safe-src",r);var s=a.wrap('').parent("a");if(!a.is(".gallery img")){s.attr("data-fancybox","default").attr("rel","default")}else{i="jg-caption"}}if(n=e.attr("title")){s.attr("data-caption",n);var o=document.createElement("span");var c=document.createTextNode(n);o.appendChild(c);o.addClass(i);e.insertAfter(o)}});u.each(e+" div.gallery",function(e,a){t(e).justifiedGallery({rowHeight:t(e).data("height")||120,rel:"gallery-"+a}).on("jg.complete",function(){t(this).find("a").each(function(e,t){t.attr("data-fancybox","gallery-"+a)})})});t.fancybox.defaults.hash=false;t(e+" .fancybox").fancybox({loop:true,helpers:{overlay:{locked:false}}})},window.jQuery)}};const fe=function(){pe();if(!u(".md"))return;ue(".post.block");u(".post.block").oncopy=function(e){J(LOCAL.copyright);if(LOCAL.nocopy){e.preventDefault();return}var t=u("#copyright");if(window.getSelection().toString().length>30&&t){e.preventDefault();var a="# "+t.child(".author").innerText;var n="# "+t.child(".link").innerText;var i="# "+t.child(".license").innerText;var r=a+"
    "+n+"
    "+i+"

    "+window.getSelection().toString().replace(/\r\n/g,"
    ");var s=a+"\n"+n+"\n"+i+"\n\n"+window.getSelection().toString().replace(/\r\n/g,"\n");if(e.clipboardData){e.clipboardData.setData("text/html",r);e.clipboardData.setData("text/plain",s)}else if(window.clipboardData){return window.clipboardData.setData("text",s)}}};u.each("li ruby",function(e){var t=e.parentNode;if(e.parentNode.tagName!="LI"){t=e.parentNode.parentNode}t.addClass("ruby")});u.each("ol[start]",function(e){e.style.counterReset="counter "+parseInt(e.attr("start")-1)});u.each(".md table",function(e){e.wrap({className:"table-container"})});u.each(".highlight > .table-container",function(e){e.className="code-container"});u.each("figure.highlight",function(e){var t=e.child(".code-container");var a=e.child("figcaption");e.insertAdjacentHTML("beforeend",'
    ');var n=e.child(".copy-btn");if(LOCAL.nocopy){n.remove()}else{n.addEventListener("click",function(e){var a=e.currentTarget;var n="",i="";t.find("pre").forEach(function(e){i+=n+e.innerText;n="\n"});te(i,function(e){a.child(".ic").className=e?"ic i-check":"ic i-times";a.blur();J(LOCAL.copyright)})});n.addEventListener("mouseleave",function(e){setTimeout(function(){e.target.child(".ic").className="ic i-clipboard"},1e3)})}var i=e.child(".breakline-btn");i.addEventListener("click",function(t){var a=t.currentTarget;if(e.hasClass("breakline")){e.removeClass("breakline");a.child(".ic").className="ic i-align-left"}else{e.addClass("breakline");a.child(".ic").className="ic i-align-justify"}});var r=e.child(".fullscreen-btn");var s=function(){e.removeClass("fullscreen");e.scrollTop=0;w.removeClass("fullscreen");r.child(".ic").className="ic i-expand"};var o=function(t){var a=t.currentTarget;if(e.hasClass("fullscreen")){s();u&&u();c(e)}else{e.addClass("fullscreen");w.addClass("fullscreen");r.child(".ic").className="ic i-compress";d&&d()}};r.addEventListener("click",o);a&&a.addEventListener("click",o);if(t&&t.find("tr").length>15){t.style.maxHeight="300px";t.insertAdjacentHTML("beforeend",'
    ');var l=t.child(".show-btn");var d=function(){t.style.maxHeight="";l.addClass("open")};var u=function(){t.style.maxHeight="300px";l.removeClass("open")};l.addEventListener("click",function(e){if(l.hasClass("open")){s();u();c(t)}else{d()}})}});u.each("pre.mermaid > svg",function(e){e.style.maxWidth=""});u.each(".reward button",function(e){e.addEventListener("click",function(e){e.preventDefault();var t=u("#qr");if(t.display()==="inline-flex"){l(t,0)}else{l(t,1,function(){t.display("inline-flex")})}})});u.each(".quiz > ul.options li",function(e){e.addEventListener("click",function(t){if(e.hasClass("correct")){e.toggleClass("right");e.parentNode.parentNode.addClass("show"); }else{e.toggleClass("wrong")}})});u.each(".quiz > p",function(e){e.addEventListener("click",function(t){e.parentNode.toggleClass("show")})});u.each(".quiz > p:first-child",function(e){var t=e.parentNode;var a="choice";if(t.hasClass("true")||t.hasClass("false"))a="true_false";if(t.hasClass("multi"))a="multiple";if(t.hasClass("fill"))a="gap_fill";if(t.hasClass("essay"))a="essay";e.attr("data-type",LOCAL.quiz[a])});u.each(".quiz .mistake",function(e){e.attr("data-type",LOCAL.quiz.mistake)});u.each("div.tags a",function(e){e.className=["primary","success","info","warning","danger"][Math.floor(Math.random()*5)]});u.each(".md div.player",function(e){p(e,{type:e.attr("data-type"),mode:"order",btns:[]}).player.load(JSON.parse(e.attr("data-src"))).fetch()})};const he=function(){var e;u.each("div.tab",function(t,a){if(t.attr("data-ready"))return;var n=t.attr("data-id");var i=t.attr("data-title");var r=u("#"+n);if(!r){r=document.createElement("div");r.className="tabs";r.id=n;r.innerHTML='
    ';var s=r.child(".show-btn");s.addEventListener("click",function(e){c(r)});t.parentNode.insertBefore(r,t);e=true}else{e=false}var o=r.child(".nav ul");if(!o){o=r.createChild("div",{className:"nav",innerHTML:"
      "}).child("ul")}var l=o.createChild("li",{innerHTML:i});if(e){l.addClass("active");t.addClass("active")}l.addEventListener("click",function(e){var a=e.currentTarget;r.find(".active").forEach(function(e){e.removeClass("active")});t.addClass("active");a.addClass("active")});r.appendChild(t);t.attr("data-ready",true)})};const pe=function(){var e=u("#comments");if(!e){A.display("none");return}else{A.display("")}if(!window.IntersectionObserver){s("valine")}else{var t=new IntersectionObserver(function(e,t){var a=e[0];s("valine");if(a.isIntersecting||a.intersectionRatio>0){l(u("#comments"),"bounceUpIn");t.disconnect()}});t.observe(e)}};const me=function(t){if(e.search===null)return;if(!S){S=w.createChild("div",{id:"search",innerHTML:'
      '})}var a=instantsearch({indexName:e.search.indexName,searchClient:algoliasearch(e.search.appID,e.search.apiKey),searchFunction:function(e){var t=u(".search-input");if(t.value){e.search()}}});a.on("render",function(){t.refresh(u("#search-hits"))});a.addWidgets([instantsearch.widgets.configure({hitsPerPage:e.search.hits.per_page||10}),instantsearch.widgets.searchBox({container:".search-input-container",placeholder:LOCAL.search.placeholder,showReset:false,showSubmit:false,showLoadingIndicator:false,cssClasses:{input:"search-input"}}),instantsearch.widgets.stats({container:"#search-stats",templates:{text:function(e){var t=LOCAL.search.stats.replace(/\$\{hits}/,e.nbHits).replace(/\$\{time}/,e.processingTimeMS);return t+'
      '}}}),instantsearch.widgets.hits({container:"#search-hits",templates:{item:function(t){var a=t.categories?""+t.categories.join('')+"":"";return''+a+t._highlightResult.title.value+""},empty:function(e){return'
      '+LOCAL.search.empty.replace(/\$\{query}/,e.query)+"
      "}},cssClasses:{item:"item"}}),instantsearch.widgets.pagination({container:"#search-pagination",scrollTo:false,showFirst:false,showLast:false,templates:{first:'',last:'',previous:'',next:''},cssClasses:{root:"pagination",item:"pagination-item",link:"page-number",selectedItem:"current",disabledItem:"disabled-item"}})]);a.start();u.each(".search",function(e){e.addEventListener("click",function(){document.body.style.overflow="hidden";l(S,"shrinkIn",function(){u(".search-input").focus()})})});const n=function(){document.body.style.overflow="";l(S,0)};S.addEventListener("click",function(e){if(e.target===S){n()}});u(".close-btn").addEventListener("click",n);window.addEventListener("pjax:success",n);window.addEventListener("keyup",function(e){if(e.key==="Escape"){n()}})};const ve=function(){u.each(".overview .menu > .item",function(e){L.child(".menu").appendChild(e.cloneNode(true))});k.addEventListener("click",X.vanish);T.addEventListener("click",ae);u(".dimmer").addEventListener("click",ae);M.child(".down").addEventListener("click",se);M.child(".up").addEventListener("click",re);if(!j){j=E.createChild("div",{id:"tool",innerHTML:'
      0%
      '})}I=j.child(".player");H=j.child(".back-to-top");A=j.child(".chat");q=j.child(".contents");H.addEventListener("click",re);A.addEventListener("click",oe);q.addEventListener("click",ae);p(I);u("main").addEventListener("click",function(){I.player.mini()})};const ge=function(){Z();if(N.hasClass("on")){l(N,function(){N.removeClass("on");T.removeClass("close")})}u("#main").innerHTML="";u("#main").appendChild(k.lastChild.cloneNode(true));c(0)};const ye=function(t){W=0;z=window.location.href;s("katex");r("copy_tex");s("mermaid");r("chart");r("valine",function(){var t=Object.assign({},e.valine);t=Object.assign(t,LOCAL.valine||{});t.el="#comments";t.pathname=LOCAL.path;t.pjax=Y;t.lazyload=F;new MiniValine(t);setTimeout(function(){ee(1);ue(".v")},1e3)},window.MiniValine);if(!t){u.each("script[data-pjax]",o)}y=document.title;K();ce();ne();ie();de();fe();he();I.player.load(LOCAL.audio||e.audio||{});X.hide();setTimeout(function(){ee()},500);le();F.observe()};const be=function(){ve();Y=new Pjax({selectors:["head title",".languages",".pjax","script[data-config]"],analytics:false,cacheBust:false});e.quicklink.ignores=LOCAL.ignores;quicklink.listen(e.quicklink);$();G();me(Y);window.addEventListener("scroll",Q);window.addEventListener("resize",K);window.addEventListener("pjax:send",ge);window.addEventListener("pjax:success",ye);window.addEventListener("beforeunload",function(){Z()});ye(1)};window.addEventListener("DOMContentLoaded",be);console.log("%c Theme.Shoka v"+e.version+" %c https://shoka.lostyu.me/ ","color: white; background: #e9546b; padding:5px 0;","padding:4px;border:1px solid #e9546b;");var we=document.createElement("canvas");we.style.cssText="position:fixed;top:0;left:0;pointer-events:none;z-index:9999999";document.body.appendChild(we);var Ce=we.getContext("2d");var xe=30;var ke=0;var Le=0;var Ee="click";var Te=e.fireworks;function Me(){we.width=window.innerWidth*2;we.height=window.innerHeight*2;we.style.width=window.innerWidth+"px";we.style.height=window.innerHeight+"px";we.getContext("2d").scale(2,2)}function Ne(e){ke=e.clientX||e.touches&&e.touches[0].clientX;Le=e.clientY||e.touches&&e.touches[0].clientY}function Oe(e){var t=anime.random(0,360)*Math.PI/180;var a=anime.random(50,180);var n=[-1,1][anime.random(0,1)]*a;return{x:e.x+n*Math.cos(t),y:e.y+n*Math.sin(t)}}function je(e,t){var a={};a.x=e;a.y=t;a.color=Te[anime.random(0,Te.length-1)];a.radius=anime.random(16,32);a.endPos=Oe(a);a.draw=function(){Ce.beginPath();Ce.arc(a.x,a.y,a.radius,0,2*Math.PI,true);Ce.fillStyle=a.color;Ce.fill()};return a}function Ie(e,t){var a={};a.x=e;a.y=t;a.color="#FFF";a.radius=.1;a.alpha=.5;a.lineWidth=6;a.draw=function(){Ce.globalAlpha=a.alpha;Ce.beginPath();Ce.arc(a.x,a.y,a.radius,0,2*Math.PI,true);Ce.lineWidth=a.lineWidth;Ce.strokeStyle=a.color;Ce.stroke();Ce.globalAlpha=1};return a}function He(e){for(var t=0;tR1ck's Playground = how
      6.7k 6 分钟

      # 一、概述 C++ 中的多态性(Polymorphism)是面向对象编程的一个关键概念,它允许对象以不同的方式呈现相同的接口。C++ 中的多态性可以分为静态多态性(静态多态)和动态多态性(动态多态),它们有不同的实现方式和特点: # 1、静态多态性(静态多态) 静态多态性是在编译时(编译期间)解析的多态性,也称为编译时多态性。它是通过函数的重载和运算符重载来实现的。 在静态多态性中,编译器在编译时根据函数参数的类型和数量来确定调用哪个函数或运算符。 例如,函数重载允许你定义多个具有相同名称但不同参数列表的函数,编译器会根据调用时的参数类型来选择正确的函数。 1234567void...
      2.8k 3 分钟

      # i、const 和 constexpr 的区别 提问:const 和 constexpr 有什么区别? 回答: const 和 constexpr 都用于创建常量,但它们之间有一些重要的区别。 以下是它们的区别以及如何使用它们: const: const 用于创建只读的常量变量,其值在运行时不能被修改。 const 变量必须在声明时初始化,并且可以在运行时计算其值。 const 变量可以用于任何数据类型,包括基本数据类型、自定义类、指针等。 const 变量的值在编译时或运行时(如果它是一个函数的局部变量)计算。 const...
      25k 23 分钟

      记录一些刷题、学习过程中接触到的一些库函数,后续(应该会)持续更新。 # i、std::function #CPP 新特性 #CPP11 std::function 是 C++11 标准引入的一个功能强大的模板类,它用于包装和管理可调用对象,包括函数指针、函数对象、Lambda 表达式和成员函数指针等。 std::function 允许你以统一的方式存储和调用不同类型的可调用对象。 以下是 std::function 的一些重要特性和用法: 1、创建 std::function 对象:你可以使用 std::function 模板来创建一个对象,该对象可以存储可调用对象,如函数指针、Lambda...
      6.3k 6 分钟

      # i、文件流构建字符串 123std::ostringstream oss;oss &lt;&lt; v.major &lt;&lt; &quot;.&quot; &lt;&lt; v.minor &lt;&lt; &quot;.&quot; &lt;&lt; v.patch;string str = oss.str(); # i、文件流分割字符串: 1234567//对于给定的以空格分割的字符串“A boy is standing outside”string...
      7.4k 7 分钟

      本文用以记录工作过程中 C++ 踩到的一些坑。 # 1、成员函数指针与普通函数指针不同 成员函数需要一个额外的参数(指向类实例的指针 this ),因此不能直接将成员函数传递给普通的函数指针参数。 1234567class OtherClass &#123;public: typedef function&lt;int(int, int)&gt; callbacktype; void registerCallBack(callbacktype cb) &#123; cout &lt;&lt; &quot;test: &quot;...
      20k 18 分钟

      # 一、Intro 完全无任何线程同步措施,线程会相互竞争打断,本例中,线程 A 的操作无法保证完整性: 123456789101112131415161718void ThreadA() &#123; while(1) &#123; &#123; std::cout &lt;&lt; &quot;===============================&quot; &lt;&lt; std::endl; std::cout &lt;&lt; &quot;Thread A: do...
      15k 13 分钟

      智能指针是 C++ 中用于管理动态分配的对象生命周期的一种特殊指针。它们提供了自动内存管理和资源释放的机制,避免了手动调用 delete 来释放内存的麻烦和潜在的内存泄漏。 C++ 标准库提供了两种常用的智能指针: std::shared_ptr 和 std::unique_ptr 。 (其实还有 std::weak_ptr ,用于破解循环引用的问题) std::shared_ptr :它是一种共享所有权的智能指针。多个 std::shared_ptr 对象可以同时拥有同一个对象的所有权。它使用引用计数的方式来跟踪对象的引用次数。当最后一个 std::shared_ptr...
      4.3k 4 分钟

      # 1、结构化绑定 #CPP 新特性 #CPP17 C++17 引入了一项非常强大的特性,称为结构化绑定(Structured Bindings),它允许你将复杂的数据结构(如 std::tuple 、数组或用户定义的类型)的成员绑定到命名变量,从而使代码更加清晰和易于理解。结构化绑定的主要目的是让代码更加直观,减少手动提取和命名数据的需要。 以下是结构化绑定的主要特点和用法: 自动解包:结构化绑定允许您将数据结构中的元素自动解包到单独的变量中,而无需手动提取元素。 更具可读性:通过为数据结构的各个成员指定名称,代码变得更加自解释和可读。 适用于多种数据结构:结构化绑定不仅适用于 STL...
      1.6k 1 分钟

      “提高效率最有效的办法,并不是那些让你更自律的手段,而是搭建一个系统,让你这个任务的产出成果,能够复用到下一个任务中,并且将其沉淀为资产,为你带来复利”。 去年在即刻上看到即友关于利用 Obsidian 搭建个人知识体系的介绍,再加上之前看到别的即友也聊到过一些关于 “建立系统复用产出” 的观点,受到了启发,于是自己结合 Obsidian 和 Git 搭建了自己的 PKM(Personal Knowledge Management)。从去年七月中旬(2023.07)开始,把自己以前写的笔记都归纳整理到一起,统一管理和维护。 一下子就打开了我的新世界。 本文从个人体验出发,就 Obsidian...
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file +R1ck's Playground = how
      6.7k 6 分钟

      # 一、概述 C++ 中的多态性(Polymorphism)是面向对象编程的一个关键概念,它允许对象以不同的方式呈现相同的接口。C++ 中的多态性可以分为静态多态性(静态多态)和动态多态性(动态多态),它们有不同的实现方式和特点: # 1、静态多态性(静态多态) 静态多态性是在编译时(编译期间)解析的多态性,也称为编译时多态性。它是通过函数的重载和运算符重载来实现的。 在静态多态性中,编译器在编译时根据函数参数的类型和数量来确定调用哪个函数或运算符。 例如,函数重载允许你定义多个具有相同名称但不同参数列表的函数,编译器会根据调用时的参数类型来选择正确的函数。 1234567void...
      2.8k 3 分钟

      # i、const 和 constexpr 的区别 提问:const 和 constexpr 有什么区别? 回答: const 和 constexpr 都用于创建常量,但它们之间有一些重要的区别。 以下是它们的区别以及如何使用它们: const: const 用于创建只读的常量变量,其值在运行时不能被修改。 const 变量必须在声明时初始化,并且可以在运行时计算其值。 const 变量可以用于任何数据类型,包括基本数据类型、自定义类、指针等。 const 变量的值在编译时或运行时(如果它是一个函数的局部变量)计算。 const...
      25k 23 分钟

      记录一些刷题、学习过程中接触到的一些库函数,后续(应该会)持续更新。 # i、std::function #CPP 新特性 #CPP11 std::function 是 C++11 标准引入的一个功能强大的模板类,它用于包装和管理可调用对象,包括函数指针、函数对象、Lambda 表达式和成员函数指针等。 std::function 允许你以统一的方式存储和调用不同类型的可调用对象。 以下是 std::function 的一些重要特性和用法: 1、创建 std::function 对象:你可以使用 std::function 模板来创建一个对象,该对象可以存储可调用对象,如函数指针、Lambda...
      6.3k 6 分钟

      # i、文件流构建字符串 123std::ostringstream oss;oss &lt;&lt; v.major &lt;&lt; &quot;.&quot; &lt;&lt; v.minor &lt;&lt; &quot;.&quot; &lt;&lt; v.patch;string str = oss.str(); # i、文件流分割字符串: 1234567//对于给定的以空格分割的字符串“A boy is standing outside”string...
      7.4k 7 分钟

      本文用以记录工作过程中 C++ 踩到的一些坑。 # 1、成员函数指针与普通函数指针不同 成员函数需要一个额外的参数(指向类实例的指针 this ),因此不能直接将成员函数传递给普通的函数指针参数。 1234567class OtherClass &#123;public: typedef function&lt;int(int, int)&gt; callbacktype; void registerCallBack(callbacktype cb) &#123; cout &lt;&lt; &quot;test: &quot;...
      20k 18 分钟

      # 一、Intro 完全无任何线程同步措施,线程会相互竞争打断,本例中,线程 A 的操作无法保证完整性: 123456789101112131415161718void ThreadA() &#123; while(1) &#123; &#123; std::cout &lt;&lt; &quot;===============================&quot; &lt;&lt; std::endl; std::cout &lt;&lt; &quot;Thread A: do...
      15k 13 分钟

      智能指针是 C++ 中用于管理动态分配的对象生命周期的一种特殊指针。它们提供了自动内存管理和资源释放的机制,避免了手动调用 delete 来释放内存的麻烦和潜在的内存泄漏。 C++ 标准库提供了两种常用的智能指针: std::shared_ptr 和 std::unique_ptr 。 (其实还有 std::weak_ptr ,用于破解循环引用的问题) std::shared_ptr :它是一种共享所有权的智能指针。多个 std::shared_ptr 对象可以同时拥有同一个对象的所有权。它使用引用计数的方式来跟踪对象的引用次数。当最后一个 std::shared_ptr...
      4.3k 4 分钟

      # 1、结构化绑定 #CPP 新特性 #CPP17 C++17 引入了一项非常强大的特性,称为结构化绑定(Structured Bindings),它允许你将复杂的数据结构(如 std::tuple 、数组或用户定义的类型)的成员绑定到命名变量,从而使代码更加清晰和易于理解。结构化绑定的主要目的是让代码更加直观,减少手动提取和命名数据的需要。 以下是结构化绑定的主要特点和用法: 自动解包:结构化绑定允许您将数据结构中的元素自动解包到单独的变量中,而无需手动提取元素。 更具可读性:通过为数据结构的各个成员指定名称,代码变得更加自解释和可读。 适用于多种数据结构:结构化绑定不仅适用于 STL...
      1.6k 1 分钟

      “提高效率最有效的办法,并不是那些让你更自律的手段,而是搭建一个系统,让你这个任务的产出成果,能够复用到下一个任务中,并且将其沉淀为资产,为你带来复利”。 去年在即刻上看到即友关于利用 Obsidian 搭建个人知识体系的介绍,再加上之前看到别的即友也聊到过一些关于 “建立系统复用产出” 的观点,受到了启发,于是自己结合 Obsidian 和 Git 搭建了自己的 PKM(Personal Knowledge Management)。从去年七月中旬(2023.07)开始,把自己以前写的笔记都归纳整理到一起,统一管理和维护。 一下子就打开了我的新世界。 本文从个人体验出发,就 Obsidian...
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file diff --git a/tags/CPP11/index.html b/tags/CPP11/index.html index ced4039..182ecfd 100644 --- a/tags/CPP11/index.html +++ b/tags/CPP11/index.html @@ -1 +1 @@ -标签:CPP11 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file +标签:CPP11 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file diff --git a/tags/CPP17/index.html b/tags/CPP17/index.html index 083968b..d09a6c8 100644 --- a/tags/CPP17/index.html +++ b/tags/CPP17/index.html @@ -1 +1 @@ -标签:CPP17 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file +标签:CPP17 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file diff --git "a/tags/CPP\346\226\260\347\211\271\346\200\247/index.html" "b/tags/CPP\346\226\260\347\211\271\346\200\247/index.html" index 9a8957a..470c809 100644 --- "a/tags/CPP\346\226\260\347\211\271\346\200\247/index.html" +++ "b/tags/CPP\346\226\260\347\211\271\346\200\247/index.html" @@ -1 +1 @@ -标签:CPP新特性 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file +标签:CPP新特性 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file diff --git a/tags/index.html b/tags/index.html index b48d29a..fda5cc0 100644 --- a/tags/index.html +++ b/tags/index.html @@ -1 +1 @@ -全部标签 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file +全部标签 | R1ck's Playground = how
      170k 字 2:35
      基于 Hexo & Theme.Shoka
      \ No newline at end of file