<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sterben_01</title><description>这里是01的博客～</description><link>https://sterben-01.github.io/</link><language>zh_CN</language><item><title>C++杂记 - 6</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B06/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B06/</guid><pubDate>Tue, 08 Apr 2025 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;std::has_unique_object_representations&lt;/h1&gt;
&lt;p&gt;这个东西用于判断是否有一个唯一性的字节表示。啥意思呢？比如我们某个结构体，可能因为不同的内存对齐要求而插入padding。但是这个8字节对齐的结构体和4字节对齐的结构体的实际含义相同，但是底层的二进制表示却不同。这时候我们就说这个对象的字节表示不唯一。&lt;strong&gt;说白了，只要是模糊地带，比如：虚函数，填充位，布尔值，架构，编译器相关的这种东西都会导致它返回false。那么它能干什么？他能干的事情主要是帮助我们进行序列化或让我们判断是否可以直接&lt;code&gt;memcpy&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;std::monostate&lt;/h1&gt;
&lt;p&gt;这个之前居然一直忘了写。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct monostate { };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他的定义就这么简单，啥都没有。为啥有这个？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有意作为 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/utility/variant&quot;&gt;std::variant&lt;/a&gt; 中的行为良好的空可选项的单位类型。具体而言，非可默认构造的变体类型可以把 &lt;code&gt;std::monostate&lt;/code&gt; 列为其首个可选项：这使得此变体自身可默认构造。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对的 就是如果&lt;code&gt;variant&lt;/code&gt;里面的类型，第一个为非可默认构造的，那么就用&lt;code&gt;monostate&lt;/code&gt;放在前面占位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct s{};

struct t{
    t(int) {}
};

int main(){
    std::variant&amp;lt;s, t&amp;gt; v{};
    std::variant&amp;lt;t, s&amp;gt; v2 {}; // 不行，t不可默认构造
    std::variant&amp;lt;std::monostate, t, s&amp;gt; v1 {};  // OK
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有人问为啥不直接给s放到t前面。主要是有些场合，t在s构造或有值前是无意义的。他不应该为variant的默认型别。&lt;/p&gt;
&lt;p&gt;除此之外还能干什么？&lt;/p&gt;
&lt;p&gt;首先注意一下，他并不是&lt;strong&gt;空&lt;/strong&gt;类型。但从严格意义上来讲，它同 &lt;code&gt;void&lt;/code&gt; 一样，是一个单值类型。C++ 并不像 Rust、Haskell、Scala 等语言那样存在真正的“空”类型。真正的“空”类型无法表示任何值，也无法被创建，所以一个以“空”类型作为参数的函数永远也无法被调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::monostate&lt;/code&gt; 类型可以被创建，但该类型只能够包含一个值，因此它也是一个 &lt;strong&gt;Unit Type&lt;/strong&gt;。单值类型只能表示一个状态，这种特殊性使其成为一个没有意义的类型。&lt;code&gt;std::nullptr_t&lt;/code&gt; 也是这样的一种类型，不过因为它能够隐式转换成任意的指针类型，并且可以被流输出，误用的几率较大，所以不适合用来作为通用的无意义表示类型。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::monostate&lt;/code&gt; 支持默认构造、拷贝构造和所有的比较操作，是一个常规类型，而 &lt;code&gt;void&lt;/code&gt; 是由关键字声明的内置类型，它是一个不完整类型（Incomplete Type），无法创建对象。因此，虽然都是 Unit Type，但 &lt;code&gt;std::monostate&lt;/code&gt; 存在一些特殊的使用场景：&lt;/p&gt;
&lt;p&gt;第一，你可以用它来测试模板容器的健壮性。因为 &lt;code&gt;std::monostate&lt;/code&gt; 不支持输出、运算、隐式转换等功能，也不包含任何成员，是最简单的常规类型，所以可以尝试以其作为模板参数实现化容器，如果没有报错，就表示容器的设计合理，没有强依赖。&lt;/p&gt;
&lt;p&gt;第二，你可以将它作为可选模板参数的默认类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename ExtraInfo = std::monostate&amp;gt;
class Data {
    // ...
    ExtraInfo info;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户可以选择是否传递额外的信息，&lt;code&gt;std::monostate&lt;/code&gt; 作为默认类型，不会误用、不占空间、也不会和其他类型产生冲突。&lt;/p&gt;
&lt;p&gt;第三，你可以将它作为无意义成员的替代类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;bool Debug&amp;gt;
class S {
public:
    void log_info(const std::string&amp;amp; msg) {
        if constexpr (Debug) {
            log_.info(msg);
        }
    }

private:
    std::conditional_t&amp;lt;Debug, Log, std::monostate&amp;gt; log_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，在非调试状态下，代码依旧可以正常编译，却不会具备真实的功能。&lt;/p&gt;
&lt;p&gt;第四，作为线性递归的 Root 类型。在 &lt;em&gt;C++ Generative Metaprogramming&lt;/em&gt; 一书的第 6.3.1 小节，单独写了一个 &lt;code&gt;struct empty_type{}&lt;/code&gt; 作为线性递归继承的默认结束条件，那是为了通用性考虑。如果在 C++17 之后也有类似的需求，那么便可以直接使用已经存在的 &lt;code&gt;std::monostate&lt;/code&gt; 类型。&lt;/p&gt;
&lt;h1&gt;命名空间&lt;/h1&gt;
&lt;p&gt;命名空间有三种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Named Namespaces
namespace A {}                        // C++11
inline namespace B {}                 // C++11
inline namespace [[deprecated]] C {}  // C++17

// Unnamed Namespaces
namespace {}                          // C++11
inline namespace {}                   // C++11
inline namespace [[deprecated]] {}    // C++17

// Nested Namespaces
namespace A::B {}                     // C++17
namespace A::inline B::C {}           // C++17
namespace A::B::inline C::inline D {} // C++20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一种特殊的形式称为 Global Namespaces，无须声明，也不属于 Unnamed Namespaces，以 &lt;code&gt;::&lt;/code&gt; 显式访问。通过不需要使用，某些情况下可能在不同的作用域间产生了命名冲突，通过 &lt;code&gt;::&lt;/code&gt; 可以明确指定访问的是最外层的实体，从而避免名称碰撞。&lt;/p&gt;
&lt;h2&gt;着重讲一下匿名命名空间 Unnamed Namespaces&lt;/h2&gt;
&lt;p&gt;Unnamed namespaces，也叫 Anonymous namespaces，是命名空间三种形式的一种。这种形式可以省略命名空间的名称，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace { /* .. . */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在语义上与等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace unique_name { /* ... */ }
using namespace unique_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器会自动生成一个唯一的名称，并使用 using-directives 自动导入名称。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;与其他形式的命名空间不同，Unnamed namespaces 的链接方式是 Internal Linkage&lt;/strong&gt;，标准描述为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;An unnamed namespace or a namespace declared directly or indirectly within an unnamed namespace has internal linkage. All other namespaces have external linkage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意，Unnamed namespaces 里面的所有内容都是 Internal Linkage，即便是用 &lt;code&gt;extern&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Global &lt;code&gt;static&lt;/code&gt; 的作用也是 Internal Linkage，Unnamed namespaces 可以作为它的一种替代方式。主要是 &lt;code&gt;static&lt;/code&gt; 所赋予的意义太多，有十几种用法，而 Unnamed namespaces 是一种专门用于控制全局可见性的方式。&lt;/p&gt;
&lt;p&gt;Unnamed namespaces 当然也有其他问题，比如无法在空间之外特化模板。所以我们有 &lt;code&gt;inline namespace&lt;/code&gt; （参考杂记2）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是在头文件中使用有隐患。因为可能会违背ODR。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;using directive 和 using declaration&lt;/h2&gt;
&lt;h3&gt;using-directive（导入整个命名空间）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;using namespace std;      // using-directive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是我们常见的导入命名空间。&lt;/p&gt;
&lt;h3&gt;using-declaration（导入特定名字/重用声明）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;using std::vector;       // 引入单个名字
using std::swap;         // 可以引入一组同名重载
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个就是我们只把&lt;strong&gt;某个名字&lt;/strong&gt;（或该名字的一组重载）引入当前作用域。它的几个扩展用法包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构造函数继承：&lt;/strong&gt; &lt;code&gt;using Base::Base;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别名模板/类型别名（C++11）：&lt;/strong&gt; &lt;code&gt;template&amp;lt;class T&amp;gt; using Vec = std::vector&amp;lt;T&amp;gt;;&lt;/code&gt; / &lt;code&gt;using i32 = int;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;注意要点：&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;如果同时使用 &lt;code&gt;using-directive&lt;/code&gt; 和 &lt;code&gt;using-declaration &lt;/code&gt;引入了一个符号，后者的优先级高于前者。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace A {
    int x = 10;
}

namespace B {
    int x = 20;
}

int main() {
    using namespace A;
    using B::x;
    std::cout &amp;lt;&amp;lt; x; // 20
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用技巧&lt;/h2&gt;
&lt;p&gt;本节梳理 C++ Core Guidelines 中的 Namespaces 最佳实践。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把helper function和作用的类放在同一命名空间。这里的helper function包括operator overload&lt;/li&gt;
&lt;li&gt;除非迫不得已，绝不要在头文件或全局范围中使用using-directive引入整个命名空间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要在头文件中使用匿名命名空间。优先使用它来管理不需要导出的内部实体&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;不要在头文件的命名空间作用域中使用命名空间别名，除非是在明确标记为仅供内部使用的命名空间中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;资料&lt;a href=&quot;https://mp.weixin.qq.com/s/whRrrSJzX_HYq5h9TT3-aA&quot;&gt;来自&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;chromium风格指南，包含了被禁用的标准库组件和讨论&lt;/h1&gt;
&lt;p&gt;https://chromium.googlesource.com/chromium/src/+/main/styleguide/c++/c++-features.md&lt;/p&gt;
</content:encoded></item><item><title>响应式设计</title><link>https://sterben-01.github.io/posts/%E5%93%8D%E5%BA%94%E5%BC%8F%E8%AE%BE%E8%AE%A1/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E5%93%8D%E5%BA%94%E5%BC%8F%E8%AE%BE%E8%AE%A1/</guid><pubDate>Sun, 22 Dec 2024 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;观察者模式到底是谁持有谁？&lt;/h1&gt;
&lt;p&gt;在 C++反应式编程书中，开头就有一个范例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
      int x[] = { 1,2,3,4,5 };
      EvenNumberObservable *t = new EvenNumberObservable(x, 5);
      IObserver&amp;lt;int&amp;gt;&amp;gt; *xy = new SimpleObserver();
      t-&amp;gt;Subscribe(*xy);
      delete xy; delete t;
      return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种响应式编程的观察者模式我觉得很奇怪吗明明&lt;code&gt;t&lt;/code&gt;是observable，是一种数据，&lt;code&gt;xy&lt;/code&gt;是observer，是观察者，那么应该是观察者订阅数据，应该是&lt;code&gt;observer-&amp;gt;subscribe(observable)&lt;/code&gt;这样的语义更加自然吧？为什么反过来了？&lt;/p&gt;
&lt;p&gt;其实并不奇怪。因为很多情况下响应式编程是“推”式设计（push-based）。把观察者注册进被观察者是一种更方便的行为。也就是当被观察者接收到数据变化，可以把信息“推”到注册进来的观察者里。这样更合理&lt;/p&gt;
&lt;h1&gt;书籍列表：&lt;/h1&gt;
&lt;p&gt;https://github.com/apachecn/apachecn-c-cpp-zh/tree/master&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apachecn/apachecn-c-cpp-zh/blob/master/docs/cpp-react-prog/01.md&quot;&gt;C++反应式编程&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>C++杂记 - 5（四）：optional、any、tuple、协程、ABI 等杂项</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05-4/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05-4/</guid><pubDate>Tue, 07 Mar 2023 06:58:00 GMT</pubDate><content:encoded>&lt;h1&gt;std::optional （C++17)&lt;/h1&gt;
&lt;p&gt;可选返回值的一个类型。要么有值要么没值。&lt;strong&gt;就是一个和bool一起打包的包装器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个大概示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class optional
{
	bool _initialized;
	std::aligned_storage_t&amp;lt;sizeof(T), alignof(T)&amp;gt; _storage;
public: 
// operations 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;初始化及简单实用示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;//初始化为空
std::optional&amp;lt;int&amp;gt; emptyInt;
std::optional&amp;lt;double&amp;gt; emptyDouble = std::nullopt;
//直接用有效值初始化
std::optional&amp;lt;int&amp;gt; intOpt{10};
std::optional intOptDeduced{10.0}; // auto deduced
//使用make_optional
auto doubleOpt = std::make_optional(10.0);
auto complexOpt = std::make_optional&amp;lt;std::complex&amp;lt;double&amp;gt;&amp;gt;(3.0, 4.0);
//使用in_place 后文会提到
std::optional&amp;lt;std::complex&amp;lt;double&amp;gt;&amp;gt; complexOpt{std::in_place, 3.0, 4.0};
std::optional&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; vectorOpt{std::in_place, {1, 2, 3}};
//使用其它optional对象构造
auto optCopied = vectorOpt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;int&amp;gt; find_index(const std::vector&amp;lt;int&amp;gt;&amp;amp; data, int value) {
    auto iter = std::find(data.begin(), data.end(), value);
    if (iter != data.end()) {
        return std::distance(data.begin(), iter);
    }
    return std::nullopt; // 注意这个 也可以直接返回{}
    // std::nullopt 是 std::nullopt_t 类型的常量，用于指示 optional 不含值。
}
int main() {
    vector&amp;lt;int&amp;gt; myvec{1, 2, 3, 4, 5};
    auto s = find_index(myvec, 3);
    auto ss = find_index(myvec, 6);
    if (s) {                        // 判断是否有值方法1
        cout &amp;lt;&amp;lt; s.value() &amp;lt;&amp;lt; endl;  // 获取值方法1
        cout &amp;lt;&amp;lt; *s &amp;lt;&amp;lt; endl;         // 获取值方法2
    }
    if (s.has_value()) {  // 判断是否有值方法2
        cout &amp;lt;&amp;lt; s.value() &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt; *s &amp;lt;&amp;lt; endl;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;optional&lt;/code&gt; 对象在下列条件下&lt;em&gt;含值&lt;/em&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象被以 &lt;code&gt;T&lt;/code&gt; 类型的值或另一&lt;em&gt;含值&lt;/em&gt; ﻿的 &lt;code&gt;optional&lt;/code&gt; 初始化/赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对象在下列条件下&lt;em&gt;不含值&lt;/em&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象被默认初始化。&lt;/li&gt;
&lt;li&gt;对象被以 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/utility/optional/nullopt_t&quot;&gt;std::nullopt_t&lt;/a&gt; 类型的值或&lt;em&gt;不含值&lt;/em&gt; ﻿的 &lt;code&gt;optional&lt;/code&gt; 对象初始化/赋值。&lt;/li&gt;
&lt;li&gt;调用了成员函数 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/utility/optional/reset&quot;&gt;reset()&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;部分成员函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;emplace()
&lt;ul&gt;
&lt;li&gt;原位构造
&lt;ul&gt;
&lt;li&gt;如果之前已含值，则先销毁再构造&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;reset()
&lt;ul&gt;
&lt;li&gt;销毁值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;swap()
&lt;ul&gt;
&lt;li&gt;交换值&lt;/li&gt;
&lt;li&gt;如果交换一个空值，则销毁原有值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;比较大小&lt;/h2&gt;
&lt;p&gt;对于定义了&lt;code&gt;&amp;lt;&lt;/code&gt;，&lt;code&gt;&amp;gt;&lt;/code&gt;，&lt;code&gt;==&lt;/code&gt;操作符的类型，保存他们的&lt;code&gt;optional&lt;/code&gt;对象也可以比较大小。如&lt;code&gt;optional&amp;lt;int&amp;gt;&lt;/code&gt;之间比较大小和直接比较&lt;code&gt;int&lt;/code&gt;数值的大小是一样的。&lt;strong&gt;比较特殊的是&lt;code&gt;std::nullopt&lt;/code&gt;，在比较大小时，它总小于存储有效值的&lt;code&gt;optional&lt;/code&gt;对象。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;int&amp;gt; int1(1);
std::optional&amp;lt;int&amp;gt; int2(10);
std::optional&amp;lt;int&amp;gt; int3;

std::cout &amp;lt;&amp;lt; std::boolalpha;
std::cout &amp;lt;&amp;lt; (int1 &amp;lt; int2) &amp;lt;&amp;lt; std::endl; // true
std::cout &amp;lt;&amp;lt; (int2 &amp;gt; int1) &amp;lt;&amp;lt; std::endl; // true
std::cout &amp;lt;&amp;lt; (int3 == std::nullopt) &amp;lt;&amp;lt; std::endl; // true
std::cout &amp;lt;&amp;lt; (int3 &amp;lt; int1) &amp;lt;&amp;lt; std::endl; // true

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;为什么需要make_optional 和std::in_place&lt;/h2&gt;
&lt;p&gt;首先我们需要注意一点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myclass {
   public:
    myclass() : mInt(100) {}

    int mInt;
};
class myclass2 {
   public:
    myclass2(int x, int y) : mInt(x+y) {
        cout &amp;lt;&amp;lt; &quot;constructor&quot; &amp;lt;&amp;lt; endl;
    }
    myclass2(const myclass2&amp;amp; x) : mInt(x.mInt) {
        cout &amp;lt;&amp;lt; &quot;copy constructor&quot; &amp;lt;&amp;lt; endl;
    }
    myclass2(myclass2&amp;amp;&amp;amp; x) : mInt(x.mInt) {
        cout &amp;lt;&amp;lt; &quot;move constructor&quot; &amp;lt;&amp;lt; endl;
    }
    int mInt;
};
class myclass3 {
   public:
    myclass3(int x) : mInt(x) {
        cout &amp;lt;&amp;lt; &quot;constructor&quot; &amp;lt;&amp;lt; endl;
    }
    myclass3(const myclass3&amp;amp; x) : mInt(x.mInt) {
        cout &amp;lt;&amp;lt; &quot;copy constructor&quot; &amp;lt;&amp;lt; endl;
    }
    myclass3(myclass3&amp;amp;&amp;amp; x) : mInt(x.mInt) {
        cout &amp;lt;&amp;lt; &quot;move constructor&quot; &amp;lt;&amp;lt; endl;
    }
    int mInt;
};
int main() {
    std::optional&amp;lt;myclass&amp;gt; sample;
    std::optional&amp;lt;myclass&amp;gt; sample2{};

    cout &amp;lt;&amp;lt; boolalpha;
    cout &amp;lt;&amp;lt; sample.has_value() &amp;lt;&amp;lt; endl; // false
    cout &amp;lt;&amp;lt; sample2.has_value() &amp;lt;&amp;lt; endl;// false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意，在这个情况下，这两种方法得到的结果都只是空的optional对象，而不是包含默认构造值的对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以此时， 我们可以这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;myclass&amp;gt; sample3{myclass()};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方法是可以工作的，我们将得到包含默认&lt;code&gt;myclass&lt;/code&gt;对象的&lt;code&gt;optional&lt;/code&gt;对象。但是在上面的代码中，将先构造出一个&lt;code&gt;myclass&lt;/code&gt;的临时对象，然后临时对象被移入optional存储的对象中，带来了额外的开销。在这种情况下，我们就可以使用&lt;code&gt;std::in_place_t&lt;/code&gt; / &lt;code&gt;std::make_optional&lt;/code&gt;来“原地”构造optional底层存储的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;myclass&amp;gt; sample4{std::in_place};
cout &amp;lt;&amp;lt; sample4.has_value() &amp;lt;&amp;lt; endl; // true
cout &amp;lt;&amp;lt; sample4-&amp;gt;mInt &amp;lt;&amp;lt; endl; // 100
std::optional&amp;lt;myclass&amp;gt; sample5(std::make_optional&amp;lt;myclass&amp;gt;());
cout &amp;lt;&amp;lt; sample5.has_value() &amp;lt;&amp;lt; endl;// true
cout &amp;lt;&amp;lt; sample5-&amp;gt;mInt &amp;lt;&amp;lt; endl; // 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再继续看例子，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;myclass2&amp;gt; sample6{std::in_place, 1,3}; //1 OK
std::optional&amp;lt;myclass2&amp;gt; sample7{1, 3}; //2 不OK
std::optional&amp;lt;myclass2&amp;gt; sample8{{1, 3}}; //3 OK 但是有拷贝


std::optional&amp;lt;myclass3&amp;gt; sample9{std::in_place, 1}; //4 OK
std::optional&amp;lt;myclass3&amp;gt; sample10{1}; //5 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;构造函数&lt;/th&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;版本起&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;constexpr optional() noexcept; &amp;lt;br /&amp;gt;constexpr optional( std::nullopt_t]) noexcept;&lt;/td&gt;
&lt;td&gt;(1)&lt;/td&gt;
&lt;td&gt;(C++17 起)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;constexpr optional( const optional&amp;amp; other );&lt;/td&gt;
&lt;td&gt;(2)&lt;/td&gt;
&lt;td&gt;(C++17 起)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;constexpr optional( optional&amp;amp;&amp;amp; other ) noexcept(&lt;em&gt;/* 见下文 */&lt;/em&gt;);&lt;/td&gt;
&lt;td&gt;(3)&lt;/td&gt;
&lt;td&gt;(C++17 起)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;(4)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template &amp;lt; class U &amp;gt; &amp;lt;br /&amp;gt;optional( const optional&amp;lt;U&amp;gt;&amp;amp; other );&lt;/td&gt;
&lt;td&gt;(C++17 起) (C++20 前) (条件性 &lt;code&gt;explicit&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template &amp;lt; class U &amp;gt; &amp;lt;br /&amp;gt;constexpr optional( const optional&amp;lt;U&amp;gt;&amp;amp; other );&lt;/td&gt;
&lt;td&gt;(C++20 起) (条件性 &lt;code&gt;explicit&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;(5)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template &amp;lt; class U &amp;gt; &amp;lt;br /&amp;gt;optional( optional&amp;lt;U&amp;gt;&amp;amp;&amp;amp; other );&lt;/td&gt;
&lt;td&gt;(C++17 起) (C++20 前) (条件性 &lt;code&gt;explicit&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template &amp;lt; class U &amp;gt; &amp;lt;br /&amp;gt;constexpr optional( optional&amp;lt;U&amp;gt;&amp;amp;&amp;amp; other );&lt;/td&gt;
&lt;td&gt;(C++20 起) (条件性 &lt;code&gt;explicit&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template&amp;lt; class... Args &amp;gt; &amp;lt;br /&amp;gt;constexpr explicit optional( std::in_place_t, Args&amp;amp;&amp;amp;... args );&lt;/td&gt;
&lt;td&gt;(6)&lt;/td&gt;
&lt;td&gt;(C++17 起)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template&amp;lt; class U, class... Args &amp;gt; &amp;lt;br /&amp;gt;constexpr explicit optional( std::in_place_t, std::initializer_list&amp;lt;U&amp;gt; ilist, Args&amp;amp;&amp;amp;... args );&lt;/td&gt;
&lt;td&gt;(7)&lt;/td&gt;
&lt;td&gt;(C++17 起)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template &amp;lt; class U = T &amp;gt; &amp;lt;br /&amp;gt;constexpr optional( U&amp;amp;&amp;amp; value );&lt;/td&gt;
&lt;td&gt;(8)&lt;/td&gt;
&lt;td&gt;(C++17 起) (条件性 &lt;code&gt;explicit&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第一个可以的原因是使用了&lt;code&gt;std::in_place&lt;/code&gt;标签，匹配到了第6个构造函数&lt;/p&gt;
&lt;p&gt;第二个不可以的原因是&lt;code&gt;std::optional&lt;/code&gt;没有一个支持不带标签的同时是多参数的构造函数。&lt;/p&gt;
&lt;p&gt;第三个可以的原因是因为额外加了花括号初始化，先初始化成&lt;code&gt;myclass2&lt;/code&gt;对象然后拷贝，匹配的是第8个构造函数&lt;/p&gt;
&lt;p&gt;第四个可以的原因是使用了标签。匹配到了第六个构造函数&lt;/p&gt;
&lt;p&gt;第五个可以的原因是因为是单参数，所以匹配到了第八个构造函数。&lt;/p&gt;
&lt;p&gt;所以， 针对不可拷贝或移动的对象，就必须使用&lt;code&gt;in_place&lt;/code&gt;了&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;核心来讲，有三个作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;区分到底是用&lt;code&gt;optional&amp;lt;T&amp;gt;&lt;/code&gt;的默认构造函数还是&lt;code&gt;T&lt;/code&gt;的默认构造函数。
&lt;ul&gt;
&lt;li&gt;(参见&lt;a href=&quot;https://link.zhihu.com/?target=http%3A//www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3527.html%23rationale.emplace&quot;&gt;N3527&lt;/a&gt;，&lt;code&gt;in_place_t&lt;/code&gt;原本的名字是&lt;code&gt;emplace&lt;/code&gt;。来自&lt;a href=&quot;https://blog.csdn.net/yihuajack/article/details/117780569&quot;&gt;这里&lt;/a&gt; 和 &lt;a href=&quot;https://stackoverflow.com/questions/49767940/why-do-stdoptional-constructors-use-stdin-place&quot;&gt;这里&lt;/a&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;允许原地构造需要多个入参的对象。&lt;/li&gt;
&lt;li&gt;让不可拷贝或移动的对象也可以使用&lt;code&gt;optional&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;std::inplace&lt;/code&gt;在&lt;code&gt; std::any&lt;/code&gt; , &lt;code&gt;std::variant&lt;/code&gt;等中亦有同样作用&lt;/p&gt;
&lt;p&gt;所以如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;myobj&amp;gt; maybe(bool n){
    if(n){
        return std::nullopt;
    }
    // return myobj(10); // 一次构造+一次移动+一次析构 性能差
    return std::optional&amp;lt;myobj&amp;gt;(10); //直接转发给optional，多参可以make_optional/in_place 性能好

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种写法性能更好。来自&lt;a href=&quot;https://felipe.rs/2021/09/19/std-optional-and-non-pod-types-in-cpp/&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;关于为什么需要nullopt&lt;/h2&gt;
&lt;p&gt;https://stackoverflow.com/questions/62933403/why-do-we-need-stdnullopt&lt;/p&gt;
&lt;p&gt;部分参考：&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/hhdshg/article/details/103433781&lt;/p&gt;
&lt;h2&gt;如何往optional里面放入不可构造、不可移动、不可拷贝的对象？&lt;/h2&gt;
&lt;p&gt;https://devblogs.microsoft.com/oldnewthing/20241115-00/?p=110527&lt;/p&gt;
&lt;p&gt;https://github.com/wanghenshui/cppweeklynews/blob/dev/posts/173.md&lt;/p&gt;
&lt;p&gt;有点复杂但又不那么复杂。&lt;/p&gt;
&lt;h2&gt;optional移出元素需要注意的点&lt;/h2&gt;
&lt;p&gt;假设我有这种代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj do_something() {
    std::optional&amp;lt;myobj&amp;gt; opt{2};
    cout &amp;lt;&amp;lt; &quot;--&quot; &amp;lt;&amp;lt; endl;
    return *opt; // 和opt.value()一样。不过现在会拷贝
}
myobj do_something2() {
    std::optional&amp;lt;myobj&amp;gt; opt{2};
    cout &amp;lt;&amp;lt; &quot;--&quot; &amp;lt;&amp;lt; endl;
    return std::move(opt.value());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;do_something&lt;/code&gt;会拷贝。假设&lt;code&gt;myobj&lt;/code&gt;很大，又有不必要开销。我们自然会想要&lt;code&gt;do_something2&lt;/code&gt;的方法，利用移动。但是我们到底应该移动什么？&lt;strong&gt;移动&lt;code&gt;optional&lt;/code&gt;里面的&lt;code&gt;value&lt;/code&gt;还是移动&lt;code&gt;optional&lt;/code&gt;本身？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设这种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void dummy(myobj obj) { cout &amp;lt;&amp;lt; &quot;dummy&quot; &amp;lt;&amp;lt; endl; }
void do_something3(bool input) {
    std::optional&amp;lt;myobj&amp;gt; opt{2};
    if (input) {
        dummy(std::move(opt.value())); // opt.value，也就是myobj本身是垃圾值了
    }
    cout &amp;lt;&amp;lt; boolalpha &amp;lt;&amp;lt; opt.has_value() &amp;lt;&amp;lt; endl; // opt含值
    // 过了很久，插入一堆其他逻辑
    if (input) {
        dummy(std::move(opt.value())); // 出问题了，move了两次
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们这时候移动的是&lt;code&gt;optional&lt;/code&gt;包裹的值。请注意。第一次移动后，&lt;code&gt;optional&lt;/code&gt;包裹的值可能是垃圾无效值。但是&lt;code&gt;optional&lt;/code&gt;本身是含值的，所以我们就算判断&lt;code&gt;has_value&lt;/code&gt;也无法解决这个问题。&lt;strong&gt;所以我们真正要做的应该是&lt;code&gt;move&lt;/code&gt;这个&lt;code&gt;optional&lt;/code&gt;本身。然后&lt;code&gt;move&lt;/code&gt;后进行&lt;code&gt;reset&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/51805059/why-does-moving-stdoptional-not-reset-state&quot;&gt;为什么optional不能像unique_ptr支持移动后自动重置状态?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;一些关于这篇&lt;a href=&quot;https://blog.tal.bi/posts/std-optional-move-pitfall&quot;&gt;胡扯博客&lt;/a&gt;的&lt;a href=&quot;https://www.reddit.com/r/cpp/comments/1ju89ie/beware_when_moving_a_stdoptional/&quot;&gt;讨论&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;std::any&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;any就是一个包装任意类型的包装器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;std::any的libstdc++和msvc的内部实现细节差异挺大的。但是换汤不换药。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;msvc的代码分析在&lt;a href=&quot;https://blog.csdn.net/haokan123456789/article/details/139158787&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;libstdc++的源码在&lt;a href=&quot;https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/any&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;any_cast为什么可以不用RTTI在&lt;a href=&quot;https://stackoverflow.com/questions/51361606/stdany-without-rtti-how-does-it-work&quot;&gt;这里&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;其实说白了就是构造的时候有一个对应类型的manager。每个manager有个静态的函数模板。每一个类型自然有自己的独有一份合成的函数示例。然后比较类型的时候只需要比较函数地址就可以了。因为如果类型相同，则对应的manager函数的地址也相同。如果没有或者是不同的话自然就炸了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;std::tuple的实现解析&lt;/h1&gt;
&lt;p&gt;https://mcyoung.xyz/2022/07/13/tuples-the-hard-way/&lt;/p&gt;
&lt;h2&gt;tuple 的 make_tuple 和 构造函数的一个细微区别&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;关于make_tuple可以查看EFF_STL章节&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;int a{1};
float b{2.3};
double c{3.2};
std::string d {&quot;abc&quot;};
auto t = std::tuple{a, b, c, d}; //c++17后可以不指定类模板参数
auto t2 = std::make_tuple(a, b, c, d);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码就是在基本情况下使用构造函数和&lt;code&gt;makr_tuple&lt;/code&gt;的基本语法。我们来看下较为复杂的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void func(T a) {
    cout &amp;lt;&amp;lt; __PRETTY_FUNCTION__ &amp;lt;&amp;lt; endl;
}

int main() {
    int a{1};
    float b{2.3};
    double c{3.2};
    std::string d{&quot;hello&quot;};
    auto t = std::tuple{std::ref(a), std::ref(b), std::cref(c), std::cref(d)}; //c++17后可以不指定类模板参数
    auto t2 = std::make_tuple(std::ref(a), std::ref(b), std::cref(c), std::cref(d));
    func(t);
    func(t2);
}
/*
void func(T) [with T = std::tuple&amp;lt;std::reference_wrapper&amp;lt;int&amp;gt;, std::reference_wrapper&amp;lt;float&amp;gt;, std::reference_wrapper&amp;lt;const double&amp;gt;, std::reference_wrapper&amp;lt;const std::__cxx11::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt; &amp;gt; &amp;gt;]
void func(T) [with T = std::tuple&amp;lt;int&amp;amp;, float&amp;amp;, const double&amp;amp;, const std::__cxx11::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt;&amp;amp;&amp;gt;]
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们注意到，使用普通的构造函数时，&lt;code&gt;tuple&lt;/code&gt;里 &lt;code&gt;std::ref/cref&lt;/code&gt;的类型没有变化。也就是每一个元素都是&lt;code&gt;std::reference_wrapper&lt;/code&gt;类型。&lt;/p&gt;
&lt;p&gt;但是在使用&lt;code&gt;make_tuple&lt;/code&gt;的时候，&lt;code&gt;tuple&lt;/code&gt;里的元素类型被脱去了，也就是&quot;退化&quot;成了对应底层类型的引用或&lt;code&gt;const&lt;/code&gt;引用类型。我们可以看下源代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241020212035.png&quot; alt=&quot;微信图片_20241020212035&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到，&lt;code&gt;make_tuple&lt;/code&gt;会对参数的&lt;code&gt;reference_wrapper&lt;/code&gt;类型进行脱去。&lt;/p&gt;
&lt;p&gt;资料来自&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1156/&quot;&gt;这篇文章的 &lt;em&gt;Errors in object lifetime: tuples that shoot at your feet&lt;/em&gt; 章节&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;C++中如何避免page fault来降低延迟&lt;/h1&gt;
&lt;p&gt;https://johnnysswlab.com/latency-sensitive-application-and-the-memory-subsystem-part-2-memory-management-mechanisms&lt;/p&gt;
&lt;h1&gt;协程&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://lewissbaker.github.io/2022/08/27/understanding-the-compiler-transform&quot;&gt;理解协程的代码转换&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;让enum class支持位运算&lt;/h1&gt;
&lt;p&gt;让枚举支持位运算组合，就像传统c的用法那样，但是枚举类是强类型，转换很不方便，怎么办？实现&lt;code&gt;operator |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;type_traits&amp;gt;

// Define a templatized struct to contain a bool constexpr that controls
// when the operators get generated.
template &amp;lt;typename E&amp;gt;
struct FEnableBitmaskOperators {
    static constexpr bool enable = false;
};

// This operator is only defined in the candidate set for a given type if the
// std::enable_if_t below evaluates to true, otherwise it is dropped.
template &amp;lt;typename E&amp;gt;
typename std::enable_if_t&amp;lt;FEnableBitmaskOperators&amp;lt;E&amp;gt;::enable, E&amp;gt; operator|(
    E Lhs, E Rhs) {
    return static_cast&amp;lt;E&amp;gt;(static_cast&amp;lt;std::underlying_type_t&amp;lt;E&amp;gt;&amp;gt;(Lhs) |
                          static_cast&amp;lt;std::underlying_type_t&amp;lt;E&amp;gt;&amp;gt;(Rhs));
}
// Rest of the operators...

With the following usage:

enum class ERenderPass : uint8_t {
    ...
};
// Specialize the struct to enable the operators for our enum.
template &amp;lt;&amp;gt;
struct FEnableBitmaskOperators&amp;lt;ERenderPass&amp;gt; {
    static constexpr bool enable = true;
};

// Works!
ERenderPass Primary = ERenderPass::Geometry | ERenderPass::Lighting;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上valkan cpp就是这么干的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename BitType, typename std::enable_if&amp;lt;FlagTraits&amp;lt;BitType&amp;gt;::isBitmask, bool&amp;gt;::type = true&amp;gt;
VULKAN_HPP_INLINE VULKAN_HPP_CONSTEXPR Flags&amp;lt;BitType&amp;gt; operator|( BitType lhs, BitType rhs ) VULKAN_HPP_NOEXCEPT {
    return Flags&amp;lt;BitType&amp;gt;( lhs ) | rhs;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来自&lt;a href=&quot;https://github.com/wanghenshui/cppweeklynews/blob/dev/posts/115.md&quot;&gt;这里&lt;/a&gt;和&lt;a href=&quot;https://github.com/KhronosGroup/Vulkan-Hpp/blob/a4cbf9c0279e6a95f1056e1a6d5ad10c8f3d2862/vulkan/vulkansc_enums.hpp#L202&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;ranges 和 view&lt;/h1&gt;
&lt;p&gt;把ranges理解为一段区间的抽象。比如容器的两个迭代器范围内。所以vector、list都可以被称为range。因为它满足range的要求。&lt;/p&gt;
&lt;p&gt;ranges的end一般叫sentinel哨位。原因是它的类型不一定必须和begin迭代器类型一致。因为sentinel不一定是迭代器。&lt;/p&gt;
&lt;p&gt;range自带的算法一般是立即求值的。但是view不同。view（范围适配器）是惰性求值的。我们可以把view理解为惰性求值的规则管线。在迭代实际发生时才会进行计算。&lt;strong&gt;view是一种特殊的、轻量级的range。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;|&lt;/code&gt;可以组成管线的原因是重载了&lt;code&gt;operator |&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;range&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;view&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;是否是概念&lt;/td&gt;
&lt;td&gt;✅ 是&lt;/td&gt;
&lt;td&gt;✅ 是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否可以遍历&lt;/td&gt;
&lt;td&gt;✅ 可以&lt;/td&gt;
&lt;td&gt;✅ 可以&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否拥有数据&lt;/td&gt;
&lt;td&gt;可能有&lt;/td&gt;
&lt;td&gt;❌ 通常没有（或只是引用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否惰性计算&lt;/td&gt;
&lt;td&gt;❌ 一般是立即&lt;/td&gt;
&lt;td&gt;✅ 是惰性的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否轻量级&lt;/td&gt;
&lt;td&gt;不一定&lt;/td&gt;
&lt;td&gt;✅ 是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能否组合管道&lt;/td&gt;
&lt;td&gt;❌（要借助 ranges）&lt;/td&gt;
&lt;td&gt;✅ 完美支持管道 .&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;所有的 &lt;strong&gt;view 是 range&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;但不是所有的 &lt;strong&gt;range 是 view&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;view 是你能用 &lt;code&gt;|&lt;/code&gt; 管道连接的东西（懒惰组合）&lt;/li&gt;
&lt;li&gt;range 更像是 “你能遍历的容器或结构”的大集合&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;LLVM的dense map解析&lt;/h1&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/669307116&lt;/p&gt;
&lt;p&gt;讲的非常好&lt;/p&gt;
&lt;h1&gt;C++ ABI的真正理解&lt;/h1&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/692886292&lt;/p&gt;
&lt;h1&gt;ASIO 的使用技巧&lt;/h1&gt;
&lt;p&gt;https://mmoemulator.com/p/going-super-sonic-with-asio/#skippable-preamble&lt;/p&gt;
&lt;h1&gt;GCC的代码模型&lt;/h1&gt;
&lt;p&gt;https://eli.thegreenplace.net/2012/01/03/understanding-the-x64-code-models&lt;/p&gt;
&lt;h1&gt;CMAKE杂记&lt;/h1&gt;
&lt;p&gt;普通库就是编译+链接，对象库只编译不链接，接口库不编译不链接。&lt;/p&gt;
&lt;h2&gt;接口库的作用：&lt;/h2&gt;
&lt;p&gt;当您在 CMake 中创建一个 INTERFACE 库并为其设置一些属性（如编译器选项、定义或&lt;strong&gt;包含目录&lt;/strong&gt;），然后其他目标链接到这个 INTERFACE 库时，这些属性就会自动应用到链接到它的所有目标。&lt;/p&gt;
&lt;p&gt;在您提到的情况中，假设有一个 &lt;code&gt;some_header.hpp&lt;/code&gt; 被多个 &lt;code&gt;.cpp&lt;/code&gt; 文件包含，在 CMake 中使用 INTERFACE 库可以确保每次编译这些 &lt;code&gt;.cpp&lt;/code&gt; 文件时都使用相同的编译器选项。这种方式简化了管理编译选项的复杂性，因为您只需要在一个地方指定这些选项，而它们就会自动应用到所有依赖的目标上。&lt;/p&gt;
&lt;p&gt;下面是一个具体示例：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: 创建一个 INTERFACE 库，并设置属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_library(some_interface_lib INTERFACE)
target_include_directories(some_interface_lib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_definitions(some_interface_lib INTERFACE SOME_DEFINE=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: 确保你的目标链接到这个 INTERFACE 库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_executable(some_executable main.cpp)
target_link_libraries(some_executable PRIVATE some_interface_lib)

add_library(some_other_lib some_other_source.cpp)
target_link_libraries(some_other_lib PUBLIC some_interface_lib)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，无论是 &lt;code&gt;some_executable&lt;/code&gt; 还是 &lt;code&gt;some_other_lib&lt;/code&gt;，任何时候它们的编译单元包含了 &lt;code&gt;some_header.hpp&lt;/code&gt;，都会自动应用 &lt;code&gt;some_interface_lib&lt;/code&gt; 的属性，&lt;strong&gt;即添加了 &lt;code&gt;${CMAKE_CURRENT_SOURCE_DIR}/include&lt;/code&gt; 到包含路径&lt;/strong&gt;，以及定义了预处理器宏 &lt;code&gt;SOME_DEFINE=1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;通过这种方式，CMake 保证了构建系统的一致性和易于维护，同时避免了在多个地方重复相同的编译配置。&lt;/p&gt;
&lt;h3&gt;接口库示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;add_library(test_interface INTERFACE) // 创建一个名为 test_interface 的接口库
// 这里没有指定源文件，因为接口库不生成二进制文件；它只用来携带相关的编译器和链接器选项。
target_include_directories(test_interface INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) // 为test_interface 添加包含目录
// 这使得任何链接到 test_interface 的目标都会在编译时包含 ${CMAKE_CURRENT_LIST_DIR}/include 目录。
// 比如如果某一个其他的目标使用target_link_libraries命令来链接test_interface， 那么这个目标也会包含${CMAKE_CURRENT_LIST_DIR}/include 目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是为什么header-only非常适合使用interface库。因为header-only没有源文件，自然不会有目标文件。当我的某个目标是一个header-only的库的时候，设置为接口库就相当于让每一个包含了这个header-only的库都自动使用同一份目录和编译选项（如有）&lt;/p&gt;
&lt;h2&gt;其他参考文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/7385776238789099529&quot;&gt;CMAKE简明教程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://aiden-dong.github.io/2019/07/20/CMake%E6%95%99%E7%A8%8B%E4%B9%8BCMake%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E5%BA%94%E7%94%A8/&quot;&gt;CMake 从入门到应用&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://elloop.github.io/tools/2016-04-10/learning-cmake-2-commands&quot;&gt;常用CMAKE命令和简要说明&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://subingwen.cn/cmake/CMake-primer/#1-CMake%E6%A6%82%E8%BF%B0&quot;&gt;CMAKE保姆级教程1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://subingwen.cn/cmake/CMake-advanced/#2-2-2-while&quot;&gt;CMAKE保姆级教程2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://a4z.gitlab.io/blog/2024/11/30/Print-all-CMake-variables.html&quot;&gt;打印出cmake的所有变量&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;memset 不可用于显式清除内存数据&lt;/h1&gt;
&lt;p&gt;先看&lt;a href=&quot;https://zh.cppreference.com/w/c/string/byte/memset&quot;&gt;cppreference&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;若 &lt;code&gt;memset&lt;/code&gt; 所修改的对象在其生存期的剩余部分不再被访问，则此函数可以被优化掉（在&lt;a href=&quot;https://zh.cppreference.com/w/c/language/as_if&quot;&gt;如同&lt;/a&gt;规则下）（例如 &lt;a href=&quot;https://gcc.gnu.org/bugzilla/show_bug.cgi?id=8537&quot;&gt;gcc 漏洞 8537&lt;/a&gt;）。为此，此函数不能用于擦洗内存（例如以零填充存储密码的数组）。&lt;/p&gt;
&lt;p&gt;对 &lt;code&gt;memset_explicit&lt;/code&gt; 和 &lt;code&gt;memset_s&lt;/code&gt; 禁止此优化：保证进行内存写。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个bug来自PPSSPP代码报告&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1167/&quot;&gt;这里&lt;/a&gt;的N18&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void sha1_hmac( unsigned char *key, int keylen,
                unsigned char *input, int ilen,
                unsigned char output[20] )
{
  sha1_context ctx;

  sha1_hmac_starts( &amp;amp;ctx, key, keylen );
  sha1_hmac_update( &amp;amp;ctx, input, ilen );
  sha1_hmac_finish( &amp;amp;ctx, output );

  memset( &amp;amp;ctx, 0, sizeof( sha1_context ) ); // &amp;lt;&amp;lt;!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为memset 存在Dead Store Elimination，所以这个memset可能会被优化掉，导致内存数据并没有被清除&lt;/p&gt;
&lt;h1&gt;std::launder&lt;/h1&gt;
&lt;p&gt;什么是&lt;a href=&quot;https://miyuki.github.io/2016/10/21/std-launder.html&quot;&gt;launder&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1160/&quot;&gt;launder和const、编译器的常量传播之间的神秘关系 查看Const and optimizations一节&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;new&amp;gt;
#include &amp;lt;iostream&amp;gt;

struct A { int x = 1; };
struct B { int y = 42; };

int main() {
    alignas(B) char buffer[sizeof(B)]; //必须和B对齐不然炸了
    A* a = new (buffer) A;
    std::cout &amp;lt;&amp;lt; a-&amp;gt;x &amp;lt;&amp;lt; &quot;\n&quot;;

    // 用 placement new 在同一块内存构造另一个类型
    B* b = new (buffer) B;

    // 直接访问 b-&amp;gt;y 是 **未定义行为**！
    // 因为优化器认为这块内存“本来是 A”，没有新对象被构造
    std::cout &amp;lt;&amp;lt; std::launder(b)-&amp;gt;y &amp;lt;&amp;lt; &quot;\n&quot;; // 正确做法
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个东西的核心就是&lt;strong&gt;刷新编译器的“类型视角”&lt;/strong&gt;：&lt;strong&gt;告诉它这里是新构造出来的对象&lt;/strong&gt;，也就是强制编译器 &lt;strong&gt;重新加载指针指向对象&lt;/strong&gt;；可以查看标准文档的 17.6.5[ptr.launder]&lt;/p&gt;
&lt;h1&gt;range based for loop的临时范围初始化器的雷&lt;/h1&gt;
&lt;p&gt;查看&lt;a href=&quot;https://stackoverflow.com/questions/72510589/a-temporary-object-in-range-based-for-loop&quot;&gt;这里&lt;/a&gt;和&lt;a href=&quot;https://stackoverflow.com/questions/51436155/range-based-for-loop-on-a-temporary-range/51440883#51440883&quot;&gt;这里&lt;/a&gt;。弄清楚何时会导致引用绑定的临时对象失效。以及在&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1156/&quot;&gt;这里的&lt;em&gt;Errors in object lifetime: lifetime extension&lt;/em&gt;章节&lt;/a&gt;看一下临时对象的生命周期被临时范围初始化器错误的延长&lt;/p&gt;
&lt;p&gt;以及这里的&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1149/&quot;&gt;Errors in object lifetime: a fly in the syntactic sugar (range-based for)&lt;/a&gt;一章看范围for循环的生命周期问题。&lt;/p&gt;
&lt;p&gt;其实说白了核心是一点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;总而言之，&lt;strong&gt;临时量的生存期不能以进一步“传递”来延续&lt;/strong&gt;：从绑定了该临时量的引用或数据成员初始化的&lt;strong&gt;第二引用不影响临时量的生存期&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;范围初始化器可以抽象看成是这样的形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
属性(可选) for ( 初始化语句(可选) 项声明 : 范围初始化器 ) 语句        
*/
{
    auto&amp;amp;&amp;amp; /* range */ = 范围初始化器;
        for (auto /* begin */ = /* 首表达式 */, /* end */ = /* 尾表达式 */;
        /* begin */ != /* end */; ++/* begin */){
            项声明 = */* begin */;
            语句
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设我们有这种代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Point {
    int x;
    int y;
};

struct Shape {
public:
    using VertexList = std::vector&amp;lt;Point&amp;gt;;
    explicit Shape(VertexList v) : vertexes(std::move(v)) {}

    const VertexList&amp;amp; Vertexes() const { // 1 返回const&amp;amp;
        return vertexes;
    }

    VertexList vertexes;
};

Shape MakeShape() {
    return Shape { Shape::VertexList{ {1,0}, {0,1}, {0,0}, {1,1} } };
}

int main() {
    for (auto v : MakeShape().Vertexes()) { // 2 注意这种范围初始化器初始方式
        std::cout &amp;lt;&amp;lt; v.x &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; v.y &amp;lt;&amp;lt; &quot;\n&quot;;
    }
    std::cout &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; std::endl;
    for (auto v : MakeShape().vertexes) { // 3 注意这种范围初始化器初始方式
        std::cout &amp;lt;&amp;lt; v.x &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; v.y &amp;lt;&amp;lt; &quot;\n&quot;;
    }
}
/*
7363 0
-1948417267 -1125936379
0 0
1 1
 
1 0
0 1
0 0
1 1
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们注意到方法2输出垃圾值。方法3是OK的 这就是雷点。&lt;/p&gt;
&lt;p&gt;我们已经知道了，&lt;strong&gt;临时量的生存期不能以进一步“传递”来延续&lt;/strong&gt;：从绑定了该临时量的引用或数据成员初始化的&lt;strong&gt;第二引用不影响临时量的生存期&lt;/strong&gt;。我们手动“扩展一下”这个范围循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto&amp;amp;&amp;amp; /* range */ = MakeShape().Vertexes(); // 方法2
auto&amp;amp;&amp;amp; /* range */ = MakeShape().vertexes; // 方法3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们已经发现端倪。方法2的&lt;code&gt;Vertexes()&lt;/code&gt;函数调用返回的临时对象已经绑定到了函数返回值的&lt;code&gt;const &amp;amp;&lt;/code&gt;虽然后续被绑定到了&lt;code&gt;auto&amp;amp;&amp;amp;&lt;/code&gt; 但是无法被二次延长。方法3当中没有方法2那种函数返回值从中作梗，所以正确的被&lt;code&gt;auto&amp;amp;&amp;amp;&lt;/code&gt;延长了生命周期。&lt;/p&gt;
&lt;h2&gt;如何避免这个雷&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;记得对任何const类型的函数进行rvalue限定的重载&lt;/li&gt;
&lt;li&gt;针对范围for循环的范围初始化器，一定仅使用变量本身或者它的成员变量。&lt;/li&gt;
&lt;li&gt;C++20后使用新语法&lt;/li&gt;
&lt;li&gt;如果有机会，使用&lt;code&gt;std::ranges::for_each&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;浮点数的各种判断是否为0&lt;/h1&gt;
&lt;p&gt;看周刊&lt;a href=&quot;https://github.com/wanghenshui/cppweeklynews/blob/dev/posts/171.md&quot;&gt;这一期&lt;/a&gt;，代码在&lt;a href=&quot;https://godbolt.org/z/jcqc38qqW&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;算术运算溢出相关的未定义行为&lt;/h1&gt;
&lt;p&gt;https://pvs-studio.com/en/blog/posts/cpp/1136/&lt;/p&gt;
&lt;h1&gt;C++中，f(x) 有多少种可能？&lt;/h1&gt;
&lt;p&gt;https://biowpn.github.io/bioweapon/2024/11/12/what-does-f-x-mean.html
强烈建议看一下&lt;/p&gt;
&lt;h1&gt;Proxy库&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;https://redhand.com.cn/2025/01/02/proxy-convension/&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;ECS模式的数据结构设计&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.google.com/presentation/d/1PbCH2IRg8lW08JlUz-xQEQjTo_Fr5n9QZOTEjGqZnuU/edit?pli=1#slide=id.g35f391192_00&quot;&gt;PPT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=b9hNKFj5R3Y&quot;&gt;讲座&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;一个复合稀疏密集数据结构的样例&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;vector&amp;gt;
#include &amp;lt;cassert&amp;gt;

template&amp;lt;typename T&amp;gt;
class DenseSparseArray {
public:
    // 插入元素（假设 entity 是唯一标识）
    void insert(uint32_t entity, const T&amp;amp; value) {
        if (entity &amp;gt;= sparse.size()) {
            sparse.resize(entity + 1, -1); // -1 表示无效索引
        }
        
        if (sparse[entity] == -1) {
            sparse[entity] = dense.size();
            dense.push_back({entity, value});
        }
    }

    // 删除元素
    void erase(uint32_t entity) {
        if (contains(entity)) {
            size_t dense_idx = sparse[entity];
            auto&amp;amp; last = dense.back();

            // 将要删除的元素与最后一个元素交换
            std::swap(dense[dense_idx], last);
            sparse[last.entity] = dense_idx;

            dense.pop_back();
            sparse[entity] = -1;
        }
    }

    // 访问元素
    T&amp;amp; operator[](uint32_t entity) {
        assert(contains(entity));
        return dense[sparse[entity]].value;
    }

    // 判断是否存在
    bool contains(uint32_t entity) const {
        return entity &amp;lt; sparse.size() &amp;amp;&amp;amp; sparse[entity] != -1;
    }

    // 迭代器支持
    auto begin() { return dense.begin(); }
    auto end() { return dense.end(); }

private:
    struct Element {
        uint32_t entity;
        T value;
    };

    std::vector&amp;lt;int&amp;gt; sparse;  // 稀疏数组（存储索引）
    std::vector&amp;lt;Element&amp;gt; dense; // 密集数组（实际数据）
};

enseSparseArray&amp;lt;int&amp;gt; arr;

arr.insert(100, 42);  // 插入 entity=100
arr.insert(200, 77);  // 插入 entity=200

std::cout &amp;lt;&amp;lt; arr[100]; // 输出 42

arr.erase(100);       // 删除 entity=100

for (auto&amp;amp; elem : arr) { // 遍历所有有效元素
    std::cout &amp;lt;&amp;lt; elem.entity &amp;lt;&amp;lt; &quot;: &quot; &amp;lt;&amp;lt; elem.value &amp;lt;&amp;lt; &quot;\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;匿名联合体&lt;/h1&gt;
&lt;p&gt;在&lt;a href=&quot;https://github.com/facebook/folly/blob/main/folly/futures/detail/Core.h&quot;&gt;folly&lt;/a&gt;中，有这样一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class ResultHolder {
 protected:
  ResultHolder() {}
  ~ResultHolder() {}
  // Using a separate base class allows us to control the placement of result_,
  // making sure that it&apos;s in the same cache line as the vtable pointer and the
  // callback_ (assuming it&apos;s small enough).
  union {
    Try&amp;lt;T&amp;gt; result_;
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个类里面的union叫做&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/union#.E5.8C.BF.E5.90.8D.E8.81.94.E5.90.88.E4.BD.93&quot;&gt;匿名联合体&lt;/a&gt;。为啥要把result用union包一下？&lt;strong&gt;目的是防止T类型没有默认构造函数导致编译错误。因为匿名联合体内的成员不会被默认初始化。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/default_initialization&lt;/p&gt;
&lt;p&gt;在默认初始化章节中提到了。&lt;/p&gt;
&lt;p&gt;包括标准库文档的11.9.3[class.base.init]节:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;otherwise, if the entity is an anonymous union or a variant member (11.5.2), no initialization is
performed;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;同时。如果一个类包含了一个匿名联合体，则这个匿名联合体不会被初始化&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/constructor&lt;/p&gt;
&lt;p&gt;在开始执行组成构造函数体的复合语句之前，所有直接基类、虚基类和非静态数据成员的初始化均已结束。这些对象的非默认初始化只能在成员初始化器列表指定。对于不能默认初始化的基类，和不能以默认初始化或以其&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/data_members#.E6.88.90.E5.91.98.E5.88.9D.E5.A7.8B.E5.8C.96&quot;&gt;默认成员初始化器&lt;/a&gt;（如果有）(C++11 起)初始化的非静态数据成员，例如引用和 const 限定的类型的成员，必须指定成员初始化器。（注意，类模板实例化的非静态数据成员的默认成员初始化器，当成员类型或初始化器待决时可能是无效的。)(C++11 起)&lt;strong&gt;对没有成员初始化器或默认成员初始化器(C++11 起)的匿名联合体或变体成员不进行初始化。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;一定要区分联合体和匿名联合体&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class ResultHolder {
public:
  union {
    T result_;
  };  // 匿名联合体
  union s{
      T result_;
  } // 联合体 但是这里只有声明没有定义
  s s_; //定义 但是报错。因为不是匿名联合体依旧会被尝试初始化
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看一个简化版的最小实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;new&amp;gt; // for placement new

struct A {
  int x;
  A() = delete; // 禁止默认构造
  A(int v) : x(v) { std::cout &amp;lt;&amp;lt; &quot;A(&quot; &amp;lt;&amp;lt; v &amp;lt;&amp;lt; &quot;) constructed\n&quot;; }
  ~A() { std::cout &amp;lt;&amp;lt; &quot;A destroyed\n&quot;; }
};

// folly 风格的 ResultHolder
template &amp;lt;typename T&amp;gt;
class ResultHolder {
public:
  ResultHolder() {}  // 允许默认构造，但不会初始化 result_
  ~ResultHolder() {
      // 不能换成
	 // result_.~T();
     // 因为我们不能确保T被构造过。生命周期管理需要对称，T是手动构造就必须手动析构。
  } // 允许默认析构，但需要手动析构 result_

  // 使用 匿名union 避免默认构造，同时不提供额外的默认构造函数
  union {
    T result_;
  };
    // T result_; // 如果用这种就会报错
};

int main() {
  // 由于 result_ 没有默认构造，需要手动构造
  ResultHolder&amp;lt;A&amp;gt; holder;
  new (&amp;amp;holder.result_) A(42); // 使用 placement new 构造 A
  std::cout &amp;lt;&amp;lt; &quot;A.x = &quot; &amp;lt;&amp;lt; holder.result_.x &amp;lt;&amp;lt; &quot;\n&quot;;

  // 手动调用析构，否则会产生未定义行为
  holder.result_.~A();

  return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;thread local 线程存储期的性能瓶颈&lt;/h1&gt;
&lt;p&gt;https://yosefk.com/blog/cxx-thread-local-storage-performance.html
这篇文章详细介绍了TLS (thread local storage)对象的性能瓶颈。说白了：&lt;/p&gt;
&lt;p&gt;tls对象在有类/构造函数维护 + fpic共享库需要额外调用__tls_get_addr 成为性能瓶颈&lt;/p&gt;
&lt;p&gt;优化指南&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TLS对象尽可能合并&lt;/li&gt;
&lt;li&gt;不要为TLS写构造函数&lt;/li&gt;
&lt;li&gt;为频繁访问的对象使用__attribute__((visibility(&quot;hidden&quot;)))&lt;/li&gt;
&lt;li&gt;为关键变量使用__attribute__((tls_model(&quot;initial-exec&quot;)))&lt;/li&gt;
&lt;li&gt;如果不是共享库，不要使用fpic （静态库链接到二进制，可以去掉）&lt;/li&gt;
&lt;li&gt;考虑使用编译器加速 比如-mtls-dialect=gnu2&lt;/li&gt;
&lt;li&gt;使用pthread key分配+自定义pthread_getspecifc绕过&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;设计几种迭代器&lt;/h1&gt;
&lt;p&gt;https://johnfarrier.com/7-interesting-and-powerful-uses-for-c-iterators/?utm_source=rss&amp;amp;utm_medium=rss&amp;amp;utm_campaign=7-interesting-and-powerful-uses-for-c-iterators&lt;/p&gt;
&lt;p&gt;个人理解里面有几种还是很有意思的。比如：&lt;/p&gt;
&lt;h2&gt;无限序列迭代器，适用于生成一些数字&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;iterator&amp;gt;

class fibonacci_iterator {
    long long a = 0, b = 1;

   public:
    using iterator_category = std::input_iterator_tag;
    using value_type = long long;
    using difference_type = std::ptrdiff_t;
    using pointer = const long long*;
    using reference = const long long&amp;amp;;

    fibonacci_iterator&amp;amp; operator++() {
        // 注意这里写的是前置自增
        long long temp = a;
        a = b;
        b += temp;
        return *this;
    }

    long long operator*() const { return a; }

    // Always true for infinite sequences
    bool operator!=(const fibonacci_iterator&amp;amp;) const { return true; }
};

int main() {
    auto it = fibonacci_iterator();
    for (int i = 0; i &amp;lt; 10; ++i, ++it) {  // 前置自增
        std::cout &amp;lt;&amp;lt; *it &amp;lt;&amp;lt; &quot; &quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类似于transform作用的迭代器适配器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;iterator&amp;gt;
#include &amp;lt;algorithm&amp;gt;

template &amp;lt;typename Iter, typename Func&amp;gt;
class transform_iterator {
    Iter it;
    Func func;

   public:
    using iterator_category = std::input_iterator_tag;
    using value_type = typename std::result_of&amp;lt;Func(
        typename std::iterator_traits&amp;lt;Iter&amp;gt;::value_type)&amp;gt;::type;  // 萃取一下函数调用的返回类型
    using difference_type = std::ptrdiff_t;
    using pointer = value_type*;
    using reference = value_type;

    transform_iterator(Iter iter, Func f) : it(iter), func(f) {}

    transform_iterator&amp;amp; operator++() {
        // 还是，前置自增
        ++it;
        return *this;
    }
    reference operator*() const {
        // 注意 这重载的是解引用操作符。不是乘法。乘法是二元，解引用是一元。
        // 这里的目的是搭配一些STL库的算法。比如copy是解引用赋值。
        // 返回reference类型的原因是解引用操作符希望返回一个引用，这样才能被赋值。
        return func(*it);
    }
    bool operator!=(const transform_iterator&amp;amp; other) const { return it != other.it; }
};

int main() {
    std::vector&amp;lt;int&amp;gt; numbers = {1, 2, 3, 4, 5};
    std::vector&amp;lt;int&amp;gt; squares{};
    auto square = [](int x) { return x * x; };
    auto begin = transform_iterator(numbers.begin(), square);
    auto end = transform_iterator(numbers.end(), square);

    std::copy(begin, end,
              std::back_inserter(
                  squares));  // 用back inserter往里面插。因为一开始没初始化，所以直接给迭代器
    for (auto i : squares) {
        std::cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; std::endl;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;就地过滤迭代器 in-place filter&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

class filtering_iterator {
    using vec_it = std::vector&amp;lt;int&amp;gt;::iterator;  // 简化实现。实际上应该使用模板参数
    vec_it current, end;
    int threshold;

   public:
    filtering_iterator(vec_it begin, vec_it end, int th) : current(begin), end(end), threshold(th) {
        advance();
    }

    filtering_iterator&amp;amp; operator++() {
        current = current == end ? end : current + 1;  // 如果是end就返回end,不然就+1
        advance();
        return *this;
    }

    int operator*() const { return *current; }

    bool operator!=(const filtering_iterator&amp;amp; other) const { return current != other.current; }

   private:
    void advance() {
        while (current != end &amp;amp;&amp;amp; *current &amp;lt; threshold) {
            current = current + 1;  // 跳过小于阈值的元素
            // 这里用 != 而不是 &amp;lt;是因为只有随机迭代器支持关系性比较
            // 另外只能+1，不然迭代器失效就炸了。如果要+n的话需要再套一个while
        }
    }
};

int main() {
    std::vector&amp;lt;int&amp;gt; data = {1, 5, 2, 8, 3, 10, 4};
    int threshold = 9;

    std::cout &amp;lt;&amp;lt; &quot;Filtered values: &quot;;
    for (filtering_iterator it(data.begin(), data.end(), threshold);
         it != filtering_iterator(data.end(), data.end(), threshold); ++it) {
        // 注意一点，我们用迭代器一般都是在for循环语句内现场创建，而不是在外面创建再传进去
        std::cout &amp;lt;&amp;lt; *it &amp;lt;&amp;lt; &quot; &quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C++杂记 - 5（三）：名称查找、翻译单元、std::string 实现与 TCMalloc</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05-3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05-3/</guid><pubDate>Tue, 07 Mar 2023 06:57:00 GMT</pubDate><content:encoded>&lt;h1&gt;标识符， 限定标识符，有限定名称查找，无限定名称查找和ADL 实参依赖查找&lt;/h1&gt;
&lt;h2&gt;标识符&lt;/h2&gt;
&lt;p&gt;C++编译器将文件代码源文件解析后，将代码分解为&lt;code&gt;identifier&lt;/code&gt;&lt;a href=&quot;https://lrita.github.io/2018/09/29/name-lookup-in-cpp11/#fn:1&quot;&gt;1&lt;/a&gt;、数值、运算符等，其中&lt;code&gt;identifier&lt;/code&gt;是由非数字开头、任意字符数字和下划线组成的部分，其用来组成&lt;code&gt;声明&lt;/code&gt;、&lt;code&gt;表达式&lt;/code&gt;、&lt;code&gt;name&lt;/code&gt;和&lt;code&gt;qualified identifier&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;声明&lt;/code&gt;中&lt;code&gt;identifier&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不能时语法关键字&lt;/li&gt;
&lt;li&gt;不要以双下划线（&lt;code&gt;__&lt;/code&gt;）或者下划线（&lt;code&gt;_&lt;/code&gt;）开头，以免和编译器或者标准库的内部声明冲突，可以参见&lt;code&gt;17.6.4.3 [reserved.names]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;identifier&lt;/code&gt;在表达式中除了表示一些简单的函数和对象外，还可以是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数写法的重载运算符名。比如 &lt;code&gt;operator+&lt;/code&gt; or &lt;code&gt;operator new&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the name of an operator function, such as &lt;code&gt;operator+&lt;/code&gt; or &lt;code&gt;operator new&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;用户定义转换函数的名字，比如 &lt;code&gt;operator bool&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;用户定义字面量运算符的名字 比如 &lt;code&gt;operator &quot;&quot; _km&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~&lt;/code&gt;字符后随类名，如&lt;code&gt;~MyClass&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~&lt;/code&gt; 字符后随decltype说明符，比如&lt;code&gt;~decltype(str)&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;模板名后随包含模板实参的角括号 比如&lt;code&gt;MyTemplate&amp;lt;int&amp;gt;&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;作用域解析操作符&lt;code&gt;::&lt;/code&gt;限定的标识符 例如 &lt;code&gt;std::string&lt;/code&gt; or &lt;code&gt;::tolower&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;限定标识符&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;qualified identifier(限定标识符)&lt;/code&gt;是由域解析符&lt;code&gt;::&lt;/code&gt;标识与&lt;code&gt;class&lt;/code&gt;名、枚举类名、&lt;code&gt;namespace&lt;/code&gt;或者&lt;code&gt;decltype&lt;/code&gt;表达式限定的一类&lt;code&gt;identifier&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;比如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::string::npos&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::tolower&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::std::cout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boost::signals2::connection&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;名称&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;name&lt;/code&gt;是指下面的一个实体或标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个标识符&lt;code&gt;identifier&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;操作符函数 (&lt;code&gt;operator+&lt;/code&gt;, &lt;code&gt;operator new&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://doc.bccnsoft.com/docs/cppreference2015/en/cpp/language/cast_operator.html&quot;&gt;用户定义的转换函数&lt;/a&gt; (&lt;code&gt;operator bool&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://doc.bccnsoft.com/docs/cppreference2015/en/cpp/language/user_literal.html&quot;&gt;用户定义的字面值转换符&lt;/a&gt; (&lt;code&gt;operator &quot;&quot; _km&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;模板 id (&lt;code&gt;name&amp;lt;arg, arg&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;goto&lt;/code&gt;语句指向的&lt;code&gt;label&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当编译器遇到一个未知的&lt;code&gt;name&lt;/code&gt;时，会进行&lt;code&gt;name lookup&lt;/code&gt;&lt;a href=&quot;https://lrita.github.io/2018/09/29/name-lookup-in-cpp11/#fn:2&quot;&gt;2&lt;/a&gt;，例如，当编译&lt;code&gt;std::cout &amp;lt;&amp;lt; std::endl;&lt;/code&gt;时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为&lt;code&gt;std&lt;/code&gt;左侧没有&lt;code&gt;::&lt;/code&gt;进行限定，则对&lt;code&gt;std&lt;/code&gt;进行&lt;code&gt;unqualified name lookup&lt;/code&gt;，发现其是一个声明在头文件&lt;code&gt;&amp;lt;iostream&amp;gt;&lt;/code&gt;中的&lt;code&gt;namespace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;因为&lt;code&gt;cout&lt;/code&gt;左侧有&lt;code&gt;::&lt;/code&gt;进行限定且其限定的域为一个&lt;code&gt;namespace&lt;/code&gt;，则对&lt;code&gt;cout&lt;/code&gt;进行&lt;code&gt;qualified name lookup&lt;/code&gt;，发现其是一个声明在&lt;code&gt;namespace std&lt;/code&gt;中的变量&lt;/li&gt;
&lt;li&gt;因为&lt;code&gt;endl&lt;/code&gt;左侧有&lt;code&gt;::&lt;/code&gt;进行限定且其限定的域为一个&lt;code&gt;namespace&lt;/code&gt;，则对&lt;code&gt;endl&lt;/code&gt;进行&lt;code&gt;qualified name lookup&lt;/code&gt;，发现其是一个声明在&lt;code&gt;namespace std&lt;/code&gt;中的函数模板&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;没有限定且为一个函数，对&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;进行&lt;code&gt;argument-dependent lookup&lt;/code&gt;，发现其是一个声明在&lt;code&gt;namespace std&lt;/code&gt;中的函数模板声明&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;其主要规则是，如果目标是一个&lt;code&gt;qualified identifier(限定标识符)&lt;/code&gt;，进行&lt;code&gt;Qualified name lookup&lt;/code&gt;&lt;a href=&quot;https://lrita.github.io/2018/09/29/name-lookup-in-cpp11/#fn:3&quot;&gt;3&lt;/a&gt;，否则进行&lt;code&gt;Unqualified name lookup&lt;/code&gt;&lt;a href=&quot;https://lrita.github.io/2018/09/29/name-lookup-in-cpp11/#fn:4&quot;&gt;4&lt;/a&gt;，对于函数还可能进行&lt;code&gt;Argument-dependent lookup&lt;/code&gt;&lt;a href=&quot;https://lrita.github.io/2018/09/29/name-lookup-in-cpp11/#fn:5&quot;&gt;5&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;有限定名称查找 Qualified name lookup&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;当遇到未知的&lt;code&gt;qualified identifier(限定标识符)&lt;/code&gt;时，会去其对应的限定符区域内进行查找，比如命名空间、类作用域、枚举空间等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;std::cout &amp;lt;&amp;lt; 1;  // 解析&quot;cout&quot;时，去命名空间std中进行查找
struct A {
    typedef int type;
};
A::type a;  // 解析&quot;type&quot;时，去A类作用域中查找
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果&lt;code&gt;::&lt;/code&gt;左侧没有限定符，则去全局命名空间进行查找，这样就避免了被本地声明遮盖的情况：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
int main() {
    struct std {};
    std::cout &amp;lt;&amp;lt; &quot;fail\n&quot;;  // Error: unqualified lookup for &apos;std&apos; finds the struct
    ::std::cout &amp;lt;&amp;lt; &quot;ok\n&quot;;  // OK: ::std finds the namespace std
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当然，在解析&lt;code&gt;::&lt;/code&gt;右手侧的&lt;code&gt;name&lt;/code&gt;之前需要先解析&lt;code&gt;::&lt;/code&gt;左手侧的&lt;code&gt;name&lt;/code&gt;（除非使用了&lt;code&gt;decltype&lt;/code&gt;表达式或者&lt;code&gt;::&lt;/code&gt;左侧为空）。&lt;strong&gt;至于带&lt;code&gt;::&lt;/code&gt;的&lt;code&gt;name&lt;/code&gt;查找时&lt;code&gt;qualified name lookup&lt;/code&gt;还是&lt;code&gt;unqualified name lookup&lt;/code&gt;，取决于&lt;code&gt;::&lt;/code&gt;左侧的&lt;code&gt;name&lt;/code&gt;。当&lt;code&gt;::&lt;/code&gt;左侧&lt;code&gt;name&lt;/code&gt;为&lt;code&gt;namespace&lt;/code&gt;、&lt;code&gt;class&lt;/code&gt;、&lt;code&gt;枚举&lt;/code&gt;、&lt;code&gt;实例化的模板&lt;/code&gt;时，为&lt;code&gt;qualified name lookup&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    static int n;
};
int A::n = 0;
int main() {
    int A;
    A::n = 42;   // OK: 对&quot;A&quot;进行&quot;unqualified lookup&quot;时，忽略了本地变量A
    A b;         // 错误: 对&quot;A&quot;进行&quot;unqualified lookup&quot;会指向本地变量A
    struct A b;  // A 前面需要增加tag &quot;struct&quot; 进行限定，以避免被本地变量A遮盖
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;qualified name&lt;/code&gt;用于&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/declarations&quot;&gt;声明&lt;/a&gt;时，当同在一个声明中的&lt;code&gt;unqualified name&lt;/code&gt;需要进行&lt;code&gt;unqualified lookup&lt;/code&gt;时，在&lt;code&gt;qualified name&lt;/code&gt;对应的类作用域、命名空间进行查找。不在同一个声明中的&lt;code&gt;unqualified name&lt;/code&gt;，不会受到这个影响。同时也不受声明类型的影响。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面这一段话比较难描述，需要结合一个例子来理解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class X {};
constexpr int number = 100;
struct C {
    class X {};
    static const int number = 50;
    static X arr[number];
};
X C::arr[number],
    brr[number];  // 错误: &quot;X&quot;经过`unqualified name`解析为`::X`与`C::arr`对应的`C::X`不同。
C::X C::arr[number], brr[number];  // OK: `C::arr`的长度为50，`brr`的长度为100
// `C::arr[number]`中的`number`受同一个声明中的&quot;qualified name&quot;`C::arr`
// 的影响，回去类作用域`C`中查找，得到`C::number`，也就是50。而`brr[number]`
// 与`C::arr[number]`不是同一个声明，则不受这种影响，得到`::number`，也就是100
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 &lt;code&gt;::&lt;/code&gt; 后随字符 &lt;code&gt;~&lt;/code&gt; 再跟着一个标识符（也就是说指定了析构函数或伪析构函数），那么该标识符将在与 &lt;code&gt;::&lt;/code&gt; 左边的名字相同的作用域中查找。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct C {
    typedef int I;
};
typedef int I1, I2;
extern int *p, *q;
struct A {
    ~A();
};
typedef A AB;
int main() {
    p-&amp;gt;C::I::~I();  // 解析`~I()`时，使用`C::I`这个`name`相同的域`C::`，则找到了`C::I`
    q-&amp;gt;I1::~I2();  // 解析`~I2()`时，使用`::I1`这个`name`相同的域，则找到了`::I2`
    AB x;
    x.AB::~AB();  // ~ 之后的名字 AB 在 :: 前面的 AB 的同一个作用域中查找
                  // 也就是说从当前的作用域中查找，因此查找结果是 ::AB
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当&lt;code&gt;::&lt;/code&gt;左侧为枚举类时，则&lt;code&gt;::&lt;/code&gt;右侧的&lt;code&gt;name&lt;/code&gt;必须属于这个枚举类，否则为&lt;code&gt;ill-formed&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;qualified name lookup&lt;/code&gt;还可以被用来调用被隐藏的方法，这种调用方式不会调用虚函数：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct B {
    virtual void foo();
};
struct D : B {
    void foo() override;
};
int main() {
    D x;
    B &amp;amp;b = x;
    b.foo();     // calls D::foo (virtual dispatch)
    b.B::foo();  // calls B::foo (static dispatch)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;模板参数进行解析时，从当前域进行解析&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;namespace N {
    template &amp;lt;typename T&amp;gt;
    struct foo {};
    struct X {};
}  // namespace N
N::foo&amp;lt;X&amp;gt; x;  // 错误：&quot;X&quot;被解析为&quot;::X&quot;，而不是&quot;N::X&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;对一个&lt;code&gt;namespace N&lt;/code&gt;进行&lt;code&gt;qualified name lookup&lt;/code&gt;时，首先考虑&lt;code&gt;namespace N&lt;/code&gt;内的声明和&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/namespace#Inline_namespaces&quot;&gt;&lt;code&gt;inline namespace members&lt;/code&gt;&lt;/a&gt;。如果没有匹配的，其次考虑&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/namespace#Using-directives&quot;&gt;&lt;code&gt;using-directives&lt;/code&gt;&lt;/a&gt;导入到&lt;code&gt;namespace N&lt;/code&gt;中的声明&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;int x;
namespace Y {
    void f(float);
    void h(int);
}  // namespace Y
namespace Z {
    void h(double);
}  // namespace Z
namespace A {
    using namespace Y;
    void f(int);
    void g(int);
    int i;
}  // namespace A
namespace B {
    using namespace Z;
    void f(char);
    int i;
}  // namespace B
namespace AB {
    using namespace A;
    using namespace B;
    void g();
}  // namespace AB
void h() {
    AB::g();  // 首先考虑&quot;AB::g&quot;，则不再对namespace A、B进行查找
    AB::f(1);  // 首先在namespace AB中查找，未匹配，然后到namespace A、B中查找，查找到&quot;A::f&quot;和&quot;B::f&quot;，
               // 有匹配，则不再对namespace Y进行查找。然后在&quot;A::f&quot;和&quot;B::f&quot;中间选择了&quot;A::f(int)&quot;。
    AB::x++;  // 首先在namespace AB中查找，未匹配。然后到namespace A、B中查找，未匹配。
              // 然后到namespace Y、Z中查找，未匹配。然后报错。
    AB::i++;  // 首先在namespace AB中查找，未匹配。然后到namespace A、B中查找，匹配到&quot;A::i&quot;和&quot;B::i&quot;
              // 存在冲突，报错。
    AB::h(16.8);  // 首先在namespace AB中查找，未匹配。然后到namespace A、B中查找，未匹配。
                  // 然后到namespace Y、Z中查找，匹配到&quot;Y::h&quot;和&quot;Z::h&quot;，选择&quot;Z::h(double)&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在&lt;code&gt;namespace&lt;/code&gt;中进行&lt;code&gt;qualified name lookup&lt;/code&gt;时，允许通过不同途径匹配到相同的类型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;namespace A {
    int a;
}  // namespace A
namespace B {
    using namespace A;
}  // namespace B
namespace D {
    using A::a;
}  // namespace D
namespace BD {
    using namespace B;
    using namespace D;
}  // namespace BD
void g() {
    BD::a++;  // 首先在 namespace BD 中查找，未匹配。然后到 namespace B、D中查找，未匹配。
              // 再下面，namespace B、D都同时指向了 namespace A，则在 namespace A 中匹
              // 配到 A::a，选择 &quot;A::a&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;无限定名称查找&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;unqualified name&lt;/code&gt;只要指那些左侧没有&lt;code&gt;::&lt;/code&gt;域符号限定的&lt;code&gt;name&lt;/code&gt;。在搜索时，在相关命名空间、&lt;code&gt;using&lt;/code&gt;引入的命名空间等域进行，直到找到一个匹配的类型则停止。&lt;/p&gt;
&lt;p&gt;当遇到一个未知的&lt;code&gt;unqualified name&lt;/code&gt;时，会从当前文件域、命名空间开始查找，具体情形比较简单。下面列出一些需要特殊注意的点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个变量的定义在其命名空间&lt;code&gt;X&lt;/code&gt;外时且定义语句中引用了&lt;code&gt;unqualified name&lt;/code&gt;，解析这个&lt;code&gt;unqualified name&lt;/code&gt;时，首先从变量命名空间&lt;code&gt;X&lt;/code&gt;开始查找。有点类似ADL，但是注意区别。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;namespace X {
    extern int x;  // 声明，不是定义
    int n = 1;  // 在解析x的定义时，其引用了unqualified name “n”，先从其定义域&quot;X&quot;开始搜索，首先发现 n=1
};  // namespace X

int n = 2;     // 如果X::n不存在时，则匹配::n，否则被遮盖。
int X::x = n;  // X::x的定义式，X::x的定义在其命名空间&quot;X&quot;外，且引用了unqualified name “n”，
               // 在解析&quot;n&quot;时，匹配X::n，则X::x为1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当在命名空间外定义一个&lt;code&gt;非成员函数&lt;/code&gt;时，如果函数内引用了&lt;code&gt;unqualified name&lt;/code&gt;时，解析这个&lt;code&gt;unqualified name&lt;/code&gt;时，依次搜索其函数定义之前的：&lt;code&gt;本地代码块&lt;/code&gt;、&lt;code&gt;代码块外层定义变量&lt;/code&gt;、函数定义之前的&lt;code&gt;命名空间&lt;/code&gt;、函数定义之前的&lt;code&gt;外层命名空间&lt;/code&gt;、函数定义之前的&lt;code&gt;全局域&lt;/code&gt;，直到找到一个匹配的&lt;code&gt;name&lt;/code&gt;，在函数定义之后的相关域不在搜索范围内。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;namespace A {
    namespace N {
        void f();
        int i = 3;  // 3: 查找&quot;命名空间&quot;，如果将①②注释掉，这句定义存在，则&quot;i&quot;的定义匹配此处，则i=3
    }  // namespace N
    int i = 4;  // 4: 查找&quot;外层命名空间&quot;，如果将①②③注释掉，这句定义存在，则&quot;i&quot;的定义匹配此处，则i=4
}  // namespace A

int i = 5;  // 5: 查找&quot;全局域&quot;，如果将①②③④注释掉，这句定义存在，则&quot;i&quot;的定义匹配此处，则i=5

// 在命名空间外定义了一个非成员函数f()，这个例子的注释需要按照序号来看。
void A::N::f() {
    int i = 2;  // 2: 其次查找&quot;外层代码块&quot;，如果将①注释掉，这句定义存在，则&quot;i&quot;的定义匹配此处，则i=2
    {
        int i = 1;  // 1: 首先查找“本地代码块”，如果这句定义存在，则&quot;i&quot;的定义匹配此处，则i=1
        std::cout &amp;lt;&amp;lt; i;  // 0: 在代码块中引用了unqualified name &quot;i&quot;，则需要查找&quot;i&quot;的定义
    }
}

// int i; // 如果将5移动到f()定义之后则不参与&quot;i&quot;的查找

namespace A {
    namespace N {
        // int i; // 如果将3移动到f()定义之后则不参与&quot;i&quot;的查找
    }
}  // namespace A
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;对于在类的定义中所使用的&lt;code&gt;unqualified name&lt;/code&gt;，当出现于除了在成员函数体、成员函数的默认实参、成员函数的异常规定、默认成员初始化器、契约条件 (C++20 起)或者嵌套类的定义（包括嵌套类从之派生的基类的名字）以外的任何位置时，要在下列作用域中查找：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类体之中直到这次使用点之前的部分以及其基类的整个类体&lt;/li&gt;
&lt;li&gt;基类的整个类体，找不到声明时，递归到基类的基类中&lt;/li&gt;
&lt;li&gt;当这个类是嵌套类时，其外围类体中直到这个类的声明之前的部分以及外围类的基类的整个类体&lt;/li&gt;
&lt;li&gt;当这个类是局部类或局部类的嵌套类时，定义了这个类的块作用域中直到其定义点之前的部分&lt;/li&gt;
&lt;li&gt;当这个类是命名空间的成员，或者命名空间成员类的嵌套类，或者命名空间成员函数的局部类时，查找这个命名空间作用域中直到这个类、其外围类或函数的定义之前的部分。若所查找的是由友元声明所引入的名字：这种情况下仅考虑其最内层的外围命名空间，否则的话，对外围命名空间的查找将照常持续直到全局作用域。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;namespace M {
    // const int i = 1; // 找不到这个
    class B {
        // const const int i = 3; // 找到了第三个（但之后会被访问检查所拒绝）
    };
}  // namespace M
// const int i = 5; // 找到了第五个
namespace N {
    // const int i = 4; // 找到了第四个
    class Y : public M::B {
        // static const int i = 2; // 找到了第二个
        class X {
            // static const int i = 1; // 找到了第一个
            int a[i];  // use of i
            // static const int i = 1; // 找不到这个
        };
        // static const int i = 2; // 找不到这个
    };
    // const int i = 4; // 找不到这个
}  // namespace N
// const int i = 5; // 找不到这个
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当在类作用域外定义一个&lt;code&gt;成员函数&lt;/code&gt;时，如果函数内引用了&lt;code&gt;unqualified name&lt;/code&gt;时，解析这个&lt;code&gt;unqualified name&lt;/code&gt;时，依次搜索其函数定义之前的：&lt;code&gt;本地代码块&lt;/code&gt;、&lt;code&gt;代码块外层定义变量&lt;/code&gt;、&lt;code&gt;类作用域&lt;/code&gt;、&lt;code&gt;基类作用域&lt;/code&gt;、函数定义之前的&lt;code&gt;命名空间&lt;/code&gt;、函数定义之前的&lt;code&gt;外层命名空间&lt;/code&gt;、函数定义之前的&lt;code&gt;全局域&lt;/code&gt;，直到找到一个匹配的&lt;code&gt;name&lt;/code&gt;，在函数定义之后的相关域不在搜索范围内。注意跟上面&lt;code&gt;非成员函数&lt;/code&gt;的区别。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class B {
    int i;  // 4: 如果1 2 3不存在，”i“匹配此处
};
namespace M {
    int i;  // 6: 如果1 2 3 4 5不存在，”i“匹配此处
    namespace N {
        int i;  // 5: 如果1 2 3 4不存在，”i“匹配此处
        class X : public B {
            int i;     // 3: 如果1 2不存在，”i“匹配此处
            void f();  // 0: class X中声明了一个函数f()
            // int i; // 将3移动到这里也OK，类成员的可见性受声明的位置的影响。
        };
        // int i; // 5移动到这里也可以，跟f()实现代码的位置有关，与f()声明的位置无关
    }  // namespace N
}  // namespace M

int i;  // 7: 如果1 2 3 4 5 6 7不存在，”i“匹配此处

// 定义class X中声明的函数f()
void M::N::X::f() {
    int i;  // 2: 如果2不存在，”i“匹配此处
    {
        int i;  // 2: 如果2存在，”i“首先匹配此处
        i = 16;
    }

    // int i; // 如果这句存在，1 2都不存在，也不会匹配此处，因为其出现在调用i的代码块之后
}
namespace M {
    namespace N {
    	// int i; // 5移动到这里不匹配，其出现在f()实现代码之后
    }
}  // namespace M
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;虚继承的优先性&lt;/code&gt;，这个概念用定义来讲非常麻烦，简单来讲就是：如果一个类A由类B派生而来、类B虚继承自类C，则在类A中进行非限定查找时，类B中的符号会隐藏遮盖掉类C中的name，如果类A还通过其他非虚继承的方式继承了类C，则类B不会隐蔽遮盖类C中的相同name，发生歧义错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct X {
    void f();
};
struct B1 : virtual X {
    void f();
};
struct B2 : virtual X {};
struct D : B1, B2 {
    void foo() {
        X::f();  // OK，调用了 X::f（有限定查找）
        f();     // OK，调用了 B1::f（无限定查找）
/*
C++98 规则：B1::f 隐藏 X::f，因此即便从 D 通过 B2 可以访问到 X::f，它也不能从 D 中的名字查找所找到。
C++11 规则：在 D 中对 f 的查找集合并未找到任何东西，继续处理其基类。 在 B1 中对 f 的查找集合找到了 B1::f，并且完成查找合并时替换了空集，此时在 C 中 对 f 的查找集合包含 B1 中的 B1::f。在 B2 中对 f 的查找集合并未找到任何东西，继续处理其基类。在 X 中对 f 的查找找到了 X::f合并时替换了空集，此时在 B2 中对 f 的查找集合包含 X 中的 X::f。当向 C 中合并时发现在 B2 的查找集合中的每个子对象（X）都是已经合并的各个子对象（B1）的基类，因此 B2 的集合被丢弃。C 剩下来的就是在 B1 中所找到的 B1::f (如果使用 struct D : B2, B1，则最后的合并将会替换掉。C 此时已经合并的 X 中的 X::f，因为已经加入到 C 中的每个子对象（就是 X）都是新集合（B1）中的至少一个子对象的基类，其最终结果是一样的：C 的查找集合只包含在 B1 中找到的 B1::f）
*/
    }
};
#include &amp;lt;iostream&amp;gt;

struct X {
    void f() { std::cout &amp;lt;&amp;lt; &quot;X::f()&quot; &amp;lt;&amp;lt; std::endl; }
};
struct B1 : virtual X {
    void f() { std::cout &amp;lt;&amp;lt; &quot;B1::f()&quot; &amp;lt;&amp;lt; std::endl; }
};
struct B2 : X {};
struct D : B1, B2 {
    void foo() {
        X::f();  // OK，调用了 X::f（有限定查找）
        f();  // 由于D还通过B2的渠道非虚继承了X，则X::f()不会被B1::f()遮盖掉，所以此处发生歧义错误。
    };
};

struct V {
    int v;
};
struct A {
    int a;
    static int s;
    enum { e };
};
struct B : A, virtual V {};
struct C : A, virtual V {};
struct D : B, C {};

void f(D &amp;amp;pd) {
    ++pd.v;        // OK：只有一个 v，因为只有一个虚基类子对象
    ++pd.s;        // OK：只有一个静态的 A::s，即便在 B 和 C 中都找到了它
    int i = pd.e;  // OK：只有一个枚举符 A::e，即便在 B 和 C 中都找到了它
    ++pd.a;        // 错误，有歧义：B 中的 A::a 和 C 中的 A::a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;类的静态成员定义时，如果引用了&lt;code&gt;unqualified name&lt;/code&gt;，其查找过程与类成员函数中&lt;code&gt;unqualified name&lt;/code&gt;的查找顺序相同&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct X {
    static int x;
    static const int n = 1;
};
int n = 2;
int X::x = n;  // 找到了 X::n，将 X::x 设置为 1 而不是 2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当一个类的友元函数被定义在类作用域内部时，该友元函数中引用的&lt;code&gt;unqualified name&lt;/code&gt;的查找顺序与该类的成员函数中的&lt;code&gt;unqualified name&lt;/code&gt;的查找顺序相同。当类的友元函数被定义在类作用域外部时，该友元函数中引用的&lt;code&gt;unqualified name&lt;/code&gt;的查找顺序与其所在命名空间的其他函数中的&lt;code&gt;unqualified name&lt;/code&gt;的查找顺序相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;int i = 3;  // 3: 当1 2不存在时，f1中的&quot;i&quot;匹配此处
            // 3: 当1 2不存在时，f2中的&quot;i&quot;也不会匹配此处
struct X {
    static const int i = 2; // 2: 当这句定义存在时，1不存在时，f1中的&quot;i&quot;匹配此处
                            // 2: 当1不存在时，f2中的&quot;i&quot;也不会匹配此处
    friend void f1(int x)
    {
        int i; // 1: 当这句定义存在时，f1中的&quot;i&quot;匹配此处
        i = x; // 0: 友元函数f1(int)被定义在类X作用域内部时，&quot;i&quot;的查找顺序与X的成员函数查找顺序相同
    }
    friend int f2();
    // static const int i = 2; // ②移动到此处也OK
};
void f2(int x) {
    int i; // 1: 当这句定义存在时，f2中的&quot;i&quot;匹配此处
    i = x; // 0: 友元函数f2(int)被定义在类X作用域外部时，&quot;i&quot;的查找顺序f2(int)所在命名空间的普通函数的查找顺序相同
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当一个类A的成员函数被声明为类B的友元，且该声明中包含&lt;code&gt;unqualified name&lt;/code&gt;，则对&lt;code&gt;unqualified name&lt;/code&gt;进行查找时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;unqualified name&lt;/code&gt;不是任何模板的参数，则首先去类作用域A中进行查找&lt;/li&gt;
&lt;li&gt;如果在类作用域A中未匹配或者其实模板参数，则去类作用域B中进行查找&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 这个类的成员函数被作为友元
struct A {
    typedef int AT;
    void f1(AT);
    void f2(float);
    template &amp;lt;class T&amp;gt;
    void f3();
};

// 这个类授予友元关系
struct B {
    typedef char AT;
    typedef float BT;
    friend void A::f1(AT);  // 对 &quot;AT&quot; 的查找时，先到类作用域A中进行查找，匹配&quot;A::AT&quot;
    friend void A::f2(BT);  // 对 &quot;BT&quot; 的查找时，先到类作用域A中进行查找，未匹配，
                            // 再去类作用域B中查找，匹配&quot;B::BT&quot;
    friend void
    A::f3&amp;lt;AT&amp;gt;();  // 对 &quot;AT&quot; 的查找时，&quot;AT&quot;是模板参数，则直接去类作用域B中查找，匹配&quot;B::AT&quot;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;unqualified name&lt;/code&gt;被当做函数默认参数时，查找其定义时，首先考虑同一函数声明中的形参:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class X {
    int a, b, i, j;

   public:
    const int &amp;amp;r;
    X(int i)
        : r(a),       // 将 X::r 初始化为指代 X::a
          b(i),       // 将 X::b 初始化为形参 i 的值
          i(i),       // 将 X::i 初始化为形参 i 的值
          j(this-&amp;gt;i)  // 将 X::j 初始化为 X::i 的值
    {}
}

int a;
int f(int a, int b = a);  // 错误：对 a 的查找找到了形参 a，而不是 ::a
                          // 但在默认实参中不允许使用形参
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在枚举类定义时，&lt;code&gt;unqualified name&lt;/code&gt;的查找首先考虑当前枚举类的作用域&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const int RED = 7;
enum class color {
    RED,
    GREEN = RED + 2,  // RED 找到了 color::RED ，而不是 ::RED ，因此 GREEN = 2
    BLUE = ::RED + 4  // 通过 qualified name lookup 找到 ::RED ， BLUE = 11
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在”try-catch”语句中，&lt;code&gt;unqualified name&lt;/code&gt;的查找跟函数体内引用的&lt;code&gt;unqualified name&lt;/code&gt;查找一样。平行的代码块内的定义不可见。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;int n = 3;          // 3
int f(int n = 2) {  // 2
    try {
        int n = -1;  // 不会匹配到该处
    } catch (...) {
        // int n = 1;   // 1: 加入此处存在，则匹配该处
        assert(n == 2);  // 0: n 按1 2 3的顺序进行依次查找，匹配f的参数n，即2
        throw;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;对于在表达式中所使用的重载运算符（比如在 &lt;code&gt;a+b&lt;/code&gt; 中使用的 operator+），其查找规则和对在如&lt;code&gt;operator+(a,b)&lt;/code&gt;这样的显式函数调用表达式中所使用的运算符是有所不同的：当处理表达式时要分别进行两次查找：对非成员的运算符重载，也对成员运算符重载（对于同时允许两种形式的运算符）。然后将这两个集合和在重载解析所述内建的运算符重载以平等的方式合并到一起。而当使用显式函数调用语法时（如&lt;code&gt;operator+(a,b)&lt;/code&gt;），则进行常规的&lt;code&gt;unqualified name lookup&lt;/code&gt;：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct A {};
void operator+(A, A);  // 用户定义的非成员 operator+

struct B {
    void operator+(B);  // 用户定义的成员 operator+
    void f();
};

A a;

void B::f()  // B 的成员函数定义
{
    operator+(a, a);  // 错误：在成员函数中的常规名字查找
                      // 找到了 B 的作用域中的 operator+ 的声明
                      // 并于此停下，而不会达到全局作用域
    a + a;            // OK：成员查找找到了 B::operator+，非成员查找
                      // 找到了 ::operator+(A,A)，重载决议选中了 ::operator+(A,A)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注入类名&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在一个类作用域中，当前类的名称被当做公开的成员名一样对待 也就是说一个类的名字会以&lt;strong&gt;unqualified-name&lt;/strong&gt;的形式，被注入到该类的作用域内。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此我们在类（类模板）的作用域内可以通过&lt;strong&gt;unqualified-name&lt;/strong&gt;的形式来指代该类。然而，&lt;strong&gt;qualified-name&lt;/strong&gt;的形式则不可以用来指代该类，因为这种形式被用来指代&lt;em&gt;构造函数&lt;/em&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int X;
struct X {
    void f() {
        X* p;  // OK ： X
               // 指代注入类名，在&quot;X&quot;的作用域中，&quot;X&quot;被当做公开成员，注意体会这个&quot;公开成员的含义&quot;
        ::X* q;  // 错误：在全局作用域&quot;::&quot;中，&quot;struct X&quot;被&quot;int X&quot;遮盖
    }
};

//-------------------------另外一个例子---------------------
int32_t C;

class C {
   private:
    int64_t i;

   public:
    static size_t f() {
        static_assert(sizeof(C) == sizeof(int64_t));
        return sizeof(C);  // C is injeceted as an `unqualified-name`
                           // into the class scope
    }

    static void g() {
        auto p = &amp;amp;::C;  // &apos;::C&apos; refers to the global variable if it
                        // is qualified with &apos;::&apos;
        static_assert(std::is_same_v&amp;lt;decltype(p), int32_t*&amp;gt;);
    }

    static void k() {
        // auto p = &amp;amp;C::C;
        // error: taking address of constructor &apos;constexpr C::C(C&amp;amp;&amp;amp;)&apos;
    }
};

auto f() -&amp;gt; size_t {
    static_assert(sizeof(C) == sizeof(int32_t));
    return sizeof(C);  // f in not in the C class scope, thus &apos;C&apos;
                       // refers to the global variable &apos;int32_t C&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在类继承的过程中，受到继承限制的控制&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;namespace detail {
    struct A {};
    struct B {};
};  // namespace detail

struct C : public detail::B, private detail::A {};

struct D : public C {
    A* a0;  // 错误：注入类名 A 受到“private”的修饰，变为非公开成员，不可访问
    detail::A* a1;  // OK：不使用注入类名
    B* b0;          // OK：通过注入类名
    detail::B* b1;  // OK：不使用注入类名
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在类模板中，类似普通类情形一样，可以被注入，当做模板名或者类型名。有下列3个情形之一时，注入的名称被当做当前模板名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它后面紧跟随 &lt;code&gt;&amp;lt;&lt;/code&gt; 符号（模板实例化标识）&lt;/li&gt;
&lt;li&gt;它被当做一个模板模板参数(&lt;code&gt;template template parameter&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;它是友元类模板声明的详细类型指定符中的最后标识符。 此外其他情形，会被当做一个实际类类型被注入，其类型为该类模板实例化后的类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 注意体会这个例子
template &amp;lt;template &amp;lt;class, class&amp;gt; class&amp;gt;
struct A;

template &amp;lt;class T1, class T2&amp;gt;
struct X {
    X&amp;lt;T1, T2&amp;gt;* p;  // X后跟随&quot;&amp;lt;&quot;，则X被当做模板 template&amp;lt;class, class&amp;gt; struct X 对待
                   // 同理，此处改为 X&amp;lt;int, int&amp;gt; *p 也是成立的，因为X是一个模板
    using a =
        A&amp;lt;X&amp;gt;;  // X被当做模板A的模板模板参数，则X被当做模板 template&amp;lt;class, class&amp;gt; struct X 对待
    template &amp;lt;class U1, class U2&amp;gt;
    friend class
        X;  // X被当友元模板类的标识符，则X被当做模板 template&amp;lt;class, class&amp;gt; struct X 对待，即::X
            // 此处含义为，当模板X实例化后，X&amp;lt;T1,T2&amp;gt;拥有友元X&amp;lt;U1,U2&amp;gt;，X&amp;lt;U1,U2&amp;gt;可以被实例化为多个类型
    X* q;  // 此处为以上三种情形之外，X被当做了一个实例化的类型名，其类型为X&amp;lt;T1,T2&amp;gt;。
           // 当X&amp;lt;T1,T2&amp;gt;被实例为X&amp;lt;int,int&amp;gt;时，q的类型为`X&amp;lt;int,int&amp;gt; *`
           // 当X&amp;lt;T1,T2&amp;gt;被实例为X&amp;lt;double,double&amp;gt;时，q的类型为`X&amp;lt;double,double&amp;gt; *`
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ADL实参依赖查找&lt;/h2&gt;
&lt;p&gt;ADL主要适用于对&lt;strong&gt;unqualified-name&lt;/strong&gt;的&lt;strong&gt;函数&lt;/strong&gt;进行的名字查找。查找对象是一个在进行函数调用或者运算符调用的&lt;strong&gt;非成员函数&lt;/strong&gt;。换句话说，ADL依据函数调用中的实参的数据类型查找未限定（unqualified）的函数名（或者函数模板名）。&lt;/p&gt;
&lt;h4&gt;哪些不触发ADL&lt;/h4&gt;
&lt;p&gt;如果通常的未限定（unqualified）名字查找所产生的候选集包括下述情形，则不会启动依赖于实参的名字查找（ADL查找不会进行）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1.类成员声明（此种情形仅指普通的类成员函数，不指类成员运算符函数）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace xx {
    struct XA {};
    void foo(XA &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;xx::foo&quot; &amp;lt;&amp;lt; std::endl; }
};  // namespace xx

namespace yy {
    void test() {
        xx::XA xa;
        foo(xa);  // 在这里，可以通过ADL来找到xx::foo
    }
    struct YA {
        int foo;
        static void test() {
            xx::XA xa;
            foo(xa);  // 在这里，由于通过unqualified name lookup找到了
                      // 成员变量int foo，所以不会进行ADL，因此会出错
        }
    };
}  // namespace yy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;2.块作用域内的&lt;strong&gt;函数&lt;/strong&gt;的声明，&lt;strong&gt;不含(using-declaration)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

namespace xx {
struct XA {};
    void foo(XA &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;xx::foo&quot; &amp;lt;&amp;lt; std::endl; }
};  // namespace xx

namespace zz {
    void foo(int a) { std::cout &amp;lt;&amp;lt; &quot;zz::foo&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace zz

namespace yy {
    void foo(int a);  // 1
    void test0() {
        xx::XA xa;
        foo(xa);  // 此处正常进行ADL，不受到外部声明1的影响 因为1不在块作用域内
    }
    void test1() {
        using zz::foo;  // 2
        xx::XA xa;
        foo(xa);  // 此处正常进行ADL，不受声明2的影响 因为2使用了using declaration
    }
    void test2() {
        void foo(int);  // 3
        xx::XA xa;
        foo(xa);  // ERROR 此处由于该块作用域内中有不含using的声明，则不会进行ADL，所以会报错
    }
}  // namespace yy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;3.&lt;strong&gt;任何不是函数或者函数模板的声明（例如函数对象或者另一个变量其名字与被查询的函数名字冲突）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意，这个是CPO的要点。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;namespace xx {
    struct XA {};
    void foo(XA &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;xx::foo&quot; &amp;lt;&amp;lt; std::endl; }
};  // namespace xx

namespace zz {
    struct foo_callable {
        void operator()(xx::XA) const {
            std::cout &amp;lt;&amp;lt; &quot;zz::foo functor called for xx::XA&quot; &amp;lt;&amp;lt; std::endl;
        }
    };

    foo_callable foo{}; // 全局可用的CPO实例
}

namespace yy {
    void foo(int a);
    void test0() {
        xx::XA xa;
        foo(xa);  // 　一切正常
    }
    void test1() {
        using zz::foo;  // 由于该处声明，引入的是一个函数对象，则1处不会进行ADL，而是直接调用zz::foo
        // 去掉这个这正常调用xx::foo
        xx::XA xa;
        foo(xa);  // 1
    }
}  // namespace yy

int main() {
    yy::test1();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;4.当调用的函数被括号包围时&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace xx {
    struct XA {};
    void foo(XA &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;xx::foo&quot; &amp;lt;&amp;lt; std::endl; }
};  // namespace xx
namespace yy {
    void test0() {
        xx::XA xa;
        foo(xa);  // OK，此处进行ADL
        (foo)(xa);  // ERROR，由于foo被括号包围，故不进行ADL，这条规则在文档中没有提及，
                    // 不知道应该被归属于哪一条，所以单独拿出来。
    }
}  // namespace yy
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;ADL的查找范围&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;函数调用表达式&lt;/strong&gt;的每个实参的&lt;strong&gt;类型&lt;/strong&gt;用于&lt;strong&gt;确定&lt;/strong&gt;命名空间与类的相关集合（associated set of namespaces and classes）并用于函数名字查找（这句话的意思简而言之就是&lt;strong&gt;ADL查找的集合范围如何确定&lt;/strong&gt;）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1.基本类型（fundamental type）实参的命名空间与类的相关集合为空。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个的意思是，当参数的类型是基本类型时（例如 &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;char&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt; 等），ADL 的处理方式如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;基本类型的命名空间和类集合为空&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;当 ADL 处理一个基本类型（如 &lt;code&gt;int&lt;/code&gt;）的参数时，它不会将任何命名空间或类添加到与该类型相关联的查找集合中。基本类型不属于任何用户定义的命名空间或类，因此它们的关联命名空间和类集合为空。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADL 对基本类型不起作用&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;由于基本类型没有相关的命名空间或类，因此 ADL 不会基于这些类型查找到任何附加的函数定义。换句话说，当函数参数是基本类型时，ADL 不会帮助找到在某个特定命名空间中定义的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;如果参数的命名空间为空，将其添加到查找集合内，但是查找时，会直接跳过空集。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;2.类类型（class type，指struct，class，union类型），相关集合包括&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;类类型自身；&lt;/li&gt;
&lt;li&gt;该类型的所有的直接或间接基类；&lt;/li&gt;
&lt;li&gt;如果类类型 T 是另一个类 G 的成员（嵌套类型），则那个包含了类类型 T 的类 G；&lt;/li&gt;
&lt;li&gt;该类类型的所有相关类的最内层外围命名空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace ADL {
    struct A;
    struct Base {
        friend void func2(const A &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;2.2&quot; &amp;lt;&amp;lt; std::endl; }
    };
    struct A : public Base {
        friend void func1(A a) { std::cout &amp;lt;&amp;lt; &quot;2.1&quot; &amp;lt;&amp;lt; std::endl; }
        struct B {};
        friend void func3(B b) { std::cout &amp;lt;&amp;lt; &quot;2.3&quot; &amp;lt;&amp;lt; std::endl; }
    };
    void func4(A a) { std::cout &amp;lt;&amp;lt; &quot;2.4&quot; &amp;lt;&amp;lt; std::endl; }
};  // namespace ADL

int main() {
    ADL::A a;
    ADL::A::B b;
    func1(a);  // 2.1
    func2(a);  // 2.2
    func3(b);  // 2.3
    func4(a);  // 2.4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;3.如果实参是&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/class_template&quot;&gt;类模板&lt;/a&gt;特化后得到的类型，在上述规则外，还检验下列规则，并添加其关联类与命名空间到集合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;类型模板形参（type template parameter）所对应的&lt;strong&gt;模板实参&lt;/strong&gt;的类型，不包括非类型的模板形参、模板模板形参；&lt;/li&gt;
&lt;li&gt;模板模板实参（template template argument）所在的命名空间；&lt;/li&gt;
&lt;li&gt;模板模板实参所在的类（如果这个类包含了这个成员模板）。&lt;/li&gt;
&lt;li&gt;对于枚举类型的实参，添加枚举类型所在的命名空间到集合。如果枚举类型是一个类的成员类型，则添加该类到集合。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

namespace xx {
    enum XType {
        XTypeA,
        XTypeB,
        XTypeC,
    };

    void foo(XType x) { std::cout &amp;lt;&amp;lt; &quot;foo 4&quot; &amp;lt;&amp;lt; std::endl; }

    struct A {
        enum AType {
            ATypeA,
            ATypeB,
        };
        friend void foo1(AType x) { std::cout &amp;lt;&amp;lt; &quot;foo 41&quot; &amp;lt;&amp;lt; std::endl; }
    };
}  // namespace xx

namespace yy {
    void test0() { foo(xx::XTypeA); }      // ADL
    void test1() { foo1(xx::A::ATypeA); }  // ADL
}  // namespace yy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;5.如果实参是类型 T 的指针或者是类型 T 的数组的指针，则检验类型 T 并添加其类与命名空间的关联集到集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

namespace xx {
    struct A {
        friend void foo1(A* x) { std::cout &amp;lt;&amp;lt; &quot;foo 6.1.1&quot; &amp;lt;&amp;lt; std::endl; }
        friend void foo2(A x[]) { std::cout &amp;lt;&amp;lt; &quot;foo 6.1.2&quot; &amp;lt;&amp;lt; std::endl; }
    };

    void foo3(A* a) { std::cout &amp;lt;&amp;lt; &quot;foo 6.2.1&quot; &amp;lt;&amp;lt; std::endl; }
    void foo4(A x[]) { std::cout &amp;lt;&amp;lt; &quot;foo 6.2.2&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace xx

namespace yy {
    void test0() {
        xx::A* a;
        xx::A aa[2];
        foo1(a);   // ADL
        foo2(aa);  // ADL
        foo3(a);   // ADL
        foo4(aa);  // ADL
    }
}  // namespace yy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;6.如果实参是函数类型，那么检验函数参数类型与函数返回值类型，并添加其类与命名空间的关联集到集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace xx {
    struct A {};
    
    void func1(A&amp;amp; a) {}
    
    A func2() { return {}; }
    
    void lookup1(void (*f)(A&amp;amp;)) { std::cout &amp;lt;&amp;lt; &quot;xx:lookup1&quot; &amp;lt;&amp;lt; std::endl; }
    
    void lookup2(A (*f)()) { std::cout &amp;lt;&amp;lt; &quot;xx:lookup2&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace xx

namespace yy {
    void test0() {
        lookup1(xx::func1);  // ok
        lookup2(xx::func2);  // ok
    }
}  // namespace yy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;7.如果实参是类 X 的成员函数 F 的指针类型参数，那么该成员函数的形参类型、该成员函数返回值的类型、该成员函数所属类 X 的相关集合都被加入到关联集到集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

namespace x {
    struct B;
}　// namespace x

namespace xx {
    struct A {
        x::B* f() { return nullptr; }
        void ff(x::B&amp;amp;) {}
    };
}  // namespace xx

namespace x {
    struct B {
        friend void lookup1(B* (xx::A::*f)()) { std::cout &amp;lt;&amp;lt; &quot;xx::lookup1&quot; &amp;lt;&amp;lt; std::endl; }
    };
    template &amp;lt;typename T&amp;gt;
    void lookup2(T t) {
        std::cout &amp;lt;&amp;lt; &quot;xx::lookup2&quot; &amp;lt;&amp;lt; std::endl;
    }
}  // namespace x

namespace yy {
    void test0() {
        lookup1(&amp;amp;xx::A::f);  // ok
        lookup1(
            nullptr);  // failed，因为这条规则标明的是实参，此处实参是nullptr，则不会查找到对应的lookup1
        lookup2(&amp;amp;xx::A::ff);  // ok
    }
}  // namespace yy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;8.如果实参是类 X 的数据成员 T 的指针类型参数，那么该成员类型、该数据成员所属类 X 的相关集合都被加入到关联集到集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace xx {
    struct A {
        int num = 0;
    };
    void lookup1(int A::*) { std::cout &amp;lt;&amp;lt; &quot;x::lookup1&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace xx

namespace yy {
    void test0() { lookup1(&amp;amp;xx::A::num); }  // ok
}  // namespace yy

int main() { yy::test0(); }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;9.若参数是&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/overloaded_address&quot;&gt;重载函数集的取址表达式&lt;/a&gt;（或对函数模板）的名称，则检验重载集中的每个元素，并添加其类与命名空间的关联集到集合。 1. 另外，若重载集为模板 id （带模板实参的模板名），则检验其所有类型模板实参与模板模板实参（但不含非类型模板实参），并添加其类与命名空间的关联集到集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

namespace xx {
    struct A {
        int num = 0;
    };
    void lookup1(void (*)()) { std::cout &amp;lt;&amp;lt; &quot;x::lookup1&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace xx

namespace yy {
    void f() {};                   // ①
    void f(xx::A*) {};             // ②
    void test0() { lookup1(&amp;amp;f); }  // ok，由于重载f②的存在，namespace xx也被加入到ADL的集合中
}  // namespace yy

int main() { yy::test0(); }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;10.如果相关集合中的任何命名空间是&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/namespace&quot;&gt;内联命名空间&lt;/a&gt;（inline namespace）, 则添加其外围命名空间到关联集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace adl {
    inline namespace inner {
        struct A {};
    }  // namespace inner

    void func(const inner::A &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;enclosed namespace added.&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace adl

int main() {
    adl::inner::A a;
    func(a);  // 因为 adl::inner 为 inline namespace，则将其最内存外围 namespace adl
              // 到查找关联集合中，则可以查找到 adl::func
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;11.如果相关集合中的一个命名空间直接包含了内联命名空间，则内联命名空间被增加到相关集合中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace adl {
    struct A {};
    inline namespace inner {
        void func(const A &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;inline namespace added.&quot; &amp;lt;&amp;lt; std::endl; }
    }  // namespace inner
}  // namespace adl

int main() {
    adl::A a;
    func(a);  // namespace adl中包含 inline namespace adl::inner，
              // 则将 adl::inner 添加到查找关联集合中，则可以查找到 adl::inner::func
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;12.在确定命名空间与类的关联集后，为了进一步的 ADL 处理，&lt;strong&gt;忽略&lt;/strong&gt;此集中所有于类中找到的&lt;strong&gt;声明&lt;/strong&gt;，除了命名空间作用域的友元函数及函数模板，陈述于后述点2。以下列特殊规则，合并普通&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/lookup&quot;&gt;无限定查找&lt;/a&gt;找到的声明集合，与在 ADL 所生成关联集的所有元素中找到的声明集合:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;忽略关联命名空间中的 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/namespace#using_.E6.8C.87.E4.BB.A4&quot;&gt;using 指令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;声明于关联类中的命名空间作用域友元函数（及函数模板）通过 ADL 可见，即使它们通过普通查找不可见。&lt;/li&gt;
&lt;li&gt;忽略函数与函数模板外的所有名称（与变量不冲突）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
namespace xx {
    struct A;
}// namespace xx

namespace x {
    void f(xx::A *) { std::cout &amp;lt;&amp;lt; &quot;x::f&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace x

namespace xx {
    using namespace x;  // 1
    struct A {};
}  // namespace xx

namespace yy {
    void test0() {
        xx::A *a;
        f(a);  // not ok, ADL时忽略1处using指令
    }
}  // namespace yy

int main() { yy::test0(); }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;通过 ADL 查找到的&lt;code&gt;name&lt;/code&gt;会和前面&lt;code&gt;unqualified name&lt;/code&gt;普通查找到的合并到一起进行选择，如果存在歧义，会报错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ADL的核心是: &lt;strong&gt;在调用&lt;em&gt;函数&lt;/em&gt;的时候，只要有一个参数的类型属于函数所在的命名空间，那么调用的时候就不用加命名空间前缀&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace A
{
    struct Empty {};
    void foo(int);
    void bar(Empty, int);
}
void func()
{
    A::foo(2); // 必须这么调用，foo(2)找不到A里面的foo
    foo(A::Empty{}, 1); // 因为第一参数的类型在A中，foo前面不用再加前缀
    std::cout &amp;lt;&amp;lt; 1;
    // 等价于operator&amp;lt;&amp;lt; (std::cout, 1)
    // 由于ADL，可以找到std中的std::operator&amp;lt;&amp;lt; (std::ostream&amp;amp;, int)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CPO 定制点对象&lt;/h2&gt;
&lt;p&gt;我们在ADL中的不触发ADL的第三点中提到了CPO的关键要素。CPO本质上，就是一个&lt;strong&gt;能够正确解决复杂定制问题的functor对象&lt;/strong&gt;。为了解决这个问题，我们需要屏蔽掉ADL调用。也就是说，我们把ADL的**两步式（先using引入，再非限定调用）**放到了定制点对象内部进行。而在外部我们并不使用ADL。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它能够在编译期对于简单的无限定调用，根据实参的不同情况进行判断，并转发到正确的实现；&lt;/li&gt;
&lt;li&gt;它能够检查实参与ADL重载（若存在）是否符合语法要求，并在需要的时候报错。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct special{};
namespace mystd{
    template&amp;lt;typename T&amp;gt;
    void foo(T t) {
        std::cout &amp;lt;&amp;lt; &quot;foo(T)&quot; &amp;lt;&amp;lt; std::endl;
    }
}

namespace yb {
    struct myclass {};
    void foo(myclass s) { std::cout &amp;lt;&amp;lt; &quot;yb::foo&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace yb

namespace xx {
    struct XA {};
    void foo(XA &amp;amp;a) { std::cout &amp;lt;&amp;lt; &quot;xx::foo&quot; &amp;lt;&amp;lt; std::endl; }
}  // namespace xx

namespace yy {
    void foo(int a) { std::cout &amp;lt;&amp;lt; &quot;yy::foo&quot; &amp;lt;&amp;lt; std::endl; }
    void test0() {
        xx::XA xa;
        foo(xa);  // 　一切正常
    }

    namespace zz {
        struct foo_callable {
            template &amp;lt;typename... Args&amp;gt;
            void operator()(Args &amp;amp;&amp;amp;...args) const {
                // using xx::foo;
                // using yb::foo;
                // using yy::foo;
                // 不用任何的using。因为我们传入的参数的namespace里都有一个叫foo的。所以正常ADL会找到的。
                using mystd::foo; // 这个using是模拟swap例子里的using。
                // 使用这个using的原因是mystd里面没有special这个成员。所以ADL也找不到。
                std::cout &amp;lt;&amp;lt; &quot;zz::foo&quot; &amp;lt;&amp;lt; std::endl;
                foo(std::forward&amp;lt;Args&amp;gt;(args)...);  // 正常ADL
            }
        };

        foo_callable foo{};  // 全局可用的CPO实例
    }  // namespace zz

    void test1() {
        using zz::foo;  // 由于该处声明，引入的是一个函数对象，则1处不会进行ADL，而是直接调用zz::foo
        xx::XA xa;
        yb::myclass s;
        foo(xa);  // 1
        foo(s);   // 2
        foo(10);  // 3
        foo(special{}); // 4
    }
}  // namespace yy

int main() {
    yy::test1();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考资料&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/518132411/answer/2360830245&quot;&gt;定制点对象&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://xyfu.me/posts/8f07748/&quot;&gt;标识符、名字查找与实参依赖查找(ADL)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://lrita.github.io/2018/09/29/name-lookup-in-cpp11/#qualified-name-lookup&quot;&gt;C++11中的name lookup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zclll.com/index.php/cpp/cpo_niebloid.html&quot;&gt;定制点对象和ADL&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://quuxplusone.github.io/blog/2021/12/07/namespace-cpo/&quot;&gt;O&apos;Dwyer的介绍&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;整理C++的三种名称查找方式&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;查找类型&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;触发形式&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;搜索范围&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;限定名称查找 Qualified&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a::b::x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅 &lt;code&gt;::&lt;/code&gt; 左侧指定作用域&lt;/td&gt;
&lt;td&gt;精确控制符号来源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非限定名称查找 Unqualified&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;由内向外逐层作用域&lt;/td&gt;
&lt;td&gt;局部变量、类成员、全局量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数依赖查找 (ADL)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func(arg)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;常规作用域 + &lt;strong&gt;参数类型命名空间&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;函数调用（尤其运算符）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;来自&lt;a href=&quot;https://www.sandordargo.com/blog/2025/05/28/three-ways-of-name-lookups&quot;&gt;参考&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;不要重新打开std空间并且往里塞东西&lt;/h1&gt;
&lt;p&gt;比如如何正确的特化&lt;code&gt;std::hash&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;不要这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Widget {};

namespace std {  // Danger!
    template&amp;lt;&amp;gt;
    struct hash&amp;lt;Widget&amp;gt; {
        size_t operator()(const Widget&amp;amp;) const;
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Widget {};

template&amp;lt;&amp;gt;
struct std::hash&amp;lt;Widget&amp;gt; {
    size_t operator()(const Widget&amp;amp;) const;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考&lt;a href=&quot;https://quuxplusone.github.io/blog/2021/10/27/dont-reopen-namespace-std/&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;翻译单元&lt;/h1&gt;
&lt;p&gt;在C++中，翻译单元指的是编译器处理的最小独立单元，通常对应一个源文件（&lt;code&gt;.cpp&lt;/code&gt;或&lt;code&gt;.c&lt;/code&gt;文件）以&lt;strong&gt;及其包含的所有头文件&lt;/strong&gt;。具体来说，一个翻译单元包括以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源代码文件自身的内容&lt;/li&gt;
&lt;li&gt;源代码文件中&lt;code&gt;#include&lt;/code&gt;的所有头文件（Header Files）的内容，这些头文件在预处理阶段会被展开替换到源文件中&lt;/li&gt;
&lt;li&gt;宏定义（Macro Definitions）以及其他预处理器指令产生的文本替换&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在编译过程中，每个源代码文件都会形成一个独立的翻译单元，编译器对每个翻译单元独立进行词法分析、语法分析、语义分析等编译步骤，最后生成对应的&lt;code&gt;.o&lt;/code&gt;（对象文件）或&lt;code&gt;.obj&lt;/code&gt;（在Windows系统中）。链接器随后会把这些独立的翻译单元链接起来，形成可执行文件或库文件。&lt;/p&gt;
&lt;h1&gt;std::string 的不同编译器实现和比较&lt;/h1&gt;
&lt;p&gt;https://devblogs.microsoft.com/oldnewthing/20240510-00/?p=109742Y3MOtw0Q:1716181180316&amp;amp;q=cpp+weekly&amp;amp;spell=1&amp;amp;sa=X&amp;amp;ved=2ahUKEwiLy_2MuZuGAxXha_UHHet8BmoQBSgAegQICBAB&amp;amp;biw=1920&amp;amp;bih=919&amp;amp;dpr=1&lt;/p&gt;
&lt;h1&gt;TCMalloc&lt;/h1&gt;
&lt;p&gt;http://blog.gerryyang.com/linux%20performance/2022/08/07/tcmalloc-in-action.html&lt;/p&gt;
</content:encoded></item><item><title>C++杂记 - 5（二）：反射与动态链接的使用经验</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05-2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05-2/</guid><pubDate>Tue, 07 Mar 2023 06:56:00 GMT</pubDate><content:encoded>&lt;h1&gt;反射&lt;/h1&gt;
&lt;p&gt;反射的核心目的是根据一个类名来生成对应的实例。也就是**如何通过类名称字符串来生成类的对象。比如有一个类&lt;code&gt;ClassA&lt;/code&gt;，那么如何通过类名称字符串&lt;code&gt;ClassA&lt;/code&gt;创建类的对象呢？**比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person {
public:
   virtual void show() = 0;
}

class Allen : public Person {
  virtual void show() {
    std::cout &amp;lt;&amp;lt; &quot;Hello, I&apos;m Allen!&quot; &amp;lt;&amp;lt; std::endl;
  }
}
std::string className = /*从配置文件中读取*/
Person *p = getNewInstance&amp;lt;Person&amp;gt;(className);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是C++不像java或者C#一样提供这种机制。但是我们可以通过工厂模式+单例模式+映射器+宏来实现一个反射&lt;/p&gt;
&lt;p&gt;一般来说，实现反射需要如下几个组件（步骤）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们的业务代码，也就是需要被反射的类。这个类通常继承自一个反射基类如&lt;code&gt;ReflectObject&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;这个步骤的目的是为抽象对象工厂类&lt;code&gt;ObjectFactory&lt;/code&gt;提供统一的接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;一个抽象对象工厂类&lt;code&gt;ObjectFactory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;具体对象工厂类
&lt;ul&gt;
&lt;li&gt;一般情况下使用宏&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;反射器
&lt;ul&gt;
&lt;li&gt;通常情况下，反射器可以是单例。&lt;/li&gt;
&lt;li&gt;反射器的核心是建立字符串（标识符）到回调函数的映射。然后通过一个容器储存。&lt;/li&gt;
&lt;li&gt;在我们即将展示的例子中，这个回调函数是一个具体对象工厂的指针。然后我们会通过这个具体对象工厂来创建具体对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;下面我们看代码&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;反射基类&lt;code&gt;ReflectObject&lt;/code&gt;。所有实现反射的类都需要继承自它。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class ReflectObject { 
public:
	virtual ~ReflectObject(){}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;具体的被反射类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Person : public ReflectObject{
public:
	Person(){
        std::cout &amp;lt;&amp;lt; &quot;Person()&quot; &amp;lt;&amp;lt; std::endl;
    }
	virtual ~Person(){
        std::cout &amp;lt;&amp;lt; &quot;~Person()&quot; &amp;lt;&amp;lt; std::endl;
    }
	virtual void show(){
        std::cout &amp;lt;&amp;lt; &quot;Hello, I&apos;m person&quot; &amp;lt;&amp;lt; std::endl;
    }
};

class Miku : public Person{
public:
	Miku(){
        std::cout &amp;lt;&amp;lt; &quot;Miku()&quot; &amp;lt;&amp;lt; std::endl;
    }
	virtual ~Miku(){
        std::cout &amp;lt;&amp;lt; &quot;~Miku()&quot; &amp;lt;&amp;lt; std::endl;
    }
	virtual void show(){
        std::cout &amp;lt;&amp;lt; &quot;Hello, I&apos;m Miku&quot; &amp;lt;&amp;lt; std::endl;
    }
};

class Animal : public ReflectObject{
public:
	Animal(){
        std::cout &amp;lt;&amp;lt; &quot;Animal()&quot; &amp;lt;&amp;lt; std::endl;
    }
	virtual ~Animal(){
        std::cout &amp;lt;&amp;lt; &quot;~Animal()&quot; &amp;lt;&amp;lt; std::endl;
    }
	virtual void bark(){
        std::cout &amp;lt;&amp;lt; &quot;animal bark&quot; &amp;lt;&amp;lt; std::endl;
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;抽象对象工厂&lt;code&gt;ObjectFactory&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class ObjectFactory {
public:
	ObjectFactory(){ std::cout &amp;lt;&amp;lt; &quot;ObjectFactory()&quot; &amp;lt;&amp;lt; std::endl; }
	virtual ~ObjectFactory(){ std::cout &amp;lt;&amp;lt; &quot;~ObjectFactory()&quot; &amp;lt;&amp;lt; std::endl; }
	virtual ReflectObject* newInstance() = 0; //提供统一的纯虚函数，返回反射基类的指针。也就是说，所有的具体对象工厂类都要通过这个函数返回具体对象并被转化为反射基类的指针
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;反射器Reflector
&lt;ul&gt;
&lt;li&gt;此处我们以单例模式呈现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Reflector
{
public:
    static Reflector&amp;amp; reflector(); //mayer’s单例模式经典方法
    Reflector(const Reflector&amp;amp;) = delete; //禁止拷贝构造
    Reflector&amp;amp; operator= (const Reflector&amp;amp;) = delete; //禁止拷贝赋值
	void registerFactory(const std::string&amp;amp; className, ObjectFactory* function); //注册至映射map

	ReflectObject* getNewInstance(const std::string&amp;amp; className); //在映射器内查找类名对应的对象工厂指针。然后通过该指针调用该对象工厂对应的创建实例的函数。此处是newInstance
private:
    Reflector(); //私有构造
	~Reflector(); //私有析构
	std::map&amp;lt;std::string, ObjectFactory*&amp;gt; objectFactories; //映射map。key是类名，value是该类的对象工厂指针。
};
Reflector::Reflector(){
    std::cout &amp;lt;&amp;lt; &quot;Reflector constructor&quot; &amp;lt;&amp;lt; std::endl;
}
Reflector::~Reflector(){
    //析构函数中释放资源。在当前业务中，反射器的映射map负责保有所有的具体对象工厂指针。所以需要负责释放。
	std::map&amp;lt;std::string, ObjectFactory*&amp;gt;::iterator it = objectFactories.begin();
	for (; it != objectFactories.end();++it){
		delete it-&amp;gt;second;
	}
	objectFactories.clear();
}

void Reflector::registerFactory(const std::string&amp;amp; className, ObjectFactory *of){
    //注册函数。首先在map中查找是否有对应的键值，如果没有则创建一个 名称-对象工厂指针 的映射
	std::map&amp;lt;std::string, ObjectFactory*&amp;gt;::iterator it = objectFactories.find(className);
	if (it != objectFactories.end()) {
		std::cout &amp;lt;&amp;lt; &quot;该类已经存在&quot; &amp;lt;&amp;lt; std::endl;
	}
	else {
		objectFactories[className] = of;
	}
}
ReflectObject* Reflector::getNewInstance(const std::string&amp;amp; className){
    //通过类名获取对象工厂的指针，然后调用对象工厂的创建具体实例的方法并返回。
	std::map&amp;lt;std::string, ObjectFactory*&amp;gt;::iterator it = objectFactories.find(className);
	if (it != objectFactories.end()) {
		ObjectFactory *of = it-&amp;gt;second; 
		return of-&amp;gt;newInstance(); //如果找到了。那么调用这个对象工厂指针的创建具体实例的方法。
	}
	return NULL;
}
Reflector&amp;amp; Reflector::reflector() {
    //经典的mayer‘s单例模式
    std::cout &amp;lt;&amp;lt;&quot;get reflector reference &quot; &amp;lt;&amp;lt; std::endl;
    static Reflector reflector;
    return reflector;
}

/*
这是个非常特殊的地方。我们有一个全局函数模板。外部调用的是这个函数。
这个函数是我们的反射器的包装。传入一个具体被映射类名字，同时将其做为模板参数。
该函数内部会先获取单例的反射器的实例，然后通过该实例调用内部的getNewInstance成员函数。
由于调用返回的是ReflectObject*指针，但是通常我们等号左侧调用的时候会是当前类型或者是实际父类，所以需要强转一下。
*/
template&amp;lt;typename T, typename... Args&amp;gt;
T* getNewInstance(const std::string&amp;amp; className) {
	return static_cast&amp;lt;T*&amp;gt;(Reflector::reflector().getNewInstance(className));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;具体对象工厂&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class ObjectFactory_Miku: public ObjectFactory{ //继承自反射基类
    public:
    	ObjectFactory_Miku(){
            std::cout &amp;lt;&amp;lt;&quot;ObjectFactory_Miku() &quot; &amp;lt;&amp;lt; std::endl;
        }
        ReflectObject* newInstance(){ //实现创建实例的接口。
            return new Miku();
        };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;主函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    Reflector::reflector().registerFactory(&quot;Miku&quot;, new ObjectFactory_Miku());
	Person *allen = getNewInstance&amp;lt;Miku&amp;gt;(&quot;Miku&quot;);
	allen-&amp;gt;show();
	delete allen;
	return 0;
}
/*
get reflector reference  获取反射器实例
Reflector constructor 反射器创建且只创建一次
ObjectFactory() new的对象工厂类的父类部分
ObjectFactory_Miku() new的对象工厂类子类部分
---以上是第一行的注册部分------
get reflector reference  第二行的getnewinstance内部调用的获取反射器实例。
Person() 反射器实例调用的getnewinstance找到的对象工厂指针调用的newInstance导致的Miku类的父类部分创建
Miku() //miku类本类创建
Hello, I&apos;m Miku //show 业务代码
~Miku()
~Person()
~ObjectFactory()
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候我们发现，写对象工厂类和注册比较麻烦，我们可以用一个宏来实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define REFLECT(name)\
class ObjectFactory_##name : public ObjectFactory{\
public:\
	ObjectFactory_##name(){ std::cout &amp;lt;&amp;lt; &quot;ObjectFactory_&quot; &amp;lt;&amp;lt; #name &amp;lt;&amp;lt; &quot;()&quot; &amp;lt;&amp;lt; std::endl; }\
	virtual ~ObjectFactory_##name(){ std::cout &amp;lt;&amp;lt; &quot;~ObjectFactory_&quot; &amp;lt;&amp;lt; #name &amp;lt;&amp;lt; &quot;()&quot; &amp;lt;&amp;lt; std::endl; }\
	ReflectObject* newInstance() {\
		return new name(); \
	}\
}; \
Reflector::reflector().registerFactory(#name, new ObjectFactory_##name());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;宏的部分语法我们在杂记4中提到过，这里不赘述&lt;/p&gt;
&lt;p&gt;这样我们可以直接&lt;code&gt;REFLECT(Animal)&lt;/code&gt;就完成了创建对象工厂和注册&lt;/p&gt;
&lt;p&gt;也可以考虑使用一个额外的类和全局对象的方式替换最后一行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Register_##name{\
public:\
	Register_##name(){\
		Reflector::reflector().registerFactory(#name, new ObjectFactory_##name()); \
	}\
};\
Register_##name register_##name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我目前不清楚有什么好处，可能和链接有关？因为全局对象的构造函数在&lt;code&gt;main&lt;/code&gt;前执行。&lt;/p&gt;
&lt;p&gt;上面的代码我们梳理了最基本的反射原理和实现，参考自&lt;a href=&quot;https://blog.csdn.net/q1007729991/article/details/56012253&quot;&gt;这里&lt;/a&gt;。现在有一个问题。如果我的类需要使用有参构造函数怎么办？可以使用可变模板参数。但是导致复杂非常多。我找到了Nebula的一种实现方式，参考自&lt;a href=&quot;https://zhuanlan.zhihu.com/p/40134288&quot;&gt;这里&lt;/a&gt;，我们一起分析一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;typeinfo&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;cxxabi.h&amp;gt;

namespace neb{
    class Actor{ //*理解为反射基类 reflectobject
    public:
        Actor(){std::cout &amp;lt;&amp;lt; &quot;Actor construct&quot; &amp;lt;&amp;lt; std::endl;}
        virtual ~Actor(){};
        virtual void Say()
        {
            std::cout &amp;lt;&amp;lt; &quot;Actor&quot; &amp;lt;&amp;lt; std::endl;
        }
    };

    template&amp;lt;typename ...Targs&amp;gt;
    class ActorFactory{  //*理解为反射器类。此处是单例模式
    public:
        //typedef Actor* (*ActorCreateFunction)();
        //std::function&amp;lt; Actor*(Targs...args) &amp;gt; pp;

        static ActorFactory* Instance(){  //! 线程不安全的单例模式。获取反射器指针。
            std::cout &amp;lt;&amp;lt; &quot;static ActorFactory* Instance()&quot; &amp;lt;&amp;lt; std::endl;
            if (nullptr == m_pActorFactory){
                m_pActorFactory = new ActorFactory();
            }
            return(m_pActorFactory);
        }

        virtual ~ActorFactory(){};

        //Lambda: static std::string ReadTypeName(const char * name)

        //bool Regist(const std::string&amp;amp; strTypeName, ActorCreateFunction pFunc)
        //bool Regist(const std::string&amp;amp; strTypeName, std::function&amp;lt;Actor*()&amp;gt; pFunc)
        /*
        
        &amp;amp;将“实例创建方法（对象工厂的CreateObject方法）”注册到ActorFactory，注册的同时赋予这个方法一个名字“类名”，后续可以通过“类名”获得该类的“实例创建方法”。
        &amp;amp;这个实例创建方法实质上是个函数指针，在C++11里std::function的可读性比函数指针更好，所以用了std::function。
        */
        bool Regist(const std::string&amp;amp; strTypeName, std::function&amp;lt;Actor*(Targs&amp;amp;&amp;amp;... args)&amp;gt; pFunc){
            std::cout &amp;lt;&amp;lt; &quot;bool ActorFactory::Regist(const std::string&amp;amp; strTypeName, std::function&amp;lt;Actor*(Targs... args)&amp;gt; pFunc)&quot; &amp;lt;&amp;lt; std::endl;
            if (nullptr == pFunc){
                return(false);
            }
            std::string strRealTypeName = strTypeName;
            //[&amp;amp;strTypeName, &amp;amp;strRealTypeName]{int iPos = strTypeName.rfind(&apos; &apos;); strRealTypeName = std::move(strTypeName.substr(iPos+1, strTypeName.length() - (iPos + 1)));};

            bool bReg = m_mapCreateFunction.insert(std::make_pair(strRealTypeName, pFunc)).second; // 创建键值对
            std::cout &amp;lt;&amp;lt; &quot;m_mapCreateFunction.size() =&quot; &amp;lt;&amp;lt; m_mapCreateFunction.size() &amp;lt;&amp;lt; std::endl;
            return(bReg);
        }

        //&amp;amp; 传入“类名”和参数创建类实例，方法内部通过“类名”从m_mapCreateFunction获得了对应的“实例创建方法（DynamicCreator的CreateObject方法）”完成实例创建操作。
        Actor* Create(const std::string&amp;amp; strTypeName, Targs&amp;amp;&amp;amp;... args){
            std::cout &amp;lt;&amp;lt; &quot;Actor* ActorFactory::Create(const std::string&amp;amp; strTypeName, Targs... args)&quot; &amp;lt;&amp;lt; std::endl;
            auto iter = m_mapCreateFunction.find(strTypeName);
            if (iter == m_mapCreateFunction.end()){
                return(nullptr);
            }
            else{
                //return(iter-&amp;gt;second());
                return(iter-&amp;gt;second(std::forward&amp;lt;Targs&amp;gt;(args)...)); //把参数完美转发至实例创建方法。
            }
        }

    private:
        ActorFactory(){std::cout &amp;lt;&amp;lt; &quot;ActorFactory construct&quot; &amp;lt;&amp;lt; std::endl;}; //私有构造，经典的单例模式
        static ActorFactory&amp;lt;Targs...&amp;gt;* m_pActorFactory;   //静态的单例的本类指针
        std::unordered_map&amp;lt;std::string, std::function&amp;lt;Actor*(Targs&amp;amp;&amp;amp;...)&amp;gt; &amp;gt; m_mapCreateFunction; //存贮映射的数据结构。第二个std::function对象统一存储返回类型是Actor*，入参可变的函数。
    };

    template&amp;lt;typename ...Targs&amp;gt;
    ActorFactory&amp;lt;Targs...&amp;gt;* ActorFactory&amp;lt;Targs...&amp;gt;::m_pActorFactory = nullptr; //静态的单例的本类指针的类外定义并初始化

    template&amp;lt;typename T, typename ...Targs&amp;gt;
    class DynamicCreator{  //*理解为对象工厂
    /*
    我们写自己的业务类的时候，不仅要继承自反射基类，也要继承自这个类。
    继承自这个类的第一个参数就是我们的类名。会通过mangled方法获取到这个字符串
    然后把这个字符串和这个类的CreateObject函数放进去
    */
    public:
        struct Register{
            Register(){
                std::cout &amp;lt;&amp;lt; &quot;DynamicCreator.Register construct&quot; &amp;lt;&amp;lt; std::endl;
                std::puts(__PRETTY_FUNCTION__);
                char* szDemangleName = nullptr;
                std::string strTypeName; //这个就是类名
                #ifdef __GNUC__
                    szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
                #else
                //in this format?:     szDemangleName =  typeid(T).name();
                    szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
                #endif
                if (nullptr != szDemangleName){
                    strTypeName = szDemangleName;
                    free(szDemangleName);
                }
                std::cout &amp;lt;&amp;lt; strTypeName &amp;lt;&amp;lt; std::endl;
                ActorFactory&amp;lt;Targs...&amp;gt;::Instance()-&amp;gt;Regist(strTypeName, CreateObject); //通过反射器类实例把类名和创建函数放进去。
            }
            inline void do_nothing()const { }; //!注意这个do_nothing。
        };
        DynamicCreator(){
            std::puts(__PRETTY_FUNCTION__);
            std::cout &amp;lt;&amp;lt; &quot;DynamicCreator construct&quot; &amp;lt;&amp;lt; std::endl;
            m_oRegister.do_nothing(); //!这里的目的是使用一次我们嵌套类的static对象。这样的话编译器不会优化掉嵌套类静态对象的构造。尤其是分离编译的时候，可能会发生链接错误。
        }
        virtual ~DynamicCreator(){m_oRegister.do_nothing();}; //!这里我就不知道为什么了

        static T* CreateObject(Targs&amp;amp;&amp;amp;... args){ //这个实例创建函数很有意思。别看它返回的是T*，但是由于我们所有的业务代码具体类都需要继承自反射基类。所以说T*是子类指针，而反射器存储的是Actor*父类指针。很自然的多态存储。
            std::cout &amp;lt;&amp;lt; &quot;static Actor* DynamicCreator::CreateObject(Targs... args)&quot; &amp;lt;&amp;lt; std::endl;
            return new T(std::forward&amp;lt;Targs&amp;gt;(args)...); //一旦调用这个函数，就执行这个new。把传进来的参数再次完美转发至对象类的构造函数。
        }

        virtual void Say(){
            std::cout &amp;lt;&amp;lt; &quot;DynamicCreator say&quot; &amp;lt;&amp;lt; std::endl;
        }
        static Register m_oRegister; //这里必须要是静态的。注意这是静态成员变量，不是静态局部变量。因为我们把注册的过程放到了Register的构造函数中。细节下面再说。
    };

    template&amp;lt;typename T, typename ...Targs&amp;gt;
    typename DynamicCreator&amp;lt;T, Targs...&amp;gt;::Register DynamicCreator&amp;lt;T, Targs...&amp;gt;::m_oRegister; //成员static变量的类外定义并默认初始化。这一行非常关键。注意这是静态成员变量，不是静态局部变量。


    //下面开始是业务类。第一个是继承自反射基类，然后继承自对象工厂类。对象工厂类第一个模板参数是本类类型，然后依次是构造函数参数类型。
    class Cmd: public Actor, public DynamicCreator&amp;lt;Cmd&amp;gt;{
    public:
        Cmd(){std::cout &amp;lt;&amp;lt; &quot;Create Cmd &quot; &amp;lt;&amp;lt; std::endl;}
        virtual void Say(){
            std::cout &amp;lt;&amp;lt; &quot;I am Cmd&quot; &amp;lt;&amp;lt; std::endl;
        }
    };

    class Step: public Actor, DynamicCreator&amp;lt;Step, std::string, int&amp;gt;{
    public:
        Step(const std::string&amp;amp; strType, int iSeq){std::cout &amp;lt;&amp;lt; &quot;Create Step &quot; &amp;lt;&amp;lt; strType &amp;lt;&amp;lt; &quot; with seq &quot; &amp;lt;&amp;lt; iSeq &amp;lt;&amp;lt; std::endl;}
        virtual void Say(){
            std::cout &amp;lt;&amp;lt; &quot;I am Step&quot; &amp;lt;&amp;lt; std::endl;
        }
    };

    struct miku:public Actor, DynamicCreator&amp;lt;miku, int, int&amp;gt;{
        miku(int x, int y){
            std::cout &amp;lt;&amp;lt;&quot;miku&quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; y &amp;lt;&amp;lt; std::endl;
        }
    };

    class Worker{
    public:
        template&amp;lt;typename ...Targs&amp;gt;
        Actor* CreateActor(const std::string&amp;amp; strTypeName, Targs&amp;amp;&amp;amp;... args){
            Actor* p = ActorFactory&amp;lt;Targs...&amp;gt;::Instance()-&amp;gt;Create(strTypeName, std::forward&amp;lt;Targs&amp;gt;(args)...);
            return(p);
        }
    };

}

int main()
{
    neb::Worker W;
    std::cout &amp;lt;&amp;lt; &quot;----------------------------------------------------------------------&quot; &amp;lt;&amp;lt; std::endl;
    neb::Actor* p1 = W.CreateActor(std::string(&quot;neb::Cmd&quot;));
    p1-&amp;gt;Say();
    std::cout &amp;lt;&amp;lt; &quot;----------------------------------------------------------------------&quot; &amp;lt;&amp;lt; std::endl;
    neb::Actor* p2 = W.CreateActor(std::string(&quot;neb::Step&quot;), std::string(&quot;neb::Step&quot;), 1002);
    p2-&amp;gt;Say();
    std::cout &amp;lt;&amp;lt; &quot;----------------------------------------------------------------------&quot; &amp;lt;&amp;lt; std::endl;
    neb::Actor* p3 = W.CreateActor(std::string(&quot;neb::miku&quot;),2,3);
    p3-&amp;gt;Say();
    return(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一些具体的代码内容已经在注释说过了。我们一起看一下执行结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DynamicCreator.Register construct //静态成员变量类外定义导致的默认初始化。
neb::DynamicCreator&amp;lt;T, Targs&amp;gt;::Register::Register() [with T = neb::Cmd; Targs = {}] //为了好看打出来的
neb::Cmd //strTypeName也就是拿到的类名
static ActorFactory* Instance()//拿到ActorFactory的实例。因为ActorFactory也是单例模式。
ActorFactory construct //构造ActorFactory。因为不同的模板参数是不同的类。所以也是三次。
bool ActorFactory::Regist(const std::string&amp;amp; strTypeName, std::function&amp;lt;Actor*(Targs... args)&amp;gt; pFunc) //注册
m_mapCreateFunction.size() =1 //不知道干啥的。
    
    
DynamicCreator.Register construct
neb::DynamicCreator&amp;lt;T, Targs&amp;gt;::Register::Register() [with T = neb::Step; Targs = {std::__cxx11::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt;, int}]
neb::Step
static ActorFactory* Instance()
ActorFactory construct
bool ActorFactory::Regist(const std::string&amp;amp; strTypeName, std::function&amp;lt;Actor*(Targs... args)&amp;gt; pFunc)
m_mapCreateFunction.size() =1
    
    
DynamicCreator.Register construct
neb::DynamicCreator&amp;lt;T, Targs&amp;gt;::Register::Register() [with T = neb::miku; Targs = {int, int}]
neb::miku
static ActorFactory* Instance()
ActorFactory construct
bool ActorFactory::Regist(const std::string&amp;amp; strTypeName, std::function&amp;lt;Actor*(Targs... args)&amp;gt; pFunc)
m_mapCreateFunction.size() =1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这一部分是在执行main的第一行代码前执行的。为什么是这个顺序？&lt;/p&gt;
&lt;p&gt;首先，我们代码的这一行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename ...Targs&amp;gt;
    typename DynamicCreator&amp;lt;T, Targs...&amp;gt;::Register DynamicCreator&amp;lt;T, Targs...&amp;gt;::m_oRegister; //成员static变量的类外定义并且被默认初始化。这一行非常关键。注意这不是静态局部变量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非常重要。首先，业务代码有三个类模板。分别继承了不同模板参数的&lt;code&gt;DynamicCreator&lt;/code&gt;类模板。所以我们有三个不同模板参数的&lt;code&gt;DynamicCreator&lt;/code&gt;。所以三个不同的类都有自己的&lt;code&gt;m_oRegister&lt;/code&gt;。同时，&lt;code&gt;m_oRegister&lt;/code&gt;不是静态局部变量，是静态成员变量。生存周期不同。所以说，我们在类外部的这个对静态成员变量的定义导致了其被&lt;strong&gt;默认初始化&lt;/strong&gt;。所以我们看到有三个&lt;code&gt;DynamicCreator&lt;/code&gt;内部的&lt;code&gt;Register&lt;/code&gt;的默认构造函数被调用。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;Register&lt;/code&gt;的默认构造函数中，我们获得到了不同的&lt;code&gt;T&lt;/code&gt;。然后通过这个&lt;code&gt;T&lt;/code&gt;，获得到了特殊处理的类名。随后，由于&lt;code&gt;ActorFactory&lt;/code&gt;也是类模板，所以它也是三份。自然也有三份不同的&lt;code&gt;m_pActorFactory&lt;/code&gt;，所以也有三个&lt;code&gt;ActorFactory&lt;/code&gt;的构造函数被调用。然后我们拿到了&lt;code&gt;ActorFactory&lt;/code&gt;的实例，调用它的&lt;code&gt;Regist&lt;/code&gt;注册，传入类名和一个函数&lt;code&gt;CreateObject&lt;/code&gt;。这个函数很特殊，是一个静态的成员函数。别看它返回的是&lt;code&gt;T*&lt;/code&gt;，但是由于我们所有的业务代码具体类都需要继承自反射基类。所以说&lt;code&gt;T*&lt;/code&gt;是子类指针，而反射器存储的是一个返回值为&lt;code&gt;Actor*&lt;/code&gt;（父类指针）的&lt;code&gt;std::function&lt;/code&gt;对象。很自然的多态存储。这个函数一旦被调用，就会&lt;code&gt;new&lt;/code&gt;。把传进来的参数再次完美转发至对象类的构造函数。&lt;/p&gt;
&lt;p&gt;好了，下面开始看执行部分。&lt;/p&gt;
&lt;p&gt;由于我们已经获取到了&lt;code&gt;ActorFactory&lt;/code&gt;的实例，所以这次在调用中不再构造。然后调用&lt;code&gt;Create&lt;/code&gt;函数。传入类名和构造函数参数。在这个函数中寻找对应类名的实例创建方法，然后完美转发参数至这个函数。进入到映射器存储的对应类的实例创建方法也就是&lt;code&gt;CreateObject&lt;/code&gt;函数，&lt;code&gt;new&lt;/code&gt;一个对应的对象。&lt;/p&gt;
&lt;p&gt;这时候，我们刚刚才开始创建对象。随后，这个对象的第一基类也就是反射基类被创建。然后第二基类，也就是对象工厂基类被创建。然后是对象的子类部分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;----------------------------------------------------------------------
static ActorFactory* Instance() //获取实例。由于上面已经构造过ActorFactory了，所以直接拿就可以。
Actor* ActorFactory::Create(const std::string&amp;amp; strTypeName, Targs... args) //正式调用create
static Actor* DynamicCreator::CreateObject(Targs... args) //调用至映射器存储的对应类的CreateObject函数，new一个对应的对象
Actor construct //对应对象的反射基类部分
neb::DynamicCreator&amp;lt;T, Targs&amp;gt;::DynamicCreator() [with T = neb::Cmd; Targs = {}]//为了好看
DynamicCreator construct //对应对象的DynamicCreator基类部分。注意这个和上面的不同。这里是基类的构造。上面执行前的是内部嵌套类静态Register对象的初始化导致的。
Create Cmd //对象子类部分
I am Cmd
----------------------------------------------------------------------
static ActorFactory* Instance()
Actor* ActorFactory::Create(const std::string&amp;amp; strTypeName, Targs... args)
static Actor* DynamicCreator::CreateObject(Targs... args)
Actor construct
neb::DynamicCreator&amp;lt;T, Targs&amp;gt;::DynamicCreator() [with T = neb::Step; Targs = {std::__cxx11::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt;, int}]
DynamicCreator construct
Create Step neb::Step with seq 1002
I am Step
----------------------------------------------------------------------
static ActorFactory* Instance()
Actor* ActorFactory::Create(const std::string&amp;amp; strTypeName, Targs... args)
static Actor* DynamicCreator::CreateObject(Targs... args)
Actor construct
neb::DynamicCreator&amp;lt;T, Targs&amp;gt;::DynamicCreator() [with T = neb::miku; Targs = {int, int}]
DynamicCreator construct
miku23
Actor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译期静态反射，来自&lt;a href=&quot;https://netcan.github.io/2020/08/01/%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E7%9A%84%E5%AE%9E%E7%8E%B0C-%E7%BC%96%E8%AF%91%E6%9C%9F%E9%9D%99%E6%80%81%E5%8F%8D%E5%B0%84/&quot;&gt;这里&lt;/a&gt;，看不懂。&lt;/p&gt;
&lt;h1&gt;动态链接的使用经验&lt;/h1&gt;
&lt;h2&gt;运行时动态链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先拥有动态库源文件，如&lt;code&gt;chn_lang.cpp&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
extern &quot;C&quot;{ //必须使用extern &quot;C&quot;
    void print_cur_language(){
        std::cout &amp;lt;&amp;lt; &quot;chinese&quot; &amp;lt;&amp;lt; std::endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编译源文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;g++ -c -fPIC chn_lang.cpp -o chn_lang.o
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;链接为动态链接库&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;g++ -shared chn_lang.o -o libchn_lang.so
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编写主文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;dlfcn.h&amp;gt;
int main() {
    void* handle = dlopen(&quot;./libchn_lang.so&quot;, RTLD_LAZY);
    if (!handle) {
        std::cout &amp;lt;&amp;lt; &quot;Failed to load the library: &quot; &amp;lt;&amp;lt; dlerror() &amp;lt;&amp;lt; std::endl;
        return 1;
    }
    // 从动态链接库中获取函数指针
    typedef void (*ExampleFunction)();
    ExampleFunction exampleFunc = (ExampleFunction) dlsym(handle, &quot;print_cur_language&quot;);
    if (!exampleFunc) {
        std::cout &amp;lt;&amp;lt; &quot;Failed to load the function: &quot; &amp;lt;&amp;lt; dlerror() &amp;lt;&amp;lt; std::endl;
        dlclose(handle);
    return 1;
    }
    // 调用函数
    exampleFunc();
    // 关闭动态链接库
    dlclose(handle);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编译主文件
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;g++&lt;/code&gt; 编译器时，使用 &lt;code&gt;-ldl&lt;/code&gt; 选项是为了链接 &lt;code&gt;libdl&lt;/code&gt; 库，该库提供了对动态链接库（&lt;code&gt;.so&lt;/code&gt; 文件）的动态加载和符号解析的支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;g++ main.cpp -ldl -o main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运行时动态链接的好处和坏处&lt;/h3&gt;
&lt;p&gt;好处是可以自由的加载和卸载所需的动态库。坏处是有些API，比如&lt;code&gt;dlsym&lt;/code&gt;是采用字符串来进行符号查找，所以&lt;code&gt;C++&lt;/code&gt;这种带mangling的就会很难受。一般会使用&lt;code&gt;extern &quot;C&quot;&lt;/code&gt;来强制使用C风格的API，但是这样就不能使用函数重载了。&lt;/p&gt;
&lt;h2&gt;加载时动态链接&lt;/h2&gt;
&lt;p&gt;根据我目前的学习方式，由于&lt;code&gt;dlsym&lt;/code&gt;是采用我们拿字符串来进行符号查找，所以C++这种带mangling的就会很难受。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先拥有动态库源文件，如&lt;code&gt;chn_lang.cpp&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
extern &quot;C&quot;{ //如果此处使用了extern &quot;C&quot;, 则后面的头文件也需要使用extern &quot;C&quot;注明
    void print_cur_language(){
        std::cout &amp;lt;&amp;lt; &quot;chinese&quot; &amp;lt;&amp;lt; std::endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编译源文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;g++ -c -fPIC chn_lang.cpp -o chn_lang.o
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;链接为动态链接库&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;g++ -shared chn_lang.o -o libchn_lang.so
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编写头文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#ifndef LANGUAGEHEADER_H
#define LANGUAGEHEADER_H

extern &quot;C&quot; void print_cur_language(); //因为我们上面使用了extern &quot;C&quot;，所以这里也需要
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编写主文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &quot;languageheader.h&quot;
int main(){
    print_cur_language();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;编译主文件并链接至动态链接库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;g++ main.cpp -l&amp;lt;library1&amp;gt; -l&amp;lt;library2&amp;gt; -o executable
如
g++ main.cpp -lchn_lang -o main
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果在使用&lt;code&gt;-l&lt;/code&gt;指定库文件名称的时候，我们的动态链接库不在标准路径，则需要配置环境变量并添加&lt;code&gt;-L&lt;/code&gt;指定库文件的目录路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;export LD_LIBRARY_PATH=&amp;lt;path_to_library&amp;gt;:$LD_LIBRARY_PATH
如
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
然后
g++ main.cpp -L./ -lchn_lang -o main
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;采用&lt;code&gt;-l&lt;/code&gt;指定库文件名称会帮助我们把动态链接库的前缀&lt;code&gt;lib&lt;/code&gt;和后缀&lt;code&gt;.so&lt;/code&gt;补齐，所以此处不需要再添加。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;搜索路径可以是绝对路径或相对路径。如果是完整的绝对路径（例如 &lt;code&gt;/path/to/library&lt;/code&gt;），则链接器将使用该路径作为库文件的准确位置。如果是相对路径（例如 &lt;code&gt;./libraries&lt;/code&gt; 或 &lt;code&gt;../libs&lt;/code&gt;），则链接器将在当前工作目录下搜索库文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230619203556.png&quot; alt=&quot;QQ截图20230619203556&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果我们的库文件的名称并不是标准化，如&lt;code&gt;lib___.so&lt;/code&gt;形式，则可以在&lt;code&gt;-l&lt;/code&gt;命令后添加冒号&lt;code&gt;:&lt;/code&gt;。然后放入库文件的全名附带后缀&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;g++ main.cpp -L./ -l:somethingchn_lang.so -o main
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果此时我们有多个动态链接库文件，并且都有同名符号，则链接顺序和符号强度决定了具体调用的函数。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ main2.cpp -L./ -lchn_lang -leng_lang -o main2
g++ main2.cpp -L./ -leng_lang -lchn_lang -o main2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一种输出&lt;code&gt;chinese&lt;/code&gt;而第二种输出&lt;code&gt;english&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;具体的一些讨论可以参考这两篇文章：&lt;a href=&quot;https://xuhongxu.com/2018/09/%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%BA%93%E7%9A%84%E7%AC%A6%E5%8F%B7%E9%87%8D%E5%90%8D%E9%97%AE%E9%A2%98/&quot;&gt;动态链接库的符号重名问题&lt;/a&gt; 和 &lt;a href=&quot;https://www.cnblogs.com/kernel_hcy/archive/2010/01/27/1657411.html&quot;&gt;强弱符号和强弱引用&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;有用的文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sandordargo.com/blog/2024/09/25/dynamic-vs-static-linking-binary-size&quot;&gt;动态链接静态链接和二进制大小&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://huweicai.com/dynamic-linking/&quot;&gt;动态链接的原理&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++杂记 - 5（一）：std::pmr 多态内存分配器</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B05/</guid><pubDate>Tue, 07 Mar 2023 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;std::pmr 多态内存分配器&lt;/h1&gt;
&lt;h2&gt;先回顾一下标准分配器&lt;/h2&gt;
&lt;p&gt;这一节最好搭配侯捷的标准STL内存分配器来看，这样可以很好帮助理解一些动作的行为。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分配器的核心就是四个步骤： 分配空间（allocate），构造对象（construct），析构对象（destruct），解分配空间（deallocate）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;标准内存分配器就提供了这四个函数。多态内存分配器也提供了这四个函数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230308160711.png&quot; alt=&quot;QQ截图20230308160711&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在标准分配器当中，&lt;code&gt;allocate&lt;/code&gt;和&lt;code&gt;deallocate&lt;/code&gt;对应的全局的&lt;code&gt;::operator new&lt;/code&gt;和&lt;code&gt;::operator delete&lt;/code&gt;。&lt;code&gt;construct&lt;/code&gt;和&lt;code&gt;destruct&lt;/code&gt;对应的就是元素的构造和析构&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么要有多态内存分配器&lt;/h2&gt;
&lt;p&gt;在没有多态内存分配器的时候，我们面临两个问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一种情况是内存分配器的传播问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们有一个独特的例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230308162015.png&quot; alt=&quot;QQ截图20230308162015&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们有一个&lt;code&gt;CustomString&lt;/code&gt;类，每一个对象接受一个&lt;code&gt;allocator&lt;/code&gt;。我们现在有三个不同&lt;code&gt;allocator&lt;/code&gt;的同类对象。注意此时对象类型均为&lt;code&gt;CustomString&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;然后我们放入&lt;code&gt;vector&lt;/code&gt;。在&lt;code&gt;push_back&lt;/code&gt; &lt;code&gt;x1&lt;/code&gt;和&lt;code&gt;x2&lt;/code&gt;的时候没什么问题，使用了对应的分配器。但是如果我们在头部插入了&lt;code&gt;x3&lt;/code&gt;，情况就不对了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt;的头插逻辑是先在下一个可用位置分配一块内存，然后使用移动（拷贝）构造在新的位置构造出最后一个元素。然后再按照顺序把前面的元素搬移（拷贝）。在搬移（拷贝中），分配器是不会被拷贝（赋值）的。因为永远不该这么做，因为内存是已经被分配好了的，无法更改。&lt;/li&gt;
&lt;li&gt;所以这个时候有问题了。&lt;code&gt;x1&lt;/code&gt;用了&lt;code&gt;alloc2&lt;/code&gt;，&lt;code&gt;x3&lt;/code&gt;用了&lt;code&gt;alloc1&lt;/code&gt;。瞬间混乱了起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;我们真正想要的是这样的东西：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230308162600.png&quot; alt=&quot;QQ截图20230308162600&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以在C++17之前，我们有一个&lt;code&gt;std::scoped_allocator_adaptor&lt;/code&gt;。我们可以把自己的分配器使用它包装，它可以保证分配器的正确传播：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;std::scoped_allocator_adaptor&lt;/code&gt; 类模板是可用于多层容器（ map 的 list 的 set 的 vector 等等）的分配器。它以外层分配器类型 &lt;code&gt;OuterAlloc&lt;/code&gt; 与零或更多个内层分配器类型 &lt;code&gt;InnerAlloc...&lt;/code&gt; 实例化。直接以 &lt;code&gt;scoped_allocator_adaptor&lt;/code&gt; 构造的容器用 &lt;code&gt;OuterAlloc&lt;/code&gt; 分配其元素，但若元素自身是容器，则它使用首个内存分配器。该容器的元素，若其自身是容器，则使用第二个内存分配器，以此类推。若容器的层级多于内层分配器的数量，则最内层分配器为所有进一步内嵌的容器再度使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;第二个问题是内存分配器固有的问题：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;针对默认内存分配器本身的性能有如下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内存分配、回收慢。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存分配可能有锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存对齐无法控制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分配位置无法控制。比如连续多次分配时，内存的定域性/局部性(locality)无法保证。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;针对使用的参数方面有如下问题（尤其针对&lt;code&gt;std::allocator&lt;/code&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;allocator&lt;/code&gt;是模板签名的一部分。不同&lt;code&gt;allocator&lt;/code&gt;的容器，无法混用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;void func(const std::vector&amp;lt;int&amp;gt;&amp;amp;); // Default allocator
//上面的vector签名是：std::vector&amp;lt;int, std::allocator&amp;lt;int&amp;gt;&amp;gt;
std::vector&amp;lt;int, MyAlloc&amp;lt;int&amp;gt;&amp;gt; v(someAlloc); // 自定义分配器
func(v); // ERROR: v is a different type than std::vector&amp;lt;int&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二者签名不匹配。我们尽管可以使用模板，但是这会让后面所有的函数都是模板。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;c++11以前，&lt;code&gt;allocator&lt;/code&gt;无状态；c++11以后，可以有状态，然而&lt;code&gt;allocator&lt;/code&gt;类型复杂难用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;allocator&lt;/code&gt;内存对齐无法控制，需要传入自定义&lt;code&gt;allocator&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以上三点、特别是第一点，造成STL无法成为软件接口(interface)的一部分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;难以将局部特种内存（比如共享内存），内存池用于STL容器。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;多态内存分配器，内存资源和容器的关系&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230308163419.png&quot; alt=&quot;QQ截图20230308163419&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;memory_resource&lt;/code&gt;是获得的原始内存，我们拿到了原始内存后，通过&lt;code&gt;polymorphic_allocator&lt;/code&gt;分配器来进行分配。最后容器需要使用分配器来给元素分配内存。说白了，分配器从内存资源中获得资源，然后进行内存的再分配。&lt;code&gt;polymorphic_allocator&lt;/code&gt;分配器是一层&lt;code&gt;memory_resource&lt;/code&gt;的包装。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多态内存分配器作用域下的容器是&lt;code&gt;pmr::&lt;/code&gt;。其实就是别名模板。所以我们以&lt;code&gt;vector&lt;/code&gt;为例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;namespace pmr {
    template&amp;lt; class T &amp;gt;
    using vector = std::vector&amp;lt;T, std::pmr::polymorphic_allocator&amp;lt;T&amp;gt;&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;由于多态内存分配器把内存资源的获取和分配分离开来，所以可以保证同一个元素哪怕底层内层资源不同，但是分配器的类型可以相同。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这部分的细节我们后面再细说。&lt;/p&gt;
&lt;h3&gt;再次解释一下多态内存分配器为什么解决了签名问题&lt;/h3&gt;
&lt;p&gt;**重申一下，内存资源不是类模板。而是使用了继承。所以这一部分是动态多态。所以说内存资源类型是运行时确定的。在编译期间，所有&lt;code&gt;pmr::vector&amp;lt;T&amp;gt;&lt;/code&gt;类型的容器都是同一个类型。**分配器使用一个内存资源的指针在内部对内存资源进行管理。在上面的&lt;code&gt;pmr::&lt;/code&gt;容器中，我们看到了多态内存分配器的模板参数是元素类型。然后我们容器已经构建完毕了。然后我们可以使用容器的构造函数传入分配器。这里用&lt;code&gt;vector&lt;/code&gt;举例子：去查看&lt;a href=&quot;https://zh.cppreference.com/w/cpp/container/vector/vector&quot;&gt;文档&lt;/a&gt; 以第二个构造函数为例，就是接受一个分配器。&lt;/p&gt;
&lt;p&gt;所以传入展开后会长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;MyClass, std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; &amp;gt; vec = std::vector&amp;lt;MyClass, std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; &amp;gt;(allocator2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们可以看到所有的模板参数都是元素类型。分配器底层的内存资源并不影响分配器的类型。因为我们重申多态内存分配器把内存资源和分配分离开来了。所以针对同一元素类型的容器，可以使用不同类型的分配器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230309004954.png&quot; alt=&quot;QQ截图20230309004954&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;内存资源 memory_resources&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/image-20230308163929518.png&quot; alt=&quot;image-20230308163929518&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;上图是一个抽象简化版的内存资源类。我们看到里面有allocate和deallocate。这两个是非虚函数。他们的分配动作是委托给私有虚成员函数&lt;code&gt;do_allocate&lt;/code&gt;和&lt;code&gt;do_deallocate&lt;/code&gt;进行的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里是模板方法模式的NVI（non-virtual-interface）技术。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;memory_resource&lt;/code&gt;是一个抽象基类。STL给我们提供了内置的5种内存资源和一些工具函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们如果要自己实现自己的内存资源，也需要继承自此类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;类 &lt;code&gt;std::pmr::memory_resource&lt;/code&gt; 是抽象接口，针对封装内存资源类的无界集。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承自&lt;code&gt;memory_resource&lt;/code&gt;的类需要实现的函数主要是四个&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;do_allocate&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从自身内存中分配特定大小的内存。
&lt;ul&gt;
&lt;li&gt;具体定义会随之变化。比如从何处分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;do_deallocate&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从自身内存中解分配某一特定位置，特定大小的区块。
&lt;ul&gt;
&lt;li&gt;具体定义会随之变化。比如解分配到何处，是否真的解分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;is_equal&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比较两个内存资源是否相等。也就是从当前这个内存资源分配的内存是否可以通过另一个内存资源解分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;release&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用上游的deallocate来释放内存。将全部内存交还给上游内存分配器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存资源是可以成为链条的。这一点我们下面讲。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意内存资源类不是类模板！！！&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;std::pmr::new_delete_resource&lt;/h3&gt;
&lt;p&gt;这是最基础的，也是默认的内存资源。它是从哪儿获取的原始内存呢？答案是从全局的&lt;code&gt;::operator new&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;返回指向使用全局 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/memory/new/operator_new&quot;&gt;operator new&lt;/a&gt; 与 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/memory/new/operator_delete&quot;&gt;operator delete&lt;/a&gt; 分配内存的 &lt;code&gt;memory_resource&lt;/code&gt; 的指针。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;allocate&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;因为这是默认的选项，所以非常自然地，内存的分配会使用&lt;code&gt;::operator new&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deallocate&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;既然分配的都用默认的了，那么解分配也是会用到&lt;code&gt;::operator delete&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一般来说，上游分配器都是它。默认也是它。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;std::pmr::monotonic_buffer_resource&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;这是一个比较特殊的资源。它特殊就特殊在我们可以使用栈内存做为内存资源。如使用&lt;code&gt;char buffer[1024]&lt;/code&gt;来获取一段栈内存做为原始内存。&lt;strong&gt;它是一种单调递增的资源。它仅在销毁资源时释放分配的内存。它的意图是提供非常快速的内存分配，在内存用于分配少量对象，并于之后一次释放的情况下。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;因为它的原理仅仅是推指针。所以分配速度非常快。属于一块线性内存。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;monotonic_buffer_resource(); 											//(1)
explicit monotonic_buffer_resource(std::pmr::memory_resource* upstream);	//(2)
explicit monotonic_buffer_resource(std::size_t initial_size);				//(3)
monotonic_buffer_resource(std::size_t initial_size, std::pmr::memory_resource* upstream);			//(4)
monotonic_buffer_resource(void* buffer, std::size_t buffer_size);			//(5)
monotonic_buffer_resource(void* buffer, std::size_t buffer_size, std::pmr::memory_resource* upstream);//(6)
monotonic_buffer_resource(const monotonic_buffer_resource&amp;amp;) = delete;		//(7)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;monotonic_buffer_resource&lt;/code&gt; 能以初始缓冲区构造，若无初始缓冲，或缓冲用尽，则从构造时提供的&lt;em&gt;上游分配器&lt;/em&gt;分配缓冲区。缓冲区的大小以几何级数增长。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比较常用的有1,2,5,6&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个就是默认构造函数。默认构造函数会使用&lt;code&gt;std::pmr::get_default_resource&lt;/code&gt;返回的&lt;code&gt;memory_resource &lt;/code&gt;当做上游分配器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二个就是指定上游分配器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第五个是提供一块给定内存&lt;code&gt;buffer&lt;/code&gt;，然后设置当前资源为&lt;code&gt;buffer&lt;/code&gt;。&lt;code&gt;buffer_size&lt;/code&gt;是下一块缓冲区的大小，然后使用&lt;code&gt;std::pmr::get_default_resource&lt;/code&gt;返回的&lt;code&gt;memory_resource &lt;/code&gt;当做上游分配器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当给定的内存用尽后，使用上游分配器进行内存分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第六个是提供一块给定内存&lt;code&gt;buffer&lt;/code&gt;，然后设置当前资源为&lt;code&gt;buffer&lt;/code&gt;。&lt;code&gt;buffer_size&lt;/code&gt;是下一块缓冲区的大小，并且指定上游分配器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当给定的内存用尽后，使用上游分配器进行内存分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;do_allocate&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;若当前缓冲区有足够的未使用空间以适合一个拥有指定大小和对齐的块，则从当前缓冲区分配返回的块。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;否则，此函数通过调用 &lt;code&gt;upstream_resource()-&amp;gt;allocate(n, m)&lt;/code&gt; 分配新缓冲区&lt;/strong&gt;，其中 &lt;code&gt;n&lt;/code&gt; 不小于&lt;code&gt;bytes&lt;/code&gt; 与&lt;em&gt;下个缓冲区大小&lt;/em&gt;的较大者，且 &lt;code&gt;m&lt;/code&gt; 不小于 &lt;code&gt;alignment&lt;/code&gt; 。它设置新缓冲区为&lt;em&gt;当前缓冲区&lt;/em&gt;，以实现定义的增长因子（不必是整数）增加&lt;em&gt;下个缓冲区大小&lt;/em&gt;，然后从新分配的缓冲区分配返回块。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插一句话，这个时候就是使用&lt;code&gt;std::pmr::null_memory_resource&lt;/code&gt;的一个典型场景&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;do_deallocate&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;此函数无效应。！！！啥也不干！！！因为我们都说了它是单调递增的！！！直到销毁资源为止（对象析构或调用release）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;release&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;**通过上游 memory_resource 的 &lt;code&gt;deallocate&lt;/code&gt; 函数，按需要释放所有分配的内存。**重设&lt;em&gt;当前缓冲区&lt;/em&gt;及&lt;em&gt;下个缓冲区大小&lt;/em&gt;为其构造时的初始值。&lt;/p&gt;
&lt;p&gt;内存被释放回上游资源，即使未对某些被分配块调用 &lt;code&gt;deallocate&lt;/code&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;释放内存。注意，这是调用&lt;strong&gt;上游&lt;/strong&gt;&lt;code&gt;deallocate&lt;/code&gt;解分配。&lt;strong&gt;解分配不代表调用内存中储存对象的析构函数。仅仅是把指针塞回给上游内存池。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果上游是&lt;code&gt;null_memory_resource&lt;/code&gt;，那就相当于啥也没干。&lt;strong&gt;同时，一般来说使用这种方式分配内存的时候，初始缓冲区一般是在栈上。如果在堆上，记得要手动回收提供的初始内存。因为这些内存资源不会帮助我们回收初始内存资源。所谓的：重设当前缓冲区及下个缓冲区大小为其构造时的初始值 的含义仅仅是把指针推回去。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;内存被释放回上游资源，即使未对某些被分配块调用 &lt;code&gt;deallocate&lt;/code&gt; 。这句话每一个内存资源的&lt;code&gt;release&lt;/code&gt;都有。我们下面再说。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;源代码，&lt;a href=&quot;https://reviews.llvm.org/D47090&quot;&gt;来自&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void monotonic_buffer_resource::release()
{
    const size_t header_size = sizeof(__monotonic_buffer_header);

    __original_.__used_ = 0;
    while (__original_.__next_ != nullptr) {
        __monotonic_buffer_header *header = __original_.__next_;
        __monotonic_buffer_header *next_header = header-&amp;gt;__next_;
        size_t aligned_capacity = header-&amp;gt;__capacity_ + header_size;
        __res_-&amp;gt;deallocate(header-&amp;gt;__start_, aligned_capacity, header-&amp;gt;__alignment_); //调用上游分配器的deallocate
        __original_.__next_ = next_header;
    }
}
//除了那一行以外其他的就是回推指针。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;析构函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;this-&amp;gt;release() &lt;/code&gt;来解分配此资源拥有的所有内存。
&lt;ul&gt;
&lt;li&gt;所以说针对这几种内存资源，压根没必要&lt;code&gt;release&lt;/code&gt;。因为已经应用了RAII特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们下面的测试是单纯的测试内存分配器。并没有结合容器。但是效果是一致的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    MyClass(int val = 0) : m_val(new int(val)) {
        std::cout &amp;lt;&amp;lt; &quot;MyClass constructed with value: &quot; &amp;lt;&amp;lt; val &amp;lt;&amp;lt; std::endl;
    }
    ~MyClass() {
        std::cout &amp;lt;&amp;lt; &quot;MyClass destroyed with value: &quot; &amp;lt;&amp;lt; *m_val &amp;lt;&amp;lt; std::endl;
        delete m_val;
    }
private:
    int* m_val;
};
int main() {
    char buffer[1024]; //从栈获取原始内存
    std::size_t buffer_size = sizeof(buffer); 
    cout &amp;lt;&amp;lt; &quot;original &quot;&amp;lt;&amp;lt;&amp;amp;buffer &amp;lt;&amp;lt; endl; //原始内存地址
    {
        std::pmr::monotonic_buffer_resource buffer_resource{buffer, buffer_size, std::pmr::null_memory_resource()};
        //monotonic memory resource使用栈获取的资源做为初始资源，使用null_memory_resource当做上游分配器强调内存用尽后抛出bad_alloc而不是从默认分配器分配
        std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; allocator{ &amp;amp;buffer_resource }; //初始化分配器
        {
            MyClass* t = allocator.allocate(1); //分配1个Myclass对象 8字节
            cout &amp;lt;&amp;lt; &amp;amp;t &amp;lt;&amp;lt; endl; //获取指针地址
            cout &amp;lt;&amp;lt; &quot;1st alloc place &quot;&amp;lt;&amp;lt;&amp;amp;*t &amp;lt;&amp;lt; endl; //获取当前分配地址
            allocator.construct(t, 20); //在该地址构造元素

            MyClass* tt = allocator.allocate(1); //再分配1个Myclass对象 8字节
            cout &amp;lt;&amp;lt; &amp;amp;tt &amp;lt;&amp;lt; endl; //获取指针地址
            cout &amp;lt;&amp;lt; &quot;2nd alloc place  &quot;&amp;lt;&amp;lt;&amp;amp;*tt &amp;lt;&amp;lt; endl; //获取当前分配地址
            allocator.construct(tt, 30); //在该地址构造元素
//------------如果不destroy会泄露。因为deallocate解分配并不会调用元素的析构函数---------
            allocator.destroy(t); //析构元素
            allocator.destroy(tt); //析构元素
//----------使用release重置内存，把首地址推回分配起始地址（原始内存首地址）           
            buffer_resource.release();
            
            MyClass* ttt = allocator.allocate(1); //再次分配1个Myclass对象
            cout &amp;lt;&amp;lt; &amp;amp;ttt &amp;lt;&amp;lt; endl;//获取指针地址 
            cout &amp;lt;&amp;lt; &quot;3rd alloc place  &quot;&amp;lt;&amp;lt;&amp;amp;*ttt &amp;lt;&amp;lt; endl; //获取当前分配地址
            allocator.construct(ttt, 40);  //在该地址构造元素
            allocator.destroy(ttt);  
        }
    }
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;original 0x7ffe4ff6eb90 起始地址
0x7ffe4ff6eb30
1st alloc place 0x7ffe4ff6eb90 起始地址和第一次分配地址一致
MyClass constructed with value: 20
0x7ffe4ff6eb38
2nd alloc place  0x7ffe4ff6eb98 第二次分配在第一次分配上+8
MyClass constructed with value: 30
MyClass destroyed with value: 20
MyClass destroyed with value: 30
0x7ffe4ff6eb40
3rd alloc place  0x7ffe4ff6eb90 release后指针推回至原生内存起始位置。
MyClass constructed with value: 40
MyClass destroyed with value: 40
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们从上面的测试能看到&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不&lt;code&gt;destroy&lt;/code&gt;会导致内存泄露。因为&lt;code&gt;deallocate&lt;/code&gt;解分配并不会调用内存中元素的析构函数，仅仅是回推指针。&lt;/li&gt;
&lt;li&gt;如果不需要重复使用内存，则无需手动调用&lt;code&gt;release&lt;/code&gt;。RAII特性会让&lt;code&gt;buffer_resource&lt;/code&gt;内存资源在析构时自动调用&lt;code&gt;release&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;注意：我们是使用&lt;code&gt;allocator&lt;/code&gt;进行内存分配，对象构造和对象摧毁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假设我们的初始内存是从堆中获取的，则必须要手动释放。因为分配器和内存资源并不保有底层原始内存&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;char* buffer = new char[1024]; //堆内存。
std::size_t buffer_size = 1024;
{
    std::pmr::monotonic_buffer_resource buffer_resource{buffer, buffer_size, std::pmr::null_memory_resource()};
    std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; allocator{ &amp;amp;buffer_resource };
    {
        //...
    }
}
delete[] buffer; //回收内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析一下&lt;code&gt;release&lt;/code&gt;过程。无论是析构还是手动调用&lt;code&gt;release&lt;/code&gt;，都有两个步骤：第一步是调用上游的&lt;code&gt;memory_resource&lt;/code&gt; 的 &lt;code&gt;deallocate&lt;/code&gt; 函数。第二步是重设当前缓冲区及下个缓冲区大小为其构造时的初始值。当前我们上游是&lt;code&gt;null_memory_resource&lt;/code&gt;，&lt;code&gt;deallocate&lt;/code&gt;函数无效果。所以仅仅是把当前缓冲区及下个缓冲区大小为其构造时的初始值。（推回指针）&lt;/p&gt;
&lt;h3&gt;std::pmr::unsynchronized_pool_resource / std::pmr::synchronized_pool_resource&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;这是一种池化的内存资源：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230308173021.png&quot; alt=&quot;QQ截图20230308173021&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体应该不用赘述。侯捷老师的课讲标准分配器的时候就提到过大概的样子。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这俩唯一的区别是一个线程安全，一个线程不安全。线程安全的性能贼差尽量别用。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它占有被分配内存并在析构时释放它，即使 &lt;code&gt;deallocate&lt;/code&gt; 未被对于某些被分配块调用。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个释放是指调用&lt;code&gt;release&lt;/code&gt;。&lt;code&gt;release&lt;/code&gt;中调用上游 memory_resource 的 &lt;code&gt;deallocate&lt;/code&gt; 函数，释放所有此资源所占有的内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它由供应不同块大小请求的池的汇集组成。每个池管理之后被分入一致大小的大块的汇集。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对 &lt;code&gt;do_allocate&lt;/code&gt;的调用被派发到供应适应请求大小的最小块的池。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在池中耗尽内存，会导致该池从上游分配器分配额外的内存大块，以填满池。获取的大块大小以几何级数增加。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求超出最大的块大小的分配，由上游分配器直接供应。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最大的块大小和大块大小的最大值可通过传递&lt;code&gt;std::pmr::pool_options&lt;/code&gt;结构体给其构造函数调节。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;构造函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;unsynchronized_pool_resource(); //(1)
explicit unsynchronized_pool_resource(std::pmr::memory_resource* upstream); //(2)
explicit unsynchronized_pool_resource(const std::pmr::pool_options&amp;amp; opts); //(3)
unsynchronized_pool_resource(const std::pmr::pool_options&amp;amp; opts,std::pmr::memory_resource* upstream); //(4)
unsynchronized_pool_resource(const unsynchronized_pool_resource&amp;amp;) = delete; //(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;有没有发现这一组构造函数和&lt;code&gt;monotonic_buffer_resource&lt;/code&gt;有个非常明显的差别？也就是我们不再有像&lt;code&gt;monotonic_buffer_resource&lt;/code&gt;第5和第6个构造函数那种提供初始原始内存的构造函数了。也就是说这两个池化内存资源必须从上游分配器获取资源。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果依旧想要从栈分配，则可以先使用栈分配&lt;code&gt;monotonic_buffer_resource&lt;/code&gt;，然后让它成为当前池化内存的上游分配器。&lt;/p&gt;
&lt;p&gt;第一个构造函数就是使用默认的池化选项和默认的上游分配器。&lt;/p&gt;
&lt;p&gt;第二，三，四个分别指定上游分配器，池化选项和二者皆指定。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;do_allocate&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;若为 &lt;code&gt;bytes&lt;/code&gt; 大小的块所选的池不足以满足来自其内部数据结构的请求，则调用上游 memory_resources 上的 &lt;code&gt;allocate()&lt;/code&gt; 以获得内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;若请求的大小大于最大的池所能处理者，则通过调用上游 memory_resources 上的 &lt;code&gt;allocate()&lt;/code&gt; 分配内存。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;do_deallocate&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;将在 &lt;code&gt;p&lt;/code&gt; 的内存返还到池。&lt;strong&gt;此操作是否或在何种场合下导致对上游 memory_resource 上的 &lt;code&gt;deallocate()&lt;/code&gt; 调用是未指定的。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在对池化资源使用&lt;code&gt;do_deallocate&lt;/code&gt;的时候一定要仅返回内存至内存池，而不是调用上游分配器的&lt;code&gt;deallocate&lt;/code&gt;直接回收。一定要格外注意。但是这一点平时普通人不需要过多操心&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;release&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;需要时，通过调用上游 memory_resource 的 &lt;code&gt;deallocate&lt;/code&gt; 函数，释放所有此资源所占有的内存。&lt;/p&gt;
&lt;p&gt;即使未对某些被分配块调用 &lt;code&gt;deallocate&lt;/code&gt; ，资源也被返还给上游 memory_resource 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这就是&lt;code&gt;release&lt;/code&gt;和&lt;code&gt;deallocate&lt;/code&gt;的最大区别。&lt;code&gt;deallocate&lt;/code&gt;是返还内存到内存池。而&lt;code&gt;release&lt;/code&gt;是释放（返还）整个内存池全部的内存至上游分配器。因为是调用上游分配器的&lt;code&gt;deallocate&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不知道这玩意是只释放内存还是摧毁整个池。标准库没说&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;析构函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过调用 &lt;code&gt;this-&amp;gt;release()&lt;/code&gt; 释放此资源所拥有的所有内存。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和其他的一样。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;我们捋一下针对池化内存release的可能情况&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;第一种情况：
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;sync&lt;/code&gt;或&lt;code&gt;unsync&lt;/code&gt;的上游分配器是&lt;code&gt;monotonic&lt;/code&gt;，&lt;code&gt;sync&lt;/code&gt;或&lt;code&gt;unsync&lt;/code&gt;调用&lt;code&gt;release&lt;/code&gt;会使上游的&lt;code&gt;monotonic&lt;/code&gt;调用&lt;code&gt;deallocate&lt;/code&gt;。但是&lt;code&gt;monotonic&lt;/code&gt;的&lt;code&gt;deallocate&lt;/code&gt;不做任何事情。所以分配出去的内存无法被&lt;code&gt;monotonic&lt;/code&gt;回收。只能单调递增直到&lt;code&gt;monotonic&lt;/code&gt;的&lt;code&gt;release&lt;/code&gt;被调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第二种情况：
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;sync&lt;/code&gt;或&lt;code&gt;unsync&lt;/code&gt;的上游是&lt;code&gt;new_delete_resource&lt;/code&gt;，则&lt;code&gt;sync/unsync&lt;/code&gt;调用&lt;code&gt;release&lt;/code&gt;会使得上游的&lt;code&gt;new_delete_resource&lt;/code&gt;调用其&lt;code&gt;deallocate&lt;/code&gt;，我们说过它的&lt;code&gt;deallocate&lt;/code&gt;是全局&lt;code&gt;::operator delete&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;在已经release的内存资源上再次调用allocate是否是UB？&lt;/h3&gt;
&lt;p&gt;标准库没有明确说明。例子如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::pmr::monotonic_buffer_resource buffer_resource{std::pmr::new_delete_resource()}; //最外层monotonic通过new_delete获取原始内存。
std::pmr::unsynchronized_pool_resource midlevel{&amp;amp;buffer_resource}; //第一层unsync池化
std::pmr::unsynchronized_pool_resource un_sync{&amp;amp;midlevel}; //第二层unsync 池化
std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; allocator{&amp;amp;un_sync};
{
    MyClass* t = allocator.allocate(1);
    cout &amp;lt;&amp;lt; &amp;amp;t &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;1st alloc place &quot;&amp;lt;&amp;lt;&amp;amp;*t &amp;lt;&amp;lt; endl;
    allocator.construct(t, 20);

    MyClass* tt = allocator.allocate(1);
    cout &amp;lt;&amp;lt; &amp;amp;tt &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;2nd alloc place  &quot;&amp;lt;&amp;lt;&amp;amp;*tt &amp;lt;&amp;lt; endl;
    allocator.construct(tt, 30);

    allocator.destroy(t);
    allocator.destroy(tt);

    un_sync.release(); //release内存资源。此行为会导致第一层unsync调用deallocate。回收已分配的内存到第一层unsync的内存池

    MyClass* ttt = allocator.allocate(1); //第二层unsync再次allocate
    cout &amp;lt;&amp;lt; &amp;amp;ttt &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;3rd alloc place  &quot;&amp;lt;&amp;lt;&amp;amp;*ttt &amp;lt;&amp;lt; endl;
    allocator.construct(ttt, 40);
    allocator.destroy(ttt);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;0x7ffe69b80ed8
1st alloc place 0x559e13abbc00 //第一次分配
MyClass constructed with value: 20
0x7ffe69b80ee0
2nd alloc place  0x559e13abbc08 //第二次分配
MyClass constructed with value: 30
MyClass destroyed with value: 20
MyClass destroyed with value: 30
释放
0x7ffe69b80ee8
3rd alloc place  0x559e13abbc00 //第三次分配。地址同第一次。
MyClass constructed with value: 40
MyClass destroyed with value: 40
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;经过我的测试，似乎是会重建内存池。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;已提问，目前无人&lt;a href=&quot;https://stackoverflow.com/questions/75680593/call-allocate-again-on-the-released-unsynchronized-pool-resource-is-an-undefin&quot;&gt;回答&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;std::pmr::null_memory_resource&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;很多人很奇怪？为啥要弄个空的呢？答案很简单。我们提到了，内存资源可以成为链条。如果某一个分配资源不够了，我们不希望它去默认的上游分配器去获取内存，就可以把这个分配器做为那个分配器的上游分配器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;allocate&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;其 &lt;code&gt;allocate()&lt;/code&gt; 函数始终抛出 std::bad_alloc；&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deallocate&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其 &lt;code&gt;deallocate()&lt;/code&gt; 函数无效果；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分配器 allocator&lt;/h2&gt;
&lt;p&gt;我们提到过。分配器是一种对内存资源的包装&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230309000840.png&quot; alt=&quot;QQ截图20230309000840&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230309004214.png&quot; alt=&quot;QQ截图20230309004214&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第二个转换构造函数非常有用。它的作用是我们可以直接把&lt;code&gt;memory_resource&lt;/code&gt;传入任何需要&lt;code&gt;polymorphic_allocator&lt;/code&gt;的地方。比如直接传入&lt;code&gt;pmr::&lt;/code&gt;容器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构造函数&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;polymorphic_allocator() noexcept; //(1)
polymorphic_allocator( const polymorphic_allocator&amp;amp; other ) = default; //(2)
template&amp;lt; class U &amp;gt; 
polymorphic_allocator( const polymorphic_allocator&amp;lt;U&amp;gt;&amp;amp; other ) noexcept; //(3) 关键的构造函数模板。
polymorphic_allocator( memory_resource* r); //(4) 转换构造
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个：默认构造函数会默认构造一个以默认值（通常为&lt;code&gt;new_delete_resource&lt;/code&gt;）为内存资源的分配器&lt;/p&gt;
&lt;p&gt;第二个：拷贝构造&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三个：构造函数模板。下面的模板参数以及正确转换全都靠它&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第四个：是上文提到的转换构造。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;allocate&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;用底层的 memory_resource 分配 &lt;code&gt;n&lt;/code&gt; 个 &lt;code&gt;T&lt;/code&gt; 类型对象的存储。&lt;/p&gt;
&lt;p&gt;等价于 &lt;code&gt;return static_cast&amp;lt;T*&amp;gt;(resource()-&amp;gt;allocate(n * sizeof(T), alignof(T)));&lt;/code&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于我们提过，分配器是一种内存资源的wrapper。所以分配器调用的&lt;code&gt;allocate&lt;/code&gt;就相当于对内存资源调用&lt;code&gt;allocate&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格外要注意的是，内存资源返回的内存是&lt;code&gt;void*&lt;/code&gt;。而分配器返回的内存是对应类型的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;deallocate&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;解分配 &lt;code&gt;p&lt;/code&gt; 所指向的存储，它必须通过与 &lt;code&gt;*resource()&lt;/code&gt; 比较等于的（相等的）&lt;code&gt;std::pmr::memory_resource&lt;/code&gt; &lt;code&gt;x&lt;/code&gt; 使用 &lt;code&gt;x.allocate(n * sizeof(T), alignof(T))&lt;/code&gt; 分配。&lt;/p&gt;
&lt;p&gt;等价于 &lt;code&gt;this-&amp;gt;resource()-&amp;gt;deallocate(p, n * sizeof(T), alignof(T));&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;还是调用了底层内存资源的&lt;code&gt;deallocate&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;construct&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这玩意有一堆重载。主要是看第一个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt; class U, class... Args &amp;gt;
void construct( U* p, Args&amp;amp;&amp;amp;... args ); //(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;p&lt;/code&gt; 所指的，分配但未初始化的存储上，以提供的构造函数参数构造一个对象。&lt;strong&gt;若对象自身拥有使用分配器的类型，或它是 &lt;code&gt;std::pair&lt;/code&gt; ，则传递 &lt;code&gt;this-&amp;gt;resource()&lt;/code&gt; 给被构造的对象。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;以&lt;a href=&quot;https://zh.cppreference.com/w/cpp/memory/uses_allocator#.E4.BD.BF.E7.94.A8.E5.88.86.E9.85.8D.E5.99.A8.E6.9E.84.E9.80.A0&quot;&gt;使用分配器构造&lt;/a&gt;的手段在 &lt;code&gt;p&lt;/code&gt; 所指示的未初始化内存位置，以 &lt;code&gt;*this&lt;/code&gt; 为分配器创建给定类型 &lt;code&gt;U&lt;/code&gt; 的对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;说白了就如果提供了分配器，就使用提供的分配器构造。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;destroy&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;销毁 &lt;code&gt;p&lt;/code&gt; 所指向的对象，如同以调用 &lt;code&gt;p-&amp;gt;~U()&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以我们强调了，&lt;code&gt;dellocate&lt;/code&gt;解分配不调用内存位置上元素的析构函数。摧毁元素和回收内存是两个独立动作。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;polymorphic_allocator 的模板参数&lt;/h2&gt;
&lt;p&gt;有一些讲座说我们应该始终以&lt;code&gt;std::byte&lt;/code&gt;为&lt;code&gt;polymorphic_allocator &lt;/code&gt;的模板参数。但是直觉来说，应该以元素类型为模板参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    MyClass(int val = 0) : m_val(new int(val)) {
        std::cout &amp;lt;&amp;lt; &quot;MyClass constructed with value: &quot; &amp;lt;&amp;lt; val &amp;lt;&amp;lt; std::endl;
    }
    ~MyClass() {
        std::cout &amp;lt;&amp;lt; &quot;MyClass destroyed with value: &quot; &amp;lt;&amp;lt; *m_val &amp;lt;&amp;lt; std::endl;
        delete m_val;
    }
private:
    int* m_val;
};
int main() {
    char buffer[1024];
    std::size_t buffer_size = 1024;
    {
        std::pmr::monotonic_buffer_resource buffer_resource{buffer, buffer_size, std::pmr::null_memory_resource()};
        //monotonic内存资源从已分配栈内存做为初始资源。并使用null做为上游资源
        std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; allocator{ &amp;amp;buffer_resource }; //版本1，分配器参数为元素类型：MyClass
        std::pmr::polymorphic_allocator&amp;lt;byte&amp;gt; allocator{ &amp;amp;buffer_resource }; //版本2，分配器参数为byte。
        {
            std::pmr::vector&amp;lt;MyClass&amp;gt; vec(allocator); //使用分配器初始化pmr vector
            vec.reserve(1);//预留一个空间
            vec.emplace_back(1);//原地构造一个对象
        }
    }
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;MyClass constructed with value: 1
MyClass destroyed with value: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意关于monotonic内存资源和winkout的联系会在下一节讲。但是这里必须要提到一点，winkout和使用何种内存资源之间无任何联系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两种分配器参数都可以正常工作。那么是什么原因呢？我们看一下这一行代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; allocator{ &amp;amp;buffer_resource };
std::pmr::vector&amp;lt;MyClass&amp;gt; vec(allocator); //此行展开
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行代码展开后的样子是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;MyClass, std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt;&amp;gt; vec = std::vector&amp;lt;MyClass, std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt;&amp;gt;(allocator);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先记住一点：&lt;code&gt;allocator&lt;/code&gt;的模板参数一定会被推导为元素类型。因为别忘了上面提到的&lt;code&gt;pmr&lt;/code&gt;是别名模板，元素类型是&lt;code&gt;T&lt;/code&gt;，&lt;code&gt;allocator&lt;/code&gt;的类型是根据&lt;code&gt;T&lt;/code&gt;来的。&lt;/p&gt;
&lt;p&gt;其次，我们发现如果分配器类型和元素类型一致，则顺利构造。左右两侧无区别。&lt;/p&gt;
&lt;p&gt;但是如果我们换成&lt;code&gt;byte&lt;/code&gt;呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::pmr::polymorphic_allocator&amp;lt;byte&amp;gt; allocator{ &amp;amp;buffer_resource };
std::pmr::vector&amp;lt;MyClass&amp;gt; vec(allocator); //此行展开
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行展开后是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;MyClass, std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt;&amp;gt; vec = std::vector&amp;lt;MyClass, std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt;&amp;gt;(std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt;(allocator));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次重申，&lt;code&gt;allocator&lt;/code&gt;的模板参数一定会被推导为元素类型。同时，右侧括号内传入的分配器发生了隐式类型转换。调用的正是我们在分配器中提到的第三个构造函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以理论上讲，此时行为都是一致的。而分配器本身的构造并不会造成性能的过多损失。但是有一点要注意，隐式类型转换后，传入的分配器是那个转换后的临时对象。所以现在&lt;code&gt;vector&lt;/code&gt;内部的分配器和外面传入的&lt;code&gt;allocator&lt;/code&gt;其实是两个独立对象了。一定要注意。最好还是使用和元素一样类型的分配器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我的&lt;a href=&quot;https://stackoverflow.com/questions/75669627/what-is-the-usage-of-the-template-parameter-of-the-stdpmrpolymorphic-allocat&quot;&gt;提问&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;分配器种类，分配器绑定方式和析构的组合&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;目前为止，分配器绑定方式可以分为两种&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型参数（模板参数）&lt;/li&gt;
&lt;li&gt;继承自抽象基类，比如：
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230309225735.png&quot; alt=&quot;QQ截图20230309225735&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意这里不是继承自&lt;code&gt;std::allocator&lt;/code&gt;。一般也不应该这样做。原因见&lt;a href=&quot;https://stackoverflow.com/questions/21081796/why-not-to-inherit-from-stdallocator&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230310032548.png&quot; alt=&quot;QQ截图20230310032548&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;目前为止，分配器内存来源可以分为这么几种&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;默认全局分配器（std::allocator)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;绑定方式一定是类型参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;New_Delete_Allocator&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和默认全局分配器原理一致，但是绑定方式一定是继承自抽象基类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多态内存资源&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如&lt;code&gt;monotonic_buffer_resource&lt;/code&gt;，&lt;code&gt;unsync/synchronized_pool_resource&lt;/code&gt;等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;析构方式又可以分为两种：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常规析构&lt;/li&gt;
&lt;li&gt;wink-out&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，一共我们可以得到这么多种搭配组合&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230309225948.png&quot; alt=&quot;QQ截图20230309225948&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;wink-out 和 常规析构&lt;/h2&gt;
&lt;p&gt;首先注意，wink-out适用于任何内存资源。&lt;strong&gt;我们还记得容器分配构造的四个步骤：分配空间，构造对象，析构对象，解分配空间。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;wink-out就是不析构，直接解分配。所以使用wink-out的时候一定要注意，如果内存区域的元素是含有额外资源的，则不可以winkout。会导致内存泄漏。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们看一下常规析构：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230309234908.png&quot; alt=&quot;QQ截图20230309234908&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一切都很正常。标准的四个步骤。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们再来看一下所谓的winked-out&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2023-03-07-%E6%9D%82%E8%AE%B05.assets/QQ%E6%88%AA%E5%9B%BE20230310023351.png&quot; alt=&quot;QQ截图20230310023351&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心意思就是我们不再需要析构和解分配两个步骤&lt;/p&gt;
&lt;p&gt;不需要析构非常好理解，因为如果某些对象没有保有其他资源，也就是析构函数是trivial的时候，调用析构是没有意义的。所以可以不调用这些析构。&lt;/p&gt;
&lt;p&gt;不需要&lt;code&gt;deallocate&lt;/code&gt;如何理解呢？一般情况下，通常只针对&lt;code&gt;monotonic_buffer_resource&lt;/code&gt;使用winkout。因为它的&lt;code&gt;deallocate&lt;/code&gt;是无作用。只有在析构的时候才会把资源交还给上游分配器。当然了，标准库提供的所有内存资源都符合RAII特性，在析构的时候都会把内存交还给上游分配器。所以这里不使用&lt;code&gt;deallocate&lt;/code&gt;也是可以的。&lt;strong&gt;一定要记住，所有解分配&lt;code&gt;deallocate&lt;/code&gt;都不会调用元素的析构函数&lt;/strong&gt;。所以要注意内存泄漏问题，使用这个技巧之前一定要清楚自己在做什么。&lt;/p&gt;
&lt;p&gt;我们来看个特殊例子，因为有时候容易错误理解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
    char buffer[1024];
    std::size_t buffer_size = 1024;
    {

        std::pmr::monotonic_buffer_resource buffer_resource{buffer, buffer_size, std::pmr::null_memory_resource()};
        //外层monotonic原始内存
        std::pmr::polymorphic_allocator&amp;lt;MyClass&amp;gt; allocator{ &amp;amp;buffer_resource }; //分配器
        {
            std::pmr::vector&amp;lt;MyClass&amp;gt; t(allocator); //分配器传入pmr vector
            t.reserve(1); //预留1个位置
            t.emplace_back(20); //原地构造元素
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;MyClass constructed with value: 20
MyClass destroyed with value: 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;奇怪？明明我们没有手动对内存资源进行操作，怎么会正确析构呢？因为这是&lt;code&gt;pmr::vector&lt;/code&gt;帮助我们做的。因为它毕竟还是&lt;code&gt;vector&lt;/code&gt;。还是会在&lt;code&gt;vector&lt;/code&gt;被销毁的时候帮助我们对元素进行析构。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;std::vector&amp;lt;T,Allocator&amp;gt;::~vector&lt;/code&gt;：调用元素的析构函数，然后解分配所用的存储。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;自定义分配器的相等比较&lt;/h2&gt;
&lt;p&gt;参考&lt;a href=&quot;https://stackoverflow.com/questions/47186641/why-c-custom-allocator-needs-comparison-operators&quot;&gt;来自这里&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[&lt;code&gt;operator ==(a1, a2)&lt;/code&gt;] returns true only if the storage allocated by the allocator &lt;code&gt;a1&lt;/code&gt; can be deallocated through &lt;code&gt;a2&lt;/code&gt;. Establishes reflexive, symmetric, and transitive relationship. Does not throw exceptions.
&lt;code&gt;operator ==(a1, a2)&lt;/code&gt;仅当分配器 &lt;code&gt;a1&lt;/code&gt; 分配的存储可以通过分配器 &lt;code&gt;a2&lt;/code&gt; 释放时才返回 true。建立了自反、对称和传递关系。不抛出异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一般来说，如果两个分配器相等，那么在移动一个容器至另一个容器的时候可以直接交换指针，在第二个容器重用第一个容器的内存。如果分配器不相等，则必须要在第二个容器中重新分配内存，然后复制每一个元素，然后释放第一个容器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;针对如何实现内存资源，和如何实现使用多态内存分配器的容器，可以看看下面参考资料的内容&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/v3dz-AKOVL8&quot;&gt;CppCon 2017针对如何实现内存资源和自己的使用分配器的容器&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/RLezJuqNcEQ&quot;&gt;CppCon 2019针对如何实现带有多态内存分配器的类和容器类&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/nZNd5FjSquk&quot;&gt;CppCon 2017理解并测试分配器&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/96089089&quot;&gt;游戏引擎新感觉&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/audi2/article/details/118252089&quot;&gt;CSDN的pmr&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.feabhas.com/2019/03/thanks-for-the-memory-allocator/&quot;&gt;分配器，内存资源，容器的关联&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/75669627/what-is-the-usage-of-the-template-parameter-of-the-stdpmrpolymorphic-allocat&quot;&gt;polymorphic allocator的模板参数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/74012811/what-is-the-difference-between-magically-winked-out-and-monotonic&quot;&gt;如何测试wink-out&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://reviews.llvm.org/D47090&quot;&gt;memory_source的LLVM源代码&lt;/a&gt; 和 &lt;a href=&quot;https://fossies.org/linux/gcc/libstdc++-v3/src/c++17/memory_resource.cc&quot;&gt;LLVM最终源代码&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Modern C++ Design 笔记</title><link>https://sterben-01.github.io/posts/ModernC++Design/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/ModernC++Design/</guid><pubDate>Fri, 03 Mar 2023 06:55:00 GMT</pubDate><content:encoded>&lt;p&gt;这本书有点儿老，好多和模板相关的都是脱裤子放屁&lt;/p&gt;
&lt;h1&gt;第一章 策略类&lt;/h1&gt;
&lt;p&gt;整个这一章讲的都是策略类的使用。这一部分分散在模板笔记当中。包括设计模式目前没有整理，后续会整理。&lt;/p&gt;
&lt;p&gt;核心要点就是让类可以定制化。当把一个类拆分为多个策略的时候，首先要把设计的功能模块抽离。同时要注意寻找正交的策略，也就是彼此之间无交互，可以独立更改的策略。&lt;/p&gt;
&lt;h1&gt;第二章 技术&lt;/h1&gt;
&lt;h2&gt;2.5 型别对型别的映射 （就是到底用不用SFINAE）&lt;/h2&gt;
&lt;p&gt;其实就是使用类似&lt;code&gt;enable_if&lt;/code&gt;来激活SFINAE。&lt;/p&gt;
&lt;p&gt;假设我们需要对&lt;code&gt;myobj&lt;/code&gt;对象进行特殊处理，如果使用嵌入式&lt;code&gt;enable_if&lt;/code&gt;会是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myobj{

};
template&amp;lt;typename T, typename U, typename = typename enable_if&amp;lt;is_same&amp;lt;U, myobj&amp;gt;::value&amp;gt;::type&amp;gt;
void create(const T&amp;amp; obj, const U&amp;amp; arg){
    std::puts(__PRETTY_FUNCTION__);
    cout &amp;lt;&amp;lt;&quot;myobj special&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T, typename U, typename = typename enable_if&amp;lt;!is_same&amp;lt;U, myobj&amp;gt;::value&amp;gt;::type, typename = int&amp;gt;
void create(const T&amp;amp; obj, const U&amp;amp; arg){
    std::puts(__PRETTY_FUNCTION__);
    cout &amp;lt;&amp;lt;&quot;normal&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    create(2,3);
    create(2,myobj{});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是老规矩，分析下过程。&lt;/p&gt;
&lt;p&gt;针对第一个，模板参数&lt;code&gt;T&lt;/code&gt;和&lt;code&gt;U&lt;/code&gt;被推导为&lt;code&gt;int&lt;/code&gt;和&lt;code&gt;int&lt;/code&gt;。然后发现&lt;code&gt;int&lt;/code&gt;和&lt;code&gt;myobj&lt;/code&gt;不同，所以第一个模板被SFINAE掉（因为为&lt;code&gt;false&lt;/code&gt;后&lt;code&gt;enable_if&lt;/code&gt;没有&lt;code&gt;type&lt;/code&gt;定义）。然后看第二个模板，第二个模板发现可以，所以选择第二个模板。&lt;/p&gt;
&lt;p&gt;针对第二个，一个道理。&lt;/p&gt;
&lt;p&gt;我们发现使用起来比较麻烦，如果我们能自己实现一个类似的呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myobj{
};

template&amp;lt;typename T&amp;gt;
struct typewrapper{
    using objtype = T;
};

template&amp;lt;typename T, typename U&amp;gt;
void anothercreate(const T&amp;amp; obj, typewrapper&amp;lt;U&amp;gt;){
    std::puts(__PRETTY_FUNCTION__);
    //void anothercreate(const T&amp;amp;, typewrapper&amp;lt;U&amp;gt;) [with T = int; U = int]
    cout &amp;lt;&amp;lt;&quot;normal&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T&amp;gt;
void anothercreate(const T&amp;amp; obj, typewrapper&amp;lt;myobj&amp;gt;){
    std::puts(__PRETTY_FUNCTION__);
    //void anothercreate(const T&amp;amp;, typewrapper&amp;lt;myobj&amp;gt;) [with T = int]
    cout &amp;lt;&amp;lt;&quot;myobj special&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    anothercreate(2,typewrapper&amp;lt;int&amp;gt;{});
    anothercreate(2,typewrapper&amp;lt;myobj&amp;gt;{});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;老规矩，分析一下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里没用到SFINAE。只是单纯的重载匹配。我们说过了SFINAE的三个要素：推导语境，失败和其他可行选项。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一个调用，模板参数&lt;code&gt;T&lt;/code&gt;和&lt;code&gt;U&lt;/code&gt;被推导为&lt;code&gt;int&lt;/code&gt; 和&lt;code&gt;int&lt;/code&gt;。匹配至第一个。&lt;strong&gt;因为第二个明显不符合类型要求。压根不存在SFINAE的情况，因为第二个重载模板从未被考虑过。因为第二个重载模板的第二个函数形参不在推导语境内。不在推导语境内，也没有错误发生，就不可能是SFINAE&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第二个调用，由于第二个重载模板比第一个更特化，所以调用第二个。针对第一个重载模板，此时虽然有推导语境，但是没有错误发生。所以也不存在SFINAE。&lt;/p&gt;
</content:encoded></item><item><title>More Effective C++ 笔记</title><link>https://sterben-01.github.io/posts/MoreEffectiveC++/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/MoreEffectiveC++/</guid><pubDate>Mon, 27 Feb 2023 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;基础议题&lt;/h1&gt;
&lt;h2&gt;条款4：非必要不提供默认构造&lt;/h2&gt;
&lt;p&gt;这一点可能和我们之前的认知有差异。但是也是正确的。原因是在语义上或者是设计上，针对某一些类型，如果我们不能提供一个初值来初始化一个对象，那么通过默认构造实例化出来的这个对象会是无意义的。&lt;/p&gt;
&lt;p&gt;当然了，还有很多类型是允许有默认对象的，比如空的容器之类的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以，如果类型展示出：从无到有生成对象是合理的 的语义，则应该有默认构造。&lt;/li&gt;
&lt;li&gt;但是如果类型展示出：必须有外来信息才能生成对象 的语义，则不应该有默认构造。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是为了表现出这种清晰的语义，会有诸多限制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当然，需要注意有些函数或容器强调参数或元素必须是可默认构造的。&lt;/li&gt;
&lt;li&gt;同时，在继承环境下，如果基类不是可默认构造的，那么就需要显式调用基类的构造。非常头疼。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;操作符&lt;/h1&gt;
&lt;h2&gt;条款8&lt;/h2&gt;
&lt;p&gt;查看memory3&lt;/p&gt;
&lt;h1&gt;效率&lt;/h1&gt;
&lt;h2&gt;条款17 考虑使用 缓式评估&lt;/h2&gt;
&lt;p&gt;缓式评估就行copy on write 写时复制一样。当我们调用拷贝构造的时候并不一定拷贝的副本立刻被使用，有可能从不使用或者是从不更改。所以此时可以单独的做一个标记，当真的对副本进行修改的时候再进行构造动作。&lt;/p&gt;
&lt;h2&gt;条款18 分期偿还预期的计算成本（超急评估）&lt;/h2&gt;
&lt;p&gt;其实就是和17反过来。如果某些大概率或一定会使用的数据，尤其当这些数据使用频繁的时候，尝试设计一种数据结构进行预加载，当做一种缓存。&lt;/p&gt;
&lt;h2&gt;条款21 考虑利用重载来避免隐式类型转换造成的临时对象。&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;我们在这一条中讨论的不是是否禁止隐式类型转换，主要是讨论如何降低开销。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设我们有这样的简朴的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myclass{
    int val;
    myclass(int x):val(x){
        cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
    };
};

const myclass operator+(const myclass&amp;amp; lhs, const myclass&amp;amp; rhs){
    return myclass(lhs.val + rhs.val);
}
int main(){
    myclass a(20);
    a+20;
    20+a;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码会一共构造五次。第一次是&lt;code&gt;a&lt;/code&gt;，第二次是&lt;code&gt;20&lt;/code&gt;的隐式类型转换。第三次是&lt;code&gt;a+20&lt;/code&gt;的返回值。第四次是&lt;code&gt;20&lt;/code&gt;的隐式类型转换，第五次是&lt;code&gt;20+a&lt;/code&gt;的返回值。&lt;/p&gt;
&lt;p&gt;我们此时可以&lt;strong&gt;多&lt;/strong&gt;写出两个重载的版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myclass operator+(const myclass&amp;amp; lhs, int rhs){
    return myclass(lhs.val + rhs);
}

const myclass operator+(int lhs, const myclass&amp;amp;  rhs){
    return myclass(lhs + rhs.val);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，执行代码就只会构造三次。分别是&lt;code&gt;a&lt;/code&gt;，&lt;code&gt;a+20&lt;/code&gt;和&lt;code&gt;20+a&lt;/code&gt;。没有隐式转换造成的临时对象的开销。&lt;/p&gt;
&lt;h2&gt;条款22 针对操作符，考虑同时提供复合形式(+=或-=)和单独形式（+，-）&lt;/h2&gt;
&lt;p&gt;主要原因是如&lt;code&gt;+=&lt;/code&gt;和&lt;code&gt;-=&lt;/code&gt;形式的符合操作符是直接作用于自身，所以返回的是&lt;code&gt;T&amp;amp;&lt;/code&gt;形式。但是单独形式的操作符一般都是&lt;code&gt;const T&lt;/code&gt;形式。所以显然前者效率较高。&lt;/p&gt;
&lt;p&gt;同时，应该以复合形式为基础，实现单独形式 。也就是在&lt;code&gt;operator+&lt;/code&gt;内部使用&lt;code&gt;operator+=&lt;/code&gt;。因为这样只需要更改&lt;code&gt;operator+=&lt;/code&gt;就可以改变其行为。&lt;/p&gt;
&lt;h2&gt;条款26 限制某个class所能产生的对象数量&lt;/h2&gt;
&lt;p&gt;主要讲了单例模式 和 对象数量计数器。需要计算数量的对象可以继承自对象数量计数器基类。行为有一点像侵入式智能指针。&lt;/p&gt;
&lt;p&gt;计数器基类大概长这样**（利用了CRTP）**：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct counts{
    static int count;
    void increase(){
        count++;
    }
    void decrease(){
        count--;
    }
    static void get(){
        cout &amp;lt;&amp;lt; count &amp;lt;&amp;lt; endl;
    }
};
template&amp;lt;typename T&amp;gt;
int counts&amp;lt;T&amp;gt;::count = 0;

class myobj : counts&amp;lt;myobj&amp;gt;{ //这是一种CRTP。
    //具体内容  
};
class myobjanother : counts&amp;lt;myobjanother&amp;gt;{
  //具体内容  
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既可以手动分离处理加减，也可以直接在计数器基类的构造或析构函数中计算加减。一个是手动一个是自动而已。&lt;/p&gt;
&lt;h2&gt;条款27 要求或禁止对象产生在堆中&lt;/h2&gt;
&lt;h3&gt;要求对象产生在堆中&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;将对象的析构函数声明为&lt;code&gt;protected&lt;/code&gt;。如果有继承，则必须要同时声明为&lt;code&gt;virtual&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;因为外部无法访问对象的析构函数，则编译器禁止在栈上创建对象&lt;/li&gt;
&lt;li&gt;声明为&lt;code&gt;protected&lt;/code&gt;而不是&lt;code&gt;private&lt;/code&gt;的目的是让子类可以访问析构函数。&lt;/li&gt;
&lt;li&gt;注意子类也应该将析构函数声明为&lt;code&gt;protected&lt;/code&gt;，否则子类对象会被允许创建在栈上。&lt;/li&gt;
&lt;li&gt;注意杂记4中提到的自动储存期限。所以子类如果在栈上，则子类的父类部分也在栈上。子类在堆上，则子类的父类部分也在堆上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;格外注意。如果将析构函数声明为&lt;code&gt;protected&lt;/code&gt;或&lt;code&gt;private&lt;/code&gt;，仅意味着不可在外部调用析构函数。但是在该类的成员函数中依旧可以调用析构函数，这意味着在该类的成员函数中，我们依旧可以将对象创建在栈上。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;声明一个&lt;code&gt;destroy&lt;/code&gt;函数，用于调用析构函数。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;destroy&lt;/code&gt;函数是否为&lt;code&gt;virtual&lt;/code&gt;不重要，因为&lt;code&gt;delete&lt;/code&gt;会调用对应的&lt;code&gt;virtual&lt;/code&gt;析构函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;map&amp;gt;
#include &amp;lt;iostream&amp;gt;
using namespace std;
struct myclass{

    myclass(){
        cout &amp;lt;&amp;lt;&quot;myclass const&quot; &amp;lt;&amp;lt; endl;
    }

    void destroy(){
        delete this;
    }

    void myclassmemberfunction(){ //成员函数
        myclass obj; //可以创建在栈上。因为成员函数可以访问到本类的所有成员
    }

    protected: //protected
    virtual ~myclass(){ //虚函数
        cout &amp;lt;&amp;lt;&quot;myclass dest&quot; &amp;lt;&amp;lt; endl;
    }
};


struct derive: public myclass{
    derive(){
        cout &amp;lt;&amp;lt; &quot;derive const&quot; &amp;lt;&amp;lt; endl;
    }
    void destroy(){
        delete this;
    }
    void derivememberfunction(){ //成员函数
        derive obj; //可以创建在栈上。因为成员函数可以访问到本类的所有成员
    }
    protected: //protected
    ~derive(){
        cout &amp;lt;&amp;lt;&quot;derive dest&quot; &amp;lt;&amp;lt; endl;
    }

};


int main(){

    myclass* p = new myclass();

    p-&amp;gt;myclassmemberfunction(); //成员函数
    p-&amp;gt;destroy();

    derive* pp = new derive();
    pp-&amp;gt;derivememberfunction(); //成员函数
    pp-&amp;gt;destroy();

    myclass* ppp = new derive();
    ppp-&amp;gt;destroy();

    myclass pppp; 	//禁止
    derive ppppp;	//禁止



    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;禁止对象产生在堆中&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;非常简单。只需要把&lt;code&gt;operator new&lt;/code&gt; 和&lt;code&gt; operator delete&lt;/code&gt;声明为&lt;code&gt;private&lt;/code&gt;即可。
&lt;ul&gt;
&lt;li&gt;也可以同时把&lt;code&gt;operator new[]&lt;/code&gt; 和&lt;code&gt; operator delete[]&lt;/code&gt;声明为&lt;code&gt;private&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct myclass{

    myclass(){
        cout &amp;lt;&amp;lt;&quot;myclass const&quot; &amp;lt;&amp;lt; endl;
    }
    ~myclass(){
        cout &amp;lt;&amp;lt;&quot;myclass dest&quot; &amp;lt;&amp;lt; endl;
    }

    private:
        static void* operator new(size_t size); //私有
        static void operator delete(void* ptr); //私有

};


struct derive: public myclass{
    derive(){
        cout &amp;lt;&amp;lt; &quot;derive const&quot; &amp;lt;&amp;lt; endl;
    }
    ~derive(){
        cout &amp;lt;&amp;lt;&quot;derive dest&quot; &amp;lt;&amp;lt; endl;
    }

};

int main(){
    derive obj;
    derive* p = new derive(); //错误
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;条款28 智能指针&lt;/h2&gt;
&lt;p&gt;都在智能指针章节。&lt;/p&gt;
&lt;h2&gt;条款29 引用计数&lt;/h2&gt;
&lt;p&gt;大概和智能指针的引用计数理论一致。额外的就是COW相关的实现。还有就是&lt;code&gt;operator[]&lt;/code&gt;的语义，这部分写在effSTL的map的&lt;code&gt;[]&lt;/code&gt;部分了。&lt;/p&gt;
&lt;h2&gt;条款 30 代理类&lt;/h2&gt;
&lt;p&gt;STL的例子是&lt;code&gt;vector&amp;lt;bool&amp;gt;&lt;/code&gt;参考effective STL 19条&lt;/p&gt;
&lt;p&gt;书里的例子是尝试用别的方法实现重载&lt;code&gt;operator[][]&lt;/code&gt;的语义。因为压根没有&lt;code&gt;operator[][]&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct arr2{
    struct arr1{ //代理类
        arr1() = default;
        arr1(int x, int y):pivot(x), pivot2(y){};  //假设每个arr1拥有两个元素
        int pivot = 20;
        int pivot2 = 30;
        int&amp;amp; operator[](size_t index){ //返回实际元素的引用，满足赋值要求。
            cout &amp;lt;&amp;lt;&quot;arr1 []&quot; &amp;lt;&amp;lt; endl;
            if(index == 0){
                return pivot;
            }
            else{
                return pivot2;
            }
        }
    };
    vector&amp;lt;arr1&amp;gt; arrs{arr1(1,2),arr1(3,4), arr1(5,6)};//我们假设arr2拥有三个arr1
    arr2() = default;
    arr1&amp;amp; operator[](size_t index){ //代理operator[]，直接返回对应下标的arr1对象的引用。满足赋值要求
        cout &amp;lt;&amp;lt;&quot;arr2 []&quot; &amp;lt;&amp;lt; endl;
        return arrs[index];
    }
};

int main(){
    arr2 obj;
    cout &amp;lt;&amp;lt; obj[2][1] &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; (obj.operator[](2)).operator[](1) &amp;lt;&amp;lt; endl; //等同于上面


    obj[2][1] = 300;
    cout &amp;lt;&amp;lt; obj[2][1] &amp;lt;&amp;lt; endl; //赋值也没问题

    arr2::arr1 temp = obj[2]; //单独提取出第一维对象也没问题。
    //第一个operator[]返回的是arr1对象。所以如果再次链式调用operator[]就会自然匹配到arr1的那个而不是arr2的
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们想模拟二维数组的语义，但是这里模拟的还是有问题。图一乐就行&lt;/p&gt;
&lt;p&gt;&lt;code&gt;obj[2][1]&lt;/code&gt;的语义是&lt;code&gt;(obj.operator[](2)).operator[](1);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们核心想法是让第一层&lt;code&gt;arr2&lt;/code&gt;储存一堆的&lt;code&gt;arr1&lt;/code&gt;。然后&lt;code&gt;arr2&lt;/code&gt;的&lt;code&gt;operator[]&lt;/code&gt;会返回对应的&lt;code&gt;arr1&lt;/code&gt;对象。此时，如果链式调用，&lt;strong&gt;第二个&lt;code&gt;operator[]&lt;/code&gt;自然会是&lt;code&gt;arr1&lt;/code&gt;的。因为当前的operator&lt;code&gt;[]&lt;/code&gt;是作用在&lt;code&gt;arr2&lt;/code&gt;的&lt;code&gt;operator[]&lt;/code&gt;返回的&lt;code&gt;arr1&lt;/code&gt;上面的那个&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;让operator[]可以区分左值和右值（不是真正区分，而是区分行为）&lt;/h3&gt;
&lt;p&gt;一般来说，左值是写入行为，右值是读取行为。这个例子只能用书里的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class String {
public:
    //代理类用于区分operator[]的读写操作
    class CharProxy { // proxies for string chars
    public:
        CharProxy(String&amp;amp; str, int index); // creation
        CharProxy&amp;amp; operator=(const CharProxy&amp;amp; rhs); // 拷贝赋值。左值运用的场景，这种适用于 s1[3] = s2[8]的场景
        CharProxy&amp;amp; operator=(char c); // 拷贝赋值。左值运用的场景，这种适用于 s2[5] = &apos;x&apos;的场景
        operator char() const;  //右值运用的场景。
    private:
        String&amp;amp; theString; //用于操作String,并在适当时机开辟新内存并复制
        int charIndex;
    };
    const CharProxy operator[](int index) const; // string类的 operator[]重载
    CharProxy operator[](int index); // string类的 operator[]重载
    ...
    friend class CharProxy;
private:
    RCPtr&amp;lt;StringValue&amp;gt; value;//见条款29
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;就像书里的例子。&lt;code&gt;s2[5] = &apos;x&apos;;&lt;/code&gt;由于下标访问运算符重载为返回一个代理对象，所以此时的赋值会调用代理类对象的赋值。也就是&lt;code&gt;CharProxy&amp;amp; operator=(char c);&lt;/code&gt;这个函数。这时候&lt;code&gt;s2[5]&lt;/code&gt;的语义是左值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;针对&lt;code&gt;s1[3] = s2[8]&lt;/code&gt;这个场景，虽然我们知道&lt;code&gt;s2[8]&lt;/code&gt;没有写入，应该是个右值。但是注意，因为书中String实现了写时复制，所以&lt;code&gt;operator[]&lt;/code&gt;本身是无成本的。因为代理对象只保有数据的引用。随后直到调用了左侧的代理对象的&lt;code&gt;CharProxy&amp;amp; operator=(const CharProxy&amp;amp; rhs); &lt;/code&gt;这个函数，这时候才开始相应的动作。所以这里我们本身无法区分左值和右值，但是我们可以从行为上区分左值和右值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;String::CharProxy&amp;amp; String::CharProxy::operator=(const CharProxy&amp;amp; rhs)
{
    if (theString.value-&amp;gt;isShared()) {
        theString.value = new StringValue(theString.value-&amp;gt;data);
    }
    theString.value-&amp;gt;data[charIndex] = rhs.theString.value-&amp;gt;data[rhs.charIndex];
    return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;针对如&lt;code&gt;cout &amp;lt;&amp;lt; s1[2];&lt;/code&gt;这种场景，非常明确&lt;code&gt;s1[2]&lt;/code&gt;的行为是右值行为，也就是读取行为。所以说压根没有必要做出任何的额外成本动作&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;String::CharProxy::operator char() const
{
return theString.value-&amp;gt;data[charIndex]; //单纯的返回一个字符。这个data的类型是一个char*数组。所以这个下标访问是内置的。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;潜在的问题是代理类非常复杂，常常会造成语义的改变。因为目标对象和代理对象的行为常常有细微差异。比如在上文的例子中&lt;code&gt;char* p = &amp;amp;s[2]&lt;/code&gt;就无法通过。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为取出来的地址的类型是代理类类型。无法赋值给&lt;code&gt;char&lt;/code&gt;类。由于我们目标类的&lt;code&gt;operator[]&lt;/code&gt;返回的是代理类对象，所以这时候我们必须重载代理类的取地址运算符&lt;code&gt;operator&amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是这不能解决所有问题。假设有一个类&lt;code&gt;A&lt;/code&gt;引用了上面的这个蕴含代理类的目标对象，那么直接针对这个&lt;code&gt;A&lt;/code&gt;使用目标对象的&lt;code&gt;operator[]&lt;/code&gt;依旧返回的是代理类对象。此时如果我们想进行函数调用，那么就会出现问题。因为取回来的并不是类&lt;code&gt;A&lt;/code&gt;对象，代理类对象并没有这个特定的成员函数。所以需要在所有的函数上都进行重载让他们也适用于代理类对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时，我不知道为啥上面的目标类的&lt;code&gt;operator[]&lt;/code&gt;一定要返回代理类的对象而不是代理类的引用。则此时在赋值方面会出现问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后一个问题是隐式类型转换。隐式类型转换中，每一个步骤都只能执行一次。也就是每个步骤都只能进行一个层次的转换。比如&lt;code&gt;a&lt;/code&gt;可以由&lt;code&gt;int&lt;/code&gt;构造，&lt;code&gt;b&lt;/code&gt;可以由&lt;code&gt;a&lt;/code&gt;构造。那么如果一个函数接受一个&lt;code&gt;b&lt;/code&gt;类对象，可以传入&lt;code&gt;a&lt;/code&gt;，但是传入&lt;code&gt;int&lt;/code&gt;就不可以。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;条款31 让函数根据一个以上的参数类型来决定如何虚化&lt;/h2&gt;
&lt;p&gt;主要讲述的是multi-dispatch。&lt;/p&gt;
&lt;p&gt;我们知道，一般来说多态是单个参数的。也就是依靠调用方的多态来决定调用哪个类的函数。那么如何实现多重派发呢（也就是同时根据调用方和入参来进行多态调用）？&lt;/p&gt;
&lt;p&gt;第一个方法是虚函数+RTTI。&lt;/p&gt;
&lt;p&gt;这个办法非常简陋。也就是先设计一层虚函数，然后再对应的虚函数内使用typeid判断，随后执行对应的函数。这种做法效率非常的低，并且伴随着一大堆的cast&lt;/p&gt;
&lt;p&gt;第二个方法就是只使用虚函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct banana; //前向声明
struct apple;
struct kiwi;

struct fruit{ //抽象基类
    virtual void blend(fruit* obj) = 0; //通过该函数决定每一个类的二次派发。
    virtual void blend(banana* obj) = 0;
    virtual void blend(apple* obj) = 0;
    virtual void blend(kiwi* obj) = 0;
};

struct banana:fruit{
    virtual void blend(fruit* obj){ //通过该函数决定每一个类的二次派发。
        obj-&amp;gt;blend(this); //注意切记不可写反写成this-&amp;gt;blend(obj)。会造成递归调用。
    }
    virtual void blend(banana* obj){
        cout &amp;lt;&amp;lt;&quot;banana blend banana&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void blend(apple* obj){
        cout &amp;lt;&amp;lt;&quot;banana blend apple&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void blend(kiwi* obj){
        cout &amp;lt;&amp;lt;&quot;banana blend kiwi&quot; &amp;lt;&amp;lt; endl;
    }
};

struct apple:fruit{
    virtual void blend(fruit* obj){
        obj-&amp;gt;blend(this);
    }
    virtual void blend(banana* obj){
        cout &amp;lt;&amp;lt;&quot;apple blend banana&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void blend(apple* obj){
        cout &amp;lt;&amp;lt;&quot;apple blend apple&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void blend(kiwi* obj){
        cout &amp;lt;&amp;lt;&quot;apple blend kiwi&quot; &amp;lt;&amp;lt; endl;
    }
};
struct kiwi:fruit{
    virtual void blend(fruit* obj){
        obj-&amp;gt;blend(this);
    }
    virtual void blend(banana* obj){
        cout &amp;lt;&amp;lt;&quot;kiwi blend banana&quot; &amp;lt;&amp;lt; endl;

    }
    virtual void blend(apple* obj){
        cout &amp;lt;&amp;lt;&quot;kiwi blend apple&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void blend(kiwi* obj){
        cout &amp;lt;&amp;lt;&quot;kiwi blend kiwi&quot; &amp;lt;&amp;lt; endl;
    }
};


int main(){

    fruit* o = new banana();

    fruit* k = new kiwi();
    o-&amp;gt;blend(k); //kiwi blend banana


    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到我们通过两层虚函数来实现两层派发。具体原理是&lt;code&gt;o-&amp;gt;blend(k)&lt;/code&gt;的&lt;code&gt;o&lt;/code&gt;是第一层派发。通过&lt;code&gt;o&lt;/code&gt;的动态类型决定进入对应的类。由于此时我们传入的指针类型是fruit。由于只能子转父，不能父转子，所以必须要有&lt;code&gt;void blend(fruit* obj)&lt;/code&gt;这个东西帮助我们进行第二层派发。&lt;code&gt;o&lt;/code&gt;的动态类型此时是&lt;code&gt;banana&lt;/code&gt;，所以此时进入了&lt;code&gt;banana&lt;/code&gt;的&lt;code&gt;blend&lt;/code&gt;函数。在这个函数中，我们的&lt;code&gt;obj-&amp;gt;(this)&lt;/code&gt;的&lt;code&gt;obj&lt;/code&gt;就是第二层派发。根据&lt;code&gt;obj&lt;/code&gt;的动态类型决定进入对应的类的&lt;code&gt;blend&lt;/code&gt;函数。此时&lt;code&gt;obj&lt;/code&gt;的动态类型是&lt;code&gt;kiwi&lt;/code&gt;。所以此时是调用了&lt;code&gt;kiwi&lt;/code&gt;类的&lt;code&gt;blend&lt;/code&gt;函数。同时我们知道，每个类的&lt;code&gt;this&lt;/code&gt;指针的“类型”都是本类类型。因为它永远指向自己。所以此时&lt;code&gt;this&lt;/code&gt;的类型是&lt;code&gt;banana&lt;/code&gt;。所以最后我们调用了&lt;code&gt;kiwi&lt;/code&gt;类的&lt;code&gt;banana&lt;/code&gt;为参数的函数。&lt;/p&gt;
&lt;p&gt;问题在于这种方案非常的不符合设计理念。因为只要我们增加了一个对应的水果，就要修改每个类型。&lt;/p&gt;
</content:encoded></item><item><title>Effective C++ 笔记</title><link>https://sterben-01.github.io/posts/EffectiveC++/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/EffectiveC++/</guid><pubDate>Sat, 25 Feb 2023 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;条款25 swap函数的实现方式&lt;/h1&gt;
&lt;p&gt;具体原因没啥好说的，都能理解。概括一下使用方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当标准库的&lt;code&gt;std::swap&lt;/code&gt;对某种自定义类型效率很低的时候，提供一个&lt;code&gt;swap&lt;/code&gt;成员函数，并且确定该函数不抛出异常。
&lt;ul&gt;
&lt;li&gt;具体自定义类应该使用pimpl手法，也就是保有资源指针。所以成员&lt;code&gt;swap&lt;/code&gt;仅仅需要交换指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;由于通常来说，我们的保有资源指针是&lt;code&gt;private&lt;/code&gt;的，所以应该再提供一个非成员函数的&lt;code&gt;swap&lt;/code&gt;，让这个&lt;code&gt;swap&lt;/code&gt;调用那个非成员函数的&lt;code&gt;swap&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对于非模板类类型，可以考虑特化&lt;code&gt;std::swap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;针对类模板的非成员&lt;code&gt;swap&lt;/code&gt;函数，应该使用&lt;code&gt;using std::swap&lt;/code&gt;引入标准库的&lt;code&gt;swap&lt;/code&gt;函数。
&lt;ul&gt;
&lt;li&gt;这样在&lt;code&gt;T&lt;/code&gt;类有合适的swap的时候，自然会使用&lt;code&gt;T&lt;/code&gt;类自己的&lt;code&gt;swap&lt;/code&gt;。如果&lt;code&gt;T&lt;/code&gt;类没有自己的&lt;code&gt;swap&lt;/code&gt;函数，则会匹配至&lt;code&gt;std::swap&lt;/code&gt;标准库的&lt;code&gt;swap&lt;/code&gt;函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最后，调用&lt;code&gt;swap&lt;/code&gt;的时候不要加任何命名空间修饰符。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第五章&lt;/h1&gt;
&lt;p&gt;26条：延后变量的定义，并且尽量直接构造对象而不是先默认构造再赋值。&lt;/p&gt;
&lt;p&gt;27条：少进行类型转换。因为很多时候可以通过重新设计避免类型转换。&lt;/p&gt;
&lt;p&gt;28条：注意避免返回可以指向某一个对象内部的指针，引用，或迭代器（统一称之为句柄或handler）。因为很多成员变量可能是private的，我们在一个成员函数直接给这个东西返回了个引用，就破坏了封装性。一是本来不应被更改的可能被更改，二来本来可见的变得可见了。同时可能发生悬空指针或悬空引用。&lt;/p&gt;
&lt;p&gt;31条：降低编译依赖。这个就是到底是用&lt;code&gt;include&lt;/code&gt;还是用前向声明。这个杂记3里有。&lt;/p&gt;
&lt;h1&gt;第六章&lt;/h1&gt;
&lt;p&gt;整个第六章讲的都是类的设计相关。这里不分别叙述，写在一起&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;32条：明确&lt;code&gt;public&lt;/code&gt;继承是is-a关系。意味着适用于基类每一件事情都适用于派生类身上。因为每一个派生类对象也都是一个基类对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题在于比如正方形继承自长方形，就会有问题。比如长方形的长宽可以不相等。正方形必须相等。假如我们在变更长方形的长或宽后检查了其长款不等，则这件事情不适用于正方形，因为正方形的长和宽永远相等。所以在这件事情上，就会出现问题，需要细细考虑。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;33条：主要是讲的继承中的隐藏（重定义）。这个在&lt;code&gt;vptr&lt;/code&gt;有提到。主要是使用&lt;code&gt;using&lt;/code&gt;引入父类的函数或变量名使其在子类作用域中可见。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;34条：主要是区分继承接口（抽象基类）和继承实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主要是研究非虚函数，虚函数和纯虚函数的意义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非虚函数的目的是为了让所有派生类继承这个函数的接口和一份强制（可能是缺省）的实现。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;因为非虚函数的意义是无论派生类多么特殊，或有多么不同的行为，这个非虚函数所展现出的事情是不应该被改变的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在public继承之下，派生类总是继承基类的接口（抽象基类）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个纯虚函数的目的是为了让派生类只继承其接口而不继承其实现。因为子类必须提供纯虚函数的一份实现。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;至于提供某一个函数的缺省实现，既可以使用一个额外的非虚函数，也可以直接给纯虚函数提供一份实现。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这样在需要使用缺省设置的时候，可以直接使用&lt;code&gt;对象.抽象基类::函数()&lt;/code&gt;来使用缺省的函数实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;35条：很多时候满足多态需求并不一定需要依靠虚函数，也可以依靠某些设计模式。&lt;strong&gt;（这里所谓的“不依靠虚函数”，不代表不使用虚函数。一定要注意。）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板方法模式。这个和模板没有任何关联。详细会在设计模式笔记介绍。
&lt;ul&gt;
&lt;li&gt;更具体点，模板方法模式的一个特殊形式是NVI手法，也就是non-virtual interface。让外部调用一个公有非虚函数。但是在非虚函数内对虚函数（可能是私有的）进行调用。&lt;/li&gt;
&lt;li&gt;优点是可以在调用虚函数的之前和之后进行一些预处理或善后工作。比如加锁解锁，记录日志，验证条件等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;策略模式。这个在模板中的19.2提到过。详细会在设计模式继续补充。
&lt;ul&gt;
&lt;li&gt;模板版本的策略模式和普通的策略模式（保有一个策略类的指针）的最大区别是能否被替换。模板版本相当于把类型信息加入了对象当中，自然不可替换。但是普通的策略模式可以随着使用的时候进行替换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;36条：绝对不要重新定义（隐藏）继承而来的非虚函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;37条：绝对不要重新定义一个继承而来的虚函数的默认参数。因为默认参数是静态绑定，虚函数本身则是动态绑定。杂记2中有详细说明&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;38条：has-a关系不仅表示包含，也可以表示：根据某物实现出&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第七章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;41条：显式接口基于函数签名，动态多态基于虚函数。隐式接口基于模板，基于有效表达式，静态多态基于函数重载。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;43条：就是继承类模板产生的名称依赖问题。深度探索对象模型的7.1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;44条：**由于模板会导致代码膨胀，**也就是&lt;code&gt;T&amp;lt;int,3&amp;gt;&lt;/code&gt;，&lt;code&gt;T&amp;lt;int, 4&amp;gt;&lt;/code&gt;，和&lt;code&gt;T&amp;lt;long, 4&amp;gt;&lt;/code&gt;是3种类型，则会生成三份类或函数的实例。尤其是在类模板中，&lt;strong&gt;假设某个针对这些类型（或所有类型）的操作是通用的，则应该以适当的方式把这部分相同的操作抽离出来。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct same{ //抽离公共部分
    protected:
    void same_way(){
        cout &amp;lt;&amp;lt;&quot;process&quot; &amp;lt;&amp;lt; endl;
    }
};

template&amp;lt;typename T, unsigned int N&amp;gt;
struct A : same&amp;lt;T&amp;gt;{
    void diff_way(){
        std::puts(__PRETTY_FUNCTION__);
    }
    void same_way_wrapper(){
        this-&amp;gt;same_way(); //必须使用this或::引入依赖名。条款43
    }
};

int main(){
    A&amp;lt;int, 2&amp;gt; obj;
    A&amp;lt;int, 3&amp;gt; obj2;

    obj.diff_way();
    obj.same_way_wrapper();
    obj2.diff_way();
    obj2.same_way_wrapper();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这样写的话，&lt;code&gt;A&lt;/code&gt;会照常实例化两份&lt;code&gt;A&amp;lt;int,2&amp;gt;&lt;/code&gt; 和&lt;code&gt; A&amp;lt;int,3&amp;gt;&lt;/code&gt;，但是&lt;code&gt;same&lt;/code&gt;只会实例化一份。也就是&lt;code&gt;same&amp;lt;int&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;尽管我们可能会说，&lt;code&gt;same_way_wrapper&lt;/code&gt;依旧实例化了两份。但是我们要知道，真实场景下，&lt;code&gt;same_way&lt;/code&gt;可能是几十几百行的函数。而&lt;code&gt;same_way_wrapper&lt;/code&gt;只有一行，几百行和一行相比，这个膨胀率我们是可以接受的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;继承只是一种实现方式。我们可以换成包含。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;46条：函数模板的类型推导不支持隐式类型转换。所以如果我们在写一个类模板，但是其中有函数需要类型转换的时候，需要写成类内部的&lt;code&gt;friend&lt;/code&gt;函数。&lt;/h2&gt;
&lt;p&gt;这一条非常值的拿出来单独一说。这个东西的例子最常见在运算符重载&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通类的运算符重载&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct myclass{
    int val = 0;
    myclass(int x):val(x){};
    myclass operator+(const myclass&amp;amp; rhs){
            return myclass(val + rhs.val);
    }
};
//---类外版本----
// myclass operator+(const myclass&amp;amp; lhs, const myclass&amp;amp; rhs){
//         return myclass(lhs.val + rhs.val);
// }
int main(){
    myclass obj(200);
    myclass another = obj+400;
    cout &amp;lt;&amp;lt; another.val &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;类模板的运算符重载&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct myTclass{
    int val = 0;
    myTclass(int x):val(x){};
    myTclass operator+(const myTclass&amp;amp; rhs){ //类内版本 OK
        return myTclass(val + rhs.val);
    }
};
//----类外版本，注意这里是错的-----
// template&amp;lt;typename T&amp;gt;
// myTclass&amp;lt;T&amp;gt; operator+(const myTclass&amp;lt;T&amp;gt;&amp;amp; lhs, const myTclass&amp;lt;T&amp;gt;&amp;amp; rhs){
//     return myTclass&amp;lt;T&amp;gt;(lhs.val + rhs.val);
// }
int main(){
    myTclass&amp;lt;int&amp;gt; obj(200);
    myTclass&amp;lt;int&amp;gt; another = obj + 400;
    cout &amp;lt;&amp;lt; another.val &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至于运算符重载，尤其是当前是加法的时候，为了满足加法交换律，我们普遍会需要在类外写一个。但是问题来了。我们类内定义的没问题，因为不涉及类型转换。&lt;strong&gt;但是类外的就出问题了。因为如&lt;code&gt;obj+400&lt;/code&gt;如果和类外的运算符重载进行匹配，&lt;code&gt;400&lt;/code&gt;是不能被隐式转换成&lt;code&gt;myTclass&amp;lt;int&amp;gt;(400)&lt;/code&gt;的。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;operator+(obj, 400)&lt;/code&gt;这个函数调用从第一个参数可以推导出&lt;code&gt;T&lt;/code&gt;的类型是&lt;code&gt;int&lt;/code&gt;，因为第一个参数的类型是&lt;code&gt;myTclass&amp;lt;int&amp;gt;&lt;/code&gt;。由于在函数签名中，两个参数类型一致，所以期望第二个参数依旧是这个类型。但是突然出现了&lt;code&gt;int&lt;/code&gt;，这两个参数类型对不上了。又不支持隐式转换。（想不通的话想一下最基础的例子：&lt;code&gt;max&amp;lt;T&amp;gt;(1,2.2)&lt;/code&gt;为什么也不行）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以解决方式是以一个友元函数的身份写在类内即可。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct myTclass{
    int val = 0;
    myTclass(int x):val(x){};
    friend myTclass&amp;lt;T&amp;gt; operator+(const myTclass&amp;lt;T&amp;gt;&amp;amp; lhs, const myTclass&amp;lt;T&amp;gt;&amp;amp; rhs){ //这里的三个&amp;lt;T&amp;gt;都是可以省略的。但是为了清晰，还是加上吧。
        return myTclass(lhs.val + rhs.val);
    }
};
int main(){
    myTclass&amp;lt;int&amp;gt; obj(200);
    myTclass&amp;lt;int&amp;gt; another = obj + 400;
    cout &amp;lt;&amp;lt; another.val &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**这么做之所以可行的原因是这个操作符重载是普通函数，并非函数模板。**它依托于整个类模板。一旦类模板被实例化，则函数也会被合成。所以当&lt;code&gt;myTclass&amp;lt;int&amp;gt; obj&lt;/code&gt;的执行让&lt;code&gt;myTclass&amp;lt;int&amp;gt;&lt;/code&gt;类被合成出来的时候，这个函数就已经可见了。然后由于是函数而非函数模板自然支持隐式类型转换。&lt;strong&gt;注意不要因为看到模板类就觉得不可能发生隐式类型转换。编译器通过模板合成出来的类或函数与普通类或函数有同样的行为。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++ 模板笔记（八）：类型属性检查、SFINAE 陷阱与编译期计算</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-8/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-8/</guid><pubDate>Wed, 14 Dec 2022 14:57:00 GMT</pubDate><content:encoded>&lt;h1&gt;几种类型属性和受支持操作&lt;/h1&gt;
&lt;h2&gt;std::is_copy_constructible / std::is_trivially_copy_constructible / std::is_trivially_copyable&lt;/h2&gt;
&lt;p&gt;这几种比较看起来容易混淆的概念其实非常好理解。从字面意义上的好理解。&lt;/p&gt;
&lt;h3&gt;std::is_trivially_copyable&lt;/h3&gt;
&lt;p&gt;用来检查某个型别是否是可平凡复制的。可平凡复制简而言之就是可以用memcpy这种按字节进行底层逐位拷贝而没有问题的。平凡的意思可以直接去cppreference看。核心来说，平凡的XX函数的含义基本都是这个函数不是由用户提供的。&lt;/p&gt;
&lt;h3&gt;std::is_trivially_copy_constructible&lt;/h3&gt;
&lt;p&gt;用来检查某个型别是否有一个平凡的拷贝构造。所以说只要满足&lt;code&gt;is_trivially_copyable&lt;/code&gt;，就一定满足&lt;code&gt;is_trivially_copy_constructible&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;std::is_copy_constructible&lt;/h3&gt;
&lt;p&gt;检查某个型别是否有个可访问的拷贝构造。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;所以说这三种从上到下，是拷贝类型限制的从严格到宽松。&lt;/p&gt;
&lt;h2&gt;std::is_destructible / std::is_trivially_destructible / std::is_nothrow_destructible&lt;/h2&gt;
&lt;h3&gt;std::is_trivially_destructible&lt;/h3&gt;
&lt;p&gt;有平凡的析构函数&lt;/p&gt;
&lt;h3&gt;std::is_nothrow_destructible&lt;/h3&gt;
&lt;p&gt;析构函数不抛出异常&lt;/p&gt;
&lt;h3&gt;std::is_destructible&lt;/h3&gt;
&lt;p&gt;有可访问的析构函数&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;这一套组更加清晰直观。我们可能只需要知道怎么用一下。一般来说，如果对象是可平凡析构的。那么压根不用调用析构函数。因为析构函数啥也不干。所以在某个实现了&lt;code&gt;std::optional&lt;/code&gt;的代码中我们可以看到这种代码段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T, bool = ::std::is_trivially_destructible&amp;lt;T&amp;gt;::value&amp;gt;
struct optional_storage_base {
//....其他代码
  ~optional_storage_base() {
    if (m_has_value) {
      m_value.~T(); //显式调用伪析构函数。学名就叫伪析构函数。
      m_has_value = false;
    }
  }
//....其他代码
};
// 可平凡析构的特化。
template &amp;lt;class T&amp;gt; 
struct optional_storage_base&amp;lt;T, true&amp;gt; {
//....其他代码
// No destructor, so this class is trivially destructible
//....其他代码
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;延伸&lt;/h2&gt;
&lt;p&gt;来自PVS Studio的这篇文章的&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1193/&quot;&gt;Program execution: trivial types and ABI章节&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;trivially_constructible 意味着不需要初始化某些东西。&lt;/li&gt;
&lt;li&gt;trivially_destructible 意味着不需要生成析构函数代码。&lt;/li&gt;
&lt;li&gt;trivially_copyable 意味着除了按字节复制以外不需要做任何其他事情。&lt;/li&gt;
&lt;li&gt;trivially_movable 表达的含义和trivially_copyable 一样，只不过是在移动语境下。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;检查是否有用户定义的swap&lt;/h1&gt;
&lt;p&gt;出自&lt;a href=&quot;https://github.com/TartanLlama/optional/blob/master/include/tl/optional.hpp&quot;&gt;一个用C++11实现的std::optional&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct tag {};

template &amp;lt;class T&amp;gt;
tag swap(T &amp;amp;, T &amp;amp;);
template &amp;lt;class T, std::size_t N&amp;gt;
tag swap(T (&amp;amp;a)[N], T (&amp;amp;b)[N]);

template &amp;lt;class, class&amp;gt;
std::false_type uses_std(...);
template &amp;lt;class T, class U&amp;gt;
std::is_same&amp;lt;decltype(swap(std::declval&amp;lt;T &amp;amp;&amp;gt;(), std::declval&amp;lt;U &amp;amp;&amp;gt;())), tag&amp;gt; uses_std(int);


struct myclass {
    int x;
};

void swap(myclass &amp;amp;, myclass &amp;amp;) { std::cout &amp;lt;&amp;lt; &quot;1&quot; &amp;lt;&amp;lt; std::boolalpha; }
int main() {
    int a = 1;
    int b = 2;
    std::cout &amp;lt;&amp;lt; std::boolalpha;
    std::cout &amp;lt;&amp;lt; decltype(swap_adl_tests::uses_std&amp;lt;int, int&amp;gt;(0))::value &amp;lt;&amp;lt; std::endl;
    std::cout &amp;lt;&amp;lt; decltype(swap_adl_tests::uses_std&amp;lt;myclass, myclass&amp;gt;(0))::value &amp;lt;&amp;lt; std::endl;
    swap(a, b);
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一个模板合成的&lt;code&gt;swap&lt;/code&gt;函数。如果有用户定义的&lt;code&gt;swap&lt;/code&gt;函数，那么模板合成的优先级低于用户定义的。用户定义的&lt;code&gt;swap&lt;/code&gt;不可能返回&lt;code&gt;tag&lt;/code&gt;标签类型。所以如果返回的不是&lt;code&gt;tag&lt;/code&gt;类型，标志着不能用&lt;code&gt;std::swap&lt;/code&gt;。之所以能找到是因为用了ADL。&lt;/p&gt;
&lt;p&gt;相反。如果没有用户定义的&lt;code&gt;swap&lt;/code&gt;，我们既没有&lt;code&gt;using namespace std&lt;/code&gt;， 也没有&lt;code&gt;using std::swap&lt;/code&gt;， 我们也是用无限定名字的&lt;code&gt;swap&lt;/code&gt;调用。所以说他看不到&lt;code&gt;std::swap&lt;/code&gt;，则肯定只能找到合成的返回&lt;code&gt;tag&lt;/code&gt;类型的&lt;/p&gt;
&lt;h1&gt;获取可变参数包中的最后一个元素&lt;/h1&gt;
&lt;p&gt;简单粗暴&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto last2 = [](auto... args) {
  return (args, ...);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来自&lt;a href=&quot;https://github.com/wanghenshui/cppweeklynews/blob/dev/posts/036.md&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;检查是否是某个类型的特化&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename...&amp;gt; typename Template&amp;gt;
struct is_specialization : std::false_type {};

template &amp;lt;template &amp;lt;typename...&amp;gt; typename Template, typename... Args&amp;gt;
struct is_specialization&amp;lt;Template&amp;lt;Args...&amp;gt;, Template&amp;gt; : std::true_type {
    void info() { cout &amp;lt;&amp;lt; __PRETTY_FUNCTION__ &amp;lt;&amp;lt; endl; }
};

int main() {
    static_assert(is_specialization&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;gt;{}, &quot;&quot;);
    is_specialization&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;gt;{}.info();
    return 0;
}

/*
void is_specialization&amp;lt;Template&amp;lt;Args ...&amp;gt;, Template&amp;gt;::info() [with Template = std::vector; Args = {int, std::allocator&amp;lt;int&amp;gt;}]
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模板模板参数&lt;code&gt;template&lt;/code&gt;就是&lt;code&gt;vector&lt;/code&gt;，&lt;code&gt;vector&lt;/code&gt;里面的参数就是&lt;code&gt;Args&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;SFINAE、enable_if 和 enable_if_t 带来的陷阱&lt;/h1&gt;
&lt;p&gt;本篇文章来自&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1160/&quot;&gt;这里&lt;/a&gt;的&lt;em&gt;Broken syntax and standard library: std::enable_if_t vs. std::void_t&lt;/em&gt;一节&lt;/p&gt;
&lt;p&gt;先来看一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct X {
    struct Inner {};
};

struct Y {
    struct Outer {};
};
template &amp;lt;class T&amp;gt;
decltype(void(std::declval&amp;lt;typename T::Inner&amp;gt;())) fun(T) {  // 1
    std::cout &amp;lt;&amp;lt; &quot;f1\n&quot;;
}

template &amp;lt;class T&amp;gt;
decltype(void(std::declval&amp;lt;typename T::Outer&amp;gt;())) fun(T) {  // 2
    std::cout &amp;lt;&amp;lt; &quot;f2\n&quot;;
}
int main(){
    X x;
    Y y;
    fun(x);   // 应该输出 &quot;fun1&quot;
    fun(y);   // 应该输出 &quot;fun2&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在“上古”年代，我们会通过这种质朴而纯真的方法来触发SFINAE。一切看起来还可以。直到后面我们有了&lt;code&gt;std::enable_if&lt;/code&gt;，陷阱出现了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt;
std::enable_if_t&amp;lt;std::is_same_v&amp;lt;typename T::Inner, typename T::Inner&amp;gt;&amp;gt; fun1(T) {  // 1
    std::cout &amp;lt;&amp;lt; &quot;f1\n&quot;;
}

template &amp;lt;class T&amp;gt;
std::enable_if_t&amp;lt;std::is_same_v&amp;lt;typename T::Outer, typename T::Outer&amp;gt;&amp;gt; fun1(T) {  // 2
    std::cout &amp;lt;&amp;lt; &quot;f2\n&quot;;
}
template &amp;lt;class T&amp;gt;
std::enable_if&amp;lt;std::is_same_v&amp;lt;typename T::Inner, typename T::Inner&amp;gt;&amp;gt; fun2(T) {  // 3
    std::cout &amp;lt;&amp;lt; &quot;f1\n&quot;;
}

template &amp;lt;class T&amp;gt;
std::enable_if&amp;lt;std::is_same_v&amp;lt;typename T::Outer, typename T::Outer&amp;gt;&amp;gt; fun2(T) {  // 4
    std::cout &amp;lt;&amp;lt; &quot;f2\n&quot;;
}
int main(){
    fun1(x);  // 应该输出 &quot;f1&quot;
    fun1(y);  // 应该输出 &quot;f2&quot;
    fun2(x);  // 应该输出 &quot;f1&quot;
    fun2(y);  // 应该输出 &quot;f2&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，&lt;code&gt;1&lt;/code&gt;和&lt;code&gt;2&lt;/code&gt;我们使用的是&lt;code&gt;std::enable_if_t&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;和&lt;code&gt;4&lt;/code&gt;使用的是&lt;code&gt;std::enable_if&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;看起来没啥问题对吧。但是它存在一个潜在的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果我们尝试检查&lt;code&gt;fun1&lt;/code&gt;和&lt;code&gt;fun2&lt;/code&gt;函数的签名。我们会发现，&lt;code&gt;fun1&lt;/code&gt;的函数签名是我们希望的，返回类型是&lt;code&gt;void&lt;/code&gt;。但是，&lt;code&gt;fun2&lt;/code&gt;的函数签名，返回类型是&lt;code&gt;std::enable_if&amp;lt;true, void&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以说我们发现了陷阱所在。一个&lt;code&gt;_t&lt;/code&gt;后缀可能会导致我们需要花大量时间排查。这同样适用于&lt;code&gt;&amp;lt;type_traits&amp;gt;&lt;/code&gt;标头中的所有其他神秘生物。每个&lt;code&gt;std::trait_X&lt;/code&gt;和&lt;code&gt;std::trait_X_t&lt;/code&gt;在混合使用的时候会导致意图不可见。&lt;/p&gt;
&lt;h1&gt;编译期表达式计算的例子&lt;/h1&gt;
&lt;p&gt;来自&lt;a href=&quot;https://istmarc.github.io/post/2024/10/27/on-designing-tenseur-a-c-tensor-library-with-lazy-evaluation/&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;核心是我们想要优化掉&lt;code&gt;Vec x =  a + b + c&lt;/code&gt; 这样带有中间结果的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename E&amp;gt;
class VecExpression {
   public:
    static constexpr bool is_leaf = false;

    double operator[](size_t i) const {
        // Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a.
        // virtual functions in C++)
        return static_cast&amp;lt;E const&amp;amp;&amp;gt;(*this)[i];
    }
    size_t size() const { return static_cast&amp;lt;E const&amp;amp;&amp;gt;(*this).size(); }
};

class Vec : public VecExpression&amp;lt;Vec&amp;gt; {
    std::array&amp;lt;double, 3&amp;gt; elems;

   public:
    static constexpr bool is_leaf = true;

    decltype(auto) operator[](size_t i) const { return elems[i]; }
    decltype(auto) operator[](size_t i) { return elems[i]; }
    size_t size() const { return elems.size(); }

    // construct Vec using initializer list
    Vec(std::initializer_list&amp;lt;double&amp;gt; init) { std::copy(init.begin(), init.end(), elems.begin()); }

    // A Vec can be constructed from any VecExpression, forcing its evaluation.
    template &amp;lt;typename E&amp;gt;
    Vec(VecExpression&amp;lt;E&amp;gt; const&amp;amp; expr) {
        for (size_t i = 0; i != expr.size(); ++i) {
            elems[i] = expr[i];
        }
    }
};
template &amp;lt;typename E1, typename E2&amp;gt;
class VecSum : public VecExpression&amp;lt;VecSum&amp;lt;E1, E2&amp;gt; &amp;gt; {
    // cref if leaf, copy otherwise
    typename std::conditional&amp;lt;E1::is_leaf, const E1&amp;amp;, const E1&amp;gt;::type _u;
    typename std::conditional&amp;lt;E2::is_leaf, const E2&amp;amp;, const E2&amp;gt;::type _v;

   public:
    static constexpr bool is_leaf = false;

    VecSum(E1 const&amp;amp; u, E2 const&amp;amp; v) : _u(u), _v(v) { assert(u.size() == v.size()); }
    decltype(auto) operator[](size_t i) const { return _u[i] + _v[i]; }
    size_t size() const { return _v.size(); }
};

template &amp;lt;typename E1, typename E2&amp;gt;
VecSum&amp;lt;E1, E2&amp;gt; operator+(VecExpression&amp;lt;E1&amp;gt; const&amp;amp; u, VecExpression&amp;lt;E2&amp;gt; const&amp;amp; v) {
    return VecSum&amp;lt;E1, E2&amp;gt;(*static_cast&amp;lt;const E1*&amp;gt;(&amp;amp;u), *static_cast&amp;lt;const E2*&amp;gt;(&amp;amp;v));
}
int main(){
    Vec v1 = {1, 2, 3};
    Vec v2 = {4, 5, 6};
    Vec v3 = v1 + v2;
    std::cout &amp;lt;&amp;lt; v3[0] &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; v3[1] &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; v3[2] &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话，这样 &lt;code&gt;a+b+c&lt;/code&gt; 的类型是 &lt;code&gt;VecSum&amp;lt;VecSum&amp;lt;Vec, Vec&amp;gt;, Vec&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Vec x = a + b + c &lt;/code&gt; 会调用&lt;code&gt;Vec(VecExpression&amp;lt;E&amp;gt; const&amp;amp; expr)&lt;/code&gt;
&lt;code&gt;elems[i] = expr[i];&lt;/code&gt;会展开成&lt;code&gt;elems[i] = a.elems[i] + b.elems[i] + c.elems[i]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这样就没有临时Vec对象了&lt;/p&gt;
&lt;h1&gt;通过值传递还是引用传递可调用对象&lt;/h1&gt;
&lt;p&gt;来自&lt;a href=&quot;https://biowpn.github.io/bioweapon/2024/09/30/template-best-practices.html&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;假设我们有一个函数模板&lt;code&gt;g&lt;/code&gt;接受一个可调用对象&lt;code&gt;f&lt;/code&gt;，我们用值传递还是引用传递？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class F&amp;gt;
void g(F f);      // (1) by value

template &amp;lt;class F&amp;gt;
void g(F&amp;amp;&amp;amp; f);    // (2) by reference
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们看看标准库，比如 &lt;code&gt;std::invoke&lt;/code&gt;, 可调用对象是引用传递&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class F, class... Args &amp;gt;
std::invoke_result_t&amp;lt;F, Args...&amp;gt;
    invoke( F&amp;amp;&amp;amp; f, Args&amp;amp;&amp;amp;... args ) noexcept(/* see below */);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在其他大多数情况，比如在 &lt;code&gt;&amp;lt;algorithm&amp;gt;&lt;/code&gt;头文件中，可调用对象是值传递&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class T, class Compare &amp;gt;
const T&amp;amp; max( const T&amp;amp; a, const T&amp;amp; b, Compare comp );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以按照作者意见：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;g&lt;/code&gt;的主要作用是调用&lt;code&gt;f&lt;/code&gt;，则使用引用&lt;/li&gt;
&lt;li&gt;其他情况下使用值传递。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做的好处是可以简化一些代码，同时可以让可调用对象直接被&lt;code&gt;algotirhm&lt;/code&gt;使用&lt;/p&gt;
&lt;p&gt;对于那些不可拷贝，或者不想拷贝的可调用对象。可以使用&lt;code&gt;std::ref&lt;/code&gt;包装&lt;/p&gt;
&lt;h1&gt;运算符重载&lt;/h1&gt;
&lt;p&gt;从最近需求中学习，也感谢磊哥的指点。我们有现在的SSCCE代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct t1 {
    int a;
    int b;
};

template &amp;lt;typename T1&amp;gt;
struct F {
    F(int x) : val_(x) {};
    F() : val_(2) {};
    template &amp;lt;typename T2&amp;gt;
    friend F&amp;lt;T1&amp;gt; operator*(const F&amp;lt;T1&amp;gt;&amp;amp; a, const F&amp;lt;T2&amp;gt;&amp;amp; b) {
        return a.val_ * b.val_;
    }
   private:
    int val_;
};
int main() {
    F&amp;lt;int&amp;gt; a;
    F&amp;lt;t1&amp;gt; b(3);
    auto s = a * b; // NOPE
    cout &amp;lt;&amp;lt; s.getval() &amp;lt;&amp;lt; endl;
}
/*
1107.cpp:16:27: error: ‘int F&amp;lt;t1&amp;gt;::val_’ is private within this context
   16 |         return a.val_ * b.val_;
      |                         ~~^~~~
1107.cpp:24:9: note: declared private here
   24 |     int val_;
      |         ^~~~
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们有一个乘法运算符重载。我们不希望写为成员函数，希望是友元函数。同时我们不想暴露某些私有方法或成员。但是现在我们似乎发现&lt;code&gt;F&amp;lt;T1&amp;gt;&lt;/code&gt;看不到&lt;code&gt;F&amp;lt;T2&amp;gt;&lt;/code&gt;的成员。这是必然的。因为类模板&lt;code&gt;F&amp;lt;X&amp;gt;&lt;/code&gt;和&lt;code&gt;F&amp;lt;Y&amp;gt;&lt;/code&gt;并不是同一类型。同时，这种友元函数的定义实际上并不是一个全局模板函数，而是一个成员模板函数。在这种情况下，operator* 只有在&lt;code&gt; F&amp;lt;T1&amp;gt;&lt;/code&gt; 的实例内部才是友元。对于&lt;code&gt; F&amp;lt;T2&amp;gt;&lt;/code&gt; 类型的对象，&lt;code&gt;F&amp;lt;T1&amp;gt;&lt;/code&gt; 的 &lt;code&gt;operator*&lt;/code&gt; 无法访问&lt;code&gt; F&amp;lt;T2&amp;gt;&lt;/code&gt; 的私有成员 &lt;code&gt;val_&lt;/code&gt;。因此编译器会报错。&lt;/p&gt;
&lt;p&gt;所以我们用如下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct t1 {
    int a;
    int b;
};

template &amp;lt;typename T1&amp;gt;
struct F {
    F(int x) : val_(x) {};
    F() : val_(2) {};
    template &amp;lt;typename T11, typename T2&amp;gt; // T11不可是T1，会shadow
    friend F&amp;lt;T11&amp;gt; operator*(const F&amp;lt;T11&amp;gt;&amp;amp; a, const F&amp;lt;T2&amp;gt;&amp;amp; b);
    int getval(){
        return val_;
    }
   private:
    int val_;
};
template&amp;lt;typename T11, typename T2&amp;gt; //T11可以是任何，可以是T1, 无所谓
F&amp;lt;T11&amp;gt; operator* (const F&amp;lt;T11&amp;gt;&amp;amp; a, const F&amp;lt;T2&amp;gt;&amp;amp; b){
    return a.val_ * b.val_;
}
int main() {
    F&amp;lt;int&amp;gt; a;
    F&amp;lt;t1&amp;gt; b(3);
    auto s = a * b; // GOOD
    cout &amp;lt;&amp;lt; s.getval() &amp;lt;&amp;lt; endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种写法，这里声明了一个独立的全局模板函数，并且声明了它为友元函数。由于这是一个独立的模板函数，它可以访问 &lt;code&gt;F&amp;lt;T11&amp;gt;&lt;/code&gt; 和 fuc&lt;code&gt;kFT2&amp;gt;&lt;/code&gt; 的私有成员。这是因为编译器在第二种情况下将&lt;code&gt;operator*&lt;/code&gt;视为所有 &lt;code&gt;F&amp;lt;T1&amp;gt;&lt;/code&gt; 和 &lt;code&gt;F&amp;lt;T2&amp;gt;&lt;/code&gt; 的友元函数，因此无论 &lt;code&gt;F&amp;lt;T11&amp;gt;&lt;/code&gt; 和 &lt;code&gt;F&amp;lt;T2&amp;gt;&lt;/code&gt; 是什么类型，它们的私有成员&lt;code&gt; val_&lt;/code&gt; 都可以被访问。&lt;/p&gt;
&lt;p&gt;可以查看&lt;a href=&quot;https://blog.csdn.net/qfturauyls/article/details/106675630&quot;&gt;这里的&lt;/a&gt; &lt;strong&gt;模板类的非约束模板友元函数&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;通用学习文章：&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://biowpn.github.io/bioweapon/2024/09/30/template-best-practices.html&quot;&gt;Template Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qfturauyls/article/details/106675630&quot;&gt;模板友元函数&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>C++ 模板笔记（七）：杂项、元函数、完美转发失败场景与成员指针</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-7/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-7/</guid><pubDate>Wed, 14 Dec 2022 14:56:00 GMT</pubDate><content:encoded>&lt;h1&gt;杂项&lt;/h1&gt;
&lt;h2&gt;类成员函数不可以既是虚函数又是模板函数 12.1.1&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先简单来说，虚函数是动态多态。模板属于静态多态。这里不能动静结合。&lt;/li&gt;
&lt;li&gt;其次，我们知道了函数模板在编译的时候会看哪些地方调用了。根据T的不同，每一份T都会导致实例化出一份T类型的函数。这就导致了会有多个入参类型不同的成员函数。但是我们又知道虚函数的虚函数表必须预先确定。而此时父类看不到子类的模板虚函数到底有几个版本，难不成都写一遍？&lt;/li&gt;
&lt;li&gt;另一个原因是如果我们有模板虚函数，那么编译器为了确定类的虚函数表的大小，就必须要知道我们一共为该成员模板函数实例化了多少个不同版本的虚函数。显然编译器需要查找所有的代码文件，才能够知道到底有几个虚函数，这对于多文件的项目来说，代价是非常高的。&lt;/li&gt;
&lt;li&gt;也就是说，如果这样做的话，虚函数表的确定时间就需要从编译器推迟到链接期。因为编译期指的是我不需要看具体实现。我只要发现你是&lt;code&gt;virtual&lt;/code&gt;我就可以把你放到表内。如果支持模板虚函数，那么就需要链接的时候把每一个实现都扫一遍然后再整理后放入虚函数表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么类模板有偏特化没有重载，但是函数模板没有偏特化有重载？&lt;/h2&gt;
&lt;p&gt;问题也是答案。因为类模板没有重载所以有偏特化。因为函数模板有重载所以没有偏特化。&lt;/p&gt;
&lt;h2&gt;关于偏序 （partial order)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;偏序适用于函数模板。也适用于类模板或变量模板&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们提到过，当可行函数集中所有的函数都是函数模板的时候，这时候要选择最特殊的那一个。&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void func(T a){ //主模板
    cout &amp;lt;&amp;lt;&quot;main&quot; &amp;lt;&amp;lt; endl;
}
template &amp;lt;typename T&amp;gt;
void func(T* a){ //重载模板
    cout &amp;lt;&amp;lt; &quot;overload&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    int* p;
    func(p); //输出overload
}
//注意，调用的时候，如果调用第一个，因为传入的是int*，所以第一个里面的T会被推导为int*
//如果调用第二个，因为传入的是int*, 而里面又已经有了个*所以T就只被推导为int。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么我们认为下面的比上面的更特殊？我们用到了偏序的具体步骤&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先选择两个函数模板，&lt;code&gt;T1&lt;/code&gt;和&lt;code&gt;T2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用假设的唯一类型&lt;code&gt;X&lt;/code&gt;取代模板&lt;code&gt;T1&lt;/code&gt;的参数&lt;/li&gt;
&lt;li&gt;用被&lt;code&gt;X&lt;/code&gt;取代后的&lt;code&gt;T1&lt;/code&gt;的参数列表，带入&lt;code&gt;T2&lt;/code&gt;，看&lt;code&gt;T2&lt;/code&gt;是否是一个有效的模板。忽略所有的隐式转换。&lt;/li&gt;
&lt;li&gt;反过来，先用&lt;code&gt;X&lt;/code&gt;取代&lt;code&gt;T2&lt;/code&gt;的参数，再把&lt;code&gt;T2&lt;/code&gt;的参数列表带入&lt;code&gt;T1&lt;/code&gt;，看看&lt;code&gt;T1&lt;/code&gt;是否有效。&lt;/li&gt;
&lt;li&gt;如果一个模板的参数比如&lt;code&gt;T1&lt;/code&gt;对于另外一个模板&lt;code&gt;T2&lt;/code&gt;是有效的，但是反之不成立，那么就说这个模板&lt;code&gt;T1&lt;/code&gt;不比&lt;code&gt;T2&lt;/code&gt;更特例化。也就是可能T2比T1更特例化。如果这两个模板的参数都可以相互代替，就说它们具有相同的特例性，这样会引起编译器混淆。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以我们有总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于一个模板，特定类型的参数比一般类型的参数，更具有特例性&lt;/li&gt;
&lt;li&gt;带有&lt;code&gt;T*&lt;/code&gt;的模板比&lt;code&gt;T&lt;/code&gt;的模板具有特例性。因为一个假设的类型&lt;code&gt;X*&lt;/code&gt;也可以被认为是&lt;code&gt;T&lt;/code&gt;类型的， 相反一个有效的&lt;code&gt;T&lt;/code&gt;类型参数，可能不是&lt;code&gt;X*&lt;/code&gt;类型的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const T&lt;/code&gt;比&lt;code&gt;T&lt;/code&gt;更特例化，道理同上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const T*&lt;/code&gt;比&lt;code&gt;const T&lt;/code&gt;更特例化，理由也是一样的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;https://stackoverflow.com/questions/18283851/template-specialization-in-case-of-multiple-base-templates-in-c/18283933?noredirect=1#comment26823443_18283933&lt;/p&gt;
&lt;p&gt;https://www.youtube.com/watch?v=NIDEjY5ywqU&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/weixin_30294295/article/details/94781059&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/390783543&lt;/p&gt;
&lt;h2&gt;关于不要显式全特化函数和函数全特化的问题&lt;/h2&gt;
&lt;p&gt;假如我们有下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
template&amp;lt;typename T&amp;gt;            // (1) - 主模板
string getTypeName(T){
    return &quot;unknown&quot;;
}

template&amp;lt;typename T&amp;gt;            // (2) - 重载自 (1) 的主模板。 针对指针类型的模板重载
string getTypeName(T*){
    return &quot;pointer&quot;;
}

template&amp;lt;&amp;gt;                      // (3) - (2) 的显式全特化。针对int指针类型的显式全特化
string getTypeName(int*){
    return &quot;int pointer&quot;;
}

// getTypeName2

template&amp;lt;typename T&amp;gt;            // (4) - 主模板
string getTypeName2(T){
    return &quot;unknown&quot;;
}

template&amp;lt;&amp;gt;                      // (5) - (4) 的显式全特化。 针对int指针类型的显式全特化
string getTypeName2(int*){
    return &quot;int pointer&quot;;
}
template&amp;lt;typename T&amp;gt;            // (6) - 重载自 (4) 的主模板。 针对指针类型的模板重载
string getTypeName2(T*){
    return &quot;pointer&quot;;
}

int main(){
    
    cout &amp;lt;&amp;lt; &apos;\n&apos;;
    int* p;
    cout &amp;lt;&amp;lt; &quot;getTypeName(p): &quot; &amp;lt;&amp;lt; getTypeName(p) &amp;lt;&amp;lt;  &apos;\n&apos;;  
    //输出getTypeName(p): int pointer
    cout &amp;lt;&amp;lt; &quot;getTypeName2(p): &quot; &amp;lt;&amp;lt; getTypeName2(p) &amp;lt;&amp;lt;  &apos;\n&apos;;
    //输出getTypeName2(p): pointer  
    cout &amp;lt;&amp;lt;  &apos;\n&apos;;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这段代码中，我们有意的把针对指针的模板重载和全特化调换了顺序写成了两个不同的函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们看到了。分别输出&lt;code&gt;getTypeName(p): int pointer&lt;/code&gt; 和 &lt;code&gt;输出getTypeName2(p): pointer &lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是这不对啊？看&lt;code&gt;getTypeName&lt;/code&gt;，为什么使用了全特化版本而不是模板重载？我们不是说了全特化版本不参与重载决议吗？&lt;/p&gt;
&lt;p&gt;我们要理解到底决议的是谁。要看匹配顺序。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;针对第一个&lt;code&gt;getTypeName&lt;/code&gt;函数调用，过程是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在一开始，重载解析会考虑 (1) 和 (2)。第三个全特化版本此时不参与重载决议。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; std::string getTypeName(T);&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1) - 主模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; std::string getTypeName(T*);&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(2) - 重载自 (1) 的主模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;template&amp;lt;&amp;gt; std::string getTypeName(int*);&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(3) - (2) 的显式全特化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显式全特化版本此时不参与，所以(2) 更合适，因此第一阶段选择 (2)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是随后看到了（3），我们发现针对我们合成出来的&lt;code&gt;T&lt;/code&gt;为&lt;code&gt;int&lt;/code&gt;类型的函数也就是（2），还有一个似乎可以比较一下。&lt;strong&gt;而且此时两个函数都是模板函数。并且其中一个是主模板合成出的。所以这时候（3）被加入到了重载决议中，开启偏序部分。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时通过偏序，&lt;strong&gt;（3)此时被认为是(2)的显式全特化&lt;/strong&gt;。和（2）相比，（3）的类型更为特化，所以此时选择了（3）。所以编译器不会从(2)合成一个，而是选取一个已经存在的(3)。&lt;em&gt;这里看一下上面偏序推导和下面的“关于为什么说函数模板不是...&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是针对第二个&lt;code&gt;getTypeName2&lt;/code&gt;函数调用，过程是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在一开始，重载解析会考虑 (4) 和 (6)。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; std::string getTypeName2(T); &lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;(4) - 主模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&amp;lt;&amp;gt; std::string getTypeName2(int*); &lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;(5) - (4) 的显式全特化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; std::string getTypeName2(T*); &lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;(6) - 重载自 (4) 的主模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;显式全特化版本此时不参与，所以(6) 更合适，因此选择 (6)。&lt;/li&gt;
&lt;li&gt;然而此时**(5) 被认为是 (4) 的显式全特化而不是(6)的显式全特化**。而且因为 (4) 没有被选中，所以重载解析也不会考虑（5）。最后选择（6）。
&lt;ul&gt;
&lt;li&gt;为什么？此时**(5) 被认为是 (4) 的全特化而不是(6)的显式全特化**。因为在进行到(5)的时候，编译器唯一能看到的只有(4)而没有(6)。[也就是在（5）眼里，只有（4）在作用域内。（6）此时还没有被看见。因为显式全特化的（5）没有名字，这时候必须依附于一个主模板。所以说（5）只能看见（4）所以认为（5）是（4）的显式全特化]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们在这里可以看出来模板特化的混乱程度。所以尽可能不要特化模板函数，而是采用普通函数。也就是不要对函数模板进行特化，而要使用&lt;strong&gt;非&lt;/strong&gt;泛型函数。&lt;/p&gt;
&lt;p&gt;所以我们在这里把&lt;code&gt;(3)&lt;/code&gt;和&lt;code&gt;(5)&lt;/code&gt;函数头的全特化指示&lt;code&gt;template&amp;lt;&amp;gt;&lt;/code&gt;删掉，则一切工作正常。都会输出&lt;code&gt;int pointer&lt;/code&gt;。因为普通函数总是优先于模板函数。&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/561977606&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说，如果在很多个函数模板中挑选，编译器会首先选中一个基础模板。然后针对这个模板再一轮搜索来查看是否有更加匹配的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了好玩，再看个像狗一样的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;            // (7) - 主模板
string getTypeName3(T){
    return &quot;unknown&quot;;
}

template&amp;lt;&amp;gt;                      // (8) - (7) 的显式全特化。 针对int指针类型的显式全特化
string getTypeName3(int*){
    return &quot;int pointer&quot;;
}
template&amp;lt;typename T&amp;gt;            // (9) - 重载自 (7) 的主模板。 针对指针类型的模板重载
string getTypeName3(T*){
    return &quot;pointer&quot;;
}
template&amp;lt;&amp;gt;                      // (10) - (9) 的显式全特化。 针对int指针类型的显式全特化
string getTypeName3(int*){
    return &quot;another int pointer&quot;;
}
int main(){
    
    cout &amp;lt;&amp;lt; &apos;\n&apos;;
    int* p;
    int a = 0;
    cout &amp;lt;&amp;lt; &quot;getTypeName3(p): &quot; &amp;lt;&amp;lt; getTypeName3(p) &amp;lt;&amp;lt;  endl;   //输出another int pointer
    cout &amp;lt;&amp;lt; &quot;getTypeName3(a): &quot; &amp;lt;&amp;lt; getTypeName3(a) &amp;lt;&amp;lt; endl; //输出 unknown
    // cout &amp;lt;&amp;lt;  &apos;\n&apos;;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;就是由于上面的原因。（8）被认为是（7）的显式全特化。因为当时它只能看到，依附于（7）。而（10）被认为是（9）的显式全特化。因为（10）可以看到（9）。而且因为（8）是（7）的显式全特化，我们不考虑（7）所以也不考虑（8）。考虑（9）然后通过偏序发现（10）比9更特殊。最后选择了（10）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么时候要显式全特化而不是重载？&lt;/h2&gt;
&lt;p&gt;一个是&lt;code&gt;std::swap&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;另外一个是阻止某些隐式转换。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt; 
void testf(T); //注意，这里没有定义！！
template &amp;lt;&amp;gt; 
void testf(int) {
    //一些内容
}
int main(){
    testf(3);//可以
    testf(3.3); //不行
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当只有&lt;code&gt;void foo(int)&lt;/code&gt;时，以浮点类型调用会发生隐式转换，这可以通过特化来阻止。正常调用&lt;code&gt;int&lt;/code&gt;没问题，但是如果入参是&lt;code&gt;float&lt;/code&gt;，会匹配到主模板。但是主模板没有定义，自然出现问题。&lt;/p&gt;
&lt;p&gt;https://segmentfault.com/q/1010000013299483&lt;/p&gt;
&lt;h2&gt;关于重载决议 (overload resolution)&lt;/h2&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/lbPdLfusUqiO-I59PcXrCQ&lt;/p&gt;
&lt;p&gt;这个cppcon关于重载解析的&lt;a href=&quot;https://youtu.be/b5Kbzgx1w9A&quot;&gt;视频&lt;/a&gt;非常好。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果函数只有返回值不同，无法重载。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为普通函数返回值不算做函数签名。因为返回值是否使用是可选的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果函数只有形参的默认值不同，无法重载。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为形参默认值不算作函数签名。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;还有一种情况。就是多个函数有相同数量的不带默认值的参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;int f();
int f(int x = 123);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这种也会二义性。因为在选择可行函数集的时候:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;If the candidate function has more than &lt;code&gt;M&lt;/code&gt; parameters and the &lt;code&gt;M+1&lt;/code&gt;&apos;st parameter and all parameters that follow have default arguments, it is viable. For the rest of overload resolution, the parameter list is truncated at &lt;code&gt;M&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;M&lt;/strong&gt; = 调用时实际提供的实参数量&lt;/p&gt;
&lt;p&gt;如果候选函数的形参比 M 多，但&lt;strong&gt;从第 M+1 个形参开始，所有形参都有默认值&lt;/strong&gt;，那么这个函数依然是&lt;strong&gt;可行函数（viable function）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在后续的重载决议中，编译器会&lt;strong&gt;临时把这个函数的参数表截断到前 M 个形参&lt;/strong&gt;，然后再和其他候选函数进行优劣比较。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个时候第二个函数给截断了，所以比较的时候这两个函数都可以是0参数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果函数的区别只有一个是&lt;code&gt;static&lt;/code&gt;一个不是，无法重载。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;重载决议只看声明，不看定义。&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;所以如果声明中包含一些计算或判断，比如萃取，decltype之类，它依旧会计算。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在一个函数调用的备选方案中包含函数模板时，编译器首先要决定应该将什么样的模板参数用于各种模板方案，然后用这些参数替换函数模板的参数列表以及返回类型，最后评估替换后的函数模板和这个调用的匹配情况（就像常规函数一样）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;也就是类型推导发生在重载决议前。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第一步 确定候选函数集&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;确定候选集: 在重载函数集中,根据作用域和函数名,来选择同名同域的重载函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;候选函数集特点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;该集合中的函数的声明,在该调用点可见&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是,和被调函数在同一作用域下&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;该集合中的函数,与被调函数同名&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是,重载函数的函数名都一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第二步 选出可行函数集&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在确定了候选函数集之后,根据被调函数的参数列表,选择出对应的可行函数集&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数列表信息 = 参数个数 + 参数类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可行函数集特点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个实参的类型要与对应的形参类型相同,或者是能够转换成形参的类型
&lt;ul&gt;
&lt;li&gt;c++支持隐式类型转换!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;参数个数与被调函数个数匹配
&lt;ul&gt;
&lt;li&gt;参数类型可以隐式转换,但是参数个数相同是最低标准&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在这个过程中会逐个移除无效的候选函数。无效的候选函数被称为not viable。如果没有找到可行函数集,则编译器将报告无法匹配函数的错误。在这一过程中，仅检查参数个数是否匹配。具体能否转换以满足条件在下一步。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;第三步 寻找最佳匹配 Ranking阶段和tie-breaker阶段&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;从可行函数集合中选出所有与本次调用最匹配的函数(注意,并不一定是完全一致的函数)&lt;/li&gt;
&lt;li&gt;编译器将依次检查可行函数集中的每一个函数&lt;/li&gt;
&lt;li&gt;依次检查每一个函数的形参,与被调函数的实参&lt;/li&gt;
&lt;li&gt;寻找到最匹配的那个可行函数。&lt;/li&gt;
&lt;li&gt;匹配成功条件
&lt;ul&gt;
&lt;li&gt;如果可行函数集合中只有一个函数并且可以匹配，成功调用。&lt;/li&gt;
&lt;li&gt;如果可行函数集合中有多个函数可能匹配，进入tie breaker阶段：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;该可行函数每个实参的匹配都&lt;em&gt;不劣于&lt;/em&gt;其他可行函数需要的匹配&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;该可行函数至少有一个实参的匹配&lt;em&gt;优于&lt;/em&gt;其他可行函数提供的匹配&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有且只有一个可行函数满足上述两个原则&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;注意: 有且只有一个可行函数满足,如果有多个可行函数同时满足,就会造成函数调用的二义性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://blog.csdn.net/jiewaikexue/article/details/120089550&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_53558968/article/details/122757998&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/640.png&quot; alt=&quot;640&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上图是重载中的决胜局tie-breaker的匹配表。它们的匹配优先级也是自上往下的，即Exact Match比Promotion更好，Promotion比Conversion更好，可以理解为完全匹配、次级匹配和低级匹配。&lt;/li&gt;
&lt;li&gt;关于lvalue-to-rvalue转换，看&lt;a href=&quot;https://stackoverflow.com/questions/20850536/lvalue-to-rvalue-implicit-conversion&quot;&gt;这里&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;s&gt;我的理解是，涉及到临时对象的都符合左值到右值转换。比如函数值传递，值返回。&lt;/s&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;因为编译器期望的是值，而不是内存位置，要从内存位置中把值提取出来，就会发生 lvalue to rvalue conversion&lt;/strong&gt;
&lt;code&gt;int a = b;&lt;/code&gt; &lt;code&gt;=&lt;/code&gt;右边期望的就是值，最终&lt;code&gt;b&lt;/code&gt; 还是会有 lvalue to rvalue conversion。不把 &lt;code&gt;b&lt;/code&gt; 的值取出来，那就不叫赋值了。&lt;/p&gt;
&lt;p&gt;注意，class type的情况下并不是lvalue to rvalue conversion。因为类对象的 &lt;code&gt;a = b&lt;/code&gt; 调用的是 &lt;code&gt;a.operator=()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;----来自群讨论&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241022005325.png&quot; alt=&quot;微信图片_20241022005325&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241022005329.png&quot; alt=&quot;微信图片_20241022005329&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241022005331.png&quot; alt=&quot;微信图片_20241022005331&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关于Promotion中的 Integral promotion&lt;strong&gt;整数提升&lt;/strong&gt;，有如下几种情况
&lt;ul&gt;
&lt;li&gt;核心意思是，任何小于&lt;code&gt;int&lt;/code&gt;的都可以提升为&lt;code&gt;int&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;任何其余的都叫转换。比如&lt;code&gt;int&lt;/code&gt;到&lt;code&gt;long&lt;/code&gt;。这不是提升，是转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;short&lt;/code&gt; 提升为&lt;code&gt; int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsigned short&lt;/code&gt;，&lt;code&gt;unsigned char&lt;/code&gt; 提升为 &lt;code&gt;unsigned int&lt;/code&gt; 或 &lt;code&gt;int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bool &lt;/code&gt;提升为 &lt;code&gt;int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char &lt;/code&gt;提升为&lt;code&gt;int&lt;/code&gt;或&lt;code&gt;unsigned int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;还有一些&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/implicit_conversion&quot;&gt;其他&lt;/a&gt;例子&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关于Promotion中的 Floating-point promotion &lt;strong&gt;浮点提升&lt;/strong&gt;，只有一种情况
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;float&lt;/code&gt; 提升为 &lt;code&gt;double&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关于Conversion中的Integral Conversion &lt;strong&gt;整数转换&lt;/strong&gt;，核心意思是任何整数类型都可以互相转换。如果该转换列在“整数类型提升”下，那么它是提升而非转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230218180447.png&quot; alt=&quot;QQ截图20230218180447&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这张图中，我们看到了上面表格里面的五种转换的对应级别。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是上面表格只到numeric conversion为止。但是在其下面还有两种：用户定义转换和省略号转换&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用户定义转换比任何标准转换的级别都要低。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户定义转换的定义是：从 任意类型 隐式转换至 任意类型&lt;/li&gt;
&lt;li&gt;比如从&lt;code&gt;const char*&lt;/code&gt;到&lt;code&gt;string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;这一步多是调用构造函数或用户定义转换函数。我们提到过的functional-style cast&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;省略号转换是最低级别的。也就是最后最后才考虑。所以经常被用作fallback。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Ranking是查找所有的匹配函数。如果有多个匹配，则进入tie-breaker阶段找到最匹配的，或者是，隐式转换次数（步数）最少的一个。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们看一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// A
void doThing_A(double, int, int) { } // overload 1
void doThing_A(int, double, double) { } // overload 2
int main() {
    doThing_A(4, 5, 6); // which overload is called?
}
// B
void doThing_B(int, int, double) { } // overload 3
void doThing_B(int, double, double) { } // overload 4
int main() {
    doThing_B(4, 5, 6); // which overload is called?
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A 编译器会报错，二义性。B是3。为什么？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;针对A，编译器考虑的是：针对重载1，传入的第一个参数不能完美匹配。这时候第一个函数已经输了。然后考虑重载2，传入的第二个参数也不能完美匹配，所以重载2也输了一次。同时这两个函数在同一个级别上。都需要conversion，且没有一个tie breaker可以解决这个问题. 因为此时的两个候选函数的参数的隐式转换中没有哪一个是优于另一个的. 这时候产生了二义性。&lt;strong&gt;这种属于double lose&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;针对B，编译器考虑的是：针对重载1和2，传入的第一个参数都可以完美匹配。针对第二个参数，只有第一个重载可以完美匹配。这个时候开始比较第三个参数。二者一致。由于重载1比重载2多赢了一次，进入tie breaker阶段然后选择了重载1。因为我们提到过：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;该可行函数每个实参的匹配都不劣于其他可行函数需要的匹配&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;该可行函数至少有一个实参的匹配 优于其他可行函数提供的匹配&lt;/strong&gt;、&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有且只有一个可行函数满足上述两个原则&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再看一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void doThing_D(int &amp;amp;) { } // overload 1
void doThing_D(int) { } // overload 2
int main() {
    int x = 42;
    doThing_D(x); // which overload is called?
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为什么二义性？
&lt;ul&gt;
&lt;li&gt;它们都是完美匹配。&lt;strong&gt;因为绑定参数到引用不被认为是一种转换&lt;/strong&gt;。这就是我们经常犯的错误。如果函数有一个值传递和一个左值引用传递，两个版本，则一定会二义性。&lt;strong&gt;这种属于double win&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;人话分析一下就是double win和double lose都不行, 也不能二者各赢一次各输一次。这样就会二义性。必须要有一个比另一个赢得多。&lt;/p&gt;
&lt;h2&gt;如何解决重载决议中的二义性？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;增加或移除一些重载&lt;/li&gt;
&lt;li&gt;给构造函数增加&lt;code&gt;explicit&lt;/code&gt;修饰符以避免隐式类型转换（杂记2）&lt;/li&gt;
&lt;li&gt;函数模板可以使用SFINAE
&lt;ul&gt;
&lt;li&gt;实例化无效的函数模板直接会被丢弃，不会放入重载集。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在函数调用前使用显式类型转换。
&lt;ul&gt;
&lt;li&gt;比如使用C或C++风格cast&lt;/li&gt;
&lt;li&gt;使用构造函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;有时候编译器选择的最佳函数并不是我们所期望的。这个时候的解决办法之一是想办法让其有二义性，然后编译器就会给你一大堆&lt;em&gt;很他妈长的&lt;/em&gt;重载决议错误。给你一个candidate列表。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;什么时候使用重载，什么时候使用模板？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;如果针对某一个特定类型，需要有特定的实现，这个时候使用重载。
&lt;ul&gt;
&lt;li&gt;比如&lt;code&gt;string&lt;/code&gt;的构造函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果针对所有类型都有相同的实现，这个时候使用模板。
&lt;ul&gt;
&lt;li&gt;比如&lt;code&gt;std::sort&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;极力避免同时使用函数重载和函数模板。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;关于为什么说函数模板不是函数。函数模板在其被实例化的时候会合成一个特化的函数&lt;/h2&gt;
&lt;p&gt;假设我们有如下函数模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T const&amp;amp;);

func(42); 	//	void func&amp;lt;&amp;gt; (int const&amp;amp;);
func(&apos;a&apos;); 	// 	void func&amp;lt;&amp;gt; (char const&amp;amp;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在两次函数调用的时候，编译器会使用函数模板帮助我们合成注释里面的函数实例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在实例化过程中，可能会发生当前要实例化的东西已经存在或发生过等效调用。所以如果存在适当的预先的全特化版本，那么那个全特化版本会被编译。（这里就是上面的“关于不要全特化...&quot;部分的例子的原因）
&lt;ul&gt;
&lt;li&gt;也就是有全特化版本了，调用的时候就不再合成，而是直接使用全特化版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果没有已经存在的全特化版本，那么编译器此时必须实例化这个全特化版本。编译器会从主模板当中复制它的定义（definition），然后进行适当替换。这就是我们说为什么显式全特化只是一个替换的定义，而不是一个替换的声明。&lt;/li&gt;
&lt;li&gt;我们想象一下，由于显式全特化的函数模板不参与重载解析。除非重载决议选择最佳匹配的主函数模板后，才检验它的特化以查看最佳匹配者。所以就是假设现在编译器合成出了一个&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;int&lt;/code&gt;的函数声明。我们此时有一个显式全特化了&lt;code&gt;int&lt;/code&gt;的函数。这时候会考虑显式全特化的那个。所以他会把全特化版本的函数实例化。然后把合成的函数声明给全特化的这个函数拼接到一起。这就是为什么全特化函数没有独立名字。因为它的声明是编译器合成的，定义是我们自己写的。这两个不能分开，自然全特化版本也没有需要独立名字的意义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;限定名、非限定名&lt;/h2&gt;
&lt;p&gt;限定名(qualified name)，故名思义，是限定了命名空间的名称。看下面这段代码，&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;就是限定名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()  {
    std::cout &amp;lt;&amp;lt; &quot;Hello world!&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;前面都有&lt;code&gt;std::&lt;/code&gt;，它限定了&lt;code&gt;std&lt;/code&gt;这个命名空间，因此称其为限定名。&lt;/p&gt;
&lt;p&gt;如果在上面这段代码中，前面用&lt;code&gt;using std::cout;&lt;/code&gt;或者&lt;code&gt;using namespace std;&lt;/code&gt;，然后使用时只用&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;，它们的前面不再有空间限定&lt;code&gt;std::&lt;/code&gt;，所以此时的&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;就叫做非限定名(unqualified name)。&lt;/p&gt;
&lt;h2&gt;依赖名、非依赖名&lt;/h2&gt;
&lt;p&gt;依赖名(dependent name)是指依赖于模板参数的名称，而非依赖名(non-dependent name)则相反，指不依赖于模板参数的名称。看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt;
class MyClass {
    int i;
    vector&amp;lt;int&amp;gt; vi;
    vector&amp;lt;int&amp;gt;::iterator vitr;

    T t;
    vector&amp;lt;T&amp;gt; vt;
    vector&amp;lt;T&amp;gt;::iterator viter;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为是内置类型，所以类中前三个定义的类型在声明这个模板类时就已知。然而对于接下来的三行定义，只有在模板实例化时才能知道它们的类型，因为它们都依赖于模板参数&lt;code&gt;T&lt;/code&gt;。因此，&lt;code&gt;T&lt;/code&gt;, &lt;code&gt;vector&amp;lt;T&amp;gt;&lt;/code&gt;和&lt;code&gt;vector&amp;lt;T&amp;gt;::iterator&lt;/code&gt;称为依赖名。前三个定义叫做非依赖名。&lt;/p&gt;
&lt;p&gt;更为复杂一点，如果用了&lt;code&gt;typedef T U; U u;&lt;/code&gt;，虽然&lt;code&gt;T&lt;/code&gt;没再出现，但是&lt;code&gt;U&lt;/code&gt;仍然是依赖名。由此可见，不管是直接还是间接，只要依赖于模板参数，该名称就是依赖名。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;typedef&lt;/code&gt; 和作用域解析运算符&lt;code&gt;::&lt;/code&gt; 和 嵌套类&lt;/h2&gt;
&lt;p&gt;我们很难把class和namespace联系起来，但是这两个在抽象层次上其实是一个概念。&lt;/p&gt;
&lt;p&gt;作用域解析运算符::的作用就是制定某一个范围。但是一旦用在了嵌套类或者类内的typedef，我们就很难理解这层含义。&lt;/p&gt;
&lt;p&gt;我们都知道&lt;strong&gt;直接访问类静态成员必须使用作用域解析运算符&lt;code&gt;::&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是我们如果要通过作用域解析运算符访问非静态成员，可以吗？当然可以。只不过需要通过对象访问。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    class testinner{ //嵌套类
        public:
        int _val;
        testinner(){}
        testinner(int x):_val(x){}
    };
    test(){}
    typedef int testdef;


    int s;
};

int main()
{
    test::testinner tt1 = test::testinner(5); //OK
    test::testinner tt2; //OK 访问嵌套类
    test::testdef tt3 = 5; //OK 访问typedef。
    test::s = 5; //不行。直接访问的成员必须是静态成员。
    test t;
    t.test::s = 5; //OK 通过对象访问，但是还是脱裤子放屁加了作用域解析运算符。

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们可以理解为我们需要访问的&lt;code&gt;typede&lt;/code&gt;f和嵌套类在&lt;code&gt;test&lt;/code&gt;类的命名空间下。我们必须要告知编译器这东西在哪，所以需要通过作用域解析运算符去访问。&lt;/p&gt;
&lt;p&gt;至于&lt;code&gt;typedef&lt;/code&gt;和嵌套类是否是一个类的成员，我没有查到确切的说法。如果说他们不是成员吧，但是有成员的属性。要是说是成员吧，我们也可以直接访问。但是也有人说嵌套类的&lt;code&gt;static&lt;/code&gt;是隐式的。也有人把嵌套类当做一个&lt;code&gt;namespace&lt;/code&gt; 来看待。所以我的理解是&lt;strong&gt;不要把嵌套类和&lt;code&gt;typedef&lt;/code&gt;看做类成员。假设他们可以直接调用即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果嵌套类没有在外部类中实例化，则实例化外部类的时候不会实例化嵌套类内容。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;最后说一下必须显式使用&lt;code&gt;typename&lt;/code&gt;的情况。&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20220613134505-16551788472405.png&quot; alt=&quot;QQ截图20220613134505&quot; /&gt;&lt;/p&gt;
&lt;p&gt;来几个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
        struct obj{
            int _sb;
        };
        obj s;
        int _val;
        test(){}
        test(int x):_val(x){}
        test(int x, int y){
            _val = x;
            s._sb = y;
        }
};
template&amp;lt;typename T&amp;gt;
void func(){
    test t(5,8);
    typename T::obj* ptr = &amp;amp;t.s;
    //这里我们的意思是有一个指针ptr指向了T类里面的obj类型的对象。翻译成人话也就是ptr是一个T::obj类型的指针。但是如果有一个T类里面的obj是一个变量，如static int obj = 8
    //那么这就变成了变量乘法。会有歧义。所以使用typedef显式告知编译器 T::obj不是一个变量，而是一个类型。
    cout &amp;lt;&amp;lt; ptr-&amp;gt;_sb &amp;lt;&amp;lt; endl;
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能还不够？再来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef iterator_traits&amp;lt;T&amp;gt;::value_type value_type_anothername; 
//这句话的意思是value_type_anothername是在模板类iterator_traits中的 变量 value_type的别名。

//所以如果有
value_type_anothername name = &quot;foward_iterator&quot;;
//这样是不行的。因为value_type_anothername是一个变量不是类型。
//所以我们需要加typedef告知编译器这个是类型

typedef typename iterator_traits&amp;lt;T&amp;gt;::value_type value_type_anothername; 
//这句话的意思是value_type_anothername是iterator_traits&amp;lt;T&amp;gt;::value_type这个 类型 的别名。

//所以这样可以有
value_type_anothername name = &quot;foward_iterator&quot;;
//因为value_type_anothername 是个类型。就好比int string这种。

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还不够？再来：&lt;/p&gt;
&lt;p&gt;这是我们前文的例子。但是为什么这里不需要加typedef呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    typedef int inputtype;
};

int main(){
    test::inputtype x = 4; //这句话等于告诉你 test类下面的inputtype这个东西是int 
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先送上大佬文章一篇：https://feihu.me/blog/2014/the-origin-and-usage-of-typename/&lt;/p&gt;
&lt;p&gt;由于&lt;code&gt;test&lt;/code&gt;已经是一个完整的定义，因此编译期它的类型就可以确定下来，也就是说&lt;code&gt;test::inputtype&lt;/code&gt;这些名称对于编译器来说也是已知的。&lt;/p&gt;
&lt;p&gt;可是，如果是像&lt;code&gt;T::inputtype&lt;/code&gt;这样呢？&lt;code&gt;T&lt;/code&gt;是模板中的类型参数，它只有等到模板实例化时才会知道是哪种类型，更不用说内部的&lt;code&gt;inputtype&lt;/code&gt;。通过前面类作用域一节的介绍，我们可以知道，&lt;code&gt;T::inputtype&lt;/code&gt;实际上可以是以下三种中的任何一种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态数据成员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态成员函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嵌套类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;typename&lt;/code&gt;的作用，简单理解就是强制告诉编译器 &lt;code&gt;namespace::objname&lt;/code&gt;这个东西是一个类型名而不是变量名。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以在模板类中，如果想要告知编译器一个使用了&lt;code&gt;::&lt;/code&gt;作用域解析运算符的东西是类型，而不是变量，就需要加&lt;code&gt;typename&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果直接把这个东西当做一个类型来进行变量的声明，那就不需要搭配&lt;code&gt;typedef&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果需要把这个东西当做一个类型来赋予一个别名，那就需要加&lt;code&gt;typedef&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，像这样就必须要加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    test(){}
    typedef int inputtype;
};


template&amp;lt;typename T&amp;gt;
class test1{
    public:
    test1(){};
    typedef typename T::inputtype inputtype; //注意这里，T::inputtype是个类型。比如test::inputtype 就是 int类型
};

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;In a declaration or a definition of a template, including alias template, &lt;strong&gt;a name that is not a member of the &lt;em&gt;current instantiation&lt;/em&gt; and is dependent on a template parameter is not considered to be a type unless the keyword typename is used or unless it was already established as a type name&lt;/strong&gt;, e.g. with a typedef declaration or by being used to name a base class.&lt;/p&gt;
&lt;p&gt;在模板（包括别名模版）的声明或定义中，&lt;strong&gt;不是&lt;em&gt;当前实例化&lt;/em&gt;的成员且取决于某个模板形参的名字不会被认为是类型，除非使用关键词 typename 或它已经被设立为类型名&lt;/strong&gt;（例如用 typedef 声明或通过用作基类名）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;特殊成员函数能否是函数模板&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;拷贝构造，拷贝赋值，移动构造，移动赋值。这四个成员函数模板不会被当成真正的特殊成员函数。如果只声明模板，编译器依旧会合成默认的。然后调用默认的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造函数是个例外。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设我们有这个类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class C{
    public:
        template&amp;lt;typename T&amp;gt;
        C(const T&amp;amp;){
            cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
};
int main(){
    C obj1(10); //called
    C obj2(obj1); //啥也没有
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们第一反应是啥：这个函数看起来非常像拷贝构造对吧。但是它不是。它是个构造函数模板。&lt;/p&gt;
&lt;p&gt;拷贝构造有严格定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;类 &lt;code&gt;T&lt;/code&gt; 的拷贝构造函数是首个形参是 T&amp;amp;、const T&amp;amp;、volatile T&amp;amp; 或 const volatile T&amp;amp;，而且要么没有其他形参，要么剩余形参均有默认值的非模板&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/initializer_list&quot;&gt;构造函数&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这里形参必须是引用，如果按照值传递就会有悖论。比如为了调用复制构造函数，必须复制一下。为了复制，必须调用复制构造函数......&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;我们这个函数他明显形参类型不是&lt;code&gt;C&lt;/code&gt;。所以他不会被当做拷贝构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所以自然而然，符合我们上面说的。我们第一个符合这个构造函数特征。然后类型会被推导。然后&lt;code&gt;C obj1(10);&lt;/code&gt;会被正常调用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然而第二个需要调用拷贝构造的时候，我们等于没有提供，我们此时使用的是编译器合成的。自然啥也没打印。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然而构造函数模板不可显式指定模板参数类型。因为显式模板实参列表跟在函数模板名之后，并且因为转换成员函数模板和&lt;strong&gt;构造函数成员函数模板是在不使用函数名的情况下调用的&lt;/strong&gt;，所以没有办法为这些函数模板提供显式模板实参列表。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意，&lt;code&gt;C obj1(10);&lt;/code&gt;这个东西不是调用构造函数。而是直接初始化。我们称之为调用构造函数是因为直接初始化就是在找构造函数。&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/silentNight/p/5545643.html&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/3960849/c-template-constructor&lt;/p&gt;
&lt;h2&gt;构造函数模板的延伸&lt;/h2&gt;
&lt;p&gt;我们有如下代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct foo {
    foo() = default; // 去掉会无法编译。
    // 我们有用户定义的构造函数, 所以编译器不会隐式生成默认构造函数。所以去掉会无法编译。
    template&amp;lt;typename T&amp;gt;
    foo(){
        cout &amp;lt;&amp;lt;&quot;ctor&quot; &amp;lt;&amp;lt; endl;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先, 构造函数可以是模板. 但是此处这个构造函数模板永远无法被调用, 原因是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;此处编译器永远无法推导出&lt;code&gt;T&lt;/code&gt;的类型. 因为此时&lt;code&gt;T&lt;/code&gt;不在推导上下文中（废话你都没有用到它）。同时也不存在为构造函数显式指定模板实参的方法，因为它们不会通过函数名调用.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;看&lt;a href=&quot;https://stackoverflow.com/questions/13002368/template-constructor-in-a-class-template-how-to-explicitly-specify-template-ar&quot;&gt;此处&lt;/a&gt; 和 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/function_template&quot;&gt;此处&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/constructor&quot;&gt;构造函数&lt;/a&gt;的显式实例化不能使用模板形参列表（语法 (1)），也始终不需要使用，因为能推导它们（语法 (2)）。
不存在为&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/operators&quot;&gt;重载的运算符&lt;/a&gt;、&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/cast_operator&quot;&gt;转换函数&lt;/a&gt;和构造函数显式指定模板实参的方法，因为它们不会通过函数名调用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以说: 这个函数模板从未被&quot;调用&quot;过. 调用需要加引号的原因是, 因为其实不是没有调用, 而是不存在这个函数. 因为这个函数模板没有被实例化. 所以从符号角度来讲压根没有这个函数.&lt;/p&gt;
&lt;h3&gt;如何“显式”指定构造函数模板的模板参数？使用std::in_place_type_t&lt;/h3&gt;
&lt;p&gt;参考自Raymond Chen的&lt;a href=&quot;https://devblogs.microsoft.com/oldnewthing/20241011-00/?p=110365&quot;&gt;这篇文章&lt;/a&gt;，和这篇&lt;a href=&quot;https://devblogs.microsoft.com/oldnewthing/20241025-00/?p=110420&quot;&gt;后续文章&lt;/a&gt; 简而言之就是多一个参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct foo {
    foo() = default;
    template&amp;lt;typename T1, typename... Args&amp;gt;
    foo(int a, Args&amp;amp;&amp;amp;... args) {
        cout &amp;lt;&amp;lt;&quot;ctor foo&quot; &amp;lt;&amp;lt; endl;
        T1(std::forward&amp;lt;Args&amp;gt;(args)...);
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;foo f1&amp;lt;int&amp;gt;(1);&lt;/code&gt; 这样做肯定不行。但我们可以使用类似于标签分派的方法，把一个东西扔到入参里面让他推导出&lt;code&gt;T1&lt;/code&gt;的类型。直接扔到入参内肯定不雅。所以标准库有一个&lt;code&gt;std::in_place_type_t&lt;/code&gt; （这玩意和它的兄弟们都是tag类型。比如&lt;code&gt;std::inplace&lt;/code&gt; 可以看看杂记5和EFF STL笔记）。&lt;/p&gt;
&lt;p&gt;言归正传，我们稍加修改下代码如这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myclass{
    myclass(int a, int b){
        cout &amp;lt;&amp;lt; &quot;ctor myclass&quot; &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    };
};
struct foo {
    foo() = default;
    template&amp;lt;typename T1, typename... Args&amp;gt;
    foo(int a, std::in_place_type_t&amp;lt;T1&amp;gt;, Args&amp;amp;&amp;amp;... args) {
        cout &amp;lt;&amp;lt;&quot;ctor foo&quot; &amp;lt;&amp;lt; endl;
        T1(std::forward&amp;lt;Args&amp;gt;(args)...);
    }
};
int main() {
    foo f1(1, std::in_place_type_t&amp;lt;int&amp;gt;{});
    foo f2(2, std::in_place_type_t&amp;lt;myclass&amp;gt;{}, 2, 3);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做就可以看到没有任何问题了。&lt;/p&gt;
&lt;h1&gt;函数模板参数推导表格和测试函数&lt;/h1&gt;
&lt;p&gt;测试函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void f(const T&amp;amp; param) {
    std::puts(__PRETTY_FUNCTION__);
}

int main() {
    int p = 2;
    f(p);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;已知A（实参类型）和P（形参类型）推T（T的类型）
P的类型可能是&lt;code&gt;T&lt;/code&gt;，&lt;code&gt;const T&lt;/code&gt;, &lt;code&gt;T&amp;amp;&lt;/code&gt;, &lt;code&gt;const T&amp;amp;&lt;/code&gt;, &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;const T&amp;amp;&amp;amp;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void f(P param){
    //...
}
int main(){
    f(A);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;P&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;T&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int*&lt;/td&gt;
&lt;td&gt;int*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int * const&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (*)(int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (*)(int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;td&gt;int *&amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int * const &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char (&amp;amp;)[2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char (&amp;amp;)[12]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (&amp;amp;)(int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;https://www.cnblogs.com/5iedu/p/11183878.html&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230227024236.png&quot; alt=&quot;QQ截图20230227024236&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;正确区分函数类型和函数指针类型在模板中的使用&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1&amp;gt;
class func{
    public:
    T1* callable; //这里必须是T1*
    func(T1 outter):callable(outter){};
    template&amp;lt;typename ...Args&amp;gt;
    void call(Args&amp;amp;&amp;amp; ...args){
        callable(forward&amp;lt;Args&amp;gt;(args)...);
    }
    void test(){
        std::puts(__PRETTY_FUNCTION__);
    }
};
template&amp;lt;typename T1&amp;gt;
class test;
template&amp;lt;typename T1, typename... T2&amp;gt; 
class test&amp;lt;T1(T2...)&amp;gt;{//实参匹配
    public:
    template&amp;lt;typename Func, typename ...Args&amp;gt;
    void construct(Func ptr, Args&amp;amp;&amp;amp; ...args){        
        func&amp;lt;T1(T2...)&amp;gt; obj(ptr);
        obj.call(forward&amp;lt;Args&amp;gt;(args)...);
        obj.test();
    }

};
int testsor(float b, int c){
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot;success&quot; &amp;lt;&amp;lt; endl;
    return 1;
}
int main(){
    test&amp;lt;int(float, int)&amp;gt; obj;
    obj.construct(testsor, 2.5f, 234);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为啥上面的部分必须是&lt;code&gt;T1*&lt;/code&gt;呢？&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;T1(T2...)&lt;/code&gt;会被推导为函数类型而非函数指针类型。也就是传入&lt;code&gt;func&lt;/code&gt;的模板参数时，&lt;code&gt;func&lt;/code&gt;的&lt;code&gt;T1&lt;/code&gt;会被推导为&lt;code&gt;int(float, int)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在杂记3中，我们区分了函数类型和函数指针类型。函数类型不可以声明变量。所以此时必须额外增加一个&lt;code&gt;*&lt;/code&gt;让其转换为指针类型。&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/17446220/c-function-types&lt;/p&gt;
&lt;h1&gt;什么是元函数&lt;/h1&gt;
&lt;p&gt;首先，这个词并不是官方定义的名词。元函数表面看起来非常高雅，但其实就是一个只在编译器进行计算的函数。它和普通的函数（运行期函数）具有截然不同的行为。这个函数不仅仅局限于函数，拥有同样作用的类依旧可以叫做元函数。我们其实可以认为元函数实现了类型萃取的功能。&lt;/p&gt;
&lt;p&gt;一个并不官方的元函数定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;输入（即参数）与输出（即返回值）均只包含两种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型（名）
&lt;ul&gt;
&lt;li&gt;比如我们的类型萃取中的类型定义，在某个函数或者类内实施一个&lt;code&gt;typedef/using&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;整形常量
&lt;ul&gt;
&lt;li&gt;比如我们固化某一个类型的某一个值为特定的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以返回一个或多个值，但不能没有返回值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此处返回值是广义的。比如下面的&lt;code&gt;type&lt;/code&gt;就算做返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;bool B, class L, class R&amp;gt;
struct IF{
  typedef R type;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有副作用：元函数在计算过程中既不能改变参数的值，也不具备“向控制台输入输出”之类的附加功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;元函数其实更多的作用是提供一种注册登记 或 映射的作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/32471222/c-are-trait-and-meta-function-synonymous&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/suparchor/article/details/115236785&lt;/p&gt;
&lt;h1&gt;完美转发的失败场景----effective modern c++ 条款30&lt;/h1&gt;
&lt;h2&gt;什么叫完美转发失败了？&lt;/h2&gt;
&lt;p&gt;假设我们有源函数&lt;code&gt;f&lt;/code&gt;和其包装的转发函数&lt;code&gt;wrapperf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果下面两个函数调用导致的操作不同，则称之为失败。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f(expression);
wrapperf(expression);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般来说完美转发会在下面的两个条件中任何一个成立的时候失败：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译器无法为一个或多个&lt;code&gt;wrapperf&lt;/code&gt;的形参推导出型别结果。在此情况下，代码无法编译通过。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;也就是模板参数推导失败。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;编译器为一个或多个 &lt;code&gt;wrapperf&lt;/code&gt;的形参推导出了错误的型别结果 。这里所谓错误的，既可以指 &lt;code&gt;wrapperf&lt;/code&gt;根据型别推导结果的实例化无法通过编译，也可以指以 &lt;code&gt;wrapperf&lt;/code&gt;推导而得的型别调用与直接以传递给 &lt;code&gt;wrapperf&lt;/code&gt;的实参调用行为不一致。这种分裂行为的源泉之一 ，可能在于&lt;code&gt;f&lt;/code&gt;是个重载函数的名字，然后，依据不正确的推导型别，&lt;code&gt;wrapperf&lt;/code&gt;里调用到的&lt;code&gt;f&lt;/code&gt;重载版本，就与直接调用&lt;code&gt;f&lt;/code&gt;的版本有异。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;也就是模板参数推导结果是错误的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;情况之一：花括号初始化器（大括号初始化物）&lt;/h2&gt;
&lt;p&gt;假设我们有如下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f(const vector&amp;lt;int&amp;gt;&amp;amp; t){ //目标函数
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}

template&amp;lt;typename... T&amp;gt;
void wrapperf(T&amp;amp;&amp;amp;... args){ //转发函数
    f(forward&amp;lt;T&amp;gt;(args)...);
}


int main(){
    f({1,2,3,4,5}); // OK
    wrapperf({1,2,3,4,5}); // 不行
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为啥第一个行，第二个不行？&lt;/p&gt;
&lt;p&gt;我们知道，编译器会进行函数的入参和形参比较，进行隐式类型转换，尽最大努力成功调用函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个行的原因是编译器进行了隐式类型转换。编译器会把&lt;code&gt;{1,2,3,4,5}&lt;/code&gt;隐式转换成&lt;code&gt;vector&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二个不行的原因是如果有转发函数，则没有隐式类型转换。这时候是通过类型推导的方式来获取入参的类型。&lt;strong&gt;但是花括号初始化器是不推导语境。注意这里是花括号初始化器而非&lt;code&gt;std::initializer_list&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;形参 P，其实参 A 是花括号初始化器列表，但 P 非 std::initializer_list、到它的引用（可以有 cv 限定），或者 (C++17 起)到数组的引用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;解决方案：&lt;/h4&gt;
&lt;p&gt;我们在聚合初始化中提到了：&lt;code&gt;auto&lt;/code&gt; 变量在以花括号初始化器完成初始化时，型别推导可以成功。这样的变量会被视为 &lt;code&gt;std::initializer_list&lt;/code&gt; 类型对象。这样一来就没问题了。因为&lt;code&gt;vector&lt;/code&gt;有一个接受 &lt;code&gt;std::initializer_list&lt;/code&gt; 对象的构造函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f(const vector&amp;lt;int&amp;gt;&amp;amp; t){ //目标函数
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}

template&amp;lt;typename... T&amp;gt;
void wrapperf(T&amp;amp;&amp;amp;... args){ //转发函数
    f(forward&amp;lt;T&amp;gt;(args)...);
}


int main(){
    auto param = {1,2,3,4,5};
    f(param); // OK
    wrapperf(param); // OK
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;情况之二：把0或NULL当做空指针&lt;/h2&gt;
&lt;p&gt;NULL就是0。杂记3中提到了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;而到了C++中，则变成了&lt;code&gt;#define NULL 0&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以这个情况模板推导会把它推导为&lt;code&gt;int&lt;/code&gt;而不是目标类型的空指针。为了避免这个问题可以使用&lt;code&gt;nullptr&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;情况之三：仅有声明的整型static const成员变量&lt;/h2&gt;
&lt;p&gt;涉及到常量替换。而且较为依靠编译器和链接器。不做介绍&lt;/p&gt;
&lt;h2&gt;情况之四：重载的函数和模板名字&lt;/h2&gt;
&lt;p&gt;假设我们有下面代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f(int(*pf)(int)){ //目标函数
    pf(100);
}

template&amp;lt;typename... T&amp;gt;
void wrapperf(T&amp;amp;&amp;amp;... args){ //转发函数
    f(forward&amp;lt;T&amp;gt;(args)...);
}

int testfunc(int a){
    cout &amp;lt;&amp;lt;&quot;testfunc&quot; &amp;lt;&amp;lt; endl;
    return 10;
}
int testfunc(int a, int b){
    cout &amp;lt;&amp;lt;&quot;testfunc overload&quot; &amp;lt;&amp;lt; endl;
    return 20;
}

int main(){
    f(testfunc); // OK
    wrapperf(testfunc); //不行
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个可以的原因是，编译器能知道我们具体传入的函数到底是哪个版本。因为此处不涉及到类型推导。而是重载决议。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二个不可以的原因是，这也是不推导语境。&lt;code&gt;testfunc&lt;/code&gt;没有类型信息。也就是入参为函数的时候且有多个候选函数的时候，是不推导语境。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;形参 &lt;code&gt;P&lt;/code&gt;，其实参 &lt;code&gt;A&lt;/code&gt; 是一个函数，没有函数或有多个函数与 &lt;code&gt;P&lt;/code&gt; 匹配的重载集，或包含一个或多个函数模板的重载集的时候属于不推导语境&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;解决方案&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;把传入函数换成传入函数指针即可。或者是强制类型转换一下。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int(*pf)(int) = testfunc;
f(pf); // OK
wrapperf(pf);// OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;情况之五：位域&lt;/h2&gt;
&lt;p&gt;不太了解，不做介绍&lt;/p&gt;
&lt;h2&gt;关于函数形参中的省略号&lt;/h2&gt;
&lt;p&gt;它其实还是变长参数列表。只不过C和C++规则有点不一样。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为变长形参对于&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/overload_resolution&quot;&gt;重载决议&lt;/a&gt;而言具有最低的优先级，所以它们常被用作 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/sfinae&quot;&gt;SFINAE&lt;/a&gt; 中的万应后备（catch-all fallback）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/variadic_arguments&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/60019443/what-does-ellipsis-as-one-and-only-function-parameter-in-a-function-protot&lt;/p&gt;
&lt;h1&gt;成员函数指针，成员指针和模板的搭配和坑点&lt;/h1&gt;
&lt;p&gt;我们在2.6提到了一部分。我们这里举个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class obj{
    public:
        bool func(int, int){
            std::cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
    	bool func2(){
            std::cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
   	    bool func3(int){
            std::cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
};


template&amp;lt;typename ret, typename... Args&amp;gt;
struct myclass{
    myclass() = default;

    template&amp;lt;typename T&amp;gt;
    myclass(ret(T::*ptr)(Args...)){ //成员指针的表达方法。
        std::puts(__PRETTY_FUNCTION__);
    };
};


int main(){
    myclass&amp;lt;bool, int, int&amp;gt; obj(&amp;amp;obj::func);
}
/*
myclass&amp;lt;ret, Args&amp;gt;::myclass(ret (T::*)(Args ...)) [with T = obj; ret = bool; Args = {int, int}]
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们可以清楚地看到，&lt;code&gt;ret&lt;/code&gt;和&lt;code&gt;args&lt;/code&gt;形参包都被正确匹配了。&lt;code&gt;T&lt;/code&gt;也被正确匹配了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;obj::func&lt;/code&gt;只是提取成员函数的地址。&lt;code&gt;obj&lt;/code&gt;是可以被匹配到T的。从字面上就能看出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看一下函数模板中的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename ret&amp;gt;
void testextract1(ret (T::*pf)){
    std::puts(__PRETTY_FUNCTION__);
}

template&amp;lt;typename T, typename ret, typename... Args&amp;gt;
void testextract2(ret (T::*pf)){ 
    std::puts(__PRETTY_FUNCTION__);
}

template&amp;lt;typename T, typename ret, typename... Args&amp;gt;
void testextract22(ret T::*pf){  //同2
    std::puts(__PRETTY_FUNCTION__);
}


template&amp;lt;typename T, typename ret, typename... Args&amp;gt;
void testextract3(ret (T::*pf)(Args...)){
    std::puts(__PRETTY_FUNCTION__);
}

template&amp;lt;typename T, typename ret, typename... Args&amp;gt;
void testextract44(ret T::*pf(Args...)){ //错误
    std::puts(__PRETTY_FUNCTION__);
}

template&amp;lt;typename T, typename ret, typename... Args&amp;gt;
void testextract4(ret (T::*pf)()){ //特殊
    std::puts(__PRETTY_FUNCTION__);
}

int main(){
    testextract1(&amp;amp;obj::func);
    testextract2(&amp;amp;obj::func);
    testextract3(&amp;amp;obj::func);
    testextract4(&amp;amp;obj::func);//错误
    testextract4(&amp;amp;obj::func2);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void testextract1(ret T::*) [with T = obj; ret = bool(int, int)]
void testextract2(ret T::*) [with T = obj; ret = bool(int, int); Args = {}]
void testextract3(ret (T::*)(Args ...)) [with T = obj; ret = bool; Args = {int, int}]
void testextract4(ret (T::*)()) [with T = obj; ret = bool; Args = {}]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意，返回值类型和函数指针类型不能分离。这俩是在一起的。所以不能不关心返回值类型。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;前两个例子当中，成员函数指针的类型是&lt;code&gt;ret&lt;/code&gt;。而&lt;code&gt;T&lt;/code&gt;只不过是绑定的对应对象的类型。为什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;因为c++语法需要通过&lt;code&gt;T::*&lt;/code&gt;外围是否有括号来判断是成员函数类型或是成员类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前两个，我们能看到&lt;code&gt;ret T::*&lt;/code&gt;之后没有括号。然后&lt;code&gt;ret&lt;/code&gt;整个被推导为指向该类成员的完整类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后两个，我们能看到&lt;code&gt;ret(T::*)&lt;/code&gt;之后有括号。然后&lt;code&gt;ret&lt;/code&gt;被推导为函数的返回值类型。也就是类型被正确的拆分了。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;注意，&lt;code&gt;testextract44&lt;/code&gt;是错误的。&lt;code&gt;ret T::*pf(Args...)&lt;/code&gt;暗示了&lt;code&gt;pf&lt;/code&gt;是一个指向类&lt;code&gt;T&lt;/code&gt;的数据成员的指针，并期望这个数据成员本身是一个函数类型。但是这是不允许的。因为C++不允许数据成员是函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意，此处针对例子1、2、22所谓的返回值类型其实不准确。应该称之为成员类型。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从第二个例子可以看出，在有可变参数的情况下，如果函数没有涉及到可变参数，则可变参数会变成空类型。因为允许为空。&lt;/li&gt;
&lt;li&gt;从第三个例子可以看出，在有可变参数的情况下，如果涉及到可变参数，则可变参数可以正确推导。&lt;/li&gt;
&lt;li&gt;从第四个例子可以看出，带有可变参数的情况下，如果&lt;strong&gt;参数位置是空括号，等同于参数是&lt;code&gt;void&lt;/code&gt;&lt;/strong&gt;。所以&lt;code&gt;testextract4(&amp;amp;obj::func);&lt;/code&gt;是错误的。因为其形参类型是&lt;code&gt;int, int&lt;/code&gt;。所以&lt;code&gt;testextract4(&amp;amp;obj::func2);&lt;/code&gt;是可以的。&lt;/li&gt;
&lt;li&gt;所以在要求匹配成员函数指针的时候，根据需求不同，需要给出不同的函数签名。也就是我们是否关心参数类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename ret, typename T&amp;gt;
void require1(ret (T::*)){ //不关心函数参数的时候
    std::puts(__PRETTY_FUNCTION__);
}
template&amp;lt;typename ret, typename T, typename... Args&amp;gt;
void require2(ret (T::*)(Args...)){ //关心函数参数的时候
    std::puts(__PRETTY_FUNCTION__);
}
int main(){
    require1(&amp;amp;obj::func);
    require1(&amp;amp;obj::func2);
    require2(&amp;amp;obj::func);
    require2(&amp;amp;obj::func2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;理解上述的区别，以及成员指针和成员函数指针的区分方式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;先说结论：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;成员指针包含成员函数指针。也就是成员函数指针是成员指针的子集。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成员指针为形参的函数既可以接受成员函数也可以接受成员变量。&lt;strong&gt;同时成员函数的参数不受限制。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时，以成员函数指针为形参的函数可以和以成员指针为形参的函数形成重载&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成员函数指针为形参的函数比成员指针为形参的函数更为特化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这也是区分成员函数指针和成员指针的一个方式。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;我们在杂记2中提到过类成员变量指针和类成员指针的具体语法。他们俩最大的区别就是后面有没有括号。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了方便理解，我们加上名字，然后换为更明显的语法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ret(T::*pf)&lt;/code&gt;/&lt;code&gt;ret T::*pf&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;后面没有括号。然后&lt;code&gt;ret&lt;/code&gt;整个被推导为整个成员指针类型。注意。这个时候&lt;code&gt;T::*pf&lt;/code&gt;本身带不带括号区别不大。因为此时都被认为是成员指针。（包含数据成员和函数成员）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ret(T::*pf)()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;后面有括号。然后&lt;code&gt;ret&lt;/code&gt;被推导为函数指针指向的函数的返回值类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何理解？为什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种，我们只知道&lt;code&gt;pf&lt;/code&gt;是一个成员指针。&lt;code&gt;ret&lt;/code&gt;是一个成员指针的类型，&lt;code&gt;T&lt;/code&gt;是其类类型。如果我们传入一个成员函数指针，则&lt;code&gt;ret&lt;/code&gt;就是成员指针类型。&lt;/li&gt;
&lt;li&gt;第二种，我们知道&lt;code&gt;pf&lt;/code&gt;是一个成员函数指针，所以&lt;code&gt;ret&lt;/code&gt;是一个函数的返回值类型，&lt;code&gt;T&lt;/code&gt;是其类类型。它只能指向成员函数，不能指向成员变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename ret&amp;gt;
void testextractmember1(ret (T::*pf)){
    std::puts(__PRETTY_FUNCTION__);
}

template&amp;lt;typename T, typename ret&amp;gt;
void testextractmember2(ret T::*pf){
    std::puts(__PRETTY_FUNCTION__);
}

testextractmember1(&amp;amp;obj::sb);
testextractmember2(&amp;amp;obj::sb);
testextractmember1(&amp;amp;obj::func3);
testextractmember2(&amp;amp;obj::func3);

//void testextractmember1(ret T::*) [with T = obj; ret = int]
//void testextractmember2(ret T::*) [with T = obj; ret = int]
//void testextractmember1(ret T::*) [with T = obj; ret = bool(int)]
//void testextractmember2(ret T::*) [with T = obj; ret = bool(int)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到。在针对成员指针的测试中，无论是数据成员指针还是函数成员指针都可以被匹配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;格外注意：函数就算带参数，也可以直接被成员指针接受。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename ret, typename T&amp;gt;
void diff(ret(T::*pf)){ //成员指针
    cout &amp;lt;&amp;lt;&quot;called variable&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    diff(&amp;amp;obj::val); //called variable
    diff(&amp;amp;obj::func2); //called variable
    diff(&amp;amp;obj::func3);//called variable 注意带参数的也可以
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这个测试证明了：成员指针为形参可以接受成员函数和成员变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename ret, typename T&amp;gt;
void diff(ret(T::*pf)()){ //成员函数指针
    cout &amp;lt;&amp;lt;&quot;called func&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    diff(&amp;amp;obj::val); //报错
    diff(&amp;amp;obj::func2);
	diff(&amp;amp;obj::func3);//报错
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这个测试证明了：成员函数指针为形参&lt;strong&gt;只&lt;/strong&gt;可以接受成员函数。&lt;strong&gt;并且要参数匹配。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename ret, typename T&amp;gt;
void diff(ret(T::*pf)){
    cout &amp;lt;&amp;lt;&quot;called variable&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename ret, typename T&amp;gt;
void diff(ret(T::*pf)()){
    cout &amp;lt;&amp;lt;&quot;called func&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    diff(&amp;amp;obj::val);	//called variable
    diff(&amp;amp;obj::func2);	//called func
    diff(&amp;amp;obj::func3); 	//called variable 格外注意这里匹配的问题
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这个测试证明了：以成员函数指针为形参的函数可以和以成员指针为形参的函数形成重载，且&lt;strong&gt;在一定程度上&lt;/strong&gt;可以正确匹配对应类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成员函数指针为形参的函数比成员指针为形参的函数更为特化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这也是区分成员函数指针和成员指针的一个方式。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是：为什么&lt;code&gt;diff(&amp;amp;obj::func3);&lt;/code&gt;匹配到了成员指针而非成员函数指针？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为成员函数指针的版本，函数指针的形参不一致。所以这个版本无法匹配，但是成员指针的版本总可以匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/72926596/type-deduction-for-a-member-function-pointer&lt;/p&gt;
&lt;h1&gt;继承自可变参数包（多重继承），类模板参数推导和用户定义的推导指引&lt;/h1&gt;
&lt;p&gt;12.4节。&lt;/p&gt;
&lt;p&gt;我们看一个C++17的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class... Ts&amp;gt; 
struct Foo : Ts... {  //继承自可变参数包
    using Ts::operator()...;  //使用using引入所有继承的父类的operator()
};
template&amp;lt;class... Ts&amp;gt; 
Foo(Ts...) -&amp;gt; Foo&amp;lt;Ts...&amp;gt;; //用户定义的推导指引 C++20起可以移除这段
// 因为CTAD从C++17引入，然后C++20放宽了限制。聚合类可以不需要用户定义推导指引。

int main() {
    Foo foo{
        [](int v){std::cout &amp;lt;&amp;lt; &quot;int&quot;;},
        [](double v){std::cout &amp;lt;&amp;lt; &quot;double&quot;;},
    };
    foo(1);
    foo(1.0);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，关于继承自可变参数包。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;假设可变参数包&lt;code&gt;Ts...&lt;/code&gt;的实参是&lt;code&gt;&amp;lt;T1, T2, T3&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;则这个&lt;code&gt;Foo&lt;/code&gt;相当于&lt;code&gt;struct Foo: T1, T2, T3{};&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后关于使用&lt;code&gt;using&lt;/code&gt;引入所有继承的父类的&lt;code&gt;operator()&lt;/code&gt;。这个没啥好解释的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后关于用户定义的推导指引，意思是告诉编译器，依照传入&lt;code&gt;Foo&lt;/code&gt;的构造函数的参数的类型来推导&lt;code&gt;Ts...&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;整个这段代码的意思是，我们看到我们构造函数传入了两个&lt;code&gt;lambda&lt;/code&gt;表达式。由于继承自可变参数包，则这个&lt;code&gt;Foo&lt;/code&gt;相当于继承自这两个lambda表达式的类型。我们也知道lambda类型相当于一个匿名类，含有&lt;code&gt;operator()&lt;/code&gt;，所以我们使用&lt;code&gt;using&lt;/code&gt;来引入所有继承的父类的&lt;code&gt;operator()&lt;/code&gt;。最后我们可以直接调用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/class_template_argument_deduction&lt;/p&gt;
&lt;h1&gt;一些实际参考案例&lt;/h1&gt;
&lt;h2&gt;根据任意可调用对象获取返回值类型/某一参数类型&lt;/h2&gt;
&lt;p&gt;下面这段代码的目的是针对任意可调用对象获取返回值类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
struct GetRet { //主模板，如果内部含有operator()就取它的类型
    private:
        using DT = std::decay_t&amp;lt;T&amp;gt;;
    public:

        using type = typename GetRet&amp;lt;decltype(&amp;amp;DT::operator())&amp;gt;::type;  //比较特殊，因为decltype(&amp;amp;DT::operator())是获取成员函数指针类型。在杂记4中提到过。然后递归调用
        using type1 = decltype(declval&amp;lt;T&amp;gt;()()); //展开是这样declval&amp;lt;T&amp;gt;().operator()()
        
        void pivot(){
            std::puts(__PRETTY_FUNCTION__);
            std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;decltype(&amp;amp;DT::operator()), int(T::*)()&amp;gt;; // true 注意是成员函数指针类型
            std::cout &amp;lt;&amp;lt; std::endl;
        }
};

// 函数类型的特化
template &amp;lt;typename R, typename... Args&amp;gt;
struct GetRet&amp;lt;R(Args...)&amp;gt; {
    using type = R;
    void pivot(){
            std::puts(__PRETTY_FUNCTION__);
    }
};

// 函数指针类型的特化
template &amp;lt;typename R, typename... Args&amp;gt;
struct GetRet&amp;lt;R(*)(Args...)&amp;gt; {
    using type = R;
    void pivot(){
            std::puts(__PRETTY_FUNCTION__);
    }
};

// 非静态成员函数指针类型的特化
template &amp;lt;typename T, typename R, typename... Args&amp;gt;
struct GetRet&amp;lt;R(T::*)(Args...)&amp;gt; {
    using type = R;
    void pivot(){
            std::puts(__PRETTY_FUNCTION__);
    }
};
// const非静态成员函数指针类型的特化
template &amp;lt;typename T, typename R, typename... Args&amp;gt;
struct GetRet&amp;lt;R(T::*)(Args...) const&amp;gt; {
    using type = R;
    void pivot(){
            std::puts(__PRETTY_FUNCTION__);
    }
};

template &amp;lt;typename T&amp;gt;
using GetRet_t = typename GetRet&amp;lt;T&amp;gt;::type;

// 测试用例
int f() {           //普通函数
    return 0;
}
struct T1 {             //非静态成员函数
    int mem_func(){
        cout &amp;lt;&amp;lt;&quot;T1 mem_func&quot; &amp;lt;&amp;lt; endl;
        return 0;
    }
};

struct T2 {
    int operator()(){ //operator()
        cout &amp;lt;&amp;lt;&quot;T2 ()&quot; &amp;lt;&amp;lt; endl;
        return 0;
    }
};

int main() {
    GetRet_t&amp;lt;decltype(f)&amp;gt; a;    // 函数类型

    GetRet_t&amp;lt;decltype(&amp;amp;f)&amp;gt; b;   // 函数指针类型

    GetRet_t&amp;lt;T2&amp;gt; c;             // 函数对象类型 -- 仿函数

    auto lamb = []()-&amp;gt;int{return 0;};   // lambda类型
    GetRet_t&amp;lt;decltype(lamb)&amp;gt; d;

    GetRet_t&amp;lt;decltype(&amp;amp;T1::mem_func)&amp;gt; e; // 非静态成员函数类型

    GetRet_t&amp;lt;std::function&amp;lt;int()&amp;gt;&amp;gt; g;   //函数对象类型 -- std::function对象

    std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;std::decay_t&amp;lt;decltype(a)&amp;gt;, int&amp;gt;; // true
    std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;std::decay_t&amp;lt;decltype(b)&amp;gt;, int&amp;gt;; // true
    std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;std::decay_t&amp;lt;decltype(c)&amp;gt;, int&amp;gt;; // true
    std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;std::decay_t&amp;lt;decltype(d)&amp;gt;, int&amp;gt;; // true
    std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;std::decay_t&amp;lt;decltype(e)&amp;gt;, int&amp;gt;; // true
    std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;std::decay_t&amp;lt;decltype(g)&amp;gt;, int&amp;gt;; // true
    std::cout &amp;lt;&amp;lt; std::is_same_v&amp;lt;decltype(declval&amp;lt;T2&amp;gt;()()), int&amp;gt;; // true
    std::cout &amp;lt;&amp;lt; std::endl;
    
    GetRet&amp;lt;decltype(f)&amp;gt; x1;
    GetRet&amp;lt;decltype(&amp;amp;f)&amp;gt; x2;
    GetRet&amp;lt;T2&amp;gt; x3;
    GetRet&amp;lt;decltype(lamb)&amp;gt; x4;
    GetRet&amp;lt;decltype(&amp;amp;T1::mem_func)&amp;gt; x5;
    GetRet&amp;lt;std::function&amp;lt;int()&amp;gt;&amp;gt; x6;
    x1.pivot();
    x2.pivot();
    x3.pivot();
    x4.pivot();
    x5.pivot();
    x6.pivot();
/*
void GetRet&amp;lt;R(Args ...)&amp;gt;::pivot() [with R = int; Args = {}]
void GetRet&amp;lt;R (*)(Args ...)&amp;gt;::pivot() [with R = int; Args = {}]
void GetRet&amp;lt;T&amp;gt;::pivot() [with T = T2]
void GetRet&amp;lt;T&amp;gt;::pivot() [with T = main()::&amp;lt;lambda()&amp;gt;]
void GetRet&amp;lt;R (T::*)(Args ...)&amp;gt;::pivot() [with T = T1; R = int; Args = {}]
void GetRet&amp;lt;T&amp;gt;::pivot() [with T = std::function&amp;lt;int()&amp;gt;]
*/
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子中，我们可以清楚的复习几个要点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;函数指针类型和函数类型不是一个东西&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员函数和模板搭配的语法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;还有&lt;code&gt;decltype&lt;/code&gt;获取成员函数指针类型的语法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尤其要注意，在主模板中，我们可以使用&lt;code&gt;type&lt;/code&gt;或&lt;code&gt;type1&lt;/code&gt;。而且要注意&lt;code&gt;type&lt;/code&gt;里面的&lt;code&gt;decltype(&amp;amp;DT::operator())&lt;/code&gt;会获取到&lt;strong&gt;成员函数指针&lt;/strong&gt;类型&lt;code&gt;int(T::*)()&lt;/code&gt;，所以是递归调用，然后我们通过它会匹配到成员函数指针类型的特化中，成功获取到&lt;code&gt;int&lt;/code&gt;。这也是为什么依旧要保留成员函数指针类型的特化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们上面弄了一堆特化，非常麻烦。不如直接使用&lt;code&gt;type1&lt;/code&gt;，也就是比较直观语义的&lt;code&gt;declval&lt;/code&gt;创建对象然后调用&lt;code&gt;operator()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;declval&lt;/code&gt;可以调用任意可调用对象，所以我们不需要区分函数类型和函数指针类型了。也就是不需要函数类型特化和函数指针类型特化的两个模板了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这时候结果就会变成：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;void GetRet&amp;lt;T&amp;gt;::pivot() [with T = int()]
void GetRet&amp;lt;T&amp;gt;::pivot() [with T = int (*)()]
void GetRet&amp;lt;T&amp;gt;::pivot() [with T = T2]
void GetRet&amp;lt;T&amp;gt;::pivot() [with T = test()::&amp;lt;lambda()&amp;gt;]
void GetRet&amp;lt;R (T::*)(Args ...)&amp;gt;::pivot() [with T = T1; R = int; Args = {}]
void GetRet&amp;lt;T&amp;gt;::pivot() [with T = std::function&amp;lt;int()&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关于获取某一特定参数的类型，就需要使用上面的&lt;code&gt;type&lt;/code&gt;了，也就是&lt;code&gt;&amp;amp;DT::operator()&lt;/code&gt;的方法，然后手动进行参数匹配，详细见&lt;a href=&quot;https://blog.csdn.net/fl2011sx/article/details/128864369&quot;&gt;这里&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++ 模板笔记（六）：标准库模板工具（ref、invoke、decay、void_t、declval 等）</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-6/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-6/</guid><pubDate>Wed, 14 Dec 2022 14:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;std::is_const&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;is_const&amp;lt;T&amp;gt;::value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用于判断&lt;code&gt;T&lt;/code&gt;类型是否是被&lt;code&gt;const&lt;/code&gt;修饰的类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 T 为引用类型则 &lt;code&gt;is_const&amp;lt;T&amp;gt;::value&lt;/code&gt; 始终为 &lt;code&gt;false&lt;/code&gt; 。检查可能为引用的类型的常性的正确方式是移除引用： &lt;code&gt;is_const&amp;lt;typename remove_reference&amp;lt;T&amp;gt;::type&amp;gt; &lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;原因是引用的&lt;code&gt;const&lt;/code&gt; 限定符仅仅意味着不能通过引用参数修改值。 它仍然可以被其他方法修改。比如直接修改变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
	std::cout &amp;lt;&amp;lt; std::boolalpha;

	std::cout &amp;lt;&amp;lt; &quot;is_const:&quot; &amp;lt;&amp;lt; std::endl;
	std::cout &amp;lt;&amp;lt; &quot;int: &quot; &amp;lt;&amp;lt; std::is_const&amp;lt;int&amp;gt;::value &amp;lt;&amp;lt; std::endl;
	std::cout &amp;lt;&amp;lt; &quot;const int: &quot; &amp;lt;&amp;lt; std::is_const&amp;lt;const int&amp;gt;::value &amp;lt;&amp;lt; std::endl;
	std::cout &amp;lt;&amp;lt; &quot;const int&amp;amp;: &quot; &amp;lt;&amp;lt; std::is_const&amp;lt;const int&amp;amp;&amp;gt;::value &amp;lt;&amp;lt; std::endl;
	std::cout &amp;lt;&amp;lt; &quot;const int* &quot; &amp;lt;&amp;lt; std::is_const&amp;lt;const int*&amp;gt;::value &amp;lt;&amp;lt; std::endl;
	std::cout &amp;lt;&amp;lt; &quot;int* const:&quot; &amp;lt;&amp;lt; std::is_const&amp;lt;int* const&amp;gt;::value &amp;lt;&amp;lt; std::endl;
	return 0;
}
/*
is_const: 
int: false
const int: true
const int&amp;amp;: false

const int*: false 这里的false是因为指针本身非const。也就是顶层非const。
int* const:true 这里的true是因为指针本身是const。也就是顶层为const
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模拟简单实现&lt;/h2&gt;
&lt;p&gt;也是简单的匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp&amp;gt;
struct my_is_const{
    void func(){
        cout &amp;lt;&amp;lt;&quot;not const&quot; &amp;lt;&amp;lt; endl;
    }
};

template&amp;lt;typename _Tp&amp;gt;
struct my_is_const&amp;lt;_Tp const&amp;gt;{
    void func(){
        cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
    }
};
my_is_const&amp;lt;int&amp;gt; a;
my_is_const&amp;lt;const int&amp;gt;b;
a.func(); //not const
b.func(); // const
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;std::ref, std::cref, std::reference_warpper&lt;/h1&gt;
&lt;p&gt;之前一直想整理一下这几个。这次一起探讨一下&lt;/p&gt;
&lt;p&gt;在很多时候比如使用&lt;code&gt;bind&lt;/code&gt;和&lt;code&gt;thread&lt;/code&gt;这种一定会拷贝参数的标准库组件时，我们如果想要按照引用传递，就必须使用&lt;code&gt;ref&lt;/code&gt;或&lt;code&gt;cref&lt;/code&gt;包装一下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一句话总结。&lt;code&gt;ref&lt;/code&gt;和&lt;code&gt;cref&lt;/code&gt;都是&lt;code&gt;reference_warpper&lt;/code&gt;的包装器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cref&lt;/code&gt;只是变成了带&lt;code&gt;const&lt;/code&gt;的&lt;code&gt;ref&lt;/code&gt;。所以下面不分析&lt;code&gt;cref&lt;/code&gt;了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;reference_warpper&lt;/code&gt;内部包含一个指向了原始对象类型的指针。其实他就是一个包含了一个指针的包装器对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它有用户定义转换函数。&lt;/li&gt;
&lt;li&gt;他还有一个&lt;code&gt;get()&lt;/code&gt;函数用于返回其原始对象的引用，通过这个方式可以像引用一样，内部修改的值可以传递到外面。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其实应该说&lt;code&gt;ref&lt;/code&gt;和&lt;code&gt;cref&lt;/code&gt;是&lt;code&gt;reference_warpper&lt;/code&gt;的辅助函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;std::reference_warpper&lt;/code&gt;其实是一种&lt;strong&gt;强别名&lt;/strong&gt;. 参考资料&lt;a href=&quot;https://www.zhihu.com/question/518132411/answer/2360830245&quot;&gt;来自这里&lt;/a&gt; 看下&lt;code&gt;CPO的故事(3)&lt;/code&gt;的后半部分&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ref 源代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp&amp;gt;
inline reference_wrapper&amp;lt;_Tp&amp;gt; ref(_Tp&amp;amp; __t) noexcept
{ return reference_wrapper&amp;lt;_Tp&amp;gt;(__t); }

template&amp;lt;typename _Tp&amp;gt;
void ref(const _Tp&amp;amp;&amp;amp;) = delete; //禁止接受右值

template&amp;lt;typename _Tp&amp;gt;
inline reference_wrapper&amp;lt;_Tp&amp;gt; ref(reference_wrapper&amp;lt;_Tp&amp;gt; __t) noexcept
{ return ref(__t.get()); }

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们可以一眼看出，&lt;code&gt;ref&lt;/code&gt;就是一个返回&lt;code&gt;reference_wrapper&lt;/code&gt;的函数&lt;/li&gt;
&lt;li&gt;因为禁止接受了形参为右值的参数。所以&lt;code&gt;ref&lt;/code&gt;不能包装右值。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::ref(5);//不可以
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ref&lt;/code&gt;的传入参数可以是一个普通的引用，也可以是另外一个&lt;code&gt;reference_wrapper&lt;/code&gt;对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;reference_wrapper 源代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp&amp;gt;
class reference_wrapper : public _Reference_wrapper_base&amp;lt;typename remove_cv&amp;lt;_Tp&amp;gt;::type&amp;gt; //这里去掉了CV。
{
    _Tp* _M_data; //内部就是包含一个指向原始对象的指针。

public:
    typedef _Tp type;

    reference_wrapper(_Tp&amp;amp; __indata) noexcept
        :_M_data(std::__addressof(__indata))
    {
    }

    reference_wrapper(_Tp&amp;amp;&amp;amp;) = delete;

    reference_wrapper(const reference_wrapper&amp;lt;_Tp&amp;gt;&amp;amp; __inref) noexcept
        :_M_data(__inref._M_data)
    {
    }

    reference_wrapper&amp;amp; operator=(const reference_wrapper&amp;lt;_Tp&amp;gt;&amp;amp; __inref) noexcept
    {
        _M_data = __inref._M_data;
        return *this;
    }

    operator _Tp&amp;amp;() const noexcept //用户定义转换函数。使用了get
    { return this-&amp;gt;get(); }

    _Tp&amp;amp; get() const noexcept //get用于返回原始对象的引用。起到引用的效果。
    { return *_M_data; }

    template&amp;lt;typename... _Args&amp;gt;
    typename result_of&amp;lt;_Tp&amp;amp;(_Args&amp;amp;&amp;amp;...)&amp;gt;::type
    operator()(_Args&amp;amp;&amp;amp;... __args) const
    {
        return __invoke(get(), std::forward&amp;lt;_Args&amp;gt;(__args)...);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;该类继承于&lt;code&gt;_Reference_wrapper_base&lt;/code&gt;_&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有一个类成员&lt;code&gt;_M_data&lt;/code&gt;，类型为所引用类型的指针。这个指针指向了原始对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一个构造函数通过调用&lt;code&gt;std::__addressof&lt;/code&gt;函数，获得了指向引用参数的指针，并赋值给了&lt;code&gt;_M_data&lt;/code&gt;（这也是为什么不支持右值引用的原因，因为取不到对应的地址）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;至于这里为什么要使用&lt;code&gt;std::__addressof&lt;/code&gt;，是因为如果有些类重载了&lt;code&gt;operator &amp;amp;&lt;/code&gt; 的话可能我们不能取到真实地址。使用这个函数不管是否重载了取地址操作符，都可以取到原始地址。具体细节不分析了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝构造函数和赋值函数就只是简单地将&lt;code&gt;_M_data&lt;/code&gt;的值进行传递而已，没有什么特殊操作。都是常规&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其余方法就是为了让&lt;code&gt;reference_wrapper&lt;/code&gt;展现出和普通的引用一样的效果而进行的运算符重载。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如重载了&lt;code&gt;operator ()&lt;/code&gt;可以让我们在&lt;code&gt;reference_wrapper&lt;/code&gt;对象储存了一个可调用对象的时候，直接使用&lt;code&gt;()&lt;/code&gt;调用其储存的可调用对象&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void this_func(){
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}

int main() {
    auto func_ptr = this_func;
    auto ref3 = ref(func_ptr); //ref3是一个reference_wrapper储存了一个函数指针
    ref3();//调用。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;比如有&lt;code&gt;get()&lt;/code&gt;可以获取一个原始对象的引用。&lt;/li&gt;
&lt;li&gt;用户定义转换函数使得它支持了向原始类型的隐式类型转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举一个非常不太对劲的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void another_func(T a){
    a.get() = 2000;
}
int main() {
    int a = 1;
    another_func(ref(a));
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; //修改为2000。
}

//或者是
void changeval(int&amp;amp; a){
    a = 20000;
}

template &amp;lt;typename T&amp;gt;
void func(T a){
    changeval(a); //因为有用户定义转换函数。这里的T本来是reference_wrapper类型的，但是可以隐式转换为原始类型。比如这里就是int&amp;amp;
}
int main() {
    int a = 1;
    func(ref(a));
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; //修改为20000

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意了。在这些情况下，&lt;code&gt;reference_wrapper&lt;/code&gt;对象本身依旧是按值传递的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/weixin_43798887/article/details/116562336&lt;/p&gt;
&lt;h1&gt;std::remove_reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;若类型 &lt;code&gt;T&lt;/code&gt; 为引用类型，则提供成员 typedef &lt;code&gt;type&lt;/code&gt; ，其为 &lt;code&gt;T&lt;/code&gt; 所引用的类型。否则 &lt;code&gt;type&lt;/code&gt; 为 &lt;code&gt;T&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个的实现比较简单。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt; 
struct remove_reference{
    typedef T type;
};
template&amp;lt;class T&amp;gt; 
struct remove_reference&amp;lt;T&amp;amp;&amp;gt;{
    typedef T type;
};
template&amp;lt;class T&amp;gt; 
struct remove_reference&amp;lt;T&amp;amp;&amp;amp;&amp;gt;{
    typedef T type;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当T是&lt;code&gt;&amp;amp;&lt;/code&gt;或者&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;的时候，自然都会有每一个匹配的版本。都会让其&lt;code&gt;T&lt;/code&gt;本身的类型成为其类型。也就是脱去引用。&lt;/p&gt;
&lt;h1&gt;std::remove_cv/remove_volatile/remove_const&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;第一个移除&lt;strong&gt;最顶层&lt;/strong&gt;CV&lt;/li&gt;
&lt;li&gt;第二个移除&lt;strong&gt;最顶层&lt;/strong&gt;V&lt;/li&gt;
&lt;li&gt;第三个移除&lt;strong&gt;最顶层&lt;/strong&gt;C&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果忘了什么叫顶层，去杂记2搜“顶层”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 200;
const int b = 200;
const int* ptr = &amp;amp;a;
int* const c_ptr = &amp;amp;a;
const int* const cc_ptr = &amp;amp;a;
using anothertype = remove_cv_t&amp;lt;const int&amp;gt;;
cout &amp;lt;&amp;lt;std::boolalpha;
cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(a), remove_cv_t&amp;lt;decltype(b)&amp;gt;&amp;gt; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_same_v&amp;lt;const int*, remove_cv_t&amp;lt;decltype(ptr)&amp;gt;&amp;gt; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_same_v&amp;lt;int*, remove_cv_t&amp;lt;decltype(c_ptr)&amp;gt;&amp;gt; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_same_v&amp;lt;const int*, remove_cv_t&amp;lt;decltype(cc_ptr)&amp;gt;&amp;gt; &amp;lt;&amp;lt; endl;
/*
true
true
true
true
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个，移除了&lt;code&gt;b&lt;/code&gt;的最顶层&lt;code&gt;const&lt;/code&gt;，变成了&lt;code&gt;int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第二个，移除了&lt;code&gt;ptr&lt;/code&gt;的最顶层&lt;code&gt;const&lt;/code&gt;，但是&lt;code&gt;ptr&lt;/code&gt;本身并非&lt;code&gt;const&lt;/code&gt;，而是&lt;code&gt;ptr&lt;/code&gt;指向的对象是&lt;code&gt;const&lt;/code&gt;。所以此时移除无效。依旧是&lt;code&gt;const int*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第三个，移除了&lt;code&gt;c_ptr&lt;/code&gt;的最顶层&lt;code&gt;const&lt;/code&gt;。这个&lt;code&gt;c_ptr&lt;/code&gt;本身是&lt;code&gt;const&lt;/code&gt;，所以移除后变成了&lt;code&gt;int*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第四个，移除了&lt;code&gt;cc_ptr&lt;/code&gt;的最顶层&lt;code&gt;const&lt;/code&gt;。这个&lt;code&gt;cc_ptr&lt;/code&gt;不仅本身是&lt;code&gt;const&lt;/code&gt;，指向的对象也是&lt;code&gt;const&lt;/code&gt;。由于仅移除最顶层，所以变成&lt;code&gt;const int*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;简单实现&lt;/h2&gt;
&lt;p&gt;依旧是非常直观的偏特化匹配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class T &amp;gt; struct remove_cv                   { typedef T type; };
template&amp;lt; class T &amp;gt; struct remove_cv&amp;lt;const T&amp;gt;          { typedef T type; };
template&amp;lt; class T &amp;gt; struct remove_cv&amp;lt;volatile T&amp;gt;       { typedef T type; };
template&amp;lt; class T &amp;gt; struct remove_cv&amp;lt;const volatile T&amp;gt; { typedef T type; };
 
template&amp;lt; class T &amp;gt; struct remove_const                { typedef T type; };
template&amp;lt; class T &amp;gt; struct remove_const&amp;lt;const T&amp;gt;       { typedef T type; };
 
template&amp;lt; class T &amp;gt; struct remove_volatile             { typedef T type; };
template&amp;lt; class T &amp;gt; struct remove_volatile&amp;lt;volatile T&amp;gt; { typedef T type; };
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;std::address_of&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class T &amp;gt;
T* addressof(T&amp;amp; arg) noexcept; //1
template &amp;lt;class T&amp;gt;
const T* addressof(const T&amp;amp;&amp;amp;) = delete; //2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;获得对象或函数 &lt;code&gt;arg&lt;/code&gt; 的实际地址，即使存在 &lt;code&gt;operator&amp;amp;&lt;/code&gt; 的重载  (1)&lt;/li&gt;
&lt;li&gt;右值重载被删除，以避免取 &lt;code&gt;const&lt;/code&gt; 右值的地址。(2)&lt;/li&gt;
&lt;li&gt;返回值是指向 &lt;code&gt;arg&lt;/code&gt; 的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在&lt;code&gt;reference_weapper&lt;/code&gt;源代码分析一节中提到了&lt;code&gt;address_of&lt;/code&gt;。它的核心作用是取得对象或函数的真实地址。因为有时候可能你使用的&lt;code&gt;operator&amp;amp;&lt;/code&gt;是被重载过的。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test
{
public:
    int* operator&amp;amp;() //重载address-of也就是取址运算符
    {
        return &amp;amp;b;
    }

    int* a_addr()
    {
        return &amp;amp;a; //注意，这个&amp;amp;用的不是重载的。因为a是int类型，这句等于a.operator&amp;amp;()，也就是int自己的取址而不是这里Test的取址
    }

    int* b_addr()
    {
        return &amp;amp;b; //注意，这个&amp;amp;用的不是重载的。因为b是int类型，这句等于b.operator&amp;amp;()，也就是int自己的取址而不是这里Test的取址
    }

private:
    int a;
    int b;
};

int main(int argc, char* argv[])
{
    Test t;
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;t.a:&quot; &amp;lt;&amp;lt; t.a_addr() &amp;lt;&amp;lt; std::endl; //0x7ffe80a9fc10
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;t.b:&quot; &amp;lt;&amp;lt; t.b_addr() &amp;lt;&amp;lt; std::endl; //0x7ffe80a9fc14
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;t:&quot; &amp;lt;&amp;lt; &amp;amp;t &amp;lt;&amp;lt; std::endl;			//0x7ffe80a9fc14 注意，这里变成了b的地址。
    std::cout &amp;lt;&amp;lt; &quot;addressof(t):&quot; &amp;lt;&amp;lt; std::addressof(t) &amp;lt;&amp;lt; std::endl;//0x7ffe80a9fc10
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到，直接对&lt;code&gt;t.a&lt;/code&gt;和&lt;code&gt;t.b&lt;/code&gt;取地址是正常的。但是使用了&lt;code&gt;t&lt;/code&gt;的重载的&lt;code&gt;operator&amp;amp;&lt;/code&gt;，由于我们让它取了&lt;code&gt;t.b&lt;/code&gt;的地址，所以返回的一定是&lt;code&gt;t.b&lt;/code&gt;的地址。但是我们用了&lt;code&gt;addressof&lt;/code&gt;就可以避免这个问题。&lt;/p&gt;
&lt;h2&gt;实现分析&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
typename std::enable_if&amp;lt;std::is_object&amp;lt;T&amp;gt;::value, T*&amp;gt;::type  addressof(T&amp;amp; arg) noexcept{
    return reinterpret_cast&amp;lt;T*&amp;gt;(//第四步
               &amp;amp;const_cast&amp;lt;char&amp;amp;&amp;gt;(//第二步和第三步。第三步单独指的取址&amp;amp;
                   reinterpret_cast&amp;lt;const volatile char&amp;amp;&amp;gt;(arg))); //第一步
}
 
template&amp;lt;class T&amp;gt;
typename std::enable_if&amp;lt;!std::is_object&amp;lt;T&amp;gt;::value, T*&amp;gt;::type addressof(T&amp;amp; arg) noexcept{
    return &amp;amp;arg;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一步，将&lt;code&gt;arg&lt;/code&gt;由类型&lt;code&gt;T&amp;amp;&lt;/code&gt;强制转换为&lt;code&gt;const volatile char&amp;amp;&lt;/code&gt;，这样做有两个作用：
&lt;ul&gt;
&lt;li&gt;一是防止后面（第三步）使用&lt;code&gt;&amp;amp;&lt;/code&gt;操作符获取地址时触发原类型(即&lt;code&gt;T&lt;/code&gt;)的重载操作(&lt;code&gt;operator&amp;amp;&lt;/code&gt;)，就像上面那个例子那样。&lt;/li&gt;
&lt;li&gt;二是&lt;code&gt;reinterpret_cast&lt;/code&gt;不可以去掉原类型的&lt;code&gt;const&lt;/code&gt;或&lt;code&gt;volatile&lt;/code&gt;，但是可以合法的在原类型的基础上&lt;strong&gt;增加&lt;/strong&gt;&lt;code&gt;const&lt;/code&gt;或&lt;code&gt;volatile&lt;/code&gt;, 所以，如果&lt;code&gt;T&lt;/code&gt;原来就带有&lt;code&gt;const&lt;/code&gt;或&lt;code&gt;volatile&lt;/code&gt;的话, 通过&lt;code&gt;reinterpret_cast&lt;/code&gt;去掉是不允许的, 因此需要加上&lt;code&gt;const volatile&lt;/code&gt;来避免编译器报错, 也就是此时不用再管&lt;code&gt;T&lt;/code&gt;是否本来就带有&lt;code&gt;const&lt;/code&gt;或&lt;code&gt;volatile&lt;/code&gt;属性了。
&lt;ul&gt;
&lt;li&gt;可能会问为啥不直接&lt;code&gt;const_cast&lt;/code&gt;？&lt;code&gt;char&lt;/code&gt;和&lt;code&gt;T&lt;/code&gt;是无关类型，只能用&lt;code&gt;reinterpret_cast&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;可能会问为啥要&lt;code&gt;cast&lt;/code&gt;到引用&lt;code&gt;&amp;amp;&lt;/code&gt;？因为如果&lt;code&gt;cast&lt;/code&gt;到变量就会生成新变量，这样地址不就变了么？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第二步，将前面转换得到的结果强制转换为&lt;code&gt;char&amp;amp;&lt;/code&gt;类型，**此时如果转换成其它类型有可能会触发强制地址对齐的操作，这样的话真实地址就有可能会被改变了，最终造成程序错误。**需要注意的是这个转换过程使用的是&lt;code&gt;const_cast&lt;/code&gt;，可以顺便将前面留下的&lt;code&gt;const&lt;/code&gt;和&lt;code&gt;volatile&lt;/code&gt;属性给去掉了。&lt;/li&gt;
&lt;li&gt;第三步使用&lt;code&gt;&amp;amp;&lt;/code&gt;符号将前面的结果的地址给取出来（此时已经不会触发重载了）&lt;/li&gt;
&lt;li&gt;最后一步使用&lt;code&gt;reinterpret_cast&lt;/code&gt;将前面获取到的地址转换回&lt;code&gt;T*&lt;/code&gt;类型，并且此时也会保留&lt;code&gt;T&lt;/code&gt;的&lt;code&gt;const&lt;/code&gt;或&lt;code&gt;volatile&lt;/code&gt;属性（如果有的话）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/16195032/implementation-of-addressof&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/weixin_43798887/article/details/117966866&lt;/p&gt;
&lt;h2&gt;重载取址运算符用在哪？代理类的时候会用到&lt;/h2&gt;
&lt;p&gt;参考more effective C++ 条款30 P.224&lt;/p&gt;
&lt;p&gt;一般当一个对象内涵一个代理类，通过某些操作返回一个代理类对象的指针的时候，由于不同类型指针之间不存在天然的转换关系，所以这个时候需要重载一个取址运算符来自然化语义。让对一个代理类取址的时候取到的是一个我们真正想取地址的类型的地址。&lt;/p&gt;
&lt;p&gt;但是非常不建议使用，原因参考Modern C++ design 7.6 P.170&lt;/p&gt;
&lt;h1&gt;std::invoke [C++17]&lt;/h1&gt;
&lt;p&gt;https://youtu.be/zt7ThwVfap0 非常好的视频。&lt;/p&gt;
&lt;h2&gt;std::invoke 是什么&lt;/h2&gt;
&lt;p&gt;invoke是C++17新出的功能。&lt;/p&gt;
&lt;p&gt;我们在上面11.1提到了可调用对象。比如有函数指针，成员函数指针，&lt;code&gt;std::function&lt;/code&gt;等. invoke让我们有一个更统一的方式来调用任何的可调用对象。&lt;/p&gt;
&lt;h2&gt;std::invoke 和 std::function 和 其他可调用对象的关系&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class F, class... Args &amp;gt;
std::invoke_result_t&amp;lt;F, Args...&amp;gt;
  invoke( F&amp;amp;&amp;amp; f, Args&amp;amp;&amp;amp;... args ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;......&lt;/p&gt;
&lt;p&gt;以参数 &lt;code&gt;f&lt;/code&gt; 调用&lt;a href=&quot;https://zh.cppreference.com/w/cpp/named_req/Callable&quot;&gt;&lt;em&gt;可调用&lt;/em&gt; &lt;em&gt;(Callable)&lt;/em&gt; &lt;/a&gt;对象。如同以 &lt;code&gt;INVOKE(std::forward&amp;lt;F&amp;gt;(f), std::forward&amp;lt;Args&amp;gt;(args)...) &lt;/code&gt;。此重载只有在 &lt;code&gt;std::is_invocable_v&amp;lt;F, Args...&amp;gt; &lt;/code&gt;为 &lt;code&gt;true&lt;/code&gt; 时才参与重载决议。&lt;/p&gt;
&lt;p&gt;.......&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;标准文档一向不说人话。&lt;/p&gt;
&lt;p&gt;具体就是：&lt;code&gt;std::function&lt;/code&gt;可以包装其他的可调用对象。而&lt;code&gt;std::invoke&lt;/code&gt;可以调用如&lt;code&gt;std::function&lt;/code&gt;这样的可调用对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;code&gt;invoke&lt;/code&gt;是有返回值的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/39398740/what-is-the-difference-between-stdinvoke-and-stdfunction&lt;/p&gt;
&lt;h2&gt;std::invoke 怎么用&lt;/h2&gt;
&lt;p&gt;std::invoke在模板类中调用成员函数指针的时候尤为有用。&lt;/p&gt;
&lt;p&gt;看看语法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用函数对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;invoke(func, args...);
//等同于
func(args...);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用成员函数指针(pointer to member function)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果可调用对象是一个指向成员函数的指针，它会将 &lt;code&gt;args...&lt;/code&gt;中的第一个参数当作&lt;code&gt;this&lt;/code&gt;对象（不是指针）。&lt;code&gt;Args...&lt;/code&gt;中其余的参数则被当做常规参数传递给可调用对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体原始调用方式在杂记2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;invoke(pmf, obj, rest...);//通过对象调用
//等同于
(obj.*pmf)(rest...);

invoke(pmf, ptr, rest...);//通过指针调用
//等同于
(ptr-&amp;gt;*pmf)(rest...);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为了化简使用，有一个特殊例子。如果可调用对象是一个指向成员函数的指针，它会将 &lt;code&gt;args...&lt;/code&gt;中的第一个参数当作 &lt;code&gt;this &lt;/code&gt;对象（不是指针）。&lt;code&gt;Args...&lt;/code&gt;中其余的参数则被当做常规参数传递给可调用对象。否则，所有的参数都被直接传递给可调用对象。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用成员变量指针 （pointer to member data)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;具体原始调用方式在杂记2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;invoke(pmd, obj);//通过对象调用
//等同于
obj.*pmd;

invoke(ptr, obj);//通过指针调用
//等同于
ptr-&amp;gt;*obj;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一点儿具体例子&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//函数对象
template&amp;lt;typename _funcObj, typename... Args&amp;gt;
void test_func(_funcObj ptr, Args&amp;amp;&amp;amp;... args){
    // ptr(args...);
    std::invoke(ptr, forward&amp;lt;Args&amp;gt;(args)...); //注意加了forward
}

//成员函数指针，对象调用。
template&amp;lt;typename _Obj, typename _Ptr, typename... Args&amp;gt;
void test_memberfunc( _Ptr ptr, _Obj&amp;amp; obj, Args&amp;amp;&amp;amp;... args){
    //(obj.*ptr)(args...);
    std::invoke(ptr, obj, forward&amp;lt;Args&amp;gt;(args)...); //注意加了forward
}

//成员函数指针，指针调用。
template&amp;lt;typename _Obj, typename _Ptr, typename... Args&amp;gt;
void test_memberfunc_ptr(_Ptr ptr, _Obj* obj, Args&amp;amp;&amp;amp;... args){
    //((*obj).*ptr)(args...);
    std::invoke(ptr, obj, forward&amp;lt;Args&amp;gt;(args)...); //注意加了forward
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;myobj obj1(1);
function&amp;lt;void(int)&amp;gt; task = bind(&amp;amp;myobj::add, &amp;amp;obj1, placeholders::_1);
std::invoke(task, 5); //调用function包装的成员函数

test_memberfunc(&amp;amp;myobj::getval, obj1); //使用成员函数指针，对象调用
test_memberfunc(&amp;amp;myobj::add, obj1, 8);

test_memberfunc_ptr(&amp;amp;myobj::add, &amp;amp;obj1, 8); //使用成员函数指针，指针调用

test_func(&amp;amp;myobj::add, &amp;amp;obj1, 8); //使用提到的特殊例子，使用成员函数指针。会被特殊处理。


&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意直接调用模板函数必须指明模板参数：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T func(T obj){
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
    return obj;
}


int main() {
    auto f = std::invoke(func&amp;lt;int&amp;gt;, 100); //必须显式指明&amp;lt;int&amp;gt;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里由于是直接调用一个模板函数。&lt;code&gt;invoke&lt;/code&gt;没办法帮我们推导这个函数应该用哪个模板参数实例化。我们上面用到的都是包了一层的，也就是&lt;code&gt;invoke&lt;/code&gt;的时候，&lt;code&gt;invoke&lt;/code&gt;内的参数有确定类型。比如上面的&lt;code&gt;ptr&lt;/code&gt;在传入&lt;code&gt;invoke&lt;/code&gt;的时候，类型已经确定为&lt;code&gt;_Ptr&lt;/code&gt;。而当前这里还需要推导，但是无法推导。所以必须显式指明。&lt;/p&gt;
&lt;h2&gt;std::invoke 源码和实现&lt;/h2&gt;
&lt;p&gt;目前有点看不懂&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/windpiaoxue/p/10009172.html&lt;/p&gt;
&lt;p&gt;https://codechina.gitcode.host/programmer/cpp-update/3-C++17-features-spotlights-2.html#stdinvoke&lt;/p&gt;
&lt;h3&gt;std::invoke 什么时候用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;非泛型编程没必要用，除非痛恨成员函数指针语法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;泛型编程可以使用，但是不要指定其&lt;code&gt;invoke&lt;/code&gt;的模板参数。把它留给&lt;code&gt;invoke&lt;/code&gt;决定&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实现&lt;code&gt;invoke&lt;/code&gt;功能是非常困难的事情。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;std::invoke 的陷阱，到底要不要使用forward&lt;/h2&gt;
&lt;p&gt;书中提到了，invoke中传递的参数不应该使用完美转发， 因为第一次调用可能会偷取相关参数的值（使用移动构造），导致随后的调用中出现错误。&lt;/p&gt;
&lt;p&gt;但是如果一个对象是不可拷贝仅可移动的呢？比如&lt;code&gt;unique_ptr&lt;/code&gt;，这时候就必须要使用forward了。来自&lt;a href=&quot;https://stackoverflow.com/questions/65025812/c-does-stdinvoke-need-perfect-forwarding&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void p_2(unique_ptr&amp;lt;int&amp;gt; myptr){
    cout &amp;lt;&amp;lt;&quot;p_2 called&quot;&amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; *myptr &amp;lt;&amp;lt; endl;
}
unique_ptr&amp;lt;int&amp;gt; ptrs(new int(10));
test_func(p_2, move(ptrs)); //外面记得也要移动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然了，问题出现在这里。如果你无意间传递了一个右值进去：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void p_3(myobj x){
    cout &amp;lt;&amp;lt;&quot;p_3 called&quot;&amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; *x.val &amp;lt;&amp;lt; endl;
}

test_func(p_3, myobj(234));
template&amp;lt;typename _funcObj, typename Args&amp;gt; //为了方便测试，这里不用变参列表了。
void test_func(_funcObj ptr, Args&amp;amp;&amp;amp; args){
    // ptr(args...);
    std::invoke(ptr, forward&amp;lt;Args&amp;gt;(args)); //如果这里使用了完美转发
    cout &amp;lt;&amp;lt; *args.val &amp;lt;&amp;lt; endl; //args的右值特性被forward保留，触发移动构造。这里会段错误。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以说，具体要不要用forward需要根据情况使用。&lt;/p&gt;
&lt;h2&gt;std::invoke的返回值&lt;/h2&gt;
&lt;p&gt;从函数签名中，我们看到了invoke是有返回值的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T func(T obj){
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
    return obj;
}


int main() {
    decltype(auto) f = std::invoke(func&amp;lt;int&amp;gt;, 100); //f 类型是 int。刚才说过此处必须显示指明这个函数的模板参数。
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl; //输出100
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;之所以要用&lt;code&gt;decltype(auto)&lt;/code&gt;而非&lt;code&gt;auto&lt;/code&gt;的原因是因为&lt;code&gt;auto&lt;/code&gt;会导致类型退化。
&lt;ul&gt;
&lt;li&gt;我们提到过，&lt;code&gt;auto&lt;/code&gt;一定会推导出返回类型为对象类型并且应用退化。&lt;/li&gt;
&lt;li&gt;而&lt;code&gt;auto&amp;amp;&lt;/code&gt;或&lt;code&gt;auto&amp;amp;&amp;amp;&lt;/code&gt;一定会推导为引用类型。&lt;/li&gt;
&lt;li&gt;而&lt;code&gt;decltype(auto)&lt;/code&gt;则可以根据具体返回值的类型进行推导。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;那么为什么不用auto&amp;amp;&amp;amp;呢？因为&lt;code&gt;auto&amp;amp;&amp;amp;&lt;/code&gt;会延长临时对象的生命周期
&lt;ul&gt;
&lt;li&gt;返回值是临时对象，纯右值，使用右值（此处顺带万能引用）引用会延长其生命周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们看到了这玩意的返回值类型是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class F, class... Args &amp;gt;
std::invoke_result_t&amp;lt;F, Args...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那我们就要研究一下这个是个什么东西了。&lt;/p&gt;
&lt;h1&gt;std::result_of[C++11~17]/invoke_result[C++17]&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;首先强调一点。对于如&lt;code&gt;F(args...)&lt;/code&gt;这样的东西，由于2.6部分提到的原因，我们把它看成是是以 &lt;code&gt;Args...&lt;/code&gt; 为参数而以 &lt;code&gt;F&lt;/code&gt; 为返回类型的函数类型。这一点非常重要，因为后面会遇到这个错误，同时它也是为什么我们在C++20后弃用&lt;code&gt;result_of&lt;/code&gt;。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;第二点：你不能问编译器一个函数对象的返回类型是什么。因为其返回类型很可能和使用的参数有关。所以你需要问：使用这一堆参数调用这个函数对象的时候的返回类型是什么。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class &amp;gt;
class result_of; // 不定义
template&amp;lt; class F, class... ArgTypes &amp;gt;
class result_of&amp;lt;F(ArgTypes...)&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;作用是在编译时推导INVOKE 表达式的返回类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个INVOKE表达式是广义的可调用类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员&lt;code&gt;type&lt;/code&gt;是以参数 &lt;code&gt;ArgTypes...&lt;/code&gt; 调用可调用 (Callable) &lt;strong&gt;类型&lt;/strong&gt;&lt;code&gt; F&lt;/code&gt; 的返回类型。仅若 &lt;code&gt;F&lt;/code&gt; 能以参数&lt;code&gt;ArgTypes...&lt;/code&gt;在不求值语境中调用才得到定义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;再次强调，F和ArgTypes都必须是类型！！！！&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们看几个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct F{
    int operator ()(int i) {
        return i; 
    }
};

int f(int i){
        return i;
}

int main(){
    cout &amp;lt;&amp;lt; std::boolalpha;
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;F(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; //可以
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;f(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; //不可以
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为什么&lt;code&gt;F(int)&lt;/code&gt;可以但是&lt;code&gt;f(int)&lt;/code&gt;不可以？这里非常重要&lt;/li&gt;
&lt;li&gt;F是类型。仿函数是对象。它是类类型。所以&lt;code&gt;F&lt;/code&gt;是个类型。而&lt;code&gt;f&lt;/code&gt;不是类型，&lt;code&gt;f&lt;/code&gt;是函数。
&lt;ul&gt;
&lt;li&gt;上面其实不严谨。具体一点：&lt;code&gt;F(int)&lt;/code&gt;是一个函数类型。这个类型指的是一个函数它接受一个&lt;code&gt;int&lt;/code&gt;参数，返回F类型对象。很明显这不是&lt;code&gt;F&lt;/code&gt;类型，同时没有&lt;code&gt;F&lt;/code&gt;到&lt;code&gt;int&lt;/code&gt;的转换，为什么这样做可以？原因在2.6写了。也就是它会把&lt;code&gt;F&lt;/code&gt;和&lt;code&gt;int&lt;/code&gt;类型拆开。最后会变成&lt;code&gt;declval&amp;lt;F&amp;gt;()(declval&amp;lt;int&amp;gt;())&lt;/code&gt;这个样子。&lt;strong&gt;但是这样做有个非常大的问题，也是为何&lt;code&gt;result_of&lt;/code&gt;在C++20被弃用。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cout &amp;lt;&amp;lt; is_same_v&amp;lt;F, F&amp;gt;&amp;lt;&amp;lt; endl; //可以。F是类型。我们可以有F obj
cout &amp;lt;&amp;lt; is_same_v&amp;lt;f, f&amp;gt;&amp;lt;&amp;lt; endl; //不可以。f不是类型。f是函数
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;所以为了获取到函数类型，有几种方案。第一种是使用&lt;code&gt;using&lt;/code&gt;定义一个函数引用别名或函数指针别名即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;using f_ref = int(&amp;amp;)(int); //函数引用
using f_ptr = int(*)(int); //函数指针
using fdef = int(int); //函数类型
cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;f_ref(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; // OK
cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;f_ptr(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; // OK
int(*pf)(int) = f;
cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;pf(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; // 错误

cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;fdef(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; // 错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么第三种错误？因为&lt;code&gt;using f_ptr = int(*)(int); &lt;/code&gt;是定义类型别名。而&lt;code&gt;int(*pf)(int) = f;&lt;/code&gt;是声明一个变量。说到底，&lt;code&gt;f_ptr&lt;/code&gt;依旧是类型，而&lt;code&gt;pf&lt;/code&gt;变成了函数指针本身，而函数指针是实体，并不是类型。&lt;code&gt;pf&lt;/code&gt;的类型是&lt;code&gt;int(*)(int)&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么第四种错误？这里有个非常关键的语法层面的问题。 对于如&lt;code&gt;F(args...)&lt;/code&gt;这样的东西，由于2.6部分提到的原因，我们把它看成是是以 &lt;code&gt;Args...&lt;/code&gt; 为参数而以 &lt;code&gt;F&lt;/code&gt; 为返回类型的函数类型。再次回到函数头，我们能看到&lt;code&gt;class result_of&amp;lt;F(ArgTypes...)&amp;gt;;&lt;/code&gt;这个声明。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以我们现在&lt;code&gt;result_of_t&amp;lt;fdef(int)&amp;gt;&lt;/code&gt;进去之后会变成啥呢？会变成一个以&lt;code&gt;fdef&lt;/code&gt;为返回类型，以&lt;code&gt;int&lt;/code&gt;为参数的函数类型。那么&lt;code&gt;fdef&lt;/code&gt;是啥呢？&lt;code&gt;fdef&lt;/code&gt;是一个函数类型。C++函数不是一等公民，不能传入和返回函数，但是可以传入和返回函数的指针或引用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以这个不能跑的原因是，我们无法定义一个返回函数的函数。而为什么用函数指针或引用的时候能跑？因为我们可以定义一个返回函数指针或函数引用的函数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二种是使用&lt;code&gt;decltype&lt;/code&gt;。我们这里拆解来看&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;using f_1 = decltype(f); //decltype函数名
using f_2 = decltype((f)); //decltype函数表达式加括号了。相当于上面的f_ref
cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;f_1(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; //不可以
cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;f_2(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; //可以
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么第一个&lt;code&gt;f_1&lt;/code&gt;不可以？我们说过，&lt;code&gt;F&lt;/code&gt;必须是一个类型而不能是函数。而且此时&lt;code&gt;f_1&lt;/code&gt;是函数类型。所以&lt;code&gt;f_1(int)&lt;/code&gt;又变成了一个返回函数的函数。所以还是有问题。第二个可以是因为函数加括号后&lt;code&gt;decltype&lt;/code&gt;的是表达式的&lt;code&gt;type&lt;/code&gt;，所以返回引用类型。所以可以。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第三种是直接使用函数模板推导让其退化为函数指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T f){
    //T会被推导为int (*)(int)。等同于上面的f_ptr
    std::puts(__PRETTY_FUNCTION__);
    cout &amp;lt;&amp;lt;boolalpha;
    cout &amp;lt;&amp;lt;is_same&amp;lt;typename result_of&amp;lt;T(int)&amp;gt;::type, int&amp;gt;::value &amp;lt;&amp;lt;endl;
}
int sample(int){
    return 0;
}
int main(){
    func(sample);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结解决方案：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用using直接定义函数指针或函数引用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用decltype的时候，把函数做为表达式传入。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或者是在使用的时候加一个&lt;code&gt;&amp;amp;&lt;/code&gt;（依旧相当于变为函数引用），比如&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cout &amp;lt;&amp;lt; is_same_v&amp;lt;std::result_of_t&amp;lt;f_1&amp;amp;(int)&amp;gt;, int&amp;gt;&amp;lt;&amp;lt; endl; //可以
//其实就相当于变为函数引用
cout &amp;lt;&amp;lt; is_same_v&amp;lt;f_1&amp;amp;,int(&amp;amp;)(int)&amp;gt;&amp;lt;&amp;lt; endl; //相当于函数引用类型
cout &amp;lt;&amp;lt; is_same_v&amp;lt;f_1,int(int)&amp;gt;&amp;lt;&amp;lt; endl; //相当于函数类型
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;直接使用函数模板推导让其退化为函数指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;进入invoke_result的世界&lt;/h2&gt;
&lt;p&gt;我们已经提过了result_of由于设计上的问题导致的诡异行为。所以invoke_result的定义是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class F, class... ArgTypes&amp;gt;
class invoke_result;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;F&lt;/code&gt; 不能是函数类型或数组类型（但能是到它们的引用）；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;若任何 &lt;code&gt;Args&lt;/code&gt; 拥有“ &lt;code&gt;T&lt;/code&gt; 的数组”类型或函数类型 &lt;code&gt;T&lt;/code&gt; ，则它被自动调整为 &lt;code&gt;T*&lt;/code&gt; ；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F&lt;/code&gt; 或任何 &lt;code&gt;Args...&lt;/code&gt; 都不能是抽象类类型；&lt;/li&gt;
&lt;li&gt;若任何 &lt;code&gt;Args...&lt;/code&gt; 拥有顶层 cv 限定符，则舍弃之；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Args...&lt;/code&gt; 均不可为 void 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最明显的两点区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，不会把模板参数合成为一个函数类型了。也就是不会产生&lt;code&gt;F(args...)&lt;/code&gt;的问题。&lt;/li&gt;
&lt;li&gt;其次，明确限制F不能是函数类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以使用的时候就不再是跟着括号了，而是逗号即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;is_same&amp;lt;std::result_of_t&amp;lt;f_2(int)&amp;gt;, int&amp;gt;;// result_of_t
is_same&amp;lt;std::invoke_result_t&amp;lt;f_2, int&amp;gt;, int&amp;gt;; // invoke_result_t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他资料如更细节的语法等：&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/54065009/using-stdinvoke-to-call-templated-function&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/tcy23456/article/details/110583343&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/hanxiaoyong_/article/details/120618869&lt;/p&gt;
&lt;h2&gt;例子&lt;/h2&gt;
&lt;p&gt;比如下面我们的想法是对一个有&lt;code&gt;T&lt;/code&gt;类型元素的&lt;code&gt;vector&lt;/code&gt;容器&lt;code&gt;vec&lt;/code&gt;，对其每一个元素调用可调用对象&lt;code&gt;c&lt;/code&gt;，然后将返回值存入新&lt;code&gt;vector&lt;/code&gt;，排序后并返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename Callable&amp;gt;
auto transform_sort(const vector&amp;lt;T&amp;gt;&amp;amp; vec, Callable c){
    vector&amp;lt;decay_t&amp;lt;result_of_t&amp;lt;Callable&amp;amp;(const T&amp;amp;)&amp;gt;&amp;gt;&amp;gt; ret; //查看对元素施加callable对象后，返回的值类型。然后创建这个类型的容器
    for(const T&amp;amp; item: vec){ //对每一个元素
        ret.push_back(invoke(c, item));//使用invoke施加callable，然后存入ret
    }
    sort(ret.begin(), ret.end());
    return ret;
}

int my_callable(const string&amp;amp; s){
    return s.size();
}
int main(){
    vector&amp;lt;string&amp;gt; my_vec{&quot;a&quot;,&quot;abcd&quot;,&quot;abc&quot;,&quot;ab&quot;};
    for(const auto&amp;amp; item: transform_sort(my_vec, my_callable)){
        cout &amp;lt;&amp;lt; item &amp;lt;&amp;lt; endl;
    }
    /*
    输出
    1
    2
    3
    4
    */
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;result_of_t&amp;lt;Callable&amp;amp;(const T&amp;amp;)&amp;gt;&amp;gt;&lt;/code&gt;这里的细节是使用了&lt;code&gt;Callable&amp;amp;&lt;/code&gt;而非&lt;code&gt;Callable&lt;/code&gt;。也就是解决方案2。这样避免了我们上面提到的诡异行为。&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;decay_t&lt;/code&gt;的原因是，可能callable的返回类型是引用或带有cv限定的。有这样元素的的容器是不合法的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invoke&lt;/code&gt;的语法不多讲。第一个是可调用对象，第二个是其参数。&lt;/li&gt;
&lt;li&gt;最后的结果我们看到了，我们的&lt;code&gt;callable&lt;/code&gt;目的是返回这个字符串的长度。所以整体目的是检查&lt;code&gt;vector&lt;/code&gt;内每一个字符串的长度，然后存到&lt;code&gt;vector&lt;/code&gt;中并返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;建议&lt;/h2&gt;
&lt;p&gt;建议不要使用result_of，除非必须场合。&lt;/p&gt;
&lt;h1&gt;std::decay&lt;/h1&gt;
&lt;p&gt;之前总谈到它，现在不整理不行了。不然看不懂书了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class T &amp;gt;
struct decay;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对类型&lt;code&gt; T&lt;/code&gt; 应用&lt;strong&gt;左值到右值（lvalue-to-rvalue）&lt;/strong&gt;、**数组到指针（array-to-pointer）&lt;strong&gt;和&lt;/strong&gt;函数到指针（function-to-pointer）**的隐式转换，移除 cv 限定符，并定义结果类型为成员 &lt;code&gt;typedef type&lt;/code&gt; 。正式而言：
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt; T&lt;/code&gt; 指名“ &lt;code&gt;U&lt;/code&gt; 的数组”或“到 &lt;code&gt;U&lt;/code&gt; 的数组的引用”类型，则成员 &lt;code&gt;typedef type&lt;/code&gt; 为 &lt;code&gt;U*&lt;/code&gt; 。
&lt;ul&gt;
&lt;li&gt;强制给数组和数组引用类型退化为指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;否则，若 &lt;code&gt;T&lt;/code&gt; 为函数类型 &lt;code&gt;F&lt;/code&gt; 或到它的引用，则成员 &lt;code&gt;typedef type&lt;/code&gt; 为&lt;code&gt;std::add_pointer&amp;lt;F&amp;gt;::type&lt;/code&gt; 。
&lt;ul&gt;
&lt;li&gt;强制给函数和函数引用类型退化为指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;否则，成员&lt;code&gt;typedef type&lt;/code&gt;为&lt;code&gt; std::remove_cv&amp;lt;std::remove_reference&amp;lt;T&amp;gt;::type&amp;gt;::type&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;强制给普通成员移除引用和CV限定符。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看看原理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp&amp;gt; 
class decay 
{ 
    typedef typename remove_reference&amp;lt;_Tp&amp;gt;::type __remove_type; //先去掉引用

public:
    typedef typename __decay_selector&amp;lt;__remove_type&amp;gt;::__type type; //然后使用下面的 __decay_selector
};


template&amp;lt;typename _Up, 
    bool _IsArray = is_array&amp;lt;_Up&amp;gt;::value,
    bool _IsFunction = is_function&amp;lt;_Up&amp;gt;::value&amp;gt; 
struct __decay_selector; //主模板，判断是不是数组或函数类型。！！！注意这里is_function判断的是函数类型而非函数指针类型。

template&amp;lt;typename _Up&amp;gt;
struct __decay_selector&amp;lt;_Up, false, false&amp;gt; //如果不是数组也不是函数，则直接去掉CV即可。第一步已经去掉过引用了。
{ typedef typename remove_cv&amp;lt;_Up&amp;gt;::type __type; };

template&amp;lt;typename _Up&amp;gt;
struct __decay_selector&amp;lt;_Up, true, false&amp;gt; //如果是数组类型，则直接弄成*就行
{ typedef typename remove_extent&amp;lt;_Up&amp;gt;::type* __type; };

template&amp;lt;typename _Up&amp;gt;
struct __decay_selector&amp;lt;_Up, false, true&amp;gt; //如果是函数类型，
{ typedef typename add_pointer&amp;lt;_Up&amp;gt;::type __type; }; //使用add_pointer转换为指针类型。这里需要细说。后面会有介绍

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其实从上面我们也发现了。主要核心就是把三种情况分开讨论，使用三个偏特化的模板进行匹配。&lt;/li&gt;
&lt;li&gt;比较需要注意的一点是，针对数组类型退化的时候，&lt;code&gt;CV&lt;/code&gt;特性会被保留。&lt;/li&gt;
&lt;li&gt;至于&lt;code&gt;add_pointer&lt;/code&gt;，我们下面说。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://blog.csdn.net/weixin_43798887/article/details/118311126&lt;/p&gt;
&lt;h1&gt;std::void_t [C++17]&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;这部分需要结合6.6部分来看。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class... _Types&amp;gt;
using void_t = void;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;功能：&lt;strong&gt;能够配合我们检测到应用SFINAE(替换失败并不是一个错误）特性时出现的非法类型&lt;/strong&gt;。换句话说，&lt;strong&gt;给进来的类型 &lt;code&gt;_Types&lt;/code&gt; 必须是一个有效的类型，不能是一个非法类型&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;它可以判断一个类内是否有个特定的类型别名、成员函数或成员变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个东西看起来贼鸡肋，我们分析分析它的简单使用。&lt;strong&gt;假设现在我们的目的是判断一个类内是否有一个叫&lt;code&gt;type&lt;/code&gt;的类型&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;判断是否存在成员类型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    using mytype = int; //类A含有mytype类型
};

struct B{
    using mytype = void; //类B也含有mytype类型
};


template&amp;lt;typename T1 , typename T2 = void &amp;gt; //主模板，第二个参数默认值是void，非常重要
struct has_type_member : std::false_type { //主模板继承false_type

 };

template&amp;lt;typename T1&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;typename T1::mytype&amp;gt;&amp;gt;: std::true_type { //偏特化继承true_type，并且使用void_t进行类型判断。
}; 

int main(){
    auto f = has_type_member&amp;lt;float&amp;gt;::value;
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    /*
    输出
    0
    1
    1
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们解析一下标准例子。为什么是这个结果。&lt;/li&gt;
&lt;li&gt;首先，我们拥有类&lt;code&gt;A&lt;/code&gt;和类&lt;code&gt;B&lt;/code&gt;。他们都有一个&lt;code&gt;mytype&lt;/code&gt;类型。
&lt;ul&gt;
&lt;li&gt;其次，我们的判断类的主模板继承自&lt;code&gt;false_type&lt;/code&gt;，第二个参数的默认值是&lt;code&gt;void&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;判断类的偏特化继承自&lt;code&gt;true_type&lt;/code&gt;，针对第二个参数进行偏特化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;我们在6.6中强调了：当模板参数有默认值的时候，它会被隐式填进去，而不是被忽略。它依旧存在。也就是我们要把默认值理解为：虽然我们不需要显式指定，但是它依旧存在。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;auto f = has_type_member&amp;lt;float&amp;gt;::value;	//扩展为has_type_member&amp;lt;float, void&amp;gt;::value;
auto a = has_type_member&amp;lt;A&amp;gt;::value;		//扩展为has_type_member&amp;lt;A, void&amp;gt;::value;
auto b = has_type_member&amp;lt;B&amp;gt;::value;		//扩展为has_type_member&amp;lt;B, void&amp;gt;::value;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们首先看&lt;code&gt;f&lt;/code&gt;。我们使用&lt;code&gt;&amp;lt;float&amp;gt;&lt;/code&gt;的时候会由于有默认参数被扩展为&lt;code&gt;&amp;lt;float, void&amp;gt;&lt;/code&gt;。然后开始进行匹配。首先选用特化模板。&lt;code&gt;T1&lt;/code&gt;会被替换为&lt;code&gt;float&lt;/code&gt;。然后在第二个模板参数&lt;code&gt;std::void_t&amp;lt;typename float::mytype&amp;gt;&amp;gt;&lt;/code&gt;进行匹配的时候，由于&lt;code&gt;float&lt;/code&gt;没有&lt;code&gt;mytype&lt;/code&gt;类型，不合法。此时会发生替换失败。SFINAE发生。所以这个模板会被丢弃。回退到主模板，应用到主模板的&lt;code&gt;void&lt;/code&gt;。因为我们提供的是&lt;code&gt;&amp;lt;float,void&amp;gt;&lt;/code&gt;。而主模板完美匹配。这个时候实例化出来的是继承自&lt;code&gt;false_type&lt;/code&gt;的。所以是&lt;code&gt;0&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后我们看&lt;code&gt;a&lt;/code&gt;。我们使用&lt;code&gt;&amp;lt;A&amp;gt;&lt;/code&gt;的时候会由于有默认参数被扩展为&lt;code&gt;&amp;lt;A, void&amp;gt;&lt;/code&gt;。然后开始进行匹配。首先选用特化模板。&lt;code&gt;T1&lt;/code&gt;会被替换为&lt;code&gt;A&lt;/code&gt;。然后在第二个模板参数&lt;code&gt;std::void_t&amp;lt;typename A::mytype&amp;gt;&amp;gt;&lt;/code&gt;进行匹配的时候，发现A有type类型。合法。所以此时通过决议。我们看到了，此时&lt;code&gt;void_t&lt;/code&gt;的推导通过，&lt;code&gt;void_t&lt;/code&gt;就是&lt;code&gt;void&lt;/code&gt;。此时整个&lt;code&gt;std::void_t&amp;lt;typename A::mytype&amp;gt;&amp;gt;&lt;/code&gt;会变为&lt;code&gt;void&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关键的来了。我们提供的是&lt;code&gt;&amp;lt;A,void&amp;gt;&lt;/code&gt; ，主模板可以匹配。同时特化版本也可以匹配，因为此时也被替换成了&lt;code&gt;&amp;lt;A,void&amp;gt;&lt;/code&gt;。但是由于下面的特化版本更特化，我们知道&lt;code&gt;全特化版本 &amp;gt; 特化版本 &amp;gt; 主模板&lt;/code&gt;。所以此时选择了偏特化版本，继承了&lt;code&gt;true_type&lt;/code&gt;所以是1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;b同a。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么必须要用void_t?&lt;/h2&gt;
&lt;p&gt;我们看下面的例子：&lt;/p&gt;
&lt;p&gt;我们假设类A和类B和上面的代码一样，只修改我们的判断类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1 , typename T2 = void &amp;gt; //主模板，第二个参数默认值是void，非常重要
struct has_type_member : std::false_type { //主模板继承false_type

 };

template&amp;lt;typename T1&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;T1, typename T1::mytype&amp;gt;: std::true_type { //偏特化继承true_type，注意这里不用void_t,直接判断其type
}; 

int main(){
    auto f = has_type_member&amp;lt;float&amp;gt;::value;
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    /*
    输出
    0
    0 //这个变了！！
    1
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个f原因同上。我们不多解释。&lt;/li&gt;
&lt;li&gt;我们来看&lt;code&gt;a&lt;/code&gt;。&lt;code&gt;A&lt;/code&gt;会被扩展为&lt;code&gt;&amp;lt;A,void&amp;gt;&lt;/code&gt;。然后开始匹配。首先选用特化模板。我们发现&lt;code&gt;A&lt;/code&gt;类有&lt;code&gt;mytype&lt;/code&gt;，所以正确推导。**但是，&lt;code&gt;A::mytype&lt;/code&gt;是&lt;code&gt;int&lt;/code&gt;类型，导致这时候模板实例化出来的会变成&lt;code&gt;has_type_member&amp;lt;A, int&amp;gt;&lt;/code&gt;。**但是我们模板实参给的是&lt;code&gt;&amp;lt;A,void&amp;gt;&lt;/code&gt;。此时发现不匹配。于是该版本被丢弃。然后发现主模板可以匹配。所以此时会选用主模板而非特化模板。主模板继承自&lt;code&gt;false_type&lt;/code&gt;。此时自然是0。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b&lt;/code&gt;的原因和上面一样，但是由于&lt;code&gt;B::mytype&lt;/code&gt;是&lt;code&gt;void&lt;/code&gt;，实例化出来的&lt;code&gt;&amp;lt;B,void&amp;gt;&lt;/code&gt; 和 实参&lt;code&gt;&amp;lt;B,void&amp;gt;&lt;/code&gt;匹配。所以选择了偏特化版本。此时自然是1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如果像6.6那样更换了主模板的默认值，会发生什么？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1 , typename T2 = int &amp;gt; //主模板，此时第二个参数默认值是int
struct has_type_member : std::false_type { //主模板继承false_type

 };

template&amp;lt;typename T1&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;typename T1::mytype&amp;gt;&amp;gt;: std::true_type { //偏特化继承true_type，并且使用void_t进行类型判断。
}; 
int main(){
    auto f = has_type_member&amp;lt;float&amp;gt;::value;
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    /*
    输出
    0
    0 //这个变了
    0 //这个也变了
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;f理由同上不解释。&lt;/li&gt;
&lt;li&gt;我们来看a。&lt;code&gt;A&lt;/code&gt;会被扩展为&lt;code&gt;&amp;lt;A,int&amp;gt;&lt;/code&gt;。然后开始匹配。首先选用特化模板。我们发现&lt;code&gt;A&lt;/code&gt;类有&lt;code&gt;mytype&lt;/code&gt;，所以正确推导。**这时候模板实例化出来的会变成&lt;code&gt;has_type_member&amp;lt;A, void&amp;gt;&lt;/code&gt;。但是，我们模板实参给的是&lt;code&gt;&amp;lt;A,int&amp;gt;&lt;/code&gt;。此时发现不匹配。**于是该版本被丢弃。然后发现主模板可以匹配。所以此时会选用主模板而非特化模板。主模板继承自&lt;code&gt;false_type&lt;/code&gt;。此时自然是0。&lt;/li&gt;
&lt;li&gt;对于b，&lt;code&gt;B&lt;/code&gt;会被扩展为&lt;code&gt;&amp;lt;B, int&amp;gt;&lt;/code&gt;。然后开始匹配。首先选用特化模板。我们发现&lt;code&gt;B&lt;/code&gt;类有&lt;code&gt;mytype&lt;/code&gt;，所以正确推导。**这时候模板实例化出来的会变成&lt;code&gt;has_type_member&amp;lt;B, void&amp;gt;&lt;/code&gt;。但是，我们模板实参给的是&lt;code&gt;&amp;lt;B,int&amp;gt;&lt;/code&gt;。此时发现不匹配。**于是该版本被丢弃。然后发现主模板可以匹配。所以此时会选用主模板而非特化模板。主模板继承自&lt;code&gt;false_type&lt;/code&gt;。此时自然是0。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;搭配decltype判断是否存在成员变量&lt;/h2&gt;
&lt;p&gt;上面一节是判断是否存在类型别名。这一节看一下判断是否存在成员变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    int val = 234;      //类A含有val变量
};

struct B{
    float val2 = 345;  //类B不含有val变量，但是含有val2变量
};


template&amp;lt;typename T1 , typename T2 = void &amp;gt; //主模板，第二个参数默认值是void，非常重要
struct has_type_member : std::false_type { //主模板继承false_type

 };

template&amp;lt;typename T1&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;decltype(T1::val)&amp;gt;&amp;gt;: std::true_type { //偏特化继承true_type，并且使用void_t且搭配decltype进行类型判断。
}; 
int main(){
    auto f = has_type_member&amp;lt;float&amp;gt;::value;
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    /*
    输出
    0
    1
    0
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;核心原理就是我们在这里需要搭配&lt;code&gt;decltype&lt;/code&gt;来对某一个变量的名字进行判断是否合法，但不推导。如果有这个变量则类型合法。如果没有则非法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;额外注意！！！在模板内的decltype表达式是不推导语境。我们需要等到特化被生成后，编译器才会开始进入推导语境。也就是目前仅仅对T进行替换但不推导表达式的实际类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;我们直接看&lt;code&gt;a&lt;/code&gt;。&lt;code&gt;A&lt;/code&gt;被扩展为&lt;code&gt;&amp;lt;A,void&amp;gt;&lt;/code&gt;。然后选择偏特化版本开始匹配。此时进入模板参数推导阶段，所有的&lt;code&gt;T1&lt;/code&gt;被替换为&lt;code&gt;A&lt;/code&gt;。但是注意，我们说了&lt;code&gt;decltype&lt;/code&gt;此处不推导。只是替换。所以此时特化为：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;A, std::void_t&amp;lt;decltype(A::val)&amp;gt;&amp;gt;: std::true_type {
}; 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;此时模板参数已被推导完毕，进入下一阶段。此时&lt;code&gt;decltype&lt;/code&gt;是可评估语境。由于类&lt;code&gt;A&lt;/code&gt;有成员变量&lt;code&gt;val&lt;/code&gt;，所以此时&lt;code&gt;decltype(A::val)&lt;/code&gt;是合法的。此时它会变成&lt;code&gt;int&lt;/code&gt;。我们说过任何经过&lt;code&gt;void_t&lt;/code&gt;判断合法的东西都会使整个&lt;code&gt;void_t&lt;/code&gt;参数变为&lt;code&gt;void&lt;/code&gt;。所以此时特化成&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;has_type_member &amp;lt;A,void&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;和上面一样，虽然主模板和特化版本都匹配，但还是匹配最特殊的，这里就是偏特化版本，自然就是true。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;b&lt;/code&gt;和&lt;code&gt;a&lt;/code&gt;的原理几乎一致。唯一区别是进行偏特化后，&lt;code&gt;B&lt;/code&gt;没有&lt;code&gt;val&lt;/code&gt;成员变量。所以&lt;code&gt;decltype&lt;/code&gt;非法。所以整个&lt;code&gt;void_t&lt;/code&gt;也非法。触发SFINAE特性移出重载集合，转为匹配主模板。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;判断是否存在成员函数&lt;/h2&gt;
&lt;p&gt;我们下面讲了&lt;code&gt;declval&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个是第一种方式，通过&lt;code&gt;&amp;lt;成员函数的返回值&amp;gt;&lt;/code&gt;是否合法来判断。限制是函数不能有参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    void funcA();
};

struct B{
    void funcB();
};


template&amp;lt;typename T1 , typename T2 = void &amp;gt; //主模板，第二个参数默认值是void，非常重要
struct has_type_member:std::false_type { //主模板继承false_type

 };

template&amp;lt;typename T1&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;decltype(std::declval&amp;lt;T1&amp;gt;().funcA())&amp;gt;&amp;gt;: std::true_type { //使用decltype和declval进行成员函数判断。
}; 
int main(){
    auto f = has_type_member&amp;lt;float&amp;gt;::value;
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    /*
    输出
    0
    1
    0
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;具体不细说了。核心还是判断类型是否存在也就是&lt;code&gt;decltype&lt;/code&gt;内的表达式是否合法。注意两点&lt;/li&gt;
&lt;li&gt;第一点&lt;code&gt;declval&lt;/code&gt;是函数模板。所以别忘了调用一下。&lt;/li&gt;
&lt;li&gt;第二点是，&lt;code&gt;decltpe&lt;/code&gt;此处我们的目的是通过函数返回值来判断是否存在函数。由于&lt;code&gt;declval&lt;/code&gt;是用来判断函数返回值的，所以必须要“调用”一下。所以这里必须得让他成为表达式。原因在&lt;a href=&quot;https://stackoverflow.com/questions/72418821/getting-the-decltype-of-a-member-function&quot;&gt;这里&lt;/a&gt;和&lt;a href=&quot;https://stackoverflow.com/questions/31783292/how-to-use-decltype-with-member-functions&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;**有一个问题，如果这个成员函数有参数咋办？这里就不能这么用了。因为如果签名需要参数，但是我们没给，就非法。**所以这时候就需要通过成员函数地址来判断了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://blog.csdn.net/baidu_41388533/article/details/109700163&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/ding_yingzi/article/details/79983042&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/27687389/how-does-void-t-work&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;另一种方式，通过&lt;code&gt;&amp;lt;取成员（函数）地址&amp;gt;&lt;/code&gt;是否合法来判断。
&lt;ul&gt;
&lt;li&gt;这个方式同样可以检测是否有成员变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    A() = delete;
    void myfunc(int s){
        //类A含有myfunc
    }
};

struct B{
    void myfuncB(){
        //类B不含有myfunc，但是含有myfuncB
    }
};


template&amp;lt;typename T1 , typename T2 = void &amp;gt; //主模板，第二个参数默认值是void，非常重要
struct has_type_member:std::false_type { //主模板继承false_type

 };

template&amp;lt;typename T1&amp;gt;
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;decltype(&amp;amp;T1::myfunc)&amp;gt;&amp;gt;: std::true_type { //通过成员函数地址判断。OK
}; 
int main(){
    auto f = has_type_member&amp;lt;float&amp;gt;::value;
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    /*
    输出
    0
    1
    0
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;有没有其他写法？&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ybaE9qlhHvw?t=1136&quot;&gt;来自这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;先说结论，以下三种写法是同一个效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void_t&amp;lt;decltype(expression)&amp;gt;; 	//1
decltype(void(expression)); 	//2
decltype(expression, void()); 	//3
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一种就是经典写法。&lt;/li&gt;
&lt;li&gt;第二种是表达式计算完毕后&lt;code&gt;void&lt;/code&gt;转型。所以如果表达式合法，最后的类型结果也是&lt;code&gt;void&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第三种是利用逗号表达式。第一个表达式计算完毕后，如果合法，就丢弃第一个结果继续计算&lt;code&gt;void()&lt;/code&gt;。最后的类型结果也是&lt;code&gt; void&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以如果换成上面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1&amp;gt;
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;decltype(&amp;amp;T1::myfunc)&amp;gt;&amp;gt;: std::true_type { //1
}; 
template&amp;lt;typename T1&amp;gt;
struct has_type_member&amp;lt;T1, decltype(void(&amp;amp;T1::myfunc))&amp;gt;: std::true_type { //2
}; 
template&amp;lt;typename T1&amp;gt;
struct has_type_member&amp;lt;T1, decltype(&amp;amp;T1::myfunc, void())&amp;gt;: std::true_type { //3
}; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;std::declval&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;declval&lt;/code&gt;基本用于推导成员函数的返回类型。千万注意&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
typename std::add_rvalue_reference&amp;lt;T&amp;gt;::type declval() noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有没有发现这个函数的声明和我们上面的&lt;code&gt;testfunc&lt;/code&gt;很像？都是只要声明不要定义，骗过编译器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;**函数模板 &lt;code&gt;std::declval()&lt;/code&gt;可以被用作某一类型的对象的引用的占位符。**该函数模板没有定义，因此不能被调用（也不会创建对象）。因此它只能被用作不会被计算的操作数（比如 decltype 和 sizeof）。&lt;strong&gt;也因此，在不创建对象的情况下，依然可以假设有相应类型的可用对象。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;书里的话非常抽象。我们看看标准文档怎么说的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将任意类型 &lt;code&gt;T&lt;/code&gt; 转换成引用类型，使得在 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/decltype&quot;&gt;&lt;code&gt;decltype&lt;/code&gt; 说明符&lt;/a&gt;的操作数中&lt;strong&gt;不必经过构造函数&lt;/strong&gt;就能使用成员函数。&lt;/p&gt;
&lt;p&gt;通常在模板中使用 &lt;code&gt;std::declval&lt;/code&gt;，模板接受的模板实参通常可能无构造函数，但有同一成员函数，均返回所需类型。&lt;/p&gt;
&lt;p&gt;注意，&lt;code&gt;std::declval&lt;/code&gt; 只能用于&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/expressions#.E6.BD.9C.E5.9C.A8.E6.B1.82.E5.80.BC.E8.A1.A8.E8.BE.BE.E5.BC.8F&quot;&gt;不求值语境&lt;/a&gt;，且不要求有定义；求值包含此函数的表达式是错误。正式的说法是 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/definition#ODR_.E4.BD.BF.E7.94.A8&quot;&gt;ODR 使用&lt;/a&gt;此函数的程序非良构。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里就非常明确了。declval可以被当做占位符的意思是。&lt;code&gt;declval&amp;lt;T&amp;gt;&lt;/code&gt;就相当于创建了一个&lt;code&gt;T&lt;/code&gt;类型的对象。但是由于实际并不创建，所以我可以通过不创建实际对象来使用某一个成员函数。所以，使用declval创建虚拟占位符意味着不调用构造。所以在某些时刻会节约其调用成本。但是它只能返回类型，所以它只能用在不求值语境中，比如decltype。（我们已经反复强调decltype并不真正求值。我们在杂记4中详细介绍了decltype的不求值语境。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    A() = delete;
    int foo();
};

int main() {
    decltype(A().foo()) foo_no; // 不OK因为构造函数无法访问
    decltype(std::declval&amp;lt;A&amp;gt;().foo()) foo = 1; // OK
    decltype(A&amp;amp;&amp;amp;.foo())foo = 1; //理论上和上面的相等，但是不能编译
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们下面那一行在理论上等于上面那一行。因为decltype不求值，所以这里可以编译通过。因为我们的目的是查看&lt;code&gt;foo&lt;/code&gt;这个函数到底返回什么类型。这个函数也不会被调用。所以你看我们根本没有给&lt;code&gt;foo&lt;/code&gt;提供定义。他们只关心类型。&lt;strong&gt;也就是说，我们告诉编译器：请假设我们现在有这个对象，那么请看看使用这个对象和这些参数来调用这个成员函数的时候，返回值是什么类型。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意一下，&lt;code&gt;declval&lt;/code&gt;是一个函数，所以记得要调用一下&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;再强调一次，不要花时间思考为什么两行语义相等但是第二行不通过。只要编译器认为它是值就可以了。不要仔细思考decltype是如何被实现的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;关于为啥declval要添加右值引用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;必须添加引用是因为可以使用不完整类型。比如数组那种&lt;code&gt;int[10]&lt;/code&gt;直接按照值返回是不行的。使用右值引用是因为可以正确施加引用折叠以避免改变类型。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于可引用的类型，其返回类型总是相关类型的右值引用，这&lt;strong&gt;能够使&lt;code&gt;declval&lt;/code&gt; 适用于那些不能够正常从函数返回的类型，比如抽象类的类型（包含纯虚函数的类型）或者数组类型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此当被用作表达式时，从类型 &lt;code&gt;T&lt;/code&gt; 到 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;的转换对&lt;code&gt;declval&amp;lt;T&amp;gt;()&lt;/code&gt;的行为是没有影响的：其结果都是右值（如果 &lt;code&gt;T&lt;/code&gt; 是对象类型的话。上面decltype笔记2.3），对于右值引用，其结果之所以不会变是因为存在引用折叠。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以要注意，如果希望返回类型不是一个引用，记得使用&lt;code&gt;decay&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stdrc.cc/post/2020/09/12/std-declval/#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%94%E5%9B%9E%E5%BC%95%E7%94%A8&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/25707441/why-does-stddeclval-add-a-reference&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/28532781/how-does-stddeclvalt-work&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;另外一点要注意：在 &lt;code&gt;noexcept&lt;/code&gt; 异常规则中提到，一个表达式不会因为使用了&lt;code&gt;declval&lt;/code&gt;而被认成是会抛出异常的。当 &lt;code&gt;declval&lt;/code&gt; 被用在 &lt;code&gt;noexcept&lt;/code&gt; 运算符上下文中时，这一特性会很有帮助&lt;/strong&gt;（参见第 19.7.2节）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如何理解 declval&lt;/h2&gt;
&lt;p&gt;在实际使用中，我们只需要理解为&lt;code&gt;declval&amp;lt;T&amp;gt;()&lt;/code&gt;是告诉编译器&lt;strong&gt;假装在这里我们有一个&lt;code&gt;T&lt;/code&gt;类型的对象&lt;/strong&gt;。我们忽略掉它是引用的事实，忽略掉它的实现（添加右值引用）。这看起来怪异也无所谓。因为它只能用在不求值语境。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在不求值语境中，任何函数调用都只需要有声明即可，并不必须提供定义。所以不求值语境中重要的是类型，而不是实例。在整个不求值的语境中，都是一堆类型在进行推导。压根没有值在这里计算。所以实例无所谓。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们说过：&lt;strong&gt;必须添加引用是因为可以使用不完整类型。比如数组那种&lt;code&gt;int[10]&lt;/code&gt;直接按照值返回是不行的。使用右值引用是因为可以正确施加引用折叠以避免改变类型。所以说，如果不考虑这些奇怪的场景，只考虑简单，最基本的例子，你甚至可以直接写成这样：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
T declval() noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们可以理解为&lt;code&gt;declval&lt;/code&gt;也是一种类型萃取。尤其是在不求值语境中帮我们进行推理。&lt;/p&gt;
&lt;p&gt;同时，很多需要SFINAE的时候都需要让表达式内嵌套一个表达式。什么意思？比如这个&lt;code&gt;is_polymophic&lt;/code&gt;的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T, class&amp;gt;  //主模板
struct IP_impl : false_type {
};
template&amp;lt;class T&amp;gt; //偏特化
struct IP_impl&amp;lt;T, decltype(dynamic_cast&amp;lt;void*&amp;gt;(declval&amp;lt;remove_cv_t&amp;lt;T&amp;gt;*&amp;gt;()))&amp;gt; : true_type {
};
template&amp;lt;class T&amp;gt;
struct is_polymorphic : IP_impl&amp;lt;T, void*&amp;gt; { //外层。注意这里第二个参数必须是void*
    //原因是因为如果是void，就算是多态也会匹配到主模板。
    //因为我们提供的第二个参数是void，但是如果满足多态，第二个参数会被偏特化模板推导为void*，这时候就不满足了
    //然后就会fallback到主模板。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里我主要想强调一下为什么必须&lt;code&gt;declval&lt;/code&gt;。我们先不要管&lt;code&gt;remove_cv_t&lt;/code&gt;。首先&lt;code&gt;dynamic_cast&lt;/code&gt;是可以转换成&lt;code&gt;void*&lt;/code&gt;的。但是想使用&lt;code&gt;dynamic_cast&lt;/code&gt;则必须要虚函数表。所以只要有虚函数表，就是多态类。回到&lt;code&gt;declval&lt;/code&gt;。如果我们这里直接使用&lt;code&gt;dynamic_cast&amp;lt;void*&amp;gt;(T*)&lt;/code&gt;可以吗？不可以。因为&lt;code&gt;dynamic_cast&lt;/code&gt;的括号里需要表达式。&lt;code&gt;T*&lt;/code&gt;是类型，不是表达式。如果我们换成&lt;code&gt;(T*)nullptr&lt;/code&gt;可以，但是某些编译器会warning。&lt;strong&gt;所以在这里，&lt;code&gt;declval&lt;/code&gt;不仅帮助我们获得了一个虚拟对象，更是满足了一些要求提供表达式的场合。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在理解过程中，我们忽略掉&lt;code&gt;declval&amp;lt;T*&amp;gt;()&lt;/code&gt;会变成&lt;code&gt;T*&amp;amp;&amp;amp;&lt;/code&gt;类型的这件事。语义上虽如此，但是不影响程序执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;说回来，我们上面提到了，甚至可以写成返回&lt;code&gt;T&lt;/code&gt;的&lt;code&gt;declval&lt;/code&gt;。在这个例子中就满足。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T fuckval() noexcept; //随便写一个

template&amp;lt;typename T, typename&amp;gt;
struct ip_jmpl:false_type{
};
template&amp;lt;typename T&amp;gt;
struct ip_jmpl&amp;lt;T, decltype(dynamic_cast&amp;lt;void*&amp;gt;(fuckval&amp;lt;T*&amp;gt;()))&amp;gt;:true_type{ //换成fuckval完全没问题。
    void f(){
        std::puts(__PRETTY_FUNCTION__);
    }
};
template&amp;lt;typename T&amp;gt;
struct ip_po : ip_jmpl&amp;lt;T, void*&amp;gt; {
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来自&lt;a href=&quot;https://github.com/CppCon/CppCon2017/blob/master/Presentations/A%20Soupcon%20of%20SFINAE/A%20Soupcon%20of%20SFINAE%20-%20Arthur%20O%27Dwyer%20-%20CppCon%202017.pdf&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/28532781/how-does-stddeclvalt-work&lt;/p&gt;
&lt;h2&gt;为啥有的declval用引用类型，有的不用？&lt;/h2&gt;
&lt;p&gt;比如这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;else if constexpr (_Reversed_subrange&amp;lt;_Ty&amp;gt; == 1) {
    using _It = decltype(_STD declval&amp;lt;_Rng&amp;amp;&amp;gt;().begin().base());
    return {_St::_Subrange_sized,
            noexcept(subrange&amp;lt;_It, _It, subrange_kind::sized&amp;gt;{
                _STD declval&amp;lt;_Rng&amp;amp;&amp;gt;().end().base(), _STD declval&amp;lt;_Rng&amp;amp;&amp;gt;().begin().base(),
                _STD declval&amp;lt;_Rng&amp;amp;&amp;gt;().size()})};
} else if constexpr (_Can_reverse&amp;lt;_Rng&amp;gt;) {
    return {_St::_Reverse, noexcept(reverse_view{_STD declval&amp;lt;_Rng&amp;gt;()})};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;答案是declval会应用正常引用折叠。会在某些情况让你得到意想不到的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {};
struct B {};

struct C {
    A f() &amp;amp;&amp;amp; { return A{}; }
    B f() &amp;amp; { return B{}; }
};

int main() {
    static_assert(std::is_same_v&amp;lt;A, decltype(std::declval&amp;lt;C&amp;gt;().f())&amp;gt;);
    static_assert(std::is_same_v&amp;lt;B, decltype(std::declval&amp;lt;C&amp;amp;&amp;gt;().f())&amp;gt;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来自&lt;a href=&quot;https://stackoverflow.com/questions/67307362/using-declval-with-a-reference-type&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;std::add_pointer&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class T &amp;gt;
struct add_pointer;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;若 &lt;code&gt;T&lt;/code&gt; 为引用类型，则提供成员 typedef &lt;code&gt;type&lt;/code&gt; ，其为指向被引用类型的指针。
&lt;ul&gt;
&lt;li&gt;也就是直接换成指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;否则，若 T 指名对象类型、无 cv 或引用限定的函数类型或（可有 cv 限定的） void 类型，则提供成员 typedef &lt;code&gt;type&lt;/code&gt; ，其为类型 &lt;code&gt;T*&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;注意这里：&lt;em&gt;如果是对象类型，&lt;strong&gt;无CV或引用限定的函数类型&lt;/strong&gt;或（可有 cv 限定的） void 类型&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;也就是给对象或函数换成指针类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;否则（若 T 为 cv 或引用限定的函数类型），提供成员 typedef &lt;code&gt;type&lt;/code&gt; ，其为类型 &lt;code&gt;T&lt;/code&gt; 。
&lt;ul&gt;
&lt;li&gt;注意这里，如果是&lt;strong&gt;有CV或引用限定的函数类型&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;也就是这时候它还是&lt;code&gt;T&lt;/code&gt;，并不添加指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;通过add_pointer来找到另一种SFIANE的方法（void_t的替代品。也就是函数模板的SFINAE）&lt;/h2&gt;
&lt;p&gt;在add_pointer的模拟实现中，有这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace detail {
 
template &amp;lt;class T&amp;gt;
struct type_identity { using type = T; }; // 或使用 std::type_identity (C++20 起)
 
template &amp;lt;class T&amp;gt;
auto try_add_pointer(int) -&amp;gt; type_identity&amp;lt;typename std::remove_reference&amp;lt;T&amp;gt;::type*&amp;gt;;
template &amp;lt;class T&amp;gt;
auto try_add_pointer(...) -&amp;gt; type_identity&amp;lt;T&amp;gt;;
 
} // namespace detail
 
template &amp;lt;class T&amp;gt;
struct add_pointer : decltype(detail::try_add_pointer&amp;lt;T&amp;gt;(0)) {};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为什么这里会有一个&lt;code&gt;try_add_pointer(int)&lt;/code&gt; 和 &lt;code&gt;try_add_pointer(...) &lt;/code&gt;?
&lt;ul&gt;
&lt;li&gt;首先，这是一个函数。它的返回类型是&lt;code&gt;type_identity&amp;lt;typename std::remove_reference&amp;lt;T&amp;gt;::type*&amp;gt;&lt;/code&gt;这个东西。&lt;/li&gt;
&lt;li&gt;我们替换&lt;code&gt;T&lt;/code&gt;然后先找特化版本。如果这个实参的传入让后面的尾置返回类型合法，推导成功就自然而然选择这个特化版本。&lt;/li&gt;
&lt;li&gt;如果这个实参的传入导致后面的尾置返回类型非法，推导失败，会触发SFINAE。这时候这个特化版本就会被丢弃。由于&lt;code&gt;...&lt;/code&gt;能够接受任何类型的任何数量的参数，（就像我们上面8.4里面提到的一样）所以其他任何导致特化版本非法的使用都会被匹配到这个通用版本。&lt;/li&gt;
&lt;li&gt;所以说，这里到底是&lt;code&gt;int&lt;/code&gt;还是&lt;code&gt;float&lt;/code&gt;不重要。传入&lt;code&gt;1&lt;/code&gt;还是&lt;code&gt;0&lt;/code&gt;也不重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举一个不太好的但是能解释原理的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    typedef int mytype;
};

struct B{
    typedef int mytypea;
};

template &amp;lt;class T&amp;gt;
auto try_add_pointer(T) -&amp;gt; typename T::mytypea{
    cout &amp;lt;&amp;lt;&quot;no&quot; &amp;lt;&amp;lt; endl;
}
template &amp;lt;class T&amp;gt;
auto try_add_pointer(...) -&amp;gt; typename T::mytype{
    cout &amp;lt;&amp;lt;&quot;..&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    A obj;
    B obj1;
    try_add_pointer&amp;lt;A&amp;gt;(obj1); //匹配到..
    try_add_pointer(obj1); //错误
    try_add_pointer&amp;lt;B&amp;gt;(obj);  //错误

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先注意一点，可变参数&lt;code&gt;...&lt;/code&gt;是最低优先级。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为何第一个匹配到&lt;code&gt;..&lt;/code&gt;而第二个错误？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一个能成功的原因是，首先我们指定了函数模板是&lt;code&gt;A&lt;/code&gt;，所以&lt;code&gt;T&lt;/code&gt;换成&lt;code&gt;A&lt;/code&gt;。进入特化版本，&lt;code&gt;A&lt;/code&gt;没有&lt;code&gt;mytypea&lt;/code&gt;。此时甚至都不必考虑&lt;code&gt;obj1&lt;/code&gt;是&lt;code&gt;B&lt;/code&gt;类型和&lt;code&gt;T&lt;/code&gt;不匹配 的问题，特化直接被丢弃。&lt;strong&gt;注意，这里不考虑隐式转换。看1.2部分。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下一步，注意看，我们的变参包并没有指明类型，也就是变参包的类型是真正的任意。&lt;strong&gt;此时没办法进行推导&lt;/strong&gt;。因为尾置返回类型的&lt;code&gt;T&lt;/code&gt;压根没有一个参照。入参没有类型，&lt;code&gt;T&lt;/code&gt;从何来？&lt;/li&gt;
&lt;li&gt;所以我们这里有显式指明模板参数类型。这里我们显式指明&lt;code&gt;T&lt;/code&gt;为&lt;code&gt;A&lt;/code&gt;。然后发现&lt;code&gt;A&lt;/code&gt;有&lt;code&gt;mytype&lt;/code&gt;，所以匹配成功。选择泛化版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以第二个错误的原因是&lt;code&gt;T&lt;/code&gt;无法推导。变参包并没有指明类型，也就是变参包的类型是真正的任意。&lt;strong&gt;此时没办法进行推导&lt;/strong&gt;。因为尾置返回类型的&lt;code&gt;T&lt;/code&gt;压根没有一个参照。入参没有类型，&lt;code&gt;T&lt;/code&gt;从何来？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果想修复这个错误，那就变成&lt;code&gt;auto try_add_pointer(T...) -&amp;gt; typename T::mytype{&lt;/code&gt;, 加一个&lt;code&gt;T&lt;/code&gt;让形参参与推导即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三个错误的原因是指明类型为&lt;code&gt;B&lt;/code&gt;，&lt;code&gt;T&lt;/code&gt;换成&lt;code&gt;B&lt;/code&gt;，进入特化版本。&lt;code&gt;B&lt;/code&gt;虽然有&lt;code&gt;mytypea&lt;/code&gt;，但是&lt;strong&gt;和指明的入参类型不符合&lt;/strong&gt;。所以特化版本被丢弃。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时看&lt;code&gt;...&lt;/code&gt;版本，我们指明&lt;code&gt;T&lt;/code&gt;为&lt;code&gt;B&lt;/code&gt;，但是&lt;code&gt;B&lt;/code&gt;没有&lt;code&gt;mytypea&lt;/code&gt;，所以这个泛化版本也无法匹配。最终错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以正常应该这么写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    typedef int mytype;
};

struct B{
    typedef int mytypea;
};

template &amp;lt;class T&amp;gt;
auto try_add_pointer(T) -&amp;gt; typename T::mytypea{
    cout &amp;lt;&amp;lt;&quot;no&quot; &amp;lt;&amp;lt; endl;
}
template &amp;lt;class T&amp;gt;
auto try_add_pointer(T...) -&amp;gt; typename T::mytype{ //让...参与类型推导
    cout &amp;lt;&amp;lt;&quot;..&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    A obj;
    B obj1;
    try_add_pointer(obj);  //OK 不指明模板参数类型。输出..
    try_add_pointer(obj1);  //OK 不指明模板参数类型。输出no

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里都可以了是因为&lt;code&gt;A&lt;/code&gt;类型虽然没有&lt;code&gt;mytypea&lt;/code&gt;，但是可以匹配到泛化版本。因为有&lt;code&gt;mytype&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;obj1&lt;/code&gt;是&lt;code&gt;B&lt;/code&gt;类型，这时候因为没有像上面那样子指明类型导致冲突，所以&lt;code&gt;T&lt;/code&gt;自然被推导为&lt;code&gt;B&lt;/code&gt;。同时有&lt;code&gt;mytypea&lt;/code&gt;。自然可以。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/57506069/a-question-regarding-the-implementation-of-stdadd-pointer&lt;/p&gt;
&lt;h1&gt;std::conditional&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;bool B, class T, class F&amp;gt;
struct conditional { using type = T; };
template&amp;lt;class T, class F&amp;gt;
struct conditional&amp;lt;false, T, F&amp;gt; { using type = F; };
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;提供成员 typedef &lt;code&gt;type&lt;/code&gt; ，若 &lt;code&gt;B&lt;/code&gt; 在编译时为 &lt;code&gt;true&lt;/code&gt; 则定义为 &lt;code&gt;T&lt;/code&gt; ，或若 &lt;code&gt;B&lt;/code&gt; 为 false 则定义为 &lt;code&gt;F&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现也很简单。没什么多说的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，如果&lt;code&gt;T&lt;/code&gt;或&lt;code&gt;F&lt;/code&gt;之一是&lt;code&gt;ill-formed&lt;/code&gt;，则整个&lt;code&gt;conditional&lt;/code&gt;表达式也会是&lt;code&gt;ill-formed&lt;/code&gt; ---来自&lt;a href=&quot;https://youtu.be/ybaE9qlhHvw?t=1282&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;CRTP&lt;/h1&gt;
&lt;p&gt;先简单介绍，之后慢慢看。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CRTP的一个最明显的特点就是把派生类作为基类的模板参数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;std::type_identity (C++20)&lt;/h1&gt;
&lt;p&gt;这个是C++20提案的一个小工具. 我们看下它的实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
struct type_identity {
    using type = T;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个东西看起来没什么用. 但是它主要作用是防止某一个参数类型参与类型推导. 核心原理是使用了不推导语境的一条规则:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/identifiers#.E6.9C.89.E9.99.90.E5.AE.9A.E7.9A.84.E6.A0.87.E8.AF.86.E7.AC.A6&quot;&gt;有限定标识&lt;/a&gt;指定的类型的 &lt;em&gt;嵌套名说明符&lt;/em&gt;（作用域解析运算符 :: 左侧的所有内容)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们看一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
T add2(T a, T b) {
    return a + b;
}
auto sum2 = add2(0.5, 1);  // 不可以
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为参数类型被第一个参数推导为&lt;code&gt;double&lt;/code&gt; 所以第二个参数类型也必须为&lt;code&gt;double&lt;/code&gt; 因为模板参数类型推导不允许隐式类型转换, 所以会推导失败.&lt;/p&gt;
&lt;p&gt;如果我们使用&lt;code&gt;type_identity&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
T add(T a, typename type_identity&amp;lt;T&amp;gt;::type b) {
    return a + b;
}
auto sum = add(0.5, 1);    // T is &quot;double&quot; OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以了.  只不过第二个参数可以被隐式转换为了&lt;code&gt;double&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一个参数的类型决定了T是什么类型，其他所有人都必须遵守。我们可以使用它来指定第二个参数是不可推断的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;另一个在任务队列中常见的情况:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void enqueue(std::function&amp;lt;void(void)&amp;gt; const&amp;amp; work) { 
    cout &amp;lt;&amp;lt; __PRETTY_FUNCTION__ &amp;lt;&amp;lt; endl; 
    // dosomething
}

template &amp;lt;typename... Args&amp;gt;
void enqueue(std::function&amp;lt;void(Args...)&amp;gt; const&amp;amp; work, Args... args) {
    enqueue([=] { work(args...); });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里其实就是一个任务包装器. 把带函数和其参数打包到一个&lt;code&gt;std::function&lt;/code&gt;当做一个任务.&lt;/p&gt;
&lt;p&gt;如果现在我这样执行, 会有错误:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enqueue([](int v) { std::cout &amp;lt;&amp;lt; v; }, 42);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因就是参与了类型推导. &lt;code&gt;std::function&lt;/code&gt;和&lt;code&gt;lambda&lt;/code&gt;是两种不同类型, 尽管&lt;code&gt;lambda&lt;/code&gt;可以转换为&lt;code&gt;std::function&lt;/code&gt;. 这个时候就可以使用我们的&lt;code&gt;type_identity&lt;/code&gt; 因为这个类型推导失败并不能代表什么, 同时我们很希望忽略掉它.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename... Args&amp;gt;
void enqueue(typename type_identity&amp;lt;std::function&amp;lt;void(Args...)&amp;gt;&amp;gt;::type const&amp;amp; work, Args... args) {
    cout &amp;lt;&amp;lt; __PRETTY_FUNCTION__ &amp;lt;&amp;lt; endl;
    enqueue([=] { work(args...); });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考:&lt;/p&gt;
&lt;p&gt;blog来自&lt;a href=&quot;https://devblogs.microsoft.com/oldnewthing/20240607-00/?p=109865&quot;&gt;这里&lt;/a&gt; .可查看文章内的bonus chatter&lt;/p&gt;
&lt;p&gt;点击&lt;a href=&quot;https://open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0887r1.pdf&quot;&gt;这里&lt;/a&gt;查看完整原始proposal&lt;/p&gt;
&lt;p&gt;点击&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/template_argument_deduction&quot;&gt;这里&lt;/a&gt;查看cppreference. 在[不推导语境]一节中的1)&lt;/p&gt;
</content:encoded></item><item><title>C++ 模板笔记（五）：类型属性重载、模板与继承、类型列表、元组与 type traits</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-5/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-5/</guid><pubDate>Wed, 14 Dec 2022 14:54:00 GMT</pubDate><content:encoded>&lt;h1&gt;第二十章 类型属性上的重载&lt;/h1&gt;
&lt;p&gt;我们知道，函数模板可以重载。类模板不可以。我们将要讨论如何实现这一类似行为&lt;/p&gt;
&lt;h2&gt;20.1 算法特化（重载）&lt;/h2&gt;
&lt;p&gt;这个部分非常直观。就是为某一个特定类型提供一个更为特化的算法实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void swap(T a, T b){
    cout &amp;lt;&amp;lt;&quot;T swap&quot; &amp;lt;&amp;lt; endl;
}

template&amp;lt;typename T&amp;gt;
void swap(vector&amp;lt;T&amp;gt; a, vector&amp;lt;T&amp;gt; b){ //针对vector类型提供的特化版本。
    cout &amp;lt;&amp;lt;&quot;vector T swap&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    vector&amp;lt;int&amp;gt; a{1,2,3,4};
    swap(1,2);
    swap(a, a);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;但是不是所有的场合都能成功实现这一操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename InputIter&amp;gt;
void swap(InputIter a, InputIter b){
    cout &amp;lt;&amp;lt;&quot;InputIter swap&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename RandomAccessIter&amp;gt;
void swap(RandomAccessIter a, RandomAccessIter b){
    cout &amp;lt;&amp;lt;&quot;RandomAccessIter swap&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于只有函数模板形参名字不同的函数模板不可以被重载，所以会有重定义问题。这时候我们可以使用标记派发。&lt;/p&gt;
&lt;h2&gt;20.2 标记派发/标记分派（tag dispatching)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;可以同时参阅effective modern C++ 条款27 - 3&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们发现上一节中提到的例子，核心是&lt;strong&gt;只有函数模板形参名字不同的函数模板不可以被重载&lt;/strong&gt;。那么我们可以让这个名字成为模板形参的一部分。也就是打标签。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;下面的&lt;code&gt;tag&lt;/code&gt;其实就是传入一个对应特定类型的匿名对象。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//这部分迭代器分类套用标准库的一套。
struct my_input_iterator_tag {};
struct my_output_iterator_tag {};
struct my_forward_iterator_tag : public my_input_iterator_tag {};
struct my_bidirectional_iterator_tag : public my_forward_iterator_tag{};
struct my_random_access_iterator_tag : public my_bidirectional_iterator_tag {};

//这部分模拟某一种迭代器。
struct my_InputIter{
    using IterType =  my_input_iterator_tag; //迭代器里面有一个成员类型指明自己是哪一个迭代器类别。
};
struct my_RandomAccessIter{
    using IterType =  my_random_access_iterator_tag;
};

//迭代器类型萃取
template &amp;lt;typename Iter&amp;gt;
struct IterTraits{
    using IterType = typename Iter::IterType; //获取当前传入的迭代器的迭代器类别
};


template&amp;lt;typename Iterator&amp;gt;
void my_swapImpl(Iterator&amp;amp; x, Iterator&amp;amp; y){ //作为标签分派的转发函数。
    my_swap(x, y, typename IterTraits&amp;lt;Iterator&amp;gt;::IterType()); //细节注意，使用typename指明是类型后，使用()生成对应类型的匿名对象。
    //因为我们下面的标签是函数参数，需要一个对象。
}

template&amp;lt;typename InputIter&amp;gt; //对应的具体swap函数。
void my_swap(InputIter&amp;amp; a, InputIter&amp;amp; b, my_input_iterator_tag){ //第三个参数是tag。tag也是对象。
    cout &amp;lt;&amp;lt;&quot;InputIter swap&quot; &amp;lt;&amp;lt; endl;
}

template&amp;lt;typename RandomAccessIter&amp;gt;
void my_swap(RandomAccessIter&amp;amp; a, RandomAccessIter&amp;amp; b, my_random_access_iterator_tag){
    cout &amp;lt;&amp;lt;&quot;RandomAccessIter swap&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    
    my_RandomAccessIter r1;
    my_RandomAccessIter r2;
    my_InputIter i1;
    my_InputIter i2;

    my_swapImpl(r1, r2);
    my_swapImpl(i1, i2);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;标记派发非常适合用于有天然层次结构，并且拥有对应的萃取机制的情况。但是如果要依赖于专有类型属性的时候，比如某个类型是否有拷贝赋值，标记派发就不太方便了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;标签分派的实现可以有特化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct a{};
struct b{};
struct c{};

template&amp;lt;typename T&amp;gt;
struct Test{
    static const int num = 0;
};

template&amp;lt;&amp;gt;
struct Test&amp;lt;a&amp;gt;{
    static const int num = 1;
};

template&amp;lt;&amp;gt;
struct Test&amp;lt;b&amp;gt;{
    static const int num = 2;
};

template&amp;lt;&amp;gt;
struct Test&amp;lt;c&amp;gt;{
    static const int num = 3;
};

int main(){
    cout &amp;lt;&amp;lt; Test&amp;lt;a&amp;gt;::num &amp;lt;&amp;lt; endl; // 1
    cout &amp;lt;&amp;lt; Test&amp;lt;b&amp;gt;::num &amp;lt;&amp;lt; endl; // 2
    cout &amp;lt;&amp;lt; Test&amp;lt;c&amp;gt;::num &amp;lt;&amp;lt; endl; // 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种可以让你给某一个类型的标签进行特化。具体应用可以查看杂记5的&lt;em&gt;让enum支持位运算&lt;/em&gt;。&lt;/p&gt;
&lt;h2&gt;20.3 Enable/Disable 启用或禁用函数模板&lt;/h2&gt;
&lt;p&gt;我们在第六章的时候讲了一些关于使用&lt;code&gt;enable_if&lt;/code&gt;启用或禁用函数模板的例子。enable_if的核心其实就是在函数模板的偏序和重载机制上，再次强化选择的能力。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Iter&amp;gt; //变量模板。判断是否是random迭代器。这里有细节。为什么要用is_convertible而不用is_same?
constexpr bool IsRandomAccessIter = is_convertible&amp;lt;typename IterTraits&amp;lt;Iter&amp;gt;::IterType, my_random_access_iterator_tag&amp;gt;::value;

template&amp;lt;typename Iter&amp;gt; //如果是random
typename enable_if&amp;lt;IsRandomAccessIter&amp;lt;Iter&amp;gt;&amp;gt;::type process(Iter&amp;amp; a, Iter&amp;amp; b){
    cout &amp;lt;&amp;lt;&quot;IS random access iter&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename Iter&amp;gt; //如果不是random
typename enable_if&amp;lt;!IsRandomAccessIter&amp;lt;Iter&amp;gt;&amp;gt;::type process(Iter&amp;amp; a, Iter&amp;amp; b){
    cout &amp;lt;&amp;lt;&quot;NOT random access iter&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    cout &amp;lt;&amp;lt; boolalpha;
    my_RandomAccessIter r1;
    my_RandomAccessIter r2;
    my_InputIter i1;
    my_InputIter i2;
    process(i1, i1);
    process(r1, r2);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;细节为何要使用&lt;code&gt;is_convertible&lt;/code&gt;而不用&lt;code&gt;is_same&lt;/code&gt;？
&lt;ul&gt;
&lt;li&gt;这是关于语义的。迭代器属于拓展类语义。越子类功能越丰富。比如如果我们想要允许有ABC功能的对象使用此类，则应该也允许有ABCDE功能的对象使用此类。也就是允许当前类的子类使用。所以使用&lt;code&gt;is_convertible&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;这里注意应当把子类放在第一个参数，父类放在第二个参数。因为是判断子类是否可以转为父类。（子转父。不能父转子）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第二是我们需要写两个版本。这是为了激活SFINAE特性。因为我们不仅要指明符合要求时应该使用的方法，也要指明不符合要求的时候应该使用的方法。也就是确保对任意一个需要比较的类型，都应该只有一个模板可以被激活。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;20.3.1 提供多种特化版本（enable_if的坑点）&lt;/h3&gt;
&lt;p&gt;假如我们还要有一个额外的条件需要判断，比如判断是否满足A功能。则我们要添加更多的&lt;code&gt;enable_if&lt;/code&gt;去区分每一个更细致的特化。因为需要让它们彼此互斥。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这就是&lt;code&gt;enable_if&lt;/code&gt;的缺点。也就是随着条件增加，需要判断的条件越来越多，则需要越来越多的&lt;code&gt;enable_if&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;标记派发和enable_if通常适用于不同的两种情况。&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;标记派发可以基于分层的（具有天然结构性的）tags支持简单的派发（条件性选择）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enable_if&lt;/code&gt;则是基于类型萃取获得的任意一组属性来支持更为复杂的派发。（之前提到的专有类型属性）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;20.3.2 enable_if应该怎么用？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;enable_if &lt;/code&gt;通常被用于函数模板的返回类型。但是&lt;code&gt;enable_if &lt;/code&gt;&lt;strong&gt;本身&lt;/strong&gt;不适用于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造函数&lt;strong&gt;模板&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;类型转换&lt;strong&gt;模板&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为它们都没有被&lt;strong&gt;指定&lt;/strong&gt;返回类型。而且，使用 &lt;code&gt;enable_if &lt;/code&gt; 也会&lt;strong&gt;使得返回类型很难被读懂。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解决办法就是把&lt;code&gt;enable_if&lt;/code&gt;放入一个额外的模板参数中当做默认模板参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Itertest{
    public:
        Itertest() = default;
        template&amp;lt;typename Iter, typename = typename enable_if&amp;lt;IsRandomAccessIter&amp;lt;Iter&amp;gt;&amp;gt;::type&amp;gt;
        Itertest(Iter begin, Iter last){
            cout &amp;lt;&amp;lt;&quot;construct called Random&quot; &amp;lt;&amp;lt; endl;
        }
        template&amp;lt;typename Iter, typename = typename enable_if&amp;lt;!IsRandomAccessIter&amp;lt;Iter&amp;gt;&amp;gt;::type&amp;gt; //不可以。
        Itertest(Iter begin, Iter last){
            cout &amp;lt;&amp;lt;&quot;construct called Otheriter&quot; &amp;lt;&amp;lt; endl;
        }

};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;但是这时候我们不能使用老方法重载。因为只有函数模板参数的默认值不同是无法重载的。因为默认参数不算做函数签名。
&lt;ul&gt;
&lt;li&gt;这个问题可以使用额外模板参数（dummy parameter）来解决。另一个方法是6.3中重定义问题中的 “让参数本身不同”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Iter, typename = typename enable_if&amp;lt;!IsRandomAccessIter&amp;lt;Iter&amp;gt;&amp;gt;::type, typename = int&amp;gt; //第三个参数是dummy parameter。
    Itertest(Iter begin, Iter last){
    cout &amp;lt;&amp;lt;&quot;construct called Otheriter&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;20.4 类的特化&lt;/h2&gt;
&lt;p&gt;类模板的偏特化和函数模板的重载很像。&lt;/p&gt;
&lt;h3&gt;20.4.1 启用/禁用 类模板&lt;/h3&gt;
&lt;p&gt;这个就是6.6提到的&lt;code&gt;enable_if&lt;/code&gt;嵌入到类模板形参。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2, typename = void&amp;gt;
class myclass{
    
};

template&amp;lt;typename T1, typename T2&amp;gt;
class myclass&amp;lt;T1, T2, enable_if&amp;lt;....&amp;gt;&amp;gt;{
    
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;20.4.2 类模板的标记派发&lt;/h3&gt;
&lt;p&gt;这个明显较为复杂。注意，这不是CRTP。&lt;/p&gt;
&lt;p&gt;我们的核心还是要在不同的模板特化中做选择。我们沿用迭代器的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
//-------可替换部分---------
template&amp;lt;typename...&amp;gt; //1
struct GetBestMatch{};

template&amp;lt;&amp;gt; //2
struct GetBestMatch&amp;lt;&amp;gt;{ //3
    static void match(...); //4
};
//----------可以把上面的两个换成下面的一个。区别下面说--------------
template&amp;lt;typename...&amp;gt;
struct GetBestMatch{
    static void match(...);
};

//----------------------------

template&amp;lt;typename T1, typename... Args&amp;gt; //5
struct GetBestMatch&amp;lt;T1, Args...&amp;gt;: public GetBestMatch&amp;lt;Args...&amp;gt;{ //6
    static T1 match(T1){ //7

    }
    GetBestMatch(){ //测试函数签名用
        std::puts(__PRETTY_FUNCTION__);
    }
    using GetBestMatch&amp;lt;Args...&amp;gt;::match; //8
};

template&amp;lt;typename T1, typename... Args&amp;gt;
struct GetBestMatchT{
    using Type = decltype(GetBestMatch&amp;lt;Args...&amp;gt;::match(declval&amp;lt;T1&amp;gt;())); //9
};

template&amp;lt;typename T1, typename... Args&amp;gt;
using GetBestMatchValue = typename GetBestMatchT&amp;lt;T1, Args...&amp;gt;::Type; //10


template&amp;lt;typename Iter, typename tag = GetBestMatchValue &amp;lt;typename IterTraits&amp;lt;Iter&amp;gt;::IterType, 
                                                                my_bidirectional_iterator_tag,
                                                                my_forward_iterator_tag,
                                                                my_input_iterator_tag,
                                                                my_output_iterator_tag,
                                                                my_random_access_iterator_tag&amp;gt;&amp;gt; //11
class myoperation{};

template&amp;lt;typename Iter&amp;gt;
class myoperation&amp;lt;Iter, my_input_iterator_tag&amp;gt;{
    public:
    void func(Iter&amp;amp; iter){
        cout &amp;lt;&amp;lt; &quot;input iter&quot; &amp;lt;&amp;lt; endl;
    }
};

template&amp;lt;typename Iter&amp;gt;
class myoperation&amp;lt;Iter, my_random_access_iterator_tag&amp;gt;{
    public:
    void func(Iter&amp;amp; iter){
        cout &amp;lt;&amp;lt;&quot;random iter&quot; &amp;lt;&amp;lt; endl;
    }
};
int main(){
    
    my_RandomAccessIter r1;
    my_RandomAccessIter r2;
    my_InputIter i1;
    my_InputIter i2;
    myoperation&amp;lt;my_InputIter&amp;gt; obj;
    obj.func(i1);
    myoperation&amp;lt;my_RandomAccessIter&amp;gt; obj2;
    obj2.func(r1);
    GetBestMatch&amp;lt;my_InputIter, my_bidirectional_iterator_tag,
                                                                my_forward_iterator_tag,
                                                                my_input_iterator_tag,
                                                                my_output_iterator_tag,
                                                                my_random_access_iterator_tag&amp;gt; test;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;GetBestMatch&amp;lt;T1, Args ...&amp;gt;::GetBestMatch() [with T1 = my_random_access_iterator_tag; Args = {}]
GetBestMatch&amp;lt;T1, Args ...&amp;gt;::GetBestMatch() [with T1 = my_output_iterator_tag; Args = {my_random_access_iterator_tag}]
GetBestMatch&amp;lt;T1, Args ...&amp;gt;::GetBestMatch() [with T1 = my_input_iterator_tag; Args = {my_output_iterator_tag, my_random_access_iterator_tag}]
GetBestMatch&amp;lt;T1, Args ...&amp;gt;::GetBestMatch() [with T1 = my_forward_iterator_tag; Args = {my_input_iterator_tag, my_output_iterator_tag, my_random_access_iterator_tag}]
GetBestMatch&amp;lt;T1, Args ...&amp;gt;::GetBestMatch() [with T1 = my_bidirectional_iterator_tag; Args = {my_forward_iterator_tag, my_input_iterator_tag, my_output_iterator_tag, my_random_access_iterator_tag}]
GetBestMatch&amp;lt;T1, Args ...&amp;gt;::GetBestMatch() [with T1 = my_InputIter; Args = {my_bidirectional_iterator_tag, my_forward_iterator_tag, my_input_iterator_tag, my_output_iterator_tag, my_random_access_iterator_tag}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个还是比较复杂。一点一点看。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GetBestMatch&lt;/code&gt;就是我们模仿重载的类。&lt;/li&gt;
&lt;li&gt;1是这个类的主模板。可以匹配任意情况，但是没有实现。为什么要这么写，下面说。&lt;/li&gt;
&lt;li&gt;2是针对空参数包（parameter pack）实现的全特化。
&lt;ul&gt;
&lt;li&gt;3是具体的零长（空）参数包语法细节（12.4.5节）。因为是全特化所以特化部分声明依旧需要&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;4是使用&lt;code&gt;...&lt;/code&gt;接受参数包的任意参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;5是一个偏特化。类的可变参数模板的偏特化是可以从变长模板里拆分的（12.4.1节）。&lt;/li&gt;
&lt;li&gt;6这个类是继承自&lt;code&gt;GetBestMarch&lt;/code&gt;以剩余参数为参数的类的。这就是递归的模板展开。我们一会儿看函数签名。&lt;/li&gt;
&lt;li&gt;7测试函数签名&lt;/li&gt;
&lt;li&gt;8是使用&lt;code&gt;using&lt;/code&gt;引入父类的&lt;code&gt;match&lt;/code&gt;函数。不引入的话，父类的&lt;code&gt;match&lt;/code&gt;函数会被&lt;strong&gt;隐藏&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;9使用&lt;code&gt;declval&lt;/code&gt;搭配&lt;code&gt;decltype&lt;/code&gt;来判断输入的迭代器到底和哪个匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于我们的 &lt;code&gt;static T1 match(T1)&lt;/code&gt; 函数，目的就是找到一个匹配的版本。也就是如果我们有&lt;code&gt;ABC&lt;/code&gt;三个tag。则根据递归模板展开，会有三个match函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A match (A)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;B match (B)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C match (C)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模板展开的同时我们使用&lt;code&gt;using&lt;/code&gt;引入了父类的&lt;code&gt;match&lt;/code&gt;，所以三个&lt;code&gt;match&lt;/code&gt;都会被看见。最后在重载选择的时候，如果我们的&lt;code&gt;T1&lt;/code&gt;是&lt;code&gt;A&lt;/code&gt;就会正确匹配到&lt;code&gt;A&lt;/code&gt;。以此类推。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同时为什么返回值也要是&lt;code&gt;T1&lt;/code&gt;不是&lt;code&gt;void&lt;/code&gt;，因为我们用了&lt;code&gt;decltype&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们看函数签名，我们发现就是一层一层的递归。每一个参数模板依次被当做T1。然后剩余参数包内容继续递归直到参数包空了为止。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;整体逻辑：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先我们的&lt;code&gt;myoperation&lt;/code&gt;类的模板形参是&lt;code&gt;iter&lt;/code&gt;类型和一个此处调用了&lt;code&gt;GetBestMatchValue&lt;/code&gt;的检查最佳匹配的模板。&lt;/p&gt;
&lt;p&gt;然后，进入到&lt;code&gt;GetBestMatchValue&lt;/code&gt;别名模板。这时候的&lt;code&gt;T1&lt;/code&gt;是当前迭代器入参的类型。参数包是所有迭代器的类型。调用&lt;code&gt;GetBestMatchT&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后进入&lt;code&gt;GetBestMatchT&lt;/code&gt;萃取模板。首先把&lt;code&gt;T1&lt;/code&gt;也就是当前迭代器类型使用&lt;code&gt;declval&lt;/code&gt;模拟一个对象，调用&lt;code&gt;GetBestMatch&lt;/code&gt;模板类内的&lt;code&gt;match&lt;/code&gt;函数。同时使用&lt;code&gt;decltype&lt;/code&gt;获取返回值类型。&lt;/p&gt;
&lt;p&gt;我们此时进入核心模拟重载类模板&lt;code&gt;GetBestMatch&lt;/code&gt;。这时候传入的&lt;code&gt;Args&lt;/code&gt;是五个&lt;code&gt;tag&lt;/code&gt;。然后进入递归阶段。模板一直展开至参数包为&lt;code&gt;0&lt;/code&gt;，则 五个&lt;code&gt;tag&lt;/code&gt;的最后面一个依次被当做&lt;code&gt;T1&lt;/code&gt;，剩下的当做参数。&lt;/p&gt;
&lt;p&gt;比如&lt;code&gt;my_random_access_iterator_tag&lt;/code&gt;是最后一个，当前的&lt;code&gt;T1&lt;/code&gt;就是&lt;code&gt;my_random_access_iterator_tag&lt;/code&gt;。则&lt;code&gt;match&lt;/code&gt;函数就是&lt;code&gt;my_random_access_iterator_tag match(my_random_access_iterator_tag)&lt;/code&gt;。此时继承的基类是针对零长参数包的特化。使用&lt;code&gt;using&lt;/code&gt;引入继承来的&lt;code&gt;match&lt;/code&gt;函数。那个&lt;code&gt;match&lt;/code&gt;可以接受任意参数。&lt;/p&gt;
&lt;p&gt;以此类推。比如&lt;code&gt;my_output_iterator_tag&lt;/code&gt;是倒数第二个。然后就是有自己的&lt;code&gt;match&lt;/code&gt;函数。然后引入继承来的&lt;code&gt;match&lt;/code&gt;函数。因为此时的父类是&lt;code&gt;GetBestMatch&amp;lt;my_random_access_iterator_tag&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后继续。剩下的看上面的函数签名推一下就好了。&lt;/p&gt;
&lt;p&gt;最后，我们&lt;code&gt;GetBestMatchValue&lt;/code&gt;应该返回的是和输入的迭代器类型最匹配的那一个类型。然后实例化&lt;code&gt;myoperation&lt;/code&gt;类的对象的时候，模板实参会被默认参数补齐。&lt;code&gt;tag&lt;/code&gt;会被替换成正确版本。然后进行匹配至特定的偏特化版本。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最后关于两种替换的写法。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两种结果都可以。但是第一种带全特化的明显更好。原因是在这种情况下，可能有一些边缘情况我们没有考虑到。所以我们不应该让主模板接受任意参数，做为默认的版本。我们不应该提供主模板的实现，只提供一个针对零长参数包的特化。这样在某些边缘情况只能匹配到主模板的时候，应该报错。因为没有考虑过边缘情况。&lt;strong&gt;核心就是防止误用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;20.5 实例化安全的模板&lt;/h2&gt;
&lt;p&gt;这个例子比较长。简而言之书里&lt;code&gt;X3&lt;/code&gt;不行的原因是&lt;code&gt;X3&lt;/code&gt;的&lt;code&gt;operator&amp;lt;&lt;/code&gt;是接受两个非常量左值引用类型。而我们&lt;code&gt;HasLess&lt;/code&gt;里面的&lt;code&gt;test&lt;/code&gt;用了临时对象&lt;code&gt;&amp;lt;&lt;/code&gt;临时对象（无论是&lt;code&gt;declval&lt;/code&gt;还是直接&lt;code&gt;U1()&lt;/code&gt;都不行。因为都是右值）。但是非常量左值引用不能接受右值，自然就不行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;额外提到了的坑是&lt;code&gt;X7&lt;/code&gt;。&lt;code&gt;X7&lt;/code&gt;的&lt;code&gt;boollike&lt;/code&gt;虽然不能隐式转为&lt;code&gt;bool&lt;/code&gt;。但是可能发生&lt;strong&gt;显式的向&lt;code&gt;bool&lt;/code&gt;类型的转换可以被隐式使用&lt;/strong&gt;，比如控制语句的&lt;code&gt;bool&lt;/code&gt;类型条件，内置的&lt;code&gt;!&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt; 运算符和三目运算符&lt;code&gt;?:&lt;/code&gt;。在这些情况下，该值被认为是 &lt;em&gt;&lt;strong&gt;语境上可以转换为&lt;code&gt;bool&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这可能会发生问题。但是我们可以解决。虽然语句和被任意类型重载的逻辑不能出现在SFINAE上下文，但是三目运算符是表达式且不能被重载。所以可以用它来测试语境上是否可以转换为&lt;code&gt;bool&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;接着讨论一下&lt;code&gt;HasLess&lt;/code&gt;的细节&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class HasLess {
    template&amp;lt;typename T&amp;gt; struct Identity; //细节1
    
    template&amp;lt;typename U1, typename U2&amp;gt; 
    static std::false_type test(...);
    
    template&amp;lt;typename U1, typename U2&amp;gt; 
    static std::true_type test(Identity&amp;lt;decltype(declval&amp;lt;U1&amp;gt;() &amp;lt; declval&amp;lt;U2&amp;gt;())&amp;gt;*); //细节2


    public:
    static constexpr bool value = decltype(test&amp;lt;T1, T2&amp;gt;(nullptr))::value;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们发现他的实现有一些不同。细节1使用了一个额外的&lt;code&gt;struct&lt;/code&gt;，并且细节2使用了这个&lt;code&gt;struct&lt;/code&gt;的指针，为什么？
&lt;ul&gt;
&lt;li&gt;首先，我们的目的和之前的不同。之前可以使用一个函数形参来检测隐式转换。但是这里我们需要让整个小于表达式合法。我们可以在下面也使用之前的形式。但是这里更为方便。&lt;/li&gt;
&lt;li&gt;我们如果&lt;code&gt;U1&lt;/code&gt;和&lt;code&gt;U2&lt;/code&gt;拥有合法的小于比较操作，则这个&lt;code&gt;decltype&lt;/code&gt;是&lt;code&gt;bool&lt;/code&gt;类型。可以实例化一个&lt;code&gt;Identity&lt;/code&gt;的指针。为什么是指针？因为&lt;code&gt;Identity&lt;/code&gt;是不完整类型。&lt;/li&gt;
&lt;li&gt;如果没有合法操作，则匹配至&lt;code&gt;...&lt;/code&gt;万能版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们更改一下实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class AnotherHasLess {
    template&amp;lt;typename T&amp;gt; struct Identity{}; //细节1

    template&amp;lt;typename U1, typename U2&amp;gt; 
    static std::false_type test(...);

    template&amp;lt;typename U1, typename U2, typename = decltype(Identity&amp;lt;decltype(declval&amp;lt;U1&amp;gt;() &amp;lt; declval&amp;lt;U2&amp;gt;())&amp;gt;())&amp;gt;  //细节2
    static std::true_type test(void*);

    public:
    static constexpr bool value = decltype(test&amp;lt;T1, T2&amp;gt;(nullptr))::value;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于我们此时给&lt;code&gt;Identity&lt;/code&gt;提供的是完整类型，所以我们在下面可以不使用指针而是使用&lt;code&gt;()&lt;/code&gt;生成匿名对象。&lt;/li&gt;
&lt;li&gt;同时我们也不一定非得放到函数参数里。我们也可以放到模板参数里。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;甚至可以这样参考19.4.1和19.5来实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class MoreHasLess {
    static bool try_call(bool); //细节1
    template&amp;lt;typename U1, typename U2&amp;gt; 
    static std::false_type test(...); 

    template&amp;lt;typename U1, typename U2, typename = decltype(try_call(declval&amp;lt;U1&amp;gt;() &amp;lt; declval&amp;lt;U2&amp;gt;()))&amp;gt;  //细节2
    static std::true_type test(void*);

    public:
    static constexpr bool value = decltype(test&amp;lt;T1, T2&amp;gt;(nullptr))::value;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里我们依旧模拟了19.4.1的形式。使用一个函数。但是这有一个小问题。问题就是我们笃定&lt;code&gt;operator&amp;lt;&lt;/code&gt;一定返回&lt;code&gt;bool&lt;/code&gt;。但其实不一定。也不一定能隐式转换。这里其实就是看语义了。&lt;/li&gt;
&lt;li&gt;细节2就是这里可以和19.5一样了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第二十一章 模板和继承&lt;/h1&gt;
&lt;h2&gt;21.2 CRTP 奇异递归模板模式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CRTP的一个最明显的特点就是把派生类作为基类的模板参数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class Base{
    
};

class Derive: public Base&amp;lt;Derive&amp;gt;{
    
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CRTP 的第一个概要展示了一个&lt;strong&gt;非依赖性基类&lt;/strong&gt;: 子类（&lt;code&gt;Derive&lt;/code&gt;）不是模板，因此不受依赖基类的一些名称可见性问题的影响。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在CRTP模式中，基类被声明成一个类模板，派生类继承自基类的一个特化——基于派生类自身的特化&lt;/strong&gt;。如上面的代码所示，基类&lt;code&gt;Base&lt;/code&gt;根据派生类&lt;code&gt;Derive&lt;/code&gt;进行实例化，而派生类&lt;code&gt;Derive&lt;/code&gt;则继承基类&lt;code&gt;Base&lt;/code&gt;根据&lt;code&gt;Derive&lt;/code&gt;实例化的那个类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CRTP的核心是利用子类的信息生成代码，使用静态多态而不是动态多态。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们首先以一个例子和一个理由来阐述为什么需要CRTP。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们用一个最简单的例子解释什么叫静态多态替换了动态多态&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class Base{
    public:
        void func(){ //注意这个不是虚函数。不是动态多态
            static_cast&amp;lt;T*&amp;gt;(this)-&amp;gt;func(); //使用static_cast转换为子类指针
        }
};
class myclass:public Base&amp;lt;myclass&amp;gt;{
    public:
        void func(){
            cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
};

int main(){
    Base&amp;lt;myclass&amp;gt;* obj = new myclass;
    obj-&amp;gt;func();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的核心原理是，我们在父类的&lt;code&gt;func&lt;/code&gt;函数中使用&lt;code&gt;static_cast&lt;/code&gt;把&lt;code&gt;this&lt;/code&gt;指针&lt;code&gt;cast&lt;/code&gt;为子类指针，然后调用子类对象。可以用&lt;code&gt;static_cast&lt;/code&gt;是因为我们基本上可以从语义上保证这一操作绝对合法。&lt;/p&gt;
&lt;p&gt;可以使用智能指针，因为允许不完整类型。&lt;/p&gt;
&lt;p&gt;表面看上去没什么用，但是这确实是介绍CRTP的一个好方式。但是&lt;strong&gt;坑有很多&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;第一个坑  子类没有实现父类调用的函数&lt;/h3&gt;
&lt;p&gt;如果在我们上一个例子中，子类没实现&lt;code&gt;func&lt;/code&gt;函数会有什么问题？答案是无限递归。并段错误。但是编译期并没有问题。我们调用&lt;code&gt;Base::func()&lt;/code&gt;的时候，紧接着会调用&lt;code&gt;myclass::func()&lt;/code&gt;，但是子类没有自己的&lt;code&gt;func()&lt;/code&gt;，此时父类的&lt;code&gt;func()&lt;/code&gt;会被调用，自然无限递归。&lt;/p&gt;
&lt;p&gt;这里的问题是，没有什么规定来强制我们重写派生类中的成员函数&lt;code&gt;func()&lt;/code&gt;，但是如果我们不重写，程序就会产生错误。问题的根源在于，我们将接口和实现混在了一起——基类中的公共成员函数声明表示所有派生类都必须有一个函数&lt;code&gt;void func()&lt;/code&gt;作为其公共接口的一部分，而派生类则应该对此函数提供不同版本的实现。针对此问题，一种临时的解决方法是，让函数实现和函数声明具有不同的函数名，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class Base{
    public:
        void func(){
            static_cast&amp;lt;T*&amp;gt;(this)-&amp;gt;func_impl();
        }
    	//void func_impl(){}//默认实现，如果想要的话。
};
class myclass:public Base&amp;lt;myclass&amp;gt;{
    public:
        void func_impl(){ //换个名字
            cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，如果我们忘记实现&lt;code&gt;myclass::func_impl()&lt;/code&gt;，那么程序就不会被编译通过，因为派生类&lt;code&gt;myclass&lt;/code&gt;中不存在这样的成员函数。通过将函数声明和实现相分离的方式，我们在编译期多态中实现了纯虚函数的机制。如果我们想实现一个常规的虚函数，只需要在基类中提供一个&lt;code&gt;func_impl&lt;/code&gt;的默认实现。&lt;/p&gt;
&lt;h3&gt;第二个坑 析构函数&lt;/h3&gt;
&lt;p&gt;CRTP模式面临的一个问题是，如何根据一个基类指针删除对应的派生类对象？在运行期多态下，这就很容易，我们直接把析构函数声明成虚函数即可。但是在编译期多态中，不依赖虚函数，该如何解决这个问题呢？一个错误的解决方法是直接对基类指针调用&lt;code&gt;delete&lt;/code&gt;操作符。在这种情况下，&lt;strong&gt;只有基类的析构函数被调用了，派生类的析构函数没有被调用。因为此时基类的析构函数不是虚函数。&lt;/strong&gt;。另一个错误的解决方法是在基类的析构函数中将基类类型转换成派生类类型，再调用派生类的析构函数。然而，在基类的析构函数中，任何对派生类的非虚成员函数的调用都可能产生未定义行为。&lt;strong&gt;即使没有产生未定义行为，派生类的析构函数会调用基类的析构函数，从而产生递归调用。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class Base{
    public:
        void func(){
            static_cast&amp;lt;T*&amp;gt;(this)-&amp;gt;func_impl();
        }
        int* baseval = new int(10);
        virtual ~Base(){ //如果不加virtual，就会泄漏
            delete baseval;
        }
};


class myclass:public Base&amp;lt;myclass&amp;gt;{
    public:
        void func_impl(){
            cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
        int* deval = new int(20);
        ~myclass(){
            delete deval;
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个问题的解决方案有两个。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是使用一个额外的函数模板，将编译期多态性应用到删除操作上。比如&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void destroy(Base&amp;lt;T&amp;gt;* b) {
  delete static_cast&amp;lt;T*&amp;gt;(b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是比较违背RAII特性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;另一个解决方案是让析构函数成为真正的虚函数，也就是直接把析构函数声明为虚的。这种方法会略微增加虚函数调用的开销。不过我们只把析构函数声明成虚函数，相比于把所有成员函数声明成虚函数要好得多。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CRTP和委托模式&lt;/h3&gt;
&lt;p&gt;这是最为常见的一种CRTP用法。&lt;/p&gt;
&lt;p&gt;我们之前通过CRTP实现了编译期多态，可以通过基类指针或引用访问派生类对象中的方法。在这种情况下，基类会定义若干通用接口，而派生类负责提供不同的实现，这种用法也被称为静态接口。但是，如果我们直接使用派生类对象，那么情况就会变得不同——基类不再是接口，派生类也不仅仅是实现。&lt;strong&gt;派生类扩展了基类的接口，基类的一些行为被委托给派生类&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;假设我们有一个需求：为所有的提供了&lt;code&gt;operator==&lt;/code&gt;的类自动提供一个&lt;code&gt;operator!=&lt;/code&gt;。我们这时候就可以使用CRTP。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class Base{
    public:
        // bool operator!=(const Base&amp;amp; rhs){ //两种写法，这种更清晰
        //     if(static_cast&amp;lt;const T&amp;amp;&amp;gt;(*this).operator==(static_cast&amp;lt;const T&amp;amp;&amp;gt;(rhs)) == true){
        //         return false;
        //     }
        //     return true;
        // }
        bool operator!=(const Base&amp;amp; rhs){
            if((static_cast&amp;lt;const T&amp;amp;&amp;gt;(*this) == static_cast&amp;lt;const T&amp;amp;&amp;gt;(rhs)) == true){ //注意cast
                return false;
            }
            return true;
        }

};


class myclass:public Base&amp;lt;myclass&amp;gt;{
    public:
        int val = 0;
        myclass() = default;
        myclass(int x):val(x){}
        bool operator==(const myclass&amp;amp; rhs) const { //必须const
            return val == rhs.val;
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里我们主要注意cast。我们由于入参是&lt;code&gt;base&lt;/code&gt;类的引用，所以要给&lt;code&gt;this&lt;/code&gt;解引用后&lt;code&gt;cast&lt;/code&gt;为&lt;code&gt;T&lt;/code&gt;的引用，同时因为&lt;code&gt;static_cast&lt;/code&gt;不能移除&lt;code&gt;const&lt;/code&gt;，所以要&lt;code&gt;cast&lt;/code&gt;为&lt;code&gt;const&lt;/code&gt;引用。同时常量引用不能调用非常量成员函数，下面的&lt;code&gt;operator==&lt;/code&gt;依旧要为&lt;code&gt;const&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CRTP和enable_shared_from_this&lt;/h3&gt;
&lt;p&gt;STL里面最经典的CRTP应用便是这个了。如果我们想获取一个指向自己的智能指针，就必须继承自这个类。然后使用&lt;code&gt;shared_from_this()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Good: std::enable_shared_from_this&amp;lt;Good&amp;gt; // 注意：继承，模板参数是本类类型。符合CRTP特征。
{
    std::shared_ptr&amp;lt;Good&amp;gt; getptr() {
        return shared_from_this();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部分源代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp&amp;gt;
    class enable_shared_from_this
    {
    protected:

      enable_shared_from_this(const enable_shared_from_this&amp;amp;) noexcept { }
      ~enable_shared_from_this() { }

    public:
      shared_ptr&amp;lt;_Tp&amp;gt;
      shared_from_this()
      { return shared_ptr&amp;lt;_Tp&amp;gt;(this-&amp;gt;_M_weak_this); } //通过弱指针来创建shared_ptr并返回。

      shared_ptr&amp;lt;const _Tp&amp;gt;
      shared_from_this() const
      { return shared_ptr&amp;lt;const _Tp&amp;gt;(this-&amp;gt;_M_weak_this); }

    private:
      mutable weak_ptr&amp;lt;_Tp&amp;gt;  _M_weak_this; //维护的类类型的弱指针
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;enable_shared_from_this的核心原理是：通过自身维护了一个&lt;code&gt;std::weak_ptr&lt;/code&gt;让所有从该对象派生的&lt;code&gt;shared_ptr&lt;/code&gt;都通过&lt;code&gt;std::weak_ptr&lt;/code&gt;构造派生。关于这个弱指针在哪儿被赋值，看&lt;a href=&quot;https://segmentfault.com/a/1190000020861953&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;CRTP和计数器&lt;/h3&gt;
&lt;p&gt;可以查看more effective c++ 条款26&lt;/p&gt;
&lt;p&gt;https://www.youtube.com/watch?v=ZQ-8laAr9Dg&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/breadheart/article/details/112451022&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/shuo-ouyang/p/15773193.html&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/happenlee/p/13278640.html&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/lgp88/article/details/7529254&lt;/p&gt;
&lt;h2&gt;21.3 mixin 混合类&lt;/h2&gt;
&lt;p&gt;这是一个非常神奇的东西。有一点依赖倒置样子。鼓励代码重用，且不会有多重继承导致的歧义（菱形问题）。&lt;/p&gt;
&lt;p&gt;继承的问题是无法自由的组合这些具体类，但是mixin是一种灵活的组合概念。&lt;/p&gt;
&lt;p&gt;它可以通过多继承来实现。&lt;/p&gt;
&lt;p&gt;上面又说组合，又说可以通过继承来实现，是不是非常疑惑？看看下面的第一个例子。&lt;/p&gt;
&lt;p&gt;我想出来了两种例子，但是我并不确定mixin是否有严格定义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一种情况。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种情况较为常见。就是传统意义的多继承。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class printBase{
    public:
        virtual void print_val() = 0;
};
class printMethod1 : public virtual printBase{
    public:
        void my_print(){
            cout &amp;lt;&amp;lt;&quot;method1&quot; &amp;lt;&amp;lt; endl;
        }
        void print_val(){
            my_print();
        }
};
class printMethod2 : public virtual printBase{
    public:
        void my_print(){
            cout &amp;lt;&amp;lt;&quot;method2&quot; &amp;lt;&amp;lt; endl;
        }
        void print_val(){
            my_print();
        }
};
class I_want_all:public printMethod1, printMethod2{
    public:
        void print_val(){
            printMethod1::my_print();
        }
        void print_method1(){
            print_val();
        }
        void print_method2(){
            printMethod2::my_print();
        }
};
int main(){
    I_want_all obj;
    obj.print_method1();
    obj.print_method2();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题在于虚继承会有损性能。如何用mixin改写？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class printMethod1 : public T{
    public:
        void my_print(){
            cout &amp;lt;&amp;lt;&quot;method1&quot; &amp;lt;&amp;lt; endl;
            T::my_print();
        }
};

template&amp;lt;typename T&amp;gt;
class printMethod2 : public T{
    public:
        void my_print(){
            cout &amp;lt;&amp;lt;&quot;method2&quot; &amp;lt;&amp;lt; endl;
            T::my_print();
        }
};

class myclass{
    public:
        void my_print(){
            cout &amp;lt;&amp;lt;&quot;my print&quot; &amp;lt;&amp;lt; endl;
        }
};

int main(){
    myclass obj;
    obj.my_print();
    cout&amp;lt;&amp;lt;&quot;-----------------------&quot;&amp;lt;&amp;lt;endl;
    printMethod1&amp;lt;myclass&amp;gt; obj2;
    obj2.my_print();
    cout&amp;lt;&amp;lt;&quot;-----------------------&quot;&amp;lt;&amp;lt;endl;
    printMethod2&amp;lt;myclass&amp;gt; obj3;
    obj3.my_print();
    cout&amp;lt;&amp;lt;&quot;-----------------------&quot;&amp;lt;&amp;lt;endl;
    printMethod1&amp;lt;printMethod2&amp;lt;myclass&amp;gt;&amp;gt; obj4;
    obj4.my_print();
    cout&amp;lt;&amp;lt;&quot;-----------------------&quot;&amp;lt;&amp;lt;endl;
    printMethod2&amp;lt;printMethod1&amp;lt;myclass&amp;gt;&amp;gt; obj5;
    obj5.my_print();
}
/*
my print
-----------------------
method1
my print
-----------------------
method2
my print
-----------------------
method1
method2
my print
-----------------------
method2
method1
my print
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们主要注意到了，不再有&lt;code&gt;printBase&lt;/code&gt;基类了。也就是这里不存在严格的单继承或多继承关系。但是非常神奇的一点是在最后&lt;code&gt;obj4&lt;/code&gt;和&lt;code&gt;obj5&lt;/code&gt;的使用中，神奇的所有需要打印的都打印了。以&lt;code&gt;obj4&lt;/code&gt;为例。&lt;code&gt;printMethod1&lt;/code&gt;继承自&lt;code&gt;printMethod2&amp;lt;myclass&amp;gt;&lt;/code&gt;。&lt;code&gt;printMethod2&lt;/code&gt;继承自&lt;code&gt;myclass&lt;/code&gt;。所以在&lt;code&gt;printMethod1&lt;/code&gt;中，打印了自己的后&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;printMethod2&amp;lt;myclass&amp;gt;&lt;/code&gt;，进入&lt;code&gt;printMethod2&lt;/code&gt;中的打印，打印自己后&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;myclass&lt;/code&gt;，所以最后进入&lt;code&gt;myclass&lt;/code&gt;打印。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们发现，这么看起来，&lt;code&gt;myclass&lt;/code&gt;反而是基类。这就是一种依赖倒置。我们提供实现了&lt;code&gt;my_print&lt;/code&gt;的类，而库只需要调用我们的类即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果严格按照继承来进行，失去了灵活的组合能力。必须固定化某个顺序。比如我们假设想要调整打印顺序，如果是默认的单继承是不可能的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第二个例子&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个例子中，非常明显的体现出了依赖倒置原则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Mixin&amp;gt;
class PointMixin : public Mixin...{
    public:
        PointMixin() = default;
        PointMixin(int x, int y): Mixin()..., posx(x), posy(y){} //Mixin()...是一个参数包展开。作用是调用所有继承来的类的默认构造。这里最好调用一下。养成好习惯。虽然此处不调用也没差。
        int posx = 0;
        int posy = 0;
};
class color{
    public:
        color(){
            cout &amp;lt;&amp;lt;&quot;color const called&quot; &amp;lt;&amp;lt; endl;
        }
        int colorID = 234;
        void colorfunc(){
            cout &amp;lt;&amp;lt;&quot;color&quot; &amp;lt;&amp;lt; endl;
        }
};

class label{
    public:
        label(){
            cout &amp;lt;&amp;lt;&quot;label const called&quot; &amp;lt;&amp;lt; endl;
        }
        char charId = &apos;a&apos;;
        void labelfunc(){
            cout &amp;lt;&amp;lt;&quot;label&quot; &amp;lt;&amp;lt; endl;
        }
};
int main(){
    PointMixin&amp;lt;color, label&amp;gt; obj;
    obj.labelfunc();
    obj.colorfunc();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子的依赖倒置就非常非常明显。我们提供的&lt;code&gt;color&lt;/code&gt;和&lt;code&gt;label&lt;/code&gt;被&lt;code&gt;PointMixin&lt;/code&gt;继承。所以就算&lt;code&gt;PointMixin&lt;/code&gt;可能是库提供的，但是它依旧是子类。这里利用了可变参数包展开的多继承。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们可以发现，&lt;code&gt;PointMixin&lt;/code&gt;成功获得了我们的&lt;code&gt;color&lt;/code&gt;和&lt;code&gt;label&lt;/code&gt;类的属性，也就是我们成功把可变属性添加到了不可变的类当中。这种依赖倒置还是非常常见的。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们甚至还可以用容器储存：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Mixins&amp;gt;
class containerMixins{
    public:
        vector&amp;lt;PointMixin&amp;lt;Mixins...&amp;gt;&amp;gt; myvec;
};
int main(){
    PointMixin&amp;lt;color, label&amp;gt; obj;
    containerMixins&amp;lt;color, label&amp;gt; mycontainer;
    mycontainer.myvec.push_back(obj);
    mycontainer.myvec[0].labelfunc();
    mycontainer.myvec[0].colorfunc();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;混合类还可以和CRTP共同使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://fuzhe1989.github.io/2018/04/21/mixin/&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/jiang4357291/article/details/103325488&lt;/p&gt;
&lt;h1&gt;第二十二章 桥接（此处的桥接是动词，连接的意思）静态和动态多态&lt;/h1&gt;
&lt;h2&gt;22.1 函数对象，函数指针和std::function&lt;/h2&gt;
&lt;p&gt;这是老生常谈的问题了。std::function使用类型擦除可以同时接收多种可调用对象。&lt;/p&gt;
&lt;p&gt;假设我们需要让一个函数接受多种可调用对象，可能的一种方式是使用模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myobj{ //重载了函数调用运算符的类
    void operator()(int x){
        cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
    }
};
void func(int x){ //函数指针
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
}


template&amp;lt;typename F&amp;gt;
void test(int x, F f){ //调用
    std::puts(__PRETTY_FUNCTION__);
    f(x);
}


int main(){
    auto mylambda = [](int x){cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;}; //lambda
    test(10, mylambda);
    test(20, func);
    test(30, myobj());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;原因是：
&lt;ul&gt;
&lt;li&gt;重载了函数调用运算符的类不能直接转换为函数指针。它是成员函数指针，和函数签名不符合。&lt;/li&gt;
&lt;li&gt;lambda只有在捕获符为空的时候才能被转换为函数指针（参见lambda介绍，只有捕获符为空的时候用户定义转换函数才会被定义）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以看到，三者的&lt;code&gt;F&lt;/code&gt;类型推导结果并不一致&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void test(int, F) [with F = main()::&amp;lt;lambda(int)&amp;gt;]
called10
void test(int, F) [with F = void (*)(int)]
called20
void test(int, F) [with F = myobj]
called30
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;所以如果不使用模板，这三者并不能被兼容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这样做有一个问题：三种类型的实例化会导致代码膨胀，所以我们有类型擦除的典范：&lt;code&gt;std::function&lt;/code&gt;。但是它性能很差。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;剩下的是std::function的模拟实现和桥接模式&lt;/h2&gt;
&lt;p&gt;给我脸都看绿了。&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/zdy0_2004/article/details/50652934&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/weixin_43798887/article/details/116571325&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/560964284&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/66301236&lt;/p&gt;
&lt;h1&gt;第二十四章 类型列表&lt;/h1&gt;
&lt;h2&gt;24.1~24.2 类型列表剖析和操作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;typelist简单的说就是一个类型容器，能够对类型提供一系列的操作（把类型当成数据来操作）。就象C++标准库中的list容器（能够对各种数值提供各种基本操作），但typelist是针对类型进行操作。&lt;/li&gt;
&lt;li&gt;从实现上，typelist是一个类模板，中文名字叫：类型列表。该类模板用来表示一个列表，在该列表中存放着一堆类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：typelist的值不可以被修改。也就是我们下面例子中任何对typelist的操作，如增添，删除，都是通过对应操作创建的新的typelist&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Items&amp;gt;
class typeList{}; //类型列表 主类 。你没看错，就这么简单

//-----获取第一个元素------
template&amp;lt;typename Items&amp;gt; //Item代表了整个typeList。所以此处不需要变参。
class FrontItem{}; //获取第一个元素的主模板。

//因为我们整个塞进去的是typeList。所以我们下面的特化是基于typeList里面的参数的。
template&amp;lt;typename First, typename... Other&amp;gt;
class FrontItem&amp;lt;typeList&amp;lt;First, Other...&amp;gt;&amp;gt;{ //偏特化，把元素按照第一个和其余的拆开。
    public:
        using type = First; //只保留第一个元素
};
//-----移除第一个元素------
template &amp;lt;typename Items&amp;gt; //此处同FrontItem
class PopFront{}; //移除第一个元素

template&amp;lt;typename First, typename... Other&amp;gt; //此处同FrontItem
class PopFront&amp;lt;typeList&amp;lt;First, Other...&amp;gt;&amp;gt;{
    public:
    using type = typeList&amp;lt;Other...&amp;gt;; //去掉第一个元素，成为新的typelist
};
//-----获取typelist的大小------
template&amp;lt;typename Items&amp;gt;
class ListSize{}; //获取类型列表的大小。此处同FrontItem

template&amp;lt;typename... Items&amp;gt; //因为我们不再需要拆开了，所以不需要First模板形参了。
class ListSize&amp;lt;typeList&amp;lt;Items...&amp;gt;&amp;gt;{
    public:
    static inline size_t value = sizeof...(Items);
};

//-----头部插入元素------
template&amp;lt;typename Items, typename NewItem&amp;gt; //此处同FrontItem, Items是整个typelist, NewItem是新的元素（类型）。
class PushFront{}; //头插

template&amp;lt;typename... Items, typename NewItem&amp;gt; //和pop相反
class PushFront&amp;lt;typeList&amp;lt;Items...&amp;gt;, NewItem&amp;gt;{ //和pop相反
    public:
        using type = typeList&amp;lt;NewItem, Items...&amp;gt;; //直接组合成新的typelist
};

//-----尾部插入元素------
template&amp;lt;typename Items, typename NewItem&amp;gt; //此处同FrontItem, Items是整个typelist, NewItem是新的类型。
class PushBack{}; //尾插

template&amp;lt;typename... Items, typename NewItem&amp;gt; //和头插相同
class PushBack&amp;lt;typeList&amp;lt;Items...&amp;gt;, NewItem&amp;gt;{ //和头插相同
    public:
        using type = typeList&amp;lt;Items..., NewItem&amp;gt;; //直接组合成新的typelist，只是和头插顺序相反。
};

//-----替换第一个元素------
template&amp;lt;typename Items, typename ReplaceItem&amp;gt;
class ReplaceFront{}; //替换第一个元素。

template&amp;lt;typename First, typename... Other ,typename ReplaceItem&amp;gt; //注意这里有三个元素。第一个，剩下的和替换的。
class ReplaceFront&amp;lt;typeList&amp;lt;First, Other...&amp;gt;, ReplaceItem&amp;gt;{
    public:
        using type = typeList&amp;lt;ReplaceItem, Other...&amp;gt;; //直接把第一个换成替换的即可。
};

//-----判断是否为空------
template&amp;lt;typename Items&amp;gt;
class isEmpty{ //判断是否为空。
    public:
        static inline const bool value = false;
};

template&amp;lt;&amp;gt;
class isEmpty&amp;lt;typeList&amp;lt;&amp;gt;&amp;gt;{ //直接全特化。如果列表为空直接匹配过来。
    public:
        static inline const bool value = true;
};

//-----真正的typelist-----
using Typelist1 = typeList&amp;lt;int, char, string, double, float&amp;gt;; //类型列表
using emptylist = typeList&amp;lt;&amp;gt;;//空的类型列表

int main(){
    cout &amp;lt;&amp;lt; boolalpha;
    cout &amp;lt;&amp;lt; typeid(FrontItem&amp;lt;Typelist1&amp;gt;::type).name() &amp;lt;&amp;lt; endl; //输出类型列表第一个元素的类型。int
    cout &amp;lt;&amp;lt; ListSize&amp;lt;Typelist1&amp;gt;::value &amp;lt;&amp;lt; endl;//输出类型列表的大小。5

    cout &amp;lt;&amp;lt; typeid(FrontItem&amp;lt;PopFront&amp;lt;Typelist1&amp;gt;::type&amp;gt;::type).name() &amp;lt;&amp;lt; endl; //先把类型列表的第一个元素去掉，然后输出第一个元素类型。char。记住，我们针对所有原始类型列表的操作，都不是作用于原始列表。都是新建列表。
    cout &amp;lt;&amp;lt; ListSize&amp;lt;PopFront&amp;lt;Typelist1&amp;gt;::type&amp;gt;::value&amp;lt;&amp;lt;endl; //获取移除第一个元素后的列表大小。4

    cout &amp;lt;&amp;lt; typeid(FrontItem&amp;lt;Typelist1&amp;gt;::type).name() &amp;lt;&amp;lt; endl; //获取原始类型列表的第一个元素，证明操作不作用于原始列表。int

    using pushedback = PushBack&amp;lt;Typelist1, int&amp;gt;::type; //使用别名方便使用。这个操作是在类型列表尾部插入新的类型int
    cout &amp;lt;&amp;lt; ListSize&amp;lt;pushedback&amp;gt;::value&amp;lt;&amp;lt;endl; //插入后，新列表大小。6（原来是5）

    using pushedfront = PushFront&amp;lt;pushedback, double&amp;gt;::type; //在尾插后的类型列表头插double
    cout &amp;lt;&amp;lt; ListSize&amp;lt;pushedfront&amp;gt;::value&amp;lt;&amp;lt;endl;//新列表大小7

    using replaced_front = ReplaceFront&amp;lt;Typelist1, float&amp;gt;::type; //把类型列表首元素换位float
    cout &amp;lt;&amp;lt; typeid(FrontItem&amp;lt;replaced_front&amp;gt;::type).name() &amp;lt;&amp;lt; endl; //新列表首元素 float


    cout &amp;lt;&amp;lt; isEmpty&amp;lt;Typelist1&amp;gt;::value &amp;lt;&amp;lt; endl; //是否为空，否
    cout &amp;lt;&amp;lt; isEmpty&amp;lt;emptylist&amp;gt;::value &amp;lt;&amp;lt; endl; //是否为空，是
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以发现，整个过程其实就是针对typelist元素的组合。应用了偏特化和全特化的知识。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;额外介绍一下索引&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Items, unsigned int m_index&amp;gt; //item是个typelist，m_index是索引
class find_element: public find_element&amp;lt;typename PopFront&amp;lt;Items&amp;gt;::type, m_index-1&amp;gt;{};
//这里面用了递归，每次都继承自把首元素去掉的typelist。注意需要使用type因为那才是我们定义的typelist。并且加typename显式指明是类型。然后index-1

template&amp;lt;typename Items&amp;gt;//这个是针对找到了对应index的特化。因为index最终会减到0。这时候的特化版本继承自FrontItem，因为这个时候typelist的首元素就是我们想要找的那个元素。
class find_element&amp;lt;Items, 0&amp;gt;: public FrontItem&amp;lt;Items&amp;gt;{};


cout &amp;lt;&amp;lt; typeid(find_element&amp;lt;Typelist1, 3&amp;gt;::type).name() &amp;lt;&amp;lt; endl; //输出double

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;所以整个过程就是&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find_element&amp;lt;typelist&amp;lt;int, char, string, double, float&amp;gt;,3&amp;gt;&lt;/code&gt; 继承自 &lt;code&gt;find_element&amp;lt;typelist&amp;lt;char, string, double, float&amp;gt;,2&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;然后继承自 &lt;code&gt;find_element&amp;lt;typelist&amp;lt;string, double, float&amp;gt;,1&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;然后当前变为&lt;code&gt;find_element&amp;lt;typelist&amp;lt;double, float&amp;gt;,0&amp;gt;&lt;/code&gt;，继承自特化版本，就是&lt;code&gt;FrontItem&amp;lt;typelist&amp;lt;double, float&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;因为&lt;code&gt;FrontItem&lt;/code&gt;定义了&lt;code&gt;type&lt;/code&gt;。所以可以直接用了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;24.3~ 这部分比较枯燥，暂时略过&lt;/h2&gt;
&lt;h1&gt;第二十五章 元组&lt;/h1&gt;
&lt;p&gt;元组就是一种可以储存不同类型的容器。通常使用模板元编程和typelist来实现。元组在语法上有一个不同的地方，也就是元组中的元素是用位置信息（下标）索引的。而不是通过名字。&lt;/p&gt;
&lt;h2&gt;25.1 基本的元组设计&lt;/h2&gt;
&lt;p&gt;元组的核心原理和上面的typelist几乎一模一样。&lt;/p&gt;
&lt;h3&gt;25.1.1 存储&lt;/h3&gt;
&lt;p&gt;我们应该记得标准库的元组的使用方式。存储通过函数模板&lt;code&gt;get()&lt;/code&gt;进行访问。对于元组&lt;code&gt;t&lt;/code&gt;，语法为&lt;code&gt;get&amp;lt;I&amp;gt;(t)&lt;/code&gt;。&lt;code&gt;I&lt;/code&gt;是元素下标。此时返回的是第&lt;code&gt;I&lt;/code&gt;个元素的&lt;strong&gt;引用&lt;/strong&gt;（和其他容器一样）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
template&amp;lt;typename... Types&amp;gt;
struct my_tuple{}; //主模板。和typelist一样。


template&amp;lt;typename First, typename... Others&amp;gt;
struct my_tuple&amp;lt;First, Others...&amp;gt;{
    First first; //头元素
    my_tuple&amp;lt;Others...&amp;gt; others; //其他的元素组成的my_tuple

    my_tuple(){
        cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl; //默认构造
    };
    my_tuple(const First&amp;amp; head, const my_tuple&amp;lt;Others...&amp;gt;&amp;amp; tail):first(head), others(tail){
        cout &amp;lt;&amp;lt;&quot;tuple const called&quot; &amp;lt;&amp;lt; endl;
        //这个构造的意义是使用一个元素和另一个tuple进行构造。
    };  

    my_tuple(const First&amp;amp; head, const Others&amp;amp;... tail):first(head), others(tail...){
        cout &amp;lt;&amp;lt; first &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt; &quot;size of rest elements: &quot;&amp;lt;&amp;lt; sizeof...(tail) &amp;lt;&amp;lt; endl;
        std::puts(__PRETTY_FUNCTION__);
        cout &amp;lt;&amp;lt;&quot;element const called&quot; &amp;lt;&amp;lt; endl;
        //这个构造才是我们直观的，使用一堆独立的元素构造
    };


    First&amp;amp; getHead(){
        cout &amp;lt;&amp;lt;&quot;getHead called&quot; &amp;lt;&amp;lt; endl;
        return first;
    }

    const First&amp;amp; getHead() const{
        cout &amp;lt;&amp;lt;&quot;getHead called&quot; &amp;lt;&amp;lt; endl;
        return first;
    }

    my_tuple&amp;lt;Others...&amp;gt;&amp;amp; getTail(){
        cout &amp;lt;&amp;lt;&quot;getTail called&quot; &amp;lt;&amp;lt; endl;
        return others;
    }

    const my_tuple&amp;lt;Others...&amp;gt;&amp;amp; getTail() const{
        cout &amp;lt;&amp;lt;&quot;getTail called&quot; &amp;lt;&amp;lt; endl;
        return others;
    }
    ~my_tuple(){
        cout &amp;lt;&amp;lt;&quot;dest&quot; &amp;lt;&amp;lt; endl;
    }
};


template&amp;lt;&amp;gt; //这个特化是针对空参数。也就是最后剩余参数为0的情况
struct my_tuple&amp;lt;&amp;gt;{

};



template &amp;lt;unsigned int m_index&amp;gt;
struct find_element{
    template&amp;lt;typename First, typename... Others&amp;gt;
    static auto m_get(const my_tuple&amp;lt;First, Others...&amp;gt;&amp;amp; t){
        cout &amp;lt;&amp;lt;&quot;current index: &quot; &amp;lt;&amp;lt; m_index &amp;lt;&amp;lt; endl;
        return find_element&amp;lt;m_index-1&amp;gt;::m_get(t.getTail());
        //这里的类模板是get&amp;lt;N&amp;gt;的N我们要显式指定
        //这里的函数模板是通过my_tuple进行推导。可变参数被拆分了。
        //随后递归调用，m_index每次-1然后调用getTail，也就是去掉每一次的头部。
    }
};

template&amp;lt;&amp;gt; //find_element的特化。也就是此时index减到为0了。我们找到了想找的元素
struct find_element&amp;lt;0&amp;gt;{
    //下面是两个版本而已。
    template&amp;lt;typename First, typename... Others&amp;gt;
    static const First&amp;amp; m_get(const my_tuple&amp;lt;First, Others...&amp;gt;&amp;amp; t){
        cout &amp;lt;&amp;lt;&quot;m_get called&quot; &amp;lt;&amp;lt; endl;
        return t.getHead();
        //由于此时头部元素就是我们想要找的，所以直接调用getHead
    }
    template&amp;lt;typename First, typename... Others&amp;gt;
    static First&amp;amp; m_get(my_tuple&amp;lt;First, Others...&amp;gt;&amp;amp; t){
        cout &amp;lt;&amp;lt;&quot;m_get called&quot; &amp;lt;&amp;lt; endl;
        return t.getHead();
    }
};

//----------const和非const的get。注意get不是成员函数。标准库也是这样设计的。
template&amp;lt;unsigned int m_index, typename... Types&amp;gt;
auto get(const my_tuple&amp;lt;Types...&amp;gt;&amp;amp; t ){
    return find_element&amp;lt;m_index&amp;gt;::m_get(t);
}
template&amp;lt;unsigned int m_index, typename... Types&amp;gt;
auto get(my_tuple&amp;lt;Types...&amp;gt;&amp;amp; t ){
    return find_element&amp;lt;m_index&amp;gt;::m_get(t);
}
int main(){
    my_tuple&amp;lt;int, char, double, float&amp;gt; testtuple(10, &apos;a&apos;, 2.3, 3.4);
    cout&amp;lt;&amp;lt;&quot;--------------------------------------&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; get&amp;lt;2&amp;gt;(testtuple) &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;3.4
size of rest elements: 0
my_tuple&amp;lt;First, Others ...&amp;gt;::my_tuple(const First&amp;amp;, const Others&amp;amp; ...) [with First = float; Others = {}]
element const called
2.3
size of rest elements: 1
my_tuple&amp;lt;First, Others ...&amp;gt;::my_tuple(const First&amp;amp;, const Others&amp;amp; ...) [with First = double; Others = {float}]
element const called
a
size of rest elements: 2
my_tuple&amp;lt;First, Others ...&amp;gt;::my_tuple(const First&amp;amp;, const Others&amp;amp; ...) [with First = char; Others = {double, float}]
element const called
10
size of rest elements: 3
my_tuple&amp;lt;First, Others ...&amp;gt;::my_tuple(const First&amp;amp;, const Others&amp;amp; ...) [with First = int; Others = {char, double, float}]
element const called
--------------------------------------
current index: 2
getTail called
current index: 1
getTail called
m_get called
getHead called
2.3
dest
dest
dest
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们发现，构造过程中和CRTP一样，是递归调用。我们通过函数签名发现，每次在构造函数中初始化本类的&lt;code&gt;others&lt;/code&gt;的时候，都会进入下一层的递归。所以在最后一层&lt;code&gt;Others&lt;/code&gt;参数包为空的时候，&lt;code&gt;first&lt;/code&gt;就是&lt;code&gt;tuple&lt;/code&gt;的最后一个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以第一个打印出来的是&lt;code&gt;3.4&lt;/code&gt; (最后一个元素)。参数包&lt;code&gt;size&lt;/code&gt;为&lt;code&gt;0&lt;/code&gt;。然后逐层弹出。&lt;/p&gt;
&lt;p&gt;所以我们发现，一共有四个&lt;code&gt;my_tuple&lt;/code&gt;对象被构造了。因为我们的&lt;code&gt;others&lt;/code&gt;成员变量本身就是一个&lt;code&gt;my_tuple&lt;/code&gt;对象。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;10, &apos;a&apos;, 2.3, 3.4&lt;/code&gt; 相当于&lt;/p&gt;
&lt;p&gt;&lt;code&gt;my_tuple(10, my_tuple(&apos;a&apos;, my_tuple(2.3, my_tuple(3.4, my_tuple&amp;lt;&amp;gt;))))&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230218045353.png&quot; alt=&quot;QQ截图20230218045353&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;然后看一下&lt;code&gt;get&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个&lt;code&gt;get&lt;/code&gt;和&lt;code&gt;typelist&lt;/code&gt;的&lt;code&gt;get&lt;/code&gt;如出一辙。都是通过&lt;code&gt;index&lt;/code&gt;逐次&lt;code&gt;-1&lt;/code&gt;然后去掉头部元素（这里是通过&lt;code&gt;gettail&lt;/code&gt;保留尾部）实现的&lt;/p&gt;
&lt;p&gt;我们从打印中能看出来。&lt;code&gt;index&lt;/code&gt;每次减掉&lt;code&gt;1&lt;/code&gt;，然后&lt;code&gt;getTail&lt;/code&gt;被调用。&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;index&lt;/code&gt;为&lt;code&gt;0&lt;/code&gt;的时候匹配至特化，这时候调用&lt;code&gt;getHead&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;剩下的还是比较多的，看书吧。&lt;/h2&gt;
&lt;p&gt;https://blog.csdn.net/baidu_41388533/article/details/109818986&lt;/p&gt;
&lt;h1&gt;std::is_same&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;is_same的主要作用是判断两个类型是否相同。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class T, class U &amp;gt;
struct is_same;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果T与U是考虑&lt;code&gt;const-volatile&lt;/code&gt;限定的相同类型，则&lt;code&gt;is_same&amp;lt;T,U&amp;gt;::value&lt;/code&gt;为true，否则为false。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    cout &amp;lt;&amp;lt; is_same&amp;lt;int, int&amp;gt;::value &amp;lt;&amp;lt; endl; //输出1 (true)
    cout &amp;lt;&amp;lt; is_same&amp;lt;int, string&amp;gt;::value &amp;lt;&amp;lt; endl; //输出0 (false)
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;可能的实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp,_Tp _v&amp;gt;
struct intergral_constant{
    static constexpr _Tp value = _v; //表示值
    typedef _Tp value_type; //值类型
    typedef intergral_constant&amp;lt;_Tp,_v&amp;gt; type; //表示自己的类型

    constexpr operator value_type () const noexcept {return value;} 
    constexpr value_type operator()() const noexcept {return value;} //since c++14
};
//定义true_type 和 false_type
typedef intergral_constant&amp;lt;bool, true&amp;gt; true_type;
typedef intergral_constant&amp;lt;bool, false&amp;gt; false_type;

//is_same的实现
template &amp;lt;typename T1,typename T2&amp;gt;
struct is_same: public false_type {};
//类型相同就相当于一个模板参数
template &amp;lt;typename _Tp&amp;gt;
struct is_same&amp;lt;_Tp,_Tp&amp;gt;: public true_type {};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首先定义了一个类模板&lt;code&gt;is_same&lt;/code&gt;，这个类模板有两个模板参数&lt;code&gt;T&lt;/code&gt;和&lt;code&gt;U&lt;/code&gt;。接着针对这个类模板&lt;code&gt;T&lt;/code&gt;和&lt;code&gt;U&lt;/code&gt;类型相同的情况进行偏特化。所以，当&lt;code&gt;T&lt;/code&gt;和&lt;code&gt;U&lt;/code&gt;为同一种类型时，将匹配到&lt;code&gt;1&lt;/code&gt;,不同则匹配到&lt;code&gt;0&lt;/code&gt;。两个版本唯一的不同是父类。&lt;/li&gt;
&lt;li&gt;然后是判断两个类型，如果两个类的类型不同，那么应该是继承自&lt;code&gt;false_type&lt;/code&gt;，如果类型相同的话应该继承自&lt;code&gt;true_type&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;自己模拟一下简单实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class MyClass {
    public:
    //主模板 如果两个类型不一则会匹配到这里
    MyClass(T1 a, T2 b):val1(a), val2(b){
        cout &amp;lt;&amp;lt;&quot;diff type&quot; &amp;lt;&amp;lt; endl;
    }
    T1 val1;
    T2 val2;
};

template&amp;lt;typename T&amp;gt;
class MyClass&amp;lt;T,T&amp;gt; {
    public:
    //同一类型的偏特化模板。如果两个类型一致则会匹配到这里
    MyClass(T a, T b):val1(a), val2(b){
        cout &amp;lt;&amp;lt; &quot;same type&quot; &amp;lt;&amp;lt; endl;
    }
    T val1;
    T val2;
};
int main(){
    MyClass&amp;lt;int, int&amp;gt; myobj1(19,22); //输出 same type
    MyClass&amp;lt;int, string&amp;gt; myobj2(11, &quot;abcde&quot;); //输出 diff type
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;std::is_convertible&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;is_convertible&lt;/code&gt;用于判断两个类型之间是否可以隐式转换：第一个模板参数的类型是否可以&lt;strong&gt;隐式转换&lt;/strong&gt;为第二个模板参数的类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class From, class To &amp;gt;
struct is_convertible;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，如果满足下列条件，则 &lt;code&gt;std::is_convertible&amp;lt;From, To&amp;gt;::value&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，否则为 &lt;code&gt;false&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;From&lt;/code&gt; 和 &lt;code&gt;To&lt;/code&gt; 均为 &lt;code&gt;void&lt;/code&gt; 类型（可含有 &lt;code&gt;cv&lt;/code&gt; 限定）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::declval&amp;lt;From&amp;gt;()&lt;/code&gt; 可隐式转换为 &lt;code&gt;To&lt;/code&gt; 类型&lt;/li&gt;
&lt;li&gt;例子1：如果&lt;code&gt;From&lt;/code&gt;，&lt;code&gt;to&lt;/code&gt;都是基本类型，那么可以隐式转换。&lt;/li&gt;
&lt;li&gt;例子2：如果&lt;code&gt;From&lt;/code&gt;是子类，&lt;code&gt;To&lt;/code&gt;是基类，那么&lt;code&gt;From&lt;/code&gt;可以转换为&lt;code&gt;To&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;核心就是查看两个类之间是否有一个转换序列可以满足类型转换。&lt;/li&gt;
&lt;li&gt;使用例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct A { };
struct B : A { };

int main() {
    cout &amp;lt;&amp;lt; std::boolalpha; /* 输出流将bool输出true/false, 而不是1/0 */
    cout &amp;lt;&amp;lt; &quot;is_convertible:&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;int =&amp;gt; float: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;int,float&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;int = &amp;gt;const int: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;int,const int&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;A =&amp;gt; B: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;A,B&amp;gt;::value &amp;lt;&amp;lt; endl; /* A不能转换为B */
    cout &amp;lt;&amp;lt; &quot;B =&amp;gt; A: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;B,A&amp;gt;::value &amp;lt;&amp;lt; endl; /* B能转换为A */
    return 0;
}
/*
is_convertible:
int =&amp;gt; float: true
int = &amp;gt;const int: true
A =&amp;gt; B: false
B =&amp;gt; A: true
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;进阶例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj2{
    public:
        int val;
        myobj2() = default;
        myobj2(int a):val(a){}; //注意这里是没有explicit
        operator int(){ //注意这里加了用户定义转换函数
            return val;
        }

};

int main(){
    cout &amp;lt;&amp;lt; &quot;myobj2 =&amp;gt; int: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;myobj2, int&amp;gt;::value &amp;lt;&amp;lt; endl; //true myobj2可以隐式转换为int
    cout &amp;lt;&amp;lt; &quot;int =&amp;gt; myobj2: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;int, myobj2&amp;gt;::value &amp;lt;&amp;lt; endl; //true int可以隐式转换为myobj2。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我们加了用户定义转换函数，所以可以从&lt;code&gt;myobj2&lt;/code&gt;隐式转为&lt;code&gt;int&lt;/code&gt;。我们也有转换构造函数，所以也可以从&lt;code&gt;int&lt;/code&gt;隐式转换为&lt;code&gt;myobj2&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果去掉类型转换函数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj2{
    public:
        int val;
        myobj2() = default;
        myobj2(int a):val(a){}; //注意这里是没有explicit
};

int main(){
    cout &amp;lt;&amp;lt; &quot;myobj2 =&amp;gt; int: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;myobj2, int&amp;gt;::value &amp;lt;&amp;lt; endl; //false myobj2不可以隐式转换为int
    cout &amp;lt;&amp;lt; &quot;int =&amp;gt; myobj2: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;int, myobj2&amp;gt;::value &amp;lt;&amp;lt; endl; //true int可以隐式转换为myobj2。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果给构造函数添加explicit，禁止隐式类型转换：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj2{
    public:
        int val;
        myobj2() = default;
        explicit myobj2(int a):val(a){}; //有explicit
};

int main(){
    cout &amp;lt;&amp;lt; &quot;myobj2 =&amp;gt; int: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;myobj2, int&amp;gt;::value &amp;lt;&amp;lt; endl; //false myobj2不可以隐式转换为int
    cout &amp;lt;&amp;lt; &quot;int =&amp;gt; myobj2: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;int, myobj2&amp;gt;::value &amp;lt;&amp;lt; endl; //false int不可以隐式转换为myobj2。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细：https://www.cnblogs.com/fortunely/p/16216310.html&lt;/p&gt;
&lt;h2&gt;关于判断对象类型还是指针类型之间是否可以转换的坑在智能指针的MEC++28条部分&lt;/h2&gt;
&lt;h1&gt;std::is_base_of&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class Base, class Derived &amp;gt;
struct is_base_of;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;若 Derived 派生自 Base 或为同一非联合类（均忽略 cv 限定），则提供等于 true 的成员常量 value 。否则 value 为 false 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实没啥好说的，就是看&lt;code&gt;Derived&lt;/code&gt;是不是派生自&lt;code&gt;Base&lt;/code&gt;。&lt;strong&gt;但是这个和上面的&lt;code&gt;std::is_convertible&lt;/code&gt;有啥区别？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;还是有区别的。因为&lt;code&gt;std::is_convertible&lt;/code&gt;核心是查看能否隐式转换。当然了，在继承链条上大部分情况都可以。但是还是有例外，比如私有继承的时候就不能转。所以说&lt;code&gt;std::is_base_of&lt;/code&gt;做的就是一件事，查看是否是派生关系。然而&lt;code&gt;std::is_convertible&lt;/code&gt;做的事情就更多了。&lt;/p&gt;
&lt;h1&gt;std::is_constructible&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T, class... Args&amp;gt;
struct is_constructible;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果通过使用 &lt;code&gt;Args&lt;/code&gt; 中的参数类型可构造类型 &lt;code&gt;T&lt;/code&gt;，则类型谓词的实例保持 &lt;code&gt;true&lt;/code&gt;；否则保持&lt;code&gt; false&lt;/code&gt;。 如果变量定义 &lt;code&gt;T t(std::declval&amp;lt;Args&amp;gt;()...);&lt;/code&gt; 的格式正确，则可以构造类型 &lt;code&gt;T&lt;/code&gt;。 &lt;code&gt;T &lt;/code&gt;和&lt;code&gt; Args&lt;/code&gt; 中的所有类型都必须是完整的类型、&lt;code&gt;void&lt;/code&gt;，或者是未知边界的数组。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这一大堆官方文档其实就是意思是看&lt;code&gt;Args&lt;/code&gt;的这些参数能不能构造&lt;code&gt;T&lt;/code&gt;类型对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上面的&lt;code&gt;is_convertable&lt;/code&gt;是来判断能否隐式转换。而这里可以被用来测试是否可以显式转换。杂记1中我们提到过：&lt;strong&gt;显式地调用构造函数进行直接初始化实际上是显式类型转换的一种。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
    int val;
    char single;
    string name;
    myobj(){}
    myobj(int a):val(a){};
    myobj(int a, char b, string&amp;amp; c): val(a), single(b), name(move(c)){};

};
int main(){
    cout &amp;lt;&amp;lt; is_constructible&amp;lt;myobj, int&amp;gt;::value &amp;lt;&amp;lt; endl; //trus
    cout &amp;lt;&amp;lt; is_constructible&amp;lt;myobj, int, char, string&amp;gt;::value &amp;lt;&amp;lt; endl; //false
    cout &amp;lt;&amp;lt; is_constructible&amp;lt;myobj, int, char, string&amp;amp;&amp;gt;::value &amp;lt;&amp;lt; endl; //true
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为啥第二个传值是&lt;code&gt;false&lt;/code&gt;？我们不是可以如这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string name = &quot;abc&quot;;
myobj a(12,&apos;a&apos;,name);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意了，这里的&lt;code&gt;name&lt;/code&gt;虽然是值，但是由于函数签名是&lt;code&gt;string&amp;amp;&lt;/code&gt;，所以&lt;code&gt;name&lt;/code&gt;这个参数在我们传递到函数内的时候，函数内会创建一个&lt;code&gt;name&lt;/code&gt;的引用，叫做&lt;code&gt;c&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这里可以理解为函数签名。你想把一个函数&lt;code&gt;void function(int&amp;amp; x)&lt;/code&gt;塞进这样的函数指针&lt;code&gt; void(*ptr)(int)&lt;/code&gt;是错的。引用和值在签名处不可混为一谈。&lt;/li&gt;
&lt;li&gt;同时，针对这个构造函数，如果我们尝试这样做：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myobj(1, &apos;c&apos;, std::string{&quot;lalala&quot;});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候就会失败。因为左值引用不能接右值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所以在这里，函数的形参类型是&lt;code&gt;string&lt;/code&gt;还是&lt;code&gt;string&amp;amp;&lt;/code&gt;有很大差别。当然了，这里如果给构造函数换成&lt;code&gt;const string&amp;amp; &lt;/code&gt;就都是&lt;code&gt;true&lt;/code&gt;了。或者是换成&lt;code&gt;string&lt;/code&gt;值传递也都是&lt;code&gt;true&lt;/code&gt;。因为常量左值引用可以接一切。&lt;strong&gt;值传递的函数塞引用也没问题。因为引用会被脱去。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进阶例子：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面我们提到了，我们给转换构造函数加了&lt;code&gt;explicit&lt;/code&gt;,所以不能隐式转换。所以&lt;code&gt;is_convertible&amp;lt;int, myobj2&amp;gt;::value&lt;/code&gt;是&lt;code&gt;false&lt;/code&gt;。我们可以尝试用这里的&lt;code&gt;is_constructible&lt;/code&gt;来看是否可以显示转换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj2{
    public:
        int val;
        myobj2() = default;
        explicit myobj2(int a):val(a){}; //依旧explicit

};
int main(){
    cout &amp;lt;&amp;lt; &quot;is_constructable:&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;int =&amp;gt; myobj2: &quot; &amp;lt;&amp;lt; is_convertible&amp;lt;int, myobj2&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;is_convertible:&quot; &amp;lt;&amp;lt; std::endl;
    cout &amp;lt;&amp;lt; &quot;int =&amp;gt; myobj2: &quot; &amp;lt;&amp;lt; is_constructible&amp;lt;myobj2,int&amp;gt;::value &amp;lt;&amp;lt; endl;
    return 0;
}
/*
is_constructable:
int =&amp;gt; myobj2: false
is_convertible:
int =&amp;gt; myobj2: true
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到依旧不能隐式转换所以第一个是&lt;code&gt;false&lt;/code&gt;。但是我们可以使用int来构造&lt;code&gt;myobj2 &lt;/code&gt;所以第二个是&lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;注意一下参数顺序。能不能转换是第一个转到第二个。能不能构造是第二个构造第一个。&lt;/p&gt;
</content:encoded></item><item><title>C++ 模板笔记（四）：深入模板基础、模板中的名称、实参推导与萃取</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-4/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-4/</guid><pubDate>Wed, 14 Dec 2022 14:53:00 GMT</pubDate><content:encoded>&lt;h1&gt;第十二章 深入模板基础&lt;/h1&gt;
&lt;h2&gt;12.1 参数化声明&lt;/h2&gt;
&lt;p&gt;C++有四种基础模板。他们既可以出现在命名空间作用域，也可以出现在类作用域。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类模板
&lt;ul&gt;
&lt;li&gt;嵌套类模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数模板
&lt;ul&gt;
&lt;li&gt;成员函数模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;变量模板
&lt;ul&gt;
&lt;li&gt;静态数据成员模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;别名模板（就是using的那个）
&lt;ul&gt;
&lt;li&gt;成员别名模板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此处举例说明如何类外定义类模板的成员函数模板。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class myclass{
    public:
        template&amp;lt;typename U&amp;gt;
        void func(T a, U b){ //类内
            cout &amp;lt;&amp;lt;&quot;in&quot; &amp;lt;&amp;lt; endl;
            cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
        }
        template&amp;lt;typename U&amp;gt;
        void out_class_func(T a, U b);
};
//类外
template&amp;lt;typename T&amp;gt;
template&amp;lt;typename U&amp;gt;
void myclass&amp;lt;T&amp;gt;::out_class_func(T a, U b){
    cout &amp;lt;&amp;lt;&quot;out&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义在类外的成员模板需要多个&lt;code&gt;template&amp;lt;... &amp;gt;&lt;/code&gt;参数化子句：每个外围作用域的类模板一个，成员模板本身也需要一个。子句从类模板最外层开始逐行展示。&lt;/p&gt;
&lt;h3&gt;12.1.1 虚成员函数&lt;/h3&gt;
&lt;p&gt;在尾部我们已经说明了为什么成员函数不能既是虚函数又是函数模板。&lt;strong&gt;额外注意一点，类模板的普通成员函数可以是虚函数。因为它们的数量是固定的&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;12.1.3 主模板&lt;/h3&gt;
&lt;p&gt;类模板和函数模板都必须要有一个主模板&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;模板的一般性声明声明了主模板(primary templates)。如此声明的模板在模板名后无需书写尖括号模板参数子句。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;非主模板会在声明类模板或变量模板的偏特化时出现。函数模板始终必须是主模板（因为函数模板没有偏特化）&lt;/p&gt;
&lt;h2&gt;12.2 模板参数(Template Parameters)&lt;/h2&gt;
&lt;p&gt;我们之前提到过模板参数主要有三种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型模板参数&lt;/li&gt;
&lt;li&gt;非类型模板参数 (3.1~3.2)&lt;/li&gt;
&lt;li&gt;模板模板参数（5.7)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些基本类型的模板参数中的任何一种都可以用作模板参数包的类型。但不可多种类型当做同一个参数包（4.1）&lt;/p&gt;
&lt;p&gt;不一定所有的模板参数都需要名称。如果没有用到，则无需命名。同时需要注意的是，模板参数名可以在后续参数声明中引（使）用（但前置则不行）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, //这个T在后面的参数声明中使用了。
		T root,					
		template&amp;lt;T&amp;gt; class Buf&amp;gt;	
class Structure;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;12.2.1 类型模板参数&lt;/h3&gt;
&lt;p&gt;好像没啥可说的吧？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;类型参数由关键字&lt;code&gt;typename&lt;/code&gt;或&lt;code&gt;class&lt;/code&gt;所引导：二者是完全等价的。关键字后必须有一个简单的标识符，并且该标识符后必须带有逗号，以表示下一个参数声明的开始，闭合的尖括号&lt;code&gt;&amp;gt;&lt;/code&gt;用以指示参数化子句的结束，&lt;code&gt;=&lt;/code&gt;用以指示一个默认模板参数的起始。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;12.2.2 非类型模板参数&lt;/h3&gt;
&lt;p&gt;搭配3.1~3.2，此处做补充&lt;/p&gt;
&lt;p&gt;针对非类型模板参数有更多深入的限制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非类型模板参数不可以有非类型指示符，比如&lt;code&gt;static&lt;/code&gt;、&lt;code&gt;mutable&lt;/code&gt;等等&lt;/li&gt;
&lt;li&gt;它们可以有&lt;code&gt;const&lt;/code&gt;和&lt;code&gt;volatile&lt;/code&gt;限定符，但是如果这种限定符出现在参数类型的最顶层，就会被忽略。换句话说，对左值引用或指针来说支持底层&lt;code&gt;const&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在表达式中使用时，非引用类型的非类型参数始终都是&lt;code&gt;prvalue&lt;/code&gt;。它们的地址无法被窃取，也无法被赋值。而另一方面，左值引用类型的非类型参数是可以像左值一样使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, static int MAXSIZE&amp;gt; //例子1，错误。不能有static
void func1(T obj){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;const int length&amp;gt; class Buffer; //二者相同。因为最顶层CV限定会被忽略
template&amp;lt;int length&amp;gt; class Buffer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用引用类型做为非类型模板参数时有诸多陷阱和要求。此处不做过多讲述，下面的两个链接非常有用。&lt;/p&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/template_parameters&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/28662784/reference-as-a-non-type-template-argument&lt;/p&gt;
&lt;h3&gt;12.2.3 模板模板参数&lt;/h3&gt;
&lt;p&gt;参考 5.7&lt;/p&gt;
&lt;h3&gt;12.2.4 模板参数包&lt;/h3&gt;
&lt;p&gt;参考第四章。此处做为补充&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主模板&lt;/strong&gt;中的类模板、变量模板和别名模板&lt;strong&gt;至多只可以有一个模板参数包，且模板参数包必须作为最后一个模板参数&lt;/strong&gt;。函数模板则少些限制：允许多个模板参数包，&lt;strong&gt;只要模板参数包后面的每个模板参数都具有默认值或可以推导&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类和变量模板的偏特化声明可以有多个参数包&lt;/strong&gt;，这与主模板不同。这是因为偏特化是通过与函数模板几乎相同的推导过程所选择的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;12.2.5 模板的默认实参&lt;/h3&gt;
&lt;p&gt;参考 1.4 2.7。此处作为补充&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模板参数包不能有默认实参。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他的模板参数可以有默认实参。但是它&lt;strong&gt;必须和相应的参数类型匹配&lt;/strong&gt;。比如类型参数不能有一个非类型默认实参&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认实参&lt;strong&gt;不能依赖于其自身的参数&lt;/strong&gt;，因为参数的名称直到默认实参之后才在作用域内生效。然而，他&lt;strong&gt;可以依赖前面的参数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename Allocator = allocator&amp;lt;T&amp;gt;&amp;gt; //OK
class List;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类模板、变量模板或别名模板的模板参数遵照函数（不是函数模板）的默认参数规则：即从有默认实参的参数开始，后面的每一个参数都必须有默认实参。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是的你没看错，可以重复声明。但是只能有一个定义（ODR原则）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2, typename T3, typename T4, typename T5&amp;gt; //可以，每一个都没有默认实参
class myclass;
template&amp;lt;typename T1, typename T2, typename T3, typename T4 = int, typename T5 = int&amp;gt; //可以，从T4开始每一个都有默认实参
class myclass;
template&amp;lt;typename T1, typename T2, typename T3 = int, typename T4, typename T5&amp;gt; //可以，由于T4和T5在上面的声明中已有默认实参，所以在这里是T3后的都有默认实参
class myclass{
    public: //只有一个定义
        void func(){
            std::puts(__PRETTY_FUNCTION__);
        }
};
//----以下是错误例子
template&amp;lt;typename T1 = char, typename T2, typename T3, typename T4, typename T5&amp;gt; //错误。T1后的参数都没有默认实参
class myclass;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;而函数模板的模板参数的默认模板实参则不受此限制。即不需要后续的模板参数必须都有默认模板实参：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;锐评：非常智能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1 = int, typename T2, typename T3 = float, typename T4 = char, typename T5&amp;gt;
void func(T1, T2, T3, T4, T5){
    std::puts(__PRETTY_FUNCTION__);
}
/*
void func(T1, T2, T3, T4, T5) [with T1 = int; T2 = int; T3 = float; T4 = char; T5 = char]
*/

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;许多上下文不允许使用默认模板实参&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;偏特化（第二章说过）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模板参数包（刚说过）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类模板成员类外定义（不太常见）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;友元类模板声明（更不太常见）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;友元函数模板声明。除非它是定义并且在编译单元的其他任何地方都没有声明（啥东西）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;12.3 模板实参(Template Arguments)&lt;/h2&gt;
&lt;h1&gt;12章至16章非常精彩，尤其是在原理分析方面。但是内容过于庞多繁杂，涉及到大量的语法和规范内容。此处暂时略过。从19章起将会讲解实践性的内容&lt;/h1&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h1&gt;第十三章 模板中的名称&lt;/h1&gt;
&lt;h2&gt;13.4 派生和类模板&lt;/h2&gt;
&lt;p&gt;类模板可以继承或被继承，模板和非模板场景之间没有显著的区别。然而，当从依赖名称引用的基类派生类模板时，有一个微妙区别。让我们首先看看非依赖型基类的情况。&lt;/p&gt;
&lt;h3&gt;13.4.1 非依赖型基类&lt;/h3&gt;
&lt;p&gt;类模板中，非依赖型基类是具有完整类型的类，可以在不知道模板参数的情况下即可确定的基类。换句话说，这个基类的名称是使用非依赖型名称表示的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class Base{
    public:
        int baseval;
        using Type = int;
};


class D1:public Base&amp;lt;double&amp;gt;{ //非依赖名基类。
    public:
        void func(){
            baseval = 200;
        }
        Type D1val = 300;
};


int main(){
    D1 obj;
    obj.func();
    cout &amp;lt;&amp;lt; obj.baseval &amp;lt;&amp;lt; obj.D1val &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;模板中的非依赖型基类类似于普通非模板类中的基类，但当在模板派生中查找非限定名称时，非依赖型基类会优先考虑该名称，而后才是模板参数列表。类模板 &lt;code&gt;D1&lt;/code&gt; 的成员&lt;code&gt; D1val&lt;/code&gt; 总是对应&lt;code&gt;Base&amp;lt;double&amp;gt;::Type&lt;/code&gt;(就是 &lt;code&gt;int&lt;/code&gt;) 的类型&lt;code&gt; Type&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;13.4.2 依赖型基类&lt;/h3&gt;
&lt;p&gt;前面的例子中，基类完全确定，不依赖于模板参数。只要知道模板定义，C++ 编译器就可以在这些基类中查找非依赖性名称。&lt;/p&gt;
&lt;p&gt;针对这一部分看笔记深度探索c++对象模型的7.1&lt;/p&gt;
&lt;h1&gt;第十五章 模板实参推导&lt;/h1&gt;
&lt;h2&gt;15.1 推导过程&lt;/h2&gt;
&lt;p&gt;基本的推导过程会比较**“函数调用的实参类型”&lt;strong&gt;与&lt;/strong&gt;“函数模板对应位置的参数化类型”**，然后针对要被推导的一到多个参数，分别尝试去推断一个正确的替换项。每个实参-参数对都会独立分析，并且如果最终得出的结论产生矛盾(differ)，那么推导过程就失败了。&lt;/p&gt;
&lt;p&gt;这里有一个非常简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T max(T a, T b){
  return b &amp;lt; a ? a : b;
}
auto g = max(1, 1.0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里第一个调用实参的类型是&lt;code&gt;int&lt;/code&gt;，因此我们的&lt;code&gt;max()&lt;/code&gt;模板的参数&lt;code&gt;T&lt;/code&gt;会被推导成&lt;code&gt;int&lt;/code&gt;。然而，第二个调用实参是&lt;code&gt;double&lt;/code&gt;类型，基于此，&lt;code&gt;T&lt;/code&gt;会被推导为&lt;code&gt;double&lt;/code&gt;，但是这就与前一个推导产生了矛盾。&lt;strong&gt;注意：我们称之为“推导过程失败”，而不是“程序非法”。毕竟，可能对于另一个名为&lt;code&gt;max&lt;/code&gt;的模板来说推导过程可能是成功的。也就是SFINAE&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;即使所有被推导的模板实参都可以一致地确定（即不产生矛盾），推导过程仍然可能会失败。这种情况发生于：在函数声明中，进行替换的模板实参可能会导致无效的结构 比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
typename T::ElementT at(T a, int i){
  return a[i];
}
void f(int* p){
  int x = at(p, 7);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里&lt;code&gt;T&lt;/code&gt;被推导为&lt;code&gt;int*&lt;/code&gt;(&lt;code&gt;T&lt;/code&gt;出现的地方只有一种参数类型，因此显然不会有解析矛盾)。然而，将&lt;code&gt;T&lt;/code&gt;替换为&lt;code&gt;int*&lt;/code&gt;在C++中对于返回类型&lt;code&gt;T::ElementT&lt;/code&gt;显然是非法的，因此推导就失败了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;推导规则在1.2部分讲过了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;15.2 推导语境（上下文）&lt;/h2&gt;
&lt;p&gt;这一章是核心，：也就是&lt;strong&gt;比&lt;code&gt;T&lt;/code&gt;复杂得多的参数化类型也可以匹配一个给定的实参类型。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void f1(T*);

template&amp;lt;typename E, int N&amp;gt;
void f2(E(&amp;amp;)[N]);

template&amp;lt;typename T1, typename T2, typename T3&amp;gt;
void f3(T1 (T2::*)(T3*));

class S {
    public:
    void f(double*);
};

template&amp;lt;typename... Args&amp;gt;
void f4(Args... callable){
    std::puts(__PRETTY_FUNCTION__);

}
template&amp;lt;typename... Args&amp;gt;
void f5(std::function&amp;lt;void(Args...)&amp;gt; callable){
    std::puts(__PRETTY_FUNCTION__);
}
void callablefunc(int a, int b, int c){
    cout &amp;lt;&amp;lt;&quot;callablefunc&quot; &amp;lt;&amp;lt; endl;
}; 
template&amp;lt;typename T&amp;gt;
void f6(T callable){
    std::puts(__PRETTY_FUNCTION__);
    
}
template&amp;lt;typename T&amp;gt;
void f7(vector&amp;lt;T&amp;gt; callable){
    std::puts(__PRETTY_FUNCTION__);
    
}
void g(int*** ppp){
    f1(ppp);		// 形参T的类型被推导为int**
    
    bool b2[42];
    f2(b);		// 形参E的类型被推导为bool，形参N的类型被推导为42
    
    f3(&amp;amp;S::f);	// 形参T1的类型被推导为void, 形参T2的类型被推导为S, 形参T3的类型被推导为double
    
    std::function&amp;lt;void(int, int, int)&amp;gt; myfunc(callablefunc);
    f4(myfunc);  //形参Args...的类型被推导为{int, int, int}
    f5(myfunc); //形参Args...的类型被推导为std::function&amp;lt;void(int, int, int)&amp;gt;

    vector&amp;lt;int&amp;gt; a{1,2,3};
    f6(a); //形参T的类型被推导为std::vector&amp;lt;int&amp;gt;
    f7(a); //形参T的类型被推导为int
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;复杂的类型声明都是用比它更基本的结构（例如指针、引用、数组、函数声明；成员指针声明；模板ID等）来组成的，匹配过程从最顶层结构开始处理，一路递归其各种组成元素。可以说基于这一方法，大部分类型声明结构都可以进行匹配，而这些结构也被称为“推导语境“。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;然而，有些结构不能作为推导语境。查看0.6，搜索“语境”或直接查看&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/template_argument_deduction&quot;&gt;文档&lt;/a&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第十九章 萃取的实现&lt;/h1&gt;
&lt;h2&gt;19.1.1 实现简单的固定萃取&lt;/h2&gt;
&lt;p&gt;假设我们需要对一个原始数组求和。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class calc_accumu_old{
    public:
        T ret_val{}; //使用值初始化，确保每一个类型都有合适的初始化值。
        auto getsum(const T* start, const T* end){
            while(start != end){
                ret_val += *start;
                ++start;
            }
            cout &amp;lt;&amp;lt; ret_val &amp;lt;&amp;lt; endl;
            return ret_val;
        }
};

int main(){
    int num[] = {1,2,3,4,5};
    char str[] = &quot;abcdefg&quot;;
    calc_accumu_old&amp;lt;int&amp;gt; calc;
    calc.getsum(num, num+5);//正常
    calc_accumu_old&amp;lt;char&amp;gt; calc_char; 
    calc_char.getsum(str, str+7);//错误了
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;表面上万事大吉，但是遇到&lt;code&gt;char&lt;/code&gt;却错误了。为什么？因为&lt;code&gt;char&lt;/code&gt;按照ASCII编码后相加，明显超出了&lt;code&gt;char&lt;/code&gt;的数据范围，就发生了溢出，这时候怎么办？&lt;/li&gt;
&lt;li&gt;我们确实可以引入额外参数，或者针对整个类进行特化，但是非常不便。我们尝试一下萃取&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct retvalTypeTraits{ //主模板
    using retType = T; //类型定义
};

template&amp;lt;&amp;gt;
struct retvalTypeTraits&amp;lt;char&amp;gt;{ //针对char类型特化
    using retType = int; //类型定义
};
template&amp;lt;&amp;gt;
struct retvalTypeTraits&amp;lt;int&amp;gt;{ //针对int类型特化
    using retType = unsigned int; //类型定义
};

template&amp;lt;typename T&amp;gt;
class calc_accumu{
    public:
        using Ret = typename retvalTypeTraits&amp;lt;T&amp;gt;::retType; //为了方便起见使用别名模板。
        Ret ret_val{}; //使用值初始化，确保每一个类型都有合适的初始化值。此处有问题，如果Ret类型没有默认构造，就不保证能够正确初始化。
        auto getsum(const T* start, const T* end){
            while(start != end){
                ret_val += *start;
                ++start;
            }
            cout &amp;lt;&amp;lt; ret_val &amp;lt;&amp;lt; endl;
            return ret_val;
        }
};

int main(){
    int num[] = {1,2,3,4,5};
    char str[] = &quot;abcdefg&quot;;
    calc_accumu&amp;lt;int&amp;gt; calc;
    calc.getsum(num, num+5);
    calc_accumu&amp;lt;char&amp;gt; calc_char;
    calc_char.getsum(str, str+7);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这样就解决了问题。我们成功应用了萃取。&lt;code&gt;retvalTypeTraits&lt;/code&gt;就是我们提到的元函数。返回类型的元函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;19.1.2 值萃取&lt;/h2&gt;
&lt;p&gt;我们在上面的例子中提到，如果值初始化的类对象没有默认构造，就不能保证其被正确初始化。那么这时候我们可能需要额外新增一种萃取。也就是值萃取&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct retvalTypeTraits{
    using retType = T; //类型定义
    static const retType zero = 0; //版本1
};

template&amp;lt;typename T&amp;gt;
struct retvalTypeTraits{
    using retType = T; //类型定义
    static constexpr retType zero = 0; //版本2
};

template&amp;lt;&amp;gt;
struct retvalTypeTraits&amp;lt;char&amp;gt;{ //某种特化
    using retType = int; //类型定义
    static const retType zero;//版本3 类内仅声明
};
const int retvalTypeTraits&amp;lt;char&amp;gt;::zero = 0;

template&amp;lt;typename T&amp;gt;
struct retvalTypeTraits{
    using retType = T; //类型定义
    inline static const retType zero = 0; //版本4 C++17
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;版本1：使用&lt;code&gt;static const&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;限制：只能使用整型或枚举类型进行类内初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;版本2：使用&lt;code&gt;static constexpr&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：可以使用浮点类型以及其他字面值类型进行类内初始化&lt;/li&gt;
&lt;li&gt;缺点：自定义类型可能不是字面值类型，比如其构造函数并不是&lt;code&gt;constexpr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;版本3：类内定义类外声明&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缺点：比较麻烦&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;版本4：使用&lt;code&gt;inline&lt;/code&gt; [c++17]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;C++17 后 静态数据成员可以声明为 &lt;code&gt;inline&lt;/code&gt;。 &lt;code&gt;inline&lt;/code&gt; 静态数据成员可以在类定义中定义，而且可以指定初始化器。它不需要类外定义&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这种就算是返回值的元函数。又因为我们既有值萃取又有类型萃取，这种可以被称为多返回值的元函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;19.1.3 参数化萃取&lt;/h2&gt;
&lt;p&gt;我们在上面的基础例子中，发现一个问题。也就是我们的返回值类型是和萃取类绑定到一起的。而且萃取类是不能更改的。始终为&lt;code&gt;retvalTypeTraits&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这样的话，如果用户想要增添更多的特化，就必须找到&lt;code&gt;retvalTypeTraits&lt;/code&gt;的文件然后添加更多的特化，比较丑陋。为了解决这一问题，可以为萃取引入一个新的模板参数 &lt;code&gt;PT&lt;/code&gt;，其默认值就是我们萃取模板的对应值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct retvalTypeTraits{ //主模板
    using retType = T; //类型定义
    static const retType zero = 0;
};

template&amp;lt;&amp;gt;
struct retvalTypeTraits&amp;lt;char&amp;gt;{ //针对char类型特化
    using retType = int; //类型定义
    static const retType zero = 0;
};

struct my_customize_trait{ //用户定义的萃取模板
    using retType = unsigned int; //类型定义
    static const retType zero = 1000;
};


template&amp;lt;typename T, typename PT = retvalTypeTraits&amp;lt;T&amp;gt;&amp;gt; //第二个参数拥有默认值，默认值就是默认萃取模板
class calc_accumu{
    public:
        using Ret = typename PT::retType; //为了方便起见使用别名模板。
        Ret ret_val = PT::zero;
        auto getsum(const T* start, const T* end){
            while(start != end){
                ret_val += *start;
                ++start;
            }
            cout &amp;lt;&amp;lt; ret_val &amp;lt;&amp;lt; endl;
            return ret_val;
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PT&lt;/code&gt;拥有默认值，默认值就是默认萃取模板。这样的好处是用于可以完全不关心第二个模板参数。而如果确有需求，可以放入自己的萃取模板。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;19.2 萃取和策略（policy）&lt;/h2&gt;
&lt;p&gt;我们有没有发现上面的代码表面是用到了模板，但其实也就仅仅是用到了模板而已。我们的&lt;code&gt;getsum&lt;/code&gt;对任意类型的&lt;code&gt;T&lt;/code&gt;都有统一的计算方式。那么问题来了，如果我想要针对某种场景进行特化呢？比如某个指针是自定义的智能指针或迭代器，且没有间接访问运算符呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这时候我们很容易想到一点：给&lt;code&gt;getsum&lt;/code&gt;也换成可变的。这种行为我们就可以叫做&lt;code&gt;policy&lt;/code&gt;。策略。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们看一下书里的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename Policy = SumPolicy, typename Traits = AccumulationTraits&amp;lt;T&amp;gt;&amp;gt;
auto accum (T const* beg, T const* end){
    using AccT = typename Traits::AccT;
    AccT total = Traits::zero();
    while (beg != end) {
    	Policy::accumulate(total, *beg);
    	++beg;
    }
    return total;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在这一版的 &lt;code&gt;accum()&lt;/code&gt;中，&lt;code&gt;SumPolicy &lt;/code&gt;是一个策略类，也就是一个通过预先商定好的接口（&lt;code&gt;accumulate&lt;/code&gt;），为算法实现了一个或多个策略的类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果提供一个不同的策略对数值进行累积的话，我们可以计算完全不同的事情。比如我们完全可以有一个&lt;code&gt;multpolicy&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class SumPolicy { //加法
    public:
    	template&amp;lt;typename T1, typename T2&amp;gt;
    	static void accumulate (T1&amp;amp; total, T2 const&amp;amp; value) {
    		total += value;
    	}
};
class MultPolicy { //乘法
    public:
    	template&amp;lt;typename T1, typename T2&amp;gt;
    	static void accumulate (T1&amp;amp; total, T2 const&amp;amp; value) {
    		total *= value;
    	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;但是此时注意，还记得我们把初始值设为0了吗？此时对于乘法就就会出现问题。因为乘以0永远是0。这就说明了需要仔细设计到底哪些是萃取，哪些是策略。同时，应该记住不是所有的东西都要用萃取和策略。比如标准库的&lt;code&gt;accumulate&lt;/code&gt;就把初始值作为了第三个参数。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.2.1 萃取和策略的区别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;策略更侧重于行为，而萃取更侧重于类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;策略代表的是泛型函数和类型（通常都有其常用地默认值）的可以配置的行为。萃取代表的是一个模板参数的本质的、额外的属性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;萃取：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;萃取在被当作固定萃取（19.1）的时候会比较有用（比如，当其不是被作为模板参数传递的时候）
&lt;ul&gt;
&lt;li&gt;一般来说，迭代器就是这么设计的。做为固定萃取&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;萃取参数通常都有很直观的默认参数（很少被重写，或者简单的说是不能被重写）。
&lt;ul&gt;
&lt;li&gt;比如有固定的&lt;code&gt;某个萃取类::type&lt;/code&gt;或&lt;code&gt;某个萃取类::value&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;萃取参数倾向于紧密的依赖于一个或者多个主模板参数。
&lt;ul&gt;
&lt;li&gt;比如19.2，&lt;code&gt;Traits&lt;/code&gt;依赖于&lt;code&gt;T&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;萃取在大多数情况下会将类型和常量结合在一起，而不是成员函数。
&lt;ul&gt;
&lt;li&gt;一般萃取都是采取&lt;code&gt;typedef/using&lt;/code&gt;或&lt;code&gt;static&lt;/code&gt;而非成员函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;萃取倾向于被汇集在萃取模板中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;策略类如果不是被作为模板参数传递的话，那么其作用会很微弱。
&lt;ul&gt;
&lt;li&gt;比如19.2，我们需要把策略类传入模板参数来确定具体使用的是哪一个Policy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;策略参数不需要有默认值，它们通常是被显式指定的（虽有有些泛型组件通常会使用默认策略）。&lt;/li&gt;
&lt;li&gt;策略参数通常是和其它模板参数无关的。
&lt;ul&gt;
&lt;li&gt;比如19.2，策略和&lt;code&gt;T&lt;/code&gt;无关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;策略类通常会包含成员函数。
&lt;ul&gt;
&lt;li&gt;一般策略都是采用的成员函数而非萃取的&lt;code&gt;typedef&lt;/code&gt;之类。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;策略可以被包含在简单类或者类模板中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;记住，策略是表达出我们希望依照某种用户提供的规则进行某项操作。所以不一定是像上面的例子那么死板。比如&lt;code&gt;std::accumulate&lt;/code&gt;的第四个参数要求输入一个函数对象。这就是策略。或者是容器类的&lt;code&gt;allocator&lt;/code&gt;分配器，也是策略。再比如&lt;code&gt;unordered_set&lt;/code&gt;的&lt;code&gt;hash&lt;/code&gt;, &lt;code&gt;keyequal&lt;/code&gt;, &lt;code&gt;allocator&lt;/code&gt;都是策略。还有，&lt;code&gt;unique_ptr&lt;/code&gt;的自定义删除器&lt;code&gt;deleter&lt;/code&gt;，也是策略。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.2.2 使用成员函数模板还是模板模板参数？&lt;/h3&gt;
&lt;p&gt;我们可以像书上那样不使用成员函数模板，转而使用模板模板参数。这样就要求了我们需要调整策略类为类模板（原来是普通类+成员函数模板，现在是模板类）。这样做其实不太好，有点不直观而且麻烦许多。&lt;/p&gt;
&lt;h3&gt;19.2.3 多个策略和萃取（说白了就是多个模板参数按照什么顺序写）&lt;/h3&gt;
&lt;p&gt;一般来说，如果这个模板参数越可能使用其默认参数，则越应该靠后。我们可以看到上面的例子中，第一个参数是类型（必须显式指定），第二个是策略类（更有可能替换，但有默认值），第三个是萃取类（不太可能被替换）。&lt;/p&gt;
&lt;h2&gt;19.3 类型函数（Type Function)&lt;/h2&gt;
&lt;p&gt;我们在最下面提到了什么叫元函数。其实就是这里的类型函数。在书中是这样定义的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;值函数（value functions）：它们接收一些值作为参数并返回一个值作为结果。&lt;/p&gt;
&lt;p&gt;类型函数（type functions）：它们接收一些类型作为参数并返回一个类型或者常量作为结果&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sizeof&lt;/code&gt;就是一个经典的内置的类型函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.3.1 元素类型&lt;/h3&gt;
&lt;p&gt;比如此时我们希望实现一个函数打印某个容器的元素类型。这时候可以通过偏特化实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt; //主模板
struct ElementType;

template &amp;lt;typename T&amp;gt;
struct ElementType&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt;{ //针对vector类型的偏特化
    using Type = T;
};

template &amp;lt;typename T&amp;gt;
struct ElementType&amp;lt;list&amp;lt;T&amp;gt;&amp;gt;{ //针对list类型的偏特化
    using Type = T;
};

template &amp;lt;typename T&amp;gt;
struct ElementType&amp;lt;deque&amp;lt;T&amp;gt;&amp;gt;{ //针对deque类型的偏特化
    using Type = T;
};
//.....其他容器类型
template&amp;lt;typename T&amp;gt;
void printType(const T&amp;amp; container){
    std::puts(__PRETTY_FUNCTION__);
    cout &amp;lt;&amp;lt; typeid(typename ElementType&amp;lt;T&amp;gt;::Type).name() &amp;lt;&amp;lt; endl; //打印元素类型
}

int main(){
    vector&amp;lt;int&amp;gt; vec_a;
    vector&amp;lt;bool&amp;gt; vec_b;
    deque&amp;lt;char&amp;gt; deq_a;
    list&amp;lt;double&amp;gt; list_a;
    printType(vec_a);
    printType(vec_b);
    printType(deq_a);
    printType(list_a);

    return 0;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;此时是一个不错的实现方式。当然了，如果我们把容器分类为标准库容器（提供了&lt;code&gt;value_type&lt;/code&gt;的容器）和自定义容器，则可以这样实现：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
struct ElementType{ //针对提供了value_type或没有写入偏特化的类型的 默认实现
    using Type = typename T::value_type;
};

template &amp;lt;&amp;gt;
struct ElementType&amp;lt;myobj&amp;gt;{ //针对myobj类型的特化。
    using Type = int;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意，如果某个类型没有&lt;code&gt;value_type&lt;/code&gt;的同时我们没有针对其类型定义偏特化萃取类模板，则会出现问题。需要特别注意。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们可能会觉得这样写是脱裤子放屁。然而不是。它**允许我们根据容器类型参数化一个模板，但是又不需要提供代表了元素类型和其它特性的参数。**比如下面这个函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename C&amp;gt;
T sumOfElements (C const&amp;amp; c);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;在这个函数中，我们的返回类型是容器的元素类型。如果此时使用的话，我们必须显式指定&lt;code&gt;T&lt;/code&gt;的类型。因为&lt;code&gt;T&lt;/code&gt;的类型无法通过函数参数进行推导。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以，针对这样的情况，我们可以换成这样的模板：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename C&amp;gt;
typename ElementType&amp;lt;C&amp;gt;::Type sumOfElements (C const&amp;amp; c);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候我们就不必显式指定返回类型。因为可以通过萃取类模板进行推导，通过类型函数得到。&lt;/p&gt;
&lt;p&gt;在上面的情况下，&lt;code&gt;ElementType&lt;/code&gt;被称之为萃取类。因为它是用来获取一个已有容器类型的萃取。因此我们发现萃取类的功能并不局限于描述容器参数的特性，而是可以描述任意主参数&lt;code&gt;T&lt;/code&gt;的特性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果想更方便一点，可以使用别名模板。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
using a_ElementType = typename ElementType&amp;lt;T&amp;gt;::Type; //别名模板

template&amp;lt;typename C&amp;gt;
a_ElementType&amp;lt;C&amp;gt; sumOfElements (C const&amp;amp; c){
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;19.3.2 转换萃取（Transformation Traits)&lt;/h3&gt;
&lt;p&gt;在刚才的一节中，我们主要使用萃取的“抽取”特性。也就是访问主参数的某些特性。在这一节中，我们会使用萃取的“转换&quot;特性。比如添加或移除引用，CV限定等。&lt;/p&gt;
&lt;h4&gt;删除引用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;删除引用是比较常见的萃取操作。比如标准库的&lt;code&gt;remove_reference&lt;/code&gt;。其具体实现已经在下面介绍了。此处不赘述。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;添加引用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;对于添加引用，标准库提供的是&lt;code&gt;add_rvalue_reference&lt;/code&gt;和&lt;code&gt;add_lvalue_reference&lt;/code&gt;。注意我们在11.2.1中提到的。&lt;code&gt;add_rvalue_reference&lt;/code&gt;会施加引用折叠。一定要格外注意。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;移除CV限定&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;对于这一系列操作，标准库提供了&lt;code&gt;remove_cv/remove_volatile/remove_const&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;退化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;对于退化操作，标准库提供了&lt;code&gt;decay&lt;/code&gt;。其模拟实现可以在书中的这一节简单了解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.3.3 预测型萃取（Predicate Traits) --- 也就是类型预测（判断）&lt;/h3&gt;
&lt;p&gt;我们之前所提到的萃取类型都是针对单个模板参数的。我们针对该模板参数进行修改或提炼。有些时候我们会针对多个模板参数进行萃取。这种就叫做类型预测。也就是一个返回&lt;code&gt;true&lt;/code&gt;或&lt;code&gt;false&lt;/code&gt;的类型函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;经典的预测型萃取有比如&lt;code&gt;is_same&lt;/code&gt;等。尤为注意在预测型萃取中，我们会返回一个&lt;code&gt;bool&lt;/code&gt;值。为了支持标记派发，产生&lt;code&gt;bool&lt;/code&gt;的萃取实现都应该通过&lt;code&gt;std::true_type&lt;/code&gt;和&lt;code&gt;std::false_type&lt;/code&gt;的类型进行继承。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;19.3.4 针对返回结果类型进行萃取（Result Type Traits)&lt;/h3&gt;
&lt;p&gt;假设我们有一个需求，是对两个&lt;code&gt;vector&lt;/code&gt;求和的函数模板。我们的第一个版本可能会长成这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
vector&amp;lt;T&amp;gt; add(const vector&amp;amp;&amp;lt;T&amp;gt; vec1, const vector&amp;lt;T&amp;gt;&amp;amp; vec2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们可能会允许两个不同类型的vector相加。但是我们如何处理返回值呢？&lt;/p&gt;
&lt;p&gt;这个时候可以使用返回值类型模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class wtf{
    public:
    int val;
    wtf() = default;
    wtf(int x):val(x){};
    wtf operator+(const wtf&amp;amp; rhs) const{ //细节1
        cout &amp;lt;&amp;lt;&quot;wtf+&quot; &amp;lt;&amp;lt; endl;
        return wtf(val + rhs.val);
    }
};

template&amp;lt;typename T1, typename T2&amp;gt;
struct VecPlusTrait{ //返回值类型萃取
    using Type = decltype(T1() + T2()); //细节2
};

template&amp;lt;typename T1, typename T2&amp;gt;
vector&amp;lt;typename VecPlusTrait&amp;lt;T1, T2&amp;gt;::Type&amp;gt; operator+(const vector&amp;lt;T1&amp;gt;&amp;amp; vec1, const vector&amp;lt;T2&amp;gt;&amp;amp; vec2 ){
    if(vec1.size() != vec2.size()){
        return {};
    }
    vector&amp;lt;typename VecPlusTrait&amp;lt;T1, T2&amp;gt;::Type&amp;gt; myvec;
    myvec.reserve(vec1.size());
    for(int i = 0; i &amp;lt; vec1.size(); i++){
        myvec.emplace_back(vec1[i] + vec2[i]);
    }
    return myvec;
}

int main(){
    vector&amp;lt;wtf&amp;gt; vec1{1,2,3,4,5,6,7};
    vector&amp;lt;wtf&amp;gt; vec2{2,3,4,5,6,7,8};
    auto ret = vec1+vec2;
    for(auto&amp;amp; i:ret){
        cout &amp;lt;&amp;lt;i.val &amp;lt;&amp;lt; endl;
    }
    return 0;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这里细节较多。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一个细节：&lt;code&gt;wtf&lt;/code&gt; 的 &lt;code&gt;operator+&lt;/code&gt;必须加&lt;code&gt;const&lt;/code&gt;。原因是下面调用&lt;code&gt;wtf&lt;/code&gt;的&lt;code&gt;operator+&lt;/code&gt;的时候，我们入参的容器是常量引用接受的。此时从容器中提出来的元素也是常量引用。此时常量引用不能调用wtf的非常量成员函数。会报错。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二个细节：此处我们使用的&lt;code&gt;decltype&lt;/code&gt;要记住传入的是表达式。&lt;code&gt;类型+()&lt;/code&gt;等于创建匿名对象，然后调用其对应类型的&lt;code&gt;operator+&lt;/code&gt;然后判断表达式的返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;但是这里有个问题，如果&lt;code&gt;wtf&lt;/code&gt;的&lt;code&gt;operator+&lt;/code&gt;返回的是一个引用呢？或者更为常见的例子是它返回的是一个&lt;code&gt;const wtf&lt;/code&gt;，这样就出问题了。容器不能储存引用也不能储存常量。那么这时候不要忘记使用&lt;code&gt;decay&lt;/code&gt;，像这样&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
vector&amp;lt;typename std::decay&amp;lt;typename VecPlusTrait&amp;lt;T1, T2&amp;gt;::Type&amp;gt;::type&amp;gt; operator+(const vector&amp;lt;T1&amp;gt;&amp;amp; vec1, const vector&amp;lt;T2&amp;gt;&amp;amp; vec2 ){ //使用decay
    if(vec1.size() != vec2.size()){
        return {};
    }
    vector&amp;lt;typename std::decay&amp;lt;typename VecPlusTrait&amp;lt;T1, T2&amp;gt;::Type&amp;gt;::type&amp;gt; myvec; /使用decay
    myvec.reserve(vec1.size());
    for(int i = 0; i &amp;lt; vec1.size(); i++){
        myvec.emplace_back(vec1[i] + vec2[i]);
    }
    return myvec;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里还有第二个问题，我们用的&lt;code&gt;decltype&lt;/code&gt;要求&lt;code&gt;T1&lt;/code&gt;和&lt;code&gt;T2&lt;/code&gt;类型必须可以构造临时对象（有并且可以访问默认构造）。但是我们并不一定非得要让这两个类拥有默认构造，所以这时候可以用&lt;code&gt;declval&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
struct VecPlusTrait{
    using Type = decltype(declval&amp;lt;T1&amp;gt;() + declval&amp;lt;T2&amp;gt;());
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;19.4 基于SFINAE的萃取&lt;/h2&gt;
&lt;p&gt;我们在8.4和&lt;code&gt;void_t&lt;/code&gt;中介绍了SFINAE和其做为排除某些重载函数或排除某些偏特化的技巧。这里再聊聊。&lt;/p&gt;
&lt;h3&gt;19.4.1 使用SFINAE排除某些重载函数&lt;/h3&gt;
&lt;p&gt;比如我们想模仿标准库写一个简单的&lt;code&gt;is_default_constructible&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct ChectIsDefaultConstructible{
    private:
        template&amp;lt;typename U, typename = decltype(U())&amp;gt; //细节1
        static char check(void*); //细节2

        template&amp;lt;typename&amp;gt;
        static int check(...); //细节3

    public:
        static constexpr bool value = is_same&amp;lt;decltype(check&amp;lt;T&amp;gt;(nullptr)), char&amp;gt;::value; //细节4
};
int main() {
    cout &amp;lt;&amp;lt;std::boolalpha;
    cout &amp;lt;&amp;lt; ChectIsDefaultConstructible&amp;lt;myclass&amp;gt;::value &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心原理就是利用函数重载。如果类型&lt;code&gt;T&lt;/code&gt;是可默认构造的，那么&lt;code&gt;T()&lt;/code&gt;是合法表达式。如果&lt;code&gt;T()&lt;/code&gt;不合法，该选择会被丢弃，转而匹配万能匹配的第二个函数。至于具体返回值，仅是当做标签使用。细节4当中就是判断其重载后函数的返回值是否是我们期待的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;首先第一个细节，我们没有使用&lt;code&gt;T&lt;/code&gt;而是使用&lt;code&gt;U&lt;/code&gt;。原因是我们在细节4当中把&lt;code&gt;T&lt;/code&gt;做为函数的&lt;code&gt;U&lt;/code&gt;传了进去。如果直接使用&lt;code&gt;T&lt;/code&gt;像这样：&lt;code&gt;template&amp;lt;typename, typename = decltype(T())&amp;gt;&lt;/code&gt;。因为类模板在实例化的时候会直接把所有的T换成传入的类型。如果传入的类型不可默认构造，则会出现意外的结果。比如我们这里使用了一个已删除的默认构造函数（如果传入的类型是不可默认构造的）。虽然g++不会报错（clang会报错），但是结果是错的。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;第二个细节和第三个细节是使用static是为了可以不通过对象调用。同时函数入参必须有区别。为了不麻烦，我们传入空指针。&lt;/li&gt;
&lt;li&gt;细节三是&lt;code&gt;...&lt;/code&gt;可以接受任意类型和数量的参数。因为函数不能仅通过返回值重载。&lt;/li&gt;
&lt;li&gt;细节四是因为静态不能类内初始化。除非是静态&lt;code&gt;constexpr&lt;/code&gt;对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.4.2 使用SFINAE排除偏特化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;还记得我们在&lt;code&gt;void_t&lt;/code&gt;章节详细讲述的它和SFINAE的关系吗？这里又来啦。还是19.4.1的例子，判断是否可以默认构造。但是我们从依靠函数重载做决定变成了依靠偏特化做决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = void&amp;gt; //原因不解释了。都在void_t章节
struct ChectIsDefaultConstructible : false_type{};  //主模板默认是false_type
template&amp;lt;typename T&amp;gt;
struct ChectIsDefaultConstructible&amp;lt;T, void_t&amp;lt;decltype(T())&amp;gt;&amp;gt; : true_type{};

int main() {
    cout &amp;lt;&amp;lt;std::boolalpha;
    cout &amp;lt;&amp;lt; ChectIsDefaultConstructible&amp;lt;myclass&amp;gt;::value &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是感觉明显比使用函数重载的清爽了很多？&lt;strong&gt;但是19.4.1依靠函数重载的实现可以允许我们使用额外的辅助函数或辅助类。此处依靠偏特化的则要求我们必须能把条件放入模板参数的声明中。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;19.4.4 SFINAE友好的萃取。注意什么是立即语境。&lt;/h3&gt;
&lt;p&gt;SFINAE并不能永远保护我们。如果错误发生在非立即语境，则会出现问题。比如我们19.3.4写的&lt;code&gt;VecPlusTrait&lt;/code&gt;萃取，当我们的两个传入参数并不能相加的时候就不会发生SFINAE，而是一个硬错误。&lt;/p&gt;
&lt;h4&gt;立即语境（立即上下文）&lt;/h4&gt;
&lt;p&gt;下面的俩链接过于抽象。什么是立即语境？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;直接上下文基本上是模板声明（包括模板参数列表、函数返回类型和函数参数列表）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;只有函数类型、其模板参数类型及其显式说明符的直接上下文中的无效类型和表达式才会导致推导失败。SFINAE&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;在推断或者替换一个备选函数模板的时候，任何发生在类模板定义的实例化过程中的事情都不是函数模板替换的立即上下文&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;只有在函数类型或其模板形参类型的立即语境中的类型与表达式中的失败是 SFINAE 错误。如果对替换后的类型/表达式的求值导致副作用，&lt;strong&gt;例如实例化某模板特化、生成某隐式定义的成员函数等&lt;/strong&gt;，那么这些副作用&lt;strong&gt;中&lt;/strong&gt;的错误被当做硬错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;通俗解释就是，我们调用函数的时候首先要实例化，如果实例化阶段就非法，那是硬错误。因为没到SFINAE阶段。所以这不是立即语境。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;如果实例化阶段结束，到了重载阶段，这个阶段发生的错误可以被SFINAE掉。这是立即语境。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当你发现模板声明中的东西可以直接确定的时候，这就是立即语境。当你发现模板声明中的东西不能直接确定，而是需要再实例化一个别的模板，那么这个实例化一个别的模板的过程中就不是立即语境。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;或者是理解为模板只管替换本身成不成功，不管其他的。如果模板本身替换失败，则触发sfinae&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果不是立即语境，那么在非立即语境中的错误都是硬错误。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230205014845.png&quot; alt=&quot;QQ截图20230205014845&quot; /&gt;&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/15260685/what-exactly-is-the-immediate-context-mentioned-in-the-c11-standard-for-whic&lt;/p&gt;
&lt;p&gt;https://www.cppstories.com/2022/sfinea-immediate-context/&lt;/p&gt;
&lt;p&gt;回到例子中来。假设我们单独为某两个没有相加操作的类的数组提供一个单独的相加函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;noaddA&amp;gt; operator+(vector&amp;lt;noaddA&amp;gt;&amp;amp; a, vector&amp;lt;noaddB&amp;gt;&amp;amp; b){
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
    return{};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是会出现问题。因为程序无法判断是这个单独为&lt;code&gt;noaddA&lt;/code&gt;和&lt;code&gt;noaddB&lt;/code&gt;准备的更好还是模板的更好。然后会尝试实例化。但是实例化中又会遇到&lt;code&gt;noaddA&lt;/code&gt;没有和&lt;code&gt;noaddB&lt;/code&gt;相加的操作的问题。&lt;strong&gt;因为我们有一个萃取的&lt;code&gt;decltype&lt;/code&gt;操作。在两个&lt;code&gt;operator+&lt;/code&gt;中，返回值的萃取操作涉及到了额外的模板实例化，所以这部分不是立即语境，如果此时&lt;code&gt;decltype&lt;/code&gt;的表达式无效，则会立即报错。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;既然如此，那么我们就需要改一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename, typename, typename = void&amp;gt; //void_t章节介绍。此处不介绍更多了。
struct HasPlusFunction: false_type{};  //判断类型是否有内置的相加操作。

template&amp;lt;typename T1, typename T2&amp;gt;
struct HasPlusFunction&amp;lt;T1, T2, void_t&amp;lt;decltype(declval&amp;lt;T1&amp;gt;() + declval&amp;lt;T2&amp;gt;())&amp;gt;&amp;gt; : true_type{}; //如果有相加的函数，则没问题。细节1


class wtf{
    public:
    int val;
    wtf() = default;
    wtf(int x):val(x){};
    wtf operator+(const wtf&amp;amp; rhs) const{ //细节1
        cout &amp;lt;&amp;lt;&quot;wtf+&quot; &amp;lt;&amp;lt; endl;
        return wtf(val + rhs.val);
    }
};
class noaddA{
    public:
    int val;
    noaddA() = default;
    noaddA(int x):val(x){};
};
class noaddB{
    public:
    int val;
    noaddB() = default;
    noaddB(int x):val(x){};
};

template&amp;lt;typename T1, typename T2, bool = HasPlusFunction&amp;lt;T1, T2&amp;gt;::value&amp;gt;
struct VecPlusTrait{ //第二次萃取，细节2
    using Type = decltype(declval&amp;lt;T1&amp;gt;() + declval&amp;lt;T2&amp;gt;());
};
template&amp;lt;typename T1, typename T2&amp;gt;
struct VecPlusTrait&amp;lt;T1, T2, false&amp;gt;{}; //针对没有内置相加操作的类型的偏特化。


template&amp;lt;typename T1, typename T2&amp;gt;
vector&amp;lt;typename std::decay&amp;lt;typename VecPlusTrait&amp;lt;T1, T2&amp;gt;::Type&amp;gt;::type&amp;gt; operator+(const vector&amp;lt;T1&amp;gt;&amp;amp; vec1, const vector&amp;lt;T2&amp;gt;&amp;amp; vec2 ){
    if(vec1.size() != vec2.size()){
        return {};
    }
    vector&amp;lt;typename std::decay&amp;lt;typename VecPlusTrait&amp;lt;T1, T2&amp;gt;::Type&amp;gt;::type&amp;gt; myvec;
    myvec.reserve(vec1.size());
    for(int i = 0; i &amp;lt; vec1.size(); i++){
        myvec.emplace_back(vec1[i] + vec2[i]);
    }
    return myvec;
}

vector&amp;lt;noaddA&amp;gt; operator+(vector&amp;lt;noaddA&amp;gt;&amp;amp; a, vector&amp;lt;noaddB&amp;gt;&amp;amp; b){
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
    return{};
}
int main(){
    vector&amp;lt;wtf&amp;gt; vec1{1,2,3,4,5,6,7};
    vector&amp;lt;int&amp;gt; vec2{2,3,4,5,6,7,8};
    auto ret = vec1+vec2;
    for(auto&amp;amp; i:ret){
        cout &amp;lt;&amp;lt;i.val &amp;lt;&amp;lt; endl;
    }
    vector&amp;lt;noaddA&amp;gt; vec3{1,2,3,4,5,6,7};
    vector&amp;lt;noaddB&amp;gt; vec4{2,3,4,5,6,7,8};
    auto ret2 = vec3+vec4;
    return 0;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;既然在两个&lt;code&gt;operator+&lt;/code&gt;中我们涉及到了额外的模板实例化操作，那么我们再次增加一层。注意我们的&lt;code&gt;VecPlusTrait&lt;/code&gt;有了第三个参数。这个参数涉及到了&lt;code&gt;HasPlusFunction&lt;/code&gt;的实例化。我们通过&lt;code&gt;HasPlusFunction&lt;/code&gt;可以判断一个类型是否有内置的加法操作。这里可以正确SFINAE掉。因为它发生在&lt;code&gt;HasPlusFunction&lt;/code&gt;的里面。这个里面是立即语境。因为针对&lt;code&gt;HasPlusFunction&lt;/code&gt;的选择操作没有额外的模板介入。&lt;/li&gt;
&lt;li&gt;随后，我们会获取到&lt;code&gt;true&lt;/code&gt;或&lt;code&gt;false&lt;/code&gt;。如果是&lt;code&gt;true&lt;/code&gt;，则&lt;code&gt;decltype&lt;/code&gt;不会出现问题。因为合法。如果是&lt;code&gt;false&lt;/code&gt;则会直接返回。不会经过&lt;code&gt;decltype&lt;/code&gt;语句。&lt;/li&gt;
&lt;li&gt;因为我们是在两个&lt;code&gt;operator+&lt;/code&gt;中进行抉择，如果刚才的返回是&lt;code&gt;false&lt;/code&gt;，则这时候我们的模板&lt;code&gt;operator+&lt;/code&gt;会被SFINAE掉。因为只有萃取部分不是立即语境（对替换后的类型的实例化某些模板特化中的错误被当做硬错误。理解为进入了另一个实例化的模板中）。但是这时候脱离了萃取部分，仅仅是两个&lt;code&gt;operator+&lt;/code&gt;中选择的话，是立即语境。所以就会去专门选择&lt;code&gt;noaddA&lt;/code&gt;和&lt;code&gt;noaddB&lt;/code&gt;指定的&lt;code&gt;operator+&lt;/code&gt;重载版本。&lt;/li&gt;
&lt;li&gt;注意，如果我们的相加操作的类型同时内置支持相加，并且也写出了单独的版本，此时会选择单独版本。因为两者重载决议中显然会选择非模板函数。
&lt;ul&gt;
&lt;li&gt;我们在1.5和重载决议中提到：重载决议只看声明，不看定义。但是我们的萃取，&lt;code&gt;decltype&lt;/code&gt;之类的都在声明里。所以它依旧会去进行计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;19.5 简单实现is_convertible&lt;/h2&gt;
&lt;p&gt;我们在下面介绍了is_convertible的用法。这里尝试进行一个简单实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename From, typename To&amp;gt;
struct IsConvertible{
    private:
        static void try_call(To); //细节1
    
        template&amp;lt;typename&amp;gt;
        static std::false_type check(...); //细节2

        template&amp;lt;typename F, typename = decltype(try_call(declval&amp;lt;F&amp;gt;()))&amp;gt;  //细节3
        static std::true_type check(void*);
    public:
        using Type = decltype(check&amp;lt;From&amp;gt;(nullptr)); //细节4
};

template &amp;lt;typename From, typename To&amp;gt;
struct IsConvertibleValue:IsConvertible&amp;lt;From, To&amp;gt;::Type{}; //细节5



int main(){
    cout &amp;lt;&amp;lt; std::boolalpha;
    cout &amp;lt;&amp;lt; is_convertible&amp;lt;int,  string&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; IsConvertibleValue&amp;lt;int,  string&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; is_convertible&amp;lt;string,  int&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; IsConvertibleValue&amp;lt;string,  int&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; is_convertible&amp;lt;double,  int&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; IsConvertibleValue&amp;lt;double,  int&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; is_convertible&amp;lt;int,  double&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; IsConvertibleValue&amp;lt;int,  double&amp;gt;::value &amp;lt;&amp;lt; endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首先，所有成员函数都要是&lt;code&gt;static&lt;/code&gt;为了可以直接调用。&lt;/li&gt;
&lt;li&gt;第一个细节。这个函数的目的是使用细节3中的&lt;code&gt;decltype&lt;/code&gt;检查&lt;code&gt;From&lt;/code&gt;类型参数能否放入&lt;code&gt;To&lt;/code&gt;类型的形参。如果合法证明可以convert。不合法证明不可以。会被丢弃。转而匹配细节2的万能fallback方案。&lt;/li&gt;
&lt;li&gt;第二个细节。&lt;code&gt;...&lt;/code&gt;匹配任意数量任意类型的参数。做为fallback方案&lt;/li&gt;
&lt;li&gt;第三个细节。我们采用&lt;code&gt;declval&lt;/code&gt;避免创建临时对象。然后传入&lt;code&gt;try_call&lt;/code&gt;后使用&lt;code&gt;decltype&lt;/code&gt;判断是否合法。&lt;/li&gt;
&lt;li&gt;第四个细节。显式指定模板函数&lt;code&gt;check&lt;/code&gt;的形参&lt;code&gt;F&lt;/code&gt;的类型是&lt;code&gt;From&lt;/code&gt;。然后传入一个参数。最后使用&lt;code&gt;decltype&lt;/code&gt;判断是&lt;code&gt;false_type&lt;/code&gt;还是&lt;code&gt;true_type&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;注意必须有&lt;code&gt;F&lt;/code&gt;，如果直接写&lt;code&gt;declval&amp;lt;From&amp;gt;&lt;/code&gt;则会在实例化阶段报错。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第五个细节。&lt;code&gt;IsConvertibleValue&lt;/code&gt;将会继承自计算后的&lt;code&gt;IsConvertible&lt;/code&gt;的&lt;code&gt;type&lt;/code&gt;类型。也就是要么继承自&lt;code&gt;false_type&lt;/code&gt;要么继承自&lt;code&gt;true_type&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;特殊情况&lt;/h3&gt;
&lt;p&gt;上面的代码无法处理以下三种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向数组类型的转换要始终返回 &lt;code&gt;false&lt;/code&gt;，但是在上面的代码中，&lt;code&gt;try_call()&lt;/code&gt;声明中的类型为 &lt;code&gt;To&lt;/code&gt;的参数会退化成指针类型，因此对于某些 &lt;code&gt;From&lt;/code&gt; 类型，它会返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;向指针类型的转换也应该始终返回 &lt;code&gt;false&lt;/code&gt;，但是和 1 中的情况一样，上述实现只会将它们当作退化后的类型。&lt;/li&gt;
&lt;li&gt;向（被 &lt;code&gt;const/volatile&lt;/code&gt; 修饰）的 &lt;code&gt;void&lt;/code&gt; 类型的转换需要返回 &lt;code&gt;true&lt;/code&gt;。但是不幸的是，在 &lt;code&gt;To&lt;/code&gt; 是 &lt;code&gt;void&lt;/code&gt; 的时候，上述实现甚至不能被正确实例化，因为参数类型不能包含 &lt;code&gt;void&lt;/code&gt; 类型（而且 &lt;code&gt;try_call()&lt;/code&gt;的定义也用到了这一参数）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决这个问题可以引入额外模板参数，使用&lt;code&gt;is_array&lt;/code&gt;, &lt;code&gt;is_function&lt;/code&gt;和&lt;code&gt;is_void&lt;/code&gt;（注意不是&lt;code&gt;void_t&lt;/code&gt;）来进行判断。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;19.6 探测（检查）成员&lt;/h2&gt;
&lt;h3&gt;19.6.1 检查类型成员 （Detect Member Type)&lt;/h3&gt;
&lt;p&gt;查看&lt;code&gt;void_t&lt;/code&gt;章节。但是这里额外增加一点注意事项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    using mytype = int; //类A含有mytype类型
};

struct B{
    using mytype = void; //类B也含有mytype类型
};


template&amp;lt;typename T1 , typename T2 = void &amp;gt; //主模板，第二个参数默认值是void，非常重要
struct has_type_member : std::false_type { //主模板继承false_type

 };

template&amp;lt;typename T1&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;typename T1::mytype&amp;gt;&amp;gt;: std::true_type { //偏特化继承true_type，并且使用void_t进行类型判断。
}; 

int main(){
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    auto c = has_type_member&amp;lt;A&amp;amp;&amp;gt;::value; //注意这两个
    auto d = has_type_member&amp;lt;B&amp;amp;&amp;gt;::value; //注意这两个
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; d &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;比如上面代码。如果正常传入一个类型本身没什么问题。但是我们如果传入引用就会出现问题。&lt;strong&gt;因为引用类型确实没有成员&lt;/strong&gt;。引用类型和其本身类型并不是同一种类型。但是我们可能会希望，当我们传递进来的模板参数是引用类型的时候，依然根据其指向的类型做判断。为了这一目的，我们可以使用&lt;code&gt;remove_reference&lt;/code&gt;进行辅助。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1 , typename T2 = void &amp;gt;
struct has_type_member : std::false_type {

 };
template&amp;lt;typename T1&amp;gt; //针对第二个参数进行偏特化
struct has_type_member&amp;lt;T1, std::void_t&amp;lt;typename remove_reference&amp;lt;T1&amp;gt;::type::mytype&amp;gt;&amp;gt;: std::true_type { //偏特化继承true_type，并且使用void_t进行类型判断。
}; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;注入类的名字 （injected-class-name）杂记4中提过。&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注入类名是在类的作用域内该类自身的名字。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在类作用域中，当前&lt;strong&gt;类的名字被当做它如同是一个公开成员名一样&lt;/strong&gt;；这被称为&lt;em&gt;注入类名（injected-class-name）&lt;/em&gt;。该名字的声明点紧跟类定义的开花括号之后。&lt;/li&gt;
&lt;li&gt;与其他成员类似，&lt;strong&gt;注入类名可被继承，但是依旧受可见性制约&lt;/strong&gt;。在私有或受保护继承的场合，可能导致某个间接基类的注入类名在派生类中最后变得不可访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所以我们的萃取代码，可以检测注入类的名字。换句话说，就是检测是否继承自某个特定类。
&lt;ul&gt;
&lt;li&gt;这里仍然受到可见性限制，也就是私有继承会返回&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct A{

};

struct B:public A{

};

struct C:private B{

};
int main(){
    auto a = has_type_member&amp;lt;A&amp;gt;::value;
    auto b = has_type_member&amp;lt;B&amp;gt;::value;
    auto c = has_type_member&amp;lt;C&amp;gt;::value;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; //true
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl; //true
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl; //false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;19.6.2 检查任意类型成员。&lt;/h3&gt;
&lt;p&gt;目前只能通过宏来实现这一操作。&lt;code&gt;##&lt;/code&gt;的作用在杂记4&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define DEFINE_HAS_TYPE(MemType)\
template&amp;lt;typename, typename = void&amp;gt;\
struct HasTypeT_##MemType : std::false_type{};\
template&amp;lt;typename T&amp;gt;\
struct HasTypeT_##MemType&amp;lt;T, std::void_t&amp;lt;typename T::MemType&amp;gt;&amp;gt; : std::true_type{} // 注意没有分号！
struct A{
    using mytype = int;
};

struct B{
    using anothertype = float;
};
DEFINE_HAS_TYPE(mytype); //我们上面故意去掉的分号原因是这里是那个分号。
DEFINE_HAS_TYPE(anothertype);
int main(){
    cout &amp;lt;&amp;lt; boolalpha;
    cout &amp;lt;&amp;lt; HasTypeT_mytype&amp;lt;A&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; HasTypeT_mytype&amp;lt;B&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; HasTypeT_anothertype&amp;lt;A&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; HasTypeT_anothertype&amp;lt;B&amp;gt;::value &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于我们传入的变量叫做&lt;code&gt;MemType&lt;/code&gt;。所以我们传入&lt;code&gt;mytype&lt;/code&gt;的时候，&lt;code&gt;HasTypeT_##MemType&lt;/code&gt;会变成&lt;code&gt;HasTypeT_mytype&lt;/code&gt;。其他的同理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.6.3 检查非类型成员&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;void_t&lt;/code&gt;中介绍了。稍微补充一下。&lt;/p&gt;
&lt;p&gt;针对数据成员，也可以使用检查成员函数的第二种方式：&lt;code&gt;&amp;lt;取成员地址&amp;gt;&lt;/code&gt;是否合法来判断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成员名字，也就是&lt;code&gt;&amp;amp;T::Member&lt;/code&gt;中的Member名字不能有歧义。比如不能是重载，不能是多继承中的同名成员。&lt;/li&gt;
&lt;li&gt;必须可以访问。&lt;/li&gt;
&lt;li&gt;必须是非类型和非枚举成员，否则&lt;code&gt;&amp;amp;&lt;/code&gt;无效。&lt;/li&gt;
&lt;li&gt;如果是&lt;code&gt;static&lt;/code&gt;静态成员，则对应类型必须提供有效的&lt;code&gt;operator&amp;amp;&lt;/code&gt;，如果无效，如设置&lt;code&gt;operator&amp;amp;&lt;/code&gt;为私有则无效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.6.4 检查其他表达式 （比如是否有特定操作，如大于小于）&lt;/h3&gt;
&lt;p&gt;这种基于偏特化的SFINAE可以应用在很多方面，包括其他表达式。也可以把多个条件放在一起（都放在&lt;code&gt;void_t&lt;/code&gt;里面，只要一个条件无效就整个偏特化丢弃。）比如我们下面的例子是探测两个类型&lt;code&gt;T1&lt;/code&gt;和&lt;code&gt;T2&lt;/code&gt;是否有小于操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {

};

struct B{
    bool operator &amp;lt;(A o1){ //B和A的小于操作
    }
};
bool operator &amp;lt;(int, A o1){ //int和A的小于操作

}

template&amp;lt;typename T1 , typename T2, typename = void &amp;gt;
struct has_less_operation:std::false_type { 

 };

template&amp;lt;typename T1, typename T2&amp;gt;
struct has_less_operation&amp;lt;T1, T2, std::void_t&amp;lt;decltype(declval&amp;lt;T1&amp;gt;() &amp;lt; declval&amp;lt;T2&amp;gt;())&amp;gt;&amp;gt;: std::true_type { //判断是否有小于操作
}; 
int main(){
    cout &amp;lt;&amp;lt;std::boolalpha;
    cout &amp;lt;&amp;lt; has_less_operation&amp;lt;int, int&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; has_less_operation&amp;lt;string, string&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; has_less_operation&amp;lt;B, A&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; has_less_operation&amp;lt;int, A&amp;gt;::value &amp;lt;&amp;lt; endl;

    cout &amp;lt;&amp;lt; has_less_operation&amp;lt;string, int&amp;gt;::value &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; has_less_operation&amp;lt;A, B&amp;gt;::value &amp;lt;&amp;lt; endl;
    
    /*
    输出
    true
    true
    true
    true
    false
    false
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采用这种方式探测表达式有效性的萃取是很稳健的：如果表达式没有问题，它会返回 &lt;code&gt;true&lt;/code&gt;，而如果&lt;code&gt;&amp;lt;&lt;/code&gt;运算符有歧义，被删除，或者不可访问的话，它也可以准确的返回&lt;code&gt; false&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;19.7 其他的萃取技术&lt;/h2&gt;
&lt;h3&gt;19.7.1 If-then-Else&lt;/h3&gt;
&lt;p&gt;个人理解这个的用处非常像&lt;code&gt;enable_if&lt;/code&gt;。但我们提到了，&lt;code&gt;enable_if&lt;/code&gt;不能用于类模板。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;bool cond, typename TypeIfTrue, typename TypeIfFalse&amp;gt;
struct IfthenElse{
    using Type = TypeIfFalse;
};

template&amp;lt;typename TypeIfTrue, typename TypeIfFalse&amp;gt;
struct IfthenElse&amp;lt;true, TypeIfTrue, TypeIfFalse&amp;gt;{
    using Type = TypeIfTrue;
};

int main() {
    IfthenElse&amp;lt;(sizeof(int) &amp;gt;= 4), int, double&amp;gt;::Type a;
    cout &amp;lt;&amp;lt; boolalpha;
    cout &amp;lt;&amp;lt; is_same&amp;lt;int, decltype(a)&amp;gt;::value &amp;lt;&amp;lt; endl; //true
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心目的是如果表达式&lt;code&gt;cond&lt;/code&gt;是&lt;code&gt;true&lt;/code&gt;，则让&lt;code&gt;type&lt;/code&gt;是&lt;code&gt;truetype&lt;/code&gt;。反之亦然。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标准库提供的是&lt;code&gt;std::conditional&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.7.2 检查操作是否抛出异常&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;我们在杂记4中介绍了&lt;code&gt;noexcept&lt;/code&gt;可以是运算符，也就是后面可以跟随一个表达式。这样做可以通过表达式结果是&lt;code&gt;true&lt;/code&gt;或&lt;code&gt;false&lt;/code&gt;来声明函数是否是&lt;code&gt;noexcept&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这种形式比较经典的应用在了&lt;code&gt;vector&lt;/code&gt;的移动构造函数中。如果元素的移动构造不能保证&lt;code&gt;noexcept&lt;/code&gt;，就不会调用移动构造而是调用拷贝构造。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们也用我们的方式来实现一下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;准备一个针对&lt;code&gt;true_type&lt;/code&gt;和&lt;code&gt;false_type&lt;/code&gt;的类型萃取。这一步的目的是可以在某个类型没有移动或构造函数的时候仍旧可以SFINAE。
&lt;ul&gt;
&lt;li&gt;没有移动或构造函数，则&lt;code&gt;T1(declval&amp;lt;T1&amp;gt;())&lt;/code&gt;无效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;bool check = false&amp;gt; //默认是false
struct bool_extraction{
    using value = false_type;
    void func(){
        cout &amp;lt;&amp;lt;&quot;false&quot; &amp;lt;&amp;lt; endl;
    }
};
template&amp;lt;&amp;gt;
struct bool_extraction&amp;lt;true&amp;gt;{ //true的偏特化。
    using value = true_type;
    void func(){
        cout &amp;lt;&amp;lt;&quot;true&quot; &amp;lt;&amp;lt; endl;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第一个版本：使用基于默认类型的SFINAE。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename = true_type&amp;gt; //必须是true_type
struct Is_Nothrow_Move_Constructible : std::false_type{
};


template&amp;lt;typename T1&amp;gt;
struct Is_Nothrow_Move_Constructible&amp;lt;T1, typename bool_extraction&amp;lt;noexcept(T1(declval&amp;lt;T1&amp;gt;()))&amp;gt;::value&amp;gt; : std::true_type{
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;code&gt;noexcept(T1(declval&amp;lt;T1&amp;gt;())&lt;/code&gt;表达式无效，则会匹配至&lt;code&gt;bool_extraction&lt;/code&gt;的主模板。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为什么&lt;code&gt;Is_Nothrow_Move_Constructible&lt;/code&gt;默认值必须是&lt;code&gt;truetype&lt;/code&gt;？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原理和&lt;code&gt;void_t&lt;/code&gt;基本差不多。&lt;/li&gt;
&lt;li&gt;如果是&lt;code&gt;false_type&lt;/code&gt;，假如&lt;code&gt;T1&lt;/code&gt;的拷贝构造或移动构造是非&lt;code&gt;noexcept&lt;/code&gt;，则类型会被推导为&lt;code&gt;&amp;lt;T1, false_type&amp;gt;&lt;/code&gt;。此时由于下面的比上面的更特化。导致继承了下面的，导致&lt;code&gt;value&lt;/code&gt;是&lt;code&gt;true_type&lt;/code&gt;。如果是&lt;code&gt;noexcept&lt;/code&gt;，则类型会被推导为&lt;code&gt;&amp;lt;T1, true_type&amp;gt;&lt;/code&gt;。但是我们的默认值是&lt;code&gt;false_type&lt;/code&gt;则相当于指定&lt;code&gt;&amp;lt;T1, false_type&amp;gt;&lt;/code&gt;为模板实参。此时模板实参和推导的模板实参不匹配，第二个版本会被丢弃。主模板可以匹配，则回退至主模板。继承了&lt;code&gt;false_type&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;所以我们看到，如果默认值是&lt;code&gt;false_type&lt;/code&gt;，则结果会完全相反。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第二个版本：使用基于&lt;code&gt;void_t&lt;/code&gt;的SFINAE&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename = void&amp;gt;
struct Is_Nothrow_Move_ConstructibleV2 : std::false_type{
};

template&amp;lt;typename T1&amp;gt;
struct Is_Nothrow_Move_ConstructibleV2&amp;lt;T1, void_t&amp;lt;decltype(T1(declval&amp;lt;T1&amp;gt;()))&amp;gt;&amp;gt; : bool_extraction&amp;lt;noexcept(T1(declval&amp;lt;T1&amp;gt;()))&amp;gt;::value{
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;说实话，我真的没感觉出来有什么具体的差别。但是在看到第一个版本默认值类型的坑后，我觉得第二个版本更加人道一些。更少的magic。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A { //无拷贝构造和移动构造  
    A(){}
    A(const A&amp;amp;) = delete;
    A(A&amp;amp;&amp;amp;) = delete;
};
struct B{ //有拷贝构造和移动构造且都为noexcept
    B(){}
    B(const B&amp;amp;) noexcept{}
    B(B&amp;amp;&amp;amp;) noexcept{}
};
struct C{ //有拷贝构造和移动构造且都无noexcept
    C(){}
    C(const C&amp;amp;){}
    C(C&amp;amp;&amp;amp;){}
};
struct D{ //有拷贝构造和移动构造且只有拷贝构造为noexcept
    D(){}
    D(const D&amp;amp;) noexcept{}
    D(D&amp;amp;&amp;amp;){}
};
cout &amp;lt;&amp;lt;&quot;A test:&quot; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_nothrow_move_constructible&amp;lt;A&amp;gt;::value &amp;lt;&amp;lt; endl; //false
cout &amp;lt;&amp;lt; Is_Nothrow_Move_Constructible&amp;lt;A&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; Is_Nothrow_Move_ConstructibleV2&amp;lt;A&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt;&quot;B test:&quot; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_nothrow_move_constructible&amp;lt;B&amp;gt;::value &amp;lt;&amp;lt; endl; //true
cout &amp;lt;&amp;lt; Is_Nothrow_Move_Constructible&amp;lt;B&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; Is_Nothrow_Move_ConstructibleV2&amp;lt;B&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt;&quot;C test:&quot; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_nothrow_move_constructible&amp;lt;C&amp;gt;::value &amp;lt;&amp;lt; endl; //false
cout &amp;lt;&amp;lt; Is_Nothrow_Move_Constructible&amp;lt;C&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; Is_Nothrow_Move_ConstructibleV2&amp;lt;C&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt;&quot;D test:&quot; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_nothrow_move_constructible&amp;lt;D&amp;gt;::value &amp;lt;&amp;lt; endl; //false
cout &amp;lt;&amp;lt; Is_Nothrow_Move_Constructible&amp;lt;D&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; Is_Nothrow_Move_ConstructibleV2&amp;lt;D&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt;&quot;int test:&quot; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_nothrow_move_constructible&amp;lt;int&amp;gt;::value &amp;lt;&amp;lt; endl; //true
cout &amp;lt;&amp;lt; Is_Nothrow_Move_Constructible&amp;lt;int&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; Is_Nothrow_Move_ConstructibleV2&amp;lt;int&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt;&quot;string test:&quot; &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; is_nothrow_move_constructible&amp;lt;string&amp;gt;::value &amp;lt;&amp;lt; endl; //true
cout &amp;lt;&amp;lt; Is_Nothrow_Move_Constructible&amp;lt;string&amp;gt;::value &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; Is_Nothrow_Move_ConstructibleV2&amp;lt;string&amp;gt;::value &amp;lt;&amp;lt; endl;

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;需要详细说明几个问题。我们可以理解移动是特殊的拷贝操作。所以如果
&lt;ul&gt;
&lt;li&gt;同时拥有拷贝构造和移动构造。只移动构造为&lt;code&gt;noexcept&lt;/code&gt;，则是&lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;同时拥有拷贝构造和移动构造。只拷贝构造为&lt;code&gt;noexcept&lt;/code&gt;，则是&lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;提供拷贝构造，不声明且不提供（不是&lt;code&gt;=delete&lt;/code&gt;)移动构造，且拷贝构造是&lt;code&gt;noexcept&lt;/code&gt;，则是&lt;code&gt;true&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;原因是编译器虽然压根不隐式生成移动构造，但不会标记为弃置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;提供拷贝构造，但是声明移动构造为弃置（&lt;code&gt;=delete&lt;/code&gt;），就算拷贝构造是&lt;code&gt;noexcept&lt;/code&gt;，也是&lt;code&gt;false&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;由于我们使用了&lt;code&gt;declval&lt;/code&gt;。&lt;code&gt;declval&lt;/code&gt;是强制转换为右值引用。&lt;/li&gt;
&lt;li&gt;因为这是&lt;code&gt;=delete&lt;/code&gt;的意义。我们已经通过&lt;code&gt;declval&lt;/code&gt;转换为右值引用了。然后进行构造函数匹配的时候检测到移动构造这个签名最为匹配，但是是&lt;code&gt;delete&lt;/code&gt;，就不应该继续选择次级匹配的。而是提示使用了不该用的东西。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;坑点 - 为什么必须要用declval？&lt;/h3&gt;
&lt;p&gt;为什么必须用&lt;code&gt;declval&lt;/code&gt;，不就是少构造一个么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因不仅是避免构造，旁通掉构造函数被删除或不可用的情况，更是因为它可以旁通掉构造函数为非&lt;code&gt;noexcept&lt;/code&gt;的情况。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct B{
    B(){}
    B(const B&amp;amp;) noexcept{

    }
    B(B&amp;amp;&amp;amp;) noexcept{
    }
};
cout &amp;lt;&amp;lt; noexcept(B(declval&amp;lt;B&amp;gt;())) &amp;lt;&amp;lt; endl; //true
cout &amp;lt;&amp;lt; noexcept(B(B())) &amp;lt;&amp;lt; endl; //false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为啥有区别？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;declval&lt;/code&gt;不要求构建对象。所以构造函数不参与。直接换成&lt;code&gt;B&amp;amp;&amp;amp;&lt;/code&gt;。&lt;code&gt;B(B&amp;amp;&amp;amp;)&lt;/code&gt;自然匹配&lt;code&gt;noexcept&lt;/code&gt;的移动构造。返回&lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;非&lt;code&gt;declval&lt;/code&gt;要求构建对象。有构造函数参与。但是构造函数为非&lt;code&gt;noexcept&lt;/code&gt;。最终结果自然是非&lt;code&gt;noexcept&lt;/code&gt;。返回&lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;19.8 类型分类&lt;/h2&gt;
&lt;h3&gt;19.8.1 检测基本类型&lt;/h3&gt;
&lt;p&gt;检测基本类型我们可以使用标准库的&lt;code&gt;std::is_fundamental&lt;/code&gt;或&lt;code&gt;std::intergral&lt;/code&gt;进行检测。具体的原理其实就是写一堆偏特化。&lt;/p&gt;
&lt;h3&gt;19.8.2 检测复合类型&lt;/h3&gt;
&lt;h4&gt;指针&lt;/h4&gt;
&lt;p&gt;检测指针类型我们可以使用标准库的&lt;code&gt;std::is_pointer&lt;/code&gt;。具体原理也是偏特化。&lt;/p&gt;
&lt;h4&gt;引用&lt;/h4&gt;
&lt;p&gt;检测引用类型我们可以使用标准库的&lt;code&gt;std::is_lvalue_reference&lt;/code&gt; 和 &lt;code&gt;std::is_rvalue_reference&lt;/code&gt;。具体原理还是偏特化。&lt;/p&gt;
&lt;h4&gt;数组&lt;/h4&gt;
&lt;p&gt;检测数组类型我们可以使用标准库的&lt;code&gt;std::is_array&lt;/code&gt;。具体原理依旧是偏特化。&lt;/p&gt;
&lt;h4&gt;指向成员的指针&lt;/h4&gt;
&lt;p&gt;针对这个类型可以使用&lt;code&gt;std::is_member_pointer&lt;/code&gt;, &lt;code&gt;std::is_member_object_pointer&lt;/code&gt;和&lt;code&gt;std::is_member_function_pointer&lt;/code&gt;。原理是偏特化和提到的函数类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct IsPointerToMemberT : std::false_type {
};
template&amp;lt;typename T, typename C&amp;gt;
struct IsPointerToMemberT&amp;lt;T C::*&amp;gt; : std::true_type { //也可以写为&amp;lt;T(C::*)&amp;gt;。之前函数类型的那种必须加括号是因为语法限制。
    using MemberT = T;
    using ClassT = C;
};
class cls {};

int main() {
    cout &amp;lt;&amp;lt; (IsPointerToMemberT&amp;lt;int cls::*&amp;gt;::value //也可以写为&amp;lt;int(cls::*)&amp;gt;。之前函数类型的那种必须加括号是因为语法限制
                     ? &quot;T is member pointer&quot;
                     : &quot;T is not a member pointer&quot;) &amp;lt;&amp;lt; &apos;\n&apos;;
    cout &amp;lt;&amp;lt; (IsPointerToMemberT&amp;lt;int&amp;gt;::value
                     ? &quot;T is member pointer&quot;
                     : &quot;T is not a member pointer&quot;) &amp;lt;&amp;lt; &apos;\n&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意带括号和不带括号的写法在这里是一个意思。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.8.3 检测函数类型&lt;/h3&gt;
&lt;p&gt;针对函数类型我们可以使用标准库的&lt;code&gt;std::is_function()&lt;/code&gt;。具体细节就是一堆偏特化。&lt;/p&gt;
&lt;p&gt;值的注意的是，这个东西的实现中，偏特化的数量非常多。一是因为会有任意多的参数，需要用可变参数。其次就是需要处理CV限定。&lt;/p&gt;
&lt;h3&gt;19.8.4 检测类类型&lt;/h3&gt;
&lt;p&gt;这个就比较有意思了。因为我们不能专门偏特化或枚举某个未知类型。但是我们可以利用一个技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表达式&lt;code&gt;X Y::*&lt;/code&gt;只有在&lt;code&gt;Y&lt;/code&gt;是类类型的时候才合法。
&lt;ul&gt;
&lt;li&gt;注意，我们只利用声明判断。所以&lt;code&gt;X&lt;/code&gt;具体是什么类型我们不关心。随意选一个就好。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = void&amp;gt;
struct IsClassT : std::false_type {
};
template&amp;lt;typename T&amp;gt;
struct IsClassT&amp;lt;T, void_t&amp;lt;char T::*&amp;gt;&amp;gt; : std::true_type { //这里char是随便选的。而且依旧加括号不加括号都可以。
};

int main() {
    cout &amp;lt;&amp;lt; std::boolalpha;
    cout &amp;lt;&amp;lt; IsClassT&amp;lt;int&amp;gt;::value &amp;lt;&amp;lt; endl;       //False
    cout &amp;lt;&amp;lt; IsClassT&amp;lt;myobj&amp;gt;::value &amp;lt;&amp;lt; endl;     //True
    cout &amp;lt;&amp;lt; IsClassT&amp;lt;myobj&amp;amp;&amp;gt;::value &amp;lt;&amp;lt; endl;    //False
    cout &amp;lt;&amp;lt; IsClassT&amp;lt;myobj*&amp;gt;::value &amp;lt;&amp;lt; endl;    //False
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;标准库中提供了&lt;code&gt;std::is_class&lt;/code&gt;和&lt;code&gt;std::is_union&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.8.5 检测枚举类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;标准库提供了&lt;code&gt;std::is_enum&lt;/code&gt;。最基础的原理是检查一个类型是否是基础类型，指针类型，引用类型，数组类型，成员指针类型，函数类型和类类型。都不是的话就是枚举类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;19.9 策略特征&lt;/h2&gt;
&lt;p&gt;我们在19.2详细探讨了策略和萃取的区别。&lt;strong&gt;但是我们在那一章节提到的是策略类而非策略特征。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;策略类通常独立于其他模板参数。&lt;/li&gt;
&lt;li&gt;策略特征通常是与模板参数相关联的&lt;strong&gt;唯一属性&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;19.9.1 利用if-then-else 处理只读参数类型&lt;/h3&gt;
&lt;p&gt;我们在19.7.1谈到了if-then-else可以看做是一种enable_if。具体的用法之一就是判断传入参数的类型应该是值还是引用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设我们打算针对小于两个指针大小的数据类型采用值传递，大于两个指针大小的数据采用常量左值引用传递：
&lt;ul&gt;
&lt;li&gt;尽管这么做不一定正确。因为对象的拷贝或移动成本不一定和对象的体积成正相关。比如一个10个&lt;code&gt;int&lt;/code&gt;的类和一个两个指针但是每一个指针都持有一个大型资源的类。或者是本身就不大的容器类。针对这些情况我们可以进行特化和偏特化处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//IfthenElse查看19.7.1
template&amp;lt;typename T&amp;gt;
struct PassByWhat{
    using Type = typename IfthenElse&amp;lt;sizeof(T) &amp;lt;= 2*sizeof(void*), T, const T&amp;amp;&amp;gt;::Type; //如果小于两个指针大小就是T，否则就是const T&amp;amp;
    void func(){
        std::puts(__PRETTY_FUNCTION__);
        cout &amp;lt;&amp;lt;&quot;normal&quot; &amp;lt;&amp;lt; endl;
    }
};

template&amp;lt;typename T&amp;gt;
struct PassByWhat&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt;{ //举个例子。针对vector类型的特化。如果下面传入的是vector&amp;lt;T&amp;gt;如vector&amp;lt;int&amp;gt;则会正确匹配到这里。不要惊讶。
    using Type = const vector&amp;lt;T&amp;gt;&amp;amp;; 
    void func(){
        std::puts(__PRETTY_FUNCTION__);
        cout &amp;lt;&amp;lt;&quot;vector&quot; &amp;lt;&amp;lt; endl;
    }
};

template&amp;lt;typename T1, typename T2&amp;gt;
void testfunc(typename PassByWhat&amp;lt;T1&amp;gt;::Type p1, typename PassByWhat&amp;lt;T2&amp;gt;::Type p2){
    std::puts(__PRETTY_FUNCTION__);
}

int main() {
    myobj obj;
    testclass2 testobj;
    testfunc&amp;lt;myobj, testclass2&amp;gt;(obj, testobj);
}
/*
myobj const
testclass2 const
myobj copy const
void testfunc(typename PassByWhat&amp;lt;T1&amp;gt;::Type, typename PassByWhat&amp;lt;T2&amp;gt;::Type) \
[with T1 = myobj; T2 = testclass2; typename PassByWhat&amp;lt;T1&amp;gt;::Type = myobj; typename PassByWhat&amp;lt;T2&amp;gt;::Type = const testclass2&amp;amp;]
myobj dest
testclass2 dest
myobj dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;针对&lt;code&gt;vector&amp;lt;T&amp;gt;&lt;/code&gt;类型的偏特化可以正确匹配：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;PassByWhat&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; check;
check.func();
/*
void PassByWhat&amp;lt;std::vector&amp;lt;_Tp&amp;gt; &amp;gt;::func() [with T = int]
vector
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这么做有缺点。第一个缺点是函数声明乱，很复杂。第二个缺点是此时无法使用函数模板的参数推导。因为模板参数只出现在函数参数的限定符中，并不是函数参数的本身。这里是&lt;strong&gt;不推导语境&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;用有限定标识指定的类型的 &lt;em&gt;嵌套名说明符&lt;/em&gt;（作用域解析运算符 &lt;code&gt;::&lt;/code&gt; 左侧的所有内容）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们看到我们函数签名的所有的模板形参均在作用域解析运算符左侧。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以配合与一个包装函数模板，提供完美转发服务。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C++ 模板笔记（三）：enable_if、传值还是传引用、编译期编程与泛型库</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-3/</guid><pubDate>Wed, 14 Dec 2022 14:52:00 GMT</pubDate><content:encoded>&lt;h1&gt;第六章 enable_if&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;目前，类模板无法应用这个东西。针对于类模板，enable_if只能起到一种static_assert的作用。由于类和类模板不能重载，所以自然不存在候选集这个东西。也不存在SFINAE。enable_if在应用中的感觉是一种调整候选集的作用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在函数模板中，enable_if起到的作用是活用了SFINAE的特性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/48045559/how-do-i-declare-sfinae-class&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/16972684/how-to-use-enable-if-for-restricting-a-whole-class&lt;/p&gt;
&lt;h2&gt;6.2 特殊成员函数模板 (笔记这里尤其指构造函数模板)&lt;/h2&gt;
&lt;p&gt;特殊成员函数也可以是模板，比如构造函数，但是有时候这可能会带来令人意外的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person
{
    private:
        string name;
    public:
    // constructor for passed initial name:
    explicit Person(const string&amp;amp; n) : name(n) { //构造函数。注意这里不是拷贝构造。参数不是person
        cout &amp;lt;&amp;lt; &quot;此处是拷贝字符串进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
    explicit Person(string&amp;amp;&amp;amp; n) : name(move(n)) { //构造函数。注意这里不是移动构造。参数不是person
        //里面的move是为了给字符串自己转为右值。触发string的移动
        cout &amp;lt;&amp;lt; &quot;此处是移动字符串进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
    // copy and move constructor:
    Person (const Person&amp;amp; p) : name(p.name) { //拷贝构造
        cout &amp;lt;&amp;lt; &quot;此处是拷贝person对象进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
    Person (Person&amp;amp;&amp;amp; p) : name(move(p.name)) { //移动构造
        cout &amp;lt;&amp;lt; &quot;此处是移动person对象进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
        //里面的move是为了给传入对象自己的string转为右值。然后触发string的移动
    }
};

int main(){
    string names = &quot;miku&quot;;
    Person p1(names); //拷贝字符串
    Person p2(&quot;miku&quot;);//&quot;miku&quot;是字面值，虽然字符串字面值是左值，但是现在它是个临时对象。所以会触发移动。这里是移动字符串。
    Person p3(p1); //拷贝对象
    Person p4(move(p1));//移动对象。这个move是转换p1为右值调用person的移动。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切都看起来不错。我们注意到针对使用&lt;code&gt;string&lt;/code&gt;来构造&lt;code&gt;person&lt;/code&gt;对象的时候，我们写了两个函数。一个针对左值&lt;code&gt;string&lt;/code&gt;,一个针对右值&lt;code&gt;string&lt;/code&gt;。下面我们尝试使用一下万能引用+完美转发来把两个函数合到一起。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person
{
    private:
        string name;
    public:
    template &amp;lt;typename T&amp;gt;
    explicit Person(T&amp;amp;&amp;amp; n) : name(forward&amp;lt;T&amp;gt;(n)) {
        cout &amp;lt;&amp;lt; &quot;万能引用+forward兼顾左值和右值。&quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
    // copy and move constructor:
    Person (Person const&amp;amp; p) : name(p.name) { //没有变化
        cout &amp;lt;&amp;lt; &quot;此处是拷贝person对象进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
    Person (Person&amp;amp;&amp;amp; p) : name(move(p.name)) { //没有变化
        cout &amp;lt;&amp;lt; &quot;此处是移动person对象进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好的，让我们测试一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    string names = &quot;miku&quot;;
    Person p1(names); //万能引用+forward兼顾左值和右值。
    Person p2(&quot;miku&quot;);//万能引用+forward兼顾左值和右值。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;真不错，一切都看起来如我们所愿。针对左值和右值的&lt;code&gt;string&lt;/code&gt;类，成功匹配到了万能引用的构造函数。&lt;/p&gt;
&lt;p&gt;注意这里在构建&lt;code&gt; p2&lt;/code&gt; 的时候并不会创建一个临时的 &lt;code&gt;string&lt;/code&gt; 对象：&lt;code&gt;T&lt;/code&gt;的类型不会退化，因为万能引用也是引用。所以&lt;code&gt;T&lt;/code&gt;会被推断为&lt;code&gt;const char[4]&lt;/code&gt;。但是将 &lt;code&gt;forward&amp;lt;T&amp;gt;&lt;/code&gt;用于指针参数没有太大意义。成员 &lt;code&gt;name &lt;/code&gt;将会被一个以&lt;code&gt;null&lt;/code&gt; 结尾的字符串构造。&lt;/p&gt;
&lt;p&gt;但是，当试图调用拷贝构造的时候，会遇到错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person p3(p1); //拷贝对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是调用移动构造却没有问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person p4(move(p1));//移动对象。输出此处是移动person对象进来 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且使用一个&lt;code&gt;const Person&lt;/code&gt;对象来（触发拷贝构造来）初始化也没问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Person cp1(&quot;miku&quot;);
Person cp2(cp1); //注意是使用const对象初始化，而不是自己是const对象
//输出：此处是拷贝person对象进来 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么原因是什么呢？问题出现在重载决议。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先我们知道两点：
&lt;ul&gt;
&lt;li&gt;如果函数模板可以实例化出一个比普通函数更完美的函数，那么会优先使用模板。只有所有情况都相同的时候，才会优先使用普通函数（1.5）&lt;/li&gt;
&lt;li&gt;在重载决议的图当中，我们看到了&lt;code&gt;qualification conversion&lt;/code&gt;是第三档。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以在这个时候，编译器发现通过构造函数模板实例化的函数会比拷贝构造更匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; 
Person(T&amp;amp;&amp;amp; n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，&lt;code&gt;T&lt;/code&gt;会被替换成&lt;code&gt;Person&amp;amp;&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;去杂记看函数模板推导。这里传入的参数是&lt;code&gt;Person&lt;/code&gt;，函数形参是&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;, 则模板参数T会被推导为&lt;code&gt;T&amp;amp;&lt;/code&gt;。随后引用折叠会变成函数形参为&lt;code&gt;Person&amp;amp;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这明显比 &lt;code&gt;const Person&amp;amp;&lt;/code&gt;更好，因为使用这个的话还需要进行&lt;code&gt;qualification conversion&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是问题在这里，我们的构造函数里面怎么写的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name(forward&amp;lt;T&amp;gt;(n));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们是尝试用&lt;code&gt;n&lt;/code&gt;去初始化&lt;code&gt;name&lt;/code&gt;。但是&lt;code&gt;name&lt;/code&gt;是&lt;code&gt;string&lt;/code&gt;，&lt;code&gt;n&lt;/code&gt;是&lt;code&gt;Person&lt;/code&gt;。你能用一个&lt;code&gt;Person&lt;/code&gt;对象初始化&lt;code&gt;string&lt;/code&gt;对象吗？肯定不行，这啥玩意呀。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以这时候，如果我们额外提供一个非&lt;code&gt;const&lt;/code&gt;的拷贝构造，那么没有问题&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Person (Person&amp;amp; p) : name(p.name) { //非const的拷贝构造
    cout &amp;lt;&amp;lt; &quot;此处是拷贝person对象进来, 非const的拷贝构造&quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
}
string names = &quot;miku&quot;;
Person p1(names); //拷贝字符串
Person p3(p1); //输出：此处是拷贝person对象进来, 非const的拷贝构造
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样做不够优雅，我们6.3会讲一些关于&lt;code&gt;enable_if&lt;/code&gt;的知识。然后我们在6.4会介绍如何搭配&lt;code&gt;enable_if&lt;/code&gt;来禁用一些模板&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无法为构造函数显式指定模板参数类型。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Person&amp;lt;int&amp;gt; f = Person&amp;lt;int&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如上是不可以的。因为&lt;code&gt;int&lt;/code&gt;被视为是&lt;code&gt;Person&lt;/code&gt;的模板参数类型而不是其构造函数的模板参数类型。构造函数的模板参数类型只能通过型别推导。这也是为什么模板构造函数非常适合搭配完美转发进行使用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;6.3 使用enable_if&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;enable_if&lt;/code&gt;可以让我们在某些&lt;strong&gt;编译期条件&lt;/strong&gt;下忽略掉函数模板。&lt;/p&gt;
&lt;p&gt;比如，如果函数模板&lt;code&gt;func&lt;/code&gt;的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
typename std::enable_if&amp;lt;(sizeof(T) &amp;gt; 4)&amp;gt;::type func() {
	//一些内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一模板定义会在 &lt;code&gt;sizeof(T) &amp;gt; 4&lt;/code&gt; 不成立的时候被忽略掉。如果&lt;code&gt; sizeof(T) &amp;gt; 4&lt;/code&gt; 成立，函数模板会展开成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func() {
    //一些内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;enable_if&lt;/code&gt;拥有第二个模板参数的时候，如果满足第一个参数的表达式，就会扩展成第二个模板参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
typename std::enable_if&amp;lt;(sizeof(T) &amp;gt; 4, MyType)&amp;gt;::type func() {
	//一些内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么在 &lt;code&gt;sizeof(T) &amp;gt; 4&lt;/code&gt;时，&lt;code&gt;enable_if &lt;/code&gt;会被扩展成其第二个模板参数。因此如果与&lt;code&gt;T&lt;/code&gt;对应的模板参数被推断为&lt;code&gt; MyType&lt;/code&gt;，而且其 &lt;code&gt;size&lt;/code&gt; 大于 4，那么其等效于：&lt;code&gt;MyType func();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;也就是说 &lt;code&gt;std::enable_if&amp;lt;&amp;gt;&lt;/code&gt;是一种类型萃取（type trait），它会根据一个作为其（第一个）模板参数的编译期表达式决定其行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果这个表达式结果为 true，它的 type 成员会返回一个类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果没有第二个模板参数，返回类型是 &lt;code&gt;void&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;否则，返回类型是其第二个参数的类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果表达式结果 false，则其成员类型是未定义的。根据模板的一个叫做 SFINAE（substitute failure is not an error，替换失败不是错误的规则，这会导致包含 &lt;code&gt;std::enable_if&amp;lt;&amp;gt;&lt;/code&gt;表达式的函数模板被忽略掉。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;简而言之，意思就是，前面的表达式为真，后面的类型定义才有意义。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单看一下原型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;bool, typename T=void&amp;gt;
struct enable_if {
};
template &amp;lt;typename T&amp;gt;
struct enable_if&amp;lt;true, T&amp;gt; { ///&amp;lt; 第一个模板参数为 true
  using type = T;           ///&amp;lt; type 才有定义
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般三种用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制函数返回类型&lt;/li&gt;
&lt;li&gt;校验函数模板参数类型&lt;/li&gt;
&lt;li&gt;类型偏特化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里就随便写个控制函数返回类型的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;int stat&amp;gt; //这里是非模板类型参数。上面提到了。注意一下
typename enable_if&amp;lt;stat == 1, bool&amp;gt;::type checkstate(){
    cout &amp;lt;&amp;lt;&quot;type is bool&quot; &amp;lt;&amp;lt; endl;
    return true;
}
template&amp;lt;int stat&amp;gt;
typename enable_if&amp;lt;stat == 0, int&amp;gt;::type checkstate(){
    cout &amp;lt;&amp;lt;&quot;type is int&quot; &amp;lt;&amp;lt; endl;
    return 5;
}

template&amp;lt;bool stat&amp;gt;			//bool也可以。隐式转换为整型了，但是必须要常量。
typename enable_if&amp;lt;stat == true, int&amp;gt;::type checkstate1(){
    cout &amp;lt;&amp;lt;&quot;type is int&quot; &amp;lt;&amp;lt; endl;
    return 5;
}

int main(){
    const int myobj = 1; 	//必须是const
    checkstate&amp;lt;myobj&amp;gt;();	//输出&quot;type is bool&quot; 
    checkstate&amp;lt;0&amp;gt;();		//输出&quot;type is int&quot; 
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意事项：在C++14前，不使用&lt;code&gt;enable_if_t&lt;/code&gt;的时候，返回类型前必须加&lt;code&gt;typename&lt;/code&gt;来告知&lt;code&gt;enable_if::type&lt;/code&gt;是个类型。还有就是非模板类型参数的限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;到了c++14，我们可以使用&lt;code&gt;enable_if_t&lt;/code&gt;来简化这一部分。我们无需在后面指定&lt;code&gt;::type&lt;/code&gt;，自然同时也无需在前面使用&lt;code&gt;typename&lt;/code&gt;。如这样：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;int stat&amp;gt;
enable_if_t&amp;lt;stat == 1, bool&amp;gt; anotherCheck(){ //没有::type 也没有typename
    cout &amp;lt;&amp;lt;&quot;type is bool&quot; &amp;lt;&amp;lt; endl;
    return true;
}
anotherCheck&amp;lt;1&amp;gt;();//一样使用
anotherCheck&amp;lt;0&amp;gt;(); //绝对不可以。就算有SFINAE,那是告诉你匹配不对的时候继续找，不报错。但是这里我们没写stat = 0情况，自然无论如何都找不到，重载决议找不到函数，调用一定会失败。所以报错。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;给enable_if 添加一个额外的带默认值的参数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;我们也可以给&lt;code&gt;enable_if_t&lt;/code&gt;的第二个参数加上一个默认参数。但是长得会和之前的不太一样。之前我们是直接把整个&lt;code&gt;enable_if_t&lt;/code&gt;或者是&lt;code&gt;typename enable_if::type &lt;/code&gt;当成函数返回值。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = std::enable_if_t&amp;lt;(sizeof(T) &amp;gt; 4)&amp;gt;&amp;gt;
T foo(T a) {
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    return a;
}
foo&amp;lt;double&amp;gt;(20.123);//没问题
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当然了，也可以使用非类型模板参数，像是这样：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;int value, typename = enable_if_t&amp;lt;value == 2&amp;gt;&amp;gt;
void myfunc(){
    cout &amp;lt;&amp;lt;&quot;2&quot; &amp;lt;&amp;lt; endl;
}
myfunc&amp;lt;2&amp;gt;();//也没问题
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;有一点需要澄清。这里的模板头如何解释？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们这里的第二个&lt;code&gt;typename&lt;/code&gt;是一个独立的模板参数，仅仅是没有加名字。因为我们没有使用到它。所以如果你愿意，你也可以这样写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;int value, typename whatever = enable_if_t&amp;lt;value == 2&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道了如果&lt;code&gt;enable_if_t&lt;/code&gt;的第一个表达式为真，则这里是通过的。又由于在当下这个例子，我们是非类型模板参数而且&lt;code&gt;enable_if_t&lt;/code&gt;没有给第二个参数，所以函数返回值就是&lt;code&gt;void&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果表达式为假，则会走SFINAE这一套。此处不会编译。针对当下的例子，如果没有做不等于&lt;code&gt;2&lt;/code&gt;的条件的处理，则会报错找不到对应的函数。&lt;/p&gt;
&lt;p&gt;但是这时候有个问题了，如果针对第一个函数，我们想让&lt;code&gt;sizeof(T) &amp;lt; 4&lt;/code&gt;也成为一个模板怎么办？我们可能会直接这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = std::enable_if&amp;lt;(sizeof(T) &amp;gt; 4)&amp;gt;&amp;gt;
T foo(T a) {
    cout &amp;lt;&amp;lt; &quot;&amp;gt;4&quot; &amp;lt;&amp;lt; endl;
    return a;
}
template&amp;lt;typename T, typename = std::enable_if&amp;lt;(sizeof(T) &amp;lt;= 4)&amp;gt;&amp;gt; //错误，重定义了。
T foo(T a) {
    cout &amp;lt;&amp;lt; &quot;&amp;lt;=4&quot; &amp;lt;&amp;lt; endl;
    return a;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样是错误的，提示重定义了。为什么？&lt;/p&gt;
&lt;h3&gt;enable_if 导致的的函数模板重定义问题&lt;/h3&gt;
&lt;p&gt;&lt;s&gt;我们知道了，enable_if 第二个参数的默认值是void（也就是表达式为真的时候，没有指定第二个参数就默认值是void）&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;所以当如果前面表达式为真的时候，这个函数会被展开成这个样子&lt;/s&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = std::enable_if_t&amp;lt;(sizeof(T) &amp;gt; 4)&amp;gt;&amp;gt;
void func() {
    
}
//展开后：
template&amp;lt;typename T, typename = void&amp;gt;
void func() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;s&gt;所以如果我们还有一个&lt;code&gt;&amp;lt;=4&lt;/code&gt;，当也为真的时候，那这个整个模板头的参数就都一样了。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;**函数模板参数的默认值的不同并不足以区分两个函数模板是重载关系。**官方是这么说的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;常见错误是声明二个函数模板，而它们仅于其默认模板实参相异。这是无效的，因为这些函数声明被当做同一函数模板的再声明（默认模板实参不为&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/function_template#.E5.87.BD.E6.95.B0.E6.A8.A1.E6.9D.BF.E9.87.8D.E8.BD.BD&quot;&gt;函数模板等价&lt;/a&gt;所考虑）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以。上面的代码。在编译器看来是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename&amp;gt;
T foo(T a);
template&amp;lt;typename T, typename&amp;gt;
T foo(T a);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就很明显是重定义了。因为我们只有第二个模板参数的默认值是不同的。但是默认值不在签名范围内。就好比不能通过默认值不同重载不同函数一样。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个详细例子&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T,
         typename = typename std::enable_if&amp;lt;std::is_same&amp;lt;int, T&amp;gt;::value&amp;gt;::type&amp;gt;
void g() { }

template&amp;lt;typename T,
         typename = typename std::enable_if&amp;lt;std::is_same&amp;lt;double, T&amp;gt;::value&amp;gt;::type&amp;gt;
void g() { }
template&amp;lt;typename T,
         typename std::enable_if&amp;lt;std::is_same&amp;lt;int, T&amp;gt;::value&amp;gt;::type* = nullptr&amp;gt;
void f() { }

template&amp;lt;typename T,
         typename std::enable_if&amp;lt;std::is_same&amp;lt;double, T&amp;gt;::value&amp;gt;::type* = nullptr&amp;gt;
void f() { }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;g()&lt;/code&gt;中 我们发现只有模板参数的默认值是依赖名，只有默认值依赖于前一个参数&lt;code&gt;T&lt;/code&gt;，但是默认值又不算做签名考虑在内，所以一定会冲突。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;f()&lt;/code&gt;中，我们发现第二个模板参数本身就是依赖名，因为里面的&lt;code&gt;T&lt;/code&gt;是依赖于第一个参数&lt;code&gt;T&lt;/code&gt;的。所以就成功区分了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么我们如何解决这个问题呢？我们可以让第二个参数本身不同即可。&lt;/p&gt;
&lt;p&gt;我们所谓的“让参数本身不同”的方法之一就是这个办法。另一种办法是20.3.2提到的添加一个额外的带默认值的模板参数。&lt;/p&gt;
&lt;p&gt;我们刚说过，上面那种的模板类型区分不开：&lt;code&gt;template&amp;lt;typename, typename&amp;gt; &lt;/code&gt;。那么我们应该这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typename&amp;lt;typename T, std::enable_if_t&amp;lt;condition&amp;gt;* whatever = nullptr&amp;gt;
//或老版本的
typename&amp;lt;typename T, typename std::enable_if&amp;lt;condition&amp;gt;::type* whatever = nullptr&amp;gt;
//老版本依旧必须有typename，为了告知type是个类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，编译器看到的模板类型是这样的：&lt;code&gt;template&amp;lt;typename, X*&amp;gt;&lt;/code&gt;。这里的&lt;code&gt;X*&lt;/code&gt;的类型依赖于第一个参数的类型。所以此时足以区分&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;whatever&lt;/code&gt;可以去掉。因为我们函数内并不使用它。&lt;/p&gt;
&lt;p&gt;详细例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T1, enable_if_t&amp;lt;(sizeof(T1) &amp;gt; 4)&amp;gt;* whatever = nullptr&amp;gt; //带t的新版
T1 functions(T1 a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot; &amp;lt;=4&quot; &amp;lt;&amp;lt; endl;
    return a;
}
template &amp;lt;typename T1, enable_if_t&amp;lt;(sizeof(T1) &amp;lt;= 4)&amp;gt;* whatever = nullptr&amp;gt;
T1 functions(T1 a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot; &amp;lt;=4&quot; &amp;lt;&amp;lt; endl;
    return a;
}



template &amp;lt;typename T1,  typename enable_if&amp;lt;(sizeof(T1) &amp;gt; 4)&amp;gt;::type* whatever = nullptr&amp;gt; //不带t的老版
T1 functions(T1 a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot; &amp;gt; 4&quot; &amp;lt;&amp;lt; endl;
    return a;
}
template &amp;lt;typename T1,  typename enable_if&amp;lt;(sizeof(T1) &amp;lt;= 4)&amp;gt;::type* whatever = nullptr&amp;gt;
T1 functions(T1 a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot; &amp;lt;= 4&quot; &amp;lt;&amp;lt; endl;
    return a;
}

functions&amp;lt;double&amp;gt;(10.123);
functions&amp;lt;int&amp;gt;(10);



//模板参数和非类型模板参数混用：
template &amp;lt;typename T, int T1,  enable_if_t&amp;lt;(T1 &amp;gt; 4)&amp;gt;* whatever = nullptr&amp;gt;
T functions(T a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot; &amp;gt; 4&quot; &amp;lt;&amp;lt; endl;
    return a;
}
template &amp;lt;typename T, int T1,  enable_if_t&amp;lt;(T1 &amp;lt;= 4)&amp;gt;* whatever = nullptr&amp;gt;
T functions(T a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot; &amp;lt;= 4&quot; &amp;lt;&amp;lt; endl;
    return a;
}
functions&amp;lt;double, 8&amp;gt;(10.123);
functions&amp;lt;string, 2&amp;gt;(&quot;12345&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然了，这时候我们也可以使用&lt;code&gt;using&lt;/code&gt;别名模板来让代码看起来更加简单。&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/52083873/c-sfinae-enable-if-t-in-member-function-how-to-disambiguate&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/31500426/why-does-enable-if-t-in-template-arguments-complains-about-redefinitions&lt;/p&gt;
&lt;h2&gt;6.4 使用enable_if 禁用某些模板&lt;/h2&gt;
&lt;p&gt;我们在6.2当中介绍了一个例子，在6.3当中学习了一下enable_if，这里我们就针对6.2当中的例子进行优化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6.2中我们想解决的问题是：如果传入的参数不能被转换为string，则不要使用其构造函数模板。
&lt;ul&gt;
&lt;li&gt;我们在6.2当中解决问题的方式是添加一个非&lt;code&gt;const&lt;/code&gt;的拷贝构造。我们在这里不这样做，而是使用&lt;code&gt;enable_if&lt;/code&gt;在某些条件下禁用构造函数模板。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是当传递的模板参数的类型不正确的时候（比如不是 &lt;code&gt;std::string&lt;/code&gt;或者可以转换成&lt;code&gt;std::string&lt;/code&gt;的类型），禁用如下构造函数模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
Person(T&amp;amp;&amp;amp; n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们搭配&lt;code&gt;is_convertiable&amp;lt;FROM, TO&amp;gt;&lt;/code&gt;来进行使用（此处看下面讲解）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, typename = std::enable_if_t&amp;lt;std::is_convertible_v&amp;lt;T,std::string&amp;gt;&amp;gt;&amp;gt;
Person(T&amp;amp;&amp;amp; n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果T可以被转换为string，则这个定义会被扩展为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = void&amp;gt;
Person(T&amp;amp;&amp;amp; n);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则这个函数模板会被忽略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以现在代码是这个样子 &lt;strong&gt;[c++17]&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Person
{
    private:
        string name;
    public:
    template &amp;lt;typename T, typename = std::enable_if_t&amp;lt;std::is_convertible_v&amp;lt;T,std::string&amp;gt;&amp;gt;&amp;gt;
    explicit Person(T&amp;amp;&amp;amp; n) : name(forward&amp;lt;T&amp;gt;(n)) {
        cout &amp;lt;&amp;lt; &quot;万能引用+forward兼顾左值和右值。&quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
    Person (Person const&amp;amp; p) : name(p.name) { //没有变化
        cout &amp;lt;&amp;lt; &quot;此处是拷贝person对象进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
    Person (Person&amp;amp;&amp;amp; p) : name(move(p.name)) { //没有变化
        cout &amp;lt;&amp;lt; &quot;此处是移动person对象进来 &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; endl;
    }
};
int main(){
    string names = &quot;miku&quot;;
    Person p1(names); //拷贝字符串 输出万能引用+forward兼顾左值和右值。
    Person p3(p1); //拷贝对象 输出此处是拷贝person对象进来。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然此处也可以使用别名模板简化，比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using EnableIfString = std::enable_if_t&amp;lt;std::is_convertible_v&amp;lt;T,std::string&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切皆大欢喜！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于&lt;strong&gt;c++14&lt;/strong&gt;没有给产生一个值的类型萃取定义带&lt;code&gt;_v&lt;/code&gt; 的别名，所以得这么写：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, typename = std::enable_if_t&amp;lt;std::is_convertible&amp;lt;T,std::string&amp;gt;::value&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于&lt;strong&gt;c++11&lt;/strong&gt;没有给产生一个类型的类型萃取定义带&lt;code&gt;_t&lt;/code&gt; 的别名，所以得这么写：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, typename = typename std::enable_if&amp;lt;std::is_convertible&amp;lt;T,std::string&amp;gt;::value&amp;gt;::type&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上都可以使用别名模板。&lt;/p&gt;
&lt;h2&gt;6.5 禁用某些特殊成员函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;注意我们不能通过使用 &lt;code&gt;enable_if&amp;lt;&amp;gt;&lt;/code&gt;来禁用 &lt;strong&gt;拷贝或移动 构造函数以及其赋值函数&lt;/strong&gt;。这是因为&lt;strong&gt;成员函数模板不会被算作拷贝构造或移动构造函数（依然会生成其默认版本）&lt;/strong&gt;，而且在需要使用这些函数的地方，相应的成员函数模板会被忽略掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以就算我们有这样的类模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class C {
	public:
		template&amp;lt;typename T&amp;gt;
		C (T const&amp;amp;) {
			std::cout &amp;lt;&amp;lt; &quot;tmpl copy constructor\n&quot;;
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在需要拷贝构造的时候，依然会调用到编译器合成的拷贝构造。&lt;/li&gt;
&lt;li&gt;删掉拷贝构造函数也不行，因为这样在需要拷贝构造函数的地方会报错说该函数被删除了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你硬要弄成模板的话，这么做：定义一个接受 &lt;code&gt;const volatile&lt;/code&gt; 的 拷贝构造函数并将其标示为&lt;code&gt;delete&lt;/code&gt;。这样做编译器就不会再隐式合成一个接受 &lt;code&gt;const&lt;/code&gt; 参数的拷贝构造函数。在此基础上，可以定义一个&lt;strong&gt;构造函数模板&lt;/strong&gt;，对于 non-volatile 的类型，它会优选被选择（相较于已删除的 copy 构造函数）&lt;strong&gt;注意这里是定义一个构造函数模板而不是拷贝构造函数模板。具体分析在最后“特殊成员函数能否是函数模板&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个东西叫做&lt;strong&gt;使用模板构造函数替代拷贝构造函数&lt;/strong&gt;。这样做是极度不推荐的！！！&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class C{
    public:
        int val;
        C() = default;

        C(int a):val(a){};

        C(const volatile C&amp;amp;) = delete;

        template&amp;lt;typename T&amp;gt;
        C(const T&amp;amp;){ //这是构造函数模板不是拷贝构造函数模板。
            cout &amp;lt;&amp;lt;&quot;template&quot; &amp;lt;&amp;lt; endl;
        }
};
int main(){
    C obj1(10);
    C obj2(obj1); //template
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意，构造函数除外。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.6 关于对整个类使用enable if&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;目前，类模板无法应用这个东西。针对于类模板，enable_if只能起到一种static_assert的作用。由于类和类模板不能重载，所以自然不存在候选集这个东西。也不存在SFINAE。enable_if在应用中的感觉是一种调整候选集的作用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在函数模板中，enable_if起到的作用是活用了SFINAE的特性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果硬要做static_assert是什么样的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = void&amp;gt; //主模板。注意这里。必须有默认值。但是类型需要判断一下
struct test{

};
template&amp;lt;typename T&amp;gt;
struct test&amp;lt;T, typename std::enable_if&amp;lt;(sizeof(T) &amp;lt;= 1)&amp;gt;::type&amp;gt;{ //偏特化。
    void func(){
        cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
    }
};
//template&amp;lt;typename T&amp;gt; 不可这么写
// struct test&amp;lt;T, typename std::enable_if&amp;lt;(sizeof(T) &amp;lt;= 1)&amp;gt;::type&amp;gt;{
//     void func(){
//         cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
//     }
// };
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一点。必须采用偏特化的形式。而且因为类模板没有重载所以不能写最下面那一组。只能要么主模板，要么一个偏特化版本。&lt;/li&gt;
&lt;li&gt;第二点：主模板的第二个对应了enable_if的模板参数必须有默认值为 为什么？
&lt;ul&gt;
&lt;li&gt;首先，我们主模板有了两个模板参数。我们如果没有默认值，则调用的时候比如&lt;code&gt;test&amp;lt;int&amp;gt; objs1;&lt;/code&gt;此时我们只显式指定了一个模板参数。类模板没法自动推导参数，那么第二个参数是空白的。编译不通过。&lt;/li&gt;
&lt;li&gt;其次，因为一旦enable_if的要求我们不能满足，在这里enable_if我们没有第二个参数，所以这个type会是void。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第三点：为什么默认值这里用了void，别的可以不？
&lt;ul&gt;
&lt;li&gt;我们要把默认值理解为：虽然我们不需要显式指定，但是它依旧存在。什么意思？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设我们现在把上面的默认值换为了&lt;code&gt;double&lt;/code&gt;。现在是这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = double&amp;gt;
struct test{
    void func(){
        cout &amp;lt;&amp;lt; &quot;default&quot; &amp;lt;&amp;lt; endl;
    }
};
template&amp;lt;typename T&amp;gt;
struct test&amp;lt;T, typename std::enable_if&amp;lt;(sizeof(T) &amp;lt;= 1)&amp;gt;::type&amp;gt;{
    void func(){
        cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
    }
};
int main() {
    test&amp;lt;char&amp;gt; objs1;
    objs1.func(); //输出default
    test&amp;lt;int&amp;gt; objs2;
    objs2.func(); //还是输出default
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么两个都是&lt;code&gt;default&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;我们思考一下刚才说的：我们要把默认值理解为：虽然我们不需要显式指定，但是它依旧存在。&lt;/p&gt;
&lt;p&gt;我们显式补齐参数类型来看一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test&amp;lt;char, double&amp;gt; objs1;
objs1.func();
test&amp;lt;int, double&amp;gt; objs2;
objs2.func();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们提到了：在这里&lt;code&gt;enable_if&lt;/code&gt;我们没有第二个参数，所以这个type会是&lt;code&gt;void&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以针对&lt;code&gt;objs1&lt;/code&gt;，编译时发现第二个条件满足，所以此时模板参数的第二个参数会是&lt;code&gt;void&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是我们的默认值是&lt;code&gt;double&lt;/code&gt;。相当于我们传入了&lt;code&gt;&amp;lt;char, double&amp;gt;&lt;/code&gt;。但是偏特化版本实例化出来的是&lt;code&gt;&amp;lt;char, void&amp;gt;&lt;/code&gt;。我们发现明显是默认版本符合参数要求。所以一定会调用&lt;code&gt;default&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时针对&lt;code&gt;obj2&lt;/code&gt;，编译时发现第二个条件不满足，但是发现主模板匹配。则匹配主模板。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时，如果我们显式的使用&lt;code&gt;&amp;lt;char, void&amp;gt;&lt;/code&gt;，则可以正确满足条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test&amp;lt;char, void&amp;gt; objs3; // called
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候，我们显式指定的值会覆盖掉默认值。此时第二个参数是&lt;code&gt;void&lt;/code&gt;。而针对于偏特化版本&lt;code&gt;enable_if&lt;/code&gt;推断的第二个参数也是&lt;code&gt;void&lt;/code&gt;。此时明显偏特化版本符合。所以这时候会调用偏特化。&lt;/p&gt;
&lt;p&gt;所以按理说，如果我们把特化版本的&lt;code&gt;enable_if&lt;/code&gt;，让他的第二个参数和主模板的类型默认值相等，这样做就可以。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename = double&amp;gt;
struct test{
    void func(){
        cout &amp;lt;&amp;lt; &quot;default&quot; &amp;lt;&amp;lt; endl;
    }
};
template&amp;lt;typename T&amp;gt;
struct test&amp;lt;T, typename std::enable_if&amp;lt;(sizeof(T) &amp;lt;= 1), double&amp;gt;::type&amp;gt;{ //第二个参数和默认值相等
    void func(){
        cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
    }
};
test&amp;lt;char&amp;gt; objs1;
objs1.func();
test&amp;lt;int&amp;gt; objs2;
objs2.func();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此时针对&lt;code&gt;obj1&lt;/code&gt;， 编译时发现第二个条件满足，且有第二个参数。所以此时模板参数的第二个参数会是&lt;code&gt;double&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;又因为默认值是&lt;code&gt;double&lt;/code&gt;，但是类模板的调用顺序是 全特化类&amp;gt;偏特化类&amp;gt;主模板类。这个时候主模板和偏特化类都符合，则优先调用偏特化类。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时针对&lt;code&gt;obj2&lt;/code&gt;，编译时发现第二个条件不满足，但是发现主模板匹配。则匹配主模板。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/48045559/how-do-i-declare-sfinae-class&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/16972684/how-to-use-enable-if-for-restricting-a-whole-class&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/75154649/why-when-use-enable-if-in-class-tempalte-have-to-set-the-second-parameters-defa/75154785?noredirect=1#comment132622904_75154785&lt;/p&gt;
&lt;h1&gt;第七章 到底是按值传递还是按引用传递？&lt;/h1&gt;
&lt;p&gt;一般来说，我们在使用引用传递的时候，会有三种情况。&lt;/p&gt;
&lt;p&gt;虽然已经是老生常谈，但是这里还是写一下做一下recall。下面的&lt;code&gt;X&lt;/code&gt;指的是具体类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;按照常量左值引用传递&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const X&amp;amp;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;由于什么类型的参数都可以接。包括右值。但是不可修改参数。所以主要目的是防止拷贝，并且接收任意类型数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按照左值引用传递&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X&amp;amp;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;主要目的是为了更改参数，并且防止拷贝。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按照右值引用传递&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;X&amp;amp;&amp;amp;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主要是为了接收形参为右值的参数。参数可以被更改或被窃取。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//更改的例子
//注意不能直接写，要写完美转发。因为具名右值是左值。
void func(int&amp;amp;&amp;amp; a){
    a = 200;
}

template&amp;lt;typename T&amp;gt;
void capsule(T&amp;amp;&amp;amp; c){
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl; //20
    func(forward&amp;lt;T&amp;gt;(c));
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;//200
}
int main(){
    capsule(20);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一般来说，在如下情况下我们需要使用引用传递参数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对象不允许被拷贝的时候&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;入参需要被修改的时候（参数被用于返回数据）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数及其所有属性需要被模板转发到别的地方的时候。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝开销极大，所以使用引用传递可以获得明显的性能提升之时。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;其余情况可以使用值传递。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;7.1 按值传递&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;有关临时对象，隐式类型转换的笔记在杂记2&lt;/p&gt;
&lt;p&gt;有关各种拷贝构造相关的在杂记&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们先来简单看看按值传递的情况&lt;/p&gt;
&lt;p&gt;当按值传递参数时，原则上所有的参数都会被拷贝。因此每一个参数都会是被传递实参的一份拷贝。对于 &lt;code&gt;T&lt;/code&gt;类的对象，参数会通过 &lt;code&gt;T&lt;/code&gt;类的拷贝构造函数来做初始化。&lt;/p&gt;
&lt;p&gt;事实上，编译器自己可以通过移动语义（move semantics）来优化掉对象的拷贝，这样即使是对复杂类型的拷贝，其成本也不会很高。&lt;/p&gt;
&lt;p&gt;假设我们有如下函数模板:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T obj){
    //一些内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我们应用于&lt;code&gt;myobj&lt;/code&gt;类型参数后，实例化后的代码为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func(myobj obj){
    //一些内容
}

myobj v1(10);
func(v1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传递&lt;code&gt;myobj&lt;/code&gt;类型的时候，&lt;code&gt;obj&lt;/code&gt;会是传入参数v1的拷贝。如果&lt;code&gt;myobj&lt;/code&gt;类型对象的拷贝的成本非常高，那么会很痛苦。但并不是所有情况都会调用拷贝构造&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//假设myobj类对象提供了全部6个特殊成员函数，所以

myobj a(20); //构造

func(a); //拷贝构造

func(myobj(20)); //构造 注意，只有一次构造。

func(move(a)); //移动构造
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在第一次调用中，被传递的参数是左值（lvalue)，因此拷贝构造函数会被调用。&lt;/p&gt;
&lt;p&gt;但是在第二次调用中，被传递的参数是纯右值 （prvalue 多在临时对象或者某个函数的返回值)，此时编译器会优化参数传递，使得拷贝构造函数不会被调用。&lt;strong&gt;从C++17开始，C++标准要求这一优化方案必须被实现。在C++17之前，如果编译器没有优化掉这一类拷贝，它至少应该先尝试使用移动语义，这通常也会使拷贝成本变得比较低廉&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编译器会尝试拷贝优化，如果不行，会尝试移动语义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在最后一次调用中，被传递参数是将亡值（xvalue ，一个使用了&lt;code&gt;move()&lt;/code&gt;的已经存在的对象), 这会通过告知编译器我们不再需要&lt;code&gt;a&lt;/code&gt;的值。通过这种方式，我们强制调用移动构造。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NRV优化在杂记2。复制省略在杂记3&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;再次重申按照值传递会导致参数类型退化。&lt;code&gt;auto&lt;/code&gt;也会退化。(杂记3)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7.2 按引用传递&lt;/h2&gt;
&lt;h3&gt;7.2.1 按常量左值引用传递 &lt;code&gt;const&amp;amp;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;为了避免（不必要的）拷贝，在传递非临时对象作为参数时，可以使用&lt;code&gt;const&lt;/code&gt;引用传递。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;比如这个函数模板就永远不会拷贝被传递对象&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void func(const T&amp;amp;){
    //一些内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内置类型通过引用传递不会提升性能。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之所以不能提高性能，&lt;strong&gt;是因为在底层实现上，按引用传递还是通过传递参数的地址实现的。地址会被简单编码，这样可以提高从调用者向被调用者传递地址的效率。不过按地址传递可能会使编译器在编译调用者的代码时有一些困惑：被调用者会怎么处理这个地址？理&lt;/strong&gt;
**论上被调用者可以随意更改该地址指向的内容。这样编译器就要假设在这次调用之后，所有缓存在寄存器中的值可能都会变为无效。**而重新载入这些变量的值可能会很耗时（可能比拷贝对象的成本高很多）。你或许会问在按 &lt;code&gt;const&lt;/code&gt; 引用传递参数时：为什么编译器不能推断出
被调用者不会改变参数的值？不幸的是，确实不能，&lt;strong&gt;因为调用者可能会通过它自己的非&lt;code&gt; const&lt;/code&gt;引用修改被引用对象的值（这个解释太好，另一种情况是被调用者可以通过&lt;code&gt;const_cast&lt;/code&gt;移除参数中的&lt;code&gt; const&lt;/code&gt;）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;inline可能会对此情况有所优化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按照引用传递不会引发类型退化。&lt;strong&gt;也就是说数组不退化为指针，也不会移除CV限定。但是要注意函数模板参数类型。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt;的类型推断不会是一个&lt;code&gt;const&lt;/code&gt;类型，因为&lt;code&gt;const&lt;/code&gt;已经是函数参数的一部分了，所以不会是模板参数类型的一部分。这里搭配1.2和函数模板参数推导看。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void f(T&amp;amp; param) { //注意这里是T&amp;amp; 
    std::puts(__PRETTY_FUNCTION__);
}

int main() {
    const int p = 2;
    f(p); //T的类型为const int。很好理解。因为引用不会去除cv
    //所以在f函数内使用T声明的变量自然会带const
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是如果是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void f(const T&amp;amp; param) { //注意这里是const T&amp;amp;
    std::puts(__PRETTY_FUNCTION__);
}

int main() {
    const int p = 2; //T的类型为int。也很好理解。因为const已经被函数模板参数匹配了。T自然就剩下了int。
    //所以在f函数内使用T声明的变量自然不会带const
    f(p);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.2.2 按照普通（左值/右值）引用传递&lt;/h3&gt;
&lt;p&gt;如果想通过调用参数来返回变量值（比如修改被传递变量的值），就需要使用非 &lt;code&gt;const&lt;/code&gt; 引用（要么就使用指针）。同样这时候也不会拷贝被传递的参数。被调用的函数模板可以直接访问被传递的参数。&lt;/p&gt;
&lt;p&gt;假如我们有如下函数模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func (T&amp;amp; arg) { 
    //一些内容
}
int main() {
    string s = &quot;miku&quot;;
    func(s); 
    func(string(&quot;miku&quot;));//不可以。左值引用不可绑定右值
    func(move(s));//不可以。左值引用不可绑定右值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;左值引用不可绑定右值（废话）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;但是有个问题。如果传入的参数是&lt;code&gt;const&lt;/code&gt;的。则在一些情况下参数类型会被推导为&lt;code&gt;const&lt;/code&gt;引用。也就是这时候可以传递右值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;原因：&lt;code&gt;const&lt;/code&gt;引用的初始化方式：在初始化常量引用时允许用任意表达式作为初始值，只要该表达式的结果能转换成引用的类型即可。----《C++ Primer》第五版2.4.1第55页&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这就是为什么常量左值引用可以接任何类型参数。&lt;/li&gt;
&lt;li&gt;也就是你可以这样：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const string&amp;amp; a = string(&quot;abc&quot;);
string s = &quot;miku&quot;;
const string&amp;amp; b = move(s); //注意这里并不调用移动构造或移动赋值。因为并没有构造对象或者是进行赋值。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以说一旦我们把东西换成这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func (T&amp;amp; arg) { 
    //一些内容
}
int main() {
    const string s = &quot;miku&quot;; //换成const
    func(s); 
    func(string(&quot;miku&quot;));//不可以。左值引用不可绑定右值
    func(move(s));//可以。T被推导为const string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;**在这种情况下，在函数模板内部，任何试图更改被传递参数的值的行为都是错误的。**所以我们可以使用&lt;code&gt;enable_if&lt;/code&gt;或&lt;code&gt;static_assert&lt;/code&gt;搭配&lt;code&gt;is_const&lt;/code&gt;来禁止向非&lt;code&gt;const&lt;/code&gt;应用传递&lt;code&gt;const&lt;/code&gt;对象&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//使用static_assert
template&amp;lt;typename T&amp;gt;
void func (T&amp;amp; arg) { 
    static_assert(!is_const&amp;lt;T&amp;gt;::value, &quot;can&apos;t use const type variable&quot;);
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
}

//使用enable_if
template&amp;lt;typename T, typename = enable_if_t&amp;lt;!is_const&amp;lt;T&amp;gt;::value&amp;gt;&amp;gt;
void func (T&amp;amp; arg) { 
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.2.3 按照万能引用传递 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;在杂记中我们详细说明了万能引用和引用折叠的部分。这里就不赘述了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;唯一要再次提醒的是，万能引用是唯一一种可能把&lt;code&gt;T&lt;/code&gt;推导为引用类型的情况。因为有引用折叠。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void f(T&amp;amp;&amp;amp; param) {
    std::puts(__PRETTY_FUNCTION__);
}


int s =5;
int&amp;amp; ref = s;
const int&amp;amp; ref2 = s;
f(5); //T推导为 int
f(ref);//T推导为 int&amp;amp;
f(ref2);//T推导为 const int&amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在第二次调用中，如果在模板内部直接用 T 声明一个未初始化的局部变量，就会触发一个错误（引用对象在创建的时候必须被初始化）所以需要格外的注意。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7.3 使用ref和cref&lt;/h2&gt;
&lt;p&gt;看下面的ref和cref介绍即可。&lt;/p&gt;
&lt;h2&gt;7.5 处理返回值。&lt;/h2&gt;
&lt;p&gt;返回值也可以被按引用或者按值返回。但是按引用返回可能会带来一些麻烦，因为它所引用的对象不能被很好的控制。不过在日常编程中，也有一些情况更倾向于按引用返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回容器或者字符串中的元素（比如通过&lt;code&gt;[]&lt;/code&gt;运算符或者 &lt;code&gt;front()&lt;/code&gt;方法访问元素）---EFFSTL笔记提到过。&lt;/li&gt;
&lt;li&gt;允许修改类对象的成员&lt;/li&gt;
&lt;li&gt;为链式调用返回一个对象（比如&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;运算符以及&lt;strong&gt;赋值运算符&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;但是使用不当可能会造成悬空引用。因此如何保证函数模板可以采用按值返回是一个问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在某些情况下，尤指万能引用的时候，&lt;code&gt;T&lt;/code&gt;会被隐式推断为引用类型&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T retR(T&amp;amp;&amp;amp; p){ // p 是万能引用
	return T{…}; // 一旦传入的T类型是左值或左值引用，则T会被统一推导为引用类型。此时T就变成引用了。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;即使函数模板被声明为按值传递，也可以显式地将 &lt;code&gt;T&lt;/code&gt; 指定为引用类型：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T retV(T p){  //T完全可以被指定为引用类型 比如：
	return T{…}; 
}
int x;
retV&amp;lt;int&amp;amp;&amp;gt;(x); // 在这里，显式指定这个函数模板的模板参数T为int&amp;amp;。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;所以，解决方案有两个。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个：使用类型萃取的&lt;code&gt;remove_reference&lt;/code&gt;去掉&lt;code&gt;T&lt;/code&gt;类型的引用。（下面有提到该部分）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;decay&lt;/code&gt;也可以。目前还没有看到这一部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
typename remove_reference&amp;lt;T&amp;gt;::type retV(T p){
	return T{…}; // T一定会被推导为非引用类型
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第二个：声明返回值为&lt;code&gt;auto&lt;/code&gt;类型。&lt;code&gt;auto&lt;/code&gt;类型会导致类型退化，也就是隐式去掉类型的引用。杂记3中提到过。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
auto retV(T p){//返回值设置为auto
	return T{…}; //一定会被推导为非引用类型。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7.6 应该如何声明模板参数？&lt;/h2&gt;
&lt;p&gt;我们在拷贝构造一章中详细分析了如何构建构造函数和几种方法的优劣。这里谈一谈和模板参数结合的情况。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;将参数声明成按值传递：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这一方法很简单，它会对字符串常量和裸数组的类型进行退化，但是对比较大的对象可能会受影响性能。在这种情况下，调用者仍然可以通过 &lt;code&gt;cref()&lt;/code&gt;和 &lt;code&gt;ref()&lt;/code&gt;按引用传递参数，但是要确保这一用法是有效的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;将参数声明成按引用传递：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;对于比较大的对象这一方法能够提供比较好的性能。尤其是在下面几种情况下：
&lt;ul&gt;
&lt;li&gt;将已经存在的对象（&lt;code&gt;lvalue&lt;/code&gt;）按照左值引用传递，&lt;/li&gt;
&lt;li&gt;将临时对象（&lt;code&gt;prvalue&lt;/code&gt;）或者被 &lt;code&gt;move()&lt;/code&gt;转换为可移动的对象（&lt;code&gt;xvalue&lt;/code&gt;）按右值引用传递，&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;或者是将以上几种类型的对象按照万能引用+完美转发传递。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;由于这几种情况下参数类型都不会退化，因此在传递字符串常量和裸数组时要格外小心。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于万能引用，需要意识到模板参数可能会被隐式推断为引用类型（引用折叠）。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上所述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认情况下，将参数声明为按值传递。这样做比较简单，即使对字符串常量也可以正常工作。对于比较小的对象、临时对象以及可移动对象，其性能也还不错。对于比较大的对象，为了避免成本高昂的拷贝，可以使用 &lt;code&gt;ref()&lt;/code&gt;和&lt;code&gt; cref()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果需要一个参数用于输出，或者即用于输入也用于输出，那么就将这个参数按非&lt;code&gt;const&lt;/code&gt; 引用传递。但是需要按照 7.2.2 节介绍的方法禁止其接受 &lt;code&gt;const&lt;/code&gt; 对象。因为针对普通引用类型为形参，如果传入一个&lt;code&gt;const&lt;/code&gt;引用则会带着其形参推导为&lt;code&gt;const&lt;/code&gt;引用。&lt;/li&gt;
&lt;li&gt;**如果使用模板是为了转发它的参数，那么就使用完美转发。**也就是将参数声明为万能引用并在合适的地方使用 &lt;code&gt;forward&amp;lt;&amp;gt;()&lt;/code&gt;。考虑使用&lt;code&gt;decay&amp;lt;&amp;gt;&lt;/code&gt;或者 &lt;code&gt;common_type&amp;lt;&amp;gt;&lt;/code&gt;来处理不同的字符串常量类型以及裸数组类型的情况。&lt;/li&gt;
&lt;li&gt;如果重点考虑程序性能，而参数拷贝的成本又很高，那么就使用&lt;code&gt; const&lt;/code&gt; 引用。不过如果最终还是要对对象进行局部拷贝的话，这一条建议不适用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7.7 不要过分泛型化&lt;/h2&gt;
&lt;p&gt;通常来说，我们在使用函数模板的时候，不可能允许该函数传递任意类型的对象。因为针对不同的对象我们会有不同的操作。比如我们可能已经知道函数模板的参数只会是某些类型的&lt;code&gt; vector&lt;/code&gt;。这时候最好不要将该函数模板定义的过于泛型化，否则，可能会有一些令人意外的副作用。针对这种情况应该使用如下的方式定义模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void printVector (const vector&amp;lt;T&amp;gt;&amp;amp; v){ //为啥不是T v?
    //一些内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为啥我们不直接&lt;code&gt;T v&lt;/code&gt;？
&lt;ul&gt;
&lt;li&gt;首先，我们可以确定&lt;code&gt;T&lt;/code&gt;不可能是引用类型。因为传入的参数必定是一个&lt;code&gt;vector&lt;/code&gt;，变动的只是&lt;code&gt;vector&lt;/code&gt;存储的元素类型。因为容器不能使用引用作为其元素类型。&lt;/li&gt;
&lt;li&gt;其次，通常来说，针对容器我们会使用引用传递。因为不必要的拷贝会极大降低性能。甚至针对不修改的情况我们要使用&lt;code&gt;const&lt;/code&gt;引用传递。&lt;/li&gt;
&lt;li&gt;最后，使用这种不过分泛型化的参数传递可以很容易的让其他人看出来我们这个函数的作用：它会干什么，它不会干什么。如果我们直接传入一个&lt;code&gt;T&lt;/code&gt;，则会非常不清晰。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第八章 编译期编程&lt;/h1&gt;
&lt;h2&gt;8.4 SFIANE&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;在一个函数调用的备选方案中包含函数模板时，编译器首先要决定应该将什么样的模板参数用于各种模板方案，然后用这些参数替换函数模板的参数列表以及返回类型，最后评估替换后的函数模板和这个调用的匹配情况（就像常规函数一样）。但是这一替换过程可能会遇到问题：&lt;strong&gt;替换产生的结果可能没有意义。不过这一类型的替换不会导致错误，C++语言规则要求忽略掉这一类型的替换结果。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是上面讲到的替换过程和实际的实例化过程不一样：&lt;strong&gt;即使对那些最终被证明不需要被实例化的模板也要进行替换（不然就无法知道到底需不需要实例化）&lt;/strong&gt;。&lt;strong&gt;不过它只会替换直接出现在函数模板声明中的相关内容（不包含函数体）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们有下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, unsigned N&amp;gt;
std::size_t len (T(&amp;amp;)[N]){ //这个版本是为没有size_type成员的对象准备的。
    cout &amp;lt;&amp;lt;&quot;no size&quot; &amp;lt;&amp;lt; endl;
    return N;
}

template&amp;lt;typename T&amp;gt;
typename T::size_type len (T const&amp;amp; t){ //这个版本是为有size_type成员的对象准备的。
    cout &amp;lt;&amp;lt;&quot;size&quot; &amp;lt;&amp;lt; endl;
    return t.size(); //同时要求必须有size成员函数
}

std::size_t len (...) //使用可变参数来对其他任何没有size_type成员 !!!!!!注意这里没有模板头
{
    cout &amp;lt;&amp;lt;&quot;other&quot; &amp;lt;&amp;lt; endl;
    return 0;
}

int main(){
    int a[10];
    len(a); // OK 原始数组没有size_type
    len(&quot;tmp&quot;); //OK: 字符串没有size_type
    std::vector&amp;lt;int&amp;gt; v;
    len(v); // OK: vector有size_type
    int* p;
    len(p); //OK: 指针类型全都不匹配。走最下面的应急类型。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;针对原始数组和字符串。因为他俩没有&lt;code&gt;size_type&lt;/code&gt;所以会忽略第二个函数模板&lt;/li&gt;
&lt;li&gt;针对&lt;code&gt;vector&lt;/code&gt;，因为有&lt;code&gt;size_type&lt;/code&gt;所以匹配第二个。
&lt;ul&gt;
&lt;li&gt;注意，如果有一些类型有&lt;code&gt;size_type&lt;/code&gt;，但是由于我们在第二个函数模板中使用了&lt;code&gt;size&lt;/code&gt;成员函数。如果类型没有这个&lt;code&gt;size&lt;/code&gt;成员函数，会报错说缺少&lt;code&gt;size&lt;/code&gt;成员函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;针对指针，啥都匹配不了，只能匹配第三个。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SFIANE 和重载解析：&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;除非在某些情况下，该模板不应该参与重载解析过程&lt;/strong&gt; 的意思就是在该情况下，使用 SFINAE 方法 SFINAE 掉了这个函数模板&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们在6.2，6.4和6.5详细说明了某些特殊成员函数模板（尤指构造函数模板）在一些情况下会导致一些错误的匹配。所以我们需要使用&lt;code&gt;enable_if&lt;/code&gt;让其在某些特殊情况下禁用这个函数模板。也就是在某些情况下（&lt;code&gt;enable_if&lt;/code&gt;不满足的条件下），让这个函数退出重载候选集。&lt;/p&gt;
&lt;h2&gt;SFINAE 需要什么条件？&lt;/h2&gt;
&lt;p&gt;SFINAE特性的激活需要三个条件发生：替换/推导语境，失败发生和其他可行选项。&lt;/p&gt;
&lt;p&gt;连起来一句话就是：当模板形参在替换/推导中失败时，从重载集中丢弃这个特化，转而寻找其他可行选项，而非导致编译失败。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;替换失败不是错误 (Substitution Failure Is Not An Error)
在函数模板的重载决议中会应用此规则：当模板形参在&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/function_template#.E6.A8.A1.E6.9D.BF.E5.AE.9E.E5.8F.82.E6.9B.BF.E6.8D.A2&quot;&gt;替换&lt;/a&gt;成显式指定的类型或&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/template_argument_deduction&quot;&gt;推导出的类型&lt;/a&gt;失败时，从&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/overload_resolution&quot;&gt;重载集&lt;/a&gt;中丢弃这个特化，而非导致编译失败。&lt;/p&gt;
&lt;p&gt;....&lt;/p&gt;
&lt;p&gt;以上类型或表达式在以用来&lt;strong&gt;替换的实参写出时谬构&lt;/strong&gt;（并带有必要的诊断）的场合是&lt;em&gt;替换失败&lt;/em&gt;。替换以词法序进行，并在遇到失败时终止。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;替换中出现错误，是替换失败。&lt;/li&gt;
&lt;li&gt;不是替换场合就只是失败。&lt;/li&gt;
&lt;li&gt;是替换场合但是没发生错误就只是替换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子请查看modern C++ design的2.5&lt;/p&gt;
&lt;h2&gt;8.4.1 使用decltype 和 SFIANE搭配解决我们上一部分的问题&lt;/h2&gt;
&lt;p&gt;首先说明，这一部分在decltype和逗号表达式的部分理解不透彻，而且属于奇技淫巧，非常复杂。我会把所有找得到的相关资料贴在这里。&lt;/p&gt;
&lt;p&gt;我们之前一节说道：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，如果有一些类型有&lt;code&gt;size_type&lt;/code&gt;，但是由于我们在第二个函数模板中使用了&lt;code&gt;size&lt;/code&gt;成员函数。如果类型没有这个&lt;code&gt;size&lt;/code&gt;成员函数，会报错说缺少&lt;code&gt;size&lt;/code&gt;成员函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题还是挺痛苦的。我们如果没有在在函数声明中以某种方式要求 &lt;code&gt;size()&lt;/code&gt;成员函数必须存在，这个函数模板就会被选择并在实例化过程中发生错误。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;处理这一情况有一种常用模式或者说习惯用法：
&lt;ul&gt;
&lt;li&gt;通过尾置返回类型语法（trailing return type syntax）来指定返回类型（在函数名前使用auto，并在函数名后面的-&amp;gt;后指定返回类型）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通过&lt;code&gt; decltype&lt;/code&gt; 和逗号运算符定义返回类型。将所有需要成立的表达式放在逗号运算符的前面（为了预防可能会发生的运算符被重载的情况，需要将这些表达式的类型转换为 void）。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在逗号运算符的末尾定义一个类型为返回类型的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
auto len (T const&amp;amp; t) -&amp;gt; decltype( (void)(t.size()), typename T::size_type()){
	return t.size();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型指示符 &lt;code&gt;decltype&lt;/code&gt; 的操作数是一组用逗号隔开的表达式，因此最后一个表达式 &lt;code&gt;T::size_type()&lt;/code&gt;会产生一个类型为返回类型的对象（&lt;code&gt;decltype&lt;/code&gt; 会将其转换为返回类型）。而在最后一个逗号前面的所有表达式都必须成立，在这个例子中逗号前面只有 &lt;code&gt;t.size()&lt;/code&gt;。&lt;strong&gt;之所以将其类型转换为&lt;code&gt;void&lt;/code&gt;，是为了避免因为用户重载了该表达式对应类型的逗号运算符而导致的不确定性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;typename T::size_type()&lt;/code&gt;为啥要这么写？
&lt;ul&gt;
&lt;li&gt;首先，我们希望&lt;code&gt;size_type&lt;/code&gt;是一个类型。但是必须要加&lt;code&gt;typename&lt;/code&gt;指明&lt;code&gt;T::size_type&lt;/code&gt;是个类型。其次，我们对一个类型使用&lt;code&gt;()&lt;/code&gt;就创建了一个这个类型的匿名对象。这是一个表达式。这个表达式是返回类类型纯右值的函数调用（说人话就是类型名+()生成匿名临时对象，这个对象的类型自然是类类型。临时对象又是纯右值），所以推导出来的类型就是它本身的类型。就好比&lt;code&gt;decltype(int())&lt;/code&gt;等于&lt;code&gt;int&lt;/code&gt;一样。&lt;/li&gt;
&lt;li&gt;为啥要放对象不能放类型？因为你不能&lt;code&gt;decltype(int)&lt;/code&gt;。我们是通过一个实体或表达式去推导出类型。而不是通过一个类型推导出一个类型。&lt;code&gt;T::size_type&lt;/code&gt;是个类型所以不可以。我们唯一能做的是使用&lt;code&gt;T::size_type&lt;/code&gt;这个类型实例化出一个这个类型的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一部分转换的粗浅理解 （其实是我理解错了，但是也还是有用信息）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，根据N4140标准的13.3.1.2/9 [over.match.oper]部分：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;对于&lt;code&gt; operator,&lt;/code&gt;、一元 &lt;code&gt;operator&amp;amp; &lt;/code&gt;和 &lt;code&gt;operator-&amp;gt;&lt;/code&gt;，如果候选函数集中没有可行函数（见后述），那么将运算符解释为内建运算符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;出自&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/overload_resolution&quot;&gt;这里&lt;/a&gt; 和 &lt;a href=&quot;https://stackoverflow.com/questions/39514765/the-void-the-comma-operator-operator-and-the-impossible-overloading&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其次，&lt;code&gt;void()&lt;/code&gt;是一个表达式。它产生一个类型为纯右值的&lt;code&gt;void&lt;/code&gt;。但是它是一个不完整类型。出自&lt;a href=&quot;https://stackoverflow.com/questions/59956374/is-void-a-valid-c-expression&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;关于decltype的更多故事
&lt;ul&gt;
&lt;li&gt;https://stackoverflow.com/questions/16044514/what-is-decltype-with-two-arguments&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/69366618/the-full-story-about-the-decltype-comma-trick&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/28837332/sfinae-static-castvoid-or-void&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/14003366/what-does-the-void-in-auto-fparams-decltype-void-do#comment19335347_14003374&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/11775639/how-is-type-deduced-from-auto-return-type&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/39279074/what-does-the-void-in-decltypevoid-mean-exactly&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/4031228/why-is-operator-void-not-invoked-with-cast-syntax&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/69314599/casting-to-void-to-avoid-use-of-overloaded-user-defined-comma-operator&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终我的核心理解为何要转换为void？答案是**使用它来确认该函数具有返回值，我们已断言忽略它是安全的。也可以不使用c风格转换专用static_cast。**出自&lt;a href=&quot;https://stackoverflow.com/questions/689677/why-cast-unused-return-values-to-void/689688#689688&quot;&gt;这里&lt;/a&gt;和&lt;a href=&quot;https://stackoverflow.com/questions/34288844/what-does-casting-to-void-really-do&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;转换为&lt;code&gt;void&lt;/code&gt;后，表达式的值被丢弃。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如换成这样c++风格：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
auto len (T const&amp;amp; t) -&amp;gt; decltype(static_cast&amp;lt;void&amp;gt;(t.size()), typename T::size_type() ){
    return t.size();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以说这里转换为void的目的是：先判断&lt;code&gt;t.size()&lt;/code&gt;这个表达式是否成立，也就是这个函数调用是否能成功，也就是&lt;code&gt;t&lt;/code&gt;是否有这个成员函数。因为我们不关心返回值，所以转成&lt;code&gt;void&lt;/code&gt;。&lt;code&gt;decltype&lt;/code&gt;可以在第一层接受如&lt;code&gt;void&lt;/code&gt;这种不完整类型。其次，就算有人重载了&lt;code&gt;operator,()&lt;/code&gt;做了一些乱七八糟的事情，由于左侧是空，所以也不会做一些什么奇怪的举动。逗号运算符只保留最右侧的作为结果。所以返回值最后还会是&lt;code&gt;T::size_type()&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;他防止的是comma的重载定义在&lt;code&gt;t.size()&lt;/code&gt;的返回值类型上，因此添加到void的转换，使得这个返回值被丢弃，不能被利用，所以就不触发comma的重载 ----萧叶轩大佬的回答&lt;/p&gt;
&lt;p&gt;和&lt;a href=&quot;https://stackoverflow.com/questions/69314599/casting-to-void-to-avoid-use-of-overloaded-user-defined-comma-operator&quot;&gt;这个&lt;/a&gt;很相似&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们之前链接里有，&lt;code&gt;operator void()&lt;/code&gt;不会被覆盖&lt;a href=&quot;https://stackoverflow.com/questions/4031228/why-is-operator-void-not-invoked-with-cast-syntax&quot;&gt;看这里&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;假设我们有个这样的残疾类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct foo {
    struct size_type {
        bool operator,(size_type) { return false;}
    };
    size_type size() { return {};}  
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它确实有一个&lt;code&gt;size_type&lt;/code&gt;并且它确实有一个&lt;code&gt;size()&lt;/code&gt;函数。但是，如果没有转换为&lt;code&gt;void&lt;/code&gt;，模板不会推断出正确的返回类型，因为&lt;code&gt;decltype( (t.size()), typename T::size_type() )&lt;/code&gt;是&lt;code&gt;bool&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;decltype( (t.size()), typename T::size_type());//等价于下面
decltype((t.size()).operator,(typename T::size_type())); //这个会返回一个false。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换成&lt;code&gt;void&lt;/code&gt;之后，也就是前面的&lt;code&gt;t.size()&lt;/code&gt;变为&lt;code&gt;void&lt;/code&gt;类型，不会触发&lt;code&gt;size_type&lt;/code&gt;类型的&lt;code&gt;operator,()&lt;/code&gt;重载。&lt;/p&gt;
&lt;p&gt;最后，我们的整体解决方案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, unsigned N&amp;gt;
std::size_t len (T(&amp;amp;)[N]){
    cout &amp;lt;&amp;lt;&quot;no size&quot; &amp;lt;&amp;lt; endl;
    return N;
}
template&amp;lt;typename T&amp;gt;
auto len (T const&amp;amp; t) -&amp;gt; decltype(t.size(), typename T::size_type()){
    cout &amp;lt;&amp;lt;&quot;has size&quot;&amp;lt;&amp;lt;endl;
    return t.size();
}
std::size_t len (...)
{
    cout &amp;lt;&amp;lt;&quot;other&quot; &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第十一章 泛型库&lt;/h1&gt;
&lt;h2&gt;11.1 可调用对象&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注意：函数对象是可调用对象的一种。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;函数对象类型可以是：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数指针类型&lt;/li&gt;
&lt;li&gt;重载了 &lt;code&gt;operator()&lt;/code&gt;的 类类型（有时被称为仿函数），这其中包含 lambda 函数&lt;/li&gt;
&lt;li&gt;包含一个可以产生一个函数指针或者函数引用的转换函数的 类类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：函数和到函数的引用不是函数对象类型，但因为函数到指针隐式转换，它们能用在期待函数对象类型的地方。但是他们都是可调用对象类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样的类型对应的值被称之为函数对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可调用对象除了函数对象以外，还包括：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如包装在&lt;code&gt;std::function&lt;/code&gt;或&lt;code&gt;std::bind&lt;/code&gt;的对象。（个人理解）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成员函数指针类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;成员变量类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;由于lambda是重载了&lt;code&gt;operator()&lt;/code&gt;的匿名类，所以符合函数对象要求。所以lambda是可调用对象，更是函数对象。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数对象和可调用对象最大的区别是，函数对象可以像函数一样调用。比如&lt;code&gt;f(args...)&lt;/code&gt;，但是可调用对象不一定可以。比如成员函数指针，就必须要进行&lt;code&gt;this&lt;/code&gt;的添加。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;尤为注意成员函数指针不是函数对象，因为不能使用如&lt;code&gt;f(args)&lt;/code&gt;这样的方式调用。有关成员函数指针在杂记2&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;11.1.1 标准库中对函数对象的支持&lt;/h3&gt;
&lt;p&gt;我们在EFFSTL中详细介绍了&lt;code&gt;for_each&lt;/code&gt;。在这里我粘贴一部分源码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class _InIt, class _Fn&amp;gt;
_CONSTEXPR20 _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
    _Adl_verify_range(_First, _Last);
    auto _UFirst      = _Get_unwrapped(_First);
    const auto _ULast = _Get_unwrapped(_Last);
    for (; _UFirst != _ULast; ++_UFirst) {
        _Func(*_UFirst); //这里已经对迭代器对象解引用了，所以直接传入的是迭代器对应的值。
        // 注意这里调用函数的方式是直接把参数塞进去。所以传递函数对象就行 不用() 加了括号叫调用
    }

    return _Func; //看好了！！有返回值！！
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当把函数名当作函数参数传递时，并不是传递函数本体，而是传递其指针或者引用。和数组情况类似，在按值传递时，函数参数退化为指针，如果参数类型是模板参数，那么类型会被推断为指向函数的指针。(函数指针可以直接使用而不用解引用。语法糖。参见杂记2)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和数组一样，按引用传递的函数的类型不会退化。但是函数类型不能真正用 &lt;code&gt;const&lt;/code&gt; 限制。如果将 &lt;code&gt;foreach()&lt;/code&gt;的最后一个参数的类型声明为 &lt;code&gt;const _Fn _Func &amp;amp;&lt;/code&gt;，&lt;code&gt;const&lt;/code&gt; 会被省略。（通常而言，在主流 C++代码中很少会用到函数的引用。）&lt;/li&gt;
&lt;li&gt;我们可以对函数进行&lt;code&gt;&amp;amp;&lt;/code&gt;去地址后显式传入函数指针。这和第一种调用方式相同（函数名会隐式的退化为成指针），但是相对而言会更清楚一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果传递的是仿函数，就是将一个类的对象当作可调用对象进行传递。通过一个类类型进行调用通常等效于调用了它的&lt;code&gt;operator()&lt;/code&gt;。因此下面这样的调用：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;_Func(*_UFirst);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会被转换为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_Func.operator()(*_UFirst);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意在定义&lt;code&gt; operator()&lt;/code&gt;的时候最好将其定义成&lt;code&gt;const&lt;/code&gt;成员函数。否则当一些框架或者库不希望该调用会改变被传递对象的状态时，会遇到很不容易发现的错误。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 类 类型的对象，有可能会被转换为指向 surrogate call function（代理函数，参见 C.3.5）的指针或者引用。此时，下面的调用：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;_Func(*current);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会被转换为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(_Func.operator F())(*_UFirst);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt; F&lt;/code&gt; 就是类类型的对象可以转换为的某一个指向函数的指针或者指向函数的引用的类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代理函数处于 C++ 最晦涩的角落。参考&lt;/strong&gt;&lt;a href=&quot;https://stackoverflow.com/questions/11354737/how-does-surrogate-call-function-work&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Lambda 表达式会产生仿函数（也称闭包），因此它与仿函数（重载了 &lt;code&gt;operator()&lt;/code&gt;的类）的情况一致。不过 Lambda 引入仿函数的方法更为简便，因此它们从 C++11 开始变得很常见。虽然我不喜欢。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们在杂记4的lambda章节中提到了一点：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;在捕获列表为空的时候，lambda还有合成的用户定义转换函数。它的作用是返回一个函数指针。这个函数指针指向内部合成的静态成员函数，这个函数内部会调用本类的函数调用运算符&lt;code&gt;operator()&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是它从来不会被当作代理函数，因为它的匹配情况总是比常规仿函数的&lt;code&gt;operator()&lt;/code&gt;要差。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;11.1.2 处理成员函数和其额外的参数&lt;/h3&gt;
&lt;p&gt;我们这一节主要讲一下上一节没有讲到的成员函数和成员函数指针。&lt;/p&gt;
&lt;p&gt;我们在杂记2的成员函数指针，杂记3的&lt;code&gt;std::bind&lt;/code&gt;和深度探索对象模型的4.4中详细介绍了其使用方法和原理。&lt;/p&gt;
&lt;p&gt;这种时候就可以会用&lt;code&gt;invoke&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;看下面的&lt;code&gt;invoke&lt;/code&gt;部分即可。&lt;/p&gt;
&lt;h3&gt;11.1.3 函数调用的包装 （也就是invoke)&lt;/h3&gt;
&lt;p&gt;如果我们想要完美转发被调用函数的返回值给调用者，我们可以使用&lt;code&gt;decltype(auto)&lt;/code&gt;搭配完美转发。&lt;/p&gt;
&lt;p&gt;之所以要用&lt;code&gt;decltype(auto)&lt;/code&gt;而非&lt;code&gt;auto&lt;/code&gt;的原因是因为&lt;code&gt;auto&lt;/code&gt;会导致类型退化。&lt;/p&gt;
&lt;p&gt;我们提到过，&lt;code&gt;auto&lt;/code&gt;一定会推导出返回类型为对象类型并且应用退化。而&lt;code&gt;auto&amp;amp;&lt;/code&gt;或&lt;code&gt;auto&amp;amp;&amp;amp;&lt;/code&gt;一定会推导为引用类型。而&lt;code&gt;decltype(auto)&lt;/code&gt;则可以根据具体返回值的类型进行推导。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj start_const(myobj s){
    return myobj(move(s));
}
template&amp;lt;typename callable, typename... Arg&amp;gt;
decltype(auto) calls(callable func, Arg&amp;amp;&amp;amp;... args){
    return invoke(func, forward&amp;lt;Arg&amp;gt;(args)...); //要使用完美转发
}

int main(){
    auto f = calls(start_const, myobj(4));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于接收&lt;code&gt;invoke&lt;/code&gt;的返回值的部分在下面&lt;code&gt;invoke&lt;/code&gt;返回值部分。&lt;/p&gt;
&lt;h2&gt;11.2 类型萃取&lt;/h2&gt;
&lt;p&gt;什么是类型萃取？我的个人理解是： 计算（查询）和修改类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算类型，就比如我们在STL2中提到的，增加一个中间层。有一些东西不是类，自己不能包含类型，那么我们就为它增加一个中间层，比如增加一些&lt;code&gt;typedef/using&lt;/code&gt;来实现类型的计算。同时，标准库提供给我们的比如&lt;code&gt;is_const&lt;/code&gt;, &lt;code&gt;is_convertable&lt;/code&gt;都算是计算类型。&lt;/li&gt;
&lt;li&gt;修改类型，就比如标准库给的一些函数，比如&lt;code&gt;remove_reference&lt;/code&gt;，可以把一个类型修改为另一个类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过类型萃取可以实现在编译期计算、查询、判断、转换和选择，增强了泛型编程的能力，也增强了程序的弹性，使得我们在编译期就能做到优化改进甚至排错，能进一步提高代码质量。&lt;/p&gt;
&lt;h3&gt;11.2.1 类型萃取中的一些注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;比如我们下面提到的remove_cv，&lt;code&gt;std::remove_cv_t&amp;lt;const int&amp;amp;&amp;gt;&lt;/code&gt;返回的类型依旧是&lt;code&gt;const int&amp;amp;&lt;/code&gt;。因为这个&lt;code&gt;const&lt;/code&gt;是底层&lt;code&gt;const&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以这时候如果我们需要拿到&lt;code&gt;int&lt;/code&gt;，需要注意顺序和搭配：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::remove_const_t&amp;lt;std::remove_reference_t&amp;lt;const int&amp;amp;&amp;gt;&amp;gt;; // 结果是int
std::remove_reference_t&amp;lt;std::remove_const_t&amp;lt;const int&amp;amp;&amp;gt;&amp;gt;; // 结果是const int
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;还有比如使用&lt;code&gt;add_rvalue_reference&lt;/code&gt;（&lt;code&gt;declval&lt;/code&gt;提到过）的时候，并不是一定会返回一个右值引用。因为如果传入一个左值引用会导致引用坍缩，返回左值引用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cout &amp;lt;&amp;lt; is_same_v&amp;lt;const int&amp;amp;&amp;amp;, add_rvalue_reference_t&amp;lt;const int&amp;gt;&amp;gt; &amp;lt;&amp;lt; endl; //true。给const int加右值引用变成const int&amp;amp;&amp;amp;
cout &amp;lt;&amp;lt; is_same_v&amp;lt;const int&amp;amp;, add_rvalue_reference_t&amp;lt;const int&amp;amp;&amp;gt;&amp;gt; &amp;lt;&amp;lt; endl; //true。给const int&amp;amp;加右值引用导致坍缩变成const int&amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;所以我们说，使用类型萃取相关的标准库函数的时候，一定要详细查阅文档，了解其精确定义！！！&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;11.3 完美转发临时变量&lt;/h2&gt;
&lt;p&gt;很多时候我们可以完美转发一些泛型参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void process(myobj&amp;amp; obj){
    cout &amp;lt;&amp;lt;&quot;&amp;amp;process called&quot; &amp;lt;&amp;lt; endl;
}
void process(myobj&amp;amp;&amp;amp; obj){
    cout &amp;lt;&amp;lt;&quot;&amp;amp;&amp;amp;process called&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename... Args&amp;gt;
void forward_func(Args&amp;amp;&amp;amp;... args){
    process(forward&amp;lt;Args&amp;gt;(args)...); //&amp;amp;&amp;amp;process called
    process(args...); //&amp;amp;process called
}
int main(){
    forward_func(myobj(10)); //注意传入的得是右值。forward是维持右值的右值性和左值的左值性，和move不同。不多赘述
    return 0;
}
/*
const
&amp;amp;&amp;amp;process called
&amp;amp;process called
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是某些情况下，在泛型代码中我们需要转发一些不是通过参数传递进来的数据。此时我们可以使用 &lt;code&gt;auto &amp;amp;&amp;amp;&lt;/code&gt;创建一个可以被转发的变量。&lt;/p&gt;
&lt;h2&gt;11.4 做为模板参数的引用（也就是模板参数类型为引用） [非常重要]&lt;/h2&gt;
&lt;p&gt;假如我们有如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void ref_test1(T arg){
    cout &amp;lt;&amp;lt;std::boolalpha;
    cout &amp;lt;&amp;lt;is_same_v&amp;lt;int&amp;amp;, decltype(arg)&amp;gt; &amp;lt;&amp;lt; endl; //false
    cout &amp;lt;&amp;lt;is_same_v&amp;lt;int&amp;amp;, T&amp;gt; &amp;lt;&amp;lt; endl; //false
}
template&amp;lt;typename T&amp;gt;
void ref_test2(T&amp;amp; arg){
    cout &amp;lt;&amp;lt;std::boolalpha;
    cout &amp;lt;&amp;lt;is_same_v&amp;lt;int&amp;amp;, decltype(arg)&amp;gt; &amp;lt;&amp;lt; endl; //true
    cout &amp;lt;&amp;lt;is_same_v&amp;lt;int&amp;amp;, T&amp;gt; &amp;lt;&amp;lt; endl; //false
}
int main(){
    int a = 20;
    int&amp;amp; ref_a = a;
    ref_test1(a);
    ref_test2(a);
    ref_test1(ref_a);
    ref_test2(ref_a);
    
    ref_test1&amp;lt;int&amp;amp;&amp;gt;(a); //true true
    ref_test2&amp;lt;int&amp;amp;&amp;gt;(a); //true true
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;需要注意的是，无论我们传入的是否是引用类型，函数的输出都是一致的。为什么？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为&lt;code&gt;ref_a&lt;/code&gt;本身是个引用，意思是表达式&lt;code&gt;ref_a&lt;/code&gt;的类型是引用。但是&lt;strong&gt;整体表达式的类型永远不会是引用。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;比较难以理解？也就是说这种情况下，&lt;code&gt;T&lt;/code&gt;永远不会被推导为引用类型。因为&lt;code&gt;T&lt;/code&gt;代表的是整体表达式的类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7.2.3反复强调，万能引用是唯一一种可能把&lt;code&gt;T&lt;/code&gt;推导为引用类型的情况。因为有引用折叠。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是，如果&lt;code&gt;arg&lt;/code&gt;是按照&lt;code&gt;T&amp;amp;&lt;/code&gt;类型传入的，那么&lt;code&gt;arg&lt;/code&gt;的类型会是&lt;code&gt;T&amp;amp;&lt;/code&gt;。所以&lt;code&gt;arg&lt;/code&gt;此时是引用类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果硬要指定为引用类型，则需要显式指定模板参数。但是很多时候会有严重问题。具体看书吧，太复杂了。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表面上看，好像一般不会让模板参数类型为引用类型。但是在C++17 中，非类型模板参数可以通过推断得到。也就是模板参数类型可能是&lt;code&gt;template&amp;lt;typename T, decltype(auto) SZ&amp;gt;&lt;/code&gt;这样的形式。但是我们在&lt;code&gt;invoke的返回值&lt;/code&gt;一章中提到过，&lt;code&gt;decltype(auto)&lt;/code&gt;会很容易的根据表达式类型推导出引用类型。所以会出现问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;11.5 推迟计算&lt;/h2&gt;
&lt;p&gt;可能某些时候我们会遇到不完整类型。如某个类可能是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class myclass{
  	public:
    	T* elem;
    	typename std::conditional&amp;lt;std::is_move_constructible&amp;lt;T&amp;gt;::value, T&amp;amp;&amp;amp;,T&amp;amp;&amp;gt;::type foo();
    //...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里通过使用 &lt;code&gt;std::conditional&lt;/code&gt;来决定 &lt;code&gt;foo()&lt;/code&gt;的返回类型是 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;还是 &lt;code&gt;T&amp;amp;&lt;/code&gt;。决策标准是看模板参数 &lt;code&gt;T&lt;/code&gt; 是否支持 移动构造。问题在于 &lt;code&gt;std::is_move_constructible&lt;/code&gt; 要求其参数必须是完整类型。所以这时候，&lt;code&gt;T&lt;/code&gt;如果是不完整类型，则会报错。&lt;/li&gt;
&lt;li&gt;此时的解决方案是使用一个成员函数模板来替换现有的&lt;code&gt;foo()&lt;/code&gt;函数。因为模板只有在调用的时候会被实例化，所以可以将 &lt;code&gt;std::is_move_constructible&lt;/code&gt;的计算推迟到&lt;code&gt;foo()&lt;/code&gt;函数的实例化阶段。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class myclass{
  	public:
    	T* elem;
    	template&amp;lt;typename D = T&amp;gt;
    	typename std::conditional&amp;lt;std::is_move_constructible&amp;lt;D&amp;gt;::value, T&amp;amp;&amp;amp;,T&amp;amp;&amp;gt;::type foo(); //成员函数模板。第一个换成D就足够。
    //...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;11.6 在写泛型库的时候需要考虑的事情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在模板中使用万能引用来实现对参数的完美转发。如果转发的参数并不是通过外部传入的，就可以使用&lt;code&gt; auto &amp;amp;&amp;amp;&lt;/code&gt;（参见 11.3）。&lt;/li&gt;
&lt;li&gt;如果一个参数被声明为万能引用，并且传递给它一个左值的话，那么模板参数会被推断为引用类型（参见杂记）。&lt;/li&gt;
&lt;li&gt;在需要一个依赖于模板参数的对象的地址的时候，最好使用 &lt;code&gt;std::addressof()&lt;/code&gt;来获取地址，这样能避免因为对象拥有一个重载了的 &lt;code&gt;operator &amp;amp;&lt;/code&gt;而导致的意外情况&lt;/li&gt;
&lt;li&gt;对于成员函数，需要确保它们不会比预定义的 拷贝或移动构造函数或者赋值运算符更能匹配某个调用（参见 6.4）。&lt;/li&gt;
&lt;li&gt;如果我们需要把函数处理的结果写回依赖于模板参数的调用参数，需要考虑如果传入参数是带有&lt;code&gt;const&lt;/code&gt; 的情况。这种情况可能会以外导致该函数可以接受右值。（参见 7.2.2）&lt;/li&gt;
&lt;li&gt;请为将引用用于模板参数的副作用做好准备（参见 11.4 节）。当然非常不推荐这么做。尤其是在需要确保返回类型不会是引用的时候（参见 7.5）。&lt;/li&gt;
&lt;li&gt;请为将不完整类型用于嵌套式数据结构这一类情况做好准备（参见 11.5 节）。比如二叉树。&lt;/li&gt;
&lt;li&gt;为所有数组类型进行重载，而不仅仅是 &lt;code&gt;T[SZ]&lt;/code&gt;（参见 5.4 节）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C++ 模板笔记（二）：变参模板与基础技巧</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF-2/</guid><pubDate>Wed, 14 Dec 2022 14:51:00 GMT</pubDate><content:encoded>&lt;h1&gt;第四章 变参模板&lt;/h1&gt;
&lt;p&gt;编译期变参模板展开有四种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数的递归调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::initializer_list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enable_if&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;折叠表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.1 函数的变参模板&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基本定义&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, typename... Args&amp;gt;  //注意这里的Args叫模板参数包
// 如果函数参数列表中一个参数的类型是一个模板参数包,
// 则此参数也是一个函数参数包
void func(const T&amp;amp; t, const Args&amp;amp;... rest); //这里的rest叫函数参数包
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一定要注意区分开在模板头声明的模板参数包和函数头的函数参数包的区别。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意，模板参数包只能接受同一个类型的参数。要么全是类型模板参数，要么全是非类型模板参数。要么全是模板类型模板参数。不能混用。这也是下面5.7匹配问题关于&lt;code&gt;array&lt;/code&gt;的问题 ----- &lt;em&gt;12.3.4章节，12.2章节&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++17之前的写法：因为没有折叠表达式。&lt;strong&gt;这种属于函数递归调用&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**当&lt;code&gt;...&lt;/code&gt;出现在变量名字左侧的时候，表示声明一个参数包。一个参数包可以绑定零个或多个参数。当&lt;code&gt;...&lt;/code&gt;出现在变量名字右侧的时候，表明它会被展开（unpack）为独立的参数。**出自&lt;a href=&quot;https://youtu.be/kRVjG3qb7RE?t=1153&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;包展开的场所&lt;/strong&gt;查看&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/parameter_pack&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;包展开的模式。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;什么是模式？简要而言，模式就是看你省略号想要和什么东西组合在一起，也就是想要扩展什么。在包展开的场所中，以函数形参列表为例。我们可能看到两种样式的包展开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;do_something(func(args)...);
//扩展为 do_something(func(var1), func(var2), func(var3));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;do_something(func(args...));
//扩展为 do_something(func(var1, var2, var3));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;格外注意第一种情况下，意味着&lt;code&gt;do_something&lt;/code&gt;函数有三个参数。然而这个括号里面的逗号并不是逗号运算符，所以三个&lt;code&gt;func()&lt;/code&gt;函数的调用的求值顺序并不被保证。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以上例子&lt;a href=&quot;https://stackoverflow.com/questions/36984193/definition-of-pattern-for-parameter-pack-expansion-especially-within-a-functi&quot;&gt;来自&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们看到了两种形式的包展开。所以所谓包展开的模式，也就是我们是想扩展的那个东西。一般来说，它会扩展它左侧的，和它贴在一起的那一个东西。例子：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;void func1(int a, int b, int c){
    cout &amp;lt;&amp;lt;&quot;func1&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; b &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
}

int func2(int a){
    cout &amp;lt;&amp;lt;&quot;func2&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    return a;
}


template&amp;lt;typename... Args&amp;gt;
void tfunc(Args... args){
    func1(func2(args)...); //会被扩展为func1(func2(1), func2(3), func2(5));
    cout &amp;lt;&amp;lt;&quot;--------------&quot; &amp;lt;&amp;lt; endl;
    func1(args...); //会被扩展为func1(1,3,5);
    cout &amp;lt;&amp;lt;&quot;--------------&quot; &amp;lt;&amp;lt; endl;
    (func2(args), ...); //这是折叠表达式, C++17
}

int main(){
    tfunc(1,3,5);
    return 0;
}
/*
func2
5
func2
3
func2
1
func1
135
--------------
func1
135
--------------
func2
1
func2
3
func2
5
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再次提示在这个&lt;code&gt;func1(func2(args)...);&lt;/code&gt;包展开中，函数调用的求职顺序不保证，这不是逗号运算符。所以GCC中的顺序是&lt;code&gt;5 3 1&lt;/code&gt;，但是clang的顺序是&lt;code&gt;1 3 5&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void print (){
//必须要有无参重载，否则会无限递归。也就是最后一次无参无法被调用。
//最后一次被解包后，参数包会为空。所以会调用无参函数。
}
template&amp;lt;typename T, typename... Args&amp;gt;
void print (T firstArg, Args... args)
{
    cout &amp;lt;&amp;lt; firstArg &amp;lt;&amp;lt; endl; //print first argument
    print(args...); // call print() for remaining arguments
}
int main() 
{
    print(1, 1000, &quot;b23&quot;, 1.123, &quot;HahaahaH&quot;, 42);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;C++17之后的写法：有了折叠表达式
&lt;ul&gt;
&lt;li&gt;此处折叠表达式和逗号运算符语法会在下文补充。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; //要有单参打印的重载。不然会无限调用。
void print(T t){
    cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl;
}


template&amp;lt;typename... Args&amp;gt;
void print(Args... args){
    (print(args), ...); //这里外部一定要加括号。注意语法。这里的,是逗号运算符。
}


int main() 
{
    print(1, 1000, &quot;b23&quot;, 1.123, &quot;HahaahaH&quot;, 42);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多种变长参数模板可以同时存在 尽管直观看起来会有二义性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非变参模板比变参模板更特化。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Args&amp;gt;
void ttss(Args... args){
    cout &amp;lt;&amp;lt; &quot;varidic&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T1, typename T2, typename T3, typename T4&amp;gt;
void ttss(T1 a, T2 b, T3 c, T4 d){
    cout &amp;lt;&amp;lt; &quot;4 args&quot; &amp;lt;&amp;lt; endl;
}


int main() 
{
    ttss(&quot;a&quot;, &quot;v&quot;, &quot;f&quot;, 5);
    return 0;
}
//输出4 args
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;搭配std::initializer_list&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;函数变长参数模板搭配搭配std::initializer_list。
&lt;ul&gt;
&lt;li&gt;这个函数会返回一个&lt;code&gt;T&lt;/code&gt;类型的&lt;code&gt;vector&lt;/code&gt;，元素是&lt;code&gt;args&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt;
vector&amp;lt;T&amp;gt; func(const Args&amp;amp;... args){
    return {args...};
}
int main(){
    auto vec = func&amp;lt;int&amp;gt;(1,2,3,4,5,6,7);
    for(auto&amp;amp; i:vec){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }
}
/*
输出：
1
2
3
4
5
6
7
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果目的不是初始化一个容器而是打印列表，可以化简为：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename... Args&amp;gt;
void FormatPrint(const Args&amp;amp;... args)
{
   std::initializer_list&amp;lt;int&amp;gt;{ (std::cout &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; args &amp;lt;&amp;lt; &quot;]&quot;, 0)...};
   std::cout &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道逗号表达式会计算每一个表达式然后只保留最后一个表达式的值。&lt;code&gt;(a, b) &lt;/code&gt;这个表达式的值就是 &lt;code&gt;b&lt;/code&gt;，那么上述代码中&lt;code&gt;(std::cout &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; args &amp;lt;&amp;lt; &quot;]&quot;, 0)&lt;/code&gt;这个表达式的值就是&lt;code&gt;0&lt;/code&gt;，但是前面的每一个表达式都会被计算。逗号表达式保证其中的内容从左往右执行，&lt;code&gt;args&lt;/code&gt;参数包会被逐步展开，表达式前的&lt;code&gt;(void)&lt;/code&gt;是为了防止变量未使用的警告，运行过后我们就得到了一个&lt;code&gt;N&lt;/code&gt;个元素为&lt;code&gt;0&lt;/code&gt;的初始化列表，内容也被格式化输出了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
   FormatPrint(1, 2, 3, 4);
   FormatPrint(&quot;good&quot;, 2, &quot;hello&quot;, 4, 110);
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当然了，把&lt;code&gt;initializer_list&lt;/code&gt;换成任何一个接受&lt;code&gt;initializer_list&lt;/code&gt;的容器都可以。比如&lt;code&gt;vector&lt;/code&gt;也可以&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1.2 变参和非变参模板的重载&lt;/h3&gt;
&lt;p&gt;我们提到了多种变长参数模板可以同时存在 尽管直观看起来会有二义性。所以上面C++17前的写法可以写成这样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void print (T arg){
    cout &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T, typename... Args&amp;gt;
void print (T firstArg, Args... args)
{
    print(firstArg); //注意这里。这里调用了无模板参数包的函数。单独打印这次解包出来的这一个变量。
    print(args...); // call print() for remaining arguments
}
int main() 
{
    print(1, 1000, &quot;b23&quot;, 1.123, &quot;HahaahaH&quot;, 42);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简单来说，当两个函数模板的区别只在于尾部的参数包的时候，会优先选择没有尾部参数包的那一个函数模板。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1.3 sizeof运算符在变长模板中的特殊应用。&lt;/h3&gt;
&lt;p&gt;沿用上面的例子，传递给 &lt;code&gt;print() &lt;/code&gt;的第一个参数之后，输出剩余两次参数的数量。对于模板参数包和函数参数包都可以使用 &lt;code&gt;sizeof...&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt;
void print (T firstArg, Args... args)
{
    print(firstArg);
    cout &amp;lt;&amp;lt; &quot;remains&quot;&amp;lt;&amp;lt;sizeof...(args) &amp;lt;&amp;lt; endl; //函数参数包
    cout &amp;lt;&amp;lt; &quot;remains&quot;&amp;lt;&amp;lt;sizeof...(Args) &amp;lt;&amp;lt; endl; //模板参数包
    print(args...);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4.2 折叠表达式 (fold expression)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;折叠表达式的展开过程并不是递归。更多的是类似于一种替换，一种编译时展开替换。个人理解。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;折叠表达式分为四种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;左折：参数从左侧开始计算&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一元左折叠&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;( ... 运算符 形参包 )
会被拓展为
(((E1 运算符 E2) 运算符 ...) 运算符 EN)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;二元左折叠&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;( 初值 运算符 ... 运算符 形参包 )	
会被拓展为
((((初值 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右折：参数从右侧开始计算&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一元右折叠&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;( 形参包 运算符 ... )
会被拓展为
(E1 运算符 (... 运算符 (EN-1 运算符 EN)))
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;二元右折叠&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;( 形参包 运算符 ... 运算符 初值 )	
会被拓展为
(E1 运算符 (... 运算符 (EN−1 运算符 (EN 运算符 初值))))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左折叠右折叠在有顺序要求的时候尤其重要。比如减法和除法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意左右不是指的是单个顺序颠倒。而是每一组顺序颠倒。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比如&lt;code&gt; ((a+b)+c)&lt;/code&gt; 变成右折并不是&lt;code&gt;(a+(c+b))&lt;/code&gt;。而是&lt;code&gt;(a+(b+c))&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;将一元折叠用于长度为零的包展开时，只能使用下列运算符：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;逻辑与（&amp;amp;&amp;amp;）。空包的值是 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑或（||）。空包的值是 false&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逗号运算符（,）。空包的值是 void()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他操作符为不合规。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在二元折叠中，两个运算符必须相同&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可以一加一减。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;语法示范：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//一元左折叠加法
template&amp;lt;typename... T&amp;gt;
auto foldSumLeft(T... s)
{
    return (... + s); // ((s1+s2)+s3)...
}

//一元右折叠加法
template&amp;lt;typename... T&amp;gt;
auto foldSumRight(T... s)
{
    return (s + ...); // (s1+...+(Sn-1 + sn)))...)
}
//一元左折叠减法
template&amp;lt;typename... T&amp;gt;
auto foldMinLeft(T... s)
{
    return (... - s); // ((s1-s2)-s3)...
}
//一元右折叠减法
template&amp;lt;typename... T&amp;gt;
auto foldMinRight(T... s)
{
    return (s - ...); // (s1-...-(Sn-1 - sn)))...)
}

int main(){
    auto sumLeft = foldSumLeft(1,2,3,4,5,6,7); //((((((1+2)+3)+4)+5)+6)+7)
    auto sumRight = foldSumRight(1,2,3,4,5,6,7);
    cout &amp;lt;&amp;lt; sumLeft &amp;lt;&amp;lt; endl; //28
    cout &amp;lt;&amp;lt; sumRight &amp;lt;&amp;lt; endl;//28
    auto sumLeftmin  = foldMinLeft(1,2,3); //((1-2)-3)
    auto sumRightmin  = foldMinRight(1,2,3);//(1-(2-3))
    cout &amp;lt;&amp;lt; sumLeft &amp;lt;&amp;lt; endl; //-4
    cout &amp;lt;&amp;lt; sumRight &amp;lt;&amp;lt; endl;//2
}


//二元左折
template &amp;lt;typename... T&amp;gt;
auto foldBinLeft(T... s)
{
    //操作对象有两个，一个是888 初值，一个是ts形参包,
    //初始值位于左边，所以为二元左折
    return (888 + ... + s);
}
//二元右折
template &amp;lt;typename... T&amp;gt;
auto foldBinRight(T... s)
{
    //操作对象有两个，一个是888 初值，一个是ts参包,
    //初始值位于右边，所以为二元右折
    return (s + ... + 888);
}
int main(){
    auto binLeft = foldBinLeft(1,2,3); //894 (((888+1)+2)+3)
    auto binRight = foldBinRight(1,2,3); //894 (1+(2+(3+888)))
    cout &amp;lt;&amp;lt; binLeft &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; binRight &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果用作 初值 或 形参包的表达式在顶层具有优先级低于转型的运算符，那么它必须加括号：&lt;/li&gt;
&lt;li&gt;此处仅用二元右折做个示范。注意此时两个操作符都是&lt;code&gt;+&lt;/code&gt;。所以合法。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename... T&amp;gt;
auto foldBinRight(T... s)
{
    return (s + ... + (888*2)); //可以
    return (s + ... + 888*2); //不可以
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;序列点和逗号运算符在折叠表达式中的特殊性质&lt;/h3&gt;
&lt;p&gt;序列点的定义可以查一下文档。简单来说就是我们需要约束一个表达式的计算顺序。&lt;/p&gt;
&lt;p&gt;我们上面提到了这个代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Args&amp;gt;
void print(Args... args){
    (print(args), ...); //这里外部一定要加括号。注意语法
    (..., print(args));//??
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现第一行是右折叠。按理来说应该是逆序输出。但是为什么这里左折和右折效果一样？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这就是序列点和逗号运算符的特殊性。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C++有强制规定：内建&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/operator_other#.E5.86.85.E5.BB.BA.E7.9A.84.E9.80.97.E5.8F.B7.E8.BF.90.E7.AE.97.E7.AC.A6&quot;&gt;逗号运算符&lt;/a&gt; , 的第一个（左）参数的每个值计算和副作用都按顺序早于第二个（右）参数的每个值计算和副作用。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;也就是说，这里的左折叠和右折叠的优先级是低于逗号运算符的。所以说逗号运算符强制从左到右的运算顺序使得这里的左折和右折失效。产生了同等效果  。&lt;/li&gt;
&lt;li&gt;所以，这里在展开后，依旧会从左至右进行运算。&lt;/li&gt;
&lt;li&gt;再次重申：折叠表达式的展开过程并不是递归。&lt;/li&gt;
&lt;li&gt;所以它展开后可以被化简看作：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;print(1), print(1000), print(&quot;b23&quot;),.....;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此部分参考：&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/46056268/order-of-evaluation-for-fold-expressions&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/59590426/variadic-template-argument-forwarding-uses-comma-operator&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/53330713/fold-expression-with-comma-operator-and-variadic-template-parameter-pack&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/45603533/how-does-folding-over-comma-work&lt;/p&gt;
&lt;h2&gt;4.3 变参模板的使用&lt;/h2&gt;
&lt;p&gt;之前关于常规模板参数的规则同样适用于变参模板参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如，如果参数是按值传递的，那么其参数会按照正常理解被拷贝，类型也会退化（decay）。&lt;/li&gt;
&lt;li&gt;如果是按引用传递的，那么参数会是实参的引用，并且类型不会退化&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.4 变参类模板和变参表达式&lt;/h2&gt;
&lt;h3&gt;4.4.1 在变参中使用表达式&lt;/h3&gt;
&lt;p&gt;此处是针对4.2的一些扩展。也就是折叠表达式的一种应用。&lt;/p&gt;
&lt;p&gt;我们可以将函数参数包参与到一些运算当中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里，我们把传入的每一个参数的数值都增加一倍&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;注意字面值是&lt;code&gt;const char*&lt;/code&gt;类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; //要有单参打印的重载。不然会无限调用。
void print(T t){
    cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl;
}


template&amp;lt;typename... Args&amp;gt;
void print(Args... args){
    (print(args), ...); //这里外部一定要加括号。注意语法。这里的,是逗号运算符。
}


template&amp;lt;typename... T&amp;gt;
void printDoubled (T const&amp;amp;... args)
{
    print (args + args...);
}   

int main(){
    printDoubled(1, 2.3, string(&quot;abcde&quot;)); //输出 2 4.6 abcdeabcde。注意字面值是`const char*`类型。
    //等于调用 print(1+1), print(2.3 + 2.3), print(string(&quot;abcde&quot;) + string(&quot;abcde&quot;))
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果只是想每一个都加1，那么后面的参数包位置则必须字面值和数字分开。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... T&amp;gt;
void printDoubled (T const&amp;amp;... args)
{
    print(1 + args...); //要么这么写
    print(args + 1 ...); //要么这么写。但是1和...必须分开。
    print((args + 1)...); //要么这么写。多加个括号。
}   
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编译阶段的表达式同样可以像上面那样包含模板参数包。比如下面这个例子可以用来判断所有参数包中参数的类型是否相同
&lt;ul&gt;
&lt;li&gt;注意这里是函数模板的例子。函数模板可以自动推导类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename... TN&amp;gt;
constexpr bool isSameType (T1, TN...)
{
    return (std::is_same&amp;lt;T1,TN&amp;gt;::value &amp;amp;&amp;amp; ...); // since C++17
}

int main(){
    cout &amp;lt;&amp;lt; isSameType(1,2,3,4) &amp;lt;&amp;lt; endl; //输出1 true
    //这一行会被扩展为 std::is_same&amp;lt;int,int&amp;gt;::value &amp;amp;&amp;amp; std::is_same&amp;lt;int,int&amp;gt;::value &amp;amp;&amp;amp; std::is_same&amp;lt;int,int&amp;gt;::value
    cout &amp;lt;&amp;lt; isSameType(1,2,&quot;123&quot;,4) &amp;lt;&amp;lt; endl; //输出0 false
    //这一行会被扩展为 std::is_same&amp;lt;int,int&amp;gt;::value &amp;amp;&amp;amp; std::is_same&amp;lt;int,const char*&amp;gt;::value &amp;amp;&amp;amp; std::is_same&amp;lt;const char*,int&amp;gt;::value
    cout &amp;lt;&amp;lt; isSameType(&quot;abc&quot;, &quot;abcde&quot;) &amp;lt;&amp;lt; endl; //输出1 true 特别注意这里
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意最后一个调用。这里因为是按值传递，所以发生了类型退化，统一被推导为&lt;code&gt;const char*&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;否则类型将依次被推断为：&lt;code&gt;const char [3]&lt;/code&gt; 和 &lt;code&gt;const char[5]&lt;/code&gt;;
&lt;ul&gt;
&lt;li&gt;所以如果&lt;code&gt;isSameType&lt;/code&gt;写成这个样子就不会退化，导致输出&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;constexpr bool isSameType (T1&amp;amp;, TN&amp;amp;...)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;这里参见函数模板参数推导规则。这里我们看到传入的是数组，然后模板函数的入参部分(不是模板参数部分)是引用了，所以这时候传入的参数不会退化。也就是里面的&lt;code&gt;T1&lt;/code&gt;和&lt;code&gt;TN&lt;/code&gt;维持住了非退化类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4.2 在变参中使用下标 (variadic indices)&lt;/h3&gt;
&lt;p&gt;这里看起来可能比较晦涩，但是是一个比较实用的功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设我们有两个参数。第一个参数是一个容器，第二个参数是一个下标。我们想访问这个容器的这个下标的元素应该怎么做？&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Container, typename Index&amp;gt;
void getItem(Container&amp;amp; container, Index idx){
    cout &amp;lt;&amp;lt; container[idx] &amp;lt;&amp;lt; endl;
}

int main(){
    //printDoubled(1, 2.3); //输出 2 4.6
    vector&amp;lt;string&amp;gt; my_vec{&quot;abc&quot;, &quot;def&quot;, &quot;ghi&quot;, &quot;123&quot;, &quot;456&quot;};
    getItem(my_vec, 3); //输出 123
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;好的。那么我们如果想要把这个功能融入到变参里面呢？比如我想要拿出多个元素，怎么做呢？
&lt;ul&gt;
&lt;li&gt;注意这里我们参数包为下标，而不是容器。&lt;/li&gt;
&lt;li&gt;非常方便。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; //要有单参打印的重载。不然会无限调用。
void print(T t){
    cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl;
}


template&amp;lt;typename... Args&amp;gt;
void print(Args... args){
    (print(args), ...); //这里外部一定要加括号。注意语法。这里的,是逗号运算符。
}
//--------------------------上面是之前我们一直在用的print模板，这里也放一个方便查看---------------------
template&amp;lt;typename Container, typename... Index&amp;gt; //模板参数包
void getMultipleItem(Container&amp;amp; container, Index... idx){//函数参数包
    print(container[idx]...); //参数包
}
int main(){
    vector&amp;lt;string&amp;gt; my_vec{&quot;abc&quot;, &quot;def&quot;, &quot;ghi&quot;, &quot;123&quot;, &quot;456&quot;};
    getMultipleItem(my_vec, 1,2,3,4);
    //输出def ghi 123 456
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getMultipleItem(my_vec, 1,2,3,4);&lt;/code&gt; 相当于调用了：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;print(my_vec[1], my_vec[2], my_vec[3], my_vec[4])&lt;/code&gt;;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;同时我们也可以将非类型模板参数声明成参数包：&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;int... Idx, typename Container&amp;gt; //细节1
void getMultipleItem(Container&amp;amp; container){ //细节2
    print(container[Idx]...);
}
int main(){
    vector&amp;lt;string&amp;gt; my_vec{&quot;abc&quot;, &quot;def&quot;, &quot;ghi&quot;, &quot;123&quot;, &quot;456&quot;};
    getMultipleItem&amp;lt;1,2,3,4&amp;gt;(my_vec); //细节3
    //输出def ghi 123 456
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首先，非类型模板参数不是类型，是变量。所以无需传入函数参数。只需在模板参数中传入（细节1，2）&lt;/li&gt;
&lt;li&gt;细节3，这里既然是模板参数而非函数参数，所以这里要用&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;传入模板参数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4.3 在类模板中使用可变参数（变参类模板）&lt;/h3&gt;
&lt;p&gt;等25和26章&lt;/p&gt;
&lt;h3&gt;4.4.4 变参推断指引&lt;/h3&gt;
&lt;p&gt;看书&lt;/p&gt;
&lt;h3&gt;4.4.5 变参基类&lt;/h3&gt;
&lt;p&gt;等26章&lt;/p&gt;
&lt;h1&gt;第五章 基础技巧&lt;/h1&gt;
&lt;h2&gt;5.1 typename关键字&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;看STL2&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;5.2 零初始化&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;此处同时参考笔记 聚合初始化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板中依旧遵循和非模板相同的初始化规则，比如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void func(){
    T x; //如果T是内置类型，则模板实例化后，x为脏数据，也就是未确定值。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果想要强制其初始化为0，则应该使用值初始化。关于值初始化依旧参考聚合初始化笔记。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void func(){
    T x{}; //如果T是内置类型，则模板实例化后，x为0值。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一些之前的知识回顾&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 C++11 之前，确保一个对象得到显示初始化的方式是&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;T x = T();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;[这一条可以参考杂记的拷贝初始化和杂记2的explicit] 由于这是一种拷贝初始化，所以在 C++17 之前，只有在与拷贝初始化对应的构造函数没有被声明为 explicit 的时候，这一方式才有效（目前也依然被支持）&lt;/li&gt;
&lt;li&gt;[强制拷贝省略技术可以参考杂记3的复制省略技术] 从 C++17 开始，由于强制拷贝省略（mandatory copy elision）的使用，这一限制被解除，因此在 C++17 之后以上两种方式都有效。不过对于用花括号初始化的情况，如果没有可用的默认构造函数，它还可以使用列表初始化构造函数（initializer-listconstructor）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;继续话题&lt;/h3&gt;
&lt;p&gt;为确保类模板中类型被参数化了的成员得到适当的初始化，可以定义一个默认的构造函数并在其中对相应成员做初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class MyClass {
    private:
        T x;
    public:
        MyClass() : x{} {} //确保当T为内置类型时，x值为0。注意第一个花括号为值初始化。第二个花括号是构造函数的函数体。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;c++11之前的语法：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;MyClass() : x() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;从C++11开始，针对&lt;strong&gt;非静态成员和非默认参数&lt;/strong&gt;部分，则也可以用这种方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class MyClass {
	private:
		T x {}; // 使用值初始化，确保当T为内置类型时，x值为0。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果针对默认参数这样使用，则会报错。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void foo(T p{}){ 
    //错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这样可以：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void foo(T p = T{}){ 
    //正确。但如果C++11之前的话必须把T{}换成T()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5.3 使用this&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里应参考 深度探索c++对象模型笔记中的7.1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;简而言之，当在模板类中使用定义于基类中的、依赖于模板参数的成员时，应当用 &lt;code&gt;this-&amp;gt;&lt;/code&gt;或者如&lt;code&gt; Base&amp;lt;T&amp;gt;::&lt;/code&gt;这样显式指定作用域来使用它&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5.4 正确处理传入模板的裸数组或字符串常量&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当向模板传递裸数组或者字符串常量时，需要格外注意以下内容：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果参数是按引用传递的，那么参数类型不会退化（decay）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说当传递&lt;code&gt;hello&lt;/code&gt;作为参数时，模板类型会被推断为 &lt;code&gt;const char[6]&lt;/code&gt;。这样当向模板传递长度不同的裸数组或者字符串常量时就可能遇到问题，因为它们对应的模板类型不一样。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只有当按值传递参数时，模板类型才会退化（decay）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这样字符串常量会被推断为&lt;code&gt;const char*&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关于具体选用按照值传递还是引用传递，可以看第七章。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不过我们可以单独处理一下裸数组或者是字符串常量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, int N, int M&amp;gt;
bool func (T(&amp;amp;a)[N], T(&amp;amp;b)[M]){
    cout &amp;lt;&amp;lt; N &amp;lt;&amp;lt; endl; //输出3
    cout &amp;lt;&amp;lt; M &amp;lt;&amp;lt; endl; //输出7
    for (int i = 0; i&amp;lt;N &amp;amp;&amp;amp; i&amp;lt;M; ++i){
        if (a[i]&amp;lt;b[i]){
            return true; 
        }
        if (b[i]&amp;lt;a[i]){
            return false;
        }
    }
    return N &amp;lt; M;
}

int main(){
    int x[] = {1,2,3};
    int y[] = {1,2,3,4,5,6,7};
    func(x,y);
    return 0;
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，&lt;code&gt;func&lt;/code&gt;中的&lt;code&gt;T&lt;/code&gt;会被实例化为&lt;code&gt;int&lt;/code&gt;，&lt;code&gt;N&lt;/code&gt;会被实例化为&lt;code&gt;3&lt;/code&gt;，&lt;code&gt;M&lt;/code&gt;会被实例化为&lt;code&gt;7&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;同样适用于字符串常量。如果我们调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func(&quot;abc&quot;, &quot;abcde&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则&lt;code&gt;func&lt;/code&gt;中的&lt;code&gt;T&lt;/code&gt;会被实例化为&lt;code&gt;const char&lt;/code&gt;，&lt;code&gt;N&lt;/code&gt;会被实例化为&lt;code&gt;4&lt;/code&gt;，&lt;code&gt;M&lt;/code&gt;会被实例化为&lt;code&gt;6&lt;/code&gt;。这里多了1是因为有字符串结束符。&lt;/p&gt;
&lt;h2&gt;5.5 成员模板&lt;/h2&gt;
&lt;p&gt;我们知道，写在尖括号&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;内部的是模板参数，也就是类型信息。所以&lt;code&gt;stack&amp;lt;int&amp;gt;&lt;/code&gt; 和 &lt;code&gt;stack&amp;lt;float&amp;gt;&lt;/code&gt;理论上不可以相互赋值。因为没有合适的转换方式。但是我们可以重载 类内的&lt;code&gt;operator=&lt;/code&gt;来实现这个不同类型间的相互赋值。比如&lt;code&gt;stack&amp;lt;float&amp;gt;&lt;/code&gt;赋值给&lt;code&gt;stack&amp;lt;int&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;具体实现还是看书比较好。&lt;/p&gt;
&lt;h3&gt;5.5.1 成员模板的特化&lt;/h3&gt;
&lt;p&gt;我们也可以给成员函数添加模板。同时成员函数模板也可以被全特化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class getInt{
    public:
        float val;
        getInt(float a): val(a){};
        template&amp;lt;typename T = int&amp;gt; //成员函数模板。此处模拟一个返回int
        T get() const{
            return val;
        }
};

template&amp;lt;&amp;gt; //全特化，此处模拟返回string。这里有细节
inline string getInt::get&amp;lt;string&amp;gt;() const{ //注意这里的inline
    return to_string(val);
}

int main(){
    getInt myobj(12.345);
    auto t = myobj.get();
    cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl; //12
    auto t1 = myobj.get&amp;lt;string&amp;gt;(); //12.345000
    cout &amp;lt;&amp;lt; t1 &amp;lt;&amp;lt; endl;
    return 0;
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个细节：c++不允许成员函数模板在类内全特化（显式特化）。必须写在类外。&lt;/li&gt;
&lt;li&gt;第二个细节：如果分离编译，则此处全特化版本函数必须inline，否则会重定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.5.2 特殊成员函数的模板 和 泛型lambda&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果能够通过特殊成员函数 拷贝或者 移动对象，那么相应的特殊成员函数（拷贝构造函数以及 移动 构造函数）也将可以被模板化。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;和前面定义的赋值运算符类似，&lt;strong&gt;构造函数也可以是模板&lt;/strong&gt;。但是需要注意的是，&lt;strong&gt;构造函数模板或者赋值运算符模板不会取代预定义的构造函数（此处尤指拷贝构造和移动构造）和赋值运算符（此处尤指拷贝赋值和移动赋值）&lt;/strong&gt;。&lt;strong&gt;成员函数模板不会被算作用来 拷贝或移动  对象的特殊成员函数。在上面的例子中，如果在相同类型的 stack 之间相互赋值，调用的依然是默认赋值运算符。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这句话有毛病，省略了几个词。经过我的多方咨询，这句话的意思是：构造函数模板就算发现参数推导匹配，也不会为我们生成拷贝构造/拷贝赋值/移动构造和移动赋值。这几个函数必须是预定义的而不能是从模板实例化的。也就是说，就算模板实例化的函数长得表面和那四个函数一样，实例化出的函数也不会被认为是特殊成员函数。----同时参考effective modern C++ 条款26&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230114134638.png&quot; alt=&quot;QQ截图20230114134638&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230114134644.png&quot; alt=&quot;QQ截图20230114134644&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这种行为既有好处也有坏处：
&lt;ul&gt;
&lt;li&gt;某些情况下，对于某些调用，构造函数模板或者赋值运算符模板可能比预定义的拷贝或移动 构造函数或者赋值运算符更匹配，虽然这些特殊成员函数模板可能原本只打算用于在不同类型的某一个类之间做初始化。详情请参见 6.2 节。&lt;/li&gt;
&lt;li&gt;想要对拷贝或移动构造函数进行模板化并不是一件容易的事情，比如该如何限制其存在的场景。详情请参见 6.4 节。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;泛型lambda [c++14]：&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;auto lambda = [](auto x, auto y){
    return x+y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，我们创建了一个泛型lambda。这个lambda可以接受任意的x和y，只要x和y可以相加，这段代码就没问题。&lt;/p&gt;
&lt;p&gt;它的匿名类可能会像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SomeCompilerSpecificName {
    public:
        SomeCompilerSpecificName(); // constructor only callable bycompiler
        
        template&amp;lt;typename T1, typename T2&amp;gt; //函数模板
        auto operator() (T1 x, T2 y) const {
            return x + y;
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个部分在&lt;code&gt;std::visit&lt;/code&gt;中有用到。节选自&lt;a href=&quot;https://zh.cppreference.com/w/cpp/utility/variant/visit&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
    std::vector&amp;lt;var_t&amp;gt; vec = {10, 15l, 1.5, &quot;hello&quot;};
 
    for (auto&amp;amp;&amp;amp; v: vec)
    {
        // 1. void 探访器，仅为它的副作用而调用
        std::visit([](auto&amp;amp;&amp;amp; arg){ std::cout &amp;lt;&amp;lt; arg; }, v);
 
        // 2. 返回值的探访器，演示返回另一变体的常见惯用法
        var_t w = std::visit([](auto&amp;amp;&amp;amp; arg) -&amp;gt; var_t { return arg + arg; }, v);
 
        // 3. 类型匹配探访器：以不同方式处理每个类型的 lambda
        std::cout &amp;lt;&amp;lt; &quot;。翻倍后，变体持有&quot;;
        // 这一部分就是泛型lambda。
        std::visit([](auto&amp;amp;&amp;amp; arg)
        {
            using T = std::decay_t&amp;lt;decltype(arg)&amp;gt;;
            if constexpr (std::is_same_v&amp;lt;T, int&amp;gt;)
                std::cout &amp;lt;&amp;lt; &quot;值为 &quot; &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &quot; 的 int\n&quot;;
            else if constexpr (std::is_same_v&amp;lt;T, long&amp;gt;)
                std::cout &amp;lt;&amp;lt; &quot;值为 &quot; &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &quot; 的 long\n&quot;;
            else if constexpr (std::is_same_v&amp;lt;T, double&amp;gt;)
                std::cout &amp;lt;&amp;lt; &quot;值为 &quot; &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &quot; 的 double\n&quot;;
            else if constexpr (std::is_same_v&amp;lt;T, std::string&amp;gt;)
                std::cout &amp;lt;&amp;lt; &quot;值为 &quot; &amp;lt;&amp;lt; std::quoted(arg) &amp;lt;&amp;lt; &quot; 的 std::string\n&quot;;
            else 
                static_assert(false, &quot;探访器无法穷尽类型！&quot;);
        }, w);
    }
 
    for (auto&amp;amp;&amp;amp; v: vec)
    {
        // 4. 另一种类型匹配探访器：有三个重载的 operator() 的类
        // 注：此情况下 &apos;(auto arg)&apos; 模板 operator() 将绑定到 &apos;int&apos; 与 &apos;long&apos;，
        //    但它不存在时 &apos;(double arg)&apos; operator() *也会* 绑定到 &apos;int&apos; 与 &apos;long&apos;，
        //    因为两者均可隐式转换到 double。使用此形式时应留心以正确处理隐式转换。
        // 这一部分可以查看 笔记尾的《继承自可变参数包》一节
        std::visit(overloaded{
            [](auto arg) { std::cout &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &apos; &apos;; },
            [](double arg) { std::cout &amp;lt;&amp;lt; std::fixed &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &apos; &apos;; },
            [](const std::string&amp;amp; arg) { std::cout &amp;lt;&amp;lt; std::quoted(arg) &amp;lt;&amp;lt; &apos; &apos;; },
        }, v);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.5.3 template 关键字 （如 .template , ::template, -&amp;gt;template)&lt;/h3&gt;
&lt;p&gt;template关键字是用来消除&lt;strong&gt;待决名&lt;/strong&gt;的歧义的. 观察下面的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
 int f(T&amp;amp; x) {
     return x.template convert&amp;lt;3&amp;gt;(pi);
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有template, 则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return x.convert&amp;lt;3&amp;gt;(pi);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能被理解为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return ((x.convert) &amp;lt; 3) &amp;gt; (pi);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以使用template来显式说明convert不是一个数据成员, 而是一个模板函数. 下面把标准照抄一遍.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用 template 的规则&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当成员模板特化的名字出现在一个后缀表达式中的&lt;code&gt;.&lt;/code&gt;或&lt;code&gt;-&amp;gt;&lt;/code&gt;之后, 或者出现一个限定标识中的嵌套的名字修饰符之后(就是&lt;code&gt;::&lt;/code&gt;), 并且后缀表达式或限定标识显示依赖于一个模板参数时, 成员模板名字必须加template关键字作为前缀, 否则该名字就被假定为一个非模板的名字.&lt;/p&gt;
&lt;p&gt;如果后缀表达式或者限定标识不是出现在一个模板的作用域时, 成员模板的名字就不应该加上template关键字作为前缀.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;必须使用template的场合&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在通过“&lt;code&gt;.&lt;/code&gt;”,“&lt;code&gt;-&amp;gt;&lt;/code&gt;”,“&lt;code&gt;::&lt;/code&gt;”限定的依赖名访问成员模板之前, template关键字必不可少.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
 void f(T&amp;amp; x, T&amp;amp; y) {
     int n = x.template convert&amp;lt;int&amp;gt;();
     int m = y-&amp;gt;template convert&amp;lt;int&amp;gt;();
 }

 template&amp;lt;class T&amp;gt; struct other;
 template&amp;lt;class T&amp;gt;
 struct dirived : other &amp;lt;T&amp;gt;::template base&amp;lt;int&amp;gt; {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;禁止使用template的场合&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;禁止用在模板之外的任何地方, 包括显式(完全)模板特化. 禁止用在using声明中.&lt;/p&gt;
&lt;h2&gt;5.6 变量模板&lt;/h2&gt;
&lt;h3&gt;5.6.1 普通变量模板&lt;/h3&gt;
&lt;p&gt;在C++14之后，我们可以对变量使用模板。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T my_val = 20.1234;
int main(){
    cout &amp;lt;&amp;lt; my_val&amp;lt;int&amp;gt; &amp;lt;&amp;lt; endl; //20
    cout &amp;lt;&amp;lt; my_val&amp;lt;double&amp;gt; &amp;lt;&amp;lt; endl; //20.1234
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意，和其它几种模板类似，这个定义最好不要出现在函数内部或者块作用域内部。不可在&lt;code&gt;main&lt;/code&gt;函数内。&lt;/li&gt;
&lt;li&gt;变量模板可以有默认参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T = double&amp;gt; //默认参数
T my_val = 20.1234;
int main(){
    cout &amp;lt;&amp;lt; my_val&amp;lt;int&amp;gt; &amp;lt;&amp;lt; endl;  //20
    cout &amp;lt;&amp;lt; my_val&amp;lt;&amp;gt; &amp;lt;&amp;lt; endl; //20.1234
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在使用变量模板的时候，必须显式指明它的类型。如果直接使用变量会报错。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cout &amp;lt;&amp;lt; my_val &amp;lt;&amp;lt; endl; //错误
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;同样可以用非类型参数对变量模板进行参数化，也可以将非类型参数用于参数器的初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.6.2 成员变量模板&lt;/h3&gt;
&lt;p&gt;虽然不知道有啥用，但是还是整理一下。&lt;/p&gt;
&lt;p&gt;假如我们有如下类模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class myobj{
    public:
        static const int val = 1000; //static 和 const一起修饰变量可以在类内赋初值
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们不用成员变量模板，类外想访问的话比较麻烦：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int out_val = myobj&amp;lt;int&amp;gt;::val;
int main(){
    cout &amp;lt;&amp;lt; out_val &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们采用成员变量模板，则可以这样使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
int out_val_T = myobj&amp;lt;T&amp;gt;::val; //成员变量模板

int out_val1 = out_val_T&amp;lt;int&amp;gt;; //直接使用

int main(){
    cout &amp;lt;&amp;lt; out_val1 &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5.7 模板模板参数&lt;/h2&gt;
&lt;p&gt;我们有过非类型模板参数，意思就是模板参数并不指定类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以这里的模板模板参数的意思就是让模板的参数也是模板。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;模板的模板参数的经典应用是在某些自定义指定储存容器类型的时候不指定元素类型。&lt;/p&gt;
&lt;p&gt;比如，在我们不用模板模板参数的时候，指定&lt;code&gt;stack&lt;/code&gt;可能需要这么写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stack&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; vStack; //integer Stack that uses a vector
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用模板模板参数，我们就可以这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stack&amp;lt;int, std::vector&amp;gt; vStack; //intege stack that uses a vector
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;函数模板和变量模板没有模板模板参数、&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最基本的举例&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;template&amp;lt;typename T&amp;gt; class container, typename T1, typename T2&amp;gt; // 最基本的举例
class test{
    container&amp;lt;T1&amp;gt; a1;
    container&amp;lt;T2&amp;gt; a2;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;茴字的三种写法, 都可以&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename&amp;gt; class Container&amp;gt;
template &amp;lt;typename T, template &amp;lt;class&amp;gt; class Container&amp;gt;
template &amp;lt;typename T, template &amp;lt;typename&amp;gt; typename Container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;更好的理解的写法:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename T1&amp;gt; typename Container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在上面的例子里面，我们用不到模板模板参数里面的占位符，所以也可以忽略不写。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模板模板参数期待的是模板名，而不是具体类型&lt;/h3&gt;
&lt;p&gt;一定要注意模板模板参数到底期待的是什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T1, typename T2&amp;gt;
struct S{

};
template &amp;lt;template &amp;lt;typename, typename&amp;gt; typename T1&amp;gt;
struct A{
    T1&amp;lt;int, double&amp;gt; obj; //可以在类内通过模板名T1这样实例化。
};

int main(){
    A&amp;lt;S&amp;gt; obj1; //模板模板参数期待的是一个模板名。
    A&amp;lt;S&amp;lt;int, double&amp;gt; obj2; //错误，不能这样指定，这是具体类型了。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在模板声明体内，此形参（我们的例子是T1）的名字是一个模板名（且需要实参以实例化）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;模板模板参数的语义&lt;/h3&gt;
&lt;p&gt;我们反复强调，模板模板参数的意义是让模板参数本身也成为模板，同时模板模板参数期待的是模板名而不是类型名。&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T1, typename T2&amp;gt; 
struct S{

};
template &amp;lt;typename T, template &amp;lt;typename, typename&amp;gt; typename T1 = S&amp;gt; 
struct A{ //这里模板模板参数的默认值必须是一个模板名，不可以是S&amp;lt;T,int&amp;gt;这样的具体类型名
    T1&amp;lt;T, double&amp;gt; obj; //可以在类内通过模板名T1这样实例化。
};

int main(){
    A&amp;lt;int, S&amp;gt; obj1; //模板模板参数期待的是一个模板名。
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果换成非模板模板参数的写法，则应该是这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T1, typename T2&amp;gt; 
struct S{
};
template &amp;lt;typename T, typename T1 = S&amp;lt;T, double&amp;gt;&amp;gt; 
struct A{ //非模板模板参数的写法
    T1 obj;
};

int main(){
    A&amp;lt;int&amp;gt; obj1;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模板模板参数的匹配问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;我们的第一个例子使用模板模板参数 [C++14]：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename Elem, typename = std::allocator&amp;lt;Elem&amp;gt;&amp;gt; typename Container = std::vector&amp;gt;
class Stack {
    public:
        Container&amp;lt;T&amp;gt; elems; // container&amp;lt;T&amp;gt;会默认调用对应的容器的默认分配器
};
int main(){
    
    Stack&amp;lt;int, deque&amp;gt; sss; //一个stack，实现基于deque类，储存int类型数据
    Stack&amp;lt;int&amp;gt; ssss; //一个stack，使用默认值实现基于vector类，储存int类型数据 

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面的这段代码，是基于C++14的。我们有一个默认值。但是为什么我们显式指明了分配器？因为在C++17之前，模板模板参数必须和实际参数的模板参数匹配。因为&lt;code&gt;vector&lt;/code&gt;和&lt;code&gt;deque&lt;/code&gt;有两个模板参数，第一个是元素类型，第二个是分配器。虽然分配器有默认参数，但是默认参数也要被匹配。所以如果不写分配器就会出现缺少参数的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以如果到了C++17，我们就可以这么写。去掉分配器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename&amp;gt; typename Container = std::vector&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这个时候会有个问题，也就是此时这样做会让&lt;code&gt;stack&lt;/code&gt;模板强制要求容器模板支持第二个参数。但是比如&lt;code&gt;std::Array&lt;/code&gt;就不支持第二个参数。因为它的第二个参数是非类型模板参数。所以这样写就不行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stack&amp;lt;int, array&amp;gt; sss;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;所以如果在C++17前，我们不想指定分配器怎么办？&lt;strong&gt;方法一&lt;/strong&gt;，使用别名模板。这时候别名模板的分配器会支持默认值。
&lt;ul&gt;
&lt;li&gt;如果我们想使用array，只需要别名模板就可以。注意array的第二个参数必须给。因为没有默认值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
using deq = deque&amp;lt;T&amp;gt;; //要不然用别名模板也算是显式指定容器元素类型和分配器。
template &amp;lt;typename T&amp;gt;
using arr = array&amp;lt;T, 10&amp;gt;; //注意array的第二个参数必须给。因为没有默认值。
template &amp;lt;typename T, template &amp;lt;typename&amp;gt; typename Container&amp;gt;
class Stack {
    public:
        Container&amp;lt;T&amp;gt; elems; // container&amp;lt;T&amp;gt;会默认调用对应的容器的默认分配器
};
int main(){
    
    Stack&amp;lt;int, deq&amp;gt; sss; //一个stack，实现基于deque类，储存int类型数据
	Stack&amp;lt;int, arr&amp;gt; ssss; //一个stack，实现基于array类，储存int类型数据
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方法二，使用可变参数模板在一定程度上简化写法。但是这里array依旧不可以。因为模板参数包只能匹配相同种类的模板参数。但是array的第一个是类型模板参数，而第二个是非类型模板参数。所以想要用array依旧需要用别名模板。 ---&lt;em&gt;12.3.4章节&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename ...&amp;gt; class Container&amp;gt;   //使用可变参数模板简化后的方式 注意array不可以 array的第二个参数是非类型模板参数
class Stack {
    public:
        Container&amp;lt;T&amp;gt; elems; // container&amp;lt;T&amp;gt;会默认调用对应的容器的默认分配器
};
int main(){
    
    Stack&amp;lt;int,deque&amp;gt; ss; //一个stack，实现基于deque类，储存int类型数据

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们梳理一下那一大堆模板头的含义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                                       ↓container类的默认参数的类型。因为没用到可以忽略  ↓ container类的默认参数。可以不写
template &amp;lt;typename T, template &amp;lt;typename U, typename Alloc = allocator&amp;lt;U&amp;gt;&amp;gt; class Container = deque&amp;gt;  
           ↑这个T意思是储存T类型数据                    ↑这个Alloc是contain类的默认方法的U类型的分配器。因为没用到可以忽略  
//上面这样做会让stack模板强制要求容器模板支持第二个参数
template &amp;lt;typename T, template &amp;lt;typename ...&amp;gt; class Container&amp;gt;   //使用可变参数模板简化后的方式
template &amp;lt;typename T, template &amp;lt;typename, typename&amp;gt; class Container&amp;gt; //不能这么写，下面container&amp;lt;T&amp;gt;会少一个参数，但是你上面必须写俩必须写分配.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模板模板参数还可以做什么？---继承自类模板&lt;/h3&gt;
&lt;p&gt;当我们需要继承自一个类模板的时候，可以使用模板模板参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T1, typename T2&amp;gt; 
struct S{
    void func(){
        std::puts(__PRETTY_FUNCTION__);
        cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
    }
};
template &amp;lt;typename T, typename T1, template&amp;lt;typename, typename &amp;gt; typename T2&amp;gt;  //T2是模板模板参数，接受一个模板名
struct A: T2&amp;lt;T, T1&amp;gt;{//继承自T2类，T和T1是实例化T2所必需的模板参数
};

int main(){
    A&amp;lt;int, double, S&amp;gt; obj;
    obj.func();
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C++ 模板笔记（一）：函数模板、类模板与非类型模板参数</title><link>https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%A8%A1%E6%9D%BF/</guid><pubDate>Wed, 14 Dec 2022 14:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++ template&lt;/h1&gt;
&lt;p&gt;此笔记应和杂记3的模板相关部分搭配查看。&lt;/p&gt;
&lt;h1&gt;第一章 函数模板&lt;/h1&gt;
&lt;p&gt;https://youtu.be/XN319NYEOcE&lt;/p&gt;
&lt;h2&gt;0.0 一些深度理解，必须提前知道&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只有函数模板（function template), 没有模板函数（templated function)。类模板同理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数（主）模板并不是函数。函数模板没有类似于函数的行为。类模板同理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数主模板本身并不能调用。&lt;/li&gt;
&lt;li&gt;我们表面上是调用了函数主模板，但是实际上是编译器推导出（或我们自己指定出）类型后，编译器将所有的&lt;code&gt;T&lt;/code&gt;替换为对应类型。然后合成出来的一个新的实例。这就是实例化。&lt;/li&gt;
&lt;li&gt;所以，函数主模板本身从不是重载决议候选人（candidate）。候选人是编译器帮助我们替换了&lt;code&gt;T&lt;/code&gt;之后的实例化的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20221225200646.png&quot; alt=&quot;QQ截图20221225200646&quot; /&gt;&lt;/p&gt;
&lt;p&gt;https://youtu.be/NIDEjY5ywqU&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译器会通过函数的（主）模板，合成（synthesize）一个特化的函数声明（declaration）[其实是签名signature]。然后实例化（instantiate)这个函数的定义（definition）。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;它会首先检验函数的调用，通过查看传入的参数或显式指定的参数获取类型。&lt;/li&gt;
&lt;li&gt;然后替换所有的占位符&lt;code&gt;T&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最后我们得到了一个用于重载决议的候选对象，也就是个函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0.1 部分名词解析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;我们知道编译器处理模板代码的时候会进行替换，也就是把每一个&lt;code&gt;T&lt;/code&gt;替换成传入的模板参数。这个过程被称之为模板的实例化（ instantiation）&lt;/li&gt;
&lt;li&gt;一旦我们有了实例化的动作，我们就拥有了一个函数。这个函数被称之为特化（specialization）。在这个时候，&lt;code&gt;T&lt;/code&gt;已经被替换为对应类型。特化是特定于类型的。他们关心类型，现在不再是类型不可知得了。类模板也一样&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特化过的类模板或函数模板，或说编译器已经通过类模板和函数模板合成出来的附带了特定类型的类或函数的行为和普通的类和函数是一致的。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;但是我们依旧可以使用&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;来显式指定一个被关联到模板参数的、被进行了特殊替换的声明。比如这样：
&lt;ul&gt;
&lt;li&gt;为了区分编译器帮助我们进行的特化，针对这种情况，我们称之为显式(全)特化(explicit specialization)&lt;/li&gt;
&lt;li&gt;一般来讲，显式特化被认为是通用模板的一种特殊形式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt; // 主模板
class MyClass { 
    //一些内容
};
template&amp;lt;&amp;gt; // 显式全特化。
class MyClass&amp;lt;std::string,float&amp;gt; {
    //一些内容
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果在显式全特化后，仍需要添加一些模板参数，我们就称之为[显式]部分特化（偏特化）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; // 偏特化
class MyClass&amp;lt;T,T&amp;gt; {
    //一些内容
};
template&amp;lt;typename T&amp;gt; // 偏特化
class MyClass&amp;lt;bool,T&amp;gt; { 
    //一些内容
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0.2 显式实例化（14.5），隐式实例化和特化。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
}

template
void func&amp;lt;float&amp;gt;(float a); //2 显式实例化

template
void func&amp;lt;&amp;gt;(int a); //也可以

int main(){
    func(10); //1 隐式实例化
    func&amp;lt;int&amp;gt;(10); //也叫隐式实例化。没错，显式指明模板参数会导致隐式实例化。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;这个过程叫做隐式实例化。隐式实例化指的是函数或类模板被使用或调用的时候，由编译器帮助我们处理的过程。因为编译器帮助我们合成了这个函数的声明，实例化了函数的定义。这个&lt;code&gt;func(10)&lt;/code&gt;本身是一个特化的函数。&lt;/li&gt;
&lt;li&gt;这个过程叫做显式实例化，也叫做外部实例化。显示实例化的意义在于在不发生函数调用的时候就已经将函数模板实例化，或者在不使用类模板的时候就已经将类模板实例化称之为模板显示实例化。这个&lt;code&gt;void func&amp;lt;float&amp;gt;(float a)&lt;/code&gt;函数本身也是一个特化的函数。
&lt;ul&gt;
&lt;li&gt;显式实例化只需要写声明，不需要写定义。也就是我们通知编译器，直接根据这个声明，实例化这个函数定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;无论是编译器帮助我们合成后实例化的，还是我们自己显式特化的，只要这个函数有了明确的类型要求，这就是一个特化的函数。（和函数模板相比）&lt;/li&gt;
&lt;li&gt;显式特化在上一节。&lt;/li&gt;
&lt;li&gt;显示实例化和显式特化的语法区别在于显式实例化的&lt;code&gt;template&lt;/code&gt;关键字后不加&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;，显式特化的&lt;code&gt;template&lt;/code&gt;关键词后要加&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当一个函数模板被定义在一个头文件中且被多个源文件包含时，每个包含该头文件的源文件都会产生模板的实例化。通常情况下，因为模板是内联的，这不会导致链接错误。然而，如果你显式地实例化了模板函数或者类模板，则可能会遇到链接问题。显式实例化告诉编译器在给定的翻译单元（通常是一个.cpp文件）中创建模板的具体实现。如果在多个源文件中进行了相同模板的显式实例化，那么链接器将报告重复定义错误。当然了, 如果使用了显式实例化, 则在其他需要用到该模板的地方需要使用外部模板声明.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;extern template void func&amp;lt;int&amp;gt;(int a);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;extern 模板声明&lt;/strong&gt;用于防止模板在当前翻译单元中被实例化. 这种情况下, 该函数的调用不会导致模板在当前翻译单元中实例化, 因为已经在外部单元实例化过了.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/2351148/explicit-template-instantiation-when-is-it-used/59614755#59614755&quot;&gt;显示实例化的各种方式, 详细例子, 和优缺点分析&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;什么时候用extern&lt;/h3&gt;
&lt;p&gt;通常来说, 我们知道模板 的声明和定义需要写在一起. 如果这样做, 我们最好在外部使用&lt;code&gt;extern template&lt;/code&gt;减少文件大小. 原因是所有&lt;code&gt;include&lt;/code&gt;了该头文件的翻译单元都会合成一份独立的函数模板, 最后在链接期间丢弃多余的符号. 比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// a.h
#pragma once
#include &amp;lt;iostream&amp;gt;
struct f {
    template &amp;lt;typename T&amp;gt;
    void ReallyBigFunction();
};

template&amp;lt;typename T&amp;gt;
void f::ReallyBigFunction(){
    std::cout &amp;lt;&amp;lt; &quot;ReallyBigFunction&quot; &amp;lt;&amp;lt; std::endl;
}

// b.cpp
#include &quot;a.h&quot;
#include &quot;iostream&quot;

extern template void f::ReallyBigFunction&amp;lt;int&amp;gt;(); // 注意这里 
void something1()
{
    f o;
    o.ReallyBigFunction&amp;lt;int&amp;gt;();
}

// c.cpp
#include &quot;a.h&quot;
#include &quot;iostream&quot;

void something2()
{
    f o;
    o.ReallyBigFunction&amp;lt;int&amp;gt;();
}

int main(){
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240429224802.png&quot; alt=&quot;微信图片_20240429224802&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到图片里, 如果我们在&lt;code&gt;b.o&lt;/code&gt;不加&lt;code&gt;extern&lt;/code&gt;, 那么&lt;code&gt;b.o&lt;/code&gt;和 &lt;code&gt;c.o&lt;/code&gt;都生成了同一个函数的符号.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240429224809.png&quot; alt=&quot;微信图片_20240429224809&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果有&lt;code&gt;extern&lt;/code&gt;, 则&lt;code&gt;b.o&lt;/code&gt;里的符号变成了&lt;code&gt;U&lt;/code&gt; 而&lt;code&gt;c.o&lt;/code&gt;里保持不变.&lt;/p&gt;
&lt;p&gt;然而如果我们用另一种写法, 也就是&lt;code&gt;.h&lt;/code&gt;文件只包含模板声明, 模板定义在&lt;code&gt;.cpp&lt;/code&gt;文件中, 同时在&lt;code&gt;.cpp&lt;/code&gt;文件中&lt;strong&gt;显式实例化&lt;/strong&gt;所需模板. 这样做的话, 其他文件就不需要使用&lt;code&gt;extern&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// a.h
#pragma once
#include &amp;lt;iostream&amp;gt;
struct f {
    template &amp;lt;typename T&amp;gt;
    void ReallyBigFunction();
};

// a.cpp
#include &quot;a.h&quot;
#include &amp;lt;iostream&amp;gt;

template&amp;lt;typename T&amp;gt;
void f::ReallyBigFunction(){
    std::cout &amp;lt;&amp;lt; &quot;ReallyBigFunction&quot; &amp;lt;&amp;lt; std::endl;
}

template void f::ReallyBigFunction&amp;lt;int&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;显示实例化和隐式实例化在类模板中的差异&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;隐式实例化的时候，编译器会合成尽可能少的成员，但是显式实例化会让类模板合成全部的成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然而，成员函数可以被单独地显式实例化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://youtu.be/XN319NYEOcE?t=3152&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230227022726.png&quot; alt=&quot;QQ截图20230227022726&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的实例化既代表了过程也代表了最后的实体&lt;/p&gt;
&lt;p&gt;最后的总结就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们可以通过显式实例化或隐式实例化的方式达成隐式特化的目标。（这一阶段就是编译器默认合成出来的函数或类）&lt;/li&gt;
&lt;li&gt;同时我们也可以通过显式特化的方式来特化一个模板。（这个指的是通过显式指定类型来让编译器和成出函数或类）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0.3 声明和定义&lt;/h2&gt;
&lt;p&gt;我们在杂记当中已经解释了什么是声明和定义，这里针对上下文再次强化一下认知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“声明”将一个名称引入或者再次引入到一个 C++作用域内。引入的过程中可能会包含这个名称的一部分类别，但是一个有效的声明并不需要相关名称的太多细节。比如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class C; // 类C的声明
void f(int p); // 函数f的声明
extern int v; // 变量v的声明。注意这里使用了extern
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对于声明 ,如果其细节已知, 或者是需要申请相关变量的存储空间，那么声明就变成了定义。
&lt;ul&gt;
&lt;li&gt;对于 类类型的定义和函数定义，意味着需要提供一个包含在&lt;code&gt;{}&lt;/code&gt;中的主体，或者是对函数使用了&lt;code&gt;=defaut/=delete&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对于变量，如果进行了初始化或者&lt;strong&gt;没有使用 &lt;code&gt;extern&lt;/code&gt;&lt;/strong&gt;，那么声明也会变成定义。下面是一些“定义”的例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class C {}; // 声明并定义 类C
void f(int p) { //声明并定义函数f
	std::cout &amp;lt;&amp;lt; p &amp;lt;&amp;lt; ’\n’;
}
extern int v = 1; // 因为初始化了，所以这里也是声明并定义了变量v
int w; // 全局变量，就算没有初始化，由于没有使用extern，所以也是声明并定义变量w
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果一个类模板或者函数模板有包含在&lt;code&gt;{}&lt;/code&gt;中的主体的话，那么声明也会变成定义。所以&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func (T); //这个是声明。

template&amp;lt;typename T&amp;gt;
class S {}; //这个是定义。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0.4 模板形参和模板实参&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1&amp;gt; // T1 是模板形参，形式上的参数。 parameter
class test;
int main(){
  test&amp;lt;int&amp;gt; obj; //int 是模板实参，实际传入的参数。 argument
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来讲可以说：模板参数是被模板实参初始化的。或者更准确的说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板参数是那些在模板定义或者声明中，出现在 template 关键字后面的尖括号中的名称。&lt;/li&gt;
&lt;li&gt;模板实参是那些用来替换模板参数的内容。不同于模板参数，模板实参可以不只是名称。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0.5 模板名（template-name) 和 模板标识（template-id)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;模板名，顾名思义就是模板的名字。如下面例子的&lt;code&gt;Demo&lt;/code&gt;就是模板名。&lt;/li&gt;
&lt;li&gt;模板标识，是模板名和其参数列表。也就是整个&lt;code&gt;模板名&amp;lt;形参列表&amp;gt;&lt;/code&gt;是一个模板标识。下面的&lt;code&gt;Demo&amp;lt;int&amp;gt;&lt;/code&gt;就是模板标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
struct Demo{ 
    // ... 
};

int main()
{
   Demo &amp;lt;int&amp;gt; d; // Demo 是 template name, Demo&amp;lt;int&amp;gt; 是 template-id
   // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/templates&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/3796558/difference-between-template-name-and-template-id&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当指出模板的模板标识 的时候，用模板实参替换模板参数的过程就是显式的，但是在很多情况这一替换则是隐式的（比如模板参数被其默认值替换的情况。一个基本原则是：任何模板实参都必须是在编译期可知的。就如接下来会澄清的，这一要求对降低模板运行期间的成本很有帮助。由于模板参数最终都会被编译期的值进行替换，它们也可以被用于编译期表达式。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0.6 不推导语境&lt;/h2&gt;
&lt;p&gt;因为比较分散，所以请在笔记内搜索关键词： 不推导语境&lt;/p&gt;
&lt;h2&gt;1.1.2 基本信息&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在声明模板变量类型的时候，尽量使用typename而非class。尽管没有区别。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;s&gt;所有的模板类型参数都应该可以被推导或有默认值。不应该无法推导的模板参数，会导致错误。（个人推理）&lt;/s&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数模板有两种类型的参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板参数：位于函数模板名称的前面，在一对尖括号内部进行声明：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;　　　　　　 //T是模板参数
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;调用参数：位于函数模板名称之后，在一对圆括号内部进行声明：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;T max (T const&amp;amp; a, T const&amp;amp; b)　　 //a和b都是调用参数
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一定要让函数模板的所有重载版本的声明都位于它们被调用的位置之前（一般都放在同一个头文件中）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1.3 二阶段检查。搭配深度探索对象模型里面的模板一起看。&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模板的工作原理，并不是把模板编译成一个可以处理任何类型的单一实体；而是对于实例化模板参数的每种类型，都从模板产生（合成）出一个不同的实体。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在实例化模板的时候，如果模板参数类型不支持所有模板中用到的操作符，将会遇到编译期错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是在定义的地方并没有遇到错误提示。这是因为模板是被分两步编译的。所以我们可以说：在实例化时，模板被编译了两次，分别发生在模板定义阶段（实例化之前），和模板实例化阶段（实例化期间）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在&lt;strong&gt;模板定义阶段&lt;/strong&gt;，模板的检查并&lt;strong&gt;不包含类型参数的检查&lt;/strong&gt;。只包含下面几个方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语法检查。比如少了分号。&lt;/li&gt;
&lt;li&gt;使用了未定义的不依赖于模板参数的名称（类型名，函数名，......）。&lt;/li&gt;
&lt;li&gt;未使用模板参数的 &lt;code&gt;static assertions&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;简而言之，就是先检查模板代码本身，查看语法是否正确；在这里会发现错误语法，如遗漏分号等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;strong&gt;模板实例化阶段&lt;/strong&gt;，为确保所有代码都是有效的，&lt;strong&gt;模板会再次被检查，尤其是那些依赖于类型参数的部分&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查模板代码，查看是否所有的调用都有效。在这里会发现无效的调用，如该实例化类型不支持某些函数调用等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void foo(T t)
{
	undeclared(); // 如果 undeclared()未定义，第一阶段就会报错，因为与模板参数无关
	undeclared(t); //如果 undeclared(t)未定义，第二阶段会报错，因为与模板参数有关
	static_assert(sizeof(int) &amp;gt; 10,&quot;int too small&quot;); // 与模板参数无关，总是报错
	static_assert(sizeof(T) &amp;gt; 10, &quot;T too small&quot;); //与模板参数有关，只会在第二阶段报错
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;需要注意的是，有些编译器并不会执行第一阶段中的所有检查。因此如果模板没有被至少实例化一次的话，你可能一直都不会发现代码中的常规错误。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1.1.4 编译和链接&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;由于 &lt;strong&gt;C++中使用的是静态模板的机制&lt;/strong&gt;，所以当使用函数模板，并且引发模板实例化的时候，编译器（在某时刻）需要查看模板的定义。这就不同于普通函数中编译和链接之间的区别，因为对于普通函数而言，只要有该函数的声明（不需要定义），就可以顺利通过编译。因为在这里需要的是定义，所以可以考虑在头文件内部实现每个模板以使编译器能够顺利的找到模板的定义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假设我们有一个模板类的&lt;code&gt;.h&lt;/code&gt;和一个&lt;code&gt;.cpp&lt;/code&gt;文件，还有一个使用了模板类的&lt;code&gt;.cpp&lt;/code&gt;文件，这个文件&lt;code&gt;include&lt;/code&gt;了&lt;code&gt;.h&lt;/code&gt;文件而不是&lt;code&gt;.cpp&lt;/code&gt;文件。首先头文件不编译。声明被拷贝至使用的文件。这时候我们使用了这个模板。由于C++是分离编译。所以在当前文件下没有问题，会留下函数符号让链接器去寻找。然后我们开始编译模板的&lt;code&gt;.cpp&lt;/code&gt;文件。模板是二段式编译。也是由于是分离编译，但是在模板的&lt;code&gt;.cpp&lt;/code&gt;中，我们只有函数定义但是没有使用函数。所以只会进行第一段也就是检查是否有错误。因为没有实例化所以根本不会进行第二段的编译。所以这时候我们使用的文件中就会有无法解析的外部符号了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以这个时候我们要么把&lt;code&gt;.h&lt;/code&gt;和&lt;code&gt;.cpp&lt;/code&gt;文件放到一起写然后&lt;code&gt;include&lt;/code&gt; &lt;code&gt;.hpp&lt;/code&gt;文件，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要么就在在使用文件中不&lt;code&gt;include&lt;/code&gt; &lt;code&gt;.h&lt;/code&gt; 而是&lt;code&gt;include&lt;/code&gt; &lt;code&gt;.cpp&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.2 函数模板参数类型推断&lt;/h2&gt;
&lt;p&gt;没啥特别要说的，注意两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个是模板形参是万能引用&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;的时候触发的引用折叠。&lt;/li&gt;
&lt;li&gt;另一个是&lt;strong&gt;利用函数参数自动推导出的模板参数右值引用&lt;/strong&gt;，只可能推导出左值引用或者基本类型。这点看下面的推导表格就能发现，&lt;code&gt;T&lt;/code&gt;永远不会被推导成&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;。都是推导成非引用或左值引用版本，然后和&lt;code&gt;T&lt;/code&gt;后附加的&lt;code&gt;&amp;amp;&lt;/code&gt;或&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;形成引用折叠
&lt;ul&gt;
&lt;li&gt;注意是永远不会推导出而不是永远不会成为。这也说明了为什么&lt;code&gt;std::forward&lt;/code&gt;在一般情况下触发的都是左值版本，除非显式指明为右值引用如&lt;code&gt;forward&amp;lt;int&amp;amp;&amp;amp;&amp;gt;(5)&lt;/code&gt;。参考杂记1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2.1 函数模板参数类型推导中的类型转换&lt;/h3&gt;
&lt;p&gt;下文的A是实参类型，P是形参类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在类型推断的时候自动（隐式）的类型转换是受限制的&lt;/strong&gt;。&lt;strong&gt;因为类型转换是重载决议做的事情，类型推断不负责。而且类型推导发生在重载决议前面。&lt;/strong&gt;（所以如果它通过入参推导出来的类型和函数形参不匹配，就不行）：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;首先&lt;em&gt;实参&lt;/em&gt;的引用属性被忽略（A 的引用属性被忽略.）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果 形参是有 cv 限定的类型，那么推导时会忽略顶层 cv 限定符&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果调用参数是按引用传递的，任何类型转换都不被允许。通过模板类型参数 T 定义的两个参数，它们实参的类型必须完全一样。每个 T 都必须正确匹配。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果形参是引用类型，那么用形参所引用的类型推导。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果调用参数是按值传递的（形参是非引用的时候），那么只有退化（decay）这一类简单转换是被允许的&lt;/strong&gt;：（和杂记里面的函数模板参数推导一样）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;volatile &lt;/code&gt;限制符会被忽略 （实参 的 &lt;strong&gt;顶层&lt;/strong&gt; cv 限定符被忽略.）&lt;/li&gt;
&lt;li&gt;raw array（原始数组） 和函数被转换为相应的指针类型。（如果 A 是数组或函数, P 是值时, 数组和函数退化为指针. ）&lt;/li&gt;
&lt;li&gt;（隐藏，此处不相干，但还是放在这里。和杂记中的函数模板参数推导一样）如果 P 是无 cv 限定符的转发引用 (即 T&amp;amp;&amp;amp;), 且 A 是左值时, T 被推导为左值引用.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通过模板类型参数 T 定义的两个参数，它们实参的类型在退化（decay） 后也必须一样&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;总结就是如果P是引用类型则不退化。P不是引用类型则发生上面的退化。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通常而言，你必须指定&lt;strong&gt;最后一个不能被隐式推导的模板实参之前的&lt;/strong&gt;所有实参类型。也就是我们必须显式指定所有模板参数的类型，直到某一个模板参数的类型可以被推断出来为止。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然而，模板的实参推导并不适合返回类型（可以把推导看成是重载解析的一部分--重载解析是一个不依赖于返回类型选择的过程，唯一的例外就是转型操作符成员的返回类型）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里搭配7.2看。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct typewrapper{
    typewrapper(T args){

    }
};
void foo(typewrapper&amp;lt;tmp&amp;gt; f){
    std::puts(__PRETTY_FUNCTION__);
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename... T&amp;gt;
void foo0(typewrapper&amp;lt;T...&amp;gt; f){
    std::puts(__PRETTY_FUNCTION__);
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}

void foo1(function&amp;lt;void(int, int, int)&amp;gt;){
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename... Args&amp;gt;
void foo2(function&amp;lt;void(Args...)&amp;gt;){
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}


template&amp;lt;typename... Args&amp;gt;
void foo3(Args... F ){
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    foo(typewrapper&amp;lt;tmp&amp;gt;(tmp{}));
    foo(tmp{});

    
    foo0(typewrapper&amp;lt;tmp&amp;gt;(tmp{}));
    foo0(tmp{}); //不行

    foo1(function&amp;lt;void(int, int, int)&amp;gt;(callablefunc));
    foo1(callablefunc);

    foo2(function&amp;lt;void(int, int, int)&amp;gt;(callablefunc));
    foo2(callablefunc); //不行
    
    foo3(function&amp;lt;void(int, int, int)&amp;gt;(callablefunc));
    foo3(callablefunc); //可以
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为啥&lt;code&gt;foo0&lt;/code&gt;和&lt;code&gt;foo2&lt;/code&gt;的第二个都不行呢？因为参数类型推导中有冲突。但是&lt;code&gt;foo&lt;/code&gt;和&lt;code&gt;foo1&lt;/code&gt;都是非模板函数，或者说，参数推导没有冲突。比如&lt;code&gt;foo3&lt;/code&gt;。这样的都可以。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为，&lt;code&gt;foo0&lt;/code&gt;的参数类型涉及到推导的部分应该是&lt;code&gt;typewrapper&lt;/code&gt;模板的模板参数类型。所以&lt;code&gt;T&lt;/code&gt;会被推导为&lt;code&gt;tmp&lt;/code&gt;类型。但是这个&lt;code&gt;foo0&lt;/code&gt;函数的参数所要求的类型应该是&lt;code&gt;typewrapper&amp;lt;T&amp;gt;&lt;/code&gt;类型，两者类型不匹配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是：&lt;code&gt;T&lt;/code&gt;类型被推导为&lt;code&gt;tmp&lt;/code&gt;，所以函数要求形参为&lt;code&gt;typewrapper&amp;lt;tmp&amp;gt;&lt;/code&gt;类型，但是传入的参数类型为&lt;code&gt;tmp&lt;/code&gt;类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;foo2同理。&lt;code&gt;foo2&lt;/code&gt;的参数类型涉及到推导的部分应该是&lt;code&gt;std::function&lt;/code&gt;模板的模板参数类型。所以&lt;code&gt;Args...&lt;/code&gt;会被推导为&lt;code&gt;int, int, int&lt;/code&gt;类型。但是这个&lt;code&gt;foo2&lt;/code&gt;函数的参数要求的类型应该是&lt;code&gt;function&amp;lt;void(Args...)&amp;gt;&lt;/code&gt;类型，两者类型不匹配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是：&lt;code&gt;Args...&lt;/code&gt;类型被推导为&lt;code&gt;int, int, int&lt;/code&gt;，所以函数要求形参为&lt;code&gt;function&amp;lt;void(int, int, int)&amp;gt;&lt;/code&gt;类型，但是传入的参数类型为&lt;code&gt;void(*)(int, int, int)&lt;/code&gt;类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;至于&lt;code&gt;foo3&lt;/code&gt;为什么可以，因为&lt;code&gt;foo3&lt;/code&gt;的&lt;code&gt;Args...&lt;/code&gt;可以被推导为任何可能的类型，不涉及到需要类型转换的部分。比如如果传入函数指针，则会被推导为&lt;code&gt;void (*)(int, int, int)&lt;/code&gt;。如果传入&lt;code&gt;std::function&lt;/code&gt;对象，则会被推导为&lt;code&gt;std::function&amp;lt;void(int, int, int)&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230512231804.png&quot; alt=&quot;QQ截图20230512231804&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;注意区别显式指定模板实参类型（不推导）和显式全特化&lt;/h3&gt;
&lt;p&gt;显式全特化是替换定义。&lt;/p&gt;
&lt;p&gt;显式指定模板的实参类型是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void func(T a, T b){
    cout &amp;lt;&amp;lt;&quot;success&quot; &amp;lt;&amp;lt; endl;
}
int main() {
    long lng = 2000;
    func&amp;lt;int&amp;gt;(lng, 2); //显式指定模板实参类型
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们显式指定模板实参类型就等于避免了参数类型推断。所以可以隐式转换了！&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如上面的函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func(lng, 2);//不行。一个是long一个是int。但是只有一个T类型
func&amp;lt;int&amp;gt;(lng, 2); //可以。显式指定T为int，lng被隐式转换了。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2.2 对默认调用参数的类型推断&lt;/h3&gt;
&lt;p&gt;需要注意的是，类型推断并不适用于默认调用参数。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T a = 12){
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    func(1); //OK 没问题 推导为int
    func(); //编译器发出抱怨，甚至想骂人。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为应对这一情况，你需要给模板类型参数也声明一个默认参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T = int&amp;gt; //注意这里
void func(T a = 12){ //这里
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    func(); //OK
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意，给定的模板默认参数应该和函数默认参数的类型一致。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;1.3 多个模板参数&lt;/h2&gt;
&lt;p&gt;好像是废话，但是水还是蛮深的。我们一起看一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
T1 func(T1 a, T2 b){
    return b &amp;lt; a ? a : b;
}
int main(){
    auto ret = func(1,2.345);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;看上去就和我们想的一样，它可以接受两个不同类型的调用参数。但是如示例代码所示，这也导致了一个问题:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果你使用其中一个类型参数的类型作为返回类型，不管是不是和调用者预期地一样，当应该返回另一个类型的值的时候，返回值会被做类型转换。这将导致返回值的具体类型和参数的传递顺序有关。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;例子：如果传递 &lt;code&gt;2.345&lt;/code&gt; 和&lt;code&gt; 1&lt;/code&gt; 给这个函数模板，返回值是 &lt;code&gt;double&lt;/code&gt; 类型 的 &lt;code&gt;2.345&lt;/code&gt;，但是如果传递 &lt;code&gt;1&lt;/code&gt; 和 &lt;code&gt;2.345&lt;/code&gt;，返回值却是 &lt;code&gt;int&lt;/code&gt; 类型的 &lt;code&gt;2&lt;/code&gt;。因为我们固定返回较大的数字。但是返回值类型会被进行隐式转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们有三个方法解决这个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入第三个模板参数作为返回类型。&lt;/li&gt;
&lt;li&gt;让编译器找出返回类型。&lt;/li&gt;
&lt;li&gt;将返回类型定义为两个参数类型的“公共类型。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.3.1 做为返回类型的模板参数&lt;/h3&gt;
&lt;p&gt;我们还记得模板有两种参数：模板参数和调用参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当模板参数和调用参数之间没有必然的联系，且模板参数不能确定的时候，就要显式的指明模板参数。比如你可以引入第三个模板来指定函数模板的返回类型&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename RT, typename T1, typename T2&amp;gt;
RT func(T1 a, T2 b){
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;**但是模板类型推断不会考虑返回类型，而 &lt;code&gt;RT&lt;/code&gt; 又没有被用作调用参数的类型。因此 &lt;code&gt;RT&lt;/code&gt; 不会被推导。**这样就必须显式的指明模板参数的类型。比如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename RT, typename T1, typename T2&amp;gt;
RT func(T1 a, T2 b){
    cout &amp;lt;&amp;lt; &quot;called&quot; &amp;lt;&amp;lt; endl;
}
int main(){ 
    func&amp;lt;int&amp;gt;(1,2.345); //注意。后面两个可以省略。因为可以被推导。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;通常而言，我们必须显式指定所有模板参数的类型，直到某一个模板参数的类型可以被推断出来为止。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1.3.2 利用auto，decltype 和（可选的）尾置返回类型进行返回类型推导&lt;/h3&gt;
&lt;p&gt;C++14开始可以使用&lt;code&gt;auto&lt;/code&gt;和&lt;code&gt;decltype &lt;/code&gt;搭配（可选的）尾置返回类型对返回值类型进行推导。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt; //不使用尾置返回类型
auto func(T1&amp;amp; a, T2&amp;amp; b){
    return b &amp;lt; a? a: b;
}


template&amp;lt;typename T1, typename T2&amp;gt; //使用尾置返回类型
auto func(T1&amp;amp; a, T2&amp;amp; b) -&amp;gt; decltype(b&amp;lt;a? a:b){
    return b &amp;lt; a? a: b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在不使用尾置返回类型（trailing return type）的情况下将&lt;code&gt; auto&lt;/code&gt; 用于返回类型，要求返回类型必须能够通过函数体中的返回语句推断出来。&lt;strong&gt;当然，这&lt;/strong&gt;首先要求返回类型能够从函数体中推断出来&lt;/strong&gt;。因此，必须要有这样可以用来推断返回类型的返回语句，而且&lt;strong&gt;多个返回语句之间的推断结果必须一致。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;注意&lt;code&gt;decltype&lt;/code&gt;推导过程是在编译期完成的，&lt;strong&gt;并且不会真正计算表达式的值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;此处要注意可能返回值类型可能会被推导为引用类型。请查看下面的文章。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;decltype&lt;/code&gt;过于复杂，可以看这里&lt;a href=&quot;https://www.cnblogs.com/5iedu/p/11222078.html&quot;&gt;decltype详解&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;尾置返回类型的 auto&lt;/h4&gt;
&lt;p&gt;尾置返回类型的 auto是占位符，所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto (*fp)() -&amp;gt; auto = f;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个情况下，第一个&lt;code&gt;auto&lt;/code&gt;仅仅是占位符，语法层面的表达。只有第二个&lt;code&gt;auto&lt;/code&gt;才会进行类型推导。来自&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/auto.html&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;1.3.3 利用std::common_type将返回类型声明为公共类型（common type)&lt;/h3&gt;
&lt;p&gt;就是获得两个类型的公共类型。&lt;/p&gt;
&lt;h2&gt;1.4 模板默认参数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;可以在模板默认参数中使用&lt;code&gt;common_type&lt;/code&gt;或者是&lt;code&gt;decay&lt;/code&gt;来获取默认值。比如&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2, typename RT = std::decay_t&amp;lt;decltype(true ? T1() : T2())&amp;gt;&amp;gt;
//也可以这样typename RT = std::common_type_t&amp;lt;T1,T2&amp;gt;&amp;gt;
RT max (T1 a, T2 b){
return b &amp;lt; a ? a : b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;和函数默认值不同，在函数模板参数中，即使后面的模板参数没有默认值，我们依然可以让第一个模板参数有默认值。并且此时调用时可以不显式指定&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename RT = int, typename T1, typename T2&amp;gt;
RT func(T1 a, T2 b){
    return a;
}
int main(){
    auto c = func(1,2.234); //并且此时调用时可以不显式指定&amp;lt;&amp;gt;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1.5 函数模板重载和显式全特化&lt;/h2&gt;
&lt;p&gt;像普通函数一样，函数模板也是可以重载的。也就是说，你可以定义多个有相同函数名的函数，当实际调用的时候，由 C++编译器负责决定具体该调用哪一个函数。即使在不考虑模板的时候，这一决策过程也可能异常复杂。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;和普通函数一样，函数模板可以被重载。不止可以被重载，还可以进行特化。但是函数模板只能全特化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;决议顺序：普通函数 &amp;gt; 主模板（未特化的模板） &amp;gt; 全特化版本。因为越特化的可能越是需要的
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当两个都为模板版本，则调用更特化的模板函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;因为越特化的可能是越实际要求的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;如果函数名指名了某个函数模板，那么首先进行模板实参推导&lt;/li&gt;
&lt;li&gt;如果它成功，那么将会生成一个单独的模板特化并添加到所要考虑的重载集合中。
&lt;ul&gt;
&lt;li&gt;这一步骤结合0中提到的，通过函数模板合成一个函数，然后添加进重载决议候选人集合中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果集合中有多于一个函数与目标匹配，且至少一个函数是非模板，那么从考虑集合中去除模板特化。
&lt;ul&gt;
&lt;li&gt;这一步是&lt;strong&gt;普通函数优先于模板合成出的函数&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果所有剩余候选者都是模板特化，那么当存在更特殊的模板特化时，移除&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/partial_specialization&quot;&gt;较不特殊&lt;/a&gt;者。如果在各项移除之后还有多于一个候选者，那么程序非良构。
&lt;ul&gt;
&lt;li&gt;这一步是&lt;strong&gt;如果剩余的函数都是模板函数，那么最特化的优先&lt;/strong&gt;。如果在这之后还是多个函数，则会有隐含问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;关于重载决议和匹配，写在文末。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果函数模板和普通函数都可以实现（同名的函数模板可以被实例化为与非模板函数具有相同类型的调用参数），&lt;strong&gt;在所有其它因素都相同的情况下，优先调用普通函数而不是从模板实例化出来的函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果模板可以实例化出一个更匹配的函数，那么就会优先调用模板函数。&lt;/strong&gt;（&lt;strong&gt;！！！！！！比如如果普通函数涉及到任何级别的类型转换的时候，如果模板能直接合成出一个完美匹配的函数模板，则此时会调用模板合成的函数！！！！！！&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可以使用空模板参数列表&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;来强制调用函数模板&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在模板参数推断时不允许自动类型转换，而常规函数是允许的&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当有多个特化的函数模板可以被匹配的时候，会有二义性导致报错。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数模板显式全特化不能包含默认实参值。然而，对于被特化的模板所指定的任何默认实参，显式特化版本都可以应用这些默认实参值（其参数默认值会被保留）。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T f(T a, T b = 42)
{   
    cout &amp;lt;&amp;lt;&quot;general&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    return b;
}
template&amp;lt;&amp;gt; 
float f &amp;lt;float&amp;gt;(float a, float b = 42.2) // 错误
{
    cout &amp;lt;&amp;lt;&quot;special&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    return b;
}
template&amp;lt;&amp;gt; 
double f &amp;lt;double&amp;gt;(double a, double b) // 可以
{
    cout &amp;lt;&amp;lt;&quot;special&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    return b;
}
f(4); //输出general 42
f(4.444); //输出special 42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;！！！注意，只有非模板和主模板重载参与重载决议。显式全特化并不是重载也从不参与重载决议，因此此时不受考虑。只有在重载决议选择最佳匹配的主函数模板后，才会考虑它的全特化版本以查看最佳匹配者。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;因为显式全特化的函数模板没有独立名称。也就是不引入名称。因为它只是一个替换的定义，而不是一个替换的声明。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;显式全特化不是重载。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
void f(T);    // #1：所有类型的重载
template&amp;lt;&amp;gt;
void f(int*); // #2：#1 的特化，针对指向 int 的指针
template&amp;lt;class T&amp;gt;
void f(T*);   // #3：所有指针类型的重载
 
f(new int(1)); // 调用 #3，虽然 #1 的特化是完美匹配
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void test(int a, int b){
    cout &amp;lt;&amp;lt; &quot;普通函数&quot; &amp;lt;&amp;lt; endl;
}

template &amp;lt;typename T&amp;gt;
void test(T a, T b){
    cout &amp;lt;&amp;lt; &quot;模板函数&quot; &amp;lt;&amp;lt; endl;
}

template &amp;lt;typename T&amp;gt;
void test(T a, T b, T c){
    cout &amp;lt;&amp;lt; &quot;重载的模板函数&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;&amp;gt;
void test&amp;lt;int&amp;gt;(int a, int b, int c){
//void test(int a, int b, int c){ //可以去掉 &amp;lt;int&amp;gt;
    cout &amp;lt;&amp;lt;&quot;全特化&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    test(1,2); //输出普通函数
    test&amp;lt;&amp;gt;(1,2); //使用空模板参数列表 输出模板函数
    test(1.1,2.2,3.3); //输出重载的模板函数
    test(1,2,3); //输出全特化
    test&amp;lt;int&amp;gt;(1,2,3); //输出全特化
    return 0;
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么后两个都输出全特化？因为他们都是&lt;code&gt;int&lt;/code&gt;。多个候选者都为模板的时候，更特化的优先。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目前只有两个模板可以接受三个参数。而此时&lt;code&gt;int&lt;/code&gt;可以匹配到下面的全特化版本，所以全特化版本优先。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;倒数第三个因为是&lt;code&gt;float&lt;/code&gt;。这样由于模板匹配的优先级大于类型转换。所以会被匹配到重载的模板函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通常而言，在重载模板的时候，要尽可能少地做改动。你应该只是改变模板参数的个数或者显式的指定某些模板参数。比如不要一会儿值传递一会儿引用传递。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数的全特化是非常不好的设计。全特化提供的是一个替换的定义，而不是一个替换的声明。在调用函数模板的时点，该调用已经完全基于函数模板而完成解析了 [16.3.2]。所以：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无法在没有主模板的时候对函数模板进行全特化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;再次重申全特化的函数没有新的mangled的名字。也就是全特化函数没有独立名字。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类成员函数的全特化必须写在类外。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果是分文件编译，为了预防重定义导致的链接错误，必须把全特化的函数模板定义为&lt;code&gt;inline&lt;/code&gt;防止重定义。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为在链接器的眼里，函数模板在全特化之后和常规函数是一样的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;函数模板重载的例子&lt;/h3&gt;
&lt;p&gt;下面的重载都是合法的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//允许参数顺序（因为类型顺序不同）不同
template&amp;lt;typename T1,typename T2&amp;gt;
void func(T1, T2){

}
template&amp;lt;typename T1,typename T2&amp;gt;
void func(T2, T1){
    
}
//允许返回值类型不同
template&amp;lt;typename T1&amp;gt;
int func1(T1){

}
template&amp;lt;typename T1&amp;gt;
char func1(T1){
    
}

//允许模板形参个数不同，允许函数形参个数不同
template&amp;lt;typename T1,typename T2&amp;gt;
void func2(T1, T2){
    cout &amp;lt;&amp;lt;&quot;2 typename&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T1&amp;gt;
void func2(T1, T1){
    cout &amp;lt;&amp;lt;&quot;1 typename &quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T1&amp;gt;
void func2(T1){
    cout &amp;lt;&amp;lt;&quot;single param&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://blog.csdn.net/qq_41453285/article/details/104447573&lt;/p&gt;
&lt;h3&gt;关于不要全特化函数和函数全特化的问题写在了最后面&lt;/h3&gt;
&lt;h2&gt;所谓的类型转换和模板参数推导的关系&lt;/h2&gt;
&lt;p&gt;我们之前说过, &lt;strong&gt;在模板参数推断时不允许自动类型转换，而常规函数是允许的&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T A, T B){
    std::puts(__PRETTY_FUNCTION__);
}
int main(){ 
    func(123.1, 2); //不可以
    func&amp;lt;int&amp;gt;(123.1, 2);  //可以
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是为什么上面的第二种就可以呢？&lt;/p&gt;
&lt;p&gt;关键在于 &lt;strong&gt;在模板参数推导时&lt;/strong&gt;不允许隐式类型转换。也就是当推导T的过程中，如果发现类型不匹配，不允许在这个时候发生隐式类型转换（把参数换成其他类型）。原因之前提到过，也就是类型转换是重载决议负责的，而不是参数推导负责的。所以第一种不可以，因为推断的时候通过第一个参数推断出&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;double&lt;/code&gt;，但是第二个参数推断出&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;int&lt;/code&gt;，发生了冲突，而此时不可以把第二个参数类型隐式转为&lt;code&gt;double&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为什么第二行可以呢？是因为我们指明了模板参数。这个时候相当于跳过了推导这一阶段，因为我们通过显式指定模板参数的方式隐式实例化了对应函数。然后进行重载决议的时候就可以进行类型转换了。所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
struct typewrapper{
    typewrapper(T args){
        cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
    }
    operator T(){
        cout &amp;lt;&amp;lt;&quot;converted&quot; &amp;lt;&amp;lt; endl;
        return T();
    }
};
class tmp{
    public:
    string _text;
    tmp() = default;
    tmp(string c): _text(c) {}			//单参构造，可以看做转换构造

};
template&amp;lt;typename T&amp;gt;
void foo3(T A, T B){
    std::puts(__PRETTY_FUNCTION__);
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    foo3&amp;lt;typewrapper&amp;lt;tmp&amp;gt;&amp;gt;(typewrapper&amp;lt;tmp&amp;gt;{tmp{}}, tmp{}); //可以
    foo3&amp;lt;tmp&amp;gt;(typewrapper&amp;lt;tmp&amp;gt;{tmp{}}, tmp{}); //可以
    foo3(typewrapper&amp;lt;tmp&amp;gt;{tmp{}}, tmp{});//不行
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们如果没有指明模板参数，则根据推导规则，类型有冲突且不能通过隐式类型转换解决问题。但是前面两个我们指明了模板参数，跳过了推导这一过程，则直接进入重载决议这一阶段就可以进行类型转换。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-12-14-%E6%A8%A1%E6%9D%BF.assets/QQ%E6%88%AA%E5%9B%BE20230512231811.png&quot; alt=&quot;QQ截图20230512231811&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;第二章 类模板&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;类模板的声明：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;类模板的声明：
template&amp;lt;typename T&amp;gt;
class Stack{
    Stack(Stack&amp;lt;T&amp;gt; const &amp;amp;);
    ~Stack();
    Stack&amp;lt;T&amp;gt; operator=(Stack&amp;lt;T&amp;gt; const &amp;amp;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;区分两种写法：类的类名 &lt;code&gt;Stack&lt;/code&gt;、类的类型 &lt;code&gt;Stack&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;当在声明中需要使用类的类型时，你必须使用&lt;code&gt; Stack&amp;lt;T&amp;gt;&lt;/code&gt;（大部分情况）&lt;/li&gt;
&lt;li&gt;然而当使用类名而不是类的类型时，就应该只用 &lt;code&gt;Stack&lt;/code&gt;（比如指定类的名称、构造和析构函数名）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类模板可以偏特化也可以全特化。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意，特化的部分我们塞进去的是实参argument。这部分非常关键&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;未被特化的部分&amp;gt;
class obj&amp;lt;特化的部分, 未被特化的部分&amp;gt;{
  ...  
};
template&amp;lt;typename T1, typename T2&amp;gt;
class test{
    T1 data1;
    T2 data2;
};
//全特化
template&amp;lt;&amp;gt;
class test&amp;lt;int, float&amp;gt;{
    int data1;
};
//偏特化
template&amp;lt;typename T2&amp;gt;
class test&amp;lt;int, T2&amp;gt;{
    int data1;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意，特化的类模板不能改变整体的模板形参数量。也就是在特化的时候，我们不可以改变特化的模板实参的数量&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1&amp;gt;
class test;
template&amp;lt;typename T1, typename T2&amp;gt; //这样做错误 ERROR。主模板只有一个形参。但是特化的时候我们塞进去了俩。不行。
class test&amp;lt;T1, T2&amp;gt;{//改变了特化的模板实参数量。错误

};

int main(){
    test&amp;lt;int, double&amp;gt; obj
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类模板调用顺序： 对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是：全特化类&amp;gt;偏特化类&amp;gt;主版本模板类。这样的优先级顺序对性能也是最好的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只有那些被调用的成员函数，才会产生这些函数的实例化代码。对于类模板，成员函数只有在被使用的时候才会实例化。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为在编译阶段，编译器无法确认模板的参数类型，所以无法创建模板类成员函数&lt;/li&gt;
&lt;li&gt;显然，这样可以节省空间和时间；另一个好处是对于那些 未能提供所有 “ 成员函数中所有操作的类型“，你也可以使用该类型来实例化类模板，只要对那些 未能提供 “  某些操作的” 成员函数，模板内部不使用就可以。而且现在的 C++标准要求编译器要尽可能的延迟实例化的时机。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态成员的无条件实例化：如果类模板中含有静态成员，那么用来实例化的每种类型，都会实例化这些静态成员。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类模板不能被重载&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.6 多模板参数的偏特化&lt;/h2&gt;
&lt;p&gt;偏特化有多种形式。下面的几种都可以。但是&lt;strong&gt;注意约束&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类模板偏特化的形参参数个数是可以和主模板不一样的，它既可以多于主模板，也可以少于主模板。但是偏特化的实参必须与主模板对应的参数相匹配。&lt;/strong&gt;------&lt;em&gt;出自16.4章&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;非常重要！！在特化类模板的时候，可以改变特化版本的形参列表的参数数量。但是最终落实（传入/应用）到实参上面的时候，参数数量不可变。&lt;/li&gt;
&lt;li&gt;此外，显式书写的模板实参数量与主模板的模板参数数量甚至也可能不同。尤其会在拥有默认模板实参或拥有可变模板时发生&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;偏特化的参数列表不能具有默认实参；作为替代，主类模板的默认实参会被使用。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;偏特化的非类型实参要么是一个非依赖型值，要么是一个普通的非类型模板参数。它们不能是更加复杂的表达式，诸如&lt;code&gt;2*N&lt;/code&gt;（&lt;code&gt;N&lt;/code&gt;是一个模板参数）。&lt;/li&gt;
&lt;li&gt;偏特化的模板实参列表不应该与主模板的参数列表完全相同（忽略重命名）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果模板实参的某一个是包展开，那么它必须位于模板实参列表的最后。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class MyClass {
    //主模板
};

template&amp;lt;typename T&amp;gt;
class MyClass&amp;lt;T,T&amp;gt; {
    //两个参数同一个类型。
    //注意这里不一定非得写T1 T1。类型具体叫什么没所谓
    //特化模板的形参列表的参数数量可以变。但是实参列表的参数数量不可变。主模板是两个形参，我们就要传入两个实参。
};

template&amp;lt;typename T&amp;gt;
class MyClass&amp;lt;T,int&amp;gt; {
    //第二个参数是int
};

template&amp;lt;typename T1, typename T2&amp;gt;
class MyClass&amp;lt;T1*,T2*&amp;gt; {
    //两个参数为指针。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们之前提到过特化的时候塞进去的是实参argument。塞进去的实参必须和主模板的形参数量匹配。为什么这么重要？我们理解一下：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1&amp;gt; //主模板只有一个形参
class test;
//----------VERSION 1---------------------
template&amp;lt;typename T1, typename... T2&amp;gt;
class test&amp;lt;T1, T2 ...&amp;gt;{ //实参数量不匹配

};

int main(){
    test&amp;lt;int,float, double&amp;gt; obj;
}

//-----------VERSION 2----------------------

template&amp;lt;typename T1&amp;gt;
class test;
template&amp;lt;typename T1, typename... T2&amp;gt; //形参可以不同但是实参必须匹配
class test&amp;lt;T1(T2 ...)&amp;gt;{//实参匹配

};

int main(){
    test&amp;lt;int(float, double)&amp;gt; obj; //实参和实参必须数量对应。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为啥version1不行，version2可以？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们说了，特化的时候必须匹配&lt;strong&gt;主模板&lt;/strong&gt;的模板形参数量。我们version 1塞进去了两个模板实参。这样发生了问题。&lt;/p&gt;
&lt;p&gt;version2为啥可以？尽管我们特化的时候，针对这个特化的模板形参是两个。但是我们在实际传入实参的时候，&lt;code&gt;T1&lt;/code&gt; 和&lt;code&gt; T2...&lt;/code&gt;被合成了一个&lt;strong&gt;&lt;em&gt;函数&lt;/em&gt;类型&lt;/strong&gt;&lt;code&gt;T1(T2...)&lt;/code&gt;。这一整个打包起来会变成主模板眼中的&lt;code&gt;T1&lt;/code&gt;。&lt;strong&gt;注意这是函数类型并非函数指针类型&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以说甚至可以对成员指针进行特化：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class List {//主模板
};
template&amp;lt;typename T, typename C&amp;gt;
class Class&amp;lt;T* C::*&amp;gt; { //针对成员指针的特化。T* C::*在主模板眼里是它的整个T
    
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为什么我们&lt;code&gt;main&lt;/code&gt;函数中的实例化也要写成&lt;code&gt;int(float, double)&lt;/code&gt;的形式？因为实参和实参必须相匹配。举个最简单的例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class MyClass {
    //主模板
};

template&amp;lt;typename T&amp;gt;
class MyClass&amp;lt;T,T&amp;gt; {
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是我们提到的例子。我们实例化的时候应该怎么写？是不是应该写&lt;code&gt;Myclass&amp;lt;int, int&amp;gt;&lt;/code&gt;这种形式的？不能写&lt;code&gt;Myclass&amp;lt;int&amp;gt;&lt;/code&gt;对吧？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为啥我这么关心这个？因为这是&lt;code&gt;packaged_task&lt;/code&gt;和&lt;code&gt;function&lt;/code&gt;的模板实现方式&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class &amp;gt; 
class packaged_task; //主模板
template&amp;lt; class R, class ...ArgTypes &amp;gt;
class packaged_task&amp;lt;R(ArgTypes...)&amp;gt;; //特化。

template&amp;lt; class &amp;gt;
class function; //主模板
template&amp;lt; class R, class... Args &amp;gt;
class function&amp;lt;R(Args...)&amp;gt;;//特化。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.7 类模板默认参数&lt;/h2&gt;
&lt;p&gt;类模板自然可以有默认参数。比如很多STL的实现。下面是一个小例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2 = int&amp;gt;
class obj{
     public:
     obj(T1 a, T2 b):val1(a), val2(b){};
     T1 val1;
     T2 val2;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.8 类型别名 Aliases&lt;/h2&gt;
&lt;p&gt;一般来说，我们有两种方式可以给类型定义别名。&lt;code&gt;using&lt;/code&gt; 和 &lt;code&gt;typedef&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;typedef&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class myobj{
    public:
    myobj(T a):val(a){};
    T val;
};

typedef myobj&amp;lt;int&amp;gt; INTmyobj;
typedef myobj&amp;lt;double&amp;gt; DOUBLEmyobj;

int main(){
    INTmyobj im(2);
    DOUBLEmyobj dm(2.345);
    cout &amp;lt;&amp;lt; im.val &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; dm.val &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;using&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;using usingINTmyobj = myobj&amp;lt;int&amp;gt;;
using usingDOUBLEmyobj = myobj&amp;lt;double&amp;gt;;

usingINTmyobj im(2);
usingDOUBLEmyobj dm(2.345);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.8.1 别名模板 Alias Templates&lt;/h3&gt;
&lt;p&gt;注意。这里是typedef和using在别名方面唯一区别。这里只有using。typedef不可以。&lt;/p&gt;
&lt;p&gt;比如我们不可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
typedef myobj&amp;lt;T&amp;gt; intobj;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
using intobj = myobj&amp;lt;T&amp;gt;;

intobj&amp;lt;int&amp;gt; myobj(2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.8.2 必须显式使用typename的情况&lt;/h3&gt;
&lt;p&gt;参看笔记STL2&lt;/p&gt;
&lt;h2&gt;2.9 类模板的类型推导&lt;/h2&gt;
&lt;p&gt;直到C++17，我们都必须显式指出所有类模板参数的类型。除非拥有默认值。&lt;/p&gt;
&lt;p&gt;剩下的看书。&lt;/p&gt;
&lt;h2&gt;2.10 聚合类型的模板化&lt;/h2&gt;
&lt;p&gt;可以定义聚合类的类模板。关于聚合类，参考聚合初始化。&lt;/p&gt;
&lt;h2&gt;2.11 类模板成员函数或变量的类外实现[自己添加]&lt;/h2&gt;
&lt;p&gt;基本类代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class test{
    public:
    T val;
    static T s_val;
    void func(T val);
    T func1();
    void func2();

};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;类静态成员变量&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T test&amp;lt;T&amp;gt;::s_val = 102;


int main(){
    test&amp;lt;int&amp;gt; obj;
    test&amp;lt;double&amp;gt; obj2;
    cout &amp;lt;&amp;lt; obj.s_val&amp;lt;&amp;lt; endl; //输出102
    cout &amp;lt;&amp;lt; obj2.s_val &amp;lt;&amp;lt; endl; //输出102
    obj2.s_val = 12345;
    cout &amp;lt;&amp;lt; obj.s_val&amp;lt;&amp;lt; endl; //输出102
    cout &amp;lt;&amp;lt; obj2.s_val &amp;lt;&amp;lt; endl; //输出12345
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;类静态成员变量在模板类中依旧遵循全类共享。&lt;strong&gt;但是注意这里是实例化后的全类共享。也就是&lt;code&gt;T&lt;/code&gt;为&lt;code&gt;int&lt;/code&gt;的&lt;code&gt;test&lt;/code&gt;和&lt;code&gt;T&lt;/code&gt;为&lt;code&gt;double&lt;/code&gt;的&lt;code&gt;test&lt;/code&gt;是两种类类型。自然二者是独立开来的。因为会分别实例化代码&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;类成员函数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
T test&amp;lt;T&amp;gt;::s_val = 102;


template&amp;lt;typename T&amp;gt;
void test&amp;lt;T&amp;gt;::func(T val){
    cout &amp;lt;&amp;lt;&quot;func&quot; &amp;lt;&amp;lt; endl; 
}

template&amp;lt;typename T&amp;gt;
T test&amp;lt;T&amp;gt;::func1(){
    cout &amp;lt;&amp;lt;&quot;func1&quot; &amp;lt;&amp;lt; endl; 
}

template&amp;lt;typename T&amp;gt;
void test&amp;lt;T&amp;gt;::func2(){
    cout &amp;lt;&amp;lt;&quot;func2&quot; &amp;lt;&amp;lt; endl; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可以看见，必须加模板头，且必须在指明类作用域的时候添加&lt;code&gt;T&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;也就是不能&lt;code&gt;test::func&lt;/code&gt; 必须是&lt;code&gt;test&amp;lt;T&amp;gt;::func&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第三章 非类型模板参数&lt;/h1&gt;
&lt;p&gt;杂记3有写。这里重新整理&lt;/p&gt;
&lt;h2&gt;3.1 类模板的非类型模板参数 - 3.2 函数模板的非类型模板参数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T obj){
    //...
}

int main(){
    func&amp;lt;int&amp;gt;(5);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们都知道模板参数一般都是类型。比如这里，&lt;code&gt;T&lt;/code&gt;就是&lt;code&gt;int&lt;/code&gt;。是类型。当我们希望传入一个不是类型的参数的时候，比如如果我们有时候需要一些特殊情况， 例如想要传点奇怪东西的时候就可以使用非类型模板参数。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, int MAXSIZE&amp;gt; //注意语法。这里非类型模板参数不再是typename了
void func1(T obj){
    vector&amp;lt;T&amp;gt;a;
    a.reserve(MAXSIZE);
    cout &amp;lt;&amp;lt; a.capacity() &amp;lt;&amp;lt; endl;
    a.push_back(obj);
    cout &amp;lt;&amp;lt; a[0] &amp;lt;&amp;lt; endl; 			//干啥了不解释了 忽略即可
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里的&lt;code&gt;int MAXSIZE&lt;/code&gt;就是非类型模板参数。因为他不是类型，而是变量。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用非类型模板参数是有限制的。通常它们只能是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;整型常量或字面值（包含枚举，或可隐式转换的比如&lt;code&gt;bool&lt;/code&gt;）（&lt;code&gt;string&lt;/code&gt; &lt;code&gt;double&lt;/code&gt;都不可以。前者是类对象，后者是浮点数）&lt;/li&gt;
&lt;li&gt;指针类型&lt;/li&gt;
&lt;li&gt;成员指针类型&lt;/li&gt;
&lt;li&gt;对象/函数的左值引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::nullptr_t&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;包含&lt;code&gt;auto&lt;/code&gt;或&lt;code&gt;decltype(auto)&lt;/code&gt;的类型 [C++17后]&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这个比较常用在把无捕获的lambda当做非类型模板参数进行传入。[C++20后]&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;参考&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/template_parameters&quot;&gt;cppreference的非类型模板形参&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当传递对象的指针或者引用作为模板参数时，对象不能是字符串常量，临时变量或者数据成员以及其他子对象。由于C++17之前，C++每次版本更新都会放宽以上限制，因此还有一些针对不同版本的限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++11中，对象必须要有外部链接&lt;/li&gt;
&lt;li&gt;C++14中，对象必须是外部链接或者内部链接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以：传入的&lt;code&gt;s&lt;/code&gt;必须是常量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int s = 8;	//必须是const
func1&amp;lt;int, s&amp;gt;(5);
func2&amp;lt;int, 4&amp;gt;(5); //或者直接传入字面值。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;搭配类模板偏特化的小例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, bool option&amp;gt;
class myclass; //主模板不实现

template&amp;lt;typename T&amp;gt;
class myclass&amp;lt;T, true&amp;gt;{ //偏特化1，注意语法。T在这里依旧要写上。
    public:
    void func(){
        cout &amp;lt;&amp;lt;&quot;true one&quot; &amp;lt;&amp;lt; endl;
    }
};


template&amp;lt;typename T&amp;gt;
class myclass&amp;lt;T, false&amp;gt;{ //偏特化2，注意语法。T在这里依旧要写上。
    public:
    void func(){
        cout &amp;lt;&amp;lt;&quot;false one&quot; &amp;lt;&amp;lt; endl;
    }
};

int main(){
    const bool myoption = true;
    myclass&amp;lt;int, false&amp;gt; obj; //直接使用字面值
    obj.func();
    myclass&amp;lt;int, myoption&amp;gt; obj1; //或必须用const常量变量。
    obj1.func();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;两者的非类型模板参数都可以指定默认值：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, int TS = 5&amp;gt; //注意语法。这里非类型模板参数不再是typename了
class myobj{
    public:
    myobj(T a):val(a){my_vec.reserve(TS);};
    T val;
    vector&amp;lt;T&amp;gt; my_vec;
};

template &amp;lt;typename T, int TS = 10&amp;gt; //注意语法。这里非类型模板参数不再是typename了
void func(T a){
    vector&amp;lt;T&amp;gt; my_vec(TS);
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; my_vec.capacity() &amp;lt;&amp;lt; endl;
}
int main(){
    myobj&amp;lt;int,8&amp;gt; instance(10);
    cout &amp;lt;&amp;lt; instance.my_vec.capacity() &amp;lt;&amp;lt; endl;
    //输出8
    func&amp;lt;int, 9&amp;gt;(2);
    //输出 called 9
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.3 避免无效表达式&lt;/h2&gt;
&lt;p&gt;非类型模板参数可以是任何编译器表达式。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//函数模板
template &amp;lt;int size, bool judge&amp;gt; //注意表达式不写在这。
void func(){
    cout &amp;lt;&amp;lt; size &amp;lt;&amp;lt; endl;
    if(judge == true){
        cout &amp;lt;&amp;lt; &quot;true&quot; &amp;lt;&amp;lt; endl;
    }
    else{
        cout &amp;lt;&amp;lt; &quot;false&quot; &amp;lt;&amp;lt; endl;
    }
}

//类模板
template &amp;lt;int size, bool judge&amp;gt;
class myobj{
    public:
    myobj(){
        my_vec.reserve(size);
        testfunc();
    };
    void testfunc(){
        cout &amp;lt;&amp;lt; my_vec.capacity() &amp;lt;&amp;lt; endl;
        if(judge == true){
            cout &amp;lt;&amp;lt; &quot;true&quot; &amp;lt;&amp;lt; endl;
        }
        else{
            cout &amp;lt;&amp;lt; &quot;false&quot; &amp;lt;&amp;lt; endl;
        }    
    }
    vector&amp;lt;int&amp;gt; my_vec;
};
int main(){
    func&amp;lt;10, sizeof(int) == 4&amp;gt;(); //注意表达式写在这。写在实例化位置而非模板参数位置。
    //输出true
    func&amp;lt;10, (sizeof(int) &amp;gt; 4 )&amp;gt;(); //注意这里表达式要额外一组括号。
    //输出false
    myobj&amp;lt;10, sizeof(int) == 4&amp;gt; instance;
    //输出10 true
    myobj&amp;lt;10, (sizeof(int) &amp;gt; 4 )&amp;gt; instance1;
    //输出10 false
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不过如果在表达式中使用了&lt;code&gt; operator &amp;gt;&lt;/code&gt;，就必须将相应表达式放在括号里面，否则&lt;code&gt;&amp;gt;&lt;/code&gt;会被作为模板参数列表末尾的&lt;code&gt;&amp;gt;&lt;/code&gt;，从而截断了参数列表&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.4 使用auto做为非模板类型参数 （C++17）&lt;/h2&gt;
&lt;p&gt;暂略。看书。&lt;/p&gt;
</content:encoded></item><item><title>C++杂记 - 4（三）：decltype、可平凡复制、注入类名与局部类</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B04-3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B04-3/</guid><pubDate>Sun, 20 Nov 2022 06:57:00 GMT</pubDate><content:encoded>&lt;h1&gt;decltype&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;C++&lt;/code&gt; 规范中，对于 &lt;code&gt;decltype&lt;/code&gt; 类型推导规则的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;decltype(expr)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;若&lt;strong&gt;expr&lt;/strong&gt;是一个没有带括号的&lt;strong&gt;标记符表达式（如局部变量名、命名空间作用域变量、函数参数等）&lt;strong&gt;或者&lt;/strong&gt;类成员访问表达式（注意，静态数据成员按规则2.2推导）&lt;/strong&gt;，那么的decltype（expr）就是&lt;strong&gt;expr所命名的实体&lt;/strong&gt;的类型（声明型别）。此外，如果expr是一个被重载的函数，则会导致编译错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;否则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若 &lt;strong&gt;expr&lt;/strong&gt; 的值类别为 &lt;strong&gt;将亡值&lt;/strong&gt; ，则 &lt;code&gt;decltype&lt;/code&gt; 产生 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt; ；&lt;/li&gt;
&lt;li&gt;若 &lt;strong&gt;expr&lt;/strong&gt; 的值类别为 &lt;strong&gt;左值&lt;/strong&gt; ，则 &lt;code&gt;decltype&lt;/code&gt; 产生 &lt;code&gt;T&amp;amp;&lt;/code&gt; ；&lt;/li&gt;
&lt;li&gt;若 &lt;strong&gt;expr&lt;/strong&gt; 的值类别为 &lt;strong&gt;纯右值&lt;/strong&gt; ，则 &lt;code&gt;decltype&lt;/code&gt; 产生 &lt;code&gt;T&lt;/code&gt; 。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对于纯右值而言，只有类类型可以保留cv限定符，其它类型则会丢失cv限定&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果 表达式 是返回类类型纯右值的函数调用，或是右操作数为这种函数调用的逗号表达式，那么不会对该纯右值引入临时量。&lt;/p&gt;
&lt;p&gt;也就是若表达式是 &lt;strong&gt;纯右值&lt;/strong&gt; ，则不从该纯右值 &lt;strong&gt;物质化&lt;/strong&gt; 临时对象：这种纯右值无结果对象。&lt;/p&gt;
&lt;p&gt;说人话就是类型名+&lt;code&gt;()&lt;/code&gt;等于调用类的默认构造生成匿名临时对象，这个临时对象的类型自然是类类型。临时对象又是纯右值。所以就是你这个表达式是返回类类型纯右值的函数调用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意如果对象的名字带有括号，则它被当做通常的 &lt;strong&gt;左值&lt;/strong&gt; 表达式，从而 &lt;code&gt;decltype(x)&lt;/code&gt; 和 &lt;code&gt;decltype((x))&lt;/code&gt; 通常是不同的类型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;表达式：表达式由一个或多个运算对象组成，对表达式求值将得到结果。字面值和变量是最简单的表达式，其结果就是字面值和变量的值。&lt;/li&gt;
&lt;li&gt;标记符表达式：即除去关键字、字面量等编译器所需要使用的标记之外的&lt;strong&gt;程序员自定义的标记(token)都是标记符&lt;/strong&gt;。而单个标记符对应的表达式就是标记符表达式。如&lt;code&gt;int arr[4];&lt;/code&gt;那么&lt;code&gt;arr&lt;/code&gt;是一个标记符表达式**，**&lt;code&gt;arr[3],arr[3]+0&lt;/code&gt;等都不是标记符表达式。&lt;/li&gt;
&lt;li&gt;上述“&lt;strong&gt;expr所命名的实体的类型&lt;/strong&gt;”和“&lt;strong&gt;expr的类型&lt;/strong&gt;”是&lt;strong&gt;不完全相同的两个概念&lt;/strong&gt;。在类成员访问表达式（如&lt;code&gt;E1.E2&lt;/code&gt;或&lt;code&gt;E1-&amp;gt;E2&lt;/code&gt;）中，&lt;strong&gt;expr所命名的实体的类型即为E2的“声明类型”，而expr的类型指整个表达式E1.E2求值结果的类型。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果E2为静态数据成员，表达式&lt;code&gt;E1.E2&lt;/code&gt;的结果始终是左值。&lt;code&gt;decltype(E1.E2)&lt;/code&gt;按规则2.2而不是规则1推导。&lt;/li&gt;
&lt;li&gt;如果E2是个引用类型（如&lt;code&gt;T&amp;amp;&lt;/code&gt;或&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;），&lt;strong&gt;decltype(E1.E2)指E2实体的声明类型&lt;/strong&gt;（&lt;code&gt;T&amp;amp;&lt;/code&gt;或&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;）。而&lt;strong&gt;整个表达式E1.E2结果则为T类型的左值&lt;/strong&gt;，即代表E2引用所指的对象或函数。&lt;/li&gt;
&lt;li&gt;若E2为非引用类型：当E1为左值时，E1.E2整个表达式为左值。而如果E1为右值，则整个表达式为右值类型。（E2为静态数据成员例外，见1的说明）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字符串字面值常量是个const的左值（可以取地址），采用规则2.2推导。而非字符串字面值常量则是个右值，采用规则2.3推导。&lt;/li&gt;
&lt;li&gt;对于类型为&lt;strong&gt;T的左值表达式&lt;/strong&gt;，&lt;strong&gt;decltype总是得出T＆类型&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;decltype&lt;/code&gt; 有两种表达方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有括号：&lt;code&gt;decltype((expr))&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;有括号的表达方法，语意是简单而统一的：它站在表达式类别的角度求类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;原本不是表达式的类型，加上括号后立刻变为表达式类型。主要针对是变量名和函数名。&lt;/li&gt;
&lt;li&gt;有括号传入变量名推导出的类型是这个变量左值引用类型。因为变量名作为表达式是左值。左值按照规则产生左值引用类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无括号：&lt;code&gt;decltype(expr)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;无括号和有括号只在针对传入变量名和函数名的时候有区别。&lt;/li&gt;
&lt;li&gt;无括号传入变量名推导出的类型就是这个变量的类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意。一个表达式的结果不是左值就是右值（包括将亡值）。然而一个变量名称本身也是一个表达式。无括号的情况下，除了一种例外，其它情况下，都与有括号场景一致。&lt;strong&gt;这个例外就是对于变量（包括常量）名字的直接求类型。这种情况，会返回变量被定义时的类型。然而给变量再包一层括号，这个变量的名字会变成表达式。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 10;
a; //表达式
decltype(a); //a被当做变量。类型类型推导为int
decltype((a));//a被当做表达式。表达式a是左值，所以类型推导为int&amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;额外注意作用于函数名会得到函数类型，不会自动转换成指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sr{
};
int func(){
}
int&amp;amp; anotherfunc(){
}
int func_with_arg(int a, int b){
    
}
int main() {
    sr obj;
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(func), int(void)&amp;gt; &amp;lt;&amp;lt; endl; //这个是变量。是函数本身。所以是函数类型。注意不是函数指针类型。
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(&amp;amp;func), int(*)(void)&amp;gt; &amp;lt;&amp;lt; endl; //这个是变量。是取函数地址。所以是函数指针类型。
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(func()), int&amp;gt; &amp;lt;&amp;lt; endl; //这是表达式。值传递的返回值是临时对象，纯右值推导为变量类型
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(sr()), sr&amp;gt; &amp;lt;&amp;lt; endl; //sr()是表达式。类匿名临时对象，纯右值。纯右值推导为变量类型
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(anotherfunc()), int&amp;amp;&amp;gt; &amp;lt;&amp;lt; endl; //这是表达式。引用返回的是左值。左值推导为引用类型
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(obj), sr&amp;gt; &amp;lt;&amp;lt; endl; //这是变量。直接作用于变量是变量类型
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(move(obj)), sr&amp;amp;&amp;amp;&amp;gt; &amp;lt;&amp;lt; endl;//这是表达式。move返回右值引用，表达式返回将亡值。将亡值是右值引用类型
    
    //----加括号部分------
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype((func)), int(&amp;amp;)(void)&amp;gt; &amp;lt;&amp;lt; endl; //注意，加了括号后变成表达式类型。具名函数也算左值，返回引用类型。所以是函数引用。
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype((func())), int&amp;gt; &amp;lt;&amp;lt; endl; //加了括号没区别
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype((sr())), sr&amp;gt; &amp;lt;&amp;lt; endl; //加了括号没区别
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype((anotherfunc())), int&amp;amp;&amp;gt; &amp;lt;&amp;lt; endl; //加了括号没区别
     cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype((obj)), sr&amp;amp;&amp;gt; &amp;lt;&amp;lt; endl; //这是表达式因为给变量多了一层括号。这个表达式是左值。因为具名变量本身是左值。左值推导为引用类型
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype((move(obj))), sr&amp;amp;&amp;amp;&amp;gt; &amp;lt;&amp;lt; endl;//加了括号没区别
    
    //-------需要强调的部分-------
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(func_with_arg), int(int,int)&amp;gt; &amp;lt;&amp;lt; endl;//这是推导函数类型
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(func_with_arg(int{}, int{})), int&amp;gt; &amp;lt;&amp;lt; endl;//函数带了括号是表达式。这是推导函数调用的返回值类型
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(func_with_arg(3,4)), int&amp;gt; &amp;lt;&amp;lt; endl;//和上面一样
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(func_with_arg(int,int)), int&amp;gt; &amp;lt;&amp;lt; endl;//不行。func_with_arg(int,int)不是有效表达式
    cout &amp;lt;&amp;lt; is_same_v&amp;lt;decltype(func_with_arg(declval&amp;lt;int&amp;gt;(),declval&amp;lt;int&amp;gt;())), int&amp;gt; &amp;lt;&amp;lt; endl;//使用declval。但是是脱裤子放屁
    
    int s = 100;
    cout &amp;lt;&amp;lt; is_same&amp;lt;decltype(2), int&amp;gt;::value &amp;lt;&amp;lt; endl; // 字面值纯右值，推导为T
    cout &amp;lt;&amp;lt; is_same&amp;lt;decltype(s), int&amp;gt;::value &amp;lt;&amp;lt; endl; //变量名不加括号，推导为其声明类型。此处就是T
    cout &amp;lt;&amp;lt; is_same&amp;lt;decltype((s)), int&amp;amp;&amp;gt;::value &amp;lt;&amp;lt; endl; //变量名加了括号是表达式。是左值。此处推导为T&amp;amp;
    cout &amp;lt;&amp;lt; is_same&amp;lt;decltype(move(s)), int&amp;amp;&amp;amp;&amp;gt;::value &amp;lt;&amp;lt; endl;//move过后变成将亡值，推导为T&amp;amp;&amp;amp;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意需要强调的部分。函数加括号就是表达式。就是函数调用。推导函数调用类型就等于推导函数返回值类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;**注意推导函数调用的时候，如果有参数必须放一个参数而不是一个类型。**比如倒数第二行，&lt;code&gt;func_with_arg(int,int)&lt;/code&gt;并不是有效的表达式，你见过这么调用的函数？就算decltype不求值，但是必须语句合法。
&lt;ul&gt;
&lt;li&gt;所以你既可以放一个参数进去，也可以用&lt;code&gt;{}&lt;/code&gt;初始化临时对象。但是不能&lt;code&gt;()&lt;/code&gt;因为会被当做函数声明。&lt;/li&gt;
&lt;li&gt;也可以像最后一行，用declval。但是在这里是脱裤子放屁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/19200513/function-pointer-vs-function-reference&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/36517596/decltype-parenthesis-syntax-for-a-lvalue&lt;/p&gt;
&lt;p&gt;https://modern-cpp.readthedocs.io/zh_CN/latest/decltype.html&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/u014609638/article/details/106987131&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/5iedu/p/11222078.html&lt;/p&gt;
&lt;h2&gt;关于decltype成员函数类型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    void func_const() const{

    }
    void func_nonconst(){

    }
    void test(){
        cout &amp;lt;&amp;lt; is_same_v&amp;lt; decltype(&amp;amp;A::func_nonconst), void(A::*)(void)&amp;gt; &amp;lt;&amp;lt; endl; //这是求函数指针类型
        cout &amp;lt;&amp;lt; is_same_v&amp;lt; decltype(declval&amp;lt;A&amp;gt;().func_nonconst()), void&amp;gt; &amp;lt;&amp;lt; endl;	//这是求函数返回值类型
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个，&lt;code&gt;&amp;amp;A::func_nonconst&lt;/code&gt;并不调用函数，它获取一个成员函数地址，可以隐式转换为成员函数指针。所以此时类型比较的是成员函数指针类型而非成员函数类型。所以是&lt;code&gt;void(A::*)(void)&lt;/code&gt; 而不是&lt;code&gt;void(void)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第二个，&lt;code&gt;declval&lt;/code&gt;目的是求函数返回值类型。所以必须要有函数调用。最后必须是&lt;code&gt;func_nonconst()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;关于decltype的不求值语境&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;decltype推导过程是在编译期完成的，并且不会真正计算表达式的值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;decltype&lt;/code&gt;不产生代码，对于表达式的要求只需要一个声明，不需要定义。也就是只需要一个名字。所以你可以这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int test(); //压根没定义
int main(){
    cout &amp;lt;&amp;lt; is_same&amp;lt;int, decltype(test())&amp;gt;::value &amp;lt;&amp;lt; endl;//true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;虽然decltype并不求值，但是其中涉及到的临时对象的“创建”和”销毁“都是受到语义限制的。也就是仍然受到创建和销毁临时所需的任何函数（包括构造函数）的可访问性的限制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;换句话说，其不会生成临时对象，但是也应遵守所有语义限制，就好像临时对象已创建并随后被销毁一样。这包括可访问性 以及它是否被删除，比如造函数和析构函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;https://stackoverflow.com/questions/25663642/why-can-i-use-private-default-constructor-in-decltype&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此处有疑问。在标准文档N4835的6.7.7一节中，这样描述：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Even when the creation of the temporary object is unevaluated (7.2), all the semantic restrictions shall
be respected as if the temporary object had been created and later destroyed. [Note: This includes
accessibility (11.9) and whether it is deleted, for the constructor selected and for the destructor. &lt;strong&gt;However, in&lt;/strong&gt;
&lt;strong&gt;the special case of the operand of a decltype-specifier (7.6.1.2), no temporary is introduced, so the foregoing&lt;/strong&gt;
&lt;strong&gt;does not apply to such a prvalue.&lt;/strong&gt; — end note]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这句话说的由于在 decltype 说明符操作数的特殊情况下，没有引入临时值，因此上述内容不适用于此类 &lt;code&gt;prvalue&lt;/code&gt;。但是我们来测试一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    A() = delete;
    int foo();
};
int main(){
    decltype(A().foo()); //我们先忽略掉如果decltype合法后提示的 does not declare anything 错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么这里会提示不合法：&lt;code&gt;use of deleted function ‘A::A()’&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这里要区分不求值和是否合法。非静态成员函数必须通过一个对象或指向对象的指针调用。就算我们这里没有引入临时值，此时不求值，但是必须要通过对象调用。没有对象无法调用。不合法。同时，在标准库文档有这样的一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果使用特殊语法 = delete 取代函数体，那么该函数被定义为&lt;em&gt;弃置的（deleted）&lt;/em&gt;。&lt;strong&gt;任何弃置函数的使用都是非良构的&lt;/strong&gt;（程序无法编译）。这包含调用，包括显式（以函数调用运算符）及隐式（对弃置的重载运算符、特殊成员函数、分配函数等的调用），构成指向弃置函数的指针或成员指针，**甚至是在不潜在求值的表达式中使用弃置函数。**但是可以隐式 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/definition#ODR_.E4.BD.BF.E7.94.A8&quot;&gt;ODR 使用&lt;/a&gt;刚好被弃置的非纯虚成员函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以在不潜在求值的表达式中使用弃置函数并不合法。&lt;/p&gt;
&lt;p&gt;如果我们换成&lt;code&gt;private&lt;/code&gt;呢？其就受到了前面提到的可见性限制。&lt;/p&gt;
&lt;p&gt;不过我们如何理解最后一句话呢？其实文档提到的是这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    A() = delete;
};
A testfunc(); //注意这里
int main(){
    decltype(testfunc()); //我们忽略掉does not declare anything 错误
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们有一个返回&lt;code&gt;A&lt;/code&gt;的全局函数。全局函数直接调用是合法的。所以完全可以调用。此时函数的返回值是个临时对象，符合&lt;code&gt;prvalue&lt;/code&gt;要求。此时表达式合法。这时候，就算&lt;code&gt;A&lt;/code&gt;的构造函数是&lt;code&gt;delete&lt;/code&gt;或&lt;code&gt;private&lt;/code&gt;，都会成功。因为结合起来应该是说，创建临时对象的过程在&lt;code&gt;decltype&lt;/code&gt;语境里是无视&lt;code&gt;delete&lt;/code&gt;和可见性限制的。但是其调用方式必须合法。也就是说它不关心&lt;code&gt;decltype&lt;/code&gt;里面那个函数创建的对象的过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/QQ%E6%88%AA%E5%9B%BE20230122025116.png&quot; alt=&quot;QQ截图20230122025116&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关于decltype其他的雷点&lt;/h2&gt;
&lt;p&gt;查看&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1149/&quot;&gt;这篇文章的&lt;/a&gt;&lt;em&gt;Problems of automatic type deduction&lt;/em&gt;一节&lt;/p&gt;
&lt;p&gt;还有O‘Dwyer的&lt;a href=&quot;https://quuxplusone.github.io/blog/2018/04/05/lifetime-extension-grudgingly-accepted/&quot;&gt;这篇文章&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;对指针解引用，返回的是对象的引用&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/image-20230120024816972.png&quot; alt=&quot;image-20230120024816972&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是想当然的。不然你没办法解引用后赋值，如&lt;code&gt;*ptr = 200;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;我们在EFFSTL中提到了对迭代器解引用返回的是引用。&lt;/li&gt;
&lt;li&gt;同时在vptr当中提到了解引用指针的多态性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;逐位拷贝 memcpy ，可平凡复制  TriviallyCopyable 和 可平凡重定位 TriviallyRelocatable&lt;/h1&gt;
&lt;p&gt;在C++中，我们把传统C风格的数据类型叫做POD（Plain Old Data）对象，即一种古老的纯数据，C的所有对象都是POD。一般来说，POD对象应该满足如下特性：其二进制内容是可以随意复制的，无论在什么地方，只要其二进制内容存在，我们就能准确无误地还原POD对象。正是由于这个原因，对于任何POD对象，我们都可以放心大胆地使用&lt;code&gt;memset()&lt;/code&gt;,&lt;code&gt;memcpy()&lt;/code&gt;,&lt;code&gt;memcmp()&lt;/code&gt;等函数对对象的内存数据进行操作。&lt;/p&gt;
&lt;p&gt;下列类型统称为&lt;em&gt;可平凡复制类型&lt;/em&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标量类型&lt;/li&gt;
&lt;li&gt;可平凡复制类类型&lt;/li&gt;
&lt;li&gt;上述类型的数组&lt;/li&gt;
&lt;li&gt;这些类型的有 cv 限定版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;什么东西不是POD类型？C++的对象大概率并不是一个POD，我们无法像在C中那样获得该对象直观简洁的内存模型。对于POD对象，我们可以通过对象的基地址和数据成员的偏移地址获得数据成员的地址。但是C++标准并未对非POD对象的内存布局做任何定义，对于不同的编译器，其对象布局是不同的。而在C语言中，对象布局仅仅会受到底层硬件系统差异的影响。针对非POD对象，其序列化会遇到一定的障碍：由于对象的不同部分可能存在于不同的地方，因为无法直接复制，只能通过手工加工序列化操作代码来处理对象数据。&lt;/p&gt;
&lt;p&gt;最简单的来说，类的特殊成员函数如拷贝构造，拷贝赋值，移动构造，移动赋值和析构函数会为我们隐式添加很多操作。尤其是含有多态的时候。我们在&lt;code&gt;vptr&lt;/code&gt;部分详细讲述了。子类对象给父类对象赋值的时候会产生切割。&lt;code&gt;vptr&lt;/code&gt;不会被复制, 会隐式的在拷贝构造或拷贝赋值中对其进行调整。但是，&lt;code&gt;memcpy&lt;/code&gt;却会完完整整的把二进制级别信息拷贝进来。所以不会针对如vptr进行调整。这时候如果针对非POD类型进行&lt;code&gt;memcpy&lt;/code&gt;操作，再调用函数就会爆炸。&lt;/p&gt;
&lt;p&gt;当然了，如果子类给子类&lt;code&gt;memcpy&lt;/code&gt;，父类给父类进行&lt;code&gt;memcpy&lt;/code&gt;，不会发生问题。因为不涉及对如&lt;code&gt;vptr&lt;/code&gt;等东西进行隐式调整。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般来说，只要是我们在语义上和行为上允许不调用析构函数，也就是析构函数不做什么事情的时候，它就是可平凡复制的，可以使用&lt;code&gt;memcpy&lt;/code&gt;&lt;/strong&gt;。&lt;a href=&quot;https://youtu.be/MfFzr9qqPDw?t=1982&quot;&gt;来自&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在上面的讲座中提到了TriviallyRelocatable可平凡重定位的。&lt;strong&gt;它是一个仍然在草案中的特性&lt;/strong&gt;。重定位就相当于移动+调用原对象的析构函数。因为移动后，被移动的对象的资源虽然移走了，但是对象本身还在，依旧需要调用析构函数。这样就是需要访问两次对象。一次移动和一次析构。而重定义相当于在移动后立即调用对象的析构。也就是只需要访问一次。&lt;/p&gt;
&lt;p&gt;可平凡重定位就相当于只需要移动而不需要调用析构函数。因为析构函数不做什么。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于一个对象&lt;code&gt;R&lt;/code&gt;，如果“移动它然后立即销毁原始对象”的操作等同于&lt;code&gt;memcpy&lt;/code&gt; ，我们可以说&lt;code&gt;R&lt;/code&gt;是可平凡重定位的。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://quuxplusone.github.io/blog/2018/07/18/announcing-trivially-relocatable/&quot;&gt;来自&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最经典的例子就是&lt;strong&gt;当&lt;code&gt;std::unique_ptr&lt;/code&gt;的内容被移动后的时候&lt;/strong&gt;。因为它下面就是个原始指针。这时候当我们把资源移走后，如果我们使用&lt;code&gt;std::move&lt;/code&gt;触发移动构造函数，则当前&lt;code&gt;unique_ptr&lt;/code&gt;下面的原始指针一定会被置空。所以这个时候调用析构函数是无副作用的。所以它完全可以被省略掉。这个时候可以把处在这个状态下的&lt;code&gt;unique_ptr&lt;/code&gt;当做可平凡重定位的。但是，不应该在&lt;code&gt;std::unique_ptr&lt;/code&gt;上面施加如此复杂的判断逻辑。所以我们这个时候可以使用&lt;code&gt;memcpy&lt;/code&gt;来直接拷贝整个&lt;code&gt;std::unique_ptr&lt;/code&gt;对象（相当于把move+析构函数的这一套操作换为&lt;code&gt;memcpy&lt;/code&gt;，也就是有意识的浅拷贝）。当然，我们不能销毁被拷贝的&lt;code&gt;std::unique_ptr&lt;/code&gt;。因为一旦销毁了，就会调用析构函数，就会导致资源释放而出现问题。&lt;/p&gt;
&lt;h1&gt;注入类名 （injected-class-name）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注入类名是在类的作用域内该类自身的名字。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在类作用域中，当前&lt;strong&gt;类的名字被当做它如同是一个公开成员名一样&lt;/strong&gt;；这被称为&lt;em&gt;注入类名（injected-class-name）&lt;/em&gt;。该名字的声明点紧跟类定义的开花括号之后。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类模板中，注入类名能用作指代当前模板的模板名，或指代当前实例化的类名。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int X;
struct X {
    void f() {
        X* p; // OK：X 指代注入类名也就是本类名字。
        ::X* q; // 错误：名称查找找到变量名，它隐藏了本类名字。
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;与其他成员类似，&lt;strong&gt;注入类名可被继承，但是依旧受可见性制约&lt;/strong&gt;。在私有或受保护继承的场合，可能导致某个间接基类的注入类名在派生类中最后变得不可访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct A {};
struct B : private A {};
struct C : public B {
    A* p; // 错误：注入类名 A 不可访问
    ::A* q; // OK：不使用注入类名
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他的太繁琐了。&lt;/p&gt;
&lt;h1&gt;局部类 local class&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;我不知道这玩意有啥用。它就是一个在函数里面的类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct base{
    virtual void func(){
        cout &amp;lt;&amp;lt;&quot;b&quot; &amp;lt;&amp;lt; endl;
    }
};

base* test(){
    struct innerclass:base{ //局部类 继承自base
        int val = 200;
        void func(){
            cout &amp;lt;&amp;lt;&quot;inner&quot; &amp;lt;&amp;lt; endl;
        } 
    };
    return new innerclass;
}

int main(){
    base* ptr = test(); //可以
    ptr-&amp;gt;func(); //可以
    //innerclass obj; 不可以，不可见。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;局部类有很多很多的限制：
&lt;ul&gt;
&lt;li&gt;必须在类内实现所有成员函数，不可以拿到类外（函数不可嵌套定义）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局部类只能访问外层作用域定义的类型，静态变量以及枚举成员或全局变量&lt;/strong&gt;。如果局部类定义在某个函数的内部，那么&lt;strong&gt;该函数的普通局部变量不能被该局部类使用&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;类内&lt;strong&gt;不可以含有静态数据成员，&lt;strong&gt;但可以含有&lt;/strong&gt;静态函数成员&lt;/strong&gt;。（reason：静态数据成员要求在类外进行初始化，在程序运行之前完成，并且用(::) 来指明所从属的类名，显然不能实现）&lt;/li&gt;
&lt;li&gt;不能从外部初始化局部类对象。
&lt;ul&gt;
&lt;li&gt;所以我们如果不提供方法返回局部类的某个变量，则外部无法访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;局部类的特性是可以被当做final。因为外界无法继承一个隐藏在函数内的class。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;代理函数 surrogate call function&lt;/h1&gt;
&lt;p&gt;这个东西在模板11.1.1当中我们提到过。我们先举个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename Fcn1, typename Fcn2&amp;gt;
struct Surrogate {
    Surrogate(Fcn1 *f1, Fcn2 *f2) : f1_(f1), f2_(f2) {}
    operator Fcn1*(){
        cout &amp;lt;&amp;lt; &quot;Fcn1*&quot; &amp;lt;&amp;lt; endl;
        //Surrogate&amp;lt;Fcn1, Fcn2&amp;gt;::operator Fcn1() [with Fcn1 = void (*)(int); Fcn2 = void (*)(double)]
        return f1_; 
    }
    operator Fcn2*(){
        cout &amp;lt;&amp;lt; &quot;Fcn2*&quot; &amp;lt;&amp;lt; endl;
        return f2_; 
    }
    Fcn1 *f1_;
    Fcn2 *f2_;
};

void foo (int i){
    cout &amp;lt;&amp;lt;&quot;foo&quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
}

void bar (double i){
    cout &amp;lt;&amp;lt;&quot;bar&quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
}

int main (){
    Surrogate&amp;lt;void(int), void(double)&amp;gt; callable(foo, bar); //注意当前版本是假设传入函数类型。我们也可以换成传入函数指针类型的
    callable(10);       // calls foo
    //static_cast&amp;lt;void (*)(int)&amp;gt;(callable.operator void (*)(int)())(10);
    callable(10.1);     // calls bar
    //static_cast&amp;lt;void (*)(double)&amp;gt;(callable.operator void (*)(double)())(10.1);
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现我们首先传入了两个函数。然后类型显式指定为函数类型。然后使用两个函数指针去接受这两个函数。&lt;/p&gt;
&lt;p&gt;然后定义了两个用户定义转换函数。一个会转换成第一个函数的函数指针类型，一个会转换成第二个函数指针类型。&lt;/p&gt;
&lt;p&gt;那么在调用的时候，为什么可以调用呢？&lt;/p&gt;
&lt;p&gt;我们不管注释掉的两行里面的&lt;code&gt;static_cast&lt;/code&gt;，我们发现是因为我们在对&lt;code&gt;callable&lt;/code&gt;进行一个函数调用，也就是实施了函数调用表达式。所以编译器会去寻找可调用对象，比如函数指针，函数，或者是带有&lt;code&gt;operator()&lt;/code&gt;的东西。然后发现虽然&lt;code&gt;callable&lt;/code&gt;不是这些东西，但是可以转换成其中之一，也就是函数指针。因为根据重载解析规则，这两个用户定义转换函数也被重载解析考虑在内（来自有点看不懂的&lt;a href=&quot;https://eel.is/c++draft/over.call.object&quot;&gt;标准文档&lt;/a&gt;）。同时这个时候因为给定的参数是&lt;code&gt;int&lt;/code&gt;，根据重载解析规则，两次调用会选择对应的版本。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/11354737/how-does-surrogate-call-function-work&quot;&gt;stackoverflow&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;标准文档翻译：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果函数调用语法中的后缀表达式&lt;code&gt;E&lt;/code&gt;计算出的是类型为&lt;code&gt;cv T&lt;/code&gt;的类对象，则候选函数集中至少包括&lt;code&gt;T&lt;/code&gt;的函数调用运算符。&lt;code&gt;T&lt;/code&gt;的函数调用运算符是在&lt;code&gt;T&lt;/code&gt;的作用域中搜索名称&lt;code&gt;operator()&lt;/code&gt;的结果。&lt;/p&gt;
&lt;p&gt;此外，对于在&lt;code&gt;T&lt;/code&gt;中声明的形式为&lt;/p&gt;
&lt;p&gt;&lt;code&gt;operator conversion-type-id () cv-qualifier-seq opt ref-qualifier opt noexcept-specifier opt attribute-specifier-seq opt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;的非显式转换函数，其中可选的&lt;code&gt;cv-qualifier-seq&lt;/code&gt;是与&lt;code&gt;cv&lt;/code&gt;相同的&lt;code&gt;cv&lt;/code&gt;限定符或更高的&lt;code&gt;cv&lt;/code&gt;限定符，而&lt;code&gt;conversion-type-id&lt;/code&gt;表示类型“返回&lt;code&gt;R&lt;/code&gt;的&lt;code&gt;(P1，...，Pn)&lt;/code&gt;函数的指针”，或类型“返回&lt;code&gt;R&lt;/code&gt;的&lt;code&gt;(P1，...，Pn)&lt;/code&gt;函数的引用”，或类型“指向返回&lt;code&gt;R&lt;/code&gt;的&lt;code&gt;(P1，...，Pn)&lt;/code&gt;函数的指针的引用”，还将考虑具有唯一名称(如下)&lt;code&gt;call-function&lt;/code&gt;的代理函数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;R call-function ( conversion-type-id F, P1 a1, ..., Pn an) { return F(a1, ..., an); }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;同样，对于&lt;code&gt;T&lt;/code&gt;的基类中声明的每个非显式转换函数，如果该函数没有被&lt;code&gt;T&lt;/code&gt;中的另一个介于该函数和&lt;code&gt;T&lt;/code&gt;之间的声明隐藏，则也将向候选函数集中添加代理调用函数。&lt;/p&gt;
&lt;p&gt;提交到重载解析的参数列表包括函数调用语法中的实参表达式，在其前面是隐含的对象参数（&lt;code&gt;E&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;【注1】：在将调用与函数调用运算符进行比较时，隐含的对象参数将与函数调用运算符的对象参数进行比较。在将调用与代理调用函数进行比较时，隐含的对象参数将与代理调用函数的第一个参数进行比较。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;函数指针类型的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename Fcn1, typename Fcn2&amp;gt;
struct Surrogate {
    Surrogate(Fcn1 f1, Fcn2 f2) : f1_(f1), f2_(f2) {}
    operator Fcn1(){
        cout &amp;lt;&amp;lt; &quot;Fcn1*&quot; &amp;lt;&amp;lt; endl;
        return f1_; 
    }
    operator Fcn2(){
        cout &amp;lt;&amp;lt; &quot;Fcn2*&quot; &amp;lt;&amp;lt; endl;
        return f2_; 
    }
    Fcn1 f1_;
    Fcn2 f2_;
};

void foo (int i){
    cout &amp;lt;&amp;lt;&quot;foo&quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
}

void bar (double i){
    cout &amp;lt;&amp;lt;&quot;bar&quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
}

int main (){
    Surrogate&amp;lt;void(*)(int), void(*)(double)&amp;gt; callable(foo, bar);
    callable(10);       // calls foo
    callable(10.1);     // calls bar
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C++杂记 - 4（二）：异常、lambda、求值顺序与宏</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B04-2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B04-2/</guid><pubDate>Sun, 20 Nov 2022 06:56:00 GMT</pubDate><content:encoded>&lt;h1&gt;异常&lt;/h1&gt;
&lt;p&gt;C++引入异常的原因是这样做可以让我们在构造函数中抛出异常。还有比如dynamic_cast传入引用时的报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;析构函数不禁止但是极度不推荐抛出异常。原因见杂记2和effective c++条款08。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;异常是一个非常难以掌握的特性。它的缺点是会增加开销（需要额外数据结构）和增加代码量。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;基本语法：&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;try{
    // 可能抛出异常的语句
}catch(exceptionType variable){
    // 处理异常的语句
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;try&lt;/code&gt;和&lt;code&gt;catch&lt;/code&gt;都是 C++ 中的关键字，后跟语句块，不能省略&lt;code&gt;{ }&lt;/code&gt;。try 中包含可能会抛出异常的语句，一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出，它只是“检测”语句块有没有异常，如果没有发生异常，它就“检测”不到。catch 是“抓住”的意思，用来捕获并处理 try 检测到的异常；如果 try 语句块没有检测到异常（没有异常抛出），那么就不会执行 catch 中的语句。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;发生异常时必须将异常明确地抛出，try 才能检测到；如果不抛出来，即使有异常 try 也检测不到。所谓抛出异常，就是明确地告诉程序发生了什么错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;异常一旦抛出，会立刻被 try 检测到，并且不会再执行异常点（异常发生位置）后面的语句。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检测到异常后程序的执行流会发生跳转，从异常点跳转到 catch 所在的位置，位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了；即使 catch 语句成功地处理了错误，程序的执行流也不会再回退到异常点，所以这些语句永远都没有执行的机会了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行完 catch 块所包含的代码后，程序会继续执行 catch 块后面的代码，就恢复了正常的执行流。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;throw&lt;/code&gt;关键字用来抛出一个异常，这个异常会被 try 检测到，进而被 catch 捕获。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;throw和异常类型的介绍：&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;exceptionType&lt;/code&gt;是异常类型，它指明了当前的 catch 可以处理什么类型的异常；&lt;code&gt;variable&lt;/code&gt;是一个变量，用来接收异常信息。当程序抛出异常时，会创建一份数据，这份数据包含了错误信息，程序员可以根据这些信息来判断到底出了什么问题，接下来怎么处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异常既然是一个对象，那么就应该有类型。C++ 规定，异常类型可以是 &lt;code&gt;int&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;bool&lt;/code&gt; 等基本类型，也可以是指针、数组、字符串、结构体、类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常，都是 exception 类或其子类的异常。也就是说，抛出异常时，会创建一个 &lt;code&gt;exception&lt;/code&gt; 类或其子类的对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exceptionType variable&lt;/code&gt;和函数的形参非常类似，当异常发生后，会将异常数据传递给 variable 这个变量，这和函数传参的过程类似。当然，只有跟 &lt;code&gt;exceptionType&lt;/code&gt; 类型匹配的异常数据才会被传递给 &lt;code&gt;variable&lt;/code&gt;，否则 catch 不会接收这份异常数据，也不会执行 catch 块中的语句。换句话说，catch 不会处理无法匹配的异常。
&lt;ul&gt;
&lt;li&gt;我们可以将 catch 看做一个没有返回值的函数，当异常发生后 catch 会被调用，并且会接收实参（异常数据）。&lt;/li&gt;
&lt;li&gt;但是 catch 和真正的函数调用又有区别：
&lt;ul&gt;
&lt;li&gt;真正的函数调用，形参和实参的类型必须要匹配，或者可以自动转换，否则在编译阶段就报错了。&lt;/li&gt;
&lt;li&gt;而对于 catch，异常是在运行阶段产生的，它可以是任何类型，没法提前预测，所以不能在编译阶段判断类型是否正确，只能等到程序运行后，真的抛出异常了，再将异常类型和 catch 能处理的类型进行匹配，匹配成功的话就“调用”当前的 catch，否则就忽略当前的 catch。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调用函数时，程序的控制权最终还会返回到函数的调用处，但是当你抛出一个异常时，控制权永远不会回到抛出异常的地方。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常对象其实是一个特殊对象。throw语句会依据异常抛出表达式来拷贝初始化（拷贝构造）异常对象。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这要求异常抛出表达式不能是一个不完全类型。并且可以进行拷贝初始化。这可能会调用右值表达式的移动构造函数。即使拷贝初始化选择了移动构造函数，从左值拷贝初始化仍必须为良式，且析构函数必须可访问&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常对象放在内存的特殊位置，该位置既不是栈也不是堆，在 window 上是放在线程信息块 TIB 中。或者是异常栈中。这个构造出来的新对象与本级的 try 所对应的 catch 语句进行类型匹配&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;由于异常对象在特殊位置，比如异常栈或TIB，所以保证最后销毁。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常对象不同于函数的局部对象，局部对象在函数调用结束后就被自动销毁，而异常对象将驻留在所有可能被激活的 catch 语句都能访问到的内存空间中，也即上文所说的 TIB。当异常对象与 catch 语句成功匹配上后，在该 catch 语句的结束处被自动析构。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在函数中返回局部变量的引用或指针几乎肯定会造成错误，同样的道理，在 throw 语句中抛出局部变量的指针或引用也几乎是错误的行为。如果指针所指向的变量在执行 catch 语句时已经被销毁，对指针进行解引用将发生意想不到的后果。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;throw 出一个表达式时，该表达式的静态编译类型将决定异常对象的类型。也就是该对象的类型与throw语句中体现的静态类型相同。即只有子类对象中的父类部分会被抛出，抛出对象的类型也是父类类型（从实现上讲，是因为复制到“临时对象”的时候，使用的是throw语句中类型的（这里是父类的）复制构造函数）。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意，这里分成两种情况&lt;/strong&gt;。一种情况是throw的指针/引用是多态形式。如&lt;code&gt;Base* ptr = new Derived(); throw ptr;&lt;/code&gt;或&lt;code&gt;Derived d; Base&amp;amp; ref = d; throw ref;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设匹配顺序正确，这时候会匹配成&lt;code&gt;Base&lt;/code&gt;。这就是所谓的静态类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是如果你是直接&lt;code&gt;throw new Derived()&lt;/code&gt;，这样会正确匹配到&lt;code&gt;Derived&lt;/code&gt;。因为这个时候并不发生多态。静态类型就是&lt;code&gt;Derived&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果值传递会发生切割。如果catch顺序不对的时候会造成提前匹配。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果throw出来的是派生类引用或指针，如果catch顺序不对的时候且可以匹配至基类也会发生提前匹配。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;虽然throw本身在匹配类型的时候会发生切割。但是throw的异常对象本身可以蕴含多态。也就是使用指针或引用可以实现多态。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//指针的切割
class Base{ };
class Derived: public Base{ };
int main(){
    try{
        Base* ptr = new Derived();
        throw ptr;
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Derived*){
        cout&amp;lt;&amp;lt;&quot;Exception type: Derived&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base*){
        cout&amp;lt;&amp;lt;&quot;Exception type: Base&quot;&amp;lt;&amp;lt;endl;
    }
    return 0;
}
//这里抛base。因为ptr的静态类型是base

//引用的切割
class Base{ };
class Derived: public Base{ };
int main(){
    try{
        Derived d;
        Base&amp;amp; b = d;
        throw b;
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Derived&amp;amp;){
        cout&amp;lt;&amp;lt;&quot;Exception type: Derived&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base&amp;amp;){
        cout&amp;lt;&amp;lt;&quot;Exception type: Base&quot;&amp;lt;&amp;lt;endl;
    }
    return 0;
}
//这里还是抛base。因为引用b的静态类型也是base

//正确匹配对象
class Base{ };
class Derived: public Base{ };
int main(){
    try{
        throw Derived();
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Derived){
        cout&amp;lt;&amp;lt;&quot;Exception type: Derived&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base){
        cout&amp;lt;&amp;lt;&quot;Exception type: Base&quot;&amp;lt;&amp;lt;endl;
    }
    return 0;
}

//正确匹配指针
class Base{ };
class Derived: public Base{ };
int main(){
    try{
        throw new Derived();
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Derived*){
        cout&amp;lt;&amp;lt;&quot;Exception type: Derived&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base*){
        cout&amp;lt;&amp;lt;&quot;Exception type: Base&quot;&amp;lt;&amp;lt;endl;
    }
    return 0;
}

//正确匹配引用
class Base{ };
class Derived: public Base{ };
int main(){
    try{
        throw Derived();
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Derived&amp;amp;){
        cout&amp;lt;&amp;lt;&quot;Exception type: Derived&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base&amp;amp;){
        cout&amp;lt;&amp;lt;&quot;Exception type: Base&quot;&amp;lt;&amp;lt;endl;
    }
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多级catch&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;try{
    //可能抛出异常的语句
}catch (exception_type_1 e){
    //处理异常的语句
}catch (exception_type_2 e){
    //处理异常的语句
}
//其他的catch
catch (exception_type_n e){
    //处理异常的语句
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当异常发生时，程序会按照从上到下的顺序，将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索，并将异常交给当前的 catch 处理（其他的 catch 不会被执行）。如果这一层没有找到匹配该异常的语句，就会交给更外层的try-catch来处理。如果最终也没有找到匹配的 catch，就只能交给系统处理，终止程序的运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果抛出的异常不能被捕获，则会调用&lt;code&gt;std::terminate&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;异常处理匹配时，只可以进行有限的类型转换。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向上转型（子转父）
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意会有提前匹配和对象切割问题。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//这版本**注意会有提前匹配问题。无论传递的是子类的对象还是指针都会提前匹配或对象切割**
class Base{ };
class Derived: public Base{ };
int main(){
    try{
        throw Derived();  //抛出自己的异常类型，实际上是创建一个Derived类型的匿名对象.
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base){
        cout&amp;lt;&amp;lt;&quot;Exception type: Base&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Derived){
        cout&amp;lt;&amp;lt;&quot;Exception type: Derived&quot;&amp;lt;&amp;lt;endl;
    }
    return 0;
}
/*
有的编译器会提示：
exception of type ‘Derived’ will be caught by earlier handler for ‘Base’

最后异常是被第一块捕获的。
*/
//这个版本调换了顺序。无论throw的是父类/子类的对象/指针，都会被正确匹配至实际类型。
class Base{ };
class Derived: public Base{ };
int main(){
    try{
        throw new Derived();  
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Derived*){
        cout&amp;lt;&amp;lt;&quot;Exception type: Derived&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base*){
        cout&amp;lt;&amp;lt;&quot;Exception type: Base&quot;&amp;lt;&amp;lt;endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt;转换&lt;/li&gt;
&lt;li&gt;数组或函数指针转换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;匹配上的未必是类型完全匹配那项，而在是最靠前的第一个匹配上的 catch 语句（最先匹配原则）。所以，派生类的处理代码 catch 语句应该放在基类的处理 catch 语句之前，否则先匹配上的总是参数类型为基类的 catch 语句，而能够精确匹配的 catch 语句却不能够被匹配上。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用&lt;code&gt;catch(...)&lt;/code&gt;捕获所有异常。但是必须要放在最末尾，否则所有其后的块都不会被匹配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，在使用捕获所有异常的时候，不存在是使用值传递或引用传递。因为捕获所有异常等于什么都&quot;做不了&quot;。参数都没有名字，你无法访问异常对象。所以根本不存在值传递。或者说，使用捕获所有异常的时候，不会有任何异常对象本身被捕获。但是代码块会被调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/QQ%E6%88%AA%E5%9B%BE20230108232955.png&quot; alt=&quot;QQ截图20230108232955&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;栈回退/栈展开（stack unwind)&lt;/h2&gt;
&lt;p&gt;其实栈展开已经在前面说过，就是从异常抛出点一路向外层函数寻找匹配的 catch 语句的过程，寻找结束于某个匹配的 catch 语句或标准库函数 terminate。这里重点要说的是栈展开过程中对局部变量的销毁问题。我们知道，在函数调用结束时，函数的局部变量会被系统自动销毁，类似的，throw 可能会导致调用链上的语句块提前退出，此时，语句块中的局部变量将按照构成生成顺序的逆序，依次调用析构函数进行对象的销毁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈回退表这个数据结构会帮助编译器正确释放资源。&lt;/li&gt;
&lt;li&gt;退栈的时候，栈上分配的东西(局部变量）都会被正确销毁。但是涉及到在堆上分配的资源则大概率会泄露。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;异常对象的传递，尽量按值抛出，按引用传递（捕获）&lt;/h2&gt;
&lt;p&gt;我们说过异常对象是一个对象。我们可以自己设计它。但是具体传递当中有很多的要点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;异常对象可以通过引用传递。通过引用传递不仅可以实现多态，而且可以减少拷贝。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A
{
public:
    A() :a(0){ cout &amp;lt;&amp;lt; &quot;A默认构造函数&quot; &amp;lt;&amp;lt; endl; }
    A(const  A&amp;amp; rhs){ cout &amp;lt;&amp;lt; &quot;A拷贝构造函数&quot; &amp;lt;&amp;lt; endl; }
    ~A(){ cout &amp;lt;&amp;lt; &quot;A析构函数&quot; &amp;lt;&amp;lt; endl; }
private:
    int  a;
};
int main()
{
        try
        {
            A a ;   
            throw a;
        }
        catch (A a) //注意这里
        {
            ;
        }
    return 0;
}
/*
A默认构造函数
A拷贝构造函数
A析构函数
A拷贝构造函数
A析构函数
A析构函数
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一次默认构造是&lt;code&gt; A a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第一次拷贝构造是&lt;code&gt; throw a&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;我们说过throw表达式是调用拷贝构造&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第一次析构是析构掉对象&lt;code&gt; A a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第二次拷贝构造是拷贝到catch块内的a&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果我们换成引用传递呢？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
        try
        {
            A a ;   
            throw a;
        }
        catch (A&amp;amp; a) //注意这里
        {
            ;
        }
    return 0;
}
/*
A默认构造函数
A拷贝构造函数
A析构函数
A析构函数
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一次默认构造是对象 &lt;code&gt;A a&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一次拷贝构造是 &lt;code&gt;throw a&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于我们是引用传递，所以没有额外的拷贝构造。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们提到过，throw表达式创建的对象是在TIB块内。所以可以离开作用域。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意，这里我们是创建对象后拷贝使用&lt;code&gt;throw&lt;/code&gt;拷贝了一个对象。如果此时我们直接&lt;code&gt;throw A()&lt;/code&gt;会直接构造因为有编译器优化。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 catch 子句的形参是引用类型，那么对它所做的任何更改都会反映到异常对象之中，且如果以 throw; 重抛这个异常，那么它可以被另一个处理块观测到。如果形参不是引用，那么任何对它的更改都是局域的，且它的生存期在处理块退出时结束。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;所以尽可能使用引用传递。尤其是当重抛一个一样的异常的时候，使用值传递会导致多个副本。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;try catch中的多态&lt;/h2&gt;
&lt;p&gt;虽然catch匹配本身不能发生多态，但是其对象本身可以发生多态。上一节我们提到了，使用引用可以实现多态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base{ 
    public:
    virtual void print(){
        cout &amp;lt;&amp;lt;&quot;base&quot; &amp;lt;&amp;lt; endl;
    }
};
class Derived: public Base{
    public:
    virtual void print(){
        cout &amp;lt;&amp;lt;&quot;derived&quot; &amp;lt;&amp;lt; endl;
    }
 };


int main(){
    try{
        throw Derived();  //抛出自己的异常类型，实际上是创建一个Derived类型的匿名对象
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base e){ //值传递。无法多态。
        e.print(); //输出 base
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们上面使用了值传递，则无法多态。如果使用引用或指针传递则可以多态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    try{
        throw Derived();  //抛出自己的异常类型，实际上是创建一个Derived类型的匿名对象
        cout&amp;lt;&amp;lt;&quot;This statement will not be executed.&quot;&amp;lt;&amp;lt;endl;
    }
    catch(Base&amp;amp; e){ //引用传递 虽然匹配无法多态，但是e本身可以多态。
        cout &amp;lt;&amp;lt;&quot;catch base&quot; &amp;lt;&amp;lt; endl;
        e.print(); //输出derive
    }
    catch(Derived&amp;amp; e){
        cout &amp;lt;&amp;lt;&quot;catch derived&quot; &amp;lt;&amp;lt; endl;
        e.print();
    }

    return 0;
}
/*
catch base //这里还是发生了提前匹配。
derived //对象本身可以多态。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;如果try中进行了内存分配，一定要记得在catch中或catch后释放它&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
        try
        {
            A* a = new A;   
            throw a;
        }
        catch (A* a)
        {
            delete a; //如果不delete则会内存泄漏
        }
    //或者放在这里也可以。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们在try内分配了内存，然后throw会拷贝a这个指针（浅拷贝）。catch会接收这个指针。所以我们必须在外部正确释放。&lt;/p&gt;
&lt;p&gt;如果是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
        try
        {
            A* a = new A;   
            throw *a; //啥情况？？？
        }
        catch (A a)
        {
            ;
        }
    return 0;
}
/*
A默认构造函数
A拷贝构造函数
A拷贝构造函数
A析构函数
A析构函数
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是非常极端的例子。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次构造是new A&lt;/li&gt;
&lt;li&gt;第一次拷贝构造是 throw *a 我们拷贝了指针a指向的对象。这非常离谱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这时候原始a指针指向的对象无法析构。内存泄漏&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;第二次的拷贝构造是把throw拷贝的对象拷贝出来到catch块的a中。&lt;/li&gt;
&lt;li&gt;然后throw的对象和catch的对象析构。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;re-throw&lt;/h2&gt;
&lt;p&gt;我们可以在&lt;code&gt;catch&lt;/code&gt;块内再次使用空语句&lt;code&gt;throw;&lt;/code&gt;来直接进行异常的再次抛出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myexcept{
    public:
    
        string msg;
        myexcept(){
            cout &amp;lt;&amp;lt;&quot;--const except--&quot; &amp;lt;&amp;lt; endl;
        }
        myexcept(const myexcept&amp;amp; obj){
            cout &amp;lt;&amp;lt;&quot;--copy const except--&quot; &amp;lt;&amp;lt; endl;
        }
        myexcept(const string&amp;amp; s):msg(move(s)){
            cout &amp;lt;&amp;lt;&quot;--const except--&quot; &amp;lt;&amp;lt; endl;
        };
        void what(){
            cout &amp;lt;&amp;lt; msg &amp;lt;&amp;lt; endl;
        }
        ~myexcept(){
            cout &amp;lt;&amp;lt; &quot;--desc except--&quot; &amp;lt;&amp;lt; endl;
        }
};

void func(){
    int errs = 0;
    for(int i = 0; i &amp;lt; 10; i++){
        try{
            cout &amp;lt;&amp;lt; &quot;try&quot; &amp;lt;&amp;lt; endl;
            throw myexcept(&quot;error occured&quot;);
        }
        catch (myexcept&amp;amp; e){
            cout &amp;lt;&amp;lt;&quot;re-throw&quot; &amp;lt;&amp;lt; endl;
            e.msg = &quot;too many err&quot;;
            throw;
        }
    }
}

int main(){
    try{
        func();
    }
    catch(myexcept&amp;amp; e){
        cout &amp;lt;&amp;lt; &quot;main&quot; &amp;lt;&amp;lt; endl;
        e.what();
    }

    return 0;
}
/*
try
--const except-- 构造异常对象
没有拷贝构造是因为编译器优化。
re-throw 进入catch准备rethrow
main 进入main
too many err 信息已被修改
--desc except-- 异常对象销毁。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们之前提到过，使用引用传递异常对象可以进行修改。尤其是在进行re-throw的时候。&lt;/li&gt;
&lt;li&gt;我们看到信息被修改了，并且没有额外拷贝开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;re-throw可以达成一些特殊目的&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如这段代码的目的是，只要是偶数就抛出异常。但是异常抛出次数小于等于4次的时候可以让程序继续运行。一旦大于4次就重抛至外部，修改异常信息并停止运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func(){
    int errs = 0;
    for(int i = 0; i &amp;lt; 10; i++){
        try{
            cout &amp;lt;&amp;lt;&quot;current is: &quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
            if(i%2 == 0){
                errs++;
                throw runtime_error(&quot;error occured&quot;);//注意这里
            }
        }
        catch (runtime_error&amp;amp; e){ //注意这里
            cout &amp;lt;&amp;lt;&quot;error: &quot; &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; endl;
            if(errs &amp;gt;= 2){
                e = runtime_error(&quot;too many errors&quot;); //注意这里发生了赋值
                throw e;
            }
        }
    }
}


int main(){
    try{
        func();
    }
    catch(runtime_error&amp;amp; e){ //注意这里
        cout &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; endl;
    }

    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意，我们这里&lt;code&gt;throw&lt;/code&gt;的和&lt;code&gt;catch&lt;/code&gt;的全部都是&lt;code&gt;runtime_error&lt;/code&gt;对象而不是&lt;code&gt;exception&lt;/code&gt;对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;exception&lt;/code&gt;是&lt;code&gt;runtime_error&lt;/code&gt;的父类。如果我们使用&lt;code&gt;exception&lt;/code&gt;进行接受并且使用子类对象&lt;code&gt;runtime_error&lt;/code&gt;进行赋值的话，则会发生切割。所以这里要格外注意。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;构造函数和异常&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;构造函数中可以抛出异常，但是抛出的异常会导致该类析构函数无法被调用。因为被视为对象没有成功构造。会存在内存泄漏风险。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;注意，不调用析构函数并不代表局部对象不会析构。该类蕴含的局部对象会析构。因为需要退栈。&lt;/li&gt;
&lt;li&gt;也就是一定要注意本类析构函数的动作不会执行。如果本类的析构函数本来就不执行什么，那么久不会出现问题。但是如果本类析构需要进行资源清理，则会出现问题。&lt;/li&gt;
&lt;li&gt;退栈的时候，栈上分配的东西(局部变量）都会被正确销毁。但是涉及到在堆上分配的资源则大概率会泄露。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;!!!!!如果委托构造函数在非委托构造函数成功完成后以异常退出，那么就会调用此对象的析构函数。!!!!!&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    A(){
        cout &amp;lt;&amp;lt;&quot;A const&quot; &amp;lt;&amp;lt; endl;
    }
    ~A(){
        cout &amp;lt;&amp;lt;&quot;A dest&quot; &amp;lt;&amp;lt; endl;
    }
};
class B{
    public:
    B(){
        cout &amp;lt;&amp;lt;&quot;B const&quot; &amp;lt;&amp;lt; endl;
    }
    ~B(){
        cout &amp;lt;&amp;lt;&quot;B dest&quot; &amp;lt;&amp;lt; endl;
    }
};
class myobj{
    public:
    A a;
    B b;
    myobj(){
        cout &amp;lt;&amp;lt;&quot;myobj const&quot; &amp;lt;&amp;lt; endl;
        throw runtime_error(&quot;runtime error&quot;); //构造函数中抛出异常。
    }
    ~myobj(){
        cout &amp;lt;&amp;lt; &quot;myobj dest&quot; &amp;lt;&amp;lt; endl;
    }
};

class myobj2 {
   public:
    A a;
    B b;
    myobj2() { cout &amp;lt;&amp;lt; &quot;myobj2 const&quot; &amp;lt;&amp;lt; endl; }
    myobj2(int x) : myobj2() {
        cout &amp;lt;&amp;lt; &quot;myobj2 int const&quot; &amp;lt;&amp;lt; endl;
        throw runtime_error(&quot;runtime error&quot;);  // 委托构造函数中抛出异常。
    }
    ~myobj2() { cout &amp;lt;&amp;lt; &quot;myobj2 dest&quot; &amp;lt;&amp;lt; endl; }
};

int main(){
    try{
        myobj a;
    }
    catch(exception&amp;amp; e){
        cout &amp;lt;&amp;lt;&quot;error catched&quot; &amp;lt;&amp;lt; endl;
    }
    return 0;
}
/*
A const
B const
myobj const
B dest
A dest
error catched
注意！ 本类析构没有正确调用。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现析构函数确实没有被调用。杂记2也提到了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;构造函数中可以抛出异常，但是抛出的异常会导致析构函数无法被调用。因为被视为对象没有成功构造。会存在内存泄漏风险&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是本类蕴含的&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;B&lt;/code&gt;对象被正确析构了。&lt;/p&gt;
&lt;h3&gt;异常和委托构造&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;如果委托构造函数在非委托构造函数成功完成后以异常退出，那么就会调用此对象的析构函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看下&lt;a href=&quot;https://cppquiz.org/quiz/giveup/374&quot;&gt;这个小测试&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
    myobj2 b;
} catch (exception&amp;amp; e) {
    cout &amp;lt;&amp;lt; &quot;myobj2 error catched&quot; &amp;lt;&amp;lt; endl;
}
/*
A const
B const
myobj2 const
myobj2 dest // 注意 myobj2的析构函数正确调用
B dest
A dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是上面的代码。如果是委托构造中抛出异常，则该类析构可以正常调用。出自标准文档的&lt;a href=&quot;https://timsong-cpp.github.io/cppwp/std23/except.ctor#4&quot;&gt;except.ctor&lt;/a&gt;。 我理解是因为委托构造中使用的非委托构造结束的一刻，对象就被视为构造完毕了。所以是合理的。&lt;/p&gt;
&lt;h3&gt;构造函数异常导致的内存泄漏&lt;/h3&gt;
&lt;p&gt;假设我们有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    A(){
        cout &amp;lt;&amp;lt;&quot;A const&quot; &amp;lt;&amp;lt; endl;
    }
    ~A(){
        cout &amp;lt;&amp;lt;&quot;A dest&quot; &amp;lt;&amp;lt; endl;
    }
};
class B{
    public:
    B(){
        cout &amp;lt;&amp;lt;&quot;B const&quot; &amp;lt;&amp;lt; endl;
    }
    ~B(){
        cout &amp;lt;&amp;lt;&quot;B dest&quot; &amp;lt;&amp;lt; endl;
    }
};

class myobj{
    public:
    A* ptra;
    B* ptrb;
    myobj(){
        ptra = new A(); //分配资源
        ptrb = new B();
        throw runtime_error(&quot;runtime error&quot;); //抛出异常
    }
    ~myobj(){
        cout &amp;lt;&amp;lt; &quot;myobj dest&quot; &amp;lt;&amp;lt; endl;
        delete ptra; //释放资源
        delete ptrb;
    }
};


int main(){
    try{
        myobj a;
    }
    catch(exception&amp;amp; e){
        cout &amp;lt;&amp;lt;&quot;error catched&quot; &amp;lt;&amp;lt; endl;
    }
    return 0;
}

/*
输出
A const
B const
error catched
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现内存泄露了。原因和上一节提到的一样。所以如果在析构函数中抛出异常则必须要妥善处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在构造函数内也使用try catch捕获所有异常并重新抛出。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
    A* ptra;
    B* ptrb;
    myobj(){
        try{
            ptra = new A();
            ptrb = new B();
            throw runtime_error(&quot;runtime error&quot;);
        }
        catch(...){ //捕获所有异常
            delete ptra;
            delete ptrb;
            cout &amp;lt;&amp;lt; &quot;inner catched&quot; &amp;lt;&amp;lt; endl;
            throw; //重抛
        }
    }
    ~myobj(){
        cout &amp;lt;&amp;lt; &quot;myobj dest&quot; &amp;lt;&amp;lt; endl;
        delete ptra;
        delete ptrb;
    }
};


int main(){
    try{
        myobj a;
    }
    catch(exception&amp;amp; e){
        cout &amp;lt;&amp;lt;&quot;error catched&quot; &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;或使用RAII如智能指针包裹&lt;code&gt;A/B&lt;/code&gt;对象。此时无需在构造函数内继续使用try-catch&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
    shared_ptr&amp;lt;A&amp;gt; ptra;
    shared_ptr&amp;lt;B&amp;gt; ptrb;
    myobj(){
        ptra = make_shared&amp;lt;A&amp;gt;(); //智能指针
        ptrb = make_shared&amp;lt;B&amp;gt;(); //智能指针
        throw runtime_error(&quot;runtime error&quot;);
    }
    ~myobj(){
        cout &amp;lt;&amp;lt; &quot;myobj dest&quot; &amp;lt;&amp;lt; endl;
    }
};


int main(){
    try{
        myobj a;
    }
    catch(exception&amp;amp; e){
        cout &amp;lt;&amp;lt;&quot;error catched&quot; &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;再次注意。构造函数中的内存泄漏针对的是本类的内存泄漏。也就是try中分配的内存需要catch中释放。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果抛出异常的构造函数的类没有在try中分配什么东西，那就不会泄露。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct ExceptionObject
{
    int a;
    ExceptionObject()
    {   
        throw &quot;an exception&quot;;
    }
};
int main()
{
    ExceptionObject* ptr = nullptr;
    try
    {
        ptr = new ExceptionObject(); //该new在构建对象的时候发现异常，则取消构造。并且调用delete释放。
    }
    catch(...)
    {
        //delete or not delete ptr?
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**上面的漏了吗？ 没漏。**因为&lt;code&gt;ExceptionObject&lt;/code&gt;没有手动分配内存。我们下面看到的&lt;code&gt;try&lt;/code&gt;里面的&lt;code&gt;new&lt;/code&gt;在构建&lt;code&gt;ExceptionObject&lt;/code&gt;对象的时候发现异常就会把自己的&lt;code&gt;new&lt;/code&gt;释放。编译器会帮助我们。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果初始化因抛出异常而终止（例如来自构造函数），那么 new 表达式在已经分配了任何存储的情况下会调用适当的&lt;a href=&quot;https://zh.cppreference.com/w/cpp/memory/new/operator_delete&quot;&gt;解分配函数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个解分配只解分配自己。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;但是下面的漏了吗？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct ExceptionObject
{
    int* a;
    ExceptionObject(){
        a = new int(10); //漏了
        throw &quot;an exception&quot;;
    }
};
int main()
{
    ExceptionObject* ptr = nullptr;
    try{
        ptr = new ExceptionObject();
    }
    catch(...){
        //delete or not delete ptr?
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;漏了。因为我们&lt;code&gt;ExceptionObject&lt;/code&gt;有手动分配内存。&lt;code&gt;ExceptionObject&lt;/code&gt;的析构不会调用。这时候就会出现泄漏&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/QQ%E6%88%AA%E5%9B%BE20230109165809.png&quot; alt=&quot;QQ截图20230109165809&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;new&lt;/code&gt;在遇到异常的时候只会解分配自己分配的内存。&lt;code&gt;ExceptionObject&lt;/code&gt;里面的那个指针分配的内存并不是这个&lt;code&gt;new&lt;/code&gt;分配的。所以它不负责。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;一个自定义异常对象的例子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class myExcept{ //异常对象
    public:
        enum m_errtype{NO_ERR, LENGTH_ERR, VALID_ERR, BOTH_ERR}; //枚举类，更加美观
        myExcept():m_ERRTYPE(NO_ERR){}; //默认构造函数，无异常
        myExcept(int x):m_ERRTYPE(LENGTH_ERR), length(x){}; //构造函数1：目的是实现第1种异常对象的构造
        myExcept(bool x):m_ERRTYPE(VALID_ERR), valid(x){}; //构造函数2：目的是实现第2种异常对象的构造
        myExcept(int x,bool y):m_ERRTYPE(BOTH_ERR), length(x), valid(y){}; //构造函数3：目的是实现第3种异常对象的构造
        void what() const{ //what函数模拟标准库exception类对象。这个函数会打印本类的具体错误。
            if(m_ERRTYPE == LENGTH_ERR){
                cout &amp;lt;&amp;lt;&quot; EXCEPTION!! exceed length size: &quot; &amp;lt;&amp;lt; length &amp;lt;&amp;lt; endl;
            }
            else if(m_ERRTYPE == VALID_ERR){
                cout &amp;lt;&amp;lt;&quot; EXCEPTION!! not valid state: &quot; &amp;lt;&amp;lt; valid &amp;lt;&amp;lt; endl;
            }
            else if(m_ERRTYPE == BOTH_ERR){
                cout &amp;lt;&amp;lt;&quot; EXCEPTION!! exceed length: &quot;&amp;lt;&amp;lt; length &amp;lt;&amp;lt;&quot; and not valid state: &quot; &amp;lt;&amp;lt; valid &amp;lt;&amp;lt; endl;
            }
        }
    private:
        m_errtype m_ERRTYPE; //错误类型
        int length; // 具体资源状态。用于显示具体错误。
        bool valid; // 具体资源状态。用于显示具体错误。
};

class myobj{
    public:
    int length;
    bool valid;
    myobj() = default;
    myobj(int x, bool y):length(x), valid(y){
        cout &amp;lt;&amp;lt; &quot;myobj constructed&quot; &amp;lt;&amp;lt; endl; 
    }
    void test_func(int x, bool y){
        cout &amp;lt;&amp;lt; &quot;test func&quot; &amp;lt;&amp;lt; endl;
        if(x &amp;lt; 10 &amp;amp;&amp;amp; y == false){
            throw myExcept(x,y);
        }
        else if(y == false){
            throw myExcept(y);
        }
        else if(x &amp;lt; 10){
            throw myExcept(x);
        }
    }
    ~myobj(){
        cout &amp;lt;&amp;lt;&quot;myobj destructed&quot; &amp;lt;&amp;lt; endl;
    }
};

int main(){
    myobj obj(1,true);
    try{
        obj.test_func(0, true); //调用时刻。
    }
    catch(myExcept&amp;amp; e){
        e.what();
    }
    cout &amp;lt;&amp;lt;&quot;main continued&quot; &amp;lt;&amp;lt; endl;
    return 0;
}
/*
第一种 obj.test_func(0, true);
myobj constructed
test func
 EXCEPTION!! exceed length size: 0
main continued
myobj destructed
剩下的不举例了。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数try块 function-try-block&lt;/h2&gt;
&lt;p&gt;这个东西在构造函数使用初始化列表的情况下偶尔有用。它主要是把构造函数初始化列表中和函数体中的异常区别开来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在进入任何构造函数上的函数 try 块的 catch 子句前，所有完整构造的成员和基类都会被销毁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;规则都一样。但是有一点不同：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在（构造）函数&lt;code&gt;catch&lt;/code&gt;块中：就算我们不显式的&lt;code&gt;throw&lt;/code&gt;，编译器也会隐式安插&lt;code&gt;throw&lt;/code&gt;。默认是重抛。所以在最外层依旧要再次处理这个异常。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它长这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    int val;
    A(){
        cout &amp;lt;&amp;lt;&quot;A const&quot; &amp;lt;&amp;lt; endl;
    }
    A(int x):val(x){
        cout &amp;lt;&amp;lt;&quot;A const&quot; &amp;lt;&amp;lt; endl;
    }
    ~A(){
        cout &amp;lt;&amp;lt;&quot;A dest&quot; &amp;lt;&amp;lt; endl;
    }
};
class B{
    public:
    int val;
    B(){
        cout &amp;lt;&amp;lt;&quot;B const&quot; &amp;lt;&amp;lt; endl;
    }
    B(int x):val(x){
        cout &amp;lt;&amp;lt;&quot;B const err&quot; &amp;lt;&amp;lt; endl;
        throw runtime_error(&quot;ERR&quot;); //b的构造函数抛一个异常。
    }
    ~B(){
        cout &amp;lt;&amp;lt;&quot;B dest&quot; &amp;lt;&amp;lt; endl;
    }
};
class myobj{
    public:
    A a;
    B b;
    myobj(int x, int y) try: a(x), b(y){ //try在这里。在初始化列表前。
        cout&amp;lt;&amp;lt;&quot;myobj const&quot; &amp;lt;&amp;lt; endl;
    }
    catch(runtime_error&amp;amp; e){ //后面紧跟catch
        // cout &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; endl;
        // throw; 
        //这里就算不throw也会隐式安插throw
        //进入这个块之前，A会被析构。
    }
    ~myobj(){
        cout &amp;lt;&amp;lt; &quot;myobj dest&quot; &amp;lt;&amp;lt; endl;
    }
};

int main(){
    try{
        myobj a(1,2); //由于myobj构造函数一定会再次重抛异常到外面。所以这里必须再次用try catch接一下。
    }
    catch(runtime_error&amp;amp; e){
        cout &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;noexcept&lt;/h2&gt;
&lt;p&gt;noexcept指明了函数不会抛出异常。这样做可以提升性能，因为编译期不会去思考栈展开的问题。但是有很多点需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只有异常说明不同的函数不能重载***（与返回类型相似，异常说明是函数类型的一部分，但不是函数签名的一部分）(C++17 起)***&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果虚函数不会抛出，那么它每个覆盖的函数的所有声明（包括定义）都必须不抛出，除非覆盖函数被定义为&lt;code&gt;delete&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;也就是父类虚函数声明抛出异常，子类的虚函数重写的时候也必须声明抛出异常。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**不会抛出的函数允许调用有可能会抛出的函数。**每当抛出异常且对处理块的查找遇到了不会抛出的函数的最外层块时，就调用函数&lt;code&gt;std::terminate&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是函数自己不抛异常，但是不代表它们内部的调用不会抛出异常，并且编译器不会提供调用者与被调用者的&lt;code&gt;noexcept&lt;/code&gt;一致性检查。&lt;/li&gt;
&lt;li&gt;当一个声明为&lt;code&gt;noexcept&lt;/code&gt;的函数抛出异常时，程序会被终止并调用&lt;code&gt;std::terminate();&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;绝大多数情况下，析构函数会被编译器隐式添加&lt;code&gt;noexcept&lt;/code&gt;&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果希望析构函数抛出异常，必须显式使用&lt;code&gt;noexcept(false)&lt;/code&gt;指明&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数指针及该指针所指的函数最好具有一致的异常说明。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;typedef &lt;/code&gt;或类型别名中则不能出现 &lt;code&gt;noexcept&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在成员函数中，&lt;code&gt;noexcept&lt;/code&gt;说明符需要跟在 &lt;code&gt;const&lt;/code&gt; 及引用限定符之后，而在 &lt;code&gt;final&lt;/code&gt;、&lt;code&gt;override &lt;/code&gt;或纯虚函数的 &lt;code&gt;=0 &lt;/code&gt;之前。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;什么时候建议使用noexcept&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;以下指的是如果这些实现不抛出异常的话。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动构造和移动赋值&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;尤其针对&lt;code&gt;vector&lt;/code&gt;而言。想要触发&lt;code&gt;vector&lt;/code&gt;使用存储元素类型对应的移动构造时，则对应元素类型的移动构造必须声明为&lt;code&gt;noexcept&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;为什么？我们想一下：如果在扩容元素时出现异常怎么办？&lt;/li&gt;
&lt;li&gt;当我们使用拷贝构造时：
&lt;ul&gt;
&lt;li&gt;申请新空间时出现异常：旧vector还是保持原有状态，抛出的异常交由用户自己处理。&lt;/li&gt;
&lt;li&gt;copy元素时出现异常：所有已经被copy的元素利用元素的析构函数释放，已经分配的空间释放掉，抛出的异常交由用户自己处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这样看起来比较不错。但是并没有移动。利用&lt;code&gt;move&lt;/code&gt;的交换类资源所有权的特性，使用vector扩容效率大大提高，但是当发生异常时怎么办？
&lt;ul&gt;
&lt;li&gt;原有容器的状态已经被破坏，有部分元素的资源已经被偷走。若要恢复会极大增加代码的复杂性和不可预测性。所以只有当vector中元素的&lt;code&gt;move constructor&lt;/code&gt;是&lt;code&gt;noexcept&lt;/code&gt;时，vector扩容才会采取move方式来提高性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;swap&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简单的函数。 例如获取类成员变量，类成员变量的简单运算等。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;noexcept()的小技巧。&lt;/h3&gt;
&lt;p&gt;我们知道不会抛出的函数允许调用有可能会抛出的函数。但是这是有问题的。那么我们又没有办法让编译器帮我们做决定呢？&lt;/p&gt;
&lt;p&gt;我们可以这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void test(int x){
    //...
}
int sum(int x, int y) noexcept(noexcept(test(x))){
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;noexcept(test(x))&lt;/code&gt;传入了一个我们希望检查是否noexcept的函数和其参数。&lt;strong&gt;编译器会在编译时检查&lt;/strong&gt;这一点并根据结果为函数添加说明符。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.cnblogs.com/RioTian/p/15115387.html&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/222167649&lt;/p&gt;
&lt;p&gt;https://songlee24.github.io/2015/01/12/cpp-exception-in-constructor/&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/liang19890820/article/details/120662921&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/weixin_50640987/article/details/124406222&lt;/p&gt;
&lt;p&gt;http://baiy.cn/doc/cpp/inside_exception.htm&lt;/p&gt;
&lt;p&gt;https://developer.aliyun.com/article/75525&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/406894769&lt;/p&gt;
&lt;h1&gt;lambda&lt;/h1&gt;
&lt;p&gt;我个人不太喜欢lambda。可读性感觉很一般。但是感觉大家都在用，那么在这里再次整理一下lambda。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;另外一点是lambda和closure的关系。lambda表达式被用来创建closure类型。&lt;/strong&gt; 来自&lt;a href=&quot;https://stackoverflow.com/questions/32911729/understanding-how-lambda-closure-type-has-deleted-default-constructor&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;lambda 表达式&lt;/strong&gt;是纯右值表达式，它&lt;strong&gt;的类型&lt;/strong&gt;是独有的无名非&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/union&quot;&gt;联合体&lt;/a&gt;非&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/aggregate_initialization&quot;&gt;聚合体&lt;/a&gt;类类型，&lt;strong&gt;被称为&lt;em&gt;闭包类型&lt;/em&gt;&lt;/strong&gt;，它（对于 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/adl&quot;&gt;实参依赖查找&lt;/a&gt; 而言）声明于含有该 lambda 表达式的最小块作用域、类作用域或命名空间作用域。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;基本语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[capture](parameters) mutable可选 throw()可选 -&amp;gt;returntype
{statement
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[capture]&lt;/code&gt;：捕捉列表。捕捉列表总是出现在Lambda函数的开始处。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在捕捉列表里面的参数我们称之为behavior parameter行为参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt;也是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数。捕捉列表能够捕捉上下文中的变量以供Lambda函数使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果把lambda当做容器的自定义排序或比较的函数对象使用，则一般都不用捕捉参数。捕捉参数可以理解为上下文所需要的内容。如果一个lambda只需要对传入的参数进行修改或访问，则不需要捕捉变量。如果需要lambda上下文中的变量，才需要进行捕捉。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;捕获列表里面的变量将成为lambda这个匿名类的成员变量。根据捕捉方式决定其是否可以改变。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;默认情况下，捕获列表内的变量不可修改。如要修改，必须给lambda表达式增添mutable关键字。&lt;/li&gt;
&lt;li&gt;捕获列表内的变量如果按照值捕获，对其的修改不会影响外部变量。因为捕获列表内的变量成为匿名类的局部变量。并且是按照值拷贝的&lt;/li&gt;
&lt;li&gt;捕获列表内的变量如果按照引用捕获，对其的修改则会影响外部变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;(parameters)&lt;/code&gt;：参数列表。与普通函数的参数列表一致。如果不需要参数传递，则可以连同括号“()”一起省略;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数列表里面的参数我们称之为call parameter调用参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mutable&lt;/code&gt;：&lt;strong&gt;mutable修饰符。默认情况下，Lambda函数总是一个const函数，mutable可以取消其常量性。在使用该修饰符时，参数列表不可省略（即使参数为空）;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意，这个mutable针对的是捕获列表里面值传递的变量。而非函数入参。且引用传入的时候默认是可以修改的，可以不添加mutable&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-&amp;gt;return-type&lt;/code&gt;：返回类型。用追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号&lt;code&gt;-&amp;gt;&lt;/code&gt;一起省略。此外，在返回类型明确的情况下，也可以省略该部分，让编译器对返回类型进行推导;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;{statement}&lt;/code&gt;：函数体。内容与普通函数一样，不过除了可以使用参数之外，还可以使用所有捕获的变量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与**普通函数最大的区别是，除了可以使用参数以外，Lambda函数还可以通过捕获列表访问一些上下文中的数据。**具体地，捕捉列表描述了上下文中哪些数据可以被Lambda使用，以及使用方式（以值传递的方式或引用传递的方式）。语法上，在&lt;code&gt;[]&lt;/code&gt;包括起来的是捕捉列表，捕捉列表由多个捕捉项组成，并以逗号分隔。捕捉列表有以下几种形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt;空表示不捕获&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[=]&lt;/code&gt;表示值传递方式捕获所有父作用域的变量（包括&lt;code&gt;this&lt;/code&gt;）
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用值传递的捕捉方式时，类中会添加相应类型的非静态数据成员。运行的时候会用复制的值去拷贝初始化成员变量&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[&amp;amp;]&lt;/code&gt;表示引用传递方式捕获所有父作用域的变量（包括&lt;code&gt;this&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[var]&lt;/code&gt;表示只以值传递方式捕获特定变量&lt;code&gt;var&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[&amp;amp;var]&lt;/code&gt;表示只引用传递捕获特定变量&lt;code&gt;var&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[=, &amp;amp;var]&lt;/code&gt;默认以值捕获所有变量，但是&lt;code&gt;var&lt;/code&gt;是例外，通过引用捕获&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[&amp;amp;, var]&lt;/code&gt;默认以引用捕获所有变量，但是&lt;code&gt;var&lt;/code&gt;是例外，通过值捕获&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[this]&lt;/code&gt;表示只以值传递方式捕获当前的&lt;code&gt;this&lt;/code&gt;指针&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这里指的是复制指针。所以是值传递指针，但由于是指针，所以也是引用捕获当前对象。这里格外注意悬垂引用问题&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[*this]&lt;/code&gt;：通过传值方式捕获当前对象 。也就是拷贝对象（C++17起）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面提到了一个父作用域，也就是包含Lambda函数的语句块，说通俗点就是包含Lambda的“{}”代码块。上面的捕捉列表还可以进行组合，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[=,&amp;amp;a,&amp;amp;b]&lt;/code&gt;表示以引用传递的方式捕捉变量&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;，以值传递方式捕捉其它所有变量;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[&amp;amp;,a,this]&lt;/code&gt;表示以值传递的方式捕捉变量&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;this&lt;/code&gt;，引用传递方式捕捉其它所有变量。 不过值得注意的是，捕捉列表不允许变量重复传递。例如：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[=,a]&lt;/code&gt;这里已经以值传递方式捕捉了所有变量，但是重复捕捉&lt;code&gt;a&lt;/code&gt;了，会报错的;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[&amp;amp;,&amp;amp;this]&lt;/code&gt;这里&lt;code&gt;&amp;amp;&lt;/code&gt;已经以引用传递方式捕捉了所有变量，再捕捉&lt;code&gt;this&lt;/code&gt;也是一种重复。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;针对匿名lambda，可以直接在其表达式结尾额外添加一组括号表示函数调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[](){cout &amp;lt;&amp;lt;&quot;hello world&quot; &amp;lt;&amp;lt; endl;}(); //注意结尾额外的一组括号
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按值捕获一个变量后，如果在lambda表达式后方修改这个变量，不会影响到lambda内这个变量的值。也就是lambda表达式之前这个变量是什么，他在lambda表达式中表现的就是什么。这个变量后续的行为和lambda表达式无关。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int val = 0; 
    auto f = [val]() mutable{ //mutable是必须要写的。否则不可修改val
        cout &amp;lt;&amp;lt; &quot;val:&quot; &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl; //0
        val++;
        cout &amp;lt;&amp;lt; &quot;val:&quot; &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl; //1
    };
    cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl; //0
    val = 28;
    cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;//28
    f();
    cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;//28
}
//针对这个例子，每一次f()的调用影响的都是lambda内部的val变量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么&lt;code&gt;val=28&lt;/code&gt;没有改变外部的值？同时lambda内部的修改值没有影响到外面？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**lambda是一个匿名类，（也就是匿名的函数对象）。捕获的变量会变成这个匿名类的类成员变量。**如果按照引用捕获那么这个成员变量就是个引用。自然你在外部修改也会影响里面的值。正因为如此，&lt;strong&gt;如果按照值来捕获，在创建这个lambda表达式的那一刻，被按照值捕获的变量会被在这个匿名类内创建（拷贝）并按照它在这一行之前的那个值来进行附初值。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为lambda的类型是匿名类，编译器认为每个lambda表达式都不同，哪怕定义完全一样其匿名类名也会不一样，所以lambda表达式只能用auto类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**lambda的默认构造函数和赋值操作默认是delete的。也就是说，lambda类型非可默认构造（C++20前）**但是，它有浅拷贝构造函数可被调用。而使用&lt;code&gt;decltype&lt;/code&gt;提取未计算的lambda表达式的类型是不允许的，但是，&lt;code&gt;decltype&lt;/code&gt;提取已计算的的lambda表达式是可以的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;auto g = [](int x){return x;}
decltype(g) another = g; //可以，已计算的lambda

decltype(auto g = [](int x){return x;}) another = g; //不可以。未计算的lambda
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;lambda没有默认构造，没有拷贝赋值。有隐式生成的拷贝构造和移动构造。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;lambda还有合成的用户定义转换函数。它的作用是返回一个函数指针。这个函数指针指向内部合成的静态成员函数（严格来说是public, 非虚，非explicit且const noexcept）， 这个转换函数返回一个指向具有 C++ &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/language_linkage&quot;&gt;语言连接&lt;/a&gt;的函数指针，调用该函数的效果与在默认构造的lambda类型实例上调用lambda类型的函数调用运算符&lt;code&gt;operator()&lt;/code&gt;的效果相同。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后一句话，我们可以理解为这个函数内部会调用本类的函数调用运算符&lt;code&gt;operator()&lt;/code&gt;。虽然事实并非如此，但是效果一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;！！！只有在捕获列表为空的时候才会合成此用户定义转换函数。！！！！&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;因此我们可以把lambda传入一个只接受函数指针的C函数。如&lt;code&gt;atexit&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们是值传递。在lambda创建的那一刻。val已经被&lt;strong&gt;拷贝构造&lt;/strong&gt;至匿名类内了。所以对类内的修改作用域只在lambda内部。自然不会影响外部。&lt;/p&gt;
&lt;p&gt;具体例子就是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//具体的类函数实现就是构造函数文件内的。这里不复制粘贴了。
int main(){
    myobj val (20);
    auto f = [val]() mutable{ //值传递
    };
}
/*
const
copy const //val被拷贝构造至lambda内。
dest
dest
*/
int main(){
    myobj val (20);
    auto f = [&amp;amp;val]() mutable{ //引用传递
    };
}
/*
const //引用传递不会发生拷贝。
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说基本情况下，一个lambda的匿名类大概长这样。所以必须要显式使用mutable让其表示可以修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Closure{
  	public:
    
    	ReturnType operator()(params) const{
            //具体内容
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://www.jianshu.com/p/d686ad9de817&lt;/p&gt;
&lt;h2&gt;lambda的大小&lt;/h2&gt;
&lt;p&gt;我们说过lambda是匿名类，并且它会根据捕获的参数和使用的是引用捕获还是值捕获来使用对应的方式初始化匿名类内对应的成员。所以它的大小是因情况而异，取决于捕获的参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char a1[20];
char b1[300];
auto f = [a1](){}; //值捕获大小为20的数组
auto f1 = [&amp;amp;a1](){}; //引用捕获大小为20的数组
auto f2 = [b1](){}; //值捕获大小为300的数组
auto f3 = [&amp;amp;b1](){}; //引用捕获大小为300的数组
cout &amp;lt;&amp;lt; sizeof(f) &amp;lt;&amp;lt; endl; //20
cout &amp;lt;&amp;lt; sizeof(f1) &amp;lt;&amp;lt; endl; //8
cout &amp;lt;&amp;lt; sizeof(f2) &amp;lt;&amp;lt; endl;//300
cout &amp;lt;&amp;lt; sizeof(f3) &amp;lt;&amp;lt; endl;//8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以这个lambda的大小会发生变化。&lt;/p&gt;
&lt;h2&gt;注意引用捕获导致的悬垂引用&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;std::function&amp;lt;int(int)&amp;gt; add_x(int x)
{
    return [&amp;amp;](int a) { return x + a; };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为参数&lt;code&gt;x&lt;/code&gt;仅是一个局部变量，函数调用后就被销毁，但是返回的&lt;code&gt;lambda&lt;/code&gt;表达式却引用了该变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename Func, typename... Args&amp;gt;
std::function&amp;lt;void()&amp;gt; addTask(Func &amp;amp;&amp;amp;f, Args&amp;amp;&amp;amp;... args){
    std::function&amp;lt;void()&amp;gt; func = [&amp;amp;](){ //注意这里是按照引用捕获了，有潜在bug
        f(std::forward&amp;lt;Args&amp;gt;(args)...);
    };
    return func;
}

void fuck1(int a){
    std::cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; std::endl;
}
void fuck2(int a, const std::string&amp;amp; b, double c){
    std::cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; b &amp;lt;&amp;lt; c &amp;lt;&amp;lt; std::endl;
}
int main(){
    std::vector&amp;lt;std::function&amp;lt;void()&amp;gt;&amp;gt; vecs;
    int input = 10;
    vecs.push_back(addTask(fuck1, 10)); //注意这里是右值。
    vecs.push_back(addTask(fuck2, 10, &quot;abcde&quot;, 6.678));
    vecs[0](); 
    vecs[1]();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的目的是使用&lt;code&gt;std::function&lt;/code&gt;来包装一个任意多参数的无返回值的函数。上面的实现是错误的。因为我们一直是用右值引用把函数和参数传入&lt;code&gt;addTask&lt;/code&gt;函数，然后&lt;code&gt;lambda&lt;/code&gt;又使用引用捕获了参数。这里会非常危险。如果像&lt;code&gt;addTask(fuck1, 10)&lt;/code&gt;这样使用，这个&lt;code&gt;10&lt;/code&gt;会在当前行返回后失效。那么&lt;code&gt;lambda&lt;/code&gt;捕获的对&lt;code&gt;10&lt;/code&gt;的引用也会失效。当然如果能保证这个&lt;code&gt;10&lt;/code&gt;一直有效，则没有问题。但问题就在于如何保证&lt;code&gt;10&lt;/code&gt;一直有效。所以我们可以使用值传递。或者是使用&lt;code&gt;std::bind&lt;/code&gt;完成我们的目的。这也就是为什么&lt;code&gt;std::bind&lt;/code&gt;一直默认是拷贝传入。&lt;/p&gt;
&lt;h2&gt;其他资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;a href=&quot;https://youtu.be/IgNUBw3vcO4&quot;&gt;这里&lt;/a&gt;观看Nicolai Josuttis在CppCon 2021中对lambda的详细介绍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用lambda的原因之一是更容易被编译器处理为内联，性能更好。关于这一点，查看&lt;a href=&quot;https://stackoverflow.com/questions/13722426/why-can-lambdas-be-better-optimized-by-the-compiler-than-plain-functions&quot;&gt;这里&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;lambda和内联，查看&lt;a href=&quot;https://stackoverflow.com/questions/15930755/are-lambdas-inlined-like-functions-in-c&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;关于lambda和智能指针/移动捕获（通用捕获）相关内容查看智能指针章节。&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注意，这个方法也可以用在当你想声明一个在lambda内部使用的变量时(理解为闭包类型的成员变量)&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;一些搭配容器的例子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int x = 100;
    auto cmp = [](const Person&amp;amp; a, const Person&amp;amp; b){return (a.id &amp;lt; b.id);
    };
    set&amp;lt;Person, decltype(cmp)&amp;gt; my_set(cmp); //set的自定义比较。利用decltype。必须传入构造函数。
    my_set.emplace(3,1);
    my_set.emplace(1,2);
    my_set.emplace(2,2);
    for(auto&amp;amp; i:my_set){
        cout &amp;lt;&amp;lt; i.id &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; i.day &amp;lt;&amp;lt; endl;
    }



    vector&amp;lt;Person&amp;gt; my_vec;
    my_vec.emplace_back(3,1);
    my_vec.emplace_back(1,2);
    my_vec.emplace_back(2,2);
    sort(my_vec.begin(), my_vec.end(), [](const Person&amp;amp; a, const Person&amp;amp; b){return a.id &amp;lt; b.id;}); //sort的自定义排序
    for(auto&amp;amp; i:my_vec){
        cout &amp;lt;&amp;lt; i.id &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; i.day &amp;lt;&amp;lt; endl;
    }
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意，在把lambda当做自定义比较器传入&lt;code&gt;set&lt;/code&gt;容器中的时候，不仅要在模板处指定类型，也要放入&lt;code&gt;set&lt;/code&gt;的构造函数中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为我们说过了。lambda没有默认构造函数和赋值操作。如果不把lambda作为参数传入进去的话，**&lt;code&gt;set&lt;/code&gt;的默认无参构造函数会调用lambda的无参构造函数。**但是lambda没有无参构造函数。所以会报错。我们必须要把lambda传入这样可以调用&lt;code&gt;set&lt;/code&gt;的另一种有参构造函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;另一种有参构造：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;set();
//具体实现：
set() : _Mybase(key_compare()) {} //直接构造函数对象。但是lambda没有默认构造。
explicit set( const Compare&amp;amp; comp,
              const Allocator&amp;amp; alloc = Allocator() );
//具体实现：
explicit set(const key_compare&amp;amp; _Pred) : _Mybase(_Pred) {} //对函数对象进行拷贝。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么说lambda是一个非常方便的在运行时定义函数的功能&lt;/h2&gt;
&lt;p&gt;假设我们需要查找容器内小于特定数值的元素，如果使用&lt;code&gt;bind&lt;/code&gt;，则会像是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool less_certain(int val, int elem){
    return elem &amp;lt; val;
}
int main(){
    vector&amp;lt;int&amp;gt;a{1,2,3,4,5,6,7};
    int val = 7;
    int num = count_if(a.begin(), a.end(), bind(less_certain, val, std::placeholders::_1));
    cout &amp;lt;&amp;lt; num &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在杂记3中讨论过，能用lambda就不要用&lt;code&gt;bind&lt;/code&gt;。所以可以像这样使用。&lt;strong&gt;我们可以让lambda的功能依靠&lt;em&gt;运行时&lt;/em&gt;参数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt;a{1,2,3,4,5,6,7};
    int val = 7;
    int num = count_if(a.begin(), a.end(), [val](int elem){
        return elem &amp;lt; val;
    });
    cout &amp;lt;&amp;lt; num &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;泛型lambda在模板笔记&lt;/h2&gt;
&lt;h2&gt;模板lambda （C++20才正式合法)&lt;/h2&gt;
&lt;p&gt;泛型lambda其实是不显式指定模板形参的情况。而模板lambda是显示指定模板形参的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto lambda = [](auto x) {
    // x 可以是任意类型，编译器会为每种调用生成对应的函数体
    return x + 1;
}; // 泛型lambda

auto lambda = []&amp;lt;typename T&amp;gt;(T x) {
    // 这里可以在 lambda 中显式使用 T
    return x + 1;
};// 模板lambda

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一些用法、提示和注意事项&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int main() {
    auto closure = []&amp;lt;typename T&amp;gt;(T x) {
        return [=] { std::cout &amp;lt;&amp;lt; &quot;Inner lambda prints: &quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &quot;\n&quot;; };
    };
    auto closure2 = closure.template operator()&amp;lt;int&amp;gt;(10);
    auto same_as_closure2 = closure(10); // 和 closure2作用一致
    auto closure3 = ([]&amp;lt;typename T&amp;gt;(T x) {
                        return [=] { std::cout &amp;lt;&amp;lt; &quot;Inner lambda prints: &quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &quot;\n&quot;; };
                    }).template operator()&amp;lt;int&amp;gt;(10);
    closure2();
        same_as_closure2();
    closure3();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道lambda是有重载了&lt;code&gt;operator()&lt;/code&gt;的类。所以我们既可以直接&lt;code&gt;lambda(input)&lt;/code&gt;调用，也可以&lt;code&gt;lambda.operator(input)&lt;/code&gt;调用。当有模板的时候，必须要显示使用&lt;code&gt;lambda.template operator(input)&lt;/code&gt;来告诉编译器我们在这里要传递&lt;strong&gt;显式&lt;/strong&gt;的模板参数。&lt;/p&gt;
&lt;p&gt;所以此处，closure2和same_as_closure2是等价的。那么啥时候会有区别呢？那就是我们无法推导出模板形参类型的时候。假设这个情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto closure4 = []&amp;lt;auto Fn&amp;gt;(int input) -&amp;gt; std::function&amp;lt;void(int)&amp;gt; {
    return [input](int x) -&amp;gt; void {
        cout &amp;lt;&amp;lt; &quot;input:&quot; &amp;lt;&amp;lt; input &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt; &quot;x: &quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
        Fn(input + x);
    };
};
auto closure5 = closure4.template operator()&amp;lt;[](int t) { cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl; }&amp;gt;(2);
// auto same_as_closure5 = closure4(2);
closure5(20);
/*
input:2
x: 20
22
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;模板笔记中强调了，非类型模板参数不能用&lt;code&gt;typename&lt;/code&gt;,是一个具体的类型或者是&lt;code&gt;auto&lt;/code&gt;、&lt;code&gt;decltype(auto)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;此处用了&lt;code&gt;std::function&lt;/code&gt;做替代因为带捕获的无法转换为函数指针&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们如何理解这个呢? &lt;code&gt;closure4&lt;/code&gt;返回了一个接受一个&lt;code&gt;int&lt;/code&gt;作为入参的&lt;code&gt;std::function&lt;/code&gt;对象，而&lt;code&gt;closure4&lt;/code&gt;的形参也是&lt;code&gt;int&lt;/code&gt;。所以说&lt;code&gt;closure5&lt;/code&gt;接受的就是&lt;code&gt;closure4&lt;/code&gt;返回的&lt;code&gt;std::functoin&lt;/code&gt;对象。所以&lt;code&gt;closure5&lt;/code&gt;的入参&lt;code&gt;20&lt;/code&gt;是&lt;code&gt;x&lt;/code&gt;。而&lt;code&gt;closure5&lt;/code&gt;后面的实参&lt;code&gt;2&lt;/code&gt;是&lt;code&gt;closure4&lt;/code&gt;的形参。所以&lt;code&gt;input&lt;/code&gt;是&lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;此处我们故意模拟了必须要显示指定&lt;code&gt;operator()&lt;/code&gt;的模板参数的情况。其实&lt;code&gt;closure5&lt;/code&gt;形式上等价于&lt;code&gt;same_as_closure5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在这个&lt;a href=&quot;https://godbolt.org/z/rq67sbPd9&quot;&gt;成员级guard&lt;/a&gt;中用到了此技法。&lt;/p&gt;
&lt;h2&gt;lambda在关键变量中的应用&lt;/h2&gt;
&lt;p&gt;在看吐槽微信扫描二维码出现bug的源码&lt;a href=&quot;https://www.bilibili.com/video/BV1MP41127XB/?share_source=copy_web&amp;amp;vd_source=d59bd34fbf591f4881d60ad6a8742eba&quot;&gt;视频&lt;/a&gt;的时候学到了一个好用的小技巧。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先就是不要重复创建变量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func(int count){
    int count2 = count; //不要重复创建变量
    //.....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做非常不好，有损语义。因为你不好说count和count2有什么联系或者区别，这时候可以使用引用创建一个别名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func(int count){
    int&amp;amp; count2 = count; //不要重复创建变量
    //.....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候有问题了，如果我想要的不是别名，而是一种修改过的值呢？比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;count2 = 2 * count;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种语义下，单纯的别名就不太好用了。因为我们希望无论&lt;code&gt;count&lt;/code&gt;何时何地被修改。&lt;code&gt;count2&lt;/code&gt;永远是&lt;code&gt;2* count&lt;/code&gt;。也就是这个语义永远成立。&lt;/p&gt;
&lt;p&gt;那么我们可以考虑使用&lt;code&gt;lambda&lt;/code&gt;把单纯的变量变成函数，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func(int count){
    auto count2 = [&amp;amp;]{return 2* count;}; //使用lambda
    cout &amp;lt;&amp;lt; count2() &amp;lt;&amp;lt; endl; //注意，这样使用就需要函数调用了 这里是40
    count = 200;
    cout &amp;lt;&amp;lt; count2() &amp;lt;&amp;lt; endl; //这里是400
}
int main(){
    func(20);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就满足了我们想要的语义。&lt;/p&gt;
&lt;p&gt;所以这里有一个技巧，也就是当某一个变量可以被某几个变量计算出来，并且重复计算代价比较低（重复计算的代价比单纯的计算后储存高不了多少的时候），可以把变量换成&lt;code&gt;lambda&lt;/code&gt;函数，让它在调用时计算。这样比较减少重复代码。&lt;/p&gt;
&lt;h2&gt;lambda和一元加法&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://ashvardanian.com/posts/parsing-json-with-allocators-cpp/#parsing-json-in-c-1&quot;&gt;这篇文章&lt;/a&gt;中，我们发现yyjson中有一段关于lambda的奇怪用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using alc_size_t = std::uint16_t;
alc.malloc = +[](void *ctx, size_t size_native) noexcept -&amp;gt; void * { ... };
alc.realloc = +[](void *ctx, void *ptr, size_t old_size_native, size_t size_native) noexcept -&amp;gt; void * { ... };
alc.free = +[](void *ctx, void *ptr) noexcept -&amp;gt; void { ... };

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为啥lambda前面要跟着个加法？&lt;/p&gt;
&lt;p&gt;在cppreference关于&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/operator_arithmetic&quot;&gt;算术运算符&lt;/a&gt;的介绍中，我们看到了内建一元加法的作用：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;于内建一元加运算符，&lt;em&gt;表达式&lt;/em&gt; ﻿必须是算术、无作用域枚举或指针类型的纯右值。如果&lt;em&gt;表达式&lt;/em&gt; ﻿具有整数或无作用域枚举类型，那么对它实施整数提升。结果的类型是（可能为提升后的）&lt;em&gt;表达式&lt;/em&gt; ﻿的类型。&lt;/p&gt;
&lt;p&gt;内建的提升操作的结果是&lt;em&gt;表达式&lt;/em&gt; ﻿的值。如果操作数为提升后的整数或指针类型的纯右值，则内建的一元加运算为无操作。否则，整数提升或左值到右值、数组到指针、函数到指针或用户定义转换会更改操作数的类型或值类别。例如在一元加表达式中 char 会被转换为 int ，而&lt;strong&gt;非泛型且无捕获的 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/lambda&quot;&gt;lambda 表达式&lt;/a&gt;会被转换为函数指针(C++11 起)&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实核心就是使用一元加法触发一下lambda到函数指针的隐式类型转换。多说一句，一元加法的唯一作用就是进行类型转换。&lt;/p&gt;
&lt;h1&gt;函数指针和函数对象的性能差异。（依靠编译器实现）&lt;/h1&gt;
&lt;p&gt;在很多情况下，比如针对容器或算法使用特定的排序要求、我们可能会传递一个函数对象或函数指针。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;函数对象类型可以是：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数指针类型&lt;/li&gt;
&lt;li&gt;重载了 &lt;code&gt;operator()&lt;/code&gt;的 类类型（有时被称为仿函数），这其中包含 lambda 函数&lt;/li&gt;
&lt;li&gt;包含一个可以产生一个函数指针或者函数引用的转换函数的 类类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;出自 C++模板第二版 -- 11.1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样的类型对应的值被称之为函数对象。&lt;/p&gt;
&lt;p&gt;如果一个回调是通过函数指针实施的，那么性能较差。因为函数指针是通过地址调用的函数。该地址在编译时可能不可见。所以编译器较难优化（比如施加inline）&lt;/p&gt;
&lt;p&gt;如果是通过函数对象实施，由于对象是固定的。对象位置在编译时可知。所以编译器可以针对其进行优化。&lt;/p&gt;
&lt;p&gt;当然了，如果我们需要在运行时动态替换某些需要传入的回调，那么还是得使用函数指针。&lt;/p&gt;
&lt;h1&gt;C中的变长原始数组&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;struct test{
    int val;
    char c;
    int arr[0]; //使用0来进行数组占位。分配一个首地址
};

int main(){

    test* T = (test*)malloc(sizeof(test) + sizeof(int) * 40); //给结构用malloc分配大小。大小为结构体长度+你想给数组分配多大。这里分了40个int长度也就是对应int a[40]
    T-&amp;gt;val = 5;
    T-&amp;gt;c = &apos;a&apos;;
    T-&amp;gt;arr[30] = 5;
    cout &amp;lt;&amp;lt; T-&amp;gt;arr[30] &amp;lt;&amp;lt; endl;
    free(T); //因为是用的malloc来分配内存所以使用free释放
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意，零长数组本身不占用内存空间。它只是首地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请记住。原始数组名称本身并不是指针。我们说的是&lt;strong&gt;原始数组的名称是数组首元素的地址&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一定要区分指针和地址的区别。地址可以隐式转换成指针而已。&lt;/li&gt;
&lt;li&gt;也就是说若&lt;code&gt;array&lt;/code&gt;是一个原始数组，则以下关系为真：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;array = &amp;amp;array[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.eet-china.com/mp/a55099.html&lt;/p&gt;
&lt;p&gt;https://www.51cto.com/article/277404.html&lt;/p&gt;
&lt;h1&gt;副作用和序列点 和 求值顺序&lt;/h1&gt;
&lt;h2&gt;副作用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;表达式有两种功能:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每个表达式都产生一个值( value )&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时可能包含副作用( side effect )。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;访问（读或写）&lt;code&gt;volatile&lt;/code&gt; 泛左值所指代的对象，修改（写入）对象，调用库 I/O 函数，或调用任何做出这些操作的函数都是副作用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;副作用的其中一点是指改变了某些变量的值。 如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;20&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这个表达式的值是&lt;code&gt;20&lt;/code&gt;。它没有副作用，因为它没有改变任何变量的值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; x=5&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这个表达式的值是&lt;code&gt;5&lt;/code&gt;。它有一个副作用，因为它改变了变量&lt;code&gt;x&lt;/code&gt;的值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; x=y++&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这个表达示有两个副作用，因为改变了两个变量的值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;x=x++&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这个表达式也有两个副作用，因为变量&lt;code&gt;x&lt;/code&gt;的值发生了两次改变。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;序列点 （C++11前）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;顺序点的意思是在一系列步骤中的一个“结算”的点，语言要求这一时刻的求值和副作用全部完成，才能进入下面的部分。在C/C++中只有以下几种存在顺序点:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;分号（每一个完整表达式结尾）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未重载的逗号运算符的左操作数赋值之后(即&lt;code&gt;,&lt;/code&gt;处)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意逗号运算符有时候需要加括号来避免歧义&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;int a = funca(), funcb(), funcc(); //不行，只调用了funca
int a = (funca(), funcb(), funcc()); //加了括号可以。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未重载的&lt;code&gt;||&lt;/code&gt;运算符的左操作数赋值之后(即&lt;code&gt;||&lt;/code&gt;处)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未重载的&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;运算符的左操作数赋值之后(即&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;处)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三元运算符&lt;code&gt;? :&lt;/code&gt;的左操作数赋值之后(即&lt;code&gt;?&lt;/code&gt;处)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在函数所有参数赋值之后但在函数第一条语句执行之前。（调用函数时，所有的函数实参的求值后有个序列点。发生于函数体内任何表达式或语句的执行前）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前。（对函数的返回值进行复制后，并在函数外任何表达式执行前有一个序列点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个基类和成员初始化之后&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在每一个完整的变量声明处有一个顺序点，例如&lt;code&gt;int i, j;&lt;/code&gt;中逗号和分号处分别有一个顺序点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for&lt;/code&gt;循环控制条件中的两个分号处各有一个顺序点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;对于任意一个顺序点，它之前的所有副作用都已经完成，它之后的所有副作用都尚未发生。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在两个顺序点之间，子表达式求值和副作用的顺序是不同步的。如果代码的结果与求值和副作用发生顺序相关，称这样的代码有不确定的行为(unspecified behavior).而且,假如期间对一个内建类型执行一次以上的写操作,则是未定义行为. 即：标准规定，在两个序列点之间，一个对象所保存的值最多只能被修改一次。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是说：**任意两个顺序点之间的副作用的发生顺序都是未定义的.**如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a() + b() + c();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个表达式有一个序列点，也就是结尾的分号处。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;operator+&lt;/code&gt; 的从左到右结合性被分析成 &lt;code&gt;(a() + b()) + c()&lt;/code&gt;，但在运行时可以首先、最后或者在 &lt;code&gt;a()&lt;/code&gt; 和 &lt;code&gt;b()&lt;/code&gt; 之间对&lt;code&gt;c()&lt;/code&gt;求值：&lt;/p&gt;
&lt;h2&gt;按顺序早于（C++11/17起)&lt;/h2&gt;
&lt;p&gt;可以直接看cppreference&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/eval_order&quot;&gt;这里&lt;/a&gt; 还有&lt;a href=&quot;https://www.cppstories.com/2021/evaluation-order-cpp17/&quot;&gt;这篇文章&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;包括这个&lt;a href=&quot;https://stackoverflow.com/questions/38501587/what-are-the-evaluation-order-guarantees-introduced-by-c17&quot;&gt;回答&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们可以精简出一些规则：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The following expressions are evaluated in the order a, then b:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;a.b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a-&amp;gt;b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a-&amp;gt;*b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;a(b1, b2, b3) // b1, b2, b3 - in any order&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b @= a // &apos;@&apos; means any operator&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a[b]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a &amp;lt;&amp;lt; b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a &amp;gt;&amp;gt; b&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意，&lt;code&gt;a(b1, b2, b3)&lt;/code&gt; 这种函数调用表达式里面的顺序依旧是未指定的。比如&lt;code&gt;foo(s(), g(), h())&lt;/code&gt; 里面 &lt;code&gt;s()&lt;/code&gt;, &lt;code&gt;g()&lt;/code&gt;，和 &lt;code&gt;h()&lt;/code&gt;的调用顺序是未指定的。&lt;/p&gt;
&lt;h1&gt;可变参数宏&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;void test(int a, ...){ //第一个参数表明后面一共有几个参数。
    va_list args; //定义一个可变参数列表
    va_start(args, a); //初始化可变参数列表
    for(; a &amp;gt; 0; a--){
        cout &amp;lt;&amp;lt; va_arg(args, int) &amp;lt;&amp;lt; endl; //使用va_arg访问下一个可变参数函数参数。第二个参数是传入参数类型
    }
    va_end(args); //释放args
}

int main(){
    test(4,10,9,8,7);
    return 0;
    /*
    输出:
    10
    9
    8
    7
    */
}
/*
注意C语言里的printf就是这个效果。
C自带的printf函数是根据第一个参数format的占位符解析出后面的变参个数和类型，
通过va_arg迭代去获取变参再填充到占位符上输出。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可变参数宏可以让我们给一个函数增添变参特性。在C++中可以使用&lt;code&gt;initializer list&lt;/code&gt;完成这件事&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_35280514/article/details/51637920&lt;/p&gt;
&lt;p&gt;https://xie.infoq.cn/article/4e927ec51b5a364f51e2ac944&lt;/p&gt;
&lt;h1&gt;宏的三个特殊符号 &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;##&lt;/code&gt; , &lt;code&gt;#@&lt;/code&gt;&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#define ToString(x) #x&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;意思是给&lt;code&gt;x&lt;/code&gt;加双引号。比如&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char str = ToString(123132);&lt;/code&gt;就成了&lt;code&gt;str=&quot;123132&quot;;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#define Conn(x,y) x##y&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;意思是&lt;code&gt;x&lt;/code&gt;连接&lt;code&gt;y&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int n = Conn(123,456);&lt;/code&gt; 结果就是&lt;code&gt;n=123456;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char* str = Conn(&quot;asdf&quot;, &quot;adf&quot;); &lt;/code&gt;结果就是 &lt;code&gt;str = &quot;asdfadf&quot;;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当可变参数宏`&lt;strong&gt;VA_ARGS&lt;/strong&gt;``为空的时候, 这样使用可以让编译器去除前面的多余逗号
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;##__VA_ARGS__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;可以查看项目相关的笔记. 里面写过&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#define ToChar(x) #@x&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;意思是给&lt;code&gt;x&lt;/code&gt;加上单引号，结果返回是一个&lt;code&gt;const char&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char a = ToChar(1);&lt;/code&gt;结果就是&lt;code&gt;a=&apos;1&apos;;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C++杂记 - 4（一）：存储期、可重入函数、特殊成员函数、constexpr 与 CRT</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B04/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B04/</guid><pubDate>Sun, 20 Nov 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;存储类说明符&lt;/h1&gt;
&lt;p&gt;这是一个被我忽视了的细节。在官方文档中提到了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;auto&lt;/code&gt;或 (C++11 前)无说明符 - &lt;strong&gt;自动&lt;/strong&gt;存储期。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这类对象的存储在外围代码块开始时分配，并在结束时解分配。未声明为 &lt;code&gt;static&lt;/code&gt;、&lt;code&gt;extern&lt;/code&gt; 或 &lt;code&gt;thread_local&lt;/code&gt; 的所有局部对象均拥有此存储期。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;register&lt;/code&gt; - 自动存储期，另提示编译器将此对象置于处理器的寄存器。(弃用) (C++17 前)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; - 静态或线程存储期和内部链接。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这类对象的存储在程序开始时分配，并在程序结束时解分配。这类对象只存在一个实例。所有在命名空间（包含全局命名空间）作用域声明的对象，加上声明带有 &lt;code&gt;static&lt;/code&gt; 或 &lt;code&gt;extern&lt;/code&gt; 的对象均拥有此存储期。有关拥有此存储期的对象的初始化的细节，见&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/initialization#.E9.9D.9E.E5.B1.80.E9.83.A8.E5.8F.98.E9.87.8F&quot;&gt;非局部变量&lt;/a&gt;与&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/storage_duration#.E9.9D.99.E6.80.81.E5.B1.80.E9.83.A8.E5.8F.98.E9.87.8F&quot;&gt;静态局部变量&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;extern &lt;/code&gt;- 静态或线程存储期和外部链接。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;thread_local &lt;/code&gt;- 线程存储期。 (C++11 起)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mutable&lt;/code&gt; - 不影响存储期或链接。解释见 const/volatile。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt;, &lt;code&gt;extern&lt;/code&gt; 和 &lt;code&gt;thread_local&lt;/code&gt; 不必多说，这里主要关注第一个自动储存期。&lt;/p&gt;
&lt;h2&gt;什么是自动储存期？&lt;/h2&gt;
&lt;p&gt;我们知道，变量一般存在下面地方。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;全局变量和静态变量：储存在静态存储区（比如BSS段和数据段）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常量：依赖于实现和上下文。一般来说，全局常量存储在静态储存区的数据段或代码段。局部常量储存在栈段。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;局部变量：栈区。储存在动态存储区&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt;的变量：堆区。储存在动态存储区&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是注意，当有如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int* ptr = new int(5);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ptr&lt;/code&gt;本身没有加储存类说明符，说明是&lt;strong&gt;自动存储期&lt;/strong&gt;。我们查看上下文发现是在局部的变量。所以指针&lt;code&gt;ptr&lt;/code&gt;自己在栈区。而&lt;code&gt;new&lt;/code&gt;出来的东西在堆区。也就是栈上的指针指向了堆区的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;自动储存期指的是变量分配在哪是依靠上下文决定的。它并不一定在栈上。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    Object obj; //什么意思？
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;要回答这个问题，我们首先要理解这个语句是什么意思。这行语句的含义是，使对象&lt;code&gt;obj&lt;/code&gt;具有自动储存期的性质。所谓自动储存期，意思是这个对象的存储位置取决于其声明所在的上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时我们看到，这个语句出现在函数内部并且没有其他修饰符，那么它就在栈上创建对象。&lt;/p&gt;
&lt;p&gt;**如果这个语句不是在函数内部，而是作为一个类的成员变量，则取决于这个类的对象是如何分配的。**考虑下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myClass{
    public:
    	Object obj; //它在哪？
};
int main(){
    myClass* ptr = new myClass;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;指针&lt;code&gt;ptr&lt;/code&gt;所指向的对象在堆上分配空间。但是因为&lt;code&gt;Object obj;&lt;/code&gt;语句的含义是“变量具有自动存储期”。我们查看其上下文，发现包含&lt;code&gt;ptr-&amp;gt;obj&lt;/code&gt;的变量处于堆上。所以，&lt;code&gt;ptr-&amp;gt;obj&lt;/code&gt;也是在堆上创建的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们继续看下面的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    Object *ptr;
	ptr = new Object;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object *ptr;&lt;/code&gt;代表，指针&lt;code&gt;ptr&lt;/code&gt;是自动存储的。查看上下文发现是在函数内部，所以&lt;code&gt;ptr&lt;/code&gt;是栈上创建的。而下面一行语句则指出，这个指针所指向的对象是在堆上面分配的。如果这两行语句出现在一个函数内部，意味着当函数结束时，&lt;code&gt;ptr&lt;/code&gt;会被销毁，但是它指向的对象不会。因此，如果不对指向的资源进行释放，会造成内存泄漏。因为那时堆上的这个地址的资源会变为不可达。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继续看下面的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    Object obj;
    Object* ptr = &amp;amp;obj;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里我们发现，&lt;code&gt;obj&lt;/code&gt;是自动储存期并且在栈上。&lt;code&gt;ptr&lt;/code&gt;也是自动储存期，也在栈上。所以这个指针指向的对象也在栈上。所以不对其处理也不会导致泄漏问题。因为都是栈上的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;可重入函数 和 不可重入函数&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在实时系统的设计中，经常会出现多个任务调用同一个函数的情况。&lt;strong&gt;如果有一个函数不幸被设计成为这样：不同任务调用这个函数时可能修改其他任务调用这个函数的数据，从而导致不可预料的后果。这样的函数是不安全的函数，也叫不可重入函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相反，肯定有一个安全的函数，这个安全的函数又叫可重入函数。那么什么是可重入函数呢？所谓可重入是指一个可以被多个任务调用的过程，任务在调用时不必担心数据是否会出错。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一个可重入的函数简单来说就是可以被中断的函数，也就是说，可以在这个函数执行的任何时刻中断它，转入OS调度下去执行另外一段代码，而返回控制时不会出现什么错误；而不可重入的函数由于使用了一些系统资源，比如全局变量区，中断向量表等，所以它如果被中断的话，可能会出现问题，这类函数是不能运行在多任务环境下的。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;也可这样理解，重入即表示重复进入，首先它意味着这个函数可以被中断，其次意味着它除了使用自己栈上的变量以外不依赖于任何环境（包括 static），这样的函数就是purecode（纯代码）可重入，可以允许有该函数的多个副本在运行，由于它们使用的是分离的栈，所以不会互相干扰。如果确实需要访问全局变量（包括 static），一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要，但是一般要为访问全局变量付出一些性能代价。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写可重入函数时，若使用全局变量，则应通过关中断、信号量（即P、V操作）等手段对其加以保护。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;p&gt;假设&lt;code&gt; Exam&lt;/code&gt; 是 &lt;code&gt;int&lt;/code&gt; 型&lt;strong&gt;全局变量&lt;/strong&gt;，函数 &lt;code&gt;Squre_Exam &lt;/code&gt;返回 &lt;code&gt;Exam&lt;/code&gt; 平方值。那么如下函数&lt;strong&gt;不具有可重入性&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int Exam = 0;
unsigned int example( int para ) 
{ 
    unsigned int temp;
    Exam = para; // （**）
    temp = Square_Exam( );
    return temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此函数若被多个进程调用的话，其结果可能是未知的，因为当（**）语句刚执行完后，另外一个使用本函数的进程可能正好被激活，那么当新激活的进程执行到此函数时，将使 &lt;code&gt;Exam&lt;/code&gt; 赋与另一个不同的 &lt;code&gt;para&lt;/code&gt; 值，所以当控制重新回到 &lt;code&gt;temp = Square_Exam( )&lt;/code&gt; 后，计算出的&lt;code&gt;temp&lt;/code&gt;很可能不是预想中的结果。此函数应加锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int Exam = 0;
unsigned int example( int para ) 
{
    unsigned int temp;
    [申请信号量操作] //(1)  加锁
    Exam = para;
    temp = Square_Exam( );
    [释放信号量操作] //     解锁 
    return temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;可重入函数特点：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;没有静态数据结构&lt;/li&gt;
&lt;li&gt;不返回指向静态数据的指针&lt;/li&gt;
&lt;li&gt;所有函数数据由函数的调用者提供&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;auto&lt;/code&gt;变量，或通过全局变量的拷贝来保护全局变量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;若必须访问全局变量，则利用互斥信号保护&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不调用不可重入函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在unix里面通常都有加上_r后缀的同名可重入函数版本。如果实在没有，不妨在可预见的发生错误的地方尝试加上保护锁同步机制等等&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;不可重入函数特点：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;函数中使用了静态变量，无论是全局静态变量还是局部静态变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数返回静态变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数中调用了不可重入函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如函数体内调用了&lt;code&gt;malloc()&lt;/code&gt;或者&lt;code&gt;free()&lt;/code&gt;函数 或者 &lt;code&gt;printf()&lt;/code&gt;函数
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;printf&lt;/code&gt; --------引用全局变量&lt;code&gt;stdout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malloc &lt;/code&gt;--------全局内存分配表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;free&lt;/code&gt; --------全局内存分配表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数体内调用了其他标准I/O函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数是singleton中的成员函数，而且使用了不属于线程独立存储的成员变量&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;满足下列条件的函数多数是不可重入（不安全）的：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;函数体内使用了静态的数据结构；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数体内调用了&lt;code&gt;malloc()&lt;/code&gt; 或者&lt;code&gt;free()&lt;/code&gt;函数；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数体内调用了标准 I/O 函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;保证函数的可重入性的方法：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在写函数时候尽量使用局部变量（例如寄存器、堆栈中的变量）；&lt;/li&gt;
&lt;li&gt;对于要使用的全局变量要加以保护（如采取关中断、信号量等互斥方法），这样构成的函数就一定是一个可重入的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;可重入规则改写不可重入函数：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;把一个不可重入函数变成可重入的唯一方法是用可重入规则来重写它。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可重入规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要使用全局变量。如果必须使用记得用互斥信号量保护&lt;/strong&gt;，因为别的代码很可能修改这些变量值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在和硬件发生交互的时候，切记执行类似&lt;code&gt;disinterrupt()&lt;/code&gt;之类的操作，就是关闭硬件中断。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成交互记得打开中断，在有些系列上，这叫做“进入/退出核心”或者用&lt;code&gt;OS_ENTER_KERNAL/ OS_EXIT_KERNAL&lt;/code&gt;来描述。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不能调用任何不可重入的函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;谨慎使用堆栈。最好先在使用前先&lt;code&gt;OS_ENTER_KERNAL&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，必须保证中断是安全的&lt;/p&gt;
&lt;h1&gt;委托构造函数&lt;/h1&gt;
&lt;p&gt;委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数，可以在变量初始化时简化操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    public:
        A(){}
        A(int a):a_(a){}

        A(int a, int b):a_(a), b_(b) {}  // 好麻烦

        A(int a, int b, int c): a_(a), b_(b), c_(c) {}  // 好麻烦

        int a_;
        int b_;
        int c_;
};

//下面是使用委托构造函数
class mod_A {
    public:
        mod_A(){}
        mod_A(int a): a_(a) {
            cout &amp;lt;&amp;lt;&quot;1param&quot; &amp;lt;&amp;lt; endl;
        }

        mod_A(int a, int b) : mod_A(a) {  //委托构造函数，注意使用后对其他成员不可再使用构造函数初始化列表初始化其他成员。
            cout &amp;lt;&amp;lt;&quot;2param&quot; &amp;lt;&amp;lt; endl;
            b_ = b; 
        }
        mod_A(int a, int b, int c) : mod_A(a, b) { 
            cout &amp;lt;&amp;lt;&quot;3param&quot; &amp;lt;&amp;lt; endl;
            c_ = c; 
        }

        int a_;
        int b_;
        int c_;
};
int main(){
    mod_A obj1(1);
    mod_A obj2(1,2);
    mod_A obj3(1,2,3);
    return 0;
}
/*
obj1:
1param

obj2:
1param
2param

obj3:
1param
2param
3param
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意。每一个构造函数函数体内的打印是必须等待构造函数初始化列表执行完毕后才能调用的。这个比较好理解。比如&lt;code&gt;obj3&lt;/code&gt;为例。首先调用了第二个委派，然后调用第一个委派。第一个委派输出&lt;code&gt;1param&lt;/code&gt;，然后执行完毕返回至第二个委派输出&lt;code&gt;2param&lt;/code&gt;，然后执行完毕返回至第三个构造函数输出&lt;code&gt;3param&lt;/code&gt;结束。&lt;/p&gt;
&lt;p&gt;也可以逆序写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class mod_A {
    public:
        mod_A(){}
        mod_A(int a): mod_A(a,0){ //委派到下面的
            cout &amp;lt;&amp;lt;&quot;1param&quot; &amp;lt;&amp;lt; endl;
        }

        mod_A(int a, int b):mod_A(a, b, 0){ //再委派到下面的
            cout &amp;lt;&amp;lt;&quot;2param&quot; &amp;lt;&amp;lt; endl;
        }
        mod_A(int a, int b, int c){ 
            cout &amp;lt;&amp;lt;&quot;3param&quot; &amp;lt;&amp;lt; endl;
            a_ = a;
            b_ = b;
            c_ = c; 
        }

        int a_;
        int b_;
        int c_;
};
int main(){
    mod_A obj1(1);
    mod_A obj2(1,2);
    mod_A obj3(1,2,3);
}
/*
obj1:
3param
2param
1param

obj2:
3param
2param

obj3:
3param
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现明明是正序调用但是为啥是逆序输出呢？也是因为在每一个构造函数函数体内的打印是必须等待构造函数初始化列表执行完毕后才能调用的。所以比如&lt;code&gt;obj1&lt;/code&gt;为例，调用了第二个委派，然后调用第三个委派，输出&lt;code&gt;3param&lt;/code&gt;，然后执行完毕返回至第二个委派输出&lt;code&gt;2param&lt;/code&gt;，然后执行完毕返回至第一个委派输出&lt;code&gt;param1&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;注意事项：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意委托构造函数不能具有其他成员初始化表达式，成员初始化列表只能包含一个其它构造函数，不能再包含其它成员变量的初始化，且参数列表必须与构造函数匹配。也就是对其他成员不可使用构造函数初始化列表&lt;/strong&gt;。我们不能在初始化列表中既初始化成员，又委托其他构造函数完成构造。
&lt;ul&gt;
&lt;li&gt;所以如果委托构造函数要给变量赋初值，初始化代码必须放在函数体中。如上面代码所示。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mod_A(){}
mod_A(int a): a_(a) {}
mod_A(int a, int b) : mod_A(a), b_(b) {} //错误。委托构造函数对其他成员不可使用构造函数初始化列表。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在构造函数较多的时候，我们可能拥有多个委托构造函数，而一些目标构造函数很可能也是委托构造函数，这样依赖，我们就可能在委托构造函数中形成链状的委托构造关系，形成委托坏（Delegation Cycle）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果在委托构造函数中使用try，可以捕获目标构造函数中抛出的异常。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;继承构造函数&lt;/h1&gt;
&lt;p&gt;继承构造函数可以让派生类直接使用基类的构造函数，如果有一个派生类，我们希望派生类采用和基类一样的构造方式，可以直接使用基类的构造函数，而不是再重新写一遍构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    public:
        A(){}
        A(int a): a_(a) {}

        A(int a, int b) : A(a) {  //委托构造
            b_ = b; 
        }
        A(int a, int b, int c) : A(a, b) {  //委托构造
            c_ = c; 
        }

        int a_;
        int b_;
        int c_;
};

class B:public A{
    public:
        B() {}
        B(int a) : A(a) {} // 好麻烦
        B(int a, int b) : A(a, b) {} // 好麻烦
        B(int a, int b, int c) : A(a, b, c) {} // 好麻烦
    	B(int a, int b, int c, int d) : A(a, b, c), d_(d) {} // 注意这不是委托构造，所以自己的成员可以放入初始化列表。
        int d_;
};


//下面使用继承构造
class mod_B:public A{
    public:
        using A::A;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 &lt;em&gt;using 声明&lt;/em&gt;指代了正在定义的类的某个直接基类的构造函数（例如 using Base::Base;），那么在初始化派生类时，该基类的所有构造函数（忽略成员访问）均对重载决议可见。&lt;/p&gt;
&lt;p&gt;如果重载决议选择了继承的构造函数，那么如果它被用于构造相应基类的对象时可访问，它也是可访问的：引入它的 using 声明的可访问性被忽略。&lt;/p&gt;
&lt;p&gt;如果在初始化这种派生类对象时重载决议选择了继承的构造函数之一，那么用这个继承的构造函数对从之继承该构造函数的 &lt;code&gt;Base&lt;/code&gt; 子对象进行初始化，而 &lt;code&gt;Derived&lt;/code&gt; 的所有其他基类和成员，都如同以预置的默认构造函数一样进行初始化（如果提供默认成员初始化器就会使用它，否则进行默认初始化）。整个初始化被视作单个函数调用：继承的构造函数的各形参的初始化，按顺序早于派生类对象的任何基类或成员的初始化。&lt;/p&gt;
&lt;p&gt;来自&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/using_declaration&quot;&gt;cppreference&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;注意事项：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;继承构造函数无法初始化派生类数据成员，继承构造函数的功能是初始化基类，对于派生类数据成员的初始化则无能为力。解决的办法主要有两个：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以通过 =、{} 对非静态成员快速地就地初始化，以减少多个构造函数重复初始化变量的工作，注意初始化列表会覆盖就地初始化操作（这句话没有别的意思。就是如果一个元素既被就地初始化（如&lt;code&gt;int val = 10&lt;/code&gt;），又被置入初始化列表，则最后的元素的值会是初始化列表的值。非常直观好理解）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class mod_B:public A{
    public:
        using A::A;
        int _d = 10;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;新增派生类构造函数，使用构造函数初始化列表初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class mod_B:public A{
    public:
        using A::A;
        mod_B(int a, int b, int c, int d): A(a, b, c), d_(d){} // 注意这不是委托构造，所以自己的成员可以放入初始化列表。
        int d_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造函数拥有默认值会产生多个构造函数版本，&lt;strong&gt;且继承构造函数无法继承基类构造函数的默认参数&lt;/strong&gt;，所以我们在使用有默认参数构造函数的基类时就必须要小心。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A
{
public:
    A(int a = 3, double b = 4):m_a(a), m_b(b){}
    void display()
    {
        cout&amp;lt;&amp;lt;m_a&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;m_b&amp;lt;&amp;lt;endl;
    }

private:
    int m_a;
    double m_b;
};

class B:public A
{
    public:
    using A::A;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么A中的构造函数会有下面几个版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A() //默认无参构造
A(int) //只输入一个值，第二个值采用默认值
A(int,double) //输入两个值，也就是忽视默认值
A(const A&amp;amp;) //拷贝构造。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么B中对应的继承构造函数将会包含如下几个版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//同上
B()
B(int)
B(int,double)
B(const B&amp;amp;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意没有&lt;code&gt;A(double)&lt;/code&gt; 或 &lt;code&gt;B(double)&lt;/code&gt; 因为不可以第一个有默认值而第二个没有默认值。也就是从第一个有默认值后面的参数都必须有默认值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多继承的情况下，继承构造函数会出现“冲突”的情况，因为多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名与参数相同，即函数签名。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;=default 类的默认函数&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;只能用于&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/member_functions#.E7.89.B9.E6.AE.8A.E6.88.90.E5.91.98.E5.87.BD.E6.95.B0&quot;&gt;特殊成员函数&lt;/a&gt;和[&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/default_comparisons&quot;&gt;比较运算符函数&lt;/a&gt; (C++20 起)]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们有如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj() = default;
        myobj(int a, int b){
            cout &amp;lt;&amp;lt;&quot;two&quot; &amp;lt;&amp;lt; endl;
        }
};

int main(){
    myobj(1,2);
    myobj s; //注意这里
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果此时我们只有第二个带参数的构造函数。如果我们执行一个新建一个对象但不提供参数，则编译器无法找到合适的构造函数。会报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只要我们有定义的构造函数，编译器无论如何都不会额外生成构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以此时我们想执行&lt;code&gt;myobj s&lt;/code&gt;应该怎么办？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要么手写一个空构造函数 如&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myobj(){}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;要么使用 &lt;code&gt;=default&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myobj() = default;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果使用default指示的办法，可以产出比用户定义的无参构造函数性能更优的代码（毕竟是编译器干活）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;还有一个作用可以让使用者一眼就看出这是一个合成版本的构造函数（相当于知道类的作者没干其他事情）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在用户没有提供构造函数，析构函数，拷贝构造函数，拷贝赋值函数，移动构造函数和移动赋值函数这六大类特殊函数的时候，编译器会提供默认版本。如果某一个函数被实现，则该函数的默认版本将不复存在。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;=default&lt;/code&gt;是函数定义&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;=delete 弃置函数&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;果使用特殊语法 &lt;code&gt;= delete ;&lt;/code&gt;取代函数体，那么该函数被定义为&lt;em&gt;弃置的（deleted）&lt;/em&gt;。&lt;strong&gt;任何弃置函数的使用都是非良构的&lt;/strong&gt;（程序无法编译）。这包含调用，包括显式（以函数调用运算符）及隐式（对弃置的重载运算符、特殊成员函数、分配函数等的调用），构成指向弃置函数的指针或成员指针，&lt;strong&gt;甚至是在不&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/expressions#.E6.BD.9C.E5.9C.A8.E6.B1.82.E5.80.BC.E8.A1.A8.E8.BE.BE.E5.BC.8F&quot;&gt;潜在求值&lt;/a&gt;的表达式中使用弃置函数&lt;/strong&gt;。但是可以隐式 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/definition#ODR_.E4.BD.BF.E7.94.A8&quot;&gt;ODR 使用&lt;/a&gt;刚好被弃置的非纯虚成员函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果函数被重载，那么首先进行&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/overload_resolution&quot;&gt;重载决议&lt;/a&gt;，且只有在选择了弃置函数时程序才非良构&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;函数的弃置定义必须是翻译单元中的&lt;strong&gt;首条&lt;/strong&gt;声明：已经声明过的函数不能声明为弃置的：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;和&lt;code&gt;=default&lt;/code&gt;不同，&lt;code&gt;=delete&lt;/code&gt;可以用于任何函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**先说结论：&lt;code&gt;=delete&lt;/code&gt;修饰的弃置函数会参与重载决议。因为弃置函数修饰的是定义，也就是指示定义被删除。而符号需要被保留，因为所有查找和重载解析都发生在删除的定义被记录之前。同时，另一个原因是符合语义规则。**比如下面这个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct onlydouble {
    onlydouble(int) = delete;
    onlydouble(double){
        cout&amp;lt;&amp;lt;&quot;called double&quot; &amp;lt;&amp;lt; endl;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设delete的含义是完全删除函数，则上面等同于下面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct onlydouble {
    onlydouble(double){
        cout&amp;lt;&amp;lt;&quot;called double&quot; &amp;lt;&amp;lt; endl;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后，我们这样的调用是合法的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 200;
onlydouble obj(a);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是从语义角度来说，我们不希望这样做。我们希望不接受&lt;code&gt;int&lt;/code&gt;为入参。&lt;/p&gt;
&lt;p&gt;所以说&lt;code&gt;=delete&lt;/code&gt;的含义只是删除了定义，并不删除声明。所以说我们按照第一个例子调用的话是不合法的。因为找到了完全匹配的函数，此时重载决议已经结束。但是函数被声明为&lt;code&gt;=delete&lt;/code&gt;，等于发现了函数是弃置的，非良构。这时候编译器就会发出提示，报错。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;归根结底：&lt;code&gt;=delete&lt;/code&gt; 的意思是“禁止使用”而不是“这个不存在”&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;可以把&lt;code&gt;=delete&lt;/code&gt;理解为&lt;code&gt;=disable&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;就像我们调用了一个声明拷贝构造为&lt;code&gt;delete&lt;/code&gt;的类的拷贝构造函数时，报错提示的是调用了&lt;code&gt;delete&lt;/code&gt;的函数，而非未定义的标识符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;=delete&lt;/code&gt;是函数定义。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/14085620/why-do-c11-deleted-functions-participate-in-overload-resolution&lt;/p&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/function#.E7.94.B1.E7.94.A8.E6.88.B7.E6.8F.90.E4.BE.9B.E7.9A.84.E5.87.BD.E6.95.B0&lt;/p&gt;
&lt;p&gt;https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2346.htm#delete&lt;/p&gt;
&lt;h1&gt;用户提供的函数&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;如果一个函数&lt;strong&gt;由用户声明且没有在它的首个声明被显式预置或显式弃置，那么它由用户提供&lt;/strong&gt;。由用户提供的显式预置的函数（即在它的首个声明后被显式预置）在它被显式预置的地方定义；如果该函数被隐式定义为弃置的，那么程序非良构。需要为不断变化的代码库提供稳定的二进制接口的情况下，&lt;strong&gt;在函数的首个声明后再定义为预置可以保证执行效率&lt;/strong&gt;，也能提供简明的定义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;注意，是&lt;strong&gt;首个声明被显式预置或显式弃置的时候，不是由用户提供的。三个要点：首个，显式预置或弃置，这时候不是用户提供的。&lt;/strong&gt;，所以说 &lt;code&gt;=default&lt;/code&gt;和&lt;code&gt;=delete&lt;/code&gt;修饰的函数&lt;strong&gt;不一定&lt;/strong&gt;是不是由用户提供的。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// trivial 的所有特殊成员函数都分别在它们的首个声明处被显式预置，因此它们都不由用户提供
struct trivial
{
    trivial() = default;
    trivial(const trivial&amp;amp;) = default;
    trivial(trivial&amp;amp;&amp;amp;) = default;
    trivial&amp;amp; operator=(const trivial&amp;amp;) = default;
    trivial&amp;amp; operator=(trivial&amp;amp;&amp;amp;) = default;
    ~trivial() = default;
};
 
struct nontrivial
{
    nontrivial(); // 首个声明，此处并没有在它的首个声明被显式预置或显式弃置。所以它是用户提供的。
};
 
// 没有在首个声明处被显式预置，
// 因此该函数由用户提供并在此定义
nontrivial::nontrivial() = default;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;用户声明的函数&lt;/h1&gt;
&lt;p&gt;只要写了声明就是用户声明的函数&lt;/p&gt;
&lt;h1&gt;用户定义的函数&lt;/h1&gt;
&lt;p&gt;就是函数不仅有声明还有定义。但是没找到这个词汇的专业解释。&lt;/p&gt;
&lt;h1&gt;编译期和运行期的思考&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类是编译期的概念，也是“访问权限”、“成员数据”、“成员函数”这几个概念的“作用域”。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;意思就是非法访问的报错是编译期就会出现问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;而对象的作用域是运行期。它包括类的实例、引用和指针。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以我们说类对象的虚函数表指针是运行期确定的。因为编译期间不会给对象分配内存（new是运行期的东西，静态的数据是编译期）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译期是指把你的源程序交给编译器编译的过程，最终目的是得到obj文件，链接后生成可执行文件（预处理、编译、汇编和连接）。运行期指的是你将可执行文件交给操作系统（输入文件名，回车）执行、直到程序执行结束。执行的目的是为了实现程序的功能 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;编译时不分配内存&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译期分配的内存，就比如静态，全局变量，全局常量。这是在编译的时候确定的。编译时分配内存”是指“编译时赋初值”，它只是形成一个文本，检查无错误，但是它并没有分配内存空间，而是类似一个占位符的检查，比如看看你在的位置对吗，这个位置该你占位嘛。记住，此时只是根据声明时的类型进行占位，到以后程序执行时分配内存才会正确。所以声明是给编译器看的&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;编译期内存错误，就是比如某个数据段DATA段或者CODE段等等，超过跑这个程序的目标机的存储器的限制。&lt;/li&gt;
&lt;li&gt;比如一个全局数组&lt;code&gt;int array[100];&lt;/code&gt;由于他被分配在&lt;code&gt;.data/.bss&lt;/code&gt;部分，所以&lt;strong&gt;编译器在编译期间决定将要把数组分配在该静态内存区域的某个地址&lt;/strong&gt;。（当然，内存地址是虚拟地址。该程序假定它拥有自己的整个内存空间（例如从 0x00000000 到 0xFFFFFFFF）。这就是为什么编译器可以做出诸如“好的，数组将位于地址 0x00A33211”之类的假设。在运行时，地址由 MMU 和操作系统转换为实际/硬件地址。）随后，直到程序被装入内存运行时，这里的内存才会被真正分配。所谓的静态分配只是预先计算好的意思。但依旧需要运行时才能将其分配。至于堆对象，更是需要等到运行时调用CRT帮助我们从OS中获取内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时必分配内存&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行期分配内存，一开始程序会初始化一些全局对象（给上面的占位符分配内存），然后找到入口函数。之后用&lt;code&gt;new&lt;/code&gt; 啊&lt;code&gt;malloc()&lt;/code&gt;之类的函数，在堆上分配内存。&lt;/li&gt;
&lt;li&gt;运行期内存错误，就是运行的时候发生的，比如申请不到内存，内存越界访问，等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关于数组不能使用非常量做为大小来声明。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int a = 10; 
    const int b = 10;
    int arr[a]; //不行
    int arr[b]; //可以
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为啥？**因为尽管我们提到过一些可以在编译时分配内存的对象。但是他们并不能在编译时计算。可以在编译时计算的只有常量表达式。**所以普通全局变量在这里也不能用。&lt;/li&gt;
&lt;li&gt;常量表达式和&lt;code&gt;constexpr&lt;/code&gt;较为复杂。比如字面值是常量表达式。用常量表达式初始化的 &lt;code&gt;const&lt;/code&gt; 对象也是常量表达式。或使用&lt;code&gt;constexpr&lt;/code&gt;指明可以编译时计算。&lt;/li&gt;
&lt;li&gt;同时，如&lt;code&gt;const int b = 10;&lt;/code&gt; 他是局部&lt;code&gt;const&lt;/code&gt;所以存储在栈段。但由于他是常量表达式，所以并不为其在栈上分配内存。所有下面用到&lt;code&gt;b&lt;/code&gt;的地方都会被直接替换为&lt;code&gt;10&lt;/code&gt;[详细说就是放入符号表。然后替换值]。直到有地方对&lt;code&gt;b&lt;/code&gt;取地址了，才会在栈上为其分配一块内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.yisu.com/zixun/609652.html&lt;/p&gt;
&lt;h1&gt;constexpr和const区别&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;constexpr不强制编译器进行constexpr优化。或者我们说，不合法的constexpr无法通过编译。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;相同点：&lt;code&gt;const&lt;/code&gt;和&lt;code&gt;consexpr&lt;/code&gt;都是用来定义常量的&lt;/li&gt;
&lt;li&gt;不同点：&lt;code&gt;const&lt;/code&gt;声明的常量，初始值引用的对象不一定是一个常量；&lt;code&gt;constexpr&lt;/code&gt;声明的常量，初始值一定是常量表达式。
&lt;ul&gt;
&lt;li&gt;也就是说：&lt;strong&gt;&lt;code&gt;const&lt;/code&gt;并未区分出编译期常量和运行期常量。但是&lt;code&gt;constexpr&lt;/code&gt;限定了编译期常量。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt;修饰的对象虽然必须声明时初始化，但是如果这个值是编译时可以确定，则在编译时初始化。如果是运行时才能确定，则在运行时初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;也就是说，为了解决 &lt;code&gt;const&lt;/code&gt; 关键字的双重语义问题，保留了 &lt;code&gt;const&lt;/code&gt; 表示“只读”的语义，而将“常量”的语义划分给了新添加的 &lt;code&gt;constexpr&lt;/code&gt; 关键字。即凡是表达“只读”语义的场景都使用&lt;code&gt; const&lt;/code&gt;，表达“常量”语义的场景都使用&lt;code&gt; constexpr&lt;/code&gt;。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，从以上信息我们可以再详细说说二者的区别&lt;/p&gt;
&lt;p&gt;*&lt;em&gt;声明对象时使用 &lt;code&gt;constexpr&lt;/code&gt; 说明符则同时蕴含 &lt;code&gt;const&lt;/code&gt;。声明函数或*静态成员变量 (C++17 起)&lt;em&gt;时使用 &lt;code&gt;constexpr&lt;/code&gt; 说明符则同时蕴含 &lt;code&gt;inline&lt;/code&gt;。如果一个函数或函数模板的某个声明拥有 &lt;code&gt;constexpr&lt;/code&gt; 说明符，那么它的所有声明都必须含有该说明符。&lt;/em&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;格外注意！！！在C++11中，声明非静态成员函数为&lt;code&gt;constexpr&lt;/code&gt;则也包含&lt;code&gt;const&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;针对对象：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt;将对象声明为常量。这意味着保证一旦初始化，该对象的值就不会改变，并且编译器可以利用这一事实进行优化。它还有助于防止程序员编写修改初始化后不打算修改的对象的代码。‘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;constexpr&lt;/code&gt;意思是该对象适合在标准中称为常量表达式的地方使用。但请注意，&lt;code&gt;constexpr&lt;/code&gt;不是唯一实现这个目的的方法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;constexpr 变量&lt;/strong&gt;必须满足下列要求：
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;它的类型必须是字面类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如&lt;code&gt;const int a = 20&lt;/code&gt;。这个没有声明为&lt;code&gt;constexpr&lt;/code&gt;也可以，因为它等同于&lt;code&gt;constexpr&lt;/code&gt;因为是字面值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;它必须立即被初始化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它的初始化包括所有隐式转换、构造函数调用等的全表达式必须是常量表达式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这句话额外针对的是自定义类型。我们可以针对内置类型使用如这样的方式直接使用constexpr&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;constexpr int a = 200;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是自定义类型不可以 如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
        int a;
        int b;
    	myclass(int l, int r):a(l),b(r){};
};
int main(){
    constexpr sss(2,3); //不可以
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这句话的意思就是如果有任何隐式转换或函数调用，则这些函数都必须满足常量表达式要求。也就是一个constexpr则全部必须constexpr。只能用constexpr函数去初始化constexpr变量。这种函数足够简单，以使得编译时就可以计算其结果。&lt;/li&gt;
&lt;li&gt;所以如果想要使用constexpr修饰自定义类型，则构造函数必须也为constexpr函数 如这样：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
        int a;
        int b;
        constexpr myclass(int l, int r):a(l),b(r){}; //必须也是constexpr
};

int main(){
    constexpr myclass obj(2,3); //可以了
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;constexpr修饰指针有特殊要求&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个constexpr指针的初始值必须是&lt;code&gt;nullptr&lt;/code&gt;或者&lt;code&gt;0&lt;/code&gt;，或者是&lt;strong&gt;存储在某个固定地址中的对象&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;固定地址的意思是必须是全局/静态变量&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int s = 100;
int main(){
    int ss = 100;
    constexpr int* b = &amp;amp;s;// OK
    constexpr int* bb = &amp;amp;ss; //不行。这不是固定地址对象。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;constexpr只对指针有效，与指针所指的对象无关。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int s = 100;
int main(){
    constexpr int* b = &amp;amp;s;
    int* const bb = &amp;amp;s; //上面的等于这个。也就是常量指针。
    int sss = 200;
    b = &amp;amp;sss;//不允许。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;针对函数：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;​	可以理解为constexpr修饰的其实是函数返回值。所以必须要入参满足constexpr，返回值也要满足constexpr，计算也要满足constexpr&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt;只能用于非静态成员函数，不能用于一般函数。它保证成员函数不会修改任何非静态数据成员（可变数据成员除外，无论如何都可以修改）。杂记2中详细解释过。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;constexpr&lt;/code&gt;可以与成员函数和非成员函数以及构造函数一起使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;constexpr&lt;/code&gt;函数必须满足如下要求&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;它必须非虚&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;它的函数体不能是函数 try 块&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于构造函数，该类必须无虚基类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它的返回类型（如果存在）和每个参数都必须是字面值类型&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++14前有如下要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数体必须被弃置或预置，或只含有下列内容：
&lt;ul&gt;
&lt;li&gt;空语句（仅分号）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static_assert &lt;/code&gt;声明&lt;/li&gt;
&lt;li&gt;不定义类或枚举的 &lt;code&gt;typedef&lt;/code&gt; 声明及别名声明&lt;/li&gt;
&lt;li&gt;&lt;code&gt;using&lt;/code&gt; 声明&lt;/li&gt;
&lt;li&gt;&lt;code&gt;using &lt;/code&gt;指令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恰好一条&lt;code&gt; return&lt;/code&gt; 语句，当函数不是构造函数时。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;注：三目运算符是OK的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++14后，20前有如下要求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数体必须&lt;strong&gt;不&lt;/strong&gt;含：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;goto&lt;/code&gt; 语句&lt;/li&gt;
&lt;li&gt;拥有除 &lt;code&gt;case&lt;/code&gt; 和 &lt;code&gt;default&lt;/code&gt; 之外的带有标签的语句&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try &lt;/code&gt;块&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asm&lt;/code&gt; 声明&lt;/li&gt;
&lt;li&gt;不进行初始化的变量定义&lt;/li&gt;
&lt;li&gt;非字面类型的变量定义&lt;/li&gt;
&lt;li&gt;静态或线程存储期变量的定义&lt;/li&gt;
&lt;li&gt;注意：是 &lt;code&gt;=default;&lt;/code&gt; 或 &lt;code&gt;=delete;&lt;/code&gt; 的函数体均不含任何上述内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;也就是说14之后可以有&lt;code&gt;if&lt;/code&gt;这样的语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;constexpr 构造函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数体不是 &lt;code&gt;=delete;&lt;/code&gt; 的 &lt;strong&gt;constexpr 构造函数&lt;/strong&gt;必须满足下列额外要求：&lt;/li&gt;
&lt;li&gt;对于类或结构体的构造函数，每个子对象和每个非变体非静态数据成员必须被初始化。&lt;/li&gt;
&lt;li&gt;如果类是联合体式的类，那么对于它的每个非空匿名联合体成员，必须恰好有一个变体成员被初始化&lt;/li&gt;
&lt;li&gt;对于非空联合体的构造函数，恰好有一个非静态数据成员被初始化&lt;/li&gt;
&lt;li&gt;每个被选用于初始化非静态成员和基类的构造函数必须是 constexpr 构造函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一堆的意思是要么构造函数不能含有函数体也就是用初始化列表，要么每一个成员必须有默认值&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
        int a = 2; //必须有默认值
        int b = 3; //必须有默认值
        constexpr myclass(int l, int r){
            a = l;
            b = r;
        };
};
//要不然就这样，用初始化列表
class myclass{
    public:
        int a;
        int b;
        constexpr myclass(int l, int r):a(l),b(r){}; //初始化列表
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;constexpr 析构函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;析构函数不能是 &lt;code&gt;constexpr&lt;/code&gt; 的，但能在常量表达式中隐式调用平凡析构函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;针对模板&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;但由于模板中类型的不确定性，因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。&lt;/li&gt;
&lt;li&gt;针对这种情况下，C++11 标准规定，如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求，则 constexpr 会被自动忽略，即该函数就等同于一个普通函数。&lt;/li&gt;
&lt;li&gt;对于 constexpr 函数模板和类模板的 constexpr 函数成员，必须至少有一个特化满足上述要求。其他特化仍被认为是 constexpr 的，尽管常量表达式中不能出现这种函数的调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/14116003/whats-the-difference-between-constexpr-and-const&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/wangmj_hdu/article/details/119516148&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_22660775/article/details/89336997&lt;/p&gt;
&lt;p&gt;https://yuhao0102.github.io/2020/09/14/cpp%E4%B8%ADconstexpr%E4%BD%9C%E7%94%A8/&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/682031684&quot;&gt;constexpr的发展史&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;ODR使用&lt;/h1&gt;
&lt;p&gt;较为复杂。但是简而言之的说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;一个对象在它的值被读取（除非它是编译时常量）或写入，或取它的地址，或者被引用绑定时，这个对象被 ODR 使用。&lt;/li&gt;
&lt;li&gt;使用“所引用的对象在编译期未知”的引用时，这个引用被 ODR 使用。&lt;/li&gt;
&lt;li&gt;一个函数在被调用或取它的地址时，被 ODR 使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果一个对象、引用或函数被 ODR 使用，那么程序中必须有它的定义；否则通常会有链接时错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class myclass {
   public:
    static constexpr int s = 10;
};
const int myclass::s; // 外部定义

int main() {
    std::cout &amp;lt;&amp;lt; myclass::s &amp;lt;&amp;lt; std::endl; // 非ODR使用
    std::cout &amp;lt;&amp;lt; *&amp;amp;myclass::s &amp;lt;&amp;lt; std::endl; // ODR使用 取地址然后解引用 必须有定义
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;static constexpr （C++14/17)&lt;/h1&gt;
&lt;p&gt;先看代码，还是上面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myclass {
   public:
    static constexpr int s = 10;
};

int main() {
    std::cout &amp;lt;&amp;lt; myclass::s &amp;lt;&amp;lt; std::endl; // 非ODR使用
    std::cout &amp;lt;&amp;lt; *&amp;amp;myclass::s &amp;lt;&amp;lt; std::endl; // ODR使用 没有类外定义所以有未定义符号错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 const 非 inline (C++17 起)静态数据成员或 constexpr 静态数据成员 (C++11 起)(C++17 前)被 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/definition#ODR_.E4.BD.BF.E7.94.A8&quot;&gt;ODR 使用&lt;/a&gt;，那么仍然需要命名空间作用域的定义，但它不能有初始化器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于constexpr从C++17开始变成了默认inline，所以可以不需要类外再次定义，可以编译通过&lt;/p&gt;
&lt;p&gt;为什么inline之后就可以编译通过了？首先，因为类内静态数据成员的声明不是定义！！来自&lt;a href=&quot;https://timsong-cpp.github.io/cppwp/n4659/class.static.data#2&quot;&gt;标准文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;所以通常静态数据成员需要类外定义。inline因为定义逐渐从优化变成了允许多次定义，可能导致违反ODR原则。所以C++17的时候，静态成员的类内初始化需要显式的inline。定义是为了给静态数据成员一个内存位置。从 C++17 开始，当你声明一个 &lt;code&gt;inline&lt;/code&gt; 变量，包括静态数据成员，它会具有外部链接，但它可以在程序中的任何地方出现多次（正如 &lt;code&gt;inline&lt;/code&gt; 函数）。编译器和链接器保证所有的定义都是相同的，并且在整个程序中只存在一个实例。&lt;/p&gt;
&lt;p&gt;在技术层面上，当一个变量被声明为 &lt;code&gt;inline&lt;/code&gt; 时，它告诉编译器和链接器允许变量在不同的编译单元（源文件）中多次定义，只要所有的定义都是相同的。此后，在链接阶段，不论这个变量在多少个编译单元中有定义，都会被视为同一个变量。这样就确保了即使对于 ODR 使用场景（例如取地址），也不会产生未定义符号错误，因为链接器能够正确地将所有引用解析到单一的定义上。&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/46874055/why-is-inline-required-on-static-inline-variables&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/65222905/undefined-reference-error-to-static-constexpr-data-member&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/77764243/undefined-symbol-when-initializing-a-static-constexpr-home-made-string-variable&lt;/p&gt;
&lt;h1&gt;友元&lt;/h1&gt;
&lt;p&gt;之前一直懒得写，这次抽时间整理一下&lt;/p&gt;
&lt;p&gt;友元可以是函数，也可以是类。友元核心意义是，我不属于你，但我成为了你。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是这个类的友元函数或者友元类可以像这个类的成员一样访问这个类的私有属性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;友元函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数，它不属于任何类，但需要在类的定义中加以声明，声明时只需在友元的名称前加上关键字&lt;code&gt;friend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;友元函数必须在类内声明。但是可以在类内或类外定义&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;友元函数和运算符重载参见杂记2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;友元类&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;友元类的所有成员函数都是另一个类的友元函数，都可以访问另一个类中的隐藏信息（包括私有成员和保护成员）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A{
  	public:
    	friend class B;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;经过以上说明后，类 B 的所有成员函数都是类 A 的友元函数，能存取类 A 的私有成员和保护成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;友元声明以关键字 friend 开始，它只能出现在类定义中。因为友元不是授权类的成员，所以它不受其所在类的声明区域 public private 和 protected 的影响。通常我们选择把所有友元声明组织在一起并放在类头之后&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;友元不是类成员，但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率，但是，它破坏了类的封装性和隐藏性，使得非成员函数可以访问类的私有成员。不过，类的访问权限确实在某些应用场合显得有些呆板，从而容忍了友元这一特别语法现象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意要点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;友元关系不能被继承。&lt;/li&gt;
&lt;li&gt;友元关系是单向的，不具有交换性。若类 B 是类 A 的友元，类 A 不一定是类B的友元，要看在类中是否有相应的声明。&lt;/li&gt;
&lt;li&gt;友元关系不具有传递性。若类 B 是类 A 的友元，类 C 是 B 的友元，类 C 不一定是 类 A 的友元，同样要看类中是否有相应的声明。也就是朋友的朋友也是朋友这句话在友元里不成立。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;使用 ifdef 条件编译&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;在编译中使用 &lt;code&gt;-D&lt;/code&gt;可以指定宏定义。
&lt;ul&gt;
&lt;li&gt;在这里如果使用 &lt;code&gt;g++ 12.15.cpp -DTEST1 -o 12.15&lt;/code&gt;则会定义&lt;code&gt;TEST1&lt;/code&gt;宏，所以会编译上面的部分。最终输出&lt;code&gt;10&lt;/code&gt;。否则会编译下面的部分，输出&lt;code&gt;10.234&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#ifdef TEST1

     #define mymacro int

#else
     #define mymacro double

#endif



int main()
{   
     mymacro a = 10.234;
     cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
     return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;main函数前后和CRT&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;操作系统创建进程后，把控制权交给程序的入口函数&lt;code&gt;（gcc –e (_startEntryPoint)）&lt;/code&gt;,   这个函数往往是运行时库的某个入口函数。 &lt;code&gt;glibc&lt;/code&gt; 的入口函数是&lt;code&gt;_start&lt;/code&gt;,&lt;code&gt; msvc(vc6.0)&lt;/code&gt;是&lt;code&gt;mainCRTStartup&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;入口函数对运行库和程序运行环境进行初始化，包括堆，I/O，线程，全局变量构造（constructor）等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用MAIN函数，正式开始执行程序主体。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行MAIN完毕，返回入口函数，进行清理工作，包括全局变量析构，堆销毁，关闭I/O等，然后进行系统调用结束进程&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;程序执行前装载器会把用户的参数和环境变量&lt;strong&gt;压入栈而不是放入寄存器&lt;/strong&gt;。接着操作系统把控制权交给入口函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;入口函数&lt;code&gt;_start&lt;/code&gt;中调用了&lt;code&gt;_libc_start_main&lt;/code&gt;函数，&lt;code&gt;_libc_start_main&lt;/code&gt;函数接受如下（仅列出一部分，更多的可以看高级C/C++编译技术3.4.2）传入参数：&lt;code&gt;_libc_csu_init&lt;/code&gt;函数指针, &lt;code&gt;__libc_csu_fini&lt;/code&gt;函数指针和&lt;code&gt;rtld_fini&lt;/code&gt;函数指针&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;下面是&lt;code&gt;__libc_start_main&lt;/code&gt;的部分细节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;__libc_start_main&lt;/code&gt;中，我们会调用我们所谓的主函数&lt;code&gt;main&lt;/code&gt;。但是在这之前还有许多工作要做。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在我们调用 &lt;code&gt;__libc_start_main&lt;/code&gt; 函数之前，所有的参数如参数，环境变量等（&lt;code&gt;argc&lt;/code&gt;, &lt;code&gt;argv&lt;/code&gt;, &lt;code&gt;envp&lt;/code&gt;）都要被准备好，并且传递给它。这些参数就是我们之前提到的会被压入栈中。&lt;/li&gt;
&lt;li&gt;具体格式参考 &lt;em&gt;&lt;strong&gt;System V ABI 3.4.1&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/image-20230520224259609.png&quot; alt=&quot;image-20230520224259609&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__libc_csu_init&lt;/code&gt;函数是在&lt;code&gt;main&lt;/code&gt;函数调用前调用的函数，全局和静态对象的构造函数就是在这个过程被执行的
&lt;ul&gt;
&lt;li&gt;它会调用下面我们说的&lt;code&gt;_init()&lt;/code&gt;函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;__libc_csu_fini&lt;/code&gt;通过&lt;code&gt;__cxa_exit()&lt;/code&gt;注册到退出链表中&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;main&lt;/code&gt;函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__libc_csu_fini&lt;/code&gt;函数是在&lt;code&gt;main&lt;/code&gt;调用后调用的函数，全局对象的析构就是在这个过程被执行的。
&lt;ul&gt;
&lt;li&gt;它会调用下面我们说的&lt;code&gt;_finit()&lt;/code&gt;函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户的参数：对应&lt;code&gt;int main(int argc,char *argv[])&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;环境变量：系统公用数据，系统搜索路径等等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化和OS版本相关的全局变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化堆，每个进程都有属于自己的堆。它是一次性从系统中申请一块比较大的虚拟空间（&lt;code&gt;malloc&lt;/code&gt;分配的是虚拟页面，直到写入时才会映射到物理页），以后在进程中由库的堆管理算法来维护这个堆。当堆不够用时再继续申请一块大的虚拟空间继续分配。 可见，并非程序每次&lt;code&gt;malloc&lt;/code&gt;都会系统调用（API调用比较耗时，涉及到用户态到内核态的上下文切换），效率比较高。这里契合了我们说的两种内存分配方式（&lt;code&gt;mmap&lt;/code&gt;和&lt;code&gt;brk&lt;/code&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆相关操作：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HeapCreate&lt;/code&gt;:创建一个堆，最终会调用&lt;code&gt;virtualAlloc()&lt;/code&gt;系统API函数去创建堆。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HeapAlloc&lt;/code&gt;: &lt;code&gt;malloc&lt;/code&gt;会调用该函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HeapFree&lt;/code&gt;:&lt;code&gt; free&lt;/code&gt;会调用该函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HeapDestroy&lt;/code&gt;：摧毁一个堆&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I/O初始化，继承父进程打开文件表。可见，子进程是可以访问父进程打开的文件。如果父进程没有打开标准的输入输出，该进程会初始化标准输入输出。即初始化以下指针变量：&lt;code&gt;stdin&lt;/code&gt;,&lt;code&gt;stdout&lt;/code&gt;,&lt;code&gt;stderr&lt;/code&gt;.它们都是&lt;code&gt;FILE&lt;/code&gt;类型指针。在&lt;code&gt;linux&lt;/code&gt;和&lt;code&gt;windows&lt;/code&gt;中，打开文件对应于操作一个内核对象，其处于内核态，因此用户态是不能直接操作该内核对象的。用户只能操作与内核对象相关联的FILE结构指针。对应关系是：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/20170927150035233.png&quot; alt=&quot;20170927150035233&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取命令行参数和环境变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化C库的一些数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局变量构造，如各个&lt;strong&gt;全局 类对象的构造函数调用和标记 &lt;code&gt;__attribute__((constructor))&lt;/code&gt;属性的各个函数&lt;/strong&gt;。它们都应该在进入&lt;code&gt;main&lt;/code&gt;前进行调用。&lt;strong&gt;需要运行时库和C/C++编译器、链接器的配合才能实现这个功能。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为全局变量必须在main函数之前构造、必须在main函数之后析构，所以&lt;strong&gt;运行库在每个目标文件中引入了两个初始化相关的段&lt;code&gt;.init&lt;/code&gt;和&lt;code&gt;.finit&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;作为扩展，GCC 支持&lt;code&gt;__attribute__((constructor))&lt;/code&gt;可以使任意函数在&lt;code&gt;main&lt;/code&gt;. 构造函数可以有一个可选的优先级 &lt;code&gt;__attribute__((constructor(N)))&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;从 0 到 100 的优先级保留用于实现（&lt;code&gt;-Wprio-ctor-dtor&lt;/code&gt;违反捕获），例如 &lt;code&gt;gcov&lt;/code&gt; 使用&lt;code&gt;__attribute__((destructor(100)))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;应用程序可以使用 101 到 65535。65535（&lt;code&gt;.init_array&lt;/code&gt;或&lt;code&gt;.ctors&lt;/code&gt;，不带后缀）与 C++ 中非局部变量的动态初始化具有相同的优先级。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;编译器编译某个&lt;code&gt;.cpp&lt;/code&gt;（翻译单元）时，会将所有的全局对象的构造函数指针作为一个整体放到&lt;code&gt;.init&lt;/code&gt;段，把析构函数指针放到&lt;code&gt;.finit&lt;/code&gt;段，然后在&lt;code&gt;.ctors&lt;/code&gt;段放置&lt;code&gt;.init&lt;/code&gt;段的地址（该地址即是该文件的各个构造函数的总入口）。
&lt;ul&gt;
&lt;li&gt;最后我们会拥有两个函数&lt;code&gt;_init()&lt;/code&gt;和&lt;code&gt;_finit()&lt;/code&gt;。这两个函数会先后于&lt;code&gt;main&lt;/code&gt;函数执行&lt;/li&gt;
&lt;li&gt;运行时库有一个库是&lt;code&gt;crtbegin.o&lt;/code&gt;，它使用&lt;code&gt;-1&lt;/code&gt;定义了&lt;code&gt;.ctors/.dtors&lt;/code&gt;，&lt;code&gt;ctrend.o&lt;/code&gt;使用&lt;code&gt;0&lt;/code&gt;定义了&lt;code&gt;.ctors/.dtors&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用链接器进行连接:&lt;code&gt;ld crtbegin.o main.o crtend.o&lt;/code&gt;一定要按这种顺序，否则出错。链接后的.ELF文件是将以上各个翻译单元的&lt;code&gt;.init/.finit/.ctors/.dtors&lt;/code&gt;等段分别合并。当然&lt;code&gt;.data/.text&lt;/code&gt;段也会相应合并。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于每个编译单元(.cpp)，GCC编译器会遍历其中所有的全局对象，生成两个特殊的函数，第一个特殊函数的作用就是调用本编译单元的所有全局对象的构造函数来对本编译单元里的所有全局对象进行初始化。第二个特殊函数的作用就是调用本编译单元的所有全局对象的析构函数来对本编译单元里的所有全局对象进行析构。它的调用顺序与调用构造函数的顺序刚好相反&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;GCC在目标代码中生成了一个名为&lt;code&gt;_GLOBAL__I_Hw&lt;/code&gt;的函数，由这个函数负责本编译单元所有的全局/静态对象的构造和析构，它的代码可以表示为：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static void GLOBAL__I_Hw(void)
{
	Hw::Hw(); // 构造对象
	atexit(__tcf_1); // 一个神秘的函数叫做__tcf_1被注册到了exit
    //可能用的不是atexit，是__cxa_exit
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一旦一个目标文件里有这样的函数，编译器会在这个编译单元产生的目标文件(.o)的&lt;code&gt;.ctors&lt;/code&gt;段里放置一个指针，这个指针指向的便是&lt;code&gt;GLOBAL__I_Hw&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局变量构造时即是遍历&lt;code&gt;.ctors&lt;/code&gt;段的内容，从&lt;code&gt;-1（crtbegin.o）&lt;/code&gt;开始，再到&lt;code&gt;-1(crtend.o)&lt;/code&gt;结束，中间每四个字节即是各个翻译单元的构造入口函数指针，如果非0，即进行调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在底层，在 ELF 平台上，初始化函数或构造函数以两种方案实现。旧版使用&lt;code&gt;.init&lt;/code&gt;/&lt;code&gt;.ctors&lt;/code&gt;而新版使用&lt;code&gt;.init_array&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/QQ%E6%88%AA%E5%9B%BE20230101224120.png&quot; alt=&quot;QQ截图20230101224120&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/QQ%E6%88%AA%E5%9B%BE20230101224127.png&quot; alt=&quot;QQ截图20230101224127&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么使用&lt;code&gt;.init_array&lt;/code&gt;和&lt;code&gt;.fini_array&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开发人员注意到&lt;code&gt;.init&lt;/code&gt;/&lt;code&gt;.ctors&lt;/code&gt;方案存在多个问题：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;碎片&lt;code&gt;_init&lt;/code&gt;化的功能丑陋且容易出错。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.ctors&lt;/code&gt;里面的-1和0很难看。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.init&lt;/code&gt;并&lt;code&gt;.ctors&lt;/code&gt;使用magic name而不是有意义的名字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以&lt;code&gt;.init_array&lt;/code&gt;里面就是各个全局对象构造函数的函数指针。等于遍历数组即可而不用遍历段了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注册析构函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了支持C++类的析构函数，和标记&lt;code&gt; attribute((deconstructor))&lt;/code&gt;属性的各个函数在&lt;code&gt;main&lt;/code&gt;之后会被调用，而且是按构造的相反顺序进行调用，同样需要编译器和链接器以及运行时库的支持，原理跟构造相仿。只是为了逆序，使用了&lt;code&gt;atexit()&lt;/code&gt;注册各个析构函数(上面提到过），注册时在链表头插入链接，&lt;code&gt;main&lt;/code&gt;退出以后也从链表头开始获取链表函数，并进行调用。&lt;/li&gt;
&lt;li&gt;我们上面提到了编译器生成的特殊函数。这个特殊函数大概这个样子&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static void __tcf_1(void) //这个名字由编译器生成
{
	Hw.~Hw();
}
/*
这个函数负责析构Hw对象，由于在GLOBAL__I_Hw中我们通过atexit()注册了__tcf_1，而且通过atexit()注册的函数在进程退出时被调用的顺序满足先注册后调用的属性，与构造和析构的顺序完全符合，于是它就很自然被用于析构函数的实现了。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;执行函数主体。调用main函数执行，等待返回。在这里可以用到之前已经初始化的各种资源，如I/O, 堆申请释放等等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt;函数返回后（&lt;strong&gt;局部变量在是在return后，exit前析构。因为_libc_start_main内部调用init,然后是main, 然后是exit。所以是main函数返回后，自然main里的局部变量全部出栈。exit函数进入前析构main的局部变量&lt;/strong&gt;），调用&lt;code&gt;exit&lt;/code&gt;函数。大概长这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void exit(int status)
{
	while (__exit_funcs != NULL)
	{
		...
		__exit_funcs = __exit_funcs-&amp;gt;next;
	}
	...
	_exit(status);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;__exit_funcs&lt;/code&gt;是存储由&lt;code&gt;__cxa_atexit&lt;/code&gt;和&lt;code&gt;atexit&lt;/code&gt;注册的（退出清理）函数的链表，这里的while循环遍历该链表并逐个调用这些注册的函数。可以明显看到，&lt;code&gt;exit&lt;/code&gt;函数会在末尾调用&lt;code&gt;_exit&lt;/code&gt;，这与OS描述“库函数&lt;code&gt;exit&lt;/code&gt;内部调用系统调用&lt;code&gt;_exit&lt;/code&gt;退出进程” 一致。而&lt;code&gt;_exit&lt;/code&gt;实现由汇编实现，与平台相关。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重申&lt;code&gt;__libc_csu_fini&lt;/code&gt;在退出链表中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;释放堆&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个程序结束，那么分配给这个进程的所有资源都会被回收。堆空间自然不例外。这里哪管你用不用析构，连房子带地皮都给你扬了。所以不会内存泄漏。这里所谓的不会泄露并不是真的不泄漏，确实泄露了。但是，就算我没有delete，但是程序结束了。对系统无影响。&lt;/li&gt;
&lt;li&gt;我们所说的内存泄漏是在程序的生命周期中发生的。比如我同样的程序变成了循环一万次，那么每一次new都不回收，这样就是内存泄露了。因为对我们有影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;释放其他资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用&lt;code&gt;exit&lt;/code&gt;系统API退出进程&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/QQ%E6%88%AA%E5%9B%BE20230102015908.png&quot; alt=&quot;QQ截图20230102015908&quot; /&gt;&lt;/p&gt;
&lt;p&gt;.init/.ctors/.init_array相关可以看&lt;a href=&quot;https://maskray.me/blog/2021-11-07-init-ctors-init-array&quot;&gt;这篇文章&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;atexit函数和__cxa_exit 函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int atexit (void (*)(void))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;atexit&lt;/code&gt;函数是一个特殊的函数，它是在正常程序退出时调用的函数，我们把他叫为登记函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意&lt;code&gt;atexit&lt;/code&gt;函数登记的函数必须是无参且无返回值的函数。也就是函数签名必须为&lt;code&gt;void (*)(void)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⼀个进程可以登记多个终止处理函数，这些函数由exit⾃动调用， &lt;code&gt;atexit&lt;/code&gt;函数可以登记这些函数。 &lt;strong&gt;&lt;code&gt;exit&lt;/code&gt;调⽤终⽌处理函数的顺序和&lt;code&gt;atexit&lt;/code&gt;登记的顺序相反（网上很多说造成顺序相反的原因是参数压栈造成的，参数的压栈是先进后出，和函数的栈帧相同），如果⼀个函数被多次登记，也会被多次调⽤&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func1() 
{ 
    printf(&quot;The process is done...\n&quot;); 
} 
void func2() 
{ 
    printf(&quot;Clean up the processing\n&quot;); 
} 
void func3() 
{ 
    printf(&quot;Exit sucessful..\n&quot;); 
} 
int main(){
    atexit(func1);
    atexit(func2);
    atexit(func3);
    return 0;
}
/*
Exit sucessful..       
Clean up the processing
The process is done... 
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以明显看到逆序输出。&lt;/p&gt;
&lt;h2&gt;atexit函数调用时机&lt;/h2&gt;
&lt;p&gt;以下函数的调用时程序异常或者正常终止：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程终⽌的⽅式有8种，前5种为正常终⽌，后三种为异常终⽌：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;从&lt;code&gt;main&lt;/code&gt;函数返回；&lt;/li&gt;
&lt;li&gt;调⽤&lt;code&gt;exit&lt;/code&gt;函数；&lt;/li&gt;
&lt;li&gt;调⽤&lt;code&gt;_exit&lt;/code&gt;或&lt;code&gt;_Exit&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;最后⼀个线程从启动例程返回；&lt;/li&gt;
&lt;li&gt;最后⼀个线程调⽤&lt;code&gt;pthread_exit&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;调⽤&lt;code&gt;abort&lt;/code&gt;函数；&lt;/li&gt;
&lt;li&gt;接到⼀个信号并终⽌；&lt;/li&gt;
&lt;li&gt;最后⼀个线程对取消请求做出响应。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;atexit函数和__cxa_atexit函数的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__cxa_atexit()&lt;/code&gt;不限于32个函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__cxa_atexit()&lt;/code&gt;在程序退出前通过&lt;code&gt;dlclose&lt;/code&gt;卸载动态库时，将调用动态库的静态析构函数。&lt;/li&gt;
&lt;li&gt;用户不应直接调用&lt;code&gt;__cxa_atexit()&lt;/code&gt;函数&lt;/li&gt;
&lt;li&gt;当然了，一些标准库如glibc的&lt;code&gt;atexit&lt;/code&gt;直接使用了&lt;code&gt;_cxa_atexit&lt;/code&gt; (1)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;EXIT和return区别&lt;/h2&gt;
&lt;h3&gt;EXIT&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;是系统层级的系统调用，&lt;strong&gt;指的是让进程退出&lt;/strong&gt;。调用后会开始进行资源的清理和回收。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exit&lt;/code&gt;是一个函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;return&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;return&lt;/code&gt;是一个关键字，用于退出这个函数。（结束函数的执行）。也就是堆栈展开并销毁局部变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;pthread_exit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;noreturn void pthread_exit(void *retval);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以使用&lt;code&gt;pthread_exit&lt;/code&gt;可以维持子线程不退出的原因是，虽然主进程也是主线程，但是我只让主进程自己的线程退出，也就是不回收进程资源，相当于进程没有结束，子线程依旧可以执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;pthread_exit&lt;/code&gt;后，当进程中最后一个线程停止时（或有某个线程执行了exit(), abort()或终止函数时 (来自&lt;a href=&quot;https://stackoverflow.com/questions/3692591/return-versus-pthread-exit-in-pthread-start-functions&quot;&gt;这里&lt;/a&gt;））,  整个进程通过调用&lt;code&gt;exit&lt;/code&gt;而退出。所以，所有进程共享的资源会被释放。并且通过&lt;code&gt;atexit&lt;/code&gt;注册的函数会被调用。来自&lt;a href=&quot;https://man7.org/linux/man-pages/man3/pthread_exit.3.html&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有可能发生多个子线程中，主线程使用了&lt;code&gt;pthread_exit&lt;/code&gt;， 然后其中一个子线程可能在其他子线程结束之前调用了&lt;code&gt;exit()&lt;/code&gt;，&lt;code&gt;abort()&lt;/code&gt;，或其他终止函数。此时无论其他线程执行完毕与否，都会直接进行进程退出阶段。也就是如上面所说，所有进程共享的资源会被释放。并且通过&lt;code&gt;atexit&lt;/code&gt;注册的函数会被调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意，&lt;code&gt;pthread_exit&lt;/code&gt;是有参数的。参数是个指针。这个参数的作用是可以把一些参数“塞到”这个指针中。相当于把一个调用&lt;code&gt;pthread_exit&lt;/code&gt;函数的线程的返回值通过这个&lt;code&gt;retval&lt;/code&gt;传出去。这样这个返回值可以被在同一个进程中的其他调用了&lt;code&gt;pthread_join()&lt;/code&gt;的线程所使用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;pthread_exit()&lt;/code&gt; function terminates the calling thread and returns a value via &lt;code&gt;retval&lt;/code&gt; that (if the thread is joinable) is available to another thread in the same process that &lt;code&gt;callspthread_join(3)&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;exit与_exit 的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无论何时执行&lt;code&gt;return&lt;/code&gt;，都会析构局部变量，并弹栈。在&lt;code&gt;main&lt;/code&gt;函数中执行&lt;code&gt;return&lt;/code&gt;，会析构局部变量、弹栈，&lt;strong&gt;然后调用&lt;code&gt;exit&lt;/code&gt;&lt;/strong&gt;（包括了析构全局变量和local static变量的过程）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;格外注意，&lt;code&gt;main&lt;/code&gt;中执行&lt;code&gt;return&lt;/code&gt;会隐式调用&lt;code&gt;exit&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;exit&lt;/code&gt;是C标准库的函数。&lt;code&gt;_exit&lt;/code&gt;是系统调用。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用&lt;code&gt;exit&lt;/code&gt;时会析构全局变量和local static变量（不析构局部变量），刷新IO缓冲区，关闭文件描述符，调用&lt;code&gt;atexit&lt;/code&gt;注册的函数（以注册时相反的顺序调用），但不弹栈。最后调用&lt;code&gt;exit&lt;/code&gt;函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接调用&lt;code&gt;_exit&lt;/code&gt;不会析构任何变量，不会刷新IO缓冲区，&lt;strong&gt;不会执行&lt;/strong&gt;任何通过&lt;code&gt;atexit&lt;/code&gt;注册的函数，不弹栈，但会关闭文件描述符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为系统调用非常暴力。直接把整个进程（状态机）直接给扬了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;exit()&lt;/code&gt;和&lt;code&gt;_exit()&lt;/code&gt;以及&lt;code&gt;_Exit()&lt;/code&gt;函数的本质区别是一个是系统调用，一个是C库函数。也就是是否立即进入内核，&lt;code&gt;_exit()&lt;/code&gt;以及&lt;code&gt;_Exit()&lt;/code&gt;函数都是在调用后立即进入内核，而不会执行一些清理处理，但是&lt;code&gt;exit()&lt;/code&gt;则会执行一些清理处理，这也是为什么会存在&lt;code&gt;atexit()&lt;/code&gt;函数的原因，因为&lt;code&gt;exit()&lt;/code&gt;函数需要执行清理处理，需要执行一系列的操作，这些终止处理函数实际上就是完成各种所谓的清除操作的实际执行体。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;_Exit() &lt;/code&gt;和&lt;code&gt;_exit()&lt;/code&gt;相同。前者来自C99，后者来自POSIX&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-11-20-%E6%9D%82%E8%AE%B04.assets/QQ%E6%88%AA%E5%9B%BE20230305233541.png&quot; alt=&quot;QQ截图20230305233541&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;为什么 exit() 函数不是线程安全的？&lt;/h2&gt;
&lt;p&gt;主要原因 &lt;code&gt;exit&lt;/code&gt;语义等同于从&lt;code&gt;main&lt;/code&gt; 返回，会涉及到资源释放等相关流程，自然引入竞争问题&lt;/p&gt;
&lt;p&gt;避免全局资源释放，使用&lt;code&gt;quick_exit&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;另外直接列一下各种exit区别 https://learn.microsoft.com/en-us/previous-versions/6wdz5232(v=vs.140)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;exit&lt;/code&gt; 执行完整的 C 库终止过程，终止进程，并向主机环境提供提供的状态代码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_Exit&lt;/code&gt; 执行最少的 C 库终止过程，终止进程，并向主机环境提供提供的状态代码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_exit&lt;/code&gt; 执行最少的 C 库终止过程，终止进程，并向主机环境提供提供的状态代码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quick_exit&lt;/code&gt; 执行快速 C 库终止过程，终止进程，并向主机环境提供提供的状态代码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_cexit&lt;/code&gt; 执行完整的 C 库终止过程并返回给调用方。不终止进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_c_exit&lt;/code&gt; 执行最少的 C 库终止过程并返回给调用方。不终止进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;例子和梳理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void func(){
    cout &amp;lt;&amp;lt;&quot;start&quot; &amp;lt;&amp;lt; endl;
    myobj a(20); //构建新对象
    sleep(5); //睡眠5秒
    cout &amp;lt;&amp;lt; &quot;end&quot; &amp;lt;&amp;lt; endl;
}
void func1() 
{ 
    printf(&quot;atexit registure func\n&quot;); 
} 
int main(){
    atexit(func1); //注册退出时执行函数判断exit是否调用了
    cout &amp;lt;&amp;lt;&quot;main start&quot; &amp;lt;&amp;lt; endl;
    thread th1(func); //新线程
    th1.detach(); //使用detach
    sleep(1);
    cout &amp;lt;&amp;lt;&quot;main end&quot; &amp;lt;&amp;lt; endl;
    pthread_exit(NULL); //强制提前退出
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一种情况，使用&lt;code&gt;join&lt;/code&gt;的时候，&lt;code&gt;main&lt;/code&gt;主线程会等候子线程执行完毕，不多说。&lt;/li&gt;
&lt;li&gt;第二种情况，使用&lt;code&gt;detach&lt;/code&gt;的时候，&lt;code&gt;main&lt;/code&gt;主线程不会等候子线程执行完毕。由于已经分离，主线程无法取得子线程控制权。所以如果&lt;code&gt;main&lt;/code&gt;主线程执行完毕，&lt;code&gt;exit&lt;/code&gt;会被调用。这时候会强制停止整个进程。所以子线程并不一定能够执行完毕。&lt;/li&gt;
&lt;li&gt;第三种情况，使用&lt;code&gt;pthread_exit&lt;/code&gt;。使用&lt;code&gt;pthread_exit&lt;/code&gt;等于强制停止主线程。&lt;code&gt;main&lt;/code&gt;在这种情况下，既是主进程又是主线程。所以使用&lt;code&gt;pthread_exit&lt;/code&gt;等于退出了&lt;code&gt;main&lt;/code&gt;主线程，但是进程还在，因为&lt;code&gt;main&lt;/code&gt;结束后的&lt;code&gt;exit&lt;/code&gt;没有被调用。此时由于&lt;code&gt;exit&lt;/code&gt;没有被调用，所以会等候子线程执行完毕。使用&lt;code&gt;pthread_exit&lt;/code&gt;后，在整个进程退出时，&lt;code&gt;exit&lt;/code&gt;还是会被调用。等于此时&lt;code&gt;main&lt;/code&gt;的&lt;code&gt;exit&lt;/code&gt;被&lt;code&gt;pthread_exit&lt;/code&gt;接管。原因在上面&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;https://www.cnblogs.com/matex/articles/10807092.html&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/yueqian_scut/article/details/24384941&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/gaoyz1/article/details/78113069&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/430683541&lt;/p&gt;
&lt;p&gt;https://www.youtube.com/watch?v=dOfucXtyEsU&lt;/p&gt;
&lt;p&gt;https://kongkongk.github.io/2020/06/30/construction-and-destruction/&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_38600065/article/details/117370846&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/pwl999/article/details/78219188&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/fortunely/p/15502849.html#111-%E5%85%A5%E5%8F%A3%E5%87%BD%E6%95%B0%E5%92%8C%E7%A8%8B%E5%BA%8F%E5%88%9D%E5%A7%8B%E5%8C%96&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/42912038/what-is-the-difference-between-cxa-atexit-and-atexit&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/u012961585/article/details/103848484&lt;/p&gt;
&lt;p&gt;http://originlee.com/2015/03/27/difference-between-return-and-exit-in-cxx/&lt;/p&gt;
&lt;p&gt;https://luomuxiaoxiao.com/?p=516&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/kongkongkkk/article/details/72861149&lt;/p&gt;
&lt;p&gt;(1)&lt;a href=&quot;https://maskray.me/blog/2024-03-17-c++-exit-time-destructors&quot;&gt;C++ exit-time destructors&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;static_assert 静态断言 和 assert 动态断言&lt;/h1&gt;
&lt;h2&gt;static_assert&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;static_assert&lt;/code&gt;静态断言，是C++关键字，作用是让编译器在&lt;strong&gt;编译时对常量表达时进行断言&lt;/strong&gt;。如果通过，就不报错；如果不通过，就报错。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static_assert ( 布尔常量表达式 , 消息 )		
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编译器在遇到一个&lt;code&gt;static_assert&lt;/code&gt;语句时，通常立刻将其第一个参数作为常量表达式进行演算，但如果该常量表达式依赖于某些模板参数，则延迟到模板实例化时再进行演算，这就让检查模板参数成为了可能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果 &lt;em&gt;布尔常量表达式&lt;/em&gt; 返回&lt;code&gt; true&lt;/code&gt;，那么该声明没有效果。否则将发布编译时错误，且当存在 &lt;em&gt;消息&lt;/em&gt;时诊断消息中会包含其文本。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;assert&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;assert&lt;/code&gt;是动态断言，是一个仿函数宏。&lt;strong&gt;&lt;code&gt;assert&lt;/code&gt;是运行期的判断，并且会强制终止程序，一般要求只能用于debug版本中&lt;/strong&gt;，是为了尽可能快的发现问题。assert是要从release版本中去掉。所以一般开发会重新定义assert宏。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态断言会降低程序性能。增大运行时花费的空间和时间。&lt;/li&gt;
&lt;li&gt;由于动态断言是运行期判断，所以如果断言不通过不影响编译。因为编译期不检查动态断言。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;assert.h&amp;gt;  
void assert( int expression );  
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;重复包含和前向声明 杂记3也有部分&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在A.h头文件中包含B.h&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在A.h头文件A类中声明一个B类的对象，可以是指针，也可以不是指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在B.h头文件中用extern声明类A，然后在类B中声明一个A类的指针对象（必须是指针型）、&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意这个应用了前向声明的头文件不需要包含前向声明的类的头文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在A.cpp和B.cpp中都只需要包含A.h，B.cpp不需要包含B.h&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后在A.cpp和B.cpp中分别定义A类的对象和B类的对象。---可选&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将它们的指针分别传给对方的对象。如果是在不同的文件，也可以是this传值，比如在A.cpp中，给B.cpp中的A类对象传递A的当前对象，b-&amp;gt;a=this;然后B类中的a就可以调用A类的成员方法了。----可选&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后就可以互相调用彼此的对象了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;main里只包含A.h 因为A有完整的B。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;个人理解，可以在头文件中使用前向声明，然后在源文件中include&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//Project.h -----------A.h
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &quot;Employee.h&quot; //包含B.h (第一条)
using namespace std;
class Project{
    public:
        string name;
        vector&amp;lt;Employee&amp;gt; peoples; //含有B的对象 （第二条）
        Project() = default;
        Project(const string&amp;amp; rhs):name(move(rhs)){};
        void SetPeople(const Employee&amp;amp; people);
        void printPeople();
};
//Employee.h -------- B.h
class Project; //前置声明A （第三条）这个文件使用了前向声明，前向声明的类的头文件不需要被当前文件包含。
class Employee{
    public:
        Project* m_proj; //包含A的指针 （第三条）
        int ID;
        Employee() = default;
        Employee(int val):ID(val){};
        void SetProj(Project* proj);
        
};

//Project.cpp ---------------A.cpp
#include &quot;Project.h&quot;  //只包含 Project.h (第四条)
using namespace std;
void Project::SetPeople(const Employee&amp;amp; people){
    peoples.emplace_back(people);
}
void Project::printPeople(){
    for(auto&amp;amp; i:peoples){
        cout &amp;lt;&amp;lt; i.ID &amp;lt;&amp;lt; endl;
    }
}

//Employee.cpp ---------------B.cpp
#include &quot;Project.h&quot; //Employee.cpp 不包含 Employee.h 只包含 Project.h (第四条)
void Employee::SetProj(Project* proj){
    m_proj = proj;
    m_proj-&amp;gt;SetPeople(*this);
}


//main.cpp

#include &quot;Project.h&quot; //只包含A.h即可（第八条）
using namespace std;


int main()
{
    Project* myproj = new Project(&quot;first project&quot;);
    Employee miku(1);
    Employee luka(2);
    miku.SetProj(myproj);
    luka.SetProj(myproj);

    myproj-&amp;gt;printPeople();

    delete myproj;
    myproj = nullptr;

	return 0;

}


&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;std::type_info和type_id操作符&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关于type_id操作符，如果当应用于多态类型的表达式时，typeid 表达式的求值可能涉及运行时开销（虚表查找），其他情况下 typeid 表达式都在编译时解决。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;typeid&lt;/code&gt;操作符的返回结果是名为&lt;code&gt;type_info的&lt;/code&gt;标准库类型的对象的&lt;code&gt;const&lt;/code&gt;引用。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++并没有规定typeid实现标准，各个编译器可能实现方式不一样。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关于type_info对象，这个类的构造函数是private的，因此用户不能直接构造这样的对象，只能通过typeid()函数来获取这个对象。但是这个类对外提供了name()，operator==()等方法供用户使用&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这样是不可以的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 20;
type_info t1 = typeid(a);//错误
const type_info&amp;amp; t1 = typeid(a); //正确
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有几个规则需要注意一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 类型 是引用类型，那么结果指代的&lt;code&gt;std::type_info&lt;/code&gt;对象表示被引用的类型的无 cv 限定版本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 类型和 表达式 的类型具有 cv 限定，那么 &lt;code&gt;typeid&lt;/code&gt; 的结果会指代对应的无 cv 限定类型（即 &lt;code&gt;typeid(T) == typeid(const T)&lt;/code&gt;）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是指针类型，这里表示忽略顶层CV，只保留底层CV。也就是不看指针本身，而是看指向的数据类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果对处于构造和销毁过程中的对象（在构造函数或析构函数之内，包括构造函数的初始化器列表或默认成员初始化器）使用 &lt;code&gt;typeid&lt;/code&gt;，那么此 &lt;code&gt;typeid&lt;/code&gt; 指代的 &lt;code&gt;std::type_info&lt;/code&gt;对象表示正在构造或销毁的类，即便它不是最终派生类。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;很好理解。先构造父类，虚指针指向父类虚函数表，自然RTTI是父类。然后执行用户代码。然后构造子类，这时候虚指针才切换至子类，自然RTTI变为子类。然后执行用户代码。&lt;/li&gt;
&lt;li&gt;在深入理解对象模型中我们反复强调：虚函数表指针的切换应该是在构造函数后，用户代码前。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先看一下基本类型的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
    int a = 20;
    const int b = 30;

    int&amp;amp; c = a;
    const int&amp;amp; d = a;

    int* e = new int(5);
    int* const f = new int(5);
    const int* g = new int(6);
    const int* const h = new int(6);

    const type_info&amp;amp; t1 = typeid(a);
    const type_info&amp;amp; t2 = typeid(b);
    const type_info&amp;amp; t3 = typeid(c);
    const type_info&amp;amp; t4 = typeid(d);
    const type_info&amp;amp; t5 = typeid(e);
    const type_info&amp;amp; t6 = typeid(f);
    const type_info&amp;amp; t7 = typeid(g);
    const type_info&amp;amp; t8 = typeid(h);

    cout &amp;lt;&amp;lt; t1.name() &amp;lt;&amp;lt; endl; //int
    cout &amp;lt;&amp;lt; t2.name() &amp;lt;&amp;lt; endl; //int
    cout &amp;lt;&amp;lt; t3.name() &amp;lt;&amp;lt; endl; //int
    cout &amp;lt;&amp;lt; t4.name() &amp;lt;&amp;lt; endl; //int
    cout &amp;lt;&amp;lt; t5.name() &amp;lt;&amp;lt; endl; //int * __ptr64
    cout &amp;lt;&amp;lt; t6.name() &amp;lt;&amp;lt; endl; //int * __ptr64
    cout &amp;lt;&amp;lt; t7.name() &amp;lt;&amp;lt; endl; //int const * __ptr64
    cout &amp;lt;&amp;lt; t8.name() &amp;lt;&amp;lt; endl; //int const * __ptr64
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们看到。针对类型本身和引用是忽略CV限定的。&lt;/li&gt;
&lt;li&gt;针对指针类型，只看其底层类型。也就是指针指向的数据类型。
&lt;ul&gt;
&lt;li&gt;无论指针本身是否是常量，如果指向的数据是常量则带&lt;code&gt;const&lt;/code&gt;。如果指向的数据不是常量则不带&lt;code&gt;const&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看一看带多态的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base {
public:
    Base() {

    }
    virtual ~Base() {

    }
    virtual void func() {

    }
};

class Derive :public Base {
public:
    Derive() {

    }
    virtual ~Derive() {

    }
    virtual void func() {

    }
};
int main()
{
    Base b1;
    Derive d1;

    Base* bptr = new Base;
    Derive* dptr = new Derive;

    Base* p = new Derive;


    const type_info&amp;amp; t1 = typeid(b1);
    const type_info&amp;amp; t2 = typeid(d1);

    const type_info&amp;amp; t3 = typeid(bptr);
    const type_info&amp;amp; t4 = typeid(dptr);
    const type_info&amp;amp; t5 = typeid(p);


    const type_info&amp;amp; t6 = typeid(*bptr);
    const type_info&amp;amp; t7 = typeid(*dptr);
    const type_info&amp;amp; t8 = typeid(*p);

    cout &amp;lt;&amp;lt; t1.name() &amp;lt;&amp;lt; endl; //class Base
    cout &amp;lt;&amp;lt; t2.name() &amp;lt;&amp;lt; endl; //class Derive
    
    cout &amp;lt;&amp;lt; t3.name() &amp;lt;&amp;lt; endl; //class Base * __ptr64
    cout &amp;lt;&amp;lt; t4.name() &amp;lt;&amp;lt; endl; //class Derive * __ptr64
    cout &amp;lt;&amp;lt; t5.name() &amp;lt;&amp;lt; endl; //class Base * __ptr64 注意这个
    
    cout &amp;lt;&amp;lt; t6.name() &amp;lt;&amp;lt; endl; //class Base
    cout &amp;lt;&amp;lt; t7.name() &amp;lt;&amp;lt; endl; //class Derive
    cout &amp;lt;&amp;lt; t8.name() &amp;lt;&amp;lt; endl; //class Derive 注意这个
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先注意必须有虚函数。虚函数是多态的必不可少的要求之一。也是因为RTTI信息在虚函数表内。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们看到。针对多态的时候的&lt;code&gt;t3&lt;/code&gt; &lt;code&gt;t4&lt;/code&gt; &lt;code&gt;t5&lt;/code&gt;，类型依旧是对应的指针本身的类型。尤其是&lt;code&gt;t5&lt;/code&gt;的类型依旧是父类指针类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们如果想看指针真正指向的类型就需要对指针解引用。比如&lt;code&gt;t6&lt;/code&gt; &lt;code&gt;t7&lt;/code&gt; &lt;code&gt;t8&lt;/code&gt;。我们是对指针进行了解引用。所以这个时候尤其是&lt;code&gt;t8&lt;/code&gt;可以看到其指向类型是子类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意：针对如果输入&lt;code&gt;typeid&lt;/code&gt;的参数多态类型的指针，而且是空指针并进行解引用，则会触发&lt;code&gt;std::bad_typeid&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>笔试题整理</title><link>https://sterben-01.github.io/posts/%E7%AC%94%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E7%AC%94%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86/</guid><pubDate>Wed, 05 Oct 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;笔试题整理&lt;/h1&gt;
&lt;h1&gt;输出下面函数的运行结果&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class A
{
public:
    virtual std::string clsname() { return &quot;A&quot;; }
    std::string clsname2() { return &quot;A&quot;; }

    void test()
    { std::cout &amp;lt;&amp;lt; &quot;A, &quot; &amp;lt;&amp;lt; clsname() &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; clsname2() &amp;lt;&amp;lt; std::endl; }
};

class B : public A
{
public:
    std::string clsname() override { return &quot;B&quot;; }
    virtual std::string clsname2() { return &quot;B&quot;; }

    virtual void test()
    { std::cout &amp;lt;&amp;lt; &quot;B, &quot; &amp;lt;&amp;lt; clsname() &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; clsname2() &amp;lt;&amp;lt; std::endl; }
};

class C : public B
{
public:
    std::string clsname() override { return &quot;C&quot;; }
    std::string clsname2() override { return &quot;C&quot;; }

    void test() override
    { std::cout &amp;lt;&amp;lt; &quot;C, &quot; &amp;lt;&amp;lt; clsname() &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; clsname2() &amp;lt;&amp;lt; std::endl; }
};

int main()
{
    A a; B b; C c; A d = c;

    b.test(); //B,B,B
    c.test(); //C,C,C
    d.test(); //A,A,A

    ((A&amp;amp;)c).test();//A,C,A
    //等同于static_cast&amp;lt;A&amp;amp;&amp;gt;(c).test();
    ((B&amp;amp;)c).test();//C,C,C

    std::bind(&amp;amp;A::test, &amp;amp;b)();//A,B,A
    std::bind(&amp;amp;A::test, &amp;amp;c)();//A,C,A

    std::bind(&amp;amp;B::test, &amp;amp;b)();//B,B,B
    std::bind(&amp;amp;B::test, &amp;amp;c)();//C,C,C

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前三个没什么好说的&lt;/li&gt;
&lt;li&gt;第四个是C风格的强制类型转换。等同于&lt;code&gt;static_cast&amp;lt;A&amp;amp;&amp;gt;(c).test();&lt;/code&gt;。杂记2的强制类型转换里面有。因为强制类型转换不改变原对象，只是依靠原对象生成新的指定类型的对象。这里相当于用&lt;code&gt;A&amp;amp;&lt;/code&gt;类型的临时对象进行调用。注意此时临时对象是&lt;code&gt;c&lt;/code&gt;的引用。并且引用一样可以触发多态。所以是ACA。&lt;/li&gt;
&lt;li&gt;第五个是CCC的原因是B类里面test也是虚函数了。&lt;/li&gt;
&lt;li&gt;六七八九就是bind。bind会返回可调用对象。注意这里也属于临时对象。第二个参数隐含的this指针传入哪个对象地址就相当于多态调用了哪个对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;异步转同步&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-15-%E7%AC%94%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220915011409.png&quot; alt=&quot;微信图片_20220915011409&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这题相当于让我们的request_sync函数等候request函数执行完毕后再返回。为此我们可以在sync函数内给request函数单独开一个线程，并且强制死锁等候调用完毕后进行解锁。&lt;/p&gt;
&lt;p&gt;大概示范：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mutex mylock; //全局变量的锁 也可以写成局部变量然后传入。

void request(const function&amp;lt;void(int)&amp;gt;&amp;amp; callback){
    int input_param;
    callback(input_param);
    mylock.unlock(); //解锁（这个是一次锁）

}

void testfunc(int){

}

vector&amp;lt;int&amp;gt; request_sync(){
    mylock.lock(); //一次加锁
    thread th(request, testfunc);
    mylock.lock(); //二次重复加锁，强制死锁
    mylock.unlock();//获取二次加锁后记得再解锁一次
    th.join();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次加锁后，第二次加锁由于抢不到锁，所以该线程会休眠。但是不影响异步调用的函数执行。等到request的异步调用callback得到足够数据了，request进行解锁。这次解锁后可以让线程唤醒然后抢到第二次的锁。所以还要再次解锁。&lt;/p&gt;
&lt;h1&gt;自我检讨&lt;/h1&gt;
&lt;h2&gt;注意不要创建不必要的数据结构。而且使用sort不一定比for循环更慢。&lt;/h2&gt;
&lt;p&gt;力扣 49&lt;/p&gt;
&lt;p&gt;我的睿智解法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; groupAnagrams(vector&amp;lt;string&amp;gt;&amp;amp; strs) {
        unordered_map&amp;lt;string, vector&amp;lt;string&amp;gt;&amp;gt; my_map;
        vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; ret;
        map&amp;lt;char, int&amp;gt; tempmap;
        for (int i = 0; i &amp;lt; strs.size(); i++) {
            int sum = 0;
            for (int j = 0; j &amp;lt; strs[i].size(); j++) {
                tempmap[strs[i][j]]++;
            }

            string key;
            for (auto&amp;amp; i : tempmap) {

                key+= i.first + to_string(i.second);
            }
            my_map[key].push_back(strs[i]);
            tempmap.clear();
        }

        vector&amp;lt;string&amp;gt; temp;
        for (auto&amp;amp; i : my_map) {
            for (auto&amp;amp; j : i.second) {
                temp.emplace_back(j);
            }
            ret.emplace_back(temp);
            temp.clear();
        }
        return ret;
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;我们完全没有必要再创建一个map来储存如&lt;code&gt;a1b2c3&lt;/code&gt;这种key。也完全没有必要按照字符逐个遍历。更没有必要做字符串拼接和字符串比较。一眼望去全是无用功。&lt;/li&gt;
&lt;li&gt;我们完全没有必要在放入结果集的时候再创建临时vector。我们map里面存的值本来就是vector，拿来就用不就行了？为什么要拿出来再放回去？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确解法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; groupAnagrams(vector&amp;lt;string&amp;gt;&amp;amp; strs) {
        unordered_map&amp;lt;string, vector&amp;lt;string&amp;gt;&amp;gt; my_map;
        vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; ret;
        for (int i = 0; i &amp;lt; strs.size(); i++) {
            string tempkey = strs[i];
            sort(tempkey.begin(), tempkey.end());
            my_map[tempkey].push_back(strs[i]);
        }

        for(auto t = my_map.begin(); t != my_map.end(); t++){
            ret.emplace_back(t-&amp;gt;second);
        }
        return ret;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非常优雅。&lt;/p&gt;
&lt;p&gt;首先，对字符串进行排序。这里排序一定会比 逐个字符遍历 + 字符串拼接 + 放入新的map里更快。也就是我们换一种哈希方式。不再采用统计数字的方式。因为所有的字母异位词（长度相同）（&lt;code&gt;dll&lt;/code&gt;和&lt;code&gt;ldll&lt;/code&gt;并不是有效的一组异位词) 在给字母排序后一定是一样的。&lt;/p&gt;
&lt;p&gt;所以这样排序后的字符串完全可以当做key。我们可以直接存入。&lt;/p&gt;
&lt;p&gt;第二点，我们map的值本来就是vector，直接置入结果数组就行了。不需要再拿出变量再创建临时数组再放进去。&lt;/p&gt;
</content:encoded></item><item><title>设计模式</title><link>https://sterben-01.github.io/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</guid><pubDate>Wed, 05 Oct 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;设计模式&lt;/h1&gt;
&lt;h1&gt;创建型模式部分&lt;/h1&gt;
&lt;p&gt;创建型模式主要关注与对象的创建。通常这类模式被用于创建对象的时候。比如抽象工厂被用于创建产品族，原型模式被用来模拟更自由的深拷贝等等。&lt;/p&gt;
&lt;h1&gt;简单工厂模式&lt;/h1&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;设计一个抽象产品类，它包含一些产品类公共方法的实现;&lt;/li&gt;
&lt;li&gt;从抽象产品类中派生出多个具体产品类，如篮球类、足球类、排球类，具体产品类中实现具体产品生产的相关代码;&lt;/li&gt;
&lt;li&gt;设计一个工厂类，工厂类中提供一个生产各种产品的工厂方法，该方法根据传入参数（产品名称)创建不同的具体产品类对象;。客户只需调用工厂类的工厂方法，并传入具体产品参数，即可得到一个具体产品对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;工厂(Factory)︰根据客户提供的具体产品类的参数，创建具体产品实例&lt;/li&gt;
&lt;li&gt;抽象产品(AbstractProduct):具体产品类的基类，包含创建产品的公共方法。可以做为抽象类（不提供实现）&lt;/li&gt;
&lt;li&gt;具体产品(ConcreteProduct):抽象产品的派生类，包含具体产品特有的实现方法，是简单工厂模式的创建目标。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;p&gt;每次新增具体产品，不仅要新增具体产品类，而且要更改工厂类。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简单工厂模式中，工厂只有一个。
&lt;ul&gt;
&lt;li&gt;工厂生产所有种类的产品&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;工厂类的返回值是一个指向具体产品类的基类指针以满足多态调用。&lt;/li&gt;
&lt;li&gt;工厂类中依据传入参数来调用对应的具体产品类。&lt;/li&gt;
&lt;li&gt;抽象产品类可以做为抽象类也可以做为普通类。&lt;/li&gt;
&lt;li&gt;具体产品类中进行具体产品的实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;工厂方法模式&lt;/h1&gt;
&lt;p&gt;代码参考&lt;code&gt;factory.cpp&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;设计一个抽象产品类，它包含一些产品类公共方法的实现;&lt;/li&gt;
&lt;li&gt;从抽象产品类中派生出多个具体产品类，如篮球类、足球类、排球类，具体产品类中实现具体产品生产的相关代码;&lt;/li&gt;
&lt;li&gt;设计一个抽象工厂类。是所有生产具体产品的工厂类的基类。提供工厂类的公共方法。&lt;/li&gt;
&lt;li&gt;从抽象工厂类中派生出多个具体工厂类，具体工厂类中提供一个生产特定具体产品的方法，该方法可以返回一个具体产品类对象的指针。&lt;/li&gt;
&lt;li&gt;使用时声明抽象工厂类指针，new一个具体工厂类的对象。让抽象工厂类指针指向具体工厂类对象。通过该指针调用具体工厂类的生产方法拿到一个指向具体产品类的抽象产品类指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;抽象工厂(AbstractFactory):所有生产具体产品的工厂类的基类，提供工厂类的公共方法;&lt;/li&gt;
&lt;li&gt;具体工厂(ConcreteFactory) : 生产具体的产品&lt;/li&gt;
&lt;li&gt;抽象产品(AbstractProduct):所有产品的基类，提供产品类的公共方法。&lt;/li&gt;
&lt;li&gt;具体产品(ConcreteProduct) ︰具体的产品类&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;p&gt;虽然我们这次不需要修改具体工厂类，只需要添加具体工厂类即可。但是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这样做增加系统复杂度，因为类的数量变多了。&lt;/li&gt;
&lt;li&gt;同时，由于工厂和产品都有抽象类，而且调用中也需要拿到对应的抽象类类型的指针，导致增加理解难度。降低代码可读性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;我们这次拥有了抽象工厂类，这样我们就可以有多个不同的具体工厂类，让每一个工厂类都生产特定类别的具体产品。&lt;/li&gt;
&lt;li&gt;抽象工厂类是抽象类。不提供具体实现。调用的时候需要声明一个抽象工厂类的指针指向某一个具体工厂
&lt;ul&gt;
&lt;li&gt;这里的意思是必须要指明我们需要哪种工厂。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拿到具体工厂类指针后，可以通过具体工厂类的生产方法得到一个指向具体产品类的抽象产品类指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工厂方法模式把产品的具体创建过程延迟到了具体工厂类中。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;抽象工厂模式&lt;/h1&gt;
&lt;p&gt;代码参考&lt;code&gt;abstractfactory.cpp&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;和工厂方法模式的区别&lt;/h2&gt;
&lt;p&gt;抽象工厂和工厂方法非常相似。唯一的区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在工厂方法中，我们每一个具体工厂类只能生产一个特定的具体产品。&lt;/li&gt;
&lt;li&gt;在抽象工厂中，每一个具体工厂可以生产多种同类相关的产品。也就是一个产品族&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在代码中，我们的产品族指的是足球产品族和篮球产品族。每一个产品族包含球和球衣。&lt;/p&gt;
&lt;p&gt;也就是说，抽象工厂模式是为了&lt;strong&gt;创建一组（有多类）相关或依赖的对象&lt;/strong&gt;提供创建接口，而 工厂模式是为&lt;strong&gt;一类对象&lt;/strong&gt;提供创建接口或延迟对象的创建到子类中实现。&lt;/p&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;p&gt;这就是所谓的抽象工厂的开闭原则的倾斜性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;增加产品族：对于增加新的产品族，工厂方法模式很好的支持了“开闭原则”，对于新增加的产品族，只需要对应增加一个新的具体工厂即可，对已有代码无须做任何修改。假设我们新增排球产品族，只需要新增工厂即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;增加新的产品具体内容：对于增加新的产品具体内容，需要修改所有的工厂角色，包括抽象工厂类，在所有的工厂类中都需要增加生产新产品的方法，不能很好地支持“开闭原则”。比如我们现在需要特定的能量饮料，就需要在所有工厂类中新增一个能量饮料的产品。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当抽象工厂模式中每一个具体工厂类只创建一个产品对象，也就是只存在一个产品具体内容时，抽象工厂模式退化成工厂方法模式；当工厂方法模式中抽象工厂与具体工厂合并，提供一个统一的工厂来创建产品对象，并将创建对象的工厂方法设计为静态方法时，工厂方法模式退化成简单工厂模式。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;工厂模式有非常高的自由度。比如可以把工厂写成嵌套类，然后定义一个嵌套类内的静态变量以达到隐藏嵌套工厂类的效果。也更加清晰明确某些类的对象就是必须使用工厂类来创建。也可以把创建方法写成lambda放入容器。总而言之，工厂模式的核心思想就是帮助我们创建对象，管理对象的创建方法，提供一个统一的接口。&lt;/p&gt;
&lt;h1&gt;创建者模式（Builder)&lt;/h1&gt;
&lt;p&gt;创建者模式最好的解释方法就是看它的名字。实际应用的场景主要是：在某些对象如果有复杂的创建过程的时候，比如可能需要10个构造函数的参数，或者是像是组成一个HTML文件，这个时候让一个类来进行实现就显得非常不雅。**创建者模式的核心就是让复杂对象的创建和表示（展示）分离开来。**在每一个构造中都可以引入参数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在软件开发中，也存在大量类似汽车一样的复杂对象，它们拥有一系列成员属性，这些成员属性中有些是引用类型的成员对象。而且在这些复杂对象中，还可能存在一些限制条件，如某些属性没有赋值则复杂对象不能作为一个完整的产品使用；有些属性的赋值必须按照某个顺序，一个属性没有赋值之前，另一个属性可能无法赋值等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者我们简单理解为，使用不同的配置文件来使得创建的对象具有不同的参数。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;产品（Product）：具体的产品对象&lt;/li&gt;
&lt;li&gt;抽象建造者（AbstractBuilder）：创建产品（Product）对象的各个部件指定的抽象接口&lt;/li&gt;
&lt;li&gt;具体建造者（ConcreteBuilder）：实现 抽象建造者（AbstractBuilder）的接口，实现各个部件的具体构造方法和装配方法，并返回创建结果。&lt;/li&gt;
&lt;li&gt;指挥者（Director）：构建一个使用抽象建造者（AbstractBuilder）接口的对象，安排复杂对象的构建过程，客户端一般只需要与指挥者交互，指定建造者类型，然后通过构造函数或者setter方法将具体建造者对象传入指挥者。它主要作用是隔离客户与对象的生产过程，并负责控制产品对象的生产过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;参考代码&lt;code&gt;builder.cpp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意在我们的代码中，我们选择让抽象建造者管理一个产品对象（利用RAII）。我们也可以选择不让它管理。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在建造者模式中， &lt;strong&gt;客户端不必知道产品内部组成的细节，将产品本身与产品的创建过程解耦，使得相同的创建过程可以创建不同的产品对象。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;每一个具体建造者都相对独立，而与其他的具体建造者无关，因此可以很方便地替换具体建造者或增加新的具体建造者， &lt;strong&gt;用户使用不同的具体建造者即可得到不同的产品对象&lt;/strong&gt; 。
&lt;ul&gt;
&lt;li&gt;也就是理解为我们说的 “替换配置文件“&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以更加精细地控制产品的创建过程&lt;/strong&gt; 。将复杂产品的创建步骤分解在不同的方法中，使得创建过程更加清晰，也更方便使用程序来控制创建过程。
&lt;ul&gt;
&lt;li&gt;也就是我们建造中拆分出来的步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增加新的具体建造者无须修改原有类库的代码，指挥者类针对抽象建造者类编程，系统扩展方便，符合“开闭原则”。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;具体建造者只需要更换具体实现，就可以当做“配置文件”传入建造者。&lt;strong&gt;指挥者保有一个抽象建造者。也就是指挥者面向抽象建造者编程。我们会发现指挥者调用的就是抽象建造者的纯虚函数。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;建造者模式所创建的产品一般具有较多的共同点，其组成部分相似，如果产品之间的差异性很大，则不适合使用建造者模式，因此其使用范围受到一定的限制。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;假如产品的建造模式有大的区别，比如需要更换抽象建造者，则不适用于这种模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果产品的内部变化复杂，可能会导致需要定义很多具体建造者类来实现这种变化，导致系统变得很庞大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;需要生成的产品对象有复杂的内部结构，这些产品对象通常包含多个成员属性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要生成的产品对象的属性相互依赖，需要指定其生成顺序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类，将创建过程封装在指挥者类中，而不在建造者类中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隔离复杂对象的创建和使用，并使得相同的创建过程可以创建不同的产品。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建造者模式与抽象工厂模式的比较:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象工厂模式相比， &lt;strong&gt;建造者模式返回一个组装好的完整产品&lt;/strong&gt; ，而 &lt;strong&gt;抽象工厂模式返回一系列相关的产品，这些产品拥有不同的具体内容，构成了一个产品族。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在抽象工厂模式中，客户端实例化工厂类，然后调用工厂方法获取所需产品对象，而在建造者模式中，客户端可以不直接调用建造者的相关方法，而是通过指挥者类来指导如何生成对象，包括对象的组装过程和建造步骤，它侧重于一步步构造一个复杂对象，返回一个完整的对象。&lt;/li&gt;
&lt;li&gt;如果将抽象工厂模式看成 &lt;strong&gt;汽车配件生产工厂&lt;/strong&gt; ，生产一个产品族的产品，那么建造者模式就是一个 &lt;strong&gt;汽车组装工厂&lt;/strong&gt; ，通过对部件的组装可以返回一辆完整的汽车。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Builder 模式强调的是一步步创建对象，并通过相同的创建过程可以获得不同的结果对象，一般来说 Builder 模式中对象不是直接返回的。而在 AbstractFactory 模式中对象是直接返回的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们可以增添链式调用以达到fluent builder的效果&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;我们可以和facade模式结合在一起。参考代码&lt;code&gt;facade_builder&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;原型模式 (Prototype)&lt;/h1&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用原型实例指定待创建对象的类型，并且通过复制这个原型来创建新的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原型模式的工作原理是将一个原型对象传给要发动创建的对象（即客户端对象)，这个要发动创建的对象通过请求原型对象复制自己来实现创建过程。&lt;/li&gt;
&lt;li&gt;从工厂方法角度而言，创建新对象的工厂就是原型类自己。&lt;/li&gt;
&lt;li&gt;软件系统中有些对象的创建过程比较复杂，且有时需要频繁创建，原型模式通过给出一个原型对象来指明所要创建的对象的类型，然后用复制这个原型对象的办法创建出更多同类型的对象，这就是原型模式的意图所在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;个人感受：我们通过某一个东西来创建出来一个它的副本。这个看上去很像拷贝构造和拷贝赋值。只不过这个模式是建立在这个基础上的。因为我们可能复制的只是一部分。可以理解为一个支持更加细粒度和更多种类操作的拷贝构造和拷贝赋值。比如拷贝特定部分，替换特定参数等等。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Abstract Prototype: 抽象原型类
&lt;ul&gt;
&lt;li&gt;声明一个clone自身的接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Concrete Prototype: 具体原型类
&lt;ul&gt;
&lt;li&gt;实现clone接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Client: 客户端类
&lt;ul&gt;
&lt;li&gt;客户端中声明一个抽象原型类，根据客户需求clone具体原型类对象实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;参考代码。里面设计有点问题。理论上应该直接使用并且返回抽象原型类指针。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;当创建新的对象实例较为复杂时，原型模式可以简化创建过程，提高创建对象的效率;&lt;/li&gt;
&lt;li&gt;可扩展: 模式中提供了抽象原型类，具体原型类可适当扩展;&lt;/li&gt;
&lt;li&gt;创建结构简单: 创建工厂即为原型对象本身&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;深克隆代码较为复杂&lt;/li&gt;
&lt;li&gt;每一个类都得配备一个clone方法，且该方法位于类的内部，修改时违背开闭原则;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;适用环境：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当创建新的对象实例较为复杂时，原型模式可以简化创建过程;&lt;/li&gt;
&lt;li&gt;结合优点第3条，需要避免使用分层次的工厂类来创建分层次的对象，并且类的实例对象只有一个或很少几个的组合状态，通过复制原型对象得到新实例，比通过使用构造函数创建一个新实例会更加方便。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原型模式可以理解为一个支持更加细粒度和更多种类操作的拷贝构造和拷贝赋值。比如拷贝特定部分，替换特定参数等等。&lt;strong&gt;但是尤其要注意深拷贝和浅拷贝问题。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;在多态类中尤其要使用原型模式，或者相似的方法实现拷贝接口。&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;因为我们在&lt;code&gt;vptr&lt;/code&gt;中提到过，如果我们想通过一个父类指针获取指向的子类对象的拷贝，直接解引用调用的拷贝构造是父类的。因为解引用后元素的类型依靠的是静态类型，所以提取出来的元素类型是父类类型的引用。所以此时无法实现多态。因为你会发现&lt;code&gt;new&lt;/code&gt;一个子类的时候没有一个对应的拷贝构造。所以这时候你只能使用类型转换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct a {
    a() = default;
    virtual void f() {
        cout &amp;lt;&amp;lt;&quot;A&quot; &amp;lt;&amp;lt; endl;
    }
    a(const a&amp;amp;) {
        cout &amp;lt;&amp;lt; &quot;a copy&quot; &amp;lt;&amp;lt; endl;
    }
};

struct b :public a {
    b() = default;
    virtual void f() {
        cout &amp;lt;&amp;lt;&quot;B&quot; &amp;lt;&amp;lt; endl;
    }
    b(const b&amp;amp;) {
        cout &amp;lt;&amp;lt; &quot;b copy&quot; &amp;lt;&amp;lt; endl;
    }
};
int main() {
    a* p = new b;
    a* err = new b(*p); //不可以。因为*p是a&amp;amp;类型。除非你在b新建一个构造函数，如 b(const a&amp;amp;)
    a* p1 = new b(*dynamic_cast&amp;lt;b*&amp;gt;(p)); //只能先转成b。这样会调用b的拷贝构造。输出b copy
    p-&amp;gt;f();
    p1-&amp;gt;f();
    delete p;
    delete p1;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这时候使用原型模式实现克隆接口就会非常顺畅：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct a {
    a() = default;
    virtual void f() {
        cout &amp;lt;&amp;lt;&quot;A&quot; &amp;lt;&amp;lt; endl;
    }
    a(const a&amp;amp;) {
        cout &amp;lt;&amp;lt; &quot;a copy&quot; &amp;lt;&amp;lt; endl;
    }
    virtual a* clone(){ //克隆接口
        return new a;
    }
};

struct b :public a {
    b() = default;
    virtual void f() {
        cout &amp;lt;&amp;lt;&quot;B&quot; &amp;lt;&amp;lt; endl;
    }
    b(const a&amp;amp;){
        cout &amp;lt;&amp;lt;&quot;wtf&quot; &amp;lt;&amp;lt; endl;
    }
    b(const b&amp;amp;) {
        cout &amp;lt;&amp;lt; &quot;b copy&quot; &amp;lt;&amp;lt; endl;
    } 
    virtual a* clone(){ //克隆接口
        return new b;
    }
};

int main() {
    a* p = new b;
    a* pclone = p-&amp;gt;clone(); //克隆
    p-&amp;gt;f();
    pclone-&amp;gt;f(); //正确多态

    delete p;
    delete pclone;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;结构型模式部分&lt;/h1&gt;
&lt;p&gt;结构型模式主要是用于设计更合理的API。也就是如何组织整个代码的结构。比如类的组织，继承，包含。哪些接口应该开放，对谁开放。这一部分中，很多模式是一种对底层类接口的包装。目的是简化使用，隐藏具体细节等等。&lt;/p&gt;
&lt;h1&gt;适配器模式 (Adapter)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;类适配器中，适配器类通过继承适配者类，并重新实现适配者的具体接口来达到适配客户所需要的接口的目的。
&lt;ul&gt;
&lt;li&gt;从图中我们可以看到，类适配器不仅要继承自目标抽象类，同时也要继承自所有的适配者类。所以类适配器需要多重继承的支持。所以如Java就不适用于这种方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对象适配器中，适配器类通过在类中实例化一个适配者类的对象，并将其封装在客户所需功能的接口里，达到最终的适配目的。
&lt;ul&gt;
&lt;li&gt;我们的代码是对象适配器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230320011708.png&quot; alt=&quot;QQ截图20230320011708&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Target：目标抽象类&lt;/li&gt;
&lt;li&gt;Adapter：适配器类&lt;/li&gt;
&lt;li&gt;Adaptee：适配者类&lt;/li&gt;
&lt;li&gt;Client：客户类 （可以简化成调用对象）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创建target类。这个类是抽象基类。&lt;/li&gt;
&lt;li&gt;创建adapter类。adapter类新增接口。调用所需的适配者类。&lt;/li&gt;
&lt;li&gt;客户通过adapter类对象调用adapter类暴露出来的接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结：&lt;/h2&gt;
&lt;p&gt;个人理解：当我们有一个任务需要调用多个类。我们就可以新增一个目标抽象类。然后适配器类继承自该目标抽象类。适配器类负责调用所需的适配者类，&lt;strong&gt;也就是假如我们需要调用ABCDE五个类来完成这个任务。然后五个类关联不是很大，但是想让他们一起工作。我们就可以新增一个类。在这个类里面去调用ABCDE这五个类。我们外部客户只需要调用这个适配器类即可完成任务。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适配器模式和外观模式的核心区别&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;适配器模式的前提是你无法改变已经既有的类的需求。简单例子就是参数不同，类型不同等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是适用于&lt;strong&gt;想使用一个已经存在了的接口，而这个接口却不符合你的需求，此时就可以考虑使用适配器模式。&lt;strong&gt;所以是&lt;/strong&gt;事后设计（擦屁股）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适配器模式的核心是转换。也就是包了一层的wrapper&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;外观模式是你自己设计的时候，想要提供一个整洁一致的（看起来简洁的）接口给调用端。&lt;strong&gt;强调是事前设计&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;外观模式的核心是提供统一接口给客户端。注意是提供给客户端。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;STL中的栈，队列，优先队列就是适配器模式的典范。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230514175903.png&quot; alt=&quot;QQ截图20230514175903&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们在杂记5中提到的&lt;code&gt;pmr&lt;/code&gt;多态资源分配器，是&lt;code&gt;memory_resource&lt;/code&gt;的适配器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230514175956.png&quot; alt=&quot;QQ截图20230514175956&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;外观模式（Facade)&lt;/h1&gt;
&lt;p&gt;核心的个人理解：一个操作需要调用五个函数，太麻烦了，我们提供统一接口。这个接口内调用五个函数，但是客户只需要调用接口。&lt;/p&gt;
&lt;p&gt;核心区别上面写过了。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一个外观类（接口）&lt;/li&gt;
&lt;li&gt;一堆你需要调用函数的类。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;增加或者减少子系统，必须要修改外观类。违反开闭原则。&lt;/li&gt;
&lt;li&gt;客户端可以直接与子系统交互。如果加强限制则使系统灵活性降低。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要试图通过外观类为子系统增加新行为&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;不要通过继承一个外观类在子系统中加入新的行为，这种做法是错误的。&lt;strong&gt;外观模式的用意是为子系统提供一个集中化和简化的沟通渠道，而不是向子系统加入新的行为&lt;/strong&gt;，新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现，不能通过外观类来实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可以考虑新增抽象外观类来一定程度上缓解违背开闭原则的情况。&lt;/li&gt;
&lt;li&gt;在一个系统中，通常情况下，一个外观类只需要一个实例。所以可以考虑将其实现为单例。当然这并不意味着在整个系统里只能有一个外观类，在一个系统中可以设计多个外观类，每个外观类都负责和一些特定的子系统交互，向用户提供相应的业务功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;享元模式 (Flyweight)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;细粒度对象的大面积复用。Flyweight 模式在实现过程中主要是要为共享对象提供一个存放的“仓库”（对象（享元）池）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一个系统在运行时创建太多相同或者相似的对象，会占用大量内存和资源，降低系统性能。享元模式通过共享技术实现相同或相似的细粒度对象的复用，提供一个享元池存储已经创建好的对象，并通过享元工厂类将享元对象提供给客户端使用。&lt;/li&gt;
&lt;li&gt;运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象，而这些对象都很相似，状态变化很小，可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象，因此它又称为轻量级模式，它是一种对象结构型模式。&lt;/li&gt;
&lt;li&gt;例子1：我们有一万颗树。但是这些树的纹理，叶子的种类等等只有那么固定的几种，变化的部分不过是位置，和具体使用的种类而已。&lt;/li&gt;
&lt;li&gt;例子2：围棋只有两种颜色。棋子与棋子之间的区别除了颜色和位置，没什么不同。也就是说，每个棋子对象的大部分状态都是一样的（形状、材料、质地等）。如果我们要设计一个程序来实现下围棋的功能，该如何来创建或者表示这上百个棋子对象呢?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230324145947.png&quot; alt=&quot;QQ截图20230324145947&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;FlyWeight： 抽象享元类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明了具体享元类的公共方法。比如提供内部状态数据（比如例子里的名字）和设置（注入）外部状态（比如例子里的端口和IP）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ConcreteFlyWeight: 共享具体享元类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;里面会含有一些具体享元类中应有的数据和方法。为内部状态提供存储空间。比如例子中的具体享元类自己的名字。&lt;/li&gt;
&lt;li&gt;一般情况下每个享元类对象只应被创建一次。所以应该是单例模式。但是例子中的代码懒得写单例了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UnsharedConcreteFlyweight: 非共享具体享元类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不是所有抽象享元类的子类都需要被共享。可以把不全部共享的子类设计为非共享具体享元类。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FlyweightFactory: 享元工厂类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建并管理具体享元对象&lt;/strong&gt;。将各种具体享元类对象储存在一个享元池中。享元池一般设计为一个储存键值对的容器。可以结合工厂模式设计。客户需要某个享元对象时，如果已有该实例，则直接返回该实例。如果没有该实例，则新建一个实例然后返回给客户，同时&lt;strong&gt;放入享元池&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;具体参考代码。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;享元模式的优点在于它可以极大减少内存中对象的数量，使得相同对象或相似对象在内存中只保存一份。&lt;/li&gt;
&lt;li&gt;享元模式的外部状态相对独立，而且不会影响其内部状态，从而使得享元对象可以在不同的环境中被共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;享元模式使得系统更加复杂，需要分离出内部状态和外部状态，这使得程序的逻辑复杂化。&lt;/li&gt;
&lt;li&gt;为了使对象可以共享，享元模式需要将享元对象的状态外部化，而读取外部状态使得运行时间变长。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;在以下情况下可以使用享元模式：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个系统有大量相同或者相似的对象，由于这类对象的大量使用，造成内存的大量耗费。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象的大部分状态都可以外部化，可以将这些外部状态传入对象中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用享元模式需要维护一个存储享元对象的享元池，而这需要耗费资源，因此，应当在多次重复使用享元对象时才值得使用享元模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;享元模式在编辑器软件中大量使用，如在一个文档中多次出现相同的图片，则只需要创建一个图片对象，通过在应用程序中设置该图片出现的位置，可以实现该图片在不同地方多次重复显示。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;单纯享元模式和复合享元模式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单纯享元模式：在单纯享元模式中，所有的享元对象都是可以共享的，即所有抽象享元类的子类都可共享，不存在非共享具体享元类。&lt;/li&gt;
&lt;li&gt;复合享元模式：将一些单纯享元使用组合模式加以组合，可以形成复合享元对象，这样的复合享元对象本身不能共享，但是它们可以分解成单纯享元对象，而后者则可以共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;享元模式与其他模式的联用&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象，使用简单工厂模式来生成享元对象。&lt;/li&gt;
&lt;li&gt;在一个系统中，通常只有唯一一个享元工厂，因此享元工厂类可以使用单例模式进行设计。&lt;/li&gt;
&lt;li&gt;享元模式可以结合组合模式形成复合享元模式，统一对享元对象设置外部状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;桥接模式（bridge）&lt;/h1&gt;
&lt;p&gt;桥接模式简而言之就是两个独立的继承体系，通过“桥接”的方式结合在一起。&lt;/p&gt;
&lt;p&gt;第一个例子：&lt;/p&gt;
&lt;p&gt;设想如果要绘制矩形、圆形、椭圆、正方形，我们至少需要4个形状类，但是如果绘制的图形需要具有不同的颜色，如红色、绿色、蓝色等，此时至少有如下两种设计方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种设计方案是为每一种形状都提供一套各种颜色的版本。&lt;/li&gt;
&lt;li&gt;第二种设计方案是根据实际需要对形状和颜色进行组合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于有两个变化维度（即两个变化的原因）的系统，采用方案二来进行设计系统中类的个数更少，且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系，从而降低了类与类之间的耦合，减少了代码编写量。&lt;/p&gt;
&lt;p&gt;第二个例子：&lt;/p&gt;
&lt;p&gt;设想如果有两台手机，我们至少需要2个形状类，但是如果手机需要具有不同的应用程序，此时至少有如下两种设计方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种设计方案是为每一台手机都提供一套安装了各个程序的版本。&lt;/li&gt;
&lt;li&gt;第二种设计方案是根据实际需要对手机和程序进行组合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;桥接模式(Bridge Pattern)：将抽象部分与它的实现部分分离，使它们都可以独立地变化。它是一种对象结构型模式，又称为柄体(Handle and Body)模式或接口(Interface)模式。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230216032436.png&quot; alt=&quot;QQ截图20230216032436&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过这张图我们能非常清楚地看到两个独立的继承体系通过桥接方式组合在一起。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Abstraction(抽象类)&lt;/strong&gt;︰定义抽象类的接口（抽象接口)，由聚合关系可知，&lt;strong&gt;抽象类中包含一个Implementor类型的对象&lt;/strong&gt;，它与Implementor之间有&lt;strong&gt;关联关系&lt;/strong&gt;，既可以包含抽象业务方法，也可以包含具体业务方法;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;lmplementor(实现类接口)&lt;/strong&gt;︰定义实现类的接口，这个接口可以与Abstraction类的接口不同一般而言，实现类接口只定义基本操作，而抽象类的接口还可能会做更多复杂的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RefinedAbstraction (扩充抽象类)&lt;/strong&gt;︰具体类，实现在抽象类中定义的接口，可以调用在lmplementor中定义的方法;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Concretelmplementor(具体实现类)&lt;/strong&gt;︰具体实现了Implementor接口，在不同的具体实现类中实现不同的具体操作。运行时Concretelmplementor将替换父类。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;简言之，在Abstraction类中维护一个Implementor类指针，需要采用不同的实现方式的时候只需要传入不同的Implementor派生类就可以了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;查看代码。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分离抽象接口与实现部分&lt;/strong&gt;，使用对象间的关联关系使抽象与实现解耦;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;桥接模式可以取代多层继承关系&lt;/strong&gt;，多层继承违背单一职责原则，不利于代码复用;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;桥接模式提高了系统可扩展性&lt;/strong&gt;，某个维度需要扩展只需增加实现类接口或者具体实现类，而且不影响另一个维度，符合开闭原则。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;桥接模式难以理解，因为关联关系建立在抽象层，需要一开始就设计抽象层&lt;/li&gt;
&lt;li&gt;如何准确识别系统中的两个维度是应用桥接模式的难点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;个人感受这个东西非常像一种组合。但是注意不是组合模式。这是两个不同的东西。重点需要理解如何将抽象化(Abstraction)与实现化(Implementation)脱耦，使得二者可以独立地变化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象化：抽象化就是忽略一些信息，把不同的实体当作同样的实体对待。在面向对象中，将对象的共同性质抽取出来形成类的过程即为抽象化的过程。&lt;/li&gt;
&lt;li&gt;实现化：针对抽象化给出的具体实现，就是实现化，抽象化与实现化是一对互逆的概念，实现化产生的对象比抽象化更具体，是对抽象化事物的进一步具体化的产物。&lt;/li&gt;
&lt;li&gt;脱耦：脱耦就是将抽象化和实现化之间的耦合解脱开，或者说是将它们之间的强关联改换成弱关联，将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦，就是指在一个软件系统的抽象化和实现化之间使用关联关系（组合或者聚合关系）而不是继承关系，从而使两者可以相对独立地变化，这就是桥接模式的用意。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在以下情况下可以使用桥接模式：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性，避免在两个层次之间建立静态的继承联系，通过桥接模式可以使它们在抽象层建立一个关联关系。&lt;/li&gt;
&lt;li&gt;抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响，在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合，即系统需要对抽象化角色和实现化角色进行动态耦合。&lt;/li&gt;
&lt;li&gt;一个类存在两个独立变化的维度，且这两个维度都需要进行扩展。&lt;/li&gt;
&lt;li&gt;虽然在系统中使用继承是没有问题的，但是由于抽象化角色和具体化角色需要独立变化，设计要求需要独立管理这两者。&lt;/li&gt;
&lt;li&gt;对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统，桥接模式尤为适用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;适配器模式与桥接模式的联用:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;桥接模式和适配器模式用于设计的不同阶段，桥接模式用于系统的初步设计，对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色，使它们可以分别进行变化；而在初步设计完成之后，当发现系统与已有类无法协同工作时，可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式，特别是那些涉及到大量第三方应用接口的情况。
&lt;ul&gt;
&lt;li&gt;换句话说，类无法协同工作的意思是各个类的所需的参数之类的都不同，很难组合到一起工作，就可以使用适配器模式，给这些类外面包一层。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以我们看到，两个不同的类，抽象和实现可以沿着自己的维度独立的变化。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;装饰模式 （Decorator）&lt;/h1&gt;
&lt;p&gt;装饰模式的核心思想是：不通过继承，而是通过组合的方式来进行对象功能的扩展。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230321050649.png&quot; alt=&quot;QQ截图20230321050649&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Component（抽象构件）︰是具体构件类和装饰类的共同基类，声明了在具体构件中定义的方法，客户端可以一致的对待使用装饰前后的对象&lt;/li&gt;
&lt;li&gt;ConcreteComponent（具体构件）︰具体构件定义了构件具体的方法，装饰类可以给它增加更多的功能;&lt;/li&gt;
&lt;li&gt;Decorator（抽象装饰类）∶用于给具体构件增加职责，但具体职责在其子类中实现。&lt;strong&gt;抽象装饰类通过聚合关系蕴含一个抽象构件的对象，通过该对象可以调用装饰之前构件的方法，并通过其子类扩展该方法，达到装饰的目的&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;ConcreteDecorator（具体装饰类）︰向构件增加新的功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子就是我们假设现在有手机，我们需要给手机贴膜，加手机壳，加挂绳。我们如果不通过继承的方式而是通过组合的方式实现就可以使用装饰模式。&lt;/p&gt;
&lt;p&gt;在这里我们的代码中，手机和抽象装饰类共同继承抽象构件接口。同时，抽象装饰类保有一个抽象构件的对象。为什么既要继承又要包含呢？查看我们的项目代码&lt;code&gt;decorate.cpp&lt;/code&gt;，我们发现在main中，我们的每一个指针，无论是语义上的具体构件，还是具体装饰类，我们统一赋值给了抽象构件类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这样做的第一个好处是我们可以把装饰器类的指针当成&lt;code&gt;component&lt;/code&gt;的指针。也就是把装饰器对象当做一个&lt;code&gt;component&lt;/code&gt;对象去使用。比较符合语义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二点是，不继承的话不符合链式装饰语义。因为每一个具体装饰器类都是接受&lt;code&gt;component&lt;/code&gt;来构造。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而链式装饰要求把每一个装饰过的装饰器指针当做&lt;code&gt;component&lt;/code&gt;传入。如果不继承&lt;code&gt;component&lt;/code&gt;的话，无法当做&lt;code&gt;component&lt;/code&gt;传入。因为没有多态性。比如115行的&lt;code&gt;Decorator *PhoneRope = new DecoratorRope(PhoneSticker2);&lt;/code&gt;。这是一种递归的函数调用。每次我们先&lt;code&gt;getComponent&lt;/code&gt;，所以先拿到贴纸的装饰器对象，然后调用对应的函数调用运算符。然后在里面继续拿，拿到了手机壳的装饰器对象，然后继续调用对应的函数调用运算符，直到拿到了&lt;code&gt;component&lt;/code&gt;类对象，这时候不拿了，直接调用。然后再依次弹出。&lt;/p&gt;
&lt;h2&gt;和模板结合以产生静态装饰&lt;/h2&gt;
&lt;p&gt;我们有没有发现上面的模式有一个缺点？因为具体构件和抽象装饰类都是继承自抽象构件。所以说他们俩的共同接口就是抽象构建提供的几个接口。假设具体构件有一些自己独特的函数，我们应该怎么做？&lt;strong&gt;可以采用&lt;code&gt;Mixin&lt;/code&gt;方法&lt;/strong&gt;搭配完美转发。这个例子有点复杂并且和我们上面的例子不太一样。因为使用了&lt;code&gt;Mixin&lt;/code&gt;，所以抽象装饰类不见了。每一个具体装饰类都继承自模板参数&lt;code&gt;T&lt;/code&gt;，而且这个&lt;code&gt;T&lt;/code&gt;不再是抽象构件，而是具体构件了。因为我们不再依赖于接口的动态。改为依赖模板的静态了。同时，由于我们继承的是&lt;code&gt;T&lt;/code&gt;类，也就是具体构件，所以我们必须使用完美转发来直接调用&lt;code&gt;T&lt;/code&gt;类的构造函数。同时，如果我们的具体装饰类有一些自己的参数需要传入，也很方便实现，只需要拆分可变参数就可以了。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;装饰模式与继承关系的目的都是要扩展对象的功能，但是装饰模式可以提供比继承更多的灵活性。&lt;/li&gt;
&lt;li&gt;可以通过一种&lt;strong&gt;动态的方式来扩展一个对象的功能&lt;/strong&gt;，通过配置文件可以在运行时选择不同的装饰器，从而实现不同的行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通过使用不同的具体装饰类以及这些装饰类的排列组合，可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象，也就是堆一个对象进行多次装饰以得到功能更为强大的对象。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具体构件类与具体装饰类可以独立变化&lt;/strong&gt;，用户可以根据需要增加新的具体构件类和具体装饰类，在使用时再对其进行组合，原有代码无须改变，&lt;strong&gt;符合“开闭原则”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用装饰模式进行系统设计时将产生很多小对象，这些对象的区别在于它们之间相互连接的方式有所不同，而不是它们的类或者属性值有所不同，同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度，加大学习与理解的难度。&lt;/li&gt;
&lt;li&gt;这种比继承更加灵活机动的特性，也同时意味着装饰模式比继承更加易于出错，排错也很困难，对于多次装饰的对象，调试时寻找错误可能需要逐级排查，较为烦琐。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在不影响其他对象的情况下，以动态、透明的方式给单个对象添加职责。&lt;/li&gt;
&lt;li&gt;需要动态地给一个对象增加功能，这些功能也可以动态地被撤销。&lt;/li&gt;
&lt;li&gt;当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类：第一类是系统中存在大量独立的扩展，为支持每一种组合将产生大量的子类，使得子类数目呈爆炸性增长；第二类是因为类定义不能继承（如final类）.&lt;/li&gt;
&lt;li&gt;一个装饰类的接口必须与被装饰类的接口保持相同，对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待。&lt;/li&gt;
&lt;li&gt;尽量保持具体构件类Component作为一个“轻”类，也就是说不要把太多的逻辑和状态放在具体构件类中，可以通过装饰类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对其进行扩展。 - 如果只有一个具体构件类而没有抽象构件类，那么抽象装饰类可以作为具体构件类的直接子类。&lt;/p&gt;
&lt;h1&gt;组合模式  (Composite)&lt;/h1&gt;
&lt;p&gt;组合模式的核心在于解决&lt;strong&gt;树状组合结构的管理&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230323000818.png&quot; alt=&quot;QQ截图20230323000818&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Component（抽象构件）: Component是一个抽象类，定义了构件的一些公共接口，这些接口是管理或者访问它的子构件的方法(如果有子构件)，具体的实现在叶子构件和容器构件中进行。&lt;/li&gt;
&lt;li&gt;Leaf（叶子构件）︰它代表树形结构中的叶子节点对象，叶&lt;strong&gt;子构件本身在一般情况下没有子节点，它是我们需要通过树状组织来管理的具体节点&lt;/strong&gt;。叶子构件它实现了在抽象构件中定义的行为。对于抽象构件定义的管理子构件的方法，叶子构件可以通过抛出异常、提示错误等方式进行处理。&lt;/li&gt;
&lt;li&gt;Composite（容器构件）︰容器构件在一方面具体实现公共接口，另一方面通过聚合关系包含子构件，子构件可以是容器构件，也可以是叶子构件。&lt;strong&gt;也就是说，容器构件一般来说会有一个&lt;code&gt;vector&lt;/code&gt;或其他的容器来储存所有的&lt;code&gt;Component*/&amp;amp;&lt;/code&gt;对象。因为容器构件本身也继承自&lt;code&gt;Component&lt;/code&gt;，所以容器构件可以包含容器构件，形成一种嵌套，也就是树状的关系。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;之所以区分安全组合模式和透明组合模式，是因为叶子构件不应该具有如添加，移除，获得叶子结点的操作。透明模式的优点是可以通过抽象构件接口获知全部动作，客户端可以一致的对待所有对象。缺点是可能会无意间调用叶子构件类的几个不应提供的函数。所以需要妥善处理。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而安全组合模式中，我们就不能通过抽象构件接口获知全部动作，客户端就要进行区别对待。尽管较为安全，但是不够透明。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;清楚地定义分层次的复杂对象，表示出复杂对象的层次结构，让客户端忽略层次的差异。&lt;/li&gt;
&lt;li&gt;客户端可以一致地使用层次结构中各个层次的对象，而不必关心其具体构件的行为如何实现。&lt;/li&gt;
&lt;li&gt;在组合模式中增加新的叶子构件和容器构件非常方便，易于扩展，符合开闭原则。&lt;/li&gt;
&lt;li&gt;为树形结构的案例提供了解决方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;子构件或容器构件的行为受限制，因为它们来自相同的抽象层&lt;/strong&gt;。如果要定义某个容器或者某个叶子节点特有的方法，那么要求在运行时判断对象类型，增加了代码的复杂度。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;此处可以使用CRTP方法来实现所谓的定义某个容器或叶子结点特有的方法。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技巧&lt;/h2&gt;
&lt;p&gt;有时候我们可以把单个对象当成一个组合（容器对待），方法就是提供&lt;code&gt;begin()&lt;/code&gt;和&lt;code&gt;end()&lt;/code&gt;函数。&lt;/p&gt;
&lt;p&gt;具体实现可以像是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Foo* begin(){
    return this;
}
Foo* end(){
    return this+1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，这个单个对象会表现的和含有一个对象的容器一样。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;组合模式的核心在于解决&lt;strong&gt;树状组合结构的管理&lt;/strong&gt;。在系统中需要用到树形结构和系统中能够分离出容器节点和叶子节点的时候比较适合使用。在具有整体和部门的层次结构中，能够通过某种方式忽略层次差异，使得客户端可以一致对待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;组合模式通过和装饰模式有着类似的结构图，但是组合模式旨在构造类，而装饰模式重在不生成子类即可给对象添加职责。装饰模式重在修饰，而组合模式重在表示&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;代理模式 （Proxy)&lt;/h1&gt;
&lt;p&gt;我们曾经在MoreEffectiveC++中的条款30曾经提到过代理类。那是代理模式的一种之一。我们在这里讨论一些比较宽泛，简单，易于理解的代理模式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代理模式的核心是创造出一个真实对象的替身，在客户端眼里，代理对象的行为和真实对象应该是一模一样的。而在幕后，可以进行一些功能的附加。比如说记录日志，增加引用计数等等。所以我们最常见的智能指针就是代理模式的一个经典例子。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230325030611.png&quot; alt=&quot;QQ截图20230325030611&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象主题角色（Subject）：声明了代理主题角色和真实主题角色共同的一些接口，因此在任何可以使用真实主题对象的地方都可以使用代理主题角色，这就是我们说的代理对象的行为和真实对象应该是一模一样的，客户端通常针对抽象主题编程。&lt;/li&gt;
&lt;li&gt;代理主题角色（Proxy）：代理主题角色通过关联关系引用真实主题角色，因此可以控制和操纵真实主题对象。代理主题角色中提供一个与真实主题角色相同的接口(以在需要时代替真实主题角色），同时还可以在调用对真实主题对象的操作之前或之后增加新的服务和功能&lt;/li&gt;
&lt;li&gt;真实主题角色（ConcreteSubject或RealSubject）：真实主题角色是代理角色所代表的真实对象，提供真正的业务操作，客户端可以通过代理主题角色间接地调用真实主题角色中定义的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一般来说，代理角色&lt;code&gt;Proxy&lt;/code&gt;会蕴含真实角色&lt;code&gt;ConcreteSubject&lt;/code&gt;。同时，如果想在幕后执行其他操作，也可以蕴含其他操作的对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在实际开发过程中，代理模式产生了很多具体化的类型:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;远程代理(Remote Proxy):为一个位于不同地址空间的对象提供一个本地的代理对象。不同的地址空间可以在相同或不同的主机中。&lt;/li&gt;
&lt;li&gt;虚拟代理(Virtual Proxy):当创建一个对象需要消耗大量资源时，可以先创建一个消耗较少资源的虚拟代理来表示，当真正需要时再创建。延迟加载时机。
&lt;ul&gt;
&lt;li&gt;这部分查看代码&lt;code&gt;proxy_virtual.cpp&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;保护代理(Protect Proxy)︰给不同的用户提供不同的对象访问权限。&lt;/li&gt;
&lt;li&gt;缓冲代理(Cache Proxy)︰为某一个目标操作的结果提供临时存储空间，以使更多用户可以共享这些结果。&lt;/li&gt;
&lt;li&gt;智能引用代理(Smart Reference Proxy)：当一个对象被引用时提供一些额外的操作，比如将对象被调用的次数记录下来等。比如智能指针&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;代理模式能够协调调用者和被调用者，降低系统耦合度;&lt;/li&gt;
&lt;li&gt;客户端针对抽象主题角色编程，如果要增加或替换代理类，无需修改源代码，符合开闭原则，系统扩展性好&lt;/li&gt;
&lt;li&gt;远程代理优点：为两个位于不同地址空间的对象的访问提供解决方案，可以将一些资源消耗较多的对象移至性能较好的计算机上，提高系统整体性能;&lt;/li&gt;
&lt;li&gt;虚拟代理优点：通过一个资源消耗较少的对象来代表一个消耗资源较多的对象，节省系统运行开销;&lt;/li&gt;
&lt;li&gt;缓冲代理优点：为某一个操作结果提供临时的存储空间，可以在后续操作中使用这些结果，缩短了执行时间;&lt;/li&gt;
&lt;li&gt;保护代理优点：控制对一个对象的访问权限，为不同客户提供不同的访问权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;由于在客户端和真实主题之间增加了代理对象，因此 有些类型的代理模式可能会造成请求的处理速度变慢。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;实现代理模式需要额外的工作，有些代理模式的实现 非常复杂。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;我们反复强调了，代理代理模式中，代理对象对用户暴露的行为应该和真实对象一致。这也是代理模式和装饰器模式的核心区别。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代理模式和装饰器模式的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代理模式提供的是一个行为完全一致的接口。而装饰器则是一种已有接口的增强（添加功能）。&lt;/li&gt;
&lt;li&gt;代理模式并不一定要含有一个需要代理的对象。而装饰器模式基本都需要含有一个需要装饰的对象。&lt;/li&gt;
&lt;li&gt;代理模式（尤其是虚拟代理模式）中，代理对象可以在不创建真实对象的情况下工作。因为此时可能并不需要创建真实对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时，我们可以针对不同需求来对代理模式进行不同的调整。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当客户端对象需要访问远程主机中的对象——可以使用远程代理&lt;/li&gt;
&lt;li&gt;当需要用一个资源消耗较少的对象来代表一个资源消耗较多的对象——虚拟代理&lt;/li&gt;
&lt;li&gt;当需要限制不同用户对一个独享的访问权限——保护代理&lt;/li&gt;
&lt;li&gt;当需要为一个频繁访问的操作结果提供临时存储空间——缓冲代理;。当需要为一个对象的访问提供一些额外的操作——智能引用代理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;vector&amp;lt;bool&amp;gt;&lt;/code&gt;就是代理模式&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;行为型模式部分&lt;/h1&gt;
&lt;p&gt;行为型模式每一种都不太一样，尽管有一些之间可能长得比较像，但是行为型模式每一种都是有针对性的，它们有针对性地解决某一种特定的问题。行为型模式不仅仅关注类和对象的结构，而且重点关注它们之间的相互作用。通过行为型模式，可以更加清晰地划分类与对象的职责，并研究系统在运行时实例对象 之间的交互。在系统运行时，对象并不是孤立的，它们可以通过相互通信与协作完成某些复杂功能，一个对象在运行时也将影响到其他对象的运行。&lt;/p&gt;
&lt;h1&gt;责任链模式 （&lt;strong&gt;Chain of Responsibility&lt;/strong&gt;）&lt;/h1&gt;
&lt;p&gt;责任链模式，顾名思义，一个链条。当我们的某一个对象需要在一个链条上面进行处理的时候，这个模式就大显神威了。什么叫链条上处理？其中一种比较简单的情况就是，根据一个条件的不同，需要层层递进去进行处理。比如请假，小于1天需要班主任批准，大于1天小于3天需要主任批准，大于3天需要校长批准。但是学生肯定是从班主任开始递交。如果某一个角色发现处理不了，则会“转发”至责任链的下一个人，让他去处理。以此类推。这种就是一个链条。&lt;/p&gt;
&lt;p&gt;另一种比较常见的情况是，当我们有一个请求，比如一个游戏角色服用了增益药水。根据增益药水的种类不同，我们可能有不同的增益效果。这时候我们可以把所有的增益视为一种链条。依次把增益请求传递给增益链条上的每一个处理对象。每一个处理对象根据传进来的增益信息来判断是否需要当前处理对象进行操作。无论是否操作，都将增益请求传递给链条上的下一个处理对象。直到处理完毕。&lt;/p&gt;
&lt;p&gt;通常情况下，这种链条会以一种链表的形式出现。也就是责任对象的衔接。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230326191219.png&quot; alt=&quot;QQ截图20230326191219&quot; /&gt;&lt;/p&gt;
&lt;p&gt;责任链模式本身结构比较简单，包含两个角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Handler（抽象处理者）︰抽象处理者一般为抽象类，声明了一个处理请求的接口&lt;code&gt;handleRequest()&lt;/code&gt;，定义了一个抽象处理者类型的对象，作为其对下游处理者的引用，通过该引用可以形成一条责任链。&lt;/li&gt;
&lt;li&gt;ConcreteHandler（具体处理者）︰是抽象处理者的子类，实现了处理请求的接口。在具体的实现中，如果该具体处理者能够处理该请求，就处理它，否则将该请求转发给后继者。具体处理者可以访问下一个对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由上述可知，在职责链模式中很多对象由每一个对象对其下家的引用连接起来形成一条链条，请求在这个链条上逐级传递，知道某一级能够处理这个请求为止。客户端不知道也不必知道是哪一级处理者处理了该请求，因为每个处理者都有相同的接口&lt;code&gt;handleRequest()&lt;/code&gt;。接下来通过一个实例来进一步认识职责链模式。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;将请求的接收者和处理者解耦，客户端无需知道具体处理者&lt;/strong&gt;，只针对抽象处理者编程，简化了客户端编程过程，降低系统耦合度&lt;/li&gt;
&lt;li&gt;在系统中增加一个新的处理者时，只需要继承抽象处理者，重新实现&lt;code&gt;handleRequest()&lt;/code&gt;接口，无需改动原有代码，符合开闭原则&lt;/li&gt;
&lt;li&gt;给对象分配职责时，职责链模式赋予系统更多灵活性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;请求没有一个明确的接收者，有可能遇到请求无法响应的问题&lt;/li&gt;
&lt;li&gt;如果有比较长的职责链，其处理过程会很长。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建立职责链的工作是在客户端进行，如果建立不当，可能导致循环调用或者调用失败。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;适用环境：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有多个对象处理同一个请求，具体由谁来处理是在运行时决定，客户端只需发出请求到职责链上，而无需关心具体是谁来处理&lt;/li&gt;
&lt;li&gt;可动态指定一组对象处理请求，客户端可以动态创建职责链来处理请求，还可以改变职责链中各个处理者之间的上下级关系。&lt;/li&gt;
&lt;li&gt;同时，责任链模式也经常搭配中介者模式和观察者模式。它可以非常简单，也可以非常复杂。我们后面会用额外例子说明。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;命令模式 （Command）&lt;/h1&gt;
&lt;p&gt;在软件设计中，我们经常需要向某些对象发送请求，但是并不知道请求的接收者是谁，也不知道被请求的操作是哪个，我们只需在程序运行时指定具体的请求接收者即可，此时，可以使用命令模式来进行设计，使得请求发送者与请求接收者消除彼此之间的耦合，让对象之间的调用关系更加灵活。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;命令模式可以对发送者和接收者完全解耦，发送者与接收者之间没有直接引用关系&lt;/strong&gt;，发送请求的对象只需要知道如何发送请求，而不必知道如何完成请求。这就是命令模式的模式动机。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;鼠标点击某个键，就好像用户在向图片浏览器发送指令，图片浏览器内部接收到指令后开始调用相应的函数，最终结果是播放上一张或下一张图片，即执行或响应了用户发出的命令。客户并不知道发出的命令是什么形式，也不知道图片浏览器内部命令是如何执行的;同样，浏览器内部也不知道是谁发送了命令。&lt;strong&gt;命令的发送方和接收方（执行方）没有任何关联。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;命令模式的命令是一个对象，是一个命令的载体。也就是说，我们把命令这个行为封装成一个对象&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要正确区分命令command和请求query的区别
&lt;ul&gt;
&lt;li&gt;命令作用于对象上的时候通常会导致对象状态被修改。比如请求某些动作，或请求某些更改。也就是说，命令强调了对状态的修改。&lt;/li&gt;
&lt;li&gt;请求作用于对象的时候通常不会导致对象状态的修改。比如请求某些信息。也就是说，请求强调的是信息的获取。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Command：抽象命令类
&lt;ul&gt;
&lt;li&gt;一般只有一个&lt;code&gt;execute&lt;/code&gt;执行接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ConcreteCommand：具体命令类。
&lt;ul&gt;
&lt;li&gt;这个类会实现&lt;code&gt;execute&lt;/code&gt;执行接口。它对应具体的接收者对象。也就是一般来说，每一个对象都会有一个对应的具体命令类。将接收者的动作（action）绑定其中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execute&lt;/code&gt;执行的就是接受者的动作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Invoker：调用者类
&lt;ul&gt;
&lt;li&gt;请求的发送者。通过命令对象来执行请求。一个调用者不需要在设计时确定接收者。所以调用者通过聚合或注入与命令类产生关联。&lt;strong&gt;也就是可以将一个具体命令对象注入到调用者中，再通过调用具体命令对象的&lt;code&gt;execute&lt;/code&gt;实现间接请求命令执行者（接收者）的操作。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Receiver：接收者类
&lt;ul&gt;
&lt;li&gt;一般实现处理请求的具体操作。也就是实现我们命令执行的动作（action）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Client：客户类
&lt;ul&gt;
&lt;li&gt;在我们实例里客户就是&lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;较为复杂 参考代码。&lt;/p&gt;
&lt;h2&gt;命令队列模式&lt;/h2&gt;
&lt;p&gt;有时候，当请求者发送一个请求时，有不止一个接收者产生响应(Qt信号槽，一个信号可以连接多个槽)，这些接收者将逐个执行业务方法，完成对请求的处理，此时可以用命令队列来实现。比如按钮开关同时控制电灯和风扇，这个例子中，请求发送者是按钮开关，有两个接收者产生响应，分别是电灯和风扇。&lt;/p&gt;
&lt;p&gt;比如在代码中，我们设置一个命令队列。里面可以储存不同的命令。然后请求者只需要使用一次&lt;code&gt;touch&lt;/code&gt;（包含命令队列类的execute）就可以执行所有的命令。&lt;/p&gt;
&lt;p&gt;执行过程就是请求者使用&lt;code&gt;touch&lt;/code&gt;调用命令队列类的&lt;code&gt;execute&lt;/code&gt;，命令队列再依次调用所有具体命令类的&lt;code&gt;execute&lt;/code&gt;来完成具体执行。&lt;/p&gt;
&lt;h2&gt;命令队列+组合模式 = 组合命令模式（宏命令）&lt;/h2&gt;
&lt;p&gt;查看&lt;code&gt;command_composite.cpp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这种就稍微复杂了，并且和业务绑定的比较紧密。但是核心还是这两个模式的核心。&lt;/p&gt;
&lt;p&gt;组合命令本身也是一个具体命令，不过它包含了对其他命令对象的引用，在调用宏命令的execute()方法时，将递归调用它所包含的每个成员命令的execute()方法，一个宏命令的成员对象可以是简单命令，还可以继续是宏命令。执行一个宏命令将执行多个具体命令，从而实现对命令的批处理。是不是非常像组合模式？&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;降低系统的耦合度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新的命令可以很容易地加入到系统中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以比较容易地设计一个命令队列和宏命令（组合命令）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宏命令也就是上面提到的组合命令模式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以方便地实现对请求的Undo和Redo。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用命令模式可能会导致某些系统有过多的具体命令类。&lt;strong&gt;因为针对每一个命令都需要设计一个具体命令类（符合我们说的，每一个命令都是一个对象）&lt;/strong&gt;，因此某些系统可能需要大量具体命令类，这将影响命令模式的使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以看到，客户端只需要有一个调用者和抽象命令类，在给调用者注入命令时，再将命令类具体化。这也就是定义中“可用不同的请求对客户进行参数化&quot;的体现。客户端并不知道命令是如何传递和响应，只需发送命令&lt;code&gt;touch()&lt;/code&gt;即可，由此实现命令发送者和接收者的解耦。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果系统中增加了新的功能，功能键与新功能对应，只需增加对应的具体命令类，在新的具体命令类中调用新的功能类的&lt;code&gt;action()&lt;/code&gt;方法，然后将该具体命令类通过注入的方式使调用者可以执行，无需修改原有代码，符合开闭原则。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在以下情况下可以使用命令模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统需要将请求调用者和请求接收者解耦，使得调用者和接收者不直接交互。&lt;/li&gt;
&lt;li&gt;系统需要在不同的时间指定请求、将请求排队和执行请求。&lt;/li&gt;
&lt;li&gt;系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。&lt;/li&gt;
&lt;li&gt;系统需要将一组操作组合在一起，即支持宏命令&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命令模式在STL中也有很多地方使用了。但是这种命令模式和策略模式很像。我们在策略模式中说明了如何区分。：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230514175510.png&quot; alt=&quot;QQ截图20230514175510&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;迭代器模式(Iterator)&lt;/h1&gt;
&lt;p&gt;迭代器模式的核心就是为某一种数据结构提供一种遍历（顺序或随机访问）的方式。&lt;/p&gt;
&lt;p&gt;其整体实现可繁可简，因为就相当于是实现了一个迭代器。但是具体功能的多少，完全取决于具体业务。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230328120909.png&quot; alt=&quot;QQ截图20230328120909&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;lterator(抽象迭代器)︰声明了访问和遍历聚合对象元素的接口，如&lt;code&gt;first()&lt;/code&gt;方法用于访问聚合对象中第一个元素，&lt;code&gt;next()&lt;/code&gt;方法用于访问下一个元素，&lt;code&gt;hasNext()&lt;/code&gt;判断是否还有下一个元素，&lt;code&gt;currentltem()&lt;/code&gt;方法用于获取当前元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Concretelterator(具体迭代器)︰实现抽象迭代器声明的方法，通常具体迭代器中会专门用个变量来记录迭代器在聚合对象中所处的位置。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;具体迭代器类包含一个具体聚合类的的指针。一定要注意。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在我们的代码里就是&lt;code&gt;RemoteControl&lt;/code&gt;类有一个&lt;code&gt;Television&lt;/code&gt;类的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Aggregate (抽象聚合类)︰用于存储和管理元素对象，声明一个创建迭代器的接口，其实是个抽象迭代器工厂的角色。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ConcreteAggregate(具体聚合类)∶实现了方法&lt;code&gt;CreateIterator()&lt;/code&gt;，该方法返回一个与该具体聚合类对应的具体迭代器Concretelterator的实例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这时候通常需要前向声明&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;支持以不同的方式遍历一个聚合对象，在同一个聚合对象上可以定义多个遍历方式。&lt;/li&gt;
&lt;li&gt;简化了聚合类，使得聚合类的职责更加单一&lt;/li&gt;
&lt;li&gt;迭代器模式中引入抽象层，易于增加新的迭代器类，便于扩展，符合开闭原则。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;将聚合类中存储对象和管理对象的职责分离，增加新的聚合类时同样需要考虑增加对应的新的迭代器类，类的个数成对增加，不利于系统管理和维护&lt;/li&gt;
&lt;li&gt;设计难度极大，需要充分考虑将来系统的扩展。尤其是涉及到模板的时候。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;其实我不太认为平时有需要自己设计迭代器。理解STL的迭代器的精髓就已经足够了。&lt;/p&gt;
&lt;h1&gt;中介者模式 （Mediator)&lt;/h1&gt;
&lt;p&gt;中介者模式的核心是使得需要互相交互的对象解耦。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用一个中介对象来封装一系列的对象交互，中介者使各对象不需要显式地相互引用，从而使其耦合松散，而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式，它是一种对象行为型模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，&lt;strong&gt;原本互相需要交互的对象不需要维护互相的引用。他们共同被中介者管理。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230329162826.png&quot; alt=&quot;QQ截图20230329162826&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mediator（抽象中介者）：声明一个用于与各个同事对象之间交互的接口，通常声明一个注册方法，用于增加同事对象&lt;/li&gt;
&lt;li&gt;ConcreteMediator（具体中介者）：实现上面的接口，协调各个同事对象来实现协作行为，&lt;strong&gt;维持对各个同事对象的引用&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;一般来说，会有一个或多个容器在具体中介者内存储同事类对象的指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一般来说中介者是单例类。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Colleague（抽象同事类）：声明各个同事类公有的接口，同时&lt;strong&gt;维持了一个对抽象中介者类的引用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;concreteColleague（具体同事类）：具体实现接口，具体同事类只需与中介者通信，通过中介者完成与其他同事类的通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们刚提到了，原本互相需要交互的对象不需要维护互相的引用。他们共同被中介者管理。所以我们再次强调，一般来说，中介者会维护各个同事类对象的指针，而同事类维护一个中介类的指针。这样就达成了同事类通过中介类和其他同事类交互的目的。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简化了对象之间的交互&lt;/strong&gt;，通过中介者，对象之间的多对多关系就简化了相对更简单的一对多关系&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可将各个同事对象解耦&lt;/strong&gt;，利于各个同事之间的松散耦合，可独立地改变和复用每一个同事对象，增加新的中介者和同事都比较方便，符合开闭原则&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可减少子类生成&lt;/strong&gt;，将原本分布于多个对象之间的行为封装在一起，只需生成新的具体中介者类就可以改变这些行为&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在具体中介者类中包含了同事之间的交互细节，可能会导致具体中介者类非常复杂，使得系统难以维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;中介者承担了两个层次的职责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;结构上起中转作用
&lt;ul&gt;
&lt;li&gt;通过中介者的中转，各个同事类的对象之间不必再相互显式的调用，或持有，只需通过中介者就可以实现交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;行为上起协调作用
&lt;ul&gt;
&lt;li&gt;中介者可以进一步地将各个同事类之间的关系进行封装，同事类对象可以一致地和中介者进行交互，而不必指出中介者具体该如何操作，中介者根据封装在自身内部的协调逻辑对同事类对象的请求进一步处理，将同事类对象之间的关系行为进行分离和封装。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以一般来说，当：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统中的对象之间存在复杂的交互关系，使得系统内逻辑错综复杂，难以管理&lt;/li&gt;
&lt;li&gt;一个对象引用了其他很多对象，并直接和这些对象交互，导致该对象难以复用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如上这几种情况发生的时候，可以考虑使用中介者模式。&lt;/p&gt;
&lt;h1&gt;备忘录模式（Memento）&lt;/h1&gt;
&lt;p&gt;备忘录模式，顾名思义。目的是提供一种保存某一对象当前状态的方法。&lt;/p&gt;
&lt;p&gt;换句话说，就是快照。我们提供一种方法让我们可以保存某一对象的某一个状态，一般情况下会同时提供Undo和Redo的功能&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在不破坏封装的前提下捕获一个对象的内部状态，并在该对象之外保存这个状态，这样可以在以后将对象恢复到原先保存的状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230330160152.png&quot; alt=&quot;QQ截图20230330160152&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Originator（原发器）︰&lt;strong&gt;这就是我们的业务代码&lt;/strong&gt;，通常是将系统中需要保存内部状态的类设计为原发器。&lt;strong&gt;提供一个方法通过创建一个备忘录对象存储当前的内部状态，也可以使用备忘录来恢复其内部状态。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;重申，业务代码也就是原发器当中需要有两个方法。一个是类似于&lt;code&gt;restore&lt;/code&gt;，即通过一个备忘录&lt;code&gt;memento&lt;/code&gt;来恢复状态。一个是类似于&lt;code&gt;save&lt;/code&gt;或者是&lt;code&gt;create&lt;/code&gt;，即创建一个&lt;code&gt;memento&lt;/code&gt;备忘录对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Memento（备忘录）∶**用于存储原发器的内部状态。备忘录的设计可以参考原发器的设计，根据需要确定备忘录类中的属性。**除了原发器类对象，不允许其他对象修改备忘录。
&lt;ul&gt;
&lt;li&gt;备忘录对象里面存储的就是我们业务代码原发器对象希望让我们存储的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Caretaker（负责人）︰负责保存备忘录，可以存储一个或多个备忘录对象，但是负责人只负责保存对象，不能修改对象，也不必知道对象的实现细节。&lt;strong&gt;负责人可以存储多个备忘录对象&lt;/strong&gt;，以实现undo和redo操作。
&lt;ul&gt;
&lt;li&gt;负责人负责保管所有的备忘录对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实现状态恢复、撤销操作的功能，用户可以恢复到指定的历史状态，让软件系统更加人性化&lt;/li&gt;
&lt;li&gt;备忘录封装了信息，除了原发器以外，其他对象访问不了备忘录的代码;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;资源消耗大。如果需要保存原生器对象的多个历史状态，那么将创建多个备忘录对象;或者如果原生器对象的很多状态都需要保存，也将消耗大量存储资源。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;感觉没什么用。资源消耗比较大。&lt;/p&gt;
&lt;h1&gt;状态模式 (State)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;个人理解就是把状态机换成了OOP。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;状态模式将一个对象的状态从对象中分离出来，封装到专门的状态类中，使得对象状态可以灵活变化。对于客户端而言，无需关心对象状态的转换和当前状态。&lt;/p&gt;
&lt;p&gt;状态模式定义:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230401225023.png&quot; alt=&quot;QQ截图20230401225023&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context: 上下文类
&lt;ul&gt;
&lt;li&gt;是&lt;strong&gt;拥有多种状态的对象&lt;/strong&gt;。上下文类的状态存在多样性，并且在不同的状态下，对象表现出不同的行为。&lt;strong&gt;在上下文类中，维护了一个抽象状态类的实例。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;State：抽象状态类
&lt;ul&gt;
&lt;li&gt;声明了一个接口，用于封装与在上下文类中的一个特定状态相关的行为在子类中实现在各种不同状态对应的方法。不同的子类可能存在不同的实现方法，相同的方法可以写在抽象状态类中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ConcreteState：具体状态类
&lt;ul&gt;
&lt;li&gt;实现具体状态下的方法。每一个具体状态类对应一个具体的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;值得注意的是，上下文中维护了一个状态类的指针或者引用，可以由上下文类来决定具体实例化为哪一个具体的状态对象，也可以由具体的状态类来决定转换为哪一个实例，所以根据实际的业务逻辑的不同，上下文类和状态类之间存在依赖甚至相互引用的关系&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;搭配代码理解下面内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这段代码的宗旨是，在使用账户游玩游戏的时候，账户等级随着输赢变化。等级不同又会有不同的游玩技能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以我们账户内部需要有一个等级的实例。然后通过这个等级去调用不同等级的方法。多态可以天然完成这个行为。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每一次在给账户类设置新的等级实例的时候，需要记得回收旧的等级实例的内存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等级类也有一个指向账户的指针用于获取账户信息。每次初始化等级实例的时候都需要设置账户。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次升级的时候，先在等级类的构造函数内把新的等级类和账户类绑定，然后把当前帐户的等级类设置为新的等级类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;上面的代码不管我们账户当前是什么级别，都统一地调用了上下文类封装好的方法&lt;code&gt;playcard()&lt;/code&gt;，即外界并不知道不同级别内部的具体实现细节。运行结果显示，账户的在不同的状态(级别)下能够表现不同的行为(不同的技能)，并且能够不断改变自身的状态(升级或降级)。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;封装了转换规则。&lt;/li&gt;
&lt;li&gt;枚举可能的状态，在枚举状态之前需要确定状态种类。&lt;/li&gt;
&lt;li&gt;将所有与某个状态有关的行为放到一个类（具体状态类）中，可以方便地增加新的状态，只需要注入（依赖）不同的状态类对象到上下文类中，即可使上下文类拥有不同的行为。&lt;/li&gt;
&lt;li&gt;允许状态转换逻辑与状态对象合成一体，而不是某一个巨大的条件语句块。可以让多个环境对象共享一个状态对象，从而减少系统中对象的个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;状态模式增加了系统中类的个数（不同的具体状态类)。&lt;/li&gt;
&lt;li&gt;结构相对复杂（如前述实例的UML图)，代码逻辑也较复杂。&lt;/li&gt;
&lt;li&gt;如果要增加新的状态，需要修改负责状态转换的代码，不符合开闭原则。
&lt;ul&gt;
&lt;li&gt;如上述实例，如果增加了一个中间级别，就要修改很多状态转换的逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对开闭原则不太友好。个人理解为适用于if else状态非常多的场景。比如HTTP报文解析，TCP链接等。这种场景下一般很少去修改或增删新的状态了。&lt;/p&gt;
&lt;p&gt;适用环境：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象的行为根据它的状态的改变而不同&lt;/li&gt;
&lt;li&gt;代码中含有大量与对象状态有关的判断逻辑(if else或switch case)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;观察者模式（Observer）&lt;/h1&gt;
&lt;p&gt;观察者模式定义：定义对象之间的一种一对多（变化）的依赖关系。以便当一个对象的状态发生改变时，所有依赖他的对象都会得到通知并自动更新。&lt;/p&gt;
&lt;p&gt;简而言之就是如果一个对象被改变了，其他所有我们有关联（被注册）的对象都会得到通知并产生对应的行为。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230331180848.png&quot; alt=&quot;QQ截图20230331180848&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Subject：被观察者的抽象类。
&lt;ul&gt;
&lt;li&gt;通常这个类会有注册，和取消注册两个方法来进行观察者的管控。也就是控制有多少个对象需要观察这个被观察对象，也就是多少个对象依赖于被观察者。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;被观察者通常会有个容器。储存所有的观察者&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;同时会有一个&lt;code&gt;notify&lt;/code&gt;函数用于在自身状态改变的时候通知观察者。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;典型实现是遍历同期内所有的观察者，调用所有观察者的&lt;code&gt;update&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Concrete Subject：被观察者的具体目标类。
&lt;ul&gt;
&lt;li&gt;通常会实现这个&lt;code&gt;notify&lt;/code&gt;通知方法。&lt;/li&gt;
&lt;li&gt;同时具体目标会拥有自身的属性和成员方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Observer：观察者的抽象类。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通常观察者会接收到观察对象的状态改变消息。所以观察者需要有&lt;code&gt;update&lt;/code&gt;来在接收到状态改变消息的时候改变自身状态。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ConcereteObserver：观察者的具体目标类。
&lt;ul&gt;
&lt;li&gt;通常会实现更新方法&lt;code&gt;update&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;具体观察者中维护了一个具体目标对象中的引用或指针用来储存目标的状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具体观察者的意义是什么？我们可以理解为体观察者指的是对某个对象（被观察者）的某个部分或整体（消息）感兴趣的对象。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;所以一个被观察者可以有多个观察者。因为感兴趣的部分不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通常，观察者模式需要使用前向声明。因为有互相包含的关系。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;参见代码。过长导致此处放不下。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;观察者模式实现了稳定的消息更新和传递的机制，通过引入抽象层可以扩展不同的具体观察者角色&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;支持广播通信&lt;/strong&gt;，所有已注册的观察者（添加到目标列表中的对象）都会得到消息更新的通知，简化了一对多设计的难度&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在我们的实例里就是遍历装有观察者的容器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;符合开闭原则&lt;/strong&gt;，增加新的观察者无需修改已有代码，在具体观察者与观察目标之间不存在关联关系的情况下增加新的观察目标也很方便。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;增加新的观察者在我们的示例里面就是被观察者的&lt;code&gt;join&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码中观察者和观察目标相互引用，存在循环依赖&lt;/strong&gt;，观察目标会触发二者循环调用，有引起系统崩溃的风险&lt;/li&gt;
&lt;li&gt;如果一个观察目标对象有很多直接和间接观察者，将所有的观察者都通知到会耗费大量时间。
&lt;ul&gt;
&lt;li&gt;因为要遍历观察者容器内的所有元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的，而仅仅只是知道观察目标发生了变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;适用场景
&lt;ul&gt;
&lt;li&gt;一个对象的改变会引起其他对象的联动改变，但并不知道是哪些对象会产生改变以及产生什么样的改变&lt;/li&gt;
&lt;li&gt;如果需要设计一个链式触发的系统，可是使用观察者模式&lt;/li&gt;
&lt;li&gt;广播通信、消息更新通知等场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成就系统&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;策略模式（Strategy）&lt;/h1&gt;
&lt;p&gt;我们在模板笔记的19.2中提到了策略模式。在那个部分中，策略模式的核心是使用一个模板参数做为策略类的选项，来为一个商定好的具体接口提供不同的实现方法。当然了，核心就是如此。只不过我们当时的重点是模板。但是在这里我们的重点是设计模式本身。策略模式也不一定非得采取模板的方式来实现。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230401224840.png&quot; alt=&quot;QQ截图20230401224840&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context（上下文类） ︰上下文类是使用算法的角色，可以在解决不同具体的问题时实例化不同的具体策略类对象。&lt;strong&gt;在动态策略模式中，上下文类通常包含一个抽象策略类的指针。静态策略模式中，则是在模板参数中传入具体策略类的类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Strategy（抽象策略类）︰声明算法的方法，抽象层的设计使上下文类可以无差别的调用不同的具体策略的方法&lt;/li&gt;
&lt;li&gt;ConcreteStrategy（具体策略类）：实现具体的算法&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略模式符合开闭原则&lt;/strong&gt;，用户可以在不修改原有系统的基础上选择算法或行为，也可以灵活地增加新的算法或行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略模式提供了管理相关的算法族的办法。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;策略模式提供了可以替换继承关系的办法。也就是&lt;strong&gt;符合里氏替换原则&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;使用策略模式可以避免使用多重条件转移语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;客户端必须知道所有的策略类，并自行决定使用哪一个策略类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略模式将造成产生很多策略类，可以通过使用享元模式在一定程度上减少对象的数量。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;通常而言，通过模板参数实现的策略模式，我们称之为静态策略模式。通过传入一个对象的方式实现的策略模式，我们称之为动态策略模式。两者各有利弊。动态策略模式因为使用了多态，会有一定的性能损耗。但是优点是可以灵活地在运行时更改策略。而静态模式因为使用了模板参数，虽然没有运行时性能损耗，但会有模板相关的代码膨胀。同时，由于模板参数是类型的一部分，不可能在运行时更换策略，这也限制了其灵活性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们发现策略模式和状态模式长得很像，那么他们有什么区别呢？可以通过环境类状态的个数来决定是使用策略模式还是状态模式。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;策略模式的环境类自己选择一个具体策略类，具体策略类无须关心环境类；而状态模式的环境类由于外在因素需要放进一个具体状态中，以便通过其方法实现状态的切换，因此环境类和状态类之间存在一种双向的关联关系。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用策略模式时，客户端需要知道所选的具体策略是哪一个，而使用状态模式时，客户端无须关心具体状态，环境类的状态会根据用户的操作自动转换。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果系统中某个类的对象存在多种状态，不同状态下行为有差异，而且这些状态之间可以发生转换时使用状态模式；如果系统中某个类的某一行为存在多种实现方式，而且这些实现方式可以互换时使用策略模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;命令模式在某些时候可能和策略模式非常像。但是有一种方法可以区分&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;策略模式强调&lt;em&gt;&lt;strong&gt;如何去做（HOW)&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;命令模式强调&lt;em&gt;&lt;strong&gt;做了什么（WHAT)&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;来自&lt;a href=&quot;https://youtu.be/OvO2NR7pXjg?t=1658&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230514175447.png&quot; alt=&quot;QQ截图20230514175447&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;模板方法模式（Template Method）&lt;/h1&gt;
&lt;p&gt;这玩意和模板没有任何关系。而且是最简单的设计模式。就是继承+虚函数+重写的多态。没了。&lt;/p&gt;
&lt;p&gt;STL在&lt;code&gt;pmr::memory_resource&lt;/code&gt;中使用了这种模式。来自&lt;a href=&quot;https://youtu.be/OvO2NR7pXjg?t=2297&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;访问者模式 （Visitor）&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;访问者模式的其中一种的核心是double-dispatch。我们在more effective c++的条款31中提到过这一点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是还是有些许差别。非访问者模式的double-dispatch是&lt;code&gt;s.cpp&lt;/code&gt;。访问者模式的double-dispatch是&lt;code&gt;sv.cpp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们能看到，在两个文件中由于具体业务原因，并没有对象结构类。含有对象结构类的访问者模式在文件夹&lt;code&gt;visitor&lt;/code&gt;中。同时，非访问者模式中，我们的所有相关函数都叫&lt;code&gt;print&lt;/code&gt;。而且没有抽象访问者类。而访问者模式中，我们把&lt;code&gt;print&lt;/code&gt;分离出来为&lt;code&gt;accept&lt;/code&gt;和&lt;code&gt;visit&lt;/code&gt;。同时新增了抽象访问者类。然后具体的访问原理在代码注释，并且和more effective c++中讨论的原理是几乎一致的。&lt;/p&gt;
&lt;p&gt;当然，也可以去掉抽象访问者类然后使用模板参数来替代。同时也可以不使用more effective c++中讨论的第二种方法也就是像现在一样只使用虚函数，可以换回&lt;code&gt;dynamic_cast&lt;/code&gt;方法&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/20191110175737470.png&quot; alt=&quot;20191110175737470&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上图和前述可以看出，访问者模式中有两个层次结构:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问者的层次结构&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象访问者和具体访问者，不同的具体访问者有不同的访问方式（&lt;code&gt;visit()&lt;/code&gt;方式）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;被访问元素的层次结构&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象元素和具体元素，不同的具体元素有不同的被访问方式（&lt;code&gt;accept()&lt;/code&gt;方式）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Visitor（抽象访问者）：抽象类，声明了访问对象结构中不同具体元素的方法&lt;code&gt;visit()&lt;/code&gt;，由方法名称可知该方法将访问对象结构中的某个具体元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ConcreteVisitor（具体访问者）：访问某个具体元素的访问者，实现具体的访问方法&lt;code&gt;visit()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Element（抽象元素）∶抽象类，一般声明一个&lt;code&gt;accept()&lt;/code&gt;的方法，用于接受访问者的访问,&lt;code&gt;accept()&lt;/code&gt;方法常常以一个抽象访问者的指针作为参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ConcreteElement（具体元素）：针对具体被访问的元素，实现&lt;code&gt;accept()&lt;/code&gt;方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ObjectStructure（对象结构）：元素的集合，提供了遍历对象结构中所有元素的方法。对象结构存储了不同类型的元素对象，以供不同的访问者访问。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增加新的访问者很方便&lt;/strong&gt;，即增加一个新的具体访问者类，定义新的访问方式，无需修改原有代码，符合开闭原则;&lt;/li&gt;
&lt;li&gt;被访问元素集中在一个对象结构中，类的职责更清晰，利于对象结构中元素对象的复用;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增加新的元素类很困难&lt;/strong&gt;，增加新的元素时，&lt;strong&gt;在抽象访问者类中需要增加一个对新增的元素方法的声明，即要修改抽象访问者代码。此外还要增加新的具体访问者以实现对新增元素的访问，不符合开闭原则;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;破坏了对象的封装性，访问者模式要求访问者对象访问并调用每一个元素对象的操作，那么元素对象必须暴露自己的内部操作和状态，否则访问者无法访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;其实访问者模式就是基于double-dispatch。具体的无需多言了。&lt;/p&gt;
&lt;h1&gt;有用的资料&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/OvO2NR7pXjg&quot;&gt;Design Patterns: Facts and Misconceptions - Klaus Iglberger - CppCon 2021&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;类型擦除&lt;/h1&gt;
&lt;p&gt;https://www.youtube.com/watch?v=4eeESJQk-mw&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型擦除有五种：多态，模板，容器，通用类型，闭包。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/99532906&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们有最经典的C风格类型擦除。就是全都转换成&lt;code&gt;void*&lt;/code&gt;。但是它类型不安全。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;什么是类型擦除？我们从生活例子说起。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们有shape类，然后有circle和square继承自shape。很合理对吧？&lt;/p&gt;
&lt;p&gt;然后针对每一个子类，都要有一个draw。所以draw是虚函数。很合理对吧？&lt;/p&gt;
&lt;p&gt;问题来了。不同的引擎要用不同的方式draw。所以我们要有metaldraw，opengldraw。所以我们要为每一个draw设计为一个单独的类。这样就会有metalcircle， openglcircle， metalsquare， openglsquare。每一个类有一个自己的draw。&lt;/p&gt;
&lt;p&gt;这时候我们不仅仅要画了，还要序列化。而这个序列化是要依赖于我们的四个子类的。这时候有Aserializeopenglsquare, bserializeopenglsquare....&lt;/p&gt;
&lt;p&gt;这是噩梦。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230206233332.png&quot; alt=&quot;QQ截图20230206233332&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们学习了设计模式，这时候可以使用策略模式。也就是把draw作为一种策略嵌入到我们的每一个子类中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们有drawstrategy。这个类有openglstrategy和metalstrategy。openglstrategy下面可以有openglcirclestrategy和openglsquarestrategy。&lt;/p&gt;
&lt;p&gt;我们的circle和square内部拥有一个drawstrategy类型的实例（指针，对象都可以）。构造circle或square对象的时候从外部传入对应的到底是openglstrategy还是metalstrategy。我们circle和square内部的draw只需要通过这个策略类实例调用策略类的draw就好了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230206233921.png&quot; alt=&quot;QQ截图20230206233921&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;但是还有问题。它性能不好，指针太多，虚函数太多，等等等等。怎么办？类型擦除。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;类型擦除的特点&lt;/h2&gt;
&lt;p&gt;类型擦除不是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不是&lt;code&gt;void*&lt;/code&gt;指针。我们确实不再关心类型，但是这是错误理解。不是C那种做法了。&lt;/li&gt;
&lt;li&gt;不是一个指向基类的指针。这就回到了多态。&lt;/li&gt;
&lt;li&gt;也不是&lt;code&gt;std::variant&lt;/code&gt;。&lt;code&gt;variant&lt;/code&gt;其实是为封闭类型提供开放操作。而类型擦除是为开放类型提供封闭操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类型擦除是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造函数模板 和&lt;/li&gt;
&lt;li&gt;完全不含有虚机制的接口 和&lt;/li&gt;
&lt;li&gt;三个设计模式的结合：外部多态（external polymorphism） + 桥接（bridge） + 原型（prototype）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;把我们刚才的例子换成类型擦除&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230207000338.png&quot; alt=&quot;QQ截图20230207000338&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-12-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/QQ%E6%88%AA%E5%9B%BE20230207000347.png&quot; alt=&quot;QQ截图20230207000347&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;现在我们的circle和square不需要知道任何关于draw和序列化的事情。他们只是单纯的几何基类。拥有必要的几何元素。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C++杂记 - 3（四）：函数类型、函数指针与函数引用</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03-4/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03-4/</guid><pubDate>Tue, 06 Sep 2022 06:58:00 GMT</pubDate><content:encoded>&lt;h1&gt;函数类型，函数指针类型和函数指针的区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数指针&lt;/strong&gt;指向的是函数而非对象。和其他指针类型一样，函数指针指向某种特定类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数类型&lt;/strong&gt;由它的返回值和参数类型决定，与函数名无关。&lt;/li&gt;
&lt;li&gt;函数指针类型就是函数指针的类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;bool func(const string&amp;amp; a, const string&amp;amp; b);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述函数类型是：&lt;code&gt;bool (const string &amp;amp;, const string &amp;amp;)&lt;/code&gt;;
函数指针类型是：&lt;code&gt;bool (*)(const string &amp;amp;, const string &amp;amp;)&lt;/code&gt;;&lt;/p&gt;
&lt;p&gt;我们提到过，函数名称在使用时会被自动转换为函数指针。也就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fptr_name = func 
    等价于 
fptr_name = &amp;amp;func
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;bool Func(const string&amp;amp;, const string&amp;amp;); // Func是函数类型；
bool (*FuncP)(const string&amp;amp;, const string&amp;amp;); // FuncP是函数指针类型；
typedef decltype(Func) Func2;  // Func2是函数类型；
typedef decltype(FuncP) *Func2P; // Func2P是函数指针类型；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;code&gt;decltype(Func)&lt;/code&gt;返回的是函数类型，而不是函数指针类型；&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下面两个声明语句是同一个函数，因为编译器会自动的将&lt;code&gt;Functoin_Type&lt;/code&gt; 转换成函数指针类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void test(int a, int b, Functoin_Type  fn);
void test(int a, int b, Funcion_Pointer fn);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Functoin_Type = int(float,int); //函数类型
using Funcion_Pointer = int(*)(float, int); //函数指针类型


void callback1(Functoin_Type callable){ //函数类型当形参。自动转换
    callable(1.2f,2);
}
void callback2(Funcion_Pointer callable){ //函数指针当形参。
    callable(1.2f,2);
}
int testsor(float b, int c){
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot;success&quot; &amp;lt;&amp;lt; endl;
    return 1;
}
int main(){
    // test&amp;lt;int(float, int)&amp;gt; obj;
    // obj.construct(testsor, 2.5f, 234);
    callback1(testsor); //效果一致
    callback2(testsor); //效果一致
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;做为返回值时，由于返回的必须是函数指针类型而非函数类型，所以&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Functoin_Type ret1(){
 //错误。返回的是函数类型
}
Funcion_Pointer ret2(){
    //正确。返回的是函数指针类型。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数类型有一个非常特殊的地方：&lt;strong&gt;不能使用函数类型声明变量，但是可以把它当做函数的形参（编译器会自动转换）&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/function&lt;/p&gt;
&lt;p&gt;形参列表部分：&lt;/p&gt;
&lt;p&gt;....&lt;/p&gt;
&lt;p&gt;形参列表中的每个函数形参的类型根据下列规则确定：&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;如果类型是函数类型&lt;code&gt; F&lt;/code&gt;，那么它被替换成类型&lt;code&gt;F&lt;/code&gt; 的指针”&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Functoin_Type f = testsor;      //错误 不可使用函数类型声明变量。
Funcion_Pointer f = testsor;    //正确
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;成员函数指针类型&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class obj{
    public:
        bool func(int, int){
            std::cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
        }
};
cout &amp;lt;&amp;lt;is_same&amp;lt;bool(obj::*)(int, int), decltype(&amp;amp;obj::func)&amp;gt;::value &amp;lt;&amp;lt; endl; //true
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意，函数指针类型是一个整体。不能把他们分开。比如函数指针&lt;code&gt;bool (*pf)(int)&lt;/code&gt; 的类型是 &lt;code&gt;bool (*)(int)&lt;/code&gt;。在模板接受一个参数的时候，不能把返回值类型&lt;code&gt;bool&lt;/code&gt;扒下来。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当然了，如果想，也可以利用成员函数指针类型的方法把他们分开。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename R, typename T, typename... Args&amp;gt;
void f(R(T::*pf)(Args... args)){
        std::puts(__PRETTY_FUNCTION__);
}
//void f(R (T::*)(Args ...)) [with R = bool; T = obj; Args = {int, int}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://www.jianshu.com/p/6ecfd541ec04&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/17446220/c-function-types&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/13233213/can-a-function-type-be-a-class-template-parameter&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/72926596/type-deduction-for-a-member-function-pointer&lt;/p&gt;
&lt;h1&gt;函数引用&lt;/h1&gt;
&lt;p&gt;和函数指针，函数名没啥区别。主要遇到的地方是杂记4中的decltype的函数名做为表达式的部分。剩下的看下面的链接即可。&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/19200513/function-pointer-vs-function-reference&lt;/p&gt;
</content:encoded></item><item><title>C++杂记 - 3（三）：锁、Futex、CAS 与若干语言细节</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03-3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03-3/</guid><pubDate>Tue, 06 Sep 2022 06:57:00 GMT</pubDate><content:encoded>&lt;h1&gt;lockguard和uniquelock&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;注意一下锁的含义。为什么锁叫互斥量？不要认为锁和资源是挨着的。或者是资源一定和锁连着。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;锁，互斥量是独立的。什么意思？假设我们有规定：想要打开抽屉，必须从桌子上拿走令牌。如果没有令牌则不能打开抽屉。所以说资源（抽屉）和锁（互斥量）不一定在一起。资源在抽屉里，锁在桌子上。两者是分离的。我只要确保每个人都可以访问到令牌（锁）和抽屉（资源）即可。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unique_lock&lt;/code&gt;功能丰富灵活得多。如果需要实现更复杂的锁策略可以用&lt;code&gt;unique_lock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果只需要基本的锁功能，优先使用更严格高效的&lt;code&gt;lock_guard&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意区分&lt;code&gt;mutex&lt;/code&gt;和&lt;code&gt;lock_guard&lt;/code&gt;/&lt;code&gt;unique_lock&lt;/code&gt;。后者是前者的RAII包装器。所以如果要锁的话必须这样&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mutex lock;
unique_lock&amp;lt;mutex&amp;gt; lkc(lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两种锁的简单概述与策略对比见下表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;类模板&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;描述&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;策略&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;std::lock_guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;严格基于作用域(scope-based)的锁管理类模板，构造时是否加锁是可选的(不加锁时假定当前线程已经获得锁的所有权—使用std::adopt_lock策略)，析构时自动释放锁，&lt;strong&gt;所有权不可转移，对象生存期内不允许手动加锁和释放锁&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;std::adopt_lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;std::unique_lock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;更加灵活的锁管理类模板，构造时是否加锁是可选的，在对象析构时如果持有锁会自动释放锁，&lt;strong&gt;所有权可以转移。对象生命期内允许手动加锁和释放锁&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;std::adopt_lock std::defer_lock std::try_to_lock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20220926064910.png&quot; alt=&quot;QQ截图20220926064910&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20220926064919.png&quot; alt=&quot;QQ截图20220926064919&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;lock_guard&lt;/code&gt;不能手动解锁&lt;/strong&gt;。这也是为什么使用条件变量的时候互斥锁一定要使用unique_lock，因为wait函数内部会进行解锁。详细查看项目相关的条件变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lock_guard&lt;/code&gt;不能创建时不锁定，也就是没有&lt;code&gt;defer&lt;/code&gt;功能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;std::lock&lt;/code&gt;对多个&lt;code&gt;unique_lock&lt;/code&gt;一起加锁可以避免死锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同时，只要传入的多个锁中的一个发生了异常，则所有已锁定的对象都会被解锁。也就是要么全都锁，要么全都不锁。&lt;/li&gt;
&lt;li&gt;原理其实也是一个包装器。遵循了&lt;em&gt;&lt;strong&gt;多线程按照同一个顺序对多个锁进行加锁一定不会死锁&lt;/strong&gt;&lt;/em&gt; 这一个唯一原则。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;shared_lock&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;shared_lock&lt;/code&gt;本身和&lt;code&gt;unique_lock&lt;/code&gt;的接口差不多.&lt;/p&gt;
&lt;p&gt;一般我们是用&lt;code&gt;shared_lock&lt;/code&gt;管理共享互斥量. 如&lt;code&gt;shared_mutex&lt;/code&gt;. 注意, 非共享互斥量不可以用&lt;code&gt;shared_lock&lt;/code&gt;管理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;mutex&amp;gt;    //unique_lock
#include &amp;lt;shared_mutex&amp;gt; //shared_mutex shared_lock
#include &amp;lt;thread&amp;gt;

std::mutex mtx;

class ThreadSaferCounter
{
private:
    mutable std::shared_mutex mutex_; // 这个mutable的作用是在下面的const成员函数中可以修改它. 更多查看杂记2
    unsigned int value_ = 0;
public:
    ThreadSaferCounter(/* args */) {};
    ~ThreadSaferCounter() {};
    
    unsigned int get() const {
        //读者, 获取共享锁, 使用shared_lock
        std::shared_lock&amp;lt;std::shared_mutex&amp;gt; lck(mutex_);//等同于执行mutex_.lock_shared();
        return value_;  //lck 析构, 执行mutex_.unlock_shared();
    }

    unsigned int increment() {
        //写者, 获取独占锁, 使用unique_lock
        std::unique_lock&amp;lt;std::shared_mutex&amp;gt; lck(mutex_);//等同于执行mutex_.lock();
        value_++;   //lck 析构, 执行mutex_.unlock();
        return value_;
    }

    void reset() {
        //写者, 获取独占锁, 使用unique_lock
        std::unique_lock&amp;lt;std::shared_mutex&amp;gt; lck(mutex_);//等同于执行mutex_.lock();
        value_ = 0;   //lck 析构, 执行mutex_.unlock();
    }
};
ThreadSaferCounter counter;
void reader(int id){
    while (true)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::unique_lock&amp;lt;std::mutex&amp;gt; ulck(mtx);//cout也需要锁去保护, 否则输出乱序
        std::cout &amp;lt;&amp;lt; &quot;reader #&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot; get value &quot; &amp;lt;&amp;lt; counter.get() &amp;lt;&amp;lt; &quot;\n&quot;;
    }    
}

void writer(int id){
    while (true)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::unique_lock&amp;lt;std::mutex&amp;gt; ulck(mtx);//cout也需要锁去保护, 否则输出乱序
        std::cout &amp;lt;&amp;lt; &quot;writer #&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot; write value &quot; &amp;lt;&amp;lt; counter.increment() &amp;lt;&amp;lt; &quot;\n&quot;;
    }
}

int main()
{
    std::thread rth[10];
    std::thread wth[10];
    for(int i=0; i&amp;lt;10; i++){
        rth[i] = std::thread(reader, i+1);
    }
    for(int i=0; i&amp;lt;10; i++){
        wth[i] = std::thread(writer, i+1);
    }

    for(int i=0; i&amp;lt;10; i++){
        rth[i].join();
    }
    for(int i=0; i&amp;lt;10; i++){
        wth[i].join();
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过实例我们可以得知. &lt;strong&gt;真正的锁本身其实是互斥量&lt;/strong&gt;. 互斥量本身需要是&lt;code&gt;shared_mutex&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;所以:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读锁使用&lt;code&gt;shared_lock&lt;/code&gt;锁定&lt;code&gt;shared_mutex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;写锁使用&lt;code&gt;unique_lock&lt;/code&gt;锁定.&lt;code&gt;shared_mutex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/princeteng/article/details/103952888&quot;&gt;参考来自&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;互斥锁的底层实现&lt;/h1&gt;
&lt;p&gt;https://zhiqiang.org/coding/std-mutex-implement.html&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存中准备一个表示锁定状态的整数&lt;/li&gt;
&lt;li&gt;使用CAS操作来尝试修改值（上锁）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供一个函数让互斥锁在被锁定的情况下等待。Linux里面这个操作是&lt;code&gt;futex&lt;/code&gt;系统调用&lt;/strong&gt;。这会将线程放入队列中，并监视内存中的整数。&lt;/li&gt;
&lt;li&gt;可能包括防止指令重排的内存屏障&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Futex&lt;/h1&gt;
&lt;h2&gt;为什么要有futex&lt;/h2&gt;
&lt;p&gt;我们已经有了互斥锁和自旋锁。我们也知道了它们的优缺点。自旋锁的主要缺点是在等待时浪费CPU资源。互斥锁的主要缺点是就算无需等待（争抢），也需要进行上下文切换进行系统调用。那么我们为何不将二者的缺点去掉，保留优点呢？所以Futex就来了。&lt;/p&gt;
&lt;p&gt;Futex是一种用户态和内核态混合的同步机制。同步的进程间通过mmap共享一段内存，futex变量就位于这段共享的内存中且操作是原子的，当进程尝试进入互斥区或者退出互斥区的时候，先去查看共享内存中的futex变量，&lt;strong&gt;如果没有竞争发生，则只修改futex&lt;/strong&gt;，而不用再执行系统调用，仅当通过访问futex变量告诉进程有竞争发生时，才执行系统调用去完成相应的处理。总结就是：&lt;strong&gt;使用一条原子指令，如果上锁成功立即返回。如果上锁失败，执行系统调用进行睡眠。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，同步的进程间通过&lt;code&gt;mmap&lt;/code&gt;共享一段内存，&lt;code&gt;futex&lt;/code&gt;变量就位于这段共享的内存中。且操作是原子的&lt;/li&gt;
&lt;li&gt;当进程尝试进入互斥区或者退出互斥区的时候，先去查看共享内存中的&lt;code&gt;futex&lt;/code&gt;变量，如果没有竞争发生，则只修改&lt;code&gt;futex&lt;/code&gt;,而不 用再执行系统调用了。&lt;/li&gt;
&lt;li&gt;当通过访问&lt;code&gt;futex&lt;/code&gt;变量告诉进程有竞争发生，则还是得执行系统调用去完成相应的处理(&lt;code&gt;wait&lt;/code&gt; 或者 &lt;code&gt;wake up&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;当进程尝试持有锁或者要进入互斥区的时候，对&lt;code&gt;futex&lt;/code&gt;执行&quot;down&quot;操作，即原子性的给&lt;code&gt;futex&lt;/code&gt;同步变量减1
&lt;ul&gt;
&lt;li&gt;如果同步变量变为0，则没有竞争发生， 进程照常执行。无需进入内核态进行系统调用。&lt;/li&gt;
&lt;li&gt;如果同步变量是个负数，则意味着有竞争发生，需要调用&lt;code&gt;futex&lt;/code&gt;系统调用的&lt;code&gt;futex_wait&lt;/code&gt;操作休眠当前进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当进程释放锁或者要离开互斥区的时候，对&lt;code&gt;futex&lt;/code&gt;进行&quot;up&quot;操作，即原子性的给&lt;code&gt;futex&lt;/code&gt;同步变量加1。
&lt;ul&gt;
&lt;li&gt;如果同步变量由0变成1，则没有竞争发生，进程照常执行。无需进入内核态进行系统调用。&lt;/li&gt;
&lt;li&gt;如果加之前同步变量是负数，则意味着有竞争发生，需要调用&lt;code&gt;futex&lt;/code&gt;系统调用的&lt;code&gt;futex_wake&lt;/code&gt;操作唤醒一个或者多个等待进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//uaddr指向一个地址，val代表这个地址期待的值，当*uaddr==val时，才会进行wait
int futex_wait(int *uaddr, int val);

//唤醒n个在uaddr指向的锁变量上挂起等待的进程
int futex_wake(int *uaddr, int n);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;futex_wait&lt;/code&gt;是用来协助加锁操作的。线程调用&lt;code&gt;pthread_mutex_lock&lt;/code&gt;，如果发现锁的值不是0，就会调用&lt;code&gt;futex_wait&lt;/code&gt;，告知内核，线程须要等待在&lt;code&gt;uaddr&lt;/code&gt;对应的锁上，请将线程挂起。&lt;strong&gt;内核会建立与&lt;code&gt;uaddr&lt;/code&gt;地址对应的等待队列。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么需要内核维护等待队列？因为一旦互斥量的持有者线程释放了互斥量，就需要及时通知那些等待在该互斥量上的线程。如果没有等待队列，内核将无法通知到那些正陷入阻塞的线程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果整个系统有很多这种互斥量，是不是需要为每个&lt;code&gt;uaddr&lt;/code&gt;地址建立一个等待队列呢？事实上不需要&lt;/strong&gt;。理论上讲，&lt;code&gt;futex&lt;/code&gt;只需要在内核之中维护一个队列就够了，当线程释放互斥量时，可能会调用&lt;code&gt;futex_wake&lt;/code&gt;，此时会将&lt;code&gt;uaddr&lt;/code&gt;传进来，内核会去遍历该队列，查找等待在该&lt;code&gt;uaddr&lt;/code&gt;地址上的线程，并将相应的线程唤醒。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;futex_wake&lt;/code&gt;操作是用来实现解锁操作的。glibc就是使用该操作来实现互斥量的解锁函数&lt;code&gt;pthread_mutex_unlock&lt;/code&gt;的。当线程执行完临界区代码，解锁时，内核需要通知那些正在等待该锁的线程。这时候就需要发挥&lt;code&gt;futex_wake&lt;/code&gt;操作的作用了。&lt;code&gt;futex_wake&lt;/code&gt;的第二个参数&lt;code&gt;n&lt;/code&gt;，表明需要唤醒几个睡眠的线程。对于互斥量而言，该值总是1，表示唤醒1个线程。当然，也可以唤醒所有正在等待该锁的线程，但是这样做并无好处，因为被唤醒的多个线程会再次竞争，却只能有一个线程抢到锁，这时其他线程不得不再次睡去，徒增了很多开销。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230423212039.png&quot; alt=&quot;QQ截图20230423212039&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然而，实际上，我们会使用下面这个大的&lt;code&gt;futex&lt;/code&gt;系统调用，通过置入选项的方式来决定是调用&lt;code&gt;futex_wait&lt;/code&gt;还是&lt;code&gt;futex_wake&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;long syscall(SYS_futex, uint32_t *uaddr, int futex_op, uint32_t val,
                    const struct timespec *timeout,   /* or: uint32_t val2 */
                    uint32_t *uaddr2, uint32_t val3);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;futex无论在什么平台都是32位。同时，第二个参数&lt;code&gt;futex_op&lt;/code&gt;用来指明到底要等待还是唤醒。&lt;/p&gt;
&lt;h2&gt;Futex的等待队列&lt;/h2&gt;
&lt;p&gt;我们说过，&lt;code&gt;futex&lt;/code&gt;变量创建于用户空间，在进程或线程间共享，当进程或线程想要进入临界区时，通常会判断&lt;code&gt;futex&lt;/code&gt;变量是否满足条件，若满足则成功进入临界区，否则则阻塞在该&lt;code&gt;futex&lt;/code&gt;变量上；当进程或线程将要离开临界区时，则会唤醒阻塞在&lt;code&gt;futex&lt;/code&gt;变量上的其他进程或线程。&lt;strong&gt;在内核中通过&lt;code&gt;struct futex_q&lt;/code&gt;结构将一个&lt;code&gt;futex&lt;/code&gt;变量与一个挂起的进程（线程）关联起来&lt;/strong&gt;，其定义以及关键成员的作用如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct futex_q {
    struct plist_node list;        //链表节点
    struct task_struct *task;      //挂起在该futex变量关联的进程（线程）
    spinlock_t *lock_ptr;          //自旋锁，控制链表访问
    union futex_key key;           //futex变量地址标识

    //下面三个与优先级继承相关，在此不多介绍
    struct futex_pi_state *pi_state;
    struct rt_mutex_waiter *rt_waiter;
    union futex_key *requeue_pi_key;
     
    u32 bitset;                    //类似掩码匹配
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;内核中通过一个全局哈希表来维护所有挂起阻塞在&lt;code&gt;futex&lt;/code&gt;变量上的进程（线程），不同的&lt;code&gt;futex&lt;/code&gt;变量会根据其地址标识计算出一个hash key并定位到一个bucket上，因此挂起阻塞在同一个&lt;code&gt;futex&lt;/code&gt;变量的所有进程（线程）会对应到同一个bucket上，数据结构如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//bucket
struct futex_hash_bucket {
    //当前自旋等待哈希桶的waiter数目
	atomic_t waiters;

    //自旋锁，用于控制chain的访问，
    //struct futex_q中lock_ptr，就是引用其所在的bucket的自旋锁
	spinlock_t lock;

	//优先级链，与传统等待队列不同，futex使用优先级链表来实现等待队列，
    //是为了实现优先级继承，从而解决优先级翻转问题
	struct plist_head chain;
} ____cacheline_aligned_in_smp;

//全局哈希表
static struct {
	struct futex_hash_bucket *queues;
	unsigned long            hashsize;
} __futex_data __read_mostly __aligned(2*sizeof(long));
#define futex_queues   (__futex_data.queues)
#define futex_hashsize (__futex_data.hashsize)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考来自&lt;a href=&quot;https://www.openeuler.org/zh/blog/wangshuo/Linux_Futex_Principle_Analysis/Linux_Futex_Principle_Analysis.html&quot;&gt;这里&lt;/a&gt;，&lt;a href=&quot;https://pzh2386034.github.io/Black-Jack/pthread/2020/02/15/linux%E9%94%81%E5%AE%9E%E7%8E%B0-futex%E5%86%85%E6%A0%B8%E5%AE%9E%E7%8E%B0/&quot;&gt;这里&lt;/a&gt;，Linux环境编程 7.7.4 和 &lt;a href=&quot;https://www.bilibili.com/video/BV1HN41197Ko/?p=9&amp;amp;share_source=copy_web&amp;amp;vd_source=d59bd34fbf591f4881d60ad6a8742eba&quot;&gt;南京大学操作系统课程2021P9&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;futex 的基本思想是&lt;strong&gt;竞争态总是很少发生的，只有在竞争态才需要进入内核，否则在用户态即可完成&lt;/strong&gt;。futex的两个目标是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尽量避免系统调用；(因为需要切换到内核态。比如线程的挂起和唤醒都需要切换至内核态)&lt;/li&gt;
&lt;li&gt;避免不必要的上下文切换（导致的TLB失效等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;不同操作系统的futex区别&lt;/h2&gt;
&lt;p&gt;https://outerproduct.net/futex-dictionary.html&lt;/p&gt;
&lt;p&gt;当做扩展阅读看看就行&lt;/p&gt;
&lt;h1&gt;自旋锁的底层实现&lt;/h1&gt;
&lt;p&gt;区别就是不挂起，一直使用CAS尝试上锁。&lt;/p&gt;
&lt;h1&gt;自旋锁和互斥锁的区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;自旋锁是一种非阻塞锁&lt;/strong&gt;，也就是说，&lt;strong&gt;如果某线程需要获取自旋锁，但该锁已经被其他线程占用时，该线程不会被挂起，而是在不断的消耗CPU的时间，不停的试图获取自旋锁&lt;/strong&gt;。&lt;strong&gt;不会引起切换&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;互斥量是阻塞锁，当某线程无法获取互斥量时，该线程会被直接挂起，该线程不再消耗CPU时间，当其他线程释放互斥量后，操作系统会激活那个被挂起的线程，让其投入运行&lt;/strong&gt;。&lt;strong&gt;会引起切换&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两种锁适用于不同场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是多核处理器，&lt;strong&gt;如果预计线程等待锁的时间很短，短到比线程两次上下文切换时间要少的情况下，使用自旋锁是划算的。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果是多核处理器，&lt;strong&gt;如果预计线程等待锁的时间较长，至少比两次线程上下文切换的时间要长，建议使用互斥量。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果是单核处理器，一般建议不要使用自旋锁。因为，在同一时间只有一个线程是处在运行状态，那如果运行线程发现无法获取锁，只能等待解锁，但因为自身不挂起，所以那个获取到锁的线程没有办法进入运行状态，只能等到运行线程把操作系统分给它的时间片用完，才能有机会被调度。这种情况下使用自旋锁的代价很高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果加锁的代码经常被调用，但竞争情况很少发生时，应该优先考虑使用自旋锁，自旋锁的开销比较小，互斥量的开销较大&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自旋锁的真正使用场景应该是操作系统内核的并发数据结构。&lt;/p&gt;
&lt;h1&gt;可重入锁和不可重入锁&lt;/h1&gt;
&lt;p&gt;简单来说我们说过同一个线程对一个锁上锁两次会被死锁。因为第二次进行上锁的时候会尝试拿锁，但是一直拿不到。所以线程会被挂起。除非其他线程帮我们解锁。（查看笔试题整理）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可重入锁（reentrant lock）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可重入锁简单如字面而言，就是可以重新进入的锁，允许同一进程多次获取同一把锁，是可重复可递归调用的锁，在外层使用锁之后，在内层仍然可以使用，并且不发生死锁（前提是同一个对象或者class），这样的锁就叫做可重入锁，也叫做递归锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;recursive_mutex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不可重入锁（non-reentrant lock）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可重入锁与可重入锁相反，如果当前线程已经获取了该锁，那么再次尝试获取该锁时，就会出现死锁的状况，被阻塞。在这个线程解锁之前，其他线程无法用这个锁再来加锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可重入锁不与可重入锁的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不可重入锁只判断这个锁有没有被锁上&lt;/strong&gt;，只要被锁上申请锁的线程都会被要求等待。实现起来较为简单。而&lt;strong&gt;可重入锁不仅要判断锁有没有被锁上，还会判断锁是谁锁上的，当就是自己锁上的时候，那么他依旧可以再次访问临界资源，并把加锁次数加一。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;CAS 的底层实现&lt;/h1&gt;
&lt;p&gt;汇编指令&lt;code&gt;LOCK CMPXCHG &lt;/code&gt;让CPU锁住cache line。获取该cache line的独占权来针对原子变量进行原子操作。&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/kendoziyu/p/16571887.html&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/27837731/is-x86-cmpxchg-atomic-if-so-why-does-it-need-lock&lt;/p&gt;
&lt;h1&gt;CAS 的缺点&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自旋时间太长&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果CAS一直不成功呢？这种情况绝对有可能发生，如果自旋CAS长时间地不成功，则会给CPU带来非常大的开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只能保证一个共享变量原子操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看了CAS的实现就知道这只能针对一个共享变量，如果是多个共享变量就只能使用锁了，当然如果你有办法把多个变量整成一个变量，利用CAS也不错。例如读写锁中state的高低位。（https://www.cnblogs.com/wait-pigblog/p/9350569.html）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ABA问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CAS需要检查操作值有没有发生改变，如果没有发生改变则更新。但是存在这样一种情况：**如果一个值原来是A，变成了B，然后又变成了A，那么在CAS检查的时候会发现没有改变，但是实质上它已经发生了改变，只是又回到了原来的值而已，这就是所谓的ABA问题。**对于ABA问题其解决方案是加上版本号，即在每个变量都加上一个版本号，每次改变时加1，即A —&amp;gt; B —&amp;gt; A，变成1A —&amp;gt; 2B —&amp;gt; 3A。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;实现一个简单的自旋锁&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;

class SpinLock {

public:
    SpinLock() : flag_(false)
    {}

    void lock()
    {
        bool expect = false; //注意，这里就是为什么这个代码不会发生竞争。这里的expect是局部变量
        while (!flag_.compare_exchange_weak(expect, true))
        {
            //这里一定要将expect复原，因为如果当前flag是true，expect是false，不相等的话exchange会把expect换成当前flag值也就是换成true。如果不换回去就拉稀了。
            expect = false;
        }
    }

    void unlock()
    {
        flag_.store(false);
    }

private:
    std::atomic&amp;lt;bool&amp;gt; flag_;
};

int main(){
    SpinLock myLock;
    myLock.lock();

    //do something

    myLock.unlock();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看一下原理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先我们初始设定类内的原子变量为false。&lt;/li&gt;
&lt;li&gt;当我们上锁的时候：
&lt;ol&gt;
&lt;li&gt;第一个线程进来，进行&lt;code&gt;compare_exchange_weak&lt;/code&gt;判断。此时发现当前值和&lt;code&gt;expect&lt;/code&gt;值相同。则设定当前值为&lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;又因为返回值不为&lt;code&gt;false&lt;/code&gt;，不进入&lt;code&gt;while&lt;/code&gt;循环。跳出。&lt;/li&gt;
&lt;li&gt;剩下的线程进来的时候，发现当前值是&lt;code&gt;true&lt;/code&gt;，和&lt;code&gt;expect&lt;/code&gt;值不同。则修改期望值（&lt;code&gt;expect&lt;/code&gt;）为当前值也就是&lt;code&gt;true&lt;/code&gt;。同时返回值是&lt;code&gt;false&lt;/code&gt;，进入&lt;code&gt;while&lt;/code&gt;循环忙等待。&lt;/li&gt;
&lt;li&gt;此时进入循环后必须继续将&lt;code&gt;expect&lt;/code&gt;设置为&lt;code&gt;false&lt;/code&gt;。因为我们之前把&lt;code&gt;expect&lt;/code&gt;修改为&lt;code&gt;true&lt;/code&gt;了&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;直到拿到锁的线程调用&lt;code&gt;unlock&lt;/code&gt;，设置当前值为&lt;code&gt;false&lt;/code&gt;。这时候其他线程之一发现，当前值和&lt;code&gt;expect&lt;/code&gt;值相同，继续重复第二步&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;注意，我们的&lt;code&gt;expect&lt;/code&gt;是&lt;code&gt;lock&lt;/code&gt;函数内的局部变量。&lt;strong&gt;意思就是每一个进入到该函数的线程都会有一个自己独有的&lt;code&gt;expect&lt;/code&gt;&lt;/strong&gt;。所以不会发生一个线程修改&lt;code&gt;expect&lt;/code&gt;是&lt;code&gt;true&lt;/code&gt;之后，在修改回&lt;code&gt;false&lt;/code&gt;之前另一个线程进来发现已经是&lt;code&gt;true&lt;/code&gt;了直接就拿到了锁的这个情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.cnblogs.com/FateTHarlaown/p/9170474.html&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更为精简和高性能的自旋锁：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SpinLock{
    public:
        SpinLock() = default;
        
        void lock(){
            while(_flag.test_and_set(std::memory_order_acquire));
        }
        void unlock(){
            _flag.clear(std::memory_order_release);
        }

    private:
        std::atomic_flag _flag = ATOMIC_FLAG_INIT;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首先我们把&lt;code&gt;atomic&lt;/code&gt;变量换成了简单的&lt;code&gt;atomic_flag&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;注意，在&lt;code&gt;lock&lt;/code&gt;函数中，&lt;code&gt;test_and_set&lt;/code&gt;的作用是给&lt;code&gt;flag&lt;/code&gt;原子赋值为&lt;code&gt;true&lt;/code&gt;并返回之前的值。也就是说当有线程抢锁的时候，发现当前的&lt;code&gt;flag&lt;/code&gt;已经是&lt;code&gt;true&lt;/code&gt;了，就再次设定为&lt;code&gt;true&lt;/code&gt;并且返回当前的&lt;code&gt;true&lt;/code&gt;值。&lt;code&gt;while&lt;/code&gt;循环就是要判断如果返回的是&lt;code&gt;true&lt;/code&gt;就一直循环等待。&lt;/li&gt;
&lt;li&gt;内存序更为合适：我们反复强调了&lt;code&gt;acquire&lt;/code&gt;理解为&lt;code&gt;lock&lt;/code&gt;。也就是针对这一行原子操作之后的操作不允许前移。&lt;code&gt;release&lt;/code&gt;操作理解为&lt;code&gt;unlock&lt;/code&gt;，也就是针对这一行原子操作之前的操作不允许后移。如果使用全序模型则会降低性能。但是这里我们并不阻止合法的指令重排。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以对任何带有&lt;code&gt;lock()&lt;/code&gt;和&lt;code&gt;unlock()&lt;/code&gt;函数的互斥类使用&lt;code&gt;lock_guard&lt;/code&gt;。看一眼源码就知道了。&lt;/p&gt;
&lt;h1&gt;前向声明&lt;/h1&gt;
&lt;p&gt;前向声明可以解决两个类互相包含的问题。因为前向声明属于不完整类型，所以有如下限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将成员声明为指向不完整类型的&lt;strong&gt;指针或引用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明&lt;/strong&gt;接受/返回不完整类型的函数或方法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;接受/返回指向不完整类型的指针/引用的函数或方法（但不使用其成员）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不可以&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将其用作基类&lt;/li&gt;
&lt;li&gt;用它来声明一个成员（使用对象）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用该类型定义函数或方法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;使用其方法或字段，实际上试图解引用类型不完整的变量
&lt;ul&gt;
&lt;li&gt;这里就是使用这个前向声明类型的指针然后对其解引用使用其类型包含的数据或方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实际上只需要分文件或者类外编写函数的定义就可以了。也就是实际使用的时候等到类已经被正式定义完毕就可以了。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;delete 作用于不完整类型是不合法的&lt;/h2&gt;
&lt;p&gt;https://en.cppreference.com/w/cpp/language/delete.html&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果被删除的对象在删除时具有不完整的类类型，并且完整的类具有非平凡的析构函数或释放函数，则行为未定义（直到 C++26）程序格式不正确（自 C++26 起）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;模板相关&lt;/h1&gt;
&lt;h2&gt;类模板定义规则&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;注意c++类模板定义方法 类模板没有自动类型推导。所以只要使用了就必须显式指定参数类型。&lt;/li&gt;
&lt;li&gt;但是类模板可以有默认参数和偏特化&lt;/li&gt;
&lt;li&gt;类模板中的成员函数只有在调用的时候才会被创建。因为在编译阶段，编译器无法确认模板的参数类型，所以无法创建模板类成员函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename 模板参数表&amp;gt;
class 类名{
    // 类定义．．．．．．
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意全特化和偏特化的语法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class test{
    T1 data1;
    T2 data2;
};
//全特化
template&amp;lt;&amp;gt;
class test&amp;lt;int, float&amp;gt;{
    int data1;
};
//偏特化
template&amp;lt;typename T2&amp;gt;
class test&amp;lt;int, T2&amp;gt;{
    int data1;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模板调用顺序 规则&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;类模板： 对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是：全特化类&amp;gt;偏特化类&amp;gt;主版本模板类。这样的优先级顺序对性能也是最好的。&lt;/li&gt;
&lt;li&gt;函数模板： 如果函数模板和普通函数都可以实现，优先调用普通函数 &lt;strong&gt;可以使用空模板参数列表来强制调用函数模板&lt;/strong&gt; 函数模板也可以重载 如果函数版可以产生更好的匹配，则优先调用函数模板.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数模板只能全特化&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void test(int a, int b){
    cout &amp;lt;&amp;lt; &quot;普通函数&quot; &amp;lt;&amp;lt; endl;
}

template &amp;lt;typename T&amp;gt;
void test(T a, T b){
    cout &amp;lt;&amp;lt; &quot;模板函数&quot; &amp;lt;&amp;lt; endl;
}

template &amp;lt;typename T&amp;gt;
void test(T a, T b, T c){
    cout &amp;lt;&amp;lt; &quot;重载的模板函数&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    test(1,2); //输出普通函数
    test&amp;lt;&amp;gt;(1,2); //使用空模板参数列表 输出模板函数
    test(1,2,3); //输出重载的模板函数
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模板模板参数&lt;/h2&gt;
&lt;p&gt;模板的模板参数的经典应用是在某些自定义指定储存容器类型的时候不指定元素类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最基本的举例&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;template&amp;lt;typename&amp;gt; class container, typename T1, typename T2&amp;gt; // 最基本的举例
class test{
    container&amp;lt;T1&amp;gt; a1;
    container&amp;lt;T2&amp;gt; a2;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;茴字的三种写法, 都可以&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename&amp;gt; class Container&amp;gt;
template &amp;lt;typename T, template &amp;lt;class&amp;gt; class Container&amp;gt;
template &amp;lt;typename T, template &amp;lt;typename&amp;gt; typename Container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;更好的理解的写法:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, template &amp;lt;typename T1&amp;gt; typename Container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;更多例子参见template_template_parameter_*文件&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;模板参数按值传递还是引用传递&lt;/h2&gt;
&lt;p&gt;https://vinkle.top/2021/06/12/cpp-template-7/#7-%E6%8C%89%E5%80%BC%E4%BC%A0%E9%80%92%E8%BF%98%E6%98%AF%E6%8C%89%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92&lt;/p&gt;
&lt;h2&gt;非类型模板参数&lt;/h2&gt;
&lt;p&gt;这块之前居然忽略了。必须简单说一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T obj){
    //...
}

int main(){
    func&amp;lt;int&amp;gt;(5);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们都知道模板参数一般都是类型。比如这里，T就是int。是类型。&lt;/p&gt;
&lt;p&gt;但是如果我们有时候需要一些特殊情况， 例如想要传点奇怪东西的时候。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, int MAXSIZE&amp;gt;
void func1(T obj){
    vector&amp;lt;T&amp;gt;a;
    a.reserve(MAXSIZE);
    cout &amp;lt;&amp;lt; a.capacity() &amp;lt;&amp;lt; endl;
    a.push_back(obj);
    cout &amp;lt;&amp;lt; a[0] &amp;lt;&amp;lt; endl; 			//干啥了不解释了 忽略即可
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;int MAXSIZE&lt;/code&gt;就是非类型模板参数。因为他不是类型，而是变量。&lt;/p&gt;
&lt;p&gt;使用非类型模板参数是有限制的。通常它们只能是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;整型常量或字面值（包含枚举，或可隐式转换的比如&lt;code&gt;bool&lt;/code&gt;）（&lt;code&gt;string&lt;/code&gt; &lt;code&gt;double&lt;/code&gt;都不可以。前者是类对象，后者是浮点数）&lt;/li&gt;
&lt;li&gt;指向对象/函数/成员变量的指针&lt;/li&gt;
&lt;li&gt;对象/函数的左值引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::nullptr_t&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当传递对象的指针或者引用作为模板参数时，对象不能是字符串常量，临时变量或者数据成员以及其他子对象。由于C++17之前，C++每次版本更新都会放宽以上限制，因此还有一些针对不同版本的限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++11中，对象必须要有外部链接&lt;/li&gt;
&lt;li&gt;C++14中，对象必须是外部链接或者内部链接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以：传入的&lt;code&gt;s&lt;/code&gt;必须是常量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int s = 8;	//必须是const
func1&amp;lt;int, s&amp;gt;(5);
func2&amp;lt;int, 4&amp;gt;(5); //或者直接传入字面值。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;搭配类模板偏特化的小例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, bool option&amp;gt;
class myclass; //主模板不实现

template&amp;lt;typename T&amp;gt;
class myclass&amp;lt;T, true&amp;gt;{ //偏特化1，注意语法。T在这里依旧要写上。
    public:
    void func(){
        cout &amp;lt;&amp;lt;&quot;true one&quot; &amp;lt;&amp;lt; endl;
    }
};


template&amp;lt;typename T&amp;gt;
class myclass&amp;lt;T, false&amp;gt;{ //偏特化2，注意语法。T在这里依旧要写上。
    public:
    void func(){
        cout &amp;lt;&amp;lt;&quot;false one&quot; &amp;lt;&amp;lt; endl;
    }
};

int main(){
    const bool myoption = true;
    myclass&amp;lt;int, false&amp;gt; obj; //直接使用字面值
    obj.func();
    myclass&amp;lt;int, myoption&amp;gt; obj1; //或必须用const常量变量。
    obj1.func();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;enable_if&lt;/h2&gt;
&lt;p&gt;简单看一下原型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;bool, typename T=void&amp;gt;
struct enable_if {
};
template &amp;lt;typename T&amp;gt;
struct enable_if&amp;lt;true, T&amp;gt; { ///&amp;lt; 第一个模板参数为 true
  using type = T;           ///&amp;lt; type 才有定义
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;意思就是，前面的表达式为真，后面的类型定义才有意义。&lt;/p&gt;
&lt;p&gt;一般三种用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制函数返回类型&lt;/li&gt;
&lt;li&gt;校验函数模板参数类型&lt;/li&gt;
&lt;li&gt;类型偏特化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里就随便写个控制函数返回类型的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;int stat&amp;gt; //这里是非模板类型参数。上面提到了。注意一下
typename enable_if&amp;lt;stat == 1, bool&amp;gt;::type checkstate(){
    cout &amp;lt;&amp;lt;&quot;type is bool&quot; &amp;lt;&amp;lt; endl;
    return true;
}
template&amp;lt;int stat&amp;gt;
typename enable_if&amp;lt;stat == 0, int&amp;gt;::type checkstate(){
    cout &amp;lt;&amp;lt;&quot;type is int&quot; &amp;lt;&amp;lt; endl;
    return 5;
}

template&amp;lt;bool stat&amp;gt;			//bool也可以。隐式转换为整型了，但是必须要常量。
typename enable_if&amp;lt;stat == true, int&amp;gt;::type checkstate1(){
    cout &amp;lt;&amp;lt;&quot;type is int&quot; &amp;lt;&amp;lt; endl;
    return 5;
}

int main(){
    const int myobj = 1; 	//必须是const
    checkstate&amp;lt;myobj&amp;gt;();	//输出&quot;type is bool&quot; 
    checkstate&amp;lt;0&amp;gt;();		//输出&quot;type is int&quot; 
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：返回类型前必须加&lt;code&gt;typename&lt;/code&gt;来告知&lt;code&gt;enable_if::type&lt;/code&gt;是个类型。还有就是非模板类型参数的限制。&lt;/p&gt;
&lt;h2&gt;函数的变长参数模板&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基本定义&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, typename... Args&amp;gt; 
// 如果函数参数列表中一个参数的类型是一个模板参数包,
// 则此参数也是一个函数参数包
void func(const T&amp;amp; t, const Args&amp;amp;... rest);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;C++17之前的写法：因为没有折叠表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void print (){
//必须要有无参重载，否则会无限递归。也就是最后一次无参无法被调用。
//最后一次被解包后，参数包会为空。所以会调用无参函数。
}
template&amp;lt;typename T, typename... Args&amp;gt;
void print (T firstArg, Args... args)
{
    cout &amp;lt;&amp;lt; firstArg &amp;lt;&amp;lt; endl; //print first argument
    print(args...); // call print() for remaining arguments
}
int main() 
{
    print(1, 1000, &quot;b23&quot;, 1.123, &quot;HahaahaH&quot;, 42);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;C++17之后的写法：有了折叠表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; //要有单参打印的重载。不然会无限调用。
void print(T t){
    cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl;
}


template&amp;lt;typename... Args&amp;gt;
void print(Args... args){
    (print(args),...); //这里外部一定要加括号。注意语法
}


int main() 
{
    print(1, 1000, &quot;b23&quot;, 1.123, &quot;HahaahaH&quot;, 42);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多种变长参数模板可以同时存在 尽管直观看起来会有二义性&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;搭配列表初始化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;函数变长参数模板搭配列表初始化。
&lt;ul&gt;
&lt;li&gt;这个函数会返回一个&lt;code&gt;T&lt;/code&gt;类型的&lt;code&gt;vector&lt;/code&gt;，元素是&lt;code&gt;args&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt;
vector&amp;lt;T&amp;gt; func(const Args&amp;amp;... args){
    return {args...};
}
int main(){
    auto vec = func&amp;lt;int&amp;gt;(1,2,3,4,5,6,7);
    for(auto&amp;amp; i:vec){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }
}
/*
输出：
1
2
3
4
5
6
7
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类的变长参数模板&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基本定义：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Ts&amp;gt; class Variadic;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;其模板实例化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;0个参数的模板类 &lt;code&gt;Variadic&amp;lt;&amp;gt; zero;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个参数的模板类 &lt;code&gt;Variadic&amp;lt;int, double, std::string, std::list&amp;lt;int&amp;gt;&amp;gt; sample;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;模板类对象做为函数参数传入&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class test{
public:
    T1 _a;
    T2 _b;
    test(T1 a, T2 b) : _a(a), _b(b) {}
};

void testprint1(test&amp;lt;int,int&amp;gt;&amp;amp; obj){ //方法1 显式指定传入类型
    cout &amp;lt;&amp;lt; obj._a &amp;lt;&amp;lt; obj._b &amp;lt;&amp;lt; endl;
}

template&amp;lt;typename T1, typename T2&amp;gt; 
void testprint2(test&amp;lt;T1, T2&amp;gt;&amp;amp; obj){ //方法2 使用参数模板
    cout &amp;lt;&amp;lt; obj._a &amp;lt;&amp;lt; obj._b &amp;lt;&amp;lt; endl;
}


template&amp;lt;typename T&amp;gt;
void testprint3(T&amp;amp; obj){ //方法3 利用函数模板的自动推导特性
    cout &amp;lt;&amp;lt; obj._a &amp;lt;&amp;lt; obj._b &amp;lt;&amp;lt; endl;
}

int main(){
    test&amp;lt;int,int&amp;gt; b(5,5);
    testprint1(b);
    testprint2(b);
    testprint3(b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类模板 模板类 的继承 子类调用父类构造函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当有多继承的时候，子类继承的父类必须全部拥有默认无参构造函数。因为创建子类对象时，一定会先调用父类构造函数。如果子类的构造函数没有指明调用父类的有参构造的话，就会调用默认构造。但是一旦类有了一个构造函数，编译器就不会自动生成默认构造函数，导致编译失败。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class test{
public:
    T1 _a;
    T2 _b;

    test(T1 a, T2 b) : _a(a), _b(b) {}

    void showitem(){
        cout &amp;lt;&amp;lt; this-&amp;gt;_a &amp;lt;&amp;lt; &quot;,&quot; &amp;lt;&amp;lt; this-&amp;gt;_b &amp;lt;&amp;lt; endl;
    }


};

template&amp;lt;typename T1, typename T2, typename T3, typename T4&amp;gt;
class test1 : public test&amp;lt;T1, T2&amp;gt;{ //!继承模板父类，子类父类都要是模板类。而且要显式指定父类模板类型。
public:
    // T1 _a;   //!如果子类也有自己的_a 和_b变量的话 那就不能调用父类构造函数赋值。
                //!想一下，子类自己的东西怎么可能让父类构造函数赋值？
                //@所以调用父类构造函数赋值其实相当于给子类的父类成分赋值。
    // T2 _b;
    T3 _c;
    T4 _d;
    //test1(T3 a, T4 b) : _a(a), _b(b) {}
    test1(T1 a, T2 b, T3 c, T4 d) : test&amp;lt;T1, T2&amp;gt;(a,b),_c(c),_d(d) //调用父类构造函数赋值 //!记得调用模板类父类构造函数的时候要显式指定数据类型
    {
        //!可以用构造列表也可以普通构造函数
        // _c = c; 
        // _d = d;
    }
    void showitem(){
        cout &amp;lt;&amp;lt; this-&amp;gt;_a &amp;lt;&amp;lt; &quot;,&quot; &amp;lt;&amp;lt; this-&amp;gt;_b &amp;lt;&amp;lt; &quot;,&quot; &amp;lt;&amp;lt; this-&amp;gt;_c &amp;lt;&amp;lt; &quot;,&quot; &amp;lt;&amp;lt; this-&amp;gt;_d&amp;lt;&amp;lt; endl;
    }

};



int main(){
    test&amp;lt;int,int&amp;gt;* b = new test&amp;lt;int, int&amp;gt;(5,5);
    b-&amp;gt;showitem();
    test1&amp;lt;int, int, char, char&amp;gt;* c = new test1&amp;lt;int, int, char, char&amp;gt;(1,2,&apos;a&apos;,&apos;b&apos;);
    c-&amp;gt;showitem();
    
    //指针和对象都可以
    test&amp;lt;int,int&amp;gt; bb(5,5);
    bb.showitem();
    test1&amp;lt;int, int, char, char&amp;gt; cc (1,2,&apos;a&apos;,&apos;b&apos;);
    cc.showitem();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;普通类的子类调用父类构造函数长这样&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class A {
    public:
        A(){}
        A(int a) :m_a(a) {}
        virtual~A(){}
    int m_a;
};

class B : public A{
    public:
        B(){}
        B(int x):m_b(x){}
        B(int no, int x):A(no),m_b(x){} //子类调用父类构造。必须用初始化列表。
        ~B(){}
        int m_b;
};

int main(){
    B b(5,8);
    cout &amp;lt;&amp;lt; b.m_a &amp;lt;&amp;lt; endl;
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模板类 类模板 成员函数 类外实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
class test{
    T1 _a;
    T2 _b;
    test(T1 a){} //构造1
    test(T1 a, T2 b){} //构造2
    void func1(); //无参函数1
    void func2(T1 a); //有参函数2
};

/*
类模板 成员函数 类外实现
头部要加模板
作用域部分要显式指明数据类型
*/
template&amp;lt;typename T1, typename T2&amp;gt;
test&amp;lt;T1, T2&amp;gt;::test(T1 a) : _a(a){} //构造1 使用初始化列表


template&amp;lt;typename T1, typename T2&amp;gt; 
test&amp;lt;T1, T2&amp;gt;::test(T1 a, T2 b){ //构造2
    _a = a;
    _b = b;
}

template&amp;lt;typename T1, typename T2&amp;gt;
void test&amp;lt;T1, T2&amp;gt;::func1(){ //无参函数1
    cout &amp;lt;&amp;lt;&quot;func1&quot; &amp;lt;&amp;lt; endl;
}

template&amp;lt;typename T1, typename T2&amp;gt;
void test&amp;lt;T1, T2&amp;gt;::func2(T1 a){ //有参函数2
    cout &amp;lt;&amp;lt;&quot;func2&quot; &amp;lt;&amp;lt; endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;万能引用相关&lt;/h2&gt;
&lt;p&gt;参见杂记。&lt;/p&gt;
&lt;h1&gt;auto&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;auto 变量必须在定义时初始化，这类似于const关键字。&lt;/li&gt;
&lt;li&gt;定义在一个auto序列的变量必须始终推导成同一类型。例如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;auto a4 = 10, a5 = 20, a6 = 30;//正确
auto b4 = 10, b5 = 20.0, b6 = &apos;a&apos;;//错误,没有推导为同一类型
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果初始化表达式是引用，则去除引用语义。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a = 10;
int &amp;amp;b = a;
 
auto c = b;//c的类型为int而非int&amp;amp;（去除引用）
c = 100;
cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl; //100
cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl; //10
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; //10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到了即使&lt;code&gt;b&lt;/code&gt;是&lt;code&gt;a&lt;/code&gt;的引用，我们使用&lt;code&gt;b&lt;/code&gt;初始化&lt;code&gt;c&lt;/code&gt;的时候依旧使用了值语义。也就是&lt;code&gt;c&lt;/code&gt;是一个独立变量。更改&lt;code&gt;c&lt;/code&gt;的值不影响&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 10;
int &amp;amp;b = a;

auto &amp;amp;c = b;//c的类型为int&amp;amp;
c = 100;
cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl; //100
cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl; //100
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; //100
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果初始化表达式为const或volatile（或者两者兼有），则除去const/volatile语义。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const int a1 = 10;
auto  b1= a1; //b1的类型为int而非const int（去除const）
const auto c1 = a1;//此时c1的类型为const int
b1 = 100;//合法
c1 = 100;//非法
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果auto关键字带上&amp;amp;，则不去除const语意。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const int a2 = 10;
auto&amp;amp; b2 = a2;//因为auto带上&amp;amp;，故不去除const，b2类型为const int
b2 = 10; //非法
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化表达式为数组时，auto关键字推导类型（退化）为指针。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a3[3] = { 1, 2, 3 };
auto b3 = a3;
cout &amp;lt;&amp;lt; typeid(b3).name() &amp;lt;&amp;lt; endl; //int *
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;若表达式为数组且auto带上&amp;amp;，则推导类型为数组类型&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a7[3] = { 1, 2, 3 };
auto &amp;amp; b7 = a7;
cout &amp;lt;&amp;lt; typeid(b7).name() &amp;lt;&amp;lt; endl; //int[3]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时刻要注意auto并不是一个真正的类型。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;auto仅仅是一个占位符，它并不是一个真正的类型，不能使用一些以类型为操作数的操作符，如sizeof或者typeid&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以我们才会有&lt;code&gt;auto&amp;amp; &lt;/code&gt;, &lt;code&gt;auto&amp;amp;&amp;amp;&lt;/code&gt;，&lt;code&gt;const auto&lt;/code&gt;,&lt;code&gt;const auto&amp;amp;&lt;/code&gt;等等。还可以有&lt;code&gt;auto*&lt;/code&gt;，也可以被cv修饰&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意在使用增强for循环遍历容器元素时，如果声明为auto则最好变成&lt;code&gt;const auto&amp;amp; &lt;/code&gt;防止修改和拷贝。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;使用增强for循环遍历容器元素时，&lt;code&gt;auto&lt;/code&gt;的类型是容器内元素的类型。所以需要加&lt;code&gt;const&lt;/code&gt;或&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://www.volatileint.dev/posts/auto-type-deduction-gauntlet/&quot;&gt;一个测试, 看你是否了解Auto&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;auto&amp;amp;&amp;amp;是万能引用&lt;/h2&gt;
&lt;p&gt;格外注意：auto&amp;amp;&amp;amp;是万能引用，除了从花括号包围的初始化器列表推导时除外&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto&amp;amp;&amp;amp; z = {1, 2, 3};//这个不是万能引用。（初始化器列表的特殊情形）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他细节参见列表初始化。&lt;/p&gt;
&lt;h1&gt;C++17 复制省略技术&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;返回值优化（RVO），即通过将返回值所占空间的分配地点从被调用端转移至调用端的手段来避免拷贝操作。返回值优化包括具名返回值优化（NRVO）与无名返回值优化（URVO），两者的区别在于返回值是具名的局部变量还是无名的临时对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;右值拷贝优化，当某一个类类型的临时对象被拷贝赋予同一类型的另一个对象时，通过直接利用该临时对象的方法来避免拷贝操作。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在一个变量的等号右侧是 &lt;strong&gt;临时变量(prvalue)&lt;/strong&gt; 的时候，这里会用 direct-initalize，而不是尝试使用 copy/move initialize。对于一些没有拷贝构造或移动构造的对象，如&lt;code&gt;unique_ptr&lt;/code&gt;、&lt;code&gt;atomic&lt;/code&gt;，&lt;code&gt;std::array&lt;/code&gt;等可以使用等号初始化了。但是前提是右侧是一个临时对象以满足右值拷贝优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto a = atomic&amp;lt;int&amp;gt;{9};  //c++14 error， C++17 OK
atomic&amp;lt;int&amp;gt;s = 4;//c++14 error， C++17 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注意一个常见误区&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;std::unique_ptr&amp;lt;int&amp;gt; func(int x){
    std::unique_ptr&amp;lt;int&amp;gt; p = std::make_unique&amp;lt;int&amp;gt;(x);
    return {p};

}

std::unique_ptr&amp;lt;int&amp;gt; func2(int x){
    std::unique_ptr&amp;lt;int&amp;gt; p = std::make_unique&amp;lt;int&amp;gt;(x);
    return p;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;func2&lt;/code&gt;可以而&lt;code&gt;func1&lt;/code&gt;编译错误的原因是因为编译器试图将 &lt;code&gt;p&lt;/code&gt;拷贝到一个新的 &lt;code&gt;std::unique_ptr&amp;lt;int&amp;gt;&lt;/code&gt; 中。然而，由于 &lt;code&gt;std::unique_ptr&lt;/code&gt; 不允许拷贝，所以编译器会报错，提示尝试使用被删除的拷贝构造函数。而&lt;code&gt;func2&lt;/code&gt;是使用了移动构造。所以这里和RVO其实没有关系。当然了，如果可行的话编译器会把它优化为NRVO.&lt;/p&gt;
&lt;p&gt;gcc14引入 &lt;code&gt;-Wnrvo&lt;/code&gt; 帮助提醒优化返回值&lt;/p&gt;
&lt;h2&gt;RVO是编译时优化&lt;/h2&gt;
&lt;p&gt;##　理解RVO语义&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;进行复制消除时，实现将被省略的复制/移动 (C++11 起)操作的源和目标单纯地当做指代同一对象的两种不同方式&lt;/strong&gt;，而该对象将在假如不进行优化时两个对象中后被销毁的对象销毁时销毁（但如果被选择的构造函数的形参是对象类型的右值引用，那么该销毁发生于目标对象本应被销毁时） (C++11 起)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意这句话：&lt;code&gt;源和目标单纯地当做指代同一对象的两种不同方式&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如何理解呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj func2(bool s){
    myobj d {2};
    if(s){
        return d;
    }
    d.val = new int(3);
    return d;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如这段代码，&lt;code&gt;myobj&lt;/code&gt;只会被构造一次。原因是编译器发现无论进行何种操作，我们返回的都是&lt;code&gt;d&lt;/code&gt;,那么此时可以进行复制消除。也就是返回出去的返回值其实和&lt;code&gt;d&lt;/code&gt;是同一个实例。有点类似于引用的关系。这种就是NRVO。请注意，在这种情况下，&lt;code&gt;T&lt;/code&gt;必须是有可访问的复制或移动构造函数，即使最终没有调用实际的复制或移动构造函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj func(bool s){
    myobj d {2};
    if(s){
        return d;
    } else {
        return myobj{3};
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码，一定会进行一次构造，而且一定会有一次拷贝或移动。因为&lt;code&gt;d&lt;/code&gt;一定会被构造，但是具体是返回&lt;code&gt;d&lt;/code&gt;还是一个新的临时对象是完全运行时确定的。编译器无法推测。所以这种情况下，要么是构造+移动，（这里的NRVO是不生效的）要么是构造+构造。（这里的第二次构造临时对象是直接构造在返回值里的。所以RVO是生效的。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj func2(bool s){
    if(s){
        return myobj{3};
    }
    myobj d {2};
    return d;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码变得奇怪了。其实是和上一段代码反过来写。但是这次，如果&lt;code&gt;s&lt;/code&gt;是&lt;code&gt;true&lt;/code&gt;， 则RVO生效。所以只有一次构造。&lt;/p&gt;
&lt;p&gt;这一部分可以看一下&lt;a href=&quot;https://artificial-mind.net/blog/2021/10/23/return-moves&quot;&gt;这篇文章&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;stringstream&lt;/h1&gt;
&lt;h2&gt;我们可以使用&lt;code&gt;stringstream&lt;/code&gt;方便的在&lt;code&gt;string&lt;/code&gt;和其他内置类型之间进行转换。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先要&lt;code&gt;#include&amp;lt;sstream&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;然后需要有一个&lt;code&gt;stringstream&lt;/code&gt; 对象&lt;code&gt;stringstream ss&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;我们可以把字符串使用流插入运算符&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;把要转换的字符串插入&lt;code&gt;stringstream&lt;/code&gt; 对象&lt;/li&gt;
&lt;li&gt;使用流提取运算符&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;把转换完成的对象提取至对应类型的变量内&lt;/li&gt;
&lt;li&gt;然后记得把&lt;code&gt;stringstream&lt;/code&gt; 对象清空。&lt;/li&gt;
&lt;li&gt;支持负数自动转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; process(vector&amp;lt;string&amp;gt;&amp;amp; input){
    vector&amp;lt;int&amp;gt; ret;
    stringstream ss; //stringstream对象
    for(int i = 0; i &amp;lt; input.size(); i++){
        ss &amp;lt;&amp;lt; input[i]; //要转换的东西插入对象
        int a;
        ss &amp;gt;&amp;gt; a; //提取至我们想要的对象
        ss.clear(); //清空stringstream对象
        ret.push_back(a);
    }
    return ret;
}
int main(){
    vector&amp;lt;string&amp;gt; s{&quot;1&quot;,&quot;2&quot;,&quot;200&quot;,&quot;-20&quot;,&quot;-10000&quot;};
    vector&amp;lt;int&amp;gt; ret = process(s);
    for(int i = 0; i &amp;lt; ret.size(); i++){
        cout &amp;lt;&amp;lt; ret[i] &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;我们还可以使用stringstream进行默认分割。此处是根据空格，tab和回车换行分割&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void split(const string&amp;amp; str){
    vector&amp;lt;string&amp;gt; item;
    stringstream ss(str); //字符串放入stringstream对象
    string temp; //储存临时分割对象
    while(ss &amp;gt;&amp;gt; temp){ //直到字符耗尽
        item.emplace_back(temp); //放入结果数组。
    }

    for(auto&amp;amp; i:item){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;我们还可以使用stringstream搭配getline进行自定义分割。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void split(const string&amp;amp; str, const char&amp;amp; splitter){
    vector&amp;lt;string&amp;gt; item;
    stringstream ss(str);
    string temp;
    while(getline(ss, temp, splitter)){
        item.emplace_back(temp);
    }
    for(auto&amp;amp; i:item){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;std::initializer_list&lt;/h1&gt;
&lt;p&gt;搭配聚合初始化一起看。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;initializer_list&lt;/code&gt;会在下列情况自动构造：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用花括号初始化器列表列表初始化一个对象，其中对应构造函数接受一个 &lt;code&gt;std::initializer_list&lt;/code&gt; 参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这句话的意思就是使用&lt;code&gt;{}&lt;/code&gt;初始化对象并且有匹配&lt;code&gt;initializer_list&lt;/code&gt;的构造函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以花括号初始化器列表为赋值的右运算数，或函数调用参数，而对应的赋值运算符/函数接受 &lt;code&gt;std::initializer_list&lt;/code&gt; 参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这句话的意思就是使用&lt;code&gt;{}&lt;/code&gt;初始化对象并且有匹配&lt;code&gt;initializer_list&lt;/code&gt;的函数形参&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绑定花括号初始化器列表到 &lt;code&gt;auto&lt;/code&gt;，包括在范围 for 循环中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这句话的意思就是我们说的 &lt;code&gt;auto&lt;/code&gt;在遇到一个用花括号初始化的对象的时候始终会认为其是一&lt;code&gt;std::initializer_list&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;auto t = {1,2,3,4}; //t会被推导为initializer_list

for(auto x:{1,2,3,4}){ //这里的{1,2,3,4}也是initializer_list
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在我们使用大括号进行列表初始化的时候，如果我们有显式的符合对应条件的列表初始化构造函数则会优先匹配。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果我们使用了大括号进行列表初始化，则编译器会强烈的，尽可能的匹配至形参为&lt;code&gt;initializer_list&lt;/code&gt;的构造函数。如果不匹配则会继续查找其他的构造函数。但凡可以匹配至&lt;code&gt;initializer_list&lt;/code&gt;，编译器就会使用这个。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;在构造函数重载决议期间，只要有任何可能，大括号初始化物就会和带有&lt;code&gt;std: : initializer_ list&lt;/code&gt; 型别的形参相匹配，即使其他重载版本有着貌似更加匹配的形参表&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;！！！！！！这里有个大坑！！！！下面说！！！！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意列表初始化只能使用相同类型或可以被转换为相同类型的参数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;匹配&lt;code&gt;initializer_list&lt;/code&gt;构造函数必须使用&lt;code&gt;{}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果有默认构造函数就算有&lt;code&gt;initializer_list&lt;/code&gt;构造函数，但是如果对象构造时入参为空，就算使用了&lt;code&gt;{}&lt;/code&gt;也优先匹配默认构造函数。除非没有默认构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语言规定，形如&lt;code&gt;T obj{}&lt;/code&gt;这样的情况下，应该执行默认构造。因为空的大括号表示的是没有实参，而不是空的&lt;code&gt;initializer_list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果确实想要表达出**空的&lt;code&gt;initializer_list&lt;/code&gt;**的含义，可以使用&lt;code&gt;T obj({})&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果没有默认构造函数，则会匹配&lt;code&gt;initializer_list&lt;/code&gt;构造函数。也就是构造对象时就算没有参数要输入也必须加&lt;code&gt;{}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;initializer_list&lt;/code&gt;构造函数拥有高优先级。但是默认构造函数(default constructor)拥有最高优先级&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
        myclass(){
          cout &amp;lt;&amp;lt;&quot;default&quot; &amp;lt;&amp;lt; endl;
        };
                
        myclass(int x, int y):val1(x), val2(y){
            cout &amp;lt;&amp;lt;&quot;cont0&quot; &amp;lt;&amp;lt; endl;
        }
        myclass(int x, int y, int z):val1(x), val2(y), val3(z){
            cout &amp;lt;&amp;lt;&quot;cont1&quot; &amp;lt;&amp;lt; endl;
        }
        myclass(int x, int y, int z, const string&amp;amp; s):val1(x), val2(y), val3(z), s_m(s){
            cout &amp;lt;&amp;lt;&quot;cont2&quot; &amp;lt;&amp;lt; endl;
        }
        myclass(std::initializer_list&amp;lt;int&amp;gt;list){
            cout &amp;lt;&amp;lt;&quot;initializer_list&quot; &amp;lt;&amp;lt; endl;
        };

        myclass(const myclass&amp;amp; rhs){
            cout &amp;lt;&amp;lt; &quot;copy const&quot; &amp;lt;&amp;lt; endl;
        }

        operator int(){ //用户定义转换函数
            cout &amp;lt;&amp;lt;&quot;user define conversion&quot; &amp;lt;&amp;lt; endl;
            return 0;
        }

        int val1;
        int val2;
        int val3;
        string s_m = &quot;&quot;;
};




int main(){
    myclass obj0{1,2};		//initializer_list 因为使用了大括号
    myclass obj1{1,2,3};	//initializer_list 因为使用了大括号
    myclass obj2{1,2,3, &quot;abcd&quot;};//cont2 虽然使用了大括号但是类型不匹配，所以转而匹配普通的符合条件的构造函数。


    myclass obj4(1,2);	//cont0 因为没有使用大括号
    myclass obj5(1,2,3);//cont1 因为没有使用大括号
    myclass obj6(1,2,3,&quot;abcd&quot;);//cont2 因为没有使用大括号
  
  
  	myclass obj7; //default 注意不能加小括号。那是函数声明。
    myclass obj8{}; //default。就算有initializer_list构造函数，但是如果创建对象是无参的也优先匹配默认无参构造函数因为他有最高优先级。除非没有默认无参构造函数。
    myclass obj9({}); //initializer_list。因为小括号套了大括号，表达出空的initializer_list意愿。


    myclass obj10(obj7); //copy const 无需解释
    myclass obj11{obj7}; //大坑。先user define conversion，再initializer_list
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这里就是刚才说的大坑。注意&lt;code&gt;obj10&lt;/code&gt;和&lt;code&gt;obj11&lt;/code&gt;。匹配至&lt;code&gt;initializer_list&lt;/code&gt;的意愿如此强烈，以至于&lt;code&gt;obj11&lt;/code&gt;是先使用了用户定义转换函数转换为&lt;code&gt;int&lt;/code&gt;，然后匹配至&lt;code&gt;initializer_list&lt;/code&gt;构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特别的，如果&lt;code&gt;initializer_list&lt;/code&gt;构造函数被&lt;code&gt;explicit&lt;/code&gt;修饰，则也需遵照其&lt;code&gt;explicit&lt;/code&gt;禁止隐式类型转换的规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果构造函数所有参数都有默认值，这个构造函数就成了默认构造函数&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;默认构造函数就是要么没有参数，要么所有参数都有默认值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class obj{
    public:
        explicit obj(int a = 10, int b = 20):val1(a), val2(b){}; //禁止隐式类型转换。
    int val1;
    int val2;
};
int main(){
    obj t1; //可以。有默认构造函数。
    obj t2{10,20}; //可以，explicit只能限制拷贝初始化。这里是直接初始化。
    obj t3 = {10,20}; //不可以，禁止隐式类型转换。
    obj t4 = obj{10,20}; //可以，显式类型转换。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意，当一个类中不仅含有普通构造函数，还含有&lt;code&gt;initializer_list&lt;/code&gt;构造函数，那么这个类在使用&lt;code&gt;()&lt;/code&gt;或&lt;code&gt;{}&lt;/code&gt;初始化的时候会有不同的含义。比如&lt;code&gt;vector&lt;/code&gt;。例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj(int x, int y){
            inner_vec.resize(x, y); //可能会有更优雅的解决方案。
        }
        myobj(initializer_list&amp;lt;int&amp;gt;my_list){
            for(auto&amp;amp; j:my_list){
                inner_vec.emplace_back(j);
            }
        }
        vector&amp;lt;int&amp;gt; inner_vec;
};



int main(){
    myobj ob1(2,3); //两个值为3的元素。
    for(auto&amp;amp; i:ob1.inner_vec){
        cout &amp;lt;&amp;lt;i &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt;&quot;----------&quot; &amp;lt;&amp;lt; endl;
    myobj ob2{1,2,3,4,5,6,7}; //把元素1,2,3,4,5,6,7放入vector
    for(auto&amp;amp; i:ob2.inner_vec){
        cout &amp;lt;&amp;lt;i &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原理分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;别他妈瞎研究了。魔法就完了。可以看看它的构造函数长啥样你就知道了。推测是当用&lt;code&gt;{}&lt;/code&gt;进行初始化的时候，首先会创建一个array，并将初始化元素存放起来。然后，调用&lt;code&gt;initializer_list&lt;/code&gt;的构造函数，用array首元素的迭代器和array的元素个数，进行初始化。（https://blog.csdn.net/xiangbaohui/article/details/103609076）。这是汇编层面了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除此之外，还有如下几个注意点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;initializer_list&lt;/code&gt;是一个轻量级的容器类型，内部定义了iterator等容器必需的概念。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;其中有3个成员接口：&lt;code&gt;size()&lt;/code&gt;、&lt;code&gt;begin()&lt;/code&gt;和&lt;code&gt;end()&lt;/code&gt;。遍历时取得的迭代器是只读的，无法修改其中的某一个元素的值；&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;code&gt;initializer_list&lt;/code&gt;而言，它可以接收任意长度的初始化列表，但要求元素必须是同种类型T(或可转换为T)；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;initializer_list&lt;/code&gt;会复制初始化传入的元素至底层的数组。这就是(无编译器优化时)额外开销的来源。但是多个复制的&lt;code&gt;std::initializer_list&lt;/code&gt;共用底层数组空间。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;上面这句话比较难理解。&lt;strong&gt;意思是虽然&lt;code&gt;std::initializer_list&lt;/code&gt;会复制初始化传入的元素，但是复制一个 &lt;code&gt;std::initializer_list&lt;/code&gt; 不会复制其底层对象。只会复制其内部的几个指针而已。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;再次注意，上面加粗的部分是两句话。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1：&lt;code&gt;std::initializer_list&lt;/code&gt;会复制初始化传入的元素。所以它是拷贝进去，然后拷贝出去。开销较大。&lt;a href=&quot;https://youtu.be/MfFzr9qqPDw?t=951&quot;&gt;来自&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2：复制一个 &lt;code&gt;std::initializer_list&lt;/code&gt; 不会复制其底层对象。只会复制其内部的几个指针而已&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所以&lt;code&gt;std::initializer_list&lt;/code&gt; 一个严重问题之一是元素不适用于仅移动类型。比如&lt;code&gt;std::unique_ptr&lt;/code&gt;。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;底层数组是 &lt;code&gt;const T[N]&lt;/code&gt; 类型的&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/implicit_conversion#.E4.B8.B4.E6.97.B6.E9.87.8F.E5.AE.9E.E8.B4.A8.E5.8C.96&quot;&gt;临时&lt;/a&gt;数组，&lt;strong&gt;其中每个元素都从原始初始化器列表的对应元素复制初始化&lt;/strong&gt;（除非窄化转换非法）。底层数组的生存期与任何其他&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/lifetime#.E4.B8.B4.E6.97.B6.E5.AF.B9.E8.B1.A1.E7.9A.84.E7.94.9F.E5.AD.98.E6.9C.9F&quot;&gt;临时对象&lt;/a&gt;相同，除了从数组初始化 initializer_list 对象会延长数组的生存期，恰如&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/reference_initialization#.E4.B8.B4.E6.97.B6.E9.87.8F.E7.94.9F.E5.AD.98.E6.9C.9F&quot;&gt;绑定引用到临时量&lt;/a&gt;（有例外，例如对于初始化非静态类成员）。&lt;strong&gt;底层数组可以分配在只读内存。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因此，通过拷贝构造对象与原对象共享列表中的元素空间。也就是说，&lt;code&gt;initializer_list&lt;/code&gt;的内部并没有内含该array的内容，仅仅是拥有指向array的迭代器。如果对这个容器进行拷贝构造或者拷贝赋值的话，array的内容只有一份，但有两份迭代器指向同一片位置。如果对&lt;code&gt;initializer_list&lt;/code&gt;对象copy一个副本，默认是浅拷贝，此时两个对象指向同一个array。这是危险的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意&lt;code&gt;std::initializer_list&lt;/code&gt;底层是常量数组 &lt;code&gt;const T[N]&lt;/code&gt;。所以不能直接修改其存储的元素。返回的引用也是常量引用。迭代器也是常量迭代器。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::initializer_list&amp;lt;int&amp;gt; func(void){
  int a = 1, b = 2;
  return {a, b};      //a与b是局部变量在func返回后会被释放，但是复制的返回值initializer_list不会创建元素副本。而是和临时对象共享空间。所以
                      //func返回后会被释放，initializer_list内部会存在空悬引用！危险！
                      //正确的做法可以将返回值改为保存副本的容器，如vector&amp;lt;int&amp;gt;
}

//注意下面s1、s2、s3和s4均共享元素空间
initializer_list&amp;lt;string&amp;gt; s1 = { &quot;aa&quot;, &quot;bb&quot;, &quot;cc&quot;, &quot;dd&quot; };
initializer_list&amp;lt;string&amp;gt; s2 = s1;
initializer_list&amp;lt;string&amp;gt; s3(s1);
initializer_list&amp;lt;string&amp;gt; s4;
s4 = s1;

//注意元素会被复制初始化
int a = 200;
int b = 300;
int c = 400;
initializer_list&amp;lt;int&amp;gt; mylist{ a,b,c };
a = 800;
cout &amp;lt;&amp;lt; *mylist.begin() &amp;lt;&amp;lt; endl; //200
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; //800

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;额外注意initializer_list会导致额外开销。但部分可以被编译器优化。&lt;/h2&gt;
&lt;p&gt;https://stackoverflow.com/questions/75291691/why-stdinitializer-list-will-cause-overhead-when-use-it-to-initialize-a-vector&lt;/p&gt;
&lt;p&gt;这东西非常看编译器。不建议深入研究。比如Clang就没第二个move。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;myobj&amp;gt; a{myobj(20)};
/*
const //构建myobj临时对象
mv //这个移动是因为vector有一个构造函数接受std::initializer_list。所以外部的{}花括号初始化器beace-init-list会把它当成std::initializer_list。这个move是为了创建这个std::initializer_list。
mv //这个移动是为了把我们刚才构建好的std::initializer_list值传递给vector的构造函数。可能因为传入的是右值则此时触发移动构造。
copy const //从initializer_list拷贝至容器内。这个是另一个开销。因为std::initializer_list永远复制进来复制出去。所以这个地方不是mv而是copy const。
dest
dest
dest
*/
std::vector&amp;lt;myobj, std::allocator&amp;lt;myobj&amp;gt; &amp;gt; a = std::vector&amp;lt;myobj, std::allocator&amp;lt;myobj&amp;gt; &amp;gt;{std::initializer_list&amp;lt;myobj&amp;gt;{myobj(myobj(myobj(20)))}, std::allocator&amp;lt;myobj&amp;gt;()}; //cpp insight 展开后长成这样。不知道为何有那一大串myobj
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;不使用initializer_list，没有额外开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myobj obj(20);
vector&amp;lt;myobj&amp;gt; a;
a.push_back(obj);
/*
const //构造myobj对象
copy const //拷贝构造至容器。
dest
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://akrzemi1.wordpress.com/2016/07/07/the-cost-of-stdinitializer_list/&lt;/p&gt;
&lt;h1&gt;指针A给指针B赋值的意思是将指针A指向的地址赋给指针B&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;int digit = 5;
int* a = &amp;amp;digit;
int* b = a; //指针a指向的地址赋给指针b 没有新对象产生所以不会调用拷贝构造。
int* c = &amp;amp;*a; //等同于将指针a先解引用（*a = 5），得到指针a指向的的值。然后把这个值取地址(&amp;amp;*a)赋值给指针c。

cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl; //打印b储存的地址。也就是b指向的地址。 也就是变量地址。等同于&amp;amp;digit 和&amp;amp;*b
cout &amp;lt;&amp;lt; &amp;amp;b &amp;lt;&amp;lt; endl; //打印b自己的地址。
cout &amp;lt;&amp;lt; &amp;amp;*b &amp;lt;&amp;lt; endl; //打印b指向的地址的值的地址。等于直接打印指针。
cout &amp;lt;&amp;lt; &amp;amp;digit &amp;lt;&amp;lt; endl; //打印变量地址。等于直接打印指针和&amp;amp;*b
cout &amp;lt;&amp;lt; *b &amp;lt;&amp;lt; endl; //解引用b 打印b指向的地址的值。

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;指针加法&lt;/h1&gt;
&lt;h2&gt;指针直接和数字相加：意思是指针当前指向的地址 + 对应指针类型的大小 * 数字。&lt;/h2&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int * p;
p = (int*)malloc(100); //分配100字节。
cout &amp;lt;&amp;lt; p&amp;lt;&amp;lt; endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。此处输出 0x771410
auto y = p + 1; //把p指向的地址偏移4位（因为int是4字节）后赋值给y。
printf(&quot;%#x&quot;,y); //打印y储存的地址。也就是指向的地址。此处输出 0x771414
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;也就是说指针直接和数字相加，可以理解为用指针操作数组。加几就是将指针往后移动几位。具体偏移量也就是对应对象的大小*移动位数。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;如果想要指定一个具体的偏移量应该怎么办？&lt;/h2&gt;
&lt;p&gt;首先，可以使用转型将指针转换为对应类型。然后再加数字。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj* p;
p = (obj*)malloc(sizeof(int) * 10);
cout &amp;lt;&amp;lt; p&amp;lt;&amp;lt; endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。 此处输出 0x771410
auto x = (char*)p + 1; //把p指向的地址偏移1位后赋值给x。因为char是1位。
printf(&quot;%#x\n&quot;,x); //打印x储存的地址。也就是指向的地址。此处输出 0x771411
auto y = (obj*)p + 1; //把p指向的地址偏移8位后赋值给x。因为obj是指针类型。8位。
printf(&quot;%#x\n&quot;,y); //打印y储存的地址。也就是指向的地址。此处输出 0x771418
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：指针类型转换指的是指针转换为对应类型的指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此处不要使用&lt;code&gt;cout&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;auto x = (char*)p&lt;/code&gt;的意思是把指针&lt;code&gt;p&lt;/code&gt;转为&lt;code&gt;char*&lt;/code&gt;类型的指针。不可以&lt;code&gt;auto x = (char)p&lt;/code&gt;。 这样做的意思是直接转换成了&lt;code&gt;char&lt;/code&gt;。当然，所有的指针都是8字节。记住，指针类型指的是告诉指针应该读取指针指向的地址之后多大的数据。&lt;/p&gt;
&lt;p&gt;怎么理解这个操作呢。我们可以想一下。如果 &lt;code&gt;a&lt;/code&gt;是&lt;code&gt;int*&lt;/code&gt;类型，也就是&lt;code&gt;int&lt;/code&gt;类型指针。意思是指针&lt;code&gt;a&lt;/code&gt;指向的数据是&lt;code&gt;int&lt;/code&gt;，所以需要以&lt;code&gt;4&lt;/code&gt;字节为一段进行读取。也就是一次读取四个字节。那么&lt;code&gt;a+1&lt;/code&gt;其实是对地址进行了&lt;code&gt;+4&lt;/code&gt;操作。因为指针+1的意思是将指针往后移动一个&lt;strong&gt;对象位。也就是指针当前指向的地址 + 对应指针类型的大小（此处为4） * 数量（此处为1）&lt;/strong&gt;。但是我们如果想要对地址&lt;code&gt;+1&lt;/code&gt;的话怎么办？我们可以把指针&lt;code&gt;a&lt;/code&gt;转换为&lt;code&gt;char*&lt;/code&gt;类型也就是&lt;code&gt;char&lt;/code&gt;类型指针。而不是&lt;code&gt;int&lt;/code&gt;类型指针。这样相当于告诉编译器，指针&lt;code&gt;a&lt;/code&gt;指向的数据是&lt;code&gt;char&lt;/code&gt;。所以需要以1字节为一段进行读取。也就是一次读取一个字节。所以此时&lt;code&gt;a+1&lt;/code&gt;就是对地址进行了&lt;code&gt;+1&lt;/code&gt;操作。&lt;strong&gt;也就是指针当前指向的地址 + 对应指针类型的大小（此处为1） * 数量（此处为1）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;移动一个对象位也就是移动的偏移量为对象大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int digit = 5;
int* a = &amp;amp;digit;
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
auto x = (char*)a + 1;
printf(&quot;%#x\n&quot;,x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意！&lt;code&gt;(char*)a + 1&lt;/code&gt;往后移动1位的时候，&lt;code&gt;(char*)a&lt;/code&gt;的意思是把指针a看成指向&lt;code&gt;char&lt;/code&gt;类型的指针（告诉编译器一段是1）。所以&lt;code&gt;(char*)a + 1&lt;/code&gt;不是+2，还是+1。因为指针&lt;code&gt;a&lt;/code&gt;指向的地址根本没变。仅仅是看成了&lt;code&gt;char&lt;/code&gt;类型的指针后+1&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;函数入参为指针的时候，指针本身会有浅拷贝&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;void func(int* a){
    cout &amp;lt;&amp;lt; &amp;amp;a &amp;lt;&amp;lt; endl; //打印指针的地址
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;  //打印指针指向的变量的地址
}

int main(){
    int* ptr = new int(5);
    cout &amp;lt;&amp;lt; &amp;amp;ptr &amp;lt;&amp;lt;endl; //打印指针的地址
    cout &amp;lt;&amp;lt; ptr &amp;lt;&amp;lt; endl; //打印指针指向的变量的地址
    func(ptr);
}
/*
输出：
0x61fe18 //指针地址
0x1b1490 //变量地址
0x61fdf0 //指针地址
0x1b1490 //变量地址
*/

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们可以看到，指针入参后，指针本身会发生拷贝。会发生指针给指针赋值。也就是新建了一个指针指向了同一个地址，也就是浅拷贝。所以此时会有两个指针指向同一个地址。一个在&lt;code&gt;func&lt;/code&gt;函数内，一个在&lt;code&gt;main&lt;/code&gt;里。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;函数指针 和 回调函数&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;函数指针&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;typedef int(*ptr)(int, int); //typedef 定义别名
ptr instance = add; //方法一 函数外直接生成示例并赋值。注意这里typedef之后直接把函数赋值过去即可。
ptr test1;
test1 = add; //这里是错的。函数体外只能进行全局函数和变量的声明，而无法执行语句或调用函数。

int add(int a, int b){ //不需要传入函数指针了。
    cout &amp;lt;&amp;lt; a+b &amp;lt;&amp;lt; endl;
    return (a+b);
}

int main(){
    ptr test; //方法二 函数内进行分离声明
    test = add;
    test(5,8);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;回调函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int add(int a, int b){ 
    cout &amp;lt;&amp;lt; a+b &amp;lt;&amp;lt; endl;
    return (a+b);
}

void callback(int(*ptr)(int,int), int a, int b){ //设立callback函数。传入函数指针和参数。
  注意这里如果需要有值传出就要设置为返回int
    ptr(a, b);
}

int main(){
    callback(add, 5, 8);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;回调函数 c++风格&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void callback(int(*ptr)(int, int), int a, int b){ //回调函数 如果需要返回值就return
    ptr(a, b);
}


class test{
    public:
        static int add(int a, int b){ //需要被执行的函数。注意要static。要么就全局。
            cout &amp;lt;&amp;lt; a+b &amp;lt;&amp;lt; endl;
            return (a+b);
        }
        void registure(){ //注册函数
            callback(add, 3, 5);
        }
        
};

int main(){
    test Test;
    Test.registure();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;回调函数 c++多态&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class basecase{
    public:
        virtual void testmsg() = 0;
        virtual ~basecase(){}; //虽然这里没什么卵用但是还是要记得虚析构，不然无法执行子类析构
};

class case1 : public basecase{ //继承
    public:
        void testmsg() override{
            cout &amp;lt;&amp;lt; &quot;Case1 coutmsg&quot; &amp;lt;&amp;lt; endl;
        }
};

class case2 : public basecase{ //继承
    public:
        void testmsg() override{
            cout &amp;lt;&amp;lt; &quot;Case2 coutmsg&quot; &amp;lt;&amp;lt; endl;
        }
};

int main(){
    basecase* test1 = new case1();
    basecase* test2 = new case2();
    test1-&amp;gt;testmsg();
    test2-&amp;gt;testmsg();
    delete test1; //养成良好习惯
    delete test2;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注意c++中的类回调函数必须是静态函数或者是全局函数。&lt;/h2&gt;
&lt;p&gt;一般来说为了封装性质，我们需要把一个函数放入类内。但是调用类成员函数需要加&lt;code&gt;this&lt;/code&gt;，也就是要通过对象调用。但我们不想这样做。于是想舍弃这个&lt;code&gt;this&lt;/code&gt;的话就应该设置为&lt;code&gt;static&lt;/code&gt;静态函数。静态函数就像一个全局函数一样，也就是带作用域的全局函数。满足我们的需求。&lt;/p&gt;
&lt;h2&gt;静态函数只能访问静态对象，想访问非静态数据怎么办？&lt;/h2&gt;
&lt;p&gt;我们可以把回调函数的入参设置为一个类对象的指针或者是一个&lt;code&gt;void*&lt;/code&gt;类型的指针。然后通过这个入参来调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例子1：传入类对象指针&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    static void callback(A *pThis); //静态函数
    void b(){ //非静态函数
        cout &amp;lt;&amp;lt;&quot;b&quot; &amp;lt;&amp;lt; endl;
    }        
}; 
 
void A::callback(A *pThis) //入参为类类型指针
{
    pThis-&amp;gt;b(); //静态函数中调用非静态函数
}
int main(){
    A* aptr = new A();
    A::callback(aptr); //指针传入回调函数。
    delete aptr;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;例子2：传入this指针&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    static void callback(A *pThis); //静态函数
    void b(){ //非静态函数
        cout &amp;lt;&amp;lt;&quot;b&quot; &amp;lt;&amp;lt; endl;
    }       
    void in_class(){
        callback(this); //直接传入this
    } 
}; 
 
void A::callback(A *pThis)
{
    pThis-&amp;gt;b(); //静态函数中调用非静态函数
}
int main(){
    A* aptr = new A();
    aptr-&amp;gt;in_class();
    delete aptr;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子3：使用void*类型做为入参后强转。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    static void callback(void *pThis); //静态函数
    void b(){ //非静态函数
        cout &amp;lt;&amp;lt;&quot;b&quot; &amp;lt;&amp;lt; endl;
    }       
    void in_class(){
        callback(this); //直接传入this
    } 
}; 
 
void A::callback(void *pThis)
{
    ((A*)pThis)-&amp;gt;b(); //静态函数中调用非静态函数。强转
}
int main(){
    A* aptr = new A();
    aptr-&amp;gt;in_class();
    delete aptr;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C++杂记 - 3（二）：修改顺序、指令重排、缓存一致性与内存模型</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03-2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03-2/</guid><pubDate>Tue, 06 Sep 2022 06:56:00 GMT</pubDate><content:encoded>&lt;h1&gt;修改顺序, 指令重排，缓存一致性，内存屏障和内存模型&lt;/h1&gt;
&lt;h2&gt;修改顺序&lt;/h2&gt;
&lt;p&gt;对一个原子变量的所有修改操作总是存在一定的先后顺序, 且所有线程都认可这个顺序, 即使这些修改操作是在不同的线程中执行的. 这个所有线程一致同意的顺序就称为&lt;strong&gt;修改顺序 (modification order)&lt;/strong&gt;. 这意味着&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两个修改操作不可能同时进行, 一定存在一个先后顺序. 这很容易理解, 因为这是原子操作必须保证的, 否则就有数据竞争的问题.&lt;/li&gt;
&lt;li&gt;即使每次运行的修改顺序可能都不同, 但所有线程看到的修改顺序总是一致的. 如果线程 a 看到原子变量 x 由 1 变成 2, 那么线程 b 就不可能看到 x 由 2 变成 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无论使用哪种内存顺序, 原子变量的操作总能满足修改顺序一致性, 即使是最松散的 &lt;code&gt;memory_order_relaxed&lt;/code&gt;. 我们来看一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;int&amp;gt; a{0};

void thread1() {
    for (int i = 0; i &amp;lt; 10; i += 2)
        a.store(i, std::memory_order_relaxed);
}

void thread2() {
    for (int i = 1; i &amp;lt; 10; i += 2)
        a.store(i, std::memory_order_relaxed);
}

void thread3(vector&amp;lt;int&amp;gt; *v) {
    for (int i = 0; i &amp;lt; 10; ++i)
        v-&amp;gt;push_back(a.load(std::memory_order_relaxed));
}

void thread4(vector&amp;lt;int&amp;gt; *v) {
    for (int i = 0; i &amp;lt; 10; ++i)
        v-&amp;gt;push_back(a.load(std::memory_order_relaxed));
}

int main() {
    vector&amp;lt;int&amp;gt; v3, v4;
    std::thread t1(thread1), t2(thread2), t3(thread3, &amp;amp;v3), t4(thread4, &amp;amp;v4);
    t1.join(), t2.join(), t3.join(), t4.join();

    for (int i : v3) cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &quot; &quot;;
    cout &amp;lt;&amp;lt; endl;
    for (int i : v4) cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &quot; &quot;;
    cout &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码创建了 4 个线程. &lt;code&gt;thread1&lt;/code&gt; 和 &lt;code&gt;thread2&lt;/code&gt; 分别将偶数和奇数依次写入原子变量 &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;thread3&lt;/code&gt; 和 &lt;code&gt;thread4&lt;/code&gt; 则读取它们. 最后输出 &lt;code&gt;thread3&lt;/code&gt; 和 &lt;code&gt;thread4&lt;/code&gt; 每次读取到的值. 程序运行的结果可能是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 8 7 7 7 9 9 9 9 9
0 2 8 8 8 7 9 9 9 9
-------------------
1 2 5 6 9 9 9 8 8 8
1 3 2 5 9 8 8 8 8 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然每次运行的修改顺序不同, 各个线程也不太可能看到每次修改的结果, 但是它们看到的修改顺序是一致的. 例如 &lt;code&gt;thread3&lt;/code&gt; 看到的顺序是 8 7 9, 那么&lt;code&gt;thread4&lt;/code&gt;看到的顺序也是8 7 9. &lt;code&gt;thread3&lt;/code&gt;看到的顺序是1 5 9 8 那么 &lt;code&gt;thread4&lt;/code&gt;看到的顺序一样是1 5 9 8&lt;/p&gt;
&lt;h2&gt;指令重排&lt;/h2&gt;
&lt;h3&gt;Happens-Before 关系&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A、B 是两个在多核 CPU 上执行的操作。如果 A &lt;strong&gt;happens-before&lt;/strong&gt; B，那么 A 所产生的内存变化会在 B 操作执行之前被看到（visible）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;简而言之, 如果操作 a “happens-before” 操作 b, 则操作 a 的结果对于操作 b 可见. happens-before 的关系可以建立在用一个线程的两个操作之间, 也可以建立在不同的线程的两个操作之间.&lt;/p&gt;
&lt;p&gt;不管我们使用什么编程语言，在同一个线程下的顺序语句总是遵循 happens-before 原则的。&lt;/p&gt;
&lt;p&gt;就像下面代码所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a, b;

void foo() {
    a = 42;
    b = a;

    assert(b == 42);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在单线程的情况下，断言是永远不会为假的。（不然这还怎么写程序...）&lt;/p&gt;
&lt;p&gt;但这并不代表 b 在内存中后于 a 被修改。就像下面这段伪代码所示的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov %eax, 42
mov (b), %eax
mov (a), %eax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然这并不能说明编译器就是这么处理的，但足以说明程序语义上的 happens-before 不能代表操作是真的 happened before 了。&lt;/p&gt;
&lt;h4&gt;Happens-before 的第一种场景: sequenced before(单线程)&lt;/h4&gt;
&lt;p&gt;单线程的情况很容易理解. 函数的语句按顺序依次执行, 前面的语句先执行, 后面的后执行. 正式地说, 前面的语句总是 &lt;strong&gt;“sequenced-before”&lt;/strong&gt; 后面的语句. 显然, 根据定义, sequenced-before 具有传递性:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果操作 &lt;code&gt;a&lt;/code&gt; &quot;sequenced-before&quot; 操作 &lt;code&gt;k&lt;/code&gt;, 且操作 &lt;code&gt;k&lt;/code&gt; &quot;sequenced-before&quot; 操作 &lt;code&gt;b&lt;/code&gt;, 则操作 &lt;code&gt;a&lt;/code&gt; &quot;sequenced-before&quot; 操作 &lt;code&gt;b&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sequenced-before 可以直接构成 happens-before 的关系. 如果操作 &lt;code&gt;a&lt;/code&gt; &quot;sequenced-before&quot; 操作 &lt;code&gt;b&lt;/code&gt;, 则操作 &lt;code&gt;a&lt;/code&gt; &quot;happens-before&quot; 操作 &lt;code&gt;b&lt;/code&gt;. 这是最基本的场景:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 42; // (1)
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; // (2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语句 (1) 在语句 (2) 的前面, 因此语句 (1) “sequenced-before” 语句 (2), 也就是 (1) “happens-before” 语句 (2). 所以 (2) 可以打印出 (1) 赋值的结果且没有任何问题&lt;/p&gt;
&lt;h4&gt;Happens-before 的第二种场景: synchronizes-with 和 inter-thread happens-before(多线程)&lt;/h4&gt;
&lt;p&gt;一般来说, 如果在多个线程间没有正确的同步操作, 就无法保证两个操作之间有 &lt;strong&gt;happens-before&lt;/strong&gt; 的关系. 如果我们通过一些手段, 让不同线程的两个操作同步, 我们称这两个操作之间有 &lt;strong&gt;synchronizes-with&lt;/strong&gt; 的关系. 稍后我们会详细讨论如何组合使用 6 种内存顺序, 让两个操作达成 synchronizes-with 的关系.&lt;/p&gt;
&lt;p&gt;如果线程 1 中的操作 &lt;code&gt;a&lt;/code&gt; “synchronizes-with” 线程 2 中的操作 &lt;code&gt;b&lt;/code&gt;, 则操作 &lt;code&gt;a&lt;/code&gt; &lt;strong&gt;“inter-thread happens-before”&lt;/strong&gt; 操作&lt;code&gt;b&lt;/code&gt;. 此外 synchronizes-with 还可以 “后接” 一个 sequenced-before 关系组合成 inter-thread happens-before 的关系:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果操作 a “synchronizes-with” 操作 k, 且操作 k “sequenced-before” 操作 b, 则操作 a “inter-thread happens-before” 操作 b.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Inter-thread happens-before 关系则可以 “前接” 一个 sequenced-before 关系以延伸它的范围; 而且 inter-thread happens-before 关系具有传递性:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果操作 a “sequenced-before” 操作 k, 且操作 k “inter-thread happens-before” 操作 b, 则操作 a “inter-thread happens-before” 操作 b.&lt;/li&gt;
&lt;li&gt;如果操作 a “inter-thread happens-before” 操作 k, 且操作 k “inter-thread happens-before” 操作 b, 则操作 a “inter-thread happens-before” 操作 b.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正如它的名字暗示的, 如果操作 a “inter-thread happens-before” 操作 b, 则操作 a “happens-before” 操作 b. 下图展示了这几个概念之间的关系:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/image-20240610001317806.png&quot; alt=&quot;image-20240610001317806&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意, 虽然 sequenced-before 和 inter-thread happens-before 都有传递性, 但是 &lt;strong&gt;happens-before 没有传递性&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;例子1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int data;
std::atomic_bool flag { false };

// Execute in thread A
void producer() {
    data = 42;  // (1)
    flag.store(true);  // (2)
}

// Execute in thread B
void consume() {
    while (!flag.load());  // (3)
    assert(data == 42);  // (4)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让两个函数分别在两个线程中运行，(4) 所示的断言将有一定几率 为假，这是为什么呢？虽然我们使用原子量 flag 作为“同步信号”，而且同一个线程中 happens-before 原则也一定会被遵循，但我们并不能保证 (4) 执行时 (1) 的修改一定会被 B 线程看到, 可能是2-3-4-1这个顺序。这&lt;strong&gt;是由于现代处理器对于程序可能会采取指令重排来提高运行效率，CPU 的读写 Cache 也可能并没有写回内存。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，企图在多线程环境中通过某原子量来做非原子量的 Synchronization 并不是可靠的（当没有 Memory Order 的约束时候）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当然，上面的代码其实基本上不会为假，因为 C++ 默认使用 memory_order_seq_cst顺序约束。而且 x86 架构中也做不到松弛（memory_order_relaxed）内存模型。也就是说，这种过于松弛的模型理论上存在，但其实不会发生。因为如果允许这个级别的松弛那么代码没法写了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例子2, &lt;strong&gt;假设下面的代码中 &lt;code&gt;unlock()&lt;/code&gt; 操作 “synchronizes-with” &lt;code&gt;lock()&lt;/code&gt; 操作&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void thread1() {
    a += 1 // (1)
    unlock(); // (2)
}

void thread2() {
    lock(); // (3)
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; // (4)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设直到 &lt;code&gt;thread1&lt;/code&gt; 执行到 (2) 之前, &lt;code&gt;thread2&lt;/code&gt; 都会阻塞在 (3) 处的 &lt;code&gt;lock()&lt;/code&gt; 中. 那么可以推导出:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据语句顺序, 有 (1) “sequenced-before” (2) 且 (3) “sequenced-before” (4);&lt;/li&gt;
&lt;li&gt;因为 (2) “synchronizes-with” (3) 且 (3) “sequenced-before” (4), 所以 (2) “inter-thread happens-before” (4);&lt;/li&gt;
&lt;li&gt;因为 (1) “sequenced-before” (2) 且 (2) “inter-thread happens-before” (4), 所以 (1) “inter-thread happens-before” (4); 所以 (1) “happens-before” (4).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 (4) 可以读到 (1) 对变量 &lt;code&gt;a&lt;/code&gt; 的修改&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在上面的例子中，我们主要讲述的是指令重排。但是内存屏障不仅仅是为了防止指令重排，也会解决缓存一致性问题。在最后我们会详细探讨这其中的关系。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;缓存一致性&lt;/h2&gt;
&lt;p&gt;我们都知道二维数组横向遍历比纵向遍历要快。为什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们知道有L1 L2 L3三级cache。然后才是内存。CPU需要把内存里的数据读到cache中。但是CPU为了最大化利用cache line的能力，一般会读取一个固定大小的数据区块。一般来说 L3 Cache 比 L1 Cache 和 L2 Cache 大很多，这是因为 &lt;strong&gt;L1 Cache 和 L2 Cache 都是每个 CPU 核心独有的，而 L3 Cache 是多个 CPU 核心共享的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也就是为什么连续内存操作会比较快，也是为什么内存对齐十分重要。因为如果起点在奇数位就会有二次操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPU cache到内存的映射有三种：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接映射
&lt;ul&gt;
&lt;li&gt;直接映射通俗易懂。就是内存地址映射到的cache line是固定的。比较好找。但是问题在于这个数量非常少。如果多个数据抢占同一个cache line，就会发生cache频繁的换入换出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;全相连映射
&lt;ul&gt;
&lt;li&gt;全相连映射的意思是任意一个内存地址可以映射到任意一个cache line。也就是见缝插针。问题在于在寻找一个内存地址是否已被映射的时候，需要遍历每一个cacheline&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;组相连映射
&lt;ul&gt;
&lt;li&gt;组相连映射也就是组间采用直接映射，组内采用全相连映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CPU cache读取过程&lt;/h3&gt;
&lt;p&gt;假设我们的L1 Cache Line是64字节：&lt;/p&gt;
&lt;p&gt;比如，有一个 &lt;code&gt;int array[100]&lt;/code&gt; 的数组，当载入 &lt;code&gt;array[0]&lt;/code&gt; 时，由于这个数组元素的大小在内存只占 4 字节，不足 64 字节，CPU 就会&lt;strong&gt;顺序加载&lt;/strong&gt;数组元素到 &lt;code&gt;array[15]&lt;/code&gt;，意味着 &lt;code&gt;array[0]~array[15]&lt;/code&gt; 数组元素都会被缓存在 CPU Cache 中了，因此当下次访问这些数组元素时，会直接从 CPU Cache 读取，而不用再从内存中读取，大大提高了 CPU 读取数据的性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事实上，CPU 读取数据的时候，无论数据是否存放到 Cache 中，CPU 都是先访问 Cache，只有当 Cache 中找不到数据时，才会去访问内存，并把内存中的数据读入到 Cache 中，CPU 再从 CPU Cache 读取数据。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们刚提到过直接映射会把内存的固定地址映射到固定的cache line。一般来说是通过取模运算来确定的。所以会存在多个内存区块映射到同一个cache line位置上。这时候就要区分他们了。为了区别不同的内存块，在对应的 CPU Line 中我们还会存储一个&lt;strong&gt;组标记（Tag）&lt;/strong&gt;。这个组标记会记录当前 CPU Line 中存储的数据对应的内存块，我们可以用这个组标记来区分不同的内存块。&lt;/p&gt;
&lt;p&gt;除了组标记信息外，CPU Line 还有两个信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个是，从内存加载过来的实际存放&lt;strong&gt;数据（Data）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;另一个是，&lt;strong&gt;有效位（Valid bit）&lt;/strong&gt;，它是用来标记对应的 CPU Line 中的数据是否是有效的，如果有效位是 0，无论 CPU Line 中是否有数据，CPU 都会直接访问内存，重新加载数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CPU 在从 CPU Cache 读取数据的时候，并不是读取 CPU Line 中的整个数据块，而是读取 CPU 所需要的一个数据片段，这样的数据统称为一个&lt;strong&gt;字（*Word*）&lt;/strong&gt;。那怎么在对应的 CPU Line 中数据块中找到所需的字呢？答案是，需要一个&lt;strong&gt;偏移量（Offset）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，一个内存的访问地址，包括&lt;strong&gt;组标记、CPU Line 索引、偏移量&lt;/strong&gt;这三种信息，于是 CPU 就能通过这些信息，在 CPU Cache 中找到缓存的数据。而对于 CPU Cache 里的数据结构，则是由&lt;strong&gt;索引 + 有效位 + 组标记 + 数据块&lt;/strong&gt;组成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114224502.png&quot; alt=&quot;QQ截图20230114224502&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果内存中的数据已经在 CPU Cahe 中了，那 CPU 访问一个内存地址的时候，会经历这 4 个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据内存地址中索引信息，计算在 CPU Cahe 中的索引，也就是找出对应的 CPU Line 的地址；&lt;/li&gt;
&lt;li&gt;找到对应 CPU Line 后，判断 CPU Line 中的有效位，确认 CPU Line 中数据是否是有效的，如果是无效的，CPU 就会直接访问内存，并重新加载数据，如果数据有效，则往下执行；&lt;/li&gt;
&lt;li&gt;对比内存地址中组标记和 CPU Line 中的组标记，确认 CPU Line 中的数据是我们要访问的内存数据，如果不是的话，CPU 就会直接访问内存，并重新加载数据，如果是的话，则往下执行；&lt;/li&gt;
&lt;li&gt;根据内存地址中偏移量信息，从 CPU Line 的数据块中，读取对应的字。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;问题在于，我们并不是只读数据。我们还会写入。&lt;strong&gt;那么如果数据写入 Cache 之后，内存与 Cache 相对应的数据将会不同，这种情况下 Cache 和内存数据都不一致了，于是我们肯定是要把 Cache 中的数据同步到内存里的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;问题来了，那在什么时机才把 Cache 中的数据写回到内存呢？为了应对这个问题，下面介绍两种针对写入数据的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写直达（&lt;em&gt;Write Through&lt;/em&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;保持内存与 Cache 一致性最简单的方式是，&lt;strong&gt;把数据同时写入内存和 Cache 中&lt;/strong&gt;，这种方法称为&lt;strong&gt;写直达（*Write Through*）&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在这个方法里，写入前会先判断数据是否已经在 CPU Cache 里面了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果数据已经在 Cache 里面，先将数据更新到 Cache 里面，再写入到内存里面；&lt;/li&gt;
&lt;li&gt;如果数据没有在 Cache 里面，就直接把数据更新到内存里面。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写直达法很直观，也很简单，但是问题明显，无论数据在不在 Cache 里面，每次写操作都会写回到内存，这样写操作将会花费大量的时间，无疑性能会受到很大的影响。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写回（&lt;em&gt;Write Back&lt;/em&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;既然写直达由于每次写操作都会把数据写回到内存，而导致影响性能，于是为了要减少数据写回内存的频率，就出现了&lt;strong&gt;写回（*Write Back*）的方法&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在写回机制中，&lt;strong&gt;当发生写操作时，新的数据仅仅被写入 Cache Block 里，只有当修改过的 Cache Block「被替换」时才需要写到内存中&lt;/strong&gt;，减少了数据写回内存的频率，这样便可以提高系统的性能。&lt;/li&gt;
&lt;li&gt;如果当发生写操作时，数据已经在 CPU Cache 里的话，则把数据更新到 CPU Cache 里，同时标记 CPU Cache 里的这个 Cache Block 为脏（Dirty）的，这个脏的标记代表这个时候，我们 CPU Cache 里面的这个 Cache Block 的数据和内存是不一致的，这种情况是不用把数据写到内存里的；&lt;/li&gt;
&lt;li&gt;如果当发生写操作时，数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话，就要检查这个 Cache Block 里的数据有没有被标记为脏的，如果是脏的话，我们就要把这个 Cache Block 里的数据写回到内存，然后再把当前要写入的数据，写入到这个 Cache Block 里，同时也把它标记为脏的；如果 Cache Block 里面的数据没有被标记为脏，则就直接将数据写入到这个 Cache Block 里，然后再把这个 Cache Block 标记为脏的就好了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以发现写回这个方法，在把数据写入到 Cache 的时候，只有在缓存不命中，同时数据对应的 Cache 中的 Cache Block 为脏标记的情况下，才会将数据写到内存中，而在缓存命中的情况下，则在写入后 Cache 后，只需把该数据对应的 Cache Block 标记为脏即可，而不用写到内存里。&lt;/p&gt;
&lt;p&gt;这样的好处是，如果我们大量的操作都能够命中缓存，那么大部分时间里 CPU 都不需要读写内存，自然性能相比写直达会高很多。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在有一个问题。我们东西确实是写入L1/L2 cache了。但是L1/L2 cache是每个核心独有的。也就是说，有可能在一个核心修改完数据后，修改的数据还在自己的缓存里。另一个核心从自己的缓存里读就会读到没有修改过的值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114224941.png&quot; alt=&quot;QQ截图20230114224941&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里的意思是有可能r2 = 0 且 r4 = 0。为什么？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们可能会回答指令重排。但是X86是不允许写-写重排的。所以导致这个情况的就是我们本节的缓存一致性。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;但是具体原因是什么呢？我们先看看MESI协议。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;那么，要解决这一问题，就需要一种机制，来同步两个不同核心里面的缓存数据。要实现的这个机制的话，要保证做到下面这 2 点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一点，某个 CPU 核心里的 Cache 数据更新时，必须要传播到其他核心的 Cache，这个称为&lt;strong&gt;写传播（*Wreite Propagation*）&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;第二点，某个 CPU 核心里对数据的操作顺序，必须在其他核心看起来顺序是一样的，这个称为&lt;strong&gt;事务的串形化（*Transaction Serialization*）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一点写传播很容易就理解，当某个核心在 Cache 更新了数据，就需要同步到其他核心的 Cache 里。而对于第二点事务事的串形化，我们举个例子来理解它。：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设我们有一个含有 4 个核心的 CPU，这 4 个核心都操作共同的变量&lt;code&gt; i&lt;/code&gt;（初始值为 0 ）。A 号核心先把 &lt;code&gt;i &lt;/code&gt;值变为 &lt;code&gt;100&lt;/code&gt;，而此时同一时间，B 号核心先把 &lt;code&gt;i &lt;/code&gt;值变为 &lt;code&gt;200&lt;/code&gt;，这里两个修改，都会「传播」到 C 和 D 号核心。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114225424.png&quot; alt=&quot;QQ截图20230114225424&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114225429.png&quot; alt=&quot;QQ截图20230114225429&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么问题就来了，C 号核心先收到了 A 号核心更新数据的事件，再收到 B 号核心更新数据的事件，因此 C 号核心看到的变量&lt;code&gt; i&lt;/code&gt; 是先变成&lt;code&gt; 100&lt;/code&gt;，后变成&lt;code&gt; 200&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;而如果 D 号核心收到的事件是反过来的，则 D 号核心看到的是变量&lt;code&gt;i&lt;/code&gt;先变成&lt;code&gt; 200&lt;/code&gt;，再变成 &lt;code&gt;100&lt;/code&gt;，虽然是做到了写传播，但是各个 Cache 里面的数据还是不一致的。&lt;/p&gt;
&lt;p&gt;所以，我们要保证 C 号核心和 D 号核心都能看到&lt;strong&gt;相同顺序的数据变化&lt;/strong&gt;，比如变量 i 都是先变成 100，再变成 200，这样的过程就是事务的串形化。&lt;/p&gt;
&lt;p&gt;要实现事务串形化，要做到 2 点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 核心对于 Cache 中数据的操作，需要同步给其他 CPU 核心；&lt;/li&gt;
&lt;li&gt;要引入「锁」的概念，如果两个 CPU 核心里有相同数据的 Cache，那么对于这个 Cache 数据的更新，只有拿到了「锁」，才能进行对应的数据更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MESI 协议&lt;/h2&gt;
&lt;p&gt;MESI 协议其实是 4 个状态单词的开头字母缩写，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Modified&lt;/em&gt;，已修改&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Exclusive&lt;/em&gt;，独占&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Shared&lt;/em&gt;，共享&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Invalidated&lt;/em&gt;，已失效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这四个状态来标记 Cache Line 四个不同的状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「已修改」状态就是我们前面提到的脏标记，代表该 Cache Block 上的数据已经被更新过，但是还没有写到内存里。&lt;/li&gt;
&lt;li&gt;「已失效」状态，表示的是这个 Cache Block 里的数据已经失效了，不可以读取该状态的数据。&lt;/li&gt;
&lt;li&gt;「独占」和「共享」状态都代表 Cache Block 里的数据是干净的，也就是说，这个时候 Cache Block 里的数据和内存里面的数据是一致性的。
&lt;ul&gt;
&lt;li&gt;「独占」和「共享」的差别在于，独占状态的时候，数据只存储在一个 CPU 核心的 Cache 里，而其他 CPU 核心的 Cache 没有该数据。这个时候，如果要向独占的 Cache 写数据，就可以直接自由地写入，而不需要通知其他 CPU 核心，因为只有你这有这个数据，就不存在缓存一致性的问题了，于是就可以随便操作该数据。&lt;/li&gt;
&lt;li&gt;另外，在「独占」状态下的数据，如果有其他核心从内存读取了相同的数据到各自的 Cache ，那么这个时候，独占状态下的数据就会变成共享状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;那么，「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有，所以当我们要更新 Cache 里面的数据的时候，不能直接修改，而是要先向所有的其他 CPU 核心广播一个请求，要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态，然后再更新当前 Cache 里面的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们举个具体的例子来看看这四个状态的转换：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 A 号 CPU 核心从内存读取变量 i 的值，数据被缓存在 A 号 CPU 核心自己的 Cache 里面，此时其他 CPU 核心的 Cache 没有缓存该数据，于是标记 Cache Line 状态为「独占」，此时其 Cache 中的数据与内存是一致的；&lt;/li&gt;
&lt;li&gt;然后 B 号 CPU 核心也从内存读取了变量 i 的值，此时会发送消息给其他 CPU 核心，由于 A 号 CPU 核心已经缓存了该数据，所以会把数据返回给 B 号 CPU 核心。在这个时候， A 和 B 核心缓存了相同的数据，Cache Line 的状态就会变成「共享」，并且其 Cache 中的数据与内存也是一致的；&lt;/li&gt;
&lt;li&gt;当 A 号 CPU 核心要修改 Cache 中 i 变量的值，发现数据对应的 Cache Line 的状态是共享状态，则要向所有的其他 CPU 核心广播一个请求，要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态，然后 A 号 CPU 核心才更新 Cache 里面的数据，同时标记 Cache Line 为「已修改」状态，此时 Cache 中的数据就与内存不一致了。&lt;/li&gt;
&lt;li&gt;如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值，由于此时的 Cache Line 是「已修改」状态，因此不需要给其他 CPU 核心发送消息，直接更新数据即可。&lt;/li&gt;
&lt;li&gt;如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」，发现 Cache Line 状态是「已修改」状态，就会在替换前先把数据同步到内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;说完了MESI协议，回到我们Example 8.5的那张图。具体缓存一致性在哪儿出问题了？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设在cache line中状态为&lt;code&gt;共享&lt;/code&gt;，在某个核更新数据时候会向总线发送&lt;code&gt;已失效&lt;/code&gt;消息以获取数据的所有权，其他消息得到此消息后更新相应cache line状态为&lt;code&gt;已失效&lt;/code&gt;，并回复&lt;code&gt;Invalidate Acknowledge&lt;/code&gt;消息。显然我们可以看到这个过程非常的繁琐，如果每个操作生效前都需要这么多操作那也太低效了，所以引入了&lt;code&gt;Invaildate queue&lt;/code&gt;和&lt;code&gt;Store Buffer&lt;/code&gt;，为的就是异步执行指令，提高以上过程的效率。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114230210.png&quot; alt=&quot;QQ截图20230114230210&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为写入到&lt;code&gt;Invaildate queue&lt;/code&gt;和&lt;code&gt;Store Buffer&lt;/code&gt;的数据并不会被立即执行。在执行写操作的核会把数据写入&lt;code&gt;store buffer&lt;/code&gt;，后面&lt;strong&gt;异步的刷新&lt;/strong&gt;到cache line，当然也会广播失效消息。而当另一个执行读操作的核收到这个失效消息时，会把消息写入自身的&lt;code&gt;Invalidate Queue&lt;/code&gt;中，随后&lt;strong&gt;异步&lt;/strong&gt;将cache line设为&lt;code&gt;失效&lt;/code&gt;状态。写操作的核在读取的时候会扫描&lt;code&gt;store buffer&lt;/code&gt;，而这个执行读操作的核在读取数据的时候并不会扫描&lt;code&gt;Invaildate queue&lt;/code&gt;，&lt;strong&gt;这意味着读操作可能会在一段时间内读到老旧数据。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先process 0和process 1把&lt;code&gt;1&lt;/code&gt;放入到地址&lt;code&gt;_x&lt;/code&gt;和&lt;code&gt;_y&lt;/code&gt;的内存上，但是此时可能这些数据还存在自己的&lt;code&gt;store buffer&lt;/code&gt;中，也就是对方都还没意识到数据已经发生修改，然后执行操作&lt;code&gt;r2&lt;/code&gt;和&lt;code&gt;r4&lt;/code&gt;的操作，此时就发生脏读，导致 &lt;code&gt;r2 = 0&lt;/code&gt; 且&lt;code&gt; r4 = 0&lt;/code&gt;这种奇怪的事情发生。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是在processor 1把&lt;code&gt;y&lt;/code&gt;的值更改为&lt;code&gt;1&lt;/code&gt;的时候，并没有直接写入内存。此时在&lt;code&gt;processor 0&lt;/code&gt;眼里&lt;code&gt;y&lt;/code&gt;还是&lt;code&gt;0&lt;/code&gt;。所以&lt;code&gt;r2&lt;/code&gt;就是&lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/Sz7QXx1h4sS7xWRUSP4ZCw&lt;/p&gt;
&lt;h2&gt;内存屏障&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;经过上面的总结，我们可以得出结论。内存屏障存在的意义是在一定程度上避免缓存一致性所带来的问题和指令重排。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在广义角度上，Intel有三种内存模型：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SFENCE，LFENCE和MFENCE指令提供了一种性能高效的方式，可确保在产生弱排序结果的例程和使用该数据的例程之间load和store内存排序。 这些指令的功能如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;SFENCE：序列化(动词，理解为串行化) 程序指令流中，在SFENCE指令之前发生的所有 store（写）操作，但不影响 load（读）操作。&lt;/li&gt;
&lt;li&gt;LFENCE：序列化 程序指令流中，在LFENCE指令之前发生的所有 load（读）操作，但不影响 store（写）操作。&lt;/li&gt;
&lt;li&gt;MFENCE：序列化 程序指令流中，在MFENCE指令之前发生的所有 store 和 load 操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，还有一些其他操作含有内存屏障的作用&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;总线上的内存映射设备和其他I / O设备通常对其I / O缓冲区的写入顺序敏感。可以使用I / O指令（IN和OUT指令）对此类访问施加强的写入顺序，如下所示。在执行I / O指令之前，处理器将等待程序中所有先前的指令完成，并等待所有缓冲的写入操作耗尽到内存。只有指令提取和页表遍历可以传递I / O指令。直到处理器确定I / O指令已完成，后续指令的执行才开始。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多处理器系统中的同步机制可能取决于强大的内存排序模型。在这里，程序可以使用诸如&lt;code&gt;XCHG&lt;/code&gt;指令或&lt;code&gt;LOCK&lt;/code&gt;前缀之类的锁定指令，以确保原子地执行对存储器的读-修改-写操作。锁定操作通常类似于I / O操作，因为它们等待所有先前的指令完成并且等待所有缓冲的写操作排入内存。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;程序同步也可以通过序列化指令执行（请参见第8.3节）。这些指令通常用于关键过程或任务边界，以在跳到新的代码段或上下文切换之前强制完成所有先前的指令。像I / O和锁定指令一样，处理器在执行序列化指令之前要等到所有先前的指令都已完成并且所有缓冲的写操作都已排入内存为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请注意，与CPUID指令相比，SFENCE，LFENCE和MFENCE指令提供了一种更有效的控制内存顺序的方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从上面，我们可以看出：&lt;strong&gt;内存屏障，带Lock或XCHG前缀的指令，I/O操作都会把数据载入内存，自然就不存在前面我们提到的脏读问题了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们一直以来提到的指令重排指的是运行时排序。编译器也会导致指令重排。这里更多的讨论见&lt;a href=&quot;https://blog.51cto.com/u_15703183/5464436&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;内存模型（内存序）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;我们前面提到了程序运行时的指令重排，内存模型其实规定的就是这些指令的重排哪些排列顺序是正确的，也就是指明在特定要求下，哪些是可以出现的，哪些是不可以出现的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;事实上对于内存模型的描述我们可以从两个方面来看，一个是硬件角度的内存模型，也就是厂商给我们提供了怎样的一种一致性保证；还有一种是软件级别的内存模型，其实说直白一点就是一些高级语言给程序员提供的一致性保证，显然此时我们可以不必费尽心机去考虑代码如何适配不同的机器。&lt;/p&gt;
&lt;p&gt;比如X86的强模型和ARM的弱模型，如果仅仅依靠硬件提供的内存屏障这个代码将非常难写， 因为不同硬件本身的提供的内存模型是不一样的，甚至有时操作也不相同。但是如果基于软件级别的内存模型，比如使用C++的内存模型（手段为内存序），那么编译器就会帮我们自动去适配不同的机器，因为这是语言保证的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;英特尔酷睿2双核，英特尔凌动，英特尔酷睿双核，奔腾4和P6家族处理器还使用处理器排序的内存排序模型，该模型可以进一步定义为“使用存储缓冲区转发进行写入排序”。 该模型的特征如下。
在用于定义为WB的内存区域的单处理器系统中，内存排序模型遵循以下原则（请注意，单处理器和多处理器系统的内存排序原则是从在以下环境中执行软件的角度编写的。其中术语“处理器”是指逻辑处理器。例如，支持多核和/或Intel超线程技术的物理处理器被视为多处理器系统。）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;读操作之间无法排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写操作不能和旧的读操作之间排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写操作与写操作之间除了以下情况以外不能重新排序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;带有non-temporal move指令的流存储（写入）（MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, 和 MOVNTPD）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;串操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行CLFLUSH指令不能重新排序对存储器的写操作。 可以通过执行CLFLUSHOPT指令的执行来重新排序写操作，该指令将刷新除正在写入的高速缓存行以外的其他高速缓存行。 CLFLUSH指令的执行不会相互重新排序。 访问不同缓存行的CLFLUSHOPT的执行可能会相互重新排序。 CLFLUSHOPT的执行可以与访问不同缓存行的CLFLUSH的执行重新排序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读取可能会与不同地址的较旧写入重新排序，但对不同地址的较旧写入则不会重新排序。（store load重排）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;读写操作不能与IO操作，带LOCK的指令，序列化指令重排序。&lt;/strong&gt;（上面曾提到过这个问题）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读操作不能越过前面的LFENCE和MFENCE操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写操作和CLFLUSH和CLFLUSHOPT的执行不能越过前面的LFENCE，SFENCE和MFENCE指令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LFENCE不能越过前面的读操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SFENCE不能越过前面的写操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MFENCE不能越过前面的读写操作以及CLFLUSH和CLFLUSHOPT的执行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/269221065?utm_id=0&lt;/p&gt;
&lt;p&gt;https://blog.51cto.com/u_15703183/5464436&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_22642239/article/details/114022306&lt;/p&gt;
&lt;p&gt;https://redrain.blog.csdn.net/article/details/111327141&lt;/p&gt;
&lt;h2&gt;C++中的内存序----三（四）种模型和六种枚举值&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;std::memory_order&lt;/code&gt; 指定内存访问，包括常规的非原子内存访问，&lt;strong&gt;如何围绕原子操作排序&lt;/strong&gt;。在没有任何制约的多处理器系统上，多个线程同时读或写数个变量时，一个线程能观测到变量值更改的顺序不同于另一个线程写它们的顺序。其实，更改的顺序甚至能在多个读取线程间相异。一些类似的效果还能在单处理器系统上出现，因为内存模型允许编译器变换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意，其实内存序并不能简单描述成为禁止某某操作排到某某操作后面。因为就算单纯禁止重排，也有可能发生我们提过的缓存一致性问题。所以说，应该用可见性这个词。可见性这个词包含了描述重排和缓存一致性的情况。所以说，我们说某某操作现在是可见的，表明了确实我们保证操作正确。也就是不仅正确处理了重排，也正确处理了缓存一致性问题。&lt;/p&gt;
&lt;h3&gt;顺序一致（sequentially consistent ordering）&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;memory_order_seq_cst&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;C++使用这个做为操作原子变量的默认值。可以直接看文档。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Load/store/RMW操作都可以使用该枚举值，用于 &lt;code&gt;load operation&lt;/code&gt;（原子读操作）的时候有&lt;code&gt;acquire operation&lt;/code&gt;的特性，用于&lt;code&gt; store operation&lt;/code&gt;(原子写操作）的时候有&lt;code&gt;release operation&lt;/code&gt;的特性, 用于 &lt;code&gt;read-modify-write operation&lt;/code&gt;（RMW）的时候有&lt;code&gt;acq_rel operation&lt;/code&gt;的特性，且所有操作都相当于一个双向屏障，&lt;strong&gt;前后语句都不能跨越该操作进行重排&lt;/strong&gt;。&lt;strong&gt;并且所有线程的语句都以全局的内存修改顺序为参照&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;看起来，这个和&lt;code&gt;memory_order_acq_rel&lt;/code&gt;差不多啊？并不是。&lt;strong&gt;这种内存序列会对拥有此标签的内存操作建立一个单独全序&lt;/strong&gt;。&lt;code&gt;memory_order_seq_cst&lt;/code&gt;比&lt;code&gt;memory_order_acq_rel&lt;/code&gt;更强，&lt;code&gt;memory_order_acq_rel&lt;/code&gt;的顺序保障，是要基于同一个原子变量的，也就是说，在这个原子变量之前的读写，不能重排到这个原子变量之后，同时这个原子变量之后的读写，也不能重排到这个原子变量之前。但是，如果两个线程基于&lt;code&gt;memory_order_acq_rel&lt;/code&gt;使用了两个不同的原子变量x1, x2，那在x1之前的读写，重排到x2之后，是完全可能的，在x1之后的读写，重排到x2之前，也是被允许的。然而，如果两个原子变量x1,x2，是基于&lt;code&gt;memory_order_seq_cst&lt;/code&gt;在操作，那么即使是x1之前的读写，也不能被重排到x2之后，x1之后的读写，也不能重排到x2之前，也就说，如果都用&lt;code&gt;memory_order_seq_cst&lt;/code&gt;，那么&lt;strong&gt;程序代码顺序(Program Order)就将会是你在多个线程上都实际观察到的顺序(Observed Order)&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;简而言之:  在这个模型下, 所有线程看到的所有操作都有一个一致的顺序, 即使这些操作可能针对不同的变量, 运行在不同的线程. 2.1 节中我们介绍了修改顺序 (modification order), 即单一变量的修改顺序在所有线程看来都是一致的. &lt;strong&gt;Sequencial consistent 则将这种一致性扩展到了所有变量&lt;/strong&gt;. 例如&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;bool&amp;gt; x{false}, y{false};

void thread1() {
    x.store(true, std::memory_order_seq_cst); // (1)
}

void thread2() {
    y.store(true, std::memory_order_seq_cst); // (2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;thread1&lt;/code&gt; 和 &lt;code&gt;thread2&lt;/code&gt; 分别修改原子变量 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt;. 运行过程中, 有可能先执行 (1) 再执行 (2), 也有可能先执行 (2) 后执行 (1). 但无论如何, 所有线程中看到的顺序都是一致的. 因此如果我们这样测试这段代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;int&amp;gt; z{0};

void read_x_then_y() {
    while (!x.load(std::memory_order_seq_cst)); // (3)
    if (y.load(std::memory_order_seq_cst)) ++z; // (4)
}

void read_y_then_x() {
    while (!y.load(std::memory_order_seq_cst)); // (5)
    if (x.load(std::memory_order_seq_cst)) ++z; // (6)
}

int main() {
    std::thread a(thread1), b(thread2), c(read_x_then_y), d(read_y_then_x);
    a.join(), b.join(), c.join(), d.join();
    assert(z.load() != 0); // (7)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(7) 处的断言永远不会失败. 因为 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 的修改顺序是全局一致的, 如果先执行 (1) 后执行 (2), 则 &lt;code&gt;read_y_then_x&lt;/code&gt; 中循环 (5) 退出时, 能保证 &lt;code&gt;y&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;, 此时 &lt;code&gt;x&lt;/code&gt; 也必然为 &lt;code&gt;true&lt;/code&gt;, 因此 (6) 会被执行; 同理, 如果先执行 (2) 后执行 (1), 则循环 (3) 退出时 &lt;code&gt;y&lt;/code&gt; 也必然为 &lt;code&gt;true&lt;/code&gt;, 因此 (4) 会被执行. 无论如何, &lt;code&gt;z&lt;/code&gt; 最终都不会等于 0.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所有线程会以相同的顺序观察到这两个操作的执行顺序. 所以如果某个线程看到&lt;code&gt;thread1&lt;/code&gt;先执行, 那么其余所有线程都会看到&lt;code&gt;thread1&lt;/code&gt;先执行.&lt;/strong&gt; 然而，这并不意味着线程1和线程2的操作之间有任何特定的顺序。具体来说，&lt;code&gt;x.store(true)&lt;/code&gt; 和 &lt;code&gt;y.store(true)&lt;/code&gt; 的顺序在不同的线程之间是有序的，但在同一线程中没有保证它们的相对顺序。&lt;/p&gt;
&lt;p&gt;Sequencial consistent 可以实现 synchronizes-with 的关系. 如果一个 &lt;code&gt;memory_order_seq_cst&lt;/code&gt; 的 load 操作在某个原子变量上读到了一个 &lt;code&gt;memory_order_seq_cst&lt;/code&gt; 的 store 操作在这个原子变量中写入的值, 则 store 操作 “synchronizes-with” load 操作. 在上面的例子中, 有 (1) “synchronizes-with” (3) 和 (2) “synchronizes-with” (5).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;顺序一致模型有一个最大的问题：它的性能差。我们很多时候并不需要对整体的原子操作进行排序。一般都是局部有序。注意到锁和内存屏障的区别。锁一般来说都是局部的，也就是针对某一个区域加锁解锁。但是不同的锁之间往往是没有顺序的。也就是说，锁更像&lt;code&gt;acquire--release&lt;/code&gt;模型，而不是&lt;code&gt;seq_cst&lt;/code&gt;模型。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;获取发布 （release-acquire/consume ordering）&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;memory_order_consume&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类似于&lt;code&gt;memory_order_acquire&lt;/code&gt;，也是用于&lt;code&gt;load&lt;/code&gt;操作，但更为宽松。针对于该&lt;code&gt;load&lt;/code&gt;操作，不允许&lt;code&gt;load&lt;/code&gt;之后的&lt;strong&gt;有关联（依赖）的&lt;/strong&gt;操作重排到&lt;code&gt;load&lt;/code&gt;之前。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;memory_order_consume&lt;/code&gt;适用于&lt;code&gt;load operation&lt;/code&gt;（原子读操作），对于采用此内存序的&lt;code&gt;load operation&lt;/code&gt;，我们可以称为&lt;code&gt;consume operation&lt;/code&gt;(consume原子读操作），设有一个原子变量M上的&lt;code&gt;consume operation&lt;/code&gt;(consume原子读操作），对周围内存序的影响是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当前线程中该&lt;code&gt;consume operation&lt;/code&gt;(consume原子读操作）后的&lt;strong&gt;依赖&lt;/strong&gt;该&lt;code&gt;consume operation&lt;/code&gt;(consume原子读操作）读取的值的写入或读取操作不能被重排到该&lt;code&gt;consume operation&lt;/code&gt;(consume原子读操作）前，其他线程中所有对M的&lt;code&gt;release operation&lt;/code&gt;（原子写操作）及其之前的对数据依赖变量的写入都对当前线程从该&lt;code&gt;consume operation&lt;/code&gt;(consume原子读操作）开始往后的操作可见&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相比较于下面讲的&lt;code&gt;memory_order_acquire&lt;/code&gt;，&lt;code&gt;memory_order_consume&lt;/code&gt;只是阻止了之后有依赖关系的重排。绝大部分平台上，这个内存序只会影响到编译器优化，依赖于dependency chain。但实际上很多编译器都没有正确地实现consume，导致等同于acquire。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;见下图，如果我们把&lt;code&gt;memory_order_acquire&lt;/code&gt;换成了&lt;code&gt;memory_order_consume&lt;/code&gt;，那么将只有&lt;code&gt;int r2 = x-&amp;gt;i&lt;/code&gt; 是有效的，因为其读取的值依赖原子变量&lt;code&gt;p&lt;/code&gt;，但&lt;code&gt;int r1 = A&lt;/code&gt;并不能保证读到线程1写入到&lt;code&gt;A&lt;/code&gt;的值，因为&lt;code&gt;A&lt;/code&gt;值并不依赖&lt;code&gt;p&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20220909144257.png&quot; alt=&quot;QQ截图20220909144257&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;memory_order_acquire&lt;/code&gt; ---- 理解为 acquire lock&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;针对于该&lt;code&gt;load&lt;/code&gt;操作，不允许&lt;code&gt;load&lt;/code&gt;之后的操作重排到&lt;code&gt;load&lt;/code&gt;之前。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;说人话就是：单向加载内存屏障，表示&lt;strong&gt;线程中的读写指令不能重排到此&lt;em&gt;读&lt;/em&gt;屏障指令&lt;em&gt;之前&lt;/em&gt;，另一个执行&lt;em&gt;写&lt;/em&gt;原子变量的线程里写操作之前的变量，可以被此线程 读取&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理解为&lt;code&gt;lock()&lt;/code&gt;。也就是&lt;code&gt;lock&lt;/code&gt;之后的代码不能放到&lt;code&gt;lock&lt;/code&gt;之前。理解为我们acquire其他线程publish的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;memory_order_acquire&lt;/code&gt;适用于&lt;code&gt;load operation&lt;/code&gt;（原子读操作），对于采用此内存序的&lt;code&gt;load operation&lt;/code&gt;（原子读操作），我们可以称为&lt;code&gt;acquire operation&lt;/code&gt;，设有一个原子变量M上的&lt;code&gt;acquire operation&lt;/code&gt;（原子读操作），对周围内存序的影响是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前线程中该&lt;code&gt;acquire operation&lt;/code&gt;（原子读操作）后的任何内存读写操作都不能被重排到该&lt;code&gt;acquire operation&lt;/code&gt;（原子读操作）前。&lt;/li&gt;
&lt;li&gt;结合下面的&lt;code&gt;memory_order_release&lt;/code&gt;我们能推导出从而会有其他线程中所有对M的&lt;code&gt;release operation&lt;/code&gt;（原子写操作）及其之前的写入都对当前线程从该&lt;code&gt;acquire operation&lt;/code&gt;（原子读操作）开始往后的操作可见。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114235531.png&quot; alt=&quot;QQ截图20230114235531&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230115005233.png&quot; alt=&quot;QQ截图20230115005233&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时在一个线程上执行时，&lt;strong&gt;读取&lt;code&gt;x&lt;/code&gt;的内存屏障操作之前的指令允许重排到&lt;code&gt;x&lt;/code&gt;之后，但是读取&lt;code&gt;x&lt;/code&gt;之后的指令不会被重排到&lt;code&gt;x&lt;/code&gt;前面。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实际上&lt;code&gt;memory_order_release&lt;/code&gt;用于写入、&lt;code&gt;memory_order_acquire&lt;/code&gt;用于读取，他们是成对使用：线程A使用&lt;code&gt;memory_order_release&lt;/code&gt;写原子变量&lt;code&gt;x&lt;/code&gt;，线程B使用&lt;code&gt;memory_order_acquire&lt;/code&gt;读原子变量&lt;code&gt;x&lt;/code&gt;。线程A写&lt;code&gt;x&lt;/code&gt;之前的操作，都可以被线程B在读&lt;code&gt;x&lt;/code&gt;之后看到&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;memory_order_release&lt;/code&gt; ---- 理解为 release unlock&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;针对于该&lt;code&gt;store&lt;/code&gt;操作，不允许&lt;code&gt;store&lt;/code&gt;之前的操作重排到&lt;code&gt;store&lt;/code&gt;之后。&lt;/li&gt;
&lt;li&gt;说人话就是：单向释放内存屏障，表示&lt;strong&gt;线程中的&lt;em&gt;读写&lt;/em&gt;指令不能重排到此&lt;em&gt;写&lt;/em&gt;原子变量指令&lt;em&gt;之后&lt;/em&gt;。另一个执行&lt;em&gt;读&lt;/em&gt;原子变量的线程，可以正确&lt;em&gt;读取&lt;/em&gt;指令之前的变量&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;理解为&lt;code&gt;unlock()&lt;/code&gt;。也就是&lt;code&gt;unlock&lt;/code&gt;前面的代码不能放到&lt;code&gt;unlock&lt;/code&gt;后面。理解为我们把我们对内存的更改 release掉。也就是publish给其他线程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_release&lt;/code&gt;适用于&lt;code&gt;store operation&lt;/code&gt;（原子写操作），对于采用此内存序的写入操作，我们可以称为&lt;code&gt;release operation&lt;/code&gt;，设有一个原子变量M上的&lt;code&gt;release operation&lt;/code&gt;(写入操作），对周围内存序的影响是：
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;该&lt;code&gt;release operation&lt;/code&gt;(写入操作）前的内存读写都不能重排到该&lt;code&gt;release operation&lt;/code&gt;(写入操作）之后。(该&lt;code&gt;store&lt;/code&gt;操作，不允许&lt;code&gt;store&lt;/code&gt;之前的操作重排到&lt;code&gt;store&lt;/code&gt;之后。)结合&lt;code&gt;memory_order_acquire&lt;/code&gt;的左右从而有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前线程截止到该&lt;code&gt;release operation&lt;/code&gt;（原子写操作）的所有内存写入都对另外线程对M的&lt;code&gt;acquire operation&lt;/code&gt;（原子读操作）以及之后的内存操作可见，这就是&lt;code&gt;release acquire&lt;/code&gt; 语义。&lt;/li&gt;
&lt;li&gt;当前线程截止到该operation的所有M所依赖的内存写入都对另外线程对M的&lt;code&gt;consume operation&lt;/code&gt;以及之后的内存操作可见，这就是&lt;code&gt;release consume&lt;/code&gt;语义。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114234826.png&quot; alt=&quot;QQ截图20230114234826&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230115005203.png&quot; alt=&quot;QQ截图20230115005203&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时在一个线程上执行时，&lt;strong&gt;写入&lt;code&gt;x&lt;/code&gt;的内存屏障操作之后的指令允许重排到&lt;code&gt;x&lt;/code&gt;之前，但是写入x之前的指令不会被重排到x后面。&lt;/strong&gt; &lt;strong&gt;不过写入x指令前后的那些指令的顺序是允许重排的&lt;/strong&gt;。所以使用&lt;code&gt;memory_order_release&lt;/code&gt;屏障后，可以保障另一个线程在执行了读取&lt;code&gt;x&lt;/code&gt;操作之后，读取&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;c&lt;/code&gt;的值是正确的，因为&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;c&lt;/code&gt;的写入操作一定不会被重排到&lt;code&gt;x&lt;/code&gt;操作之后&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;memory_order_acq_rel&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;双向的”加载-释放”内存屏障。&lt;/li&gt;
&lt;li&gt;多用于CAS操作。比如&lt;code&gt;compare_exchange_weak/strong&lt;/code&gt;, &lt;code&gt;fetch_add/sub/...&lt;/code&gt;等&lt;/li&gt;
&lt;li&gt;用于RMW(read-modify-write)原子操作，RMW操作前后的语句都不允许跨越该操作而重排。该操作相当于兼具load(acquire)和store(release)，可以看作由这两个操作组成，但是整体上是原子的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_acq_rel&lt;/code&gt;适用于&lt;code&gt;read-modify-write operation&lt;/code&gt;(RMW操作)，对于采用此内存序的&lt;code&gt;read-modify-write operation&lt;/code&gt;，我们可以称为&lt;code&gt;acq_rel operation&lt;/code&gt;，既属于&lt;code&gt;acquire operation&lt;/code&gt; 也是&lt;code&gt;release operation&lt;/code&gt;. 设有一个原子变量M上的&lt;code&gt;acq_rel operation&lt;/code&gt;：自然的，因为同时具有两种属性，所以该&lt;code&gt;acq_rel operation&lt;/code&gt;之前的内存读写都不能重排到该&lt;code&gt;acq_rel operation&lt;/code&gt;之后，该&lt;code&gt;acq_rel operation&lt;/code&gt;之后的内存读写都不能重排到该&lt;code&gt;acq_rel operation&lt;/code&gt;之前. 其他线程中所有对M的&lt;code&gt;release operation&lt;/code&gt;(写入操作）及其之前的写入都对当前线程从该&lt;code&gt;acq_rel operation&lt;/code&gt;开始的操作可见，并且截止到该&lt;code&gt;acq_rel operation&lt;/code&gt;的所有内存写入都对另外线程对M的&lt;code&gt;acquire operation&lt;/code&gt;（原子读操作）以及之后的内存操作可见。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意，针对内存屏障，多线程之间必须要使用的是同一个原子变量。因为是使用这一个原子变量的值进行同步的。这里和顺序一致模型有区别&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;release-acquire 可以实现 synchronizes-with 的关系. 如果一个 acquire 操作在同一个原子变量上读取到了一个 release 操作写入的值, 则这个 release 操作 “synchronizes-with” 这个 acquire 操作. 我们来看一个例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;bool&amp;gt; x{false}, y{false};

void thread1() {
    x.store(true, std::memory_order_relaxed); // (1)
    y.store(true, std::memory_order_release); // (2)
}

void thread2() {
    while (!y.load(std::memory_order_acquire)); // (3)
    assert(x.load(std::memory_order_relaxed)); // (4)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中, 语句 (2) 使用 &lt;code&gt;memory_order_release&lt;/code&gt; 在 &lt;code&gt;y&lt;/code&gt; 中写入 &lt;code&gt;true&lt;/code&gt;, 语句 (3) 中使用 &lt;code&gt;memory_order_acquire&lt;/code&gt; 从 &lt;code&gt;y&lt;/code&gt; 中读取值. 循环 (3) 退出时, 它已经读取到了 &lt;code&gt;y&lt;/code&gt; 的值为 &lt;code&gt;true&lt;/code&gt;, 也就是读取到了操作 (2) 中写入的值. 因此有 (2) “synchronizes-with” (3). 所以我们可以推导出:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为 (2) “synchronizes-with” (3) 且 (3) “sequenced-before” (4), 所以 (2) “inter-thread happens-before” (4);&lt;/li&gt;
&lt;li&gt;因为 (1) “sequenced-before” (2) 且 (2) “inter-thread happens-before” (4), 所以 (1) “inter-thread happens-before” (4);&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 (1) “happens-before” (4). 因此 (4) 能读取到 (1) 中写入的值, 断言永远不会失败. 即使 (1) 和 (4) 用的是 &lt;code&gt;memory_order_relaxed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;我们提到 sequencial consistent 模型可以实现 synchronizes-with 关系. 事实上, 内存顺序为 &lt;code&gt;memory_order_seq_cst&lt;/code&gt; 的 load 操作和 store 操作可以分别视为 acquire 操作和 release 操作. 因此对于两个指定了 &lt;code&gt;memory_order_seq_cst&lt;/code&gt; 的 store 操作和 load 操作, 如果后者读到了前者写入的值, 则前者 “synchronizes-with” 后者.&lt;/p&gt;
&lt;p&gt;为了实现 synchronizes-with 关系, acquire 操作和 release 操作应该成对出现. 如果 &lt;code&gt;memory_order_acquire&lt;/code&gt; 的 load 读到了 &lt;code&gt;memory_order_relaxed&lt;/code&gt; 的 store 写入的值, 或者 &lt;code&gt;memory_order_relaxed&lt;/code&gt; 的 load 读到了 &lt;code&gt;memory_order_release&lt;/code&gt; 的 store 写入的值, 都不能实现 synchronizes-with 的关系.&lt;/p&gt;
&lt;p&gt;虽然 sequencial consistent 顺序一致模型能够像 release-acquire 一样实现同步, &lt;strong&gt;但是反过来 release-acquire 模型不能像 sequencial consistent 一样提供全局顺序一致性&lt;/strong&gt;. 如果将顺序一致一节例子中的 &lt;code&gt;memory_order_seq_cst&lt;/code&gt; 换成 &lt;code&gt;memory_order_acquire&lt;/code&gt; 和 &lt;code&gt;memory_order_release&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void thread1() {
    x.store(true, std::memory_order_release); // (1)
}

void thread2() {
    y.store(true, std::memory_order_release); // (2)
}

void read_x_then_y() {
    while (!x.load(std::memory_order_acquire)); // (3)
    if (y.load(std::memory_order_acquire)) ++z; // (4)
}

void read_y_then_x() {
    while (!y.load(std::memory_order_acquire)); // (5)
    if (x.load(std::memory_order_acquire)) ++z; // (6)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则最终不能保证 &lt;code&gt;z&lt;/code&gt; 不为 0. 在同一次运行中, &lt;code&gt;read_x_then_y&lt;/code&gt; 有可能看到先 (1) 后 (2), 而 &lt;code&gt;read_y_then_x&lt;/code&gt; 有可能看到先 (2) 后 (1). 这样有可能 (4) 和 (6) 的 load 的结果都为 &lt;code&gt;false&lt;/code&gt;, 导致最后 &lt;code&gt;z&lt;/code&gt; 仍然为 0.&lt;/p&gt;
&lt;p&gt;release-acquire 的开销比 sequencial consistent 小. 在 x86 架构下, &lt;code&gt;memory_order_acquire&lt;/code&gt; 和 &lt;code&gt;memory_order_release&lt;/code&gt; 的操作不会产生任何其他的指令, 只会影响编译器的优化: 任何指令都不能重排到 acquire 操作的前面, 且不能重排到 release 操作的后面; 否则会违反 release-acquire 的语义. 因此很多需要实现 synchronizes-with 关系的场景都会使用 release-acquire&lt;/p&gt;
&lt;h3&gt;宽松（relaxed ordering ）&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt; 可以用于 store, load 和 read-modify-write 操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这种模型下, 只能保证操作的原子性和修改顺序 (modification order) 一致性, 无法实现 synchronizes-with 的关系. 对于其它读写操作没有任何同步和重排的限制，仅要求保证读写的原子性和内存一致性。除此之外，不提供任何跨线程的同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一般应用于计数器场景&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完上面的程序，可能出现&lt;code&gt;r1 == r2 == 42&lt;/code&gt;。理解这一点并不难，因为编译器允许调整 C 和 D 的执行顺序。如果程序的执行顺序是 D -&amp;gt; A -&amp;gt; B -&amp;gt; C，那么就会出现&lt;code&gt;r1 == r2 == 42&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;bool&amp;gt; x{false}, y{false};

void thread1() {
    x.store(true, std::memory_order_relaxed); // (1)
    y.store(true, std::memory_order_relaxed); // (2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;thread1&lt;/code&gt; 对不同的变量执行 store 操作. 那么在某些线程看来, 有可能是 &lt;code&gt;x&lt;/code&gt; 先变为 &lt;code&gt;true&lt;/code&gt;, y 后变为 &lt;code&gt;true&lt;/code&gt;; 另一些线程看来, 又有可能是 &lt;code&gt;y&lt;/code&gt; 先变为 &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt; 后变为 &lt;code&gt;true&lt;/code&gt;. 如果这样测试这段代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void thread2() {
    while (!y.load(std::memory_order_relaxed)); // (3)
    assert(x.load()); // (4)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(4) 处的断言就有可能失败. 因为 (2) 与 (3) 之间没有 synchronizes-with 的关系, 所以就不能保证 (1) “happens-before” (4). 因此 (4) 就有可能读到 &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230114234414.png&quot; alt=&quot;QQ截图20230114234414&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图中x代表atomic类型变量。此时在一个线程上执行时，在这个线程认为不影响最终结果的前提下，实际执行时指令可能完全是乱的。写入a、写入b的操作实际执行时可能是调换了；写入c的操作可能实际在写入x之后执行；读出b的操作实际在写入x之前执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Relaxed 顺序模型的开销很小. 在 x86 架构下, &lt;code&gt;memory_order_relaxed&lt;/code&gt; 的操作不会产生任何其他的指令, 只会影响编译器优化, 确保操作是原子的. Relaxed 模型可以用在一些不需要线程同步的场景, 但是使用时要小心. &lt;strong&gt;例如 &lt;code&gt;std::shared_ptr&lt;/code&gt; 增加引用计数时用的就是 &lt;code&gt;memory_order_relaxed&lt;/code&gt;, 因为不需要同步; 但是减小应用计数不能用它, 因为需要与析构操作同步&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;小的总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;有效的Memory order枚举值&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Load&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt;, &lt;code&gt;memory_order_consume&lt;/code&gt;, &lt;code&gt;memory_order_acquire&lt;/code&gt;, &lt;code&gt;memory_order_seq_cst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;其它枚举值不合法， MS STL的实现是将其当作memory_order_seq_cst处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt;, &lt;code&gt;memory_order_release&lt;/code&gt;, &lt;code&gt;memory_order_seq_cst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read-modify-write&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt;, &lt;code&gt;memory_order_consume&lt;/code&gt;, &lt;code&gt;memory_order_acquire&lt;/code&gt;, &lt;code&gt;memory_order_release&lt;/code&gt;, &lt;code&gt;memory_order_acq_rel&lt;/code&gt;, &lt;code&gt;memory_order_seq_cst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt;: 最宽松的内存顺序, 只保证操作的&lt;strong&gt;原子性&lt;/strong&gt;和&lt;strong&gt;修改顺序 (modification order)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_acquire&lt;/code&gt;, &lt;code&gt;memory_order_release&lt;/code&gt; 和 &lt;code&gt;memory_order_acq_rel&lt;/code&gt;: 实现 &lt;strong&gt;acquire 操作&lt;/strong&gt;和 &lt;strong&gt;release 操作&lt;/strong&gt;, 如果 acquire 操作读到了 release 操作写入的值, 或其 release sequence 写入的值, 则构成 &lt;strong&gt;synchronizes-with 关系&lt;/strong&gt;, 进而可以推导出 &lt;strong&gt;happens-before 的关系&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_consume&lt;/code&gt;: 实现 &lt;strong&gt;consume 操作&lt;/strong&gt;, 能实现数据依赖相关的同步关系. 如果 consume 操作读到了 release 操作写入的值, 或其 release sequence 写入的值, 则构成 &lt;strong&gt;dependency-ordered before 的关系&lt;/strong&gt;, 对于有数据依赖的操作可以进而推导出 &lt;strong&gt;happens-before 的关系&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_seq_cst&lt;/code&gt;: 加强版的 acquire-release 模型, 除了可以实现 &lt;strong&gt;synchronizes-with 关系&lt;/strong&gt;, 还保证&lt;strong&gt;全局顺序一致&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考资料：https://zhuanlan.zhihu.com/p/382372072?utm_id=0&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/wxj1992/category_11580766.html&lt;/p&gt;
&lt;p&gt;https://lday.me/2017/12/02/0018_cpp_atomic_summary/&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;memory_order_acquire&lt;/code&gt;和&lt;code&gt;memory_order_release&lt;/code&gt; 如何搭配使用&lt;/h2&gt;
&lt;p&gt;往往&lt;code&gt;memory_order_acquire&lt;/code&gt;和&lt;code&gt;memory_order_release&lt;/code&gt;是配合着一起使用的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程1使用&lt;code&gt;memory_order_release&lt;/code&gt;写入&lt;strong&gt;原子变量x&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;线程2使用&lt;code&gt;memory_order_acquire&lt;/code&gt;读出&lt;strong&gt;原子变量x&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所有在线程1上，在写入x之前的写入操作，都将在线程2上，在读出x之后，被看到。使用单向“加载”+单向“释放”协议的场景往往是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程1，写入一些实际数据，接着通过将原子变量x设置为某个值&lt;code&gt;A&lt;/code&gt;（通过使用&lt;code&gt;memory_order_release&lt;/code&gt;写入原子变量x）来“发布”这些数据。&lt;/li&gt;
&lt;li&gt;线程2，通过读取并判断x已被设置为&lt;code&gt;A&lt;/code&gt;（通过使用&lt;code&gt;memory_order_acquire&lt;/code&gt;来读取原子变量x），进而读取线程1实际“发布”的那些数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;必须操作的是同一个原子变量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230115011213.png&quot; alt=&quot;QQ截图20230115011213&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，thread_1在&lt;code&gt;release&lt;/code&gt;写入&lt;code&gt;x&lt;/code&gt;(值:&lt;code&gt;A&lt;/code&gt;)之前，写入了待发布的&lt;code&gt;a&lt;/code&gt;,&lt;code&gt;b&lt;/code&gt;的数据，而thread_2，将在&lt;code&gt;acquire&lt;/code&gt;读出&lt;code&gt;x&lt;/code&gt;且为&lt;code&gt;A&lt;/code&gt;之后，将读到thread_1发布的&lt;code&gt;a&lt;/code&gt;,&lt;code&gt;b&lt;/code&gt;的数据。同时，我们可以注意到，在thread_2上，在&lt;code&gt;acquire&lt;/code&gt;读出&lt;code&gt;x&lt;/code&gt;之前，如果对&lt;code&gt;a&lt;/code&gt;进行读操作，我们是无法确认读到的&lt;code&gt;a&lt;/code&gt;一定会thread_1在之前最后写入的&lt;code&gt;a&lt;/code&gt;，这里的顺序是不会被保证的，重排是被允许的。同时，在之后，读取&lt;code&gt;c&lt;/code&gt;，读到的是否为thread_1最后写入的&lt;code&gt;c&lt;/code&gt;，也是不确定的，因为，在&lt;code&gt;x&lt;/code&gt;写入之后，thread_1上又出现了一次写入，而如果在此之前，还有一次写入， 这两次写入之间，是不存在限制，可能会被重排的。&lt;/p&gt;
&lt;p&gt;thread_1上有了&lt;code&gt;release_store&lt;/code&gt;，对于&lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt;的写入就一定会在&lt;code&gt;x&lt;/code&gt;的改变之前，在thread_2上，就不会出现类似读出&lt;code&gt;c&lt;/code&gt;，的不确定性。thread_2上有了acquire_load，右侧的读出&lt;code&gt;a&lt;/code&gt;，就不会被重排读到左侧，而左侧读出&lt;code&gt;a&lt;/code&gt;的不确定性，也不存在。thread_1卡住的是：对于数据&lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt;的写入不能排到&lt;code&gt;x&lt;/code&gt;的写入之后，thread_2卡住的，是对于数据&lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt;的读取，不能排到读取&lt;code&gt;x&lt;/code&gt;之前，这样，就保证了数据&lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt;，与”信号量”&lt;code&gt;x&lt;/code&gt;，之间，在thread_1, thread_2上的同步关系。&lt;/p&gt;
&lt;h2&gt;Release sequences 释放序列&lt;/h2&gt;
&lt;p&gt;到目前为止我们看到的, 无论是 sequencial consistent 还是 release-acquire, 要想实现 synchronizes-with 的关系, acquire 操作必须在&lt;strong&gt;同一个原子变量&lt;/strong&gt;上读到 release 操作的写入的值. 如果 acquire 操作没有读到 release 操作写入的值, 那么它俩之间通常没有 synchronizes-with 的关系. 例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;int&amp;gt; x{0}, y{0};

void thread1() {
    x.store(1, std::memory_order_relaxed); // (1)
    y.store(1, std::memory_order_release); // (2)
}

void thread2() {
    y.store(2, std::memory_order_release); // (3)
}

void thread3() {
    while (!y.load(std::memory_order_acquire)); // (4)
    assert(x.load(std::memory_order_relaxed) == 1); // (5)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子中, 只要 &lt;code&gt;y&lt;/code&gt; 的值非 0 循环 (4) 就会退出. 当它退出时, 有可能读到 (2) 写入的值, 也有可能读到 (3) 写入的值. 如果是后者, 则只能保证 (3) “synchronizes-with” (4), 不能保证与 (2) 与 (4) 之间有同步关系. 因此 (5) 处的断言就有可能失败.&lt;/p&gt;
&lt;p&gt;但并不是只有在 acquire 操作读取到 release 操作写入的值时才能构成 synchronizes-with 关系. 为了说这种情况, 我们需要引入 &lt;strong&gt;release sequence&lt;/strong&gt; 这个概念.&lt;/p&gt;
&lt;p&gt;针对一个原子变量 M 的 release 操作 A 完成后, 接下来 M 上可能还会有一连串的其他操作. 如果这一连串操作是由&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一线程上的写操作, 或者&lt;/li&gt;
&lt;li&gt;任意线程上的 read-modify-write 操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两种构成的, 则称这一连串的操作为&lt;strong&gt;以 release 操作 A 为首的 release sequence&lt;/strong&gt;. 这里的写操作和 read-modify-write 操作可以使用任意内存顺序.&lt;/p&gt;
&lt;p&gt;如果一个 acquire 操作在同一个原子变量上读到了一个 release 操作写入的值, 或者读到了以这个 release 操作为首的 release sequence 写入的值, 那么这个 release 操作 “synchronizes-with” 这个 acquire 操作. 我们来看个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;int&amp;gt; data;
std::atomic&amp;lt;int&amp;gt; flag{0};

void thread1() {
    data.push_back(42); // (1)
    flag.store(1, std::memory_order_release); // (2)
}

void thread2() {
    int expected = 1;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) // (3) 注意此处使用了宽松
        expected = 1;
}

void thread3() {
    while (flag.load(std::memory_order_acquire) &amp;lt; 2); // (4)
    assert(data.at(0) == 42); // (5)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子中, (3) 处的 &lt;code&gt;compare_exchange_strong&lt;/code&gt; 是一种 RMW 操作, 它判断原子变量的值是否与期望的值 (第一个参数) 相等, 如果相等则将原子变量设置成目标值 (第二个参数) 并返回 &lt;code&gt;true&lt;/code&gt;, 否则将第一个参数 (引用传递) 设置成原子变量当前值并返回 &lt;code&gt;false&lt;/code&gt;. 操作 (3) 会一直循环检查, 当 &lt;code&gt;flag&lt;/code&gt; 当值为 1 时, 将其替换成 2. 所以 (3) 属于 (2) 的 release sequence. 而循环 (4) 退出时, 它已经读到了 (3) 写入的值, 也就是 release 操作 (2) 为首的 release sequence 写入的值. 所以有 (2) “synchronizes-with” (4). 因此 (1) “happens-before” (5), (5) 处的断言不会失败.&lt;/p&gt;
&lt;p&gt;注意 (3) 处的 &lt;code&gt;compare_exchange_strong&lt;/code&gt; 的内存顺序是 &lt;code&gt;memory_order_relaxed&lt;/code&gt;, 所以 (2) 与 (3) 并不构成 synchronizes-with 的关系. 也就是说, 当循环 (3) 退出时, 并不能保证 &lt;code&gt;thread2&lt;/code&gt; 能读到 &lt;code&gt;data.at(0)&lt;/code&gt; 为 42. 但是 (3) 属于 (2) 的 release sequence, 当 (4) 以 &lt;code&gt;memory_order_acquire&lt;/code&gt; 的内存顺序读到 (2) 的 release sequence 写入的值时, 可以与 (2) 构成 synchronizes-with 的关系&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意, 这种只是一种概念. 依赖于代码的正确性. 同时, 我们说 &quot;当循环 (3) 退出时, 并不能保证 &lt;code&gt;thread2&lt;/code&gt; 能读到 &lt;code&gt;data.at(0)&lt;/code&gt; 为 42&quot;的原因是 这个插入操作由于缓存一致性的原因, 并不保证被刷新到thread2的cache内. 尽管从逻辑上讲, 当flag从1变为2的时候, 写入操作已经发生了, 但是这个时候这个写入并不一定保证对thread2&lt;em&gt;可见&lt;/em&gt;. 当然了, 假设这个原子变量和写入数据在一个cache line中, 这个操作可能对thread2是可见的&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;如何理解内存序和锁之间的联系？&lt;/h2&gt;
&lt;p&gt;多线程编程，临界区是一个很重要的概念。我们对此再做进一步的认识。&lt;/p&gt;
&lt;p&gt;对于临界区区内的语句，显而易见，我们不能将其移出临界区，如下图所示，编译器和CPU都不会做这种移出临界区的优化：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230115011701.png&quot; alt=&quot;QQ截图20230115011701&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但我们可以将临界区区外的代码移进来，如下图所示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230115011724.png&quot; alt=&quot;QQ截图20230115011724&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230116025012.png&quot; alt=&quot;QQ截图20230116025012&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上面两张图我们可以看出，&lt;code&gt;lock&lt;/code&gt;和&lt;code&gt;unlock&lt;/code&gt;可以看作两个单方向的屏障，&lt;strong&gt;&lt;code&gt;lock&lt;/code&gt;对应的屏障，只允许代码往下方向移动，而`unlock则只允许上方向移动。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以我们是不是发现：&lt;code&gt;memory_order_acquire&lt;/code&gt;类似&lt;code&gt;lock&lt;/code&gt;，&lt;code&gt;memory_order_release&lt;/code&gt;类似&lt;code&gt;unlock&lt;/code&gt;，这两个都是单方向的屏障（One-way Barriers: acquire barrier, release barrier）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以借助内存屏障+原子变量，在比互斥锁更底层的层面，我们也能实现线程间同步。比如下图的例子：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;p&lt;/code&gt;原子变量，当线程2看到&lt;code&gt;p&lt;/code&gt;为非空时，后续的&lt;code&gt;r1 = A&lt;/code&gt;语句可以看到线程1对&lt;code&gt;A&lt;/code&gt;的写操作结果。这是因为线程1对&lt;code&gt;p&lt;/code&gt;的&lt;code&gt;store&lt;/code&gt;操作Synchronizes-with线程2对&lt;code&gt;p&lt;/code&gt;的&lt;code&gt;load&lt;/code&gt;操作，这样加之Happens-before关系，我们实现了线程间同步。值得一提的是，线程1中，&lt;code&gt;A=B+1&lt;/code&gt;Happens-before &lt;code&gt;B = 1&lt;/code&gt;，但&lt;code&gt;B = 1&lt;/code&gt;可能先执行，但线程1对&lt;code&gt;A&lt;/code&gt;的写操作的结果，依旧能正确地被线程2中&lt;code&gt;int r1 = A&lt;/code&gt;语句读取。&lt;/p&gt;
&lt;p&gt;换句话说，我们利用了&lt;code&gt;memory_order_release&lt;/code&gt;，使得针对&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;B&lt;/code&gt;的写入一定会发生在写入原子变量&lt;code&gt;p&lt;/code&gt;之前。我们利用了&lt;code&gt;memory_order_acquire&lt;/code&gt;使得针对&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;x&lt;/code&gt;的读取一定发生在读取原子变量&lt;code&gt;p&lt;/code&gt;之前。&lt;/p&gt;
&lt;p&gt;有人也许会问，用非原子变量难道不能实现Synchronizes-with关系？首先，非原子变量，显然不能被多个线程同时读写，再者，其无法提供内存屏障，不要忘了前文重排的例子，试想，若线程1中&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;B&lt;/code&gt;的写入操作被重排到了&lt;code&gt;p.store&lt;/code&gt;的后面，且即使替换&lt;code&gt;p.store&lt;/code&gt;的非原子变量操作在多线程下可以正确执行，但此时&lt;code&gt;int r1 = A&lt;/code&gt;能读取到正确的值吗？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20220909144257.png&quot; alt=&quot;QQ截图20220909144257&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;这里所谓的指定内存序，&lt;strong&gt;指的是对执行语句所在的线程内部的限制，也就是只影响一个cpu核心，但是这些对单线程内部的限制组合起来就能实现多线程之间数据同步的效果&lt;/strong&gt;，&lt;/h3&gt;
&lt;h2&gt;同步的传递性和acquire搭配release的小例子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;int&amp;gt; data[5];
atomic&amp;lt;bool&amp;gt; sync1(false), sync2(false);

void thread_1_func(){
  data[0].store(1, memory_order_relaxed);
  data[1].store(2, memory_order_relaxed);
  data[2].store(3, memory_order_relaxed);
  data[3].store(4, memory_order_relaxed);
  data[4].store(5, memory_order_relaxed);
  sync1.store(true, memory_order_release); //注释1
}

void thread_2_func(){
  while(!sync1.load(memory_order_acquire)); //注释2
  sync2.store(true, memory_order_release); //注释3
}

void thread_3_func(){
  while(!sync2.load(memory_order_acquire)); //注释4
  assert(data[0].load(memory_order_relaxed) == 1);
  assert(data[1].load(memory_order_relaxed) == 2);
  assert(data[2].load(memory_order_relaxed) == 3);
  assert(data[3].load(memory_order_relaxed) == 4);
  assert(data[4].load(memory_order_relaxed) == 5);
  cout &amp;lt;&amp;lt; &quot;all success&quot; &amp;lt;&amp;lt; endl;
}

int main(){
  thread th1(thread_1_func);
  thread th2(thread_2_func);
  thread th3(thread_3_func);

  th1.join();
  th2.join();
  th3.join();
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;func1&lt;/code&gt;当中，我们的数据存储使用了&lt;code&gt;relaxed&lt;/code&gt;，所以可能是乱序，但是无关紧要。我们的核心目的是保证&lt;code&gt;thread3&lt;/code&gt;能看到&lt;code&gt;thread1&lt;/code&gt;中存储的全部数据。所以：&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;func1&lt;/code&gt;中，我们针对&lt;code&gt;sync1&lt;/code&gt;的写入使用了&lt;code&gt;release&lt;/code&gt;。也就是说，在把&lt;code&gt;sync1&lt;/code&gt;修改为&lt;code&gt;true&lt;/code&gt;的这个操作之前，可以保证前面的操作全部完成，不会重排到这个操作之后，同时也保证缓存一致性，也就是之前的操作全部对其他线程可见。&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;func2&lt;/code&gt;中，我们针对&lt;code&gt;sync1&lt;/code&gt;的读取使用了&lt;code&gt;acquire&lt;/code&gt;操作。也就是说，在读取到&lt;code&gt;sync1&lt;/code&gt;是&lt;code&gt;true&lt;/code&gt;之前，任何操作不能被重排到该操作之前。也就保证了针对&lt;code&gt;sync2&lt;/code&gt;的修改一定发生在发现&lt;code&gt;sync1&lt;/code&gt;是&lt;code&gt;true&lt;/code&gt;之后。&lt;/li&gt;
&lt;li&gt;随后在&lt;code&gt;func2&lt;/code&gt;中，我们针对&lt;code&gt;sync3&lt;/code&gt;的写入使用了&lt;code&gt;release&lt;/code&gt;。原因同上。&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;func3&lt;/code&gt;中，针对&lt;code&gt;sync3&lt;/code&gt;的读取使用了&lt;code&gt;acquire&lt;/code&gt;，原因同上。&lt;/li&gt;
&lt;li&gt;最后在&lt;code&gt;assert&lt;/code&gt;中，乱序可能发生，但是无所谓。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230115184315.png&quot; alt=&quot;QQ截图20230115184315&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;acquire&lt;/code&gt;和&lt;code&gt;release&lt;/code&gt;的语义可以形象理解为：如果我不释放（release），你的请求（acquire）必须等待，直到我释放为止。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;atomic_thread_fence分类和效果&lt;/h2&gt;
&lt;p&gt;在C++ 11及之后的标准里，除了利用原子操作指定内存序，还定义了单独使用memory fence（&lt;code&gt;std::atomic_thread_fence&lt;/code&gt;）的方式，fence可以和原子操作组合进行同步，也可以fence之间进行同步，fence不光可以不依赖原子操作进行同步，而且相比较于同样memory order的原子操作，具有更强的内存同步效果&lt;/p&gt;
&lt;p&gt;和&lt;code&gt;atomic&lt;/code&gt;变量类似，&lt;code&gt;atomic_thread_fence&lt;/code&gt;也可以指定六种内存序，指定不同内存序的fence可以分为以下几类：
（1） &lt;code&gt;std::atomic_thread_fence(memory_order_relaxed)&lt;/code&gt;，没有任何效果。
（2） &lt;code&gt;std::atomic_thread_fence(memory_order_acquire)&lt;/code&gt; 和 &lt;code&gt;std::atomic_thread_fence(memory_order_consume)&lt;/code&gt; 属于acquire fence。
（3）&lt;code&gt;std::atomic_thread_fence(memory_order_release)&lt;/code&gt;属于release fence。
（4）&lt;code&gt;std::atomic_thread_fence(memory_order_acq_rel)&lt;/code&gt;既是acquire fence 也是release fence，为了方便这里称为full fence。
（5）&lt;code&gt;std::atomic_thread_fence(memory_order_seq_cst)&lt;/code&gt;额外保证有单独全序的full fence。&lt;/p&gt;
&lt;p&gt;也就是说，如果不考虑单独全序，那么有release fence、acquire fence 和full fence三种。下面就根据以前介绍过的四种重排来介绍下这三种fence的效果。&lt;/p&gt;
&lt;p&gt;不同类型的Fence对于乱序的保护是不一样的。我们可以将读和写的交错分成下面四种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load-Load：读接着读&lt;/li&gt;
&lt;li&gt;Load-Store：先读后写&lt;/li&gt;
&lt;li&gt;Store-Load：先写后读&lt;/li&gt;
&lt;li&gt;Store-Store：写接着写&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;release fence&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Release fence&lt;/code&gt;可以防止fence前的内存操作重排到fence后的任意store（写入）之后，即阻止load-store重排和store-store重排。（&lt;strong&gt;阻止了所有在它之前的读写操作和在它之后的写操作乱序&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20220909153644.png&quot; alt=&quot;QQ截图20220909153644&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;acquire fence&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;acquire fence&lt;/code&gt;可以防止fence后的内存操作重排到fence前的任意load（读取）之前，即阻止load-load重排和load-store重排。（&lt;strong&gt;阻止了所有在它之前的读操作与在它之后的读写操作乱序。&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20220909153840.png&quot; alt=&quot;QQ截图20220909153840&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;full fence&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;因为&lt;code&gt;full fence&lt;/code&gt;是release fence和acquire fence的组合，所以也就是防止load-load、load-store、store-store重排&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20220909154223.png&quot; alt=&quot;QQ截图20220909154223&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;C++标准中，三种fence不禁止store-load（先写后读）的重排。&lt;/h3&gt;
&lt;p&gt;即便是&lt;code&gt;std::atomic_thread_fence(memory_order_seq_cst)&lt;/code&gt;也一样，只是需要额外保证单独全序，&lt;strong&gt;但是在实际的实现上为了实现这个全序编译器大都是采用了硬件层面的能够阻止storeload重排的full barrier指令&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;p&gt;https://paul.pub/cpp-memory-model/&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/wxj1992/article/details/103917093&lt;/p&gt;
&lt;p&gt;https://luyuhuang.tech/2022/06/25/cpp-memory-order.html&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/682286231&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nekrozqliphort.github.io/posts/happens-b4/&quot;&gt;什么是strongly happens before&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;来自&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=Mzg5MjU0NTI5OQ==&amp;amp;mid=2247489076&amp;amp;idx=1&amp;amp;sn=748bf716d94d5ed2739ea8a9385cd4a6&amp;amp;chksm=c03d2648f74aaf5e11298cf450c3453a273eb6d2161bc90e411b6d62fa0c1b96a45e411af805&amp;amp;token=1987775079&amp;amp;lang=zh_CN&amp;amp;scene=21#wechat_redirect&quot;&gt;百度&lt;/a&gt;的介绍内存访问，分配，MESI，内存序优化的简短文章&lt;/p&gt;
&lt;h3&gt;memory barrier 和 memory fence的细微区别&lt;/h3&gt;
&lt;p&gt;chatGPT的回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在C++11标准中，memory fence和memory barrier是等价的概念，它们都用于确保多线程环境下的内存可见性和顺序性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在C++11之前，memory fence通常是指硬件级别的操作，而memory barrier则是指编译器级别的操作，但是这种区分在C++11之后已经不再重要。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在C++11中，memory fence和memory barrier都有两种类型：acquire和release。acquire fence/barrier用于确保之前的读操作在当前之后的操作之前执行，release fence/barrier用于确保之后的写操作在当前之前的操作之后执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;区别在于，memory fence是一种完全的内存屏障，它会禁止编译器和CPU对内存操作的任何优化，强制所有线程按照特定的顺序执行内存操作。而memory barrier只是一种编译器屏障，它只能保证编译器不会对内存操作进行优化，但不能保证CPU不进行优化，因此在某些平台上可能不够稳定。所以memory barrier又称为 compiler barrier&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此外，memory fence和memory barrier的使用需要根据具体的情况来确定。通常，只有在需要非常精细的内存控制时才需要使用memory fence和memory barrier，因为它们会影响程序的性能。在一般情况下，可以通过使用std::atomic类型来实现线程安全的操作，它会自动处理内存屏障和同步操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230421014842.png&quot; alt=&quot;QQ截图20230421014842&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;NULL和nullptr区别&lt;/h1&gt;
&lt;p&gt;其实&lt;code&gt;NULL&lt;/code&gt;根据命名全大写可以看出来，&lt;strong&gt;它是一个常量，既然是常量，就需要进行宏定义。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C语言的标准头文件是这样定义的&lt;code&gt; #define NULL ((void*)0)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;而到了C++中，则变成了&lt;code&gt;#define NULL 0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从定义中可以看出，C++中，NULL其实就是0，但是也可以用作空指针，只是用作空指针可能是为了兼容C，迫于无奈。&lt;/p&gt;
&lt;p&gt;但是当NULL既可以表示0又可以表示空指针的时候，发生函数重载的时候就会有二义性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void test(void *p)
{
    cout&amp;lt;&amp;lt;&quot;p is pointer &quot;&amp;lt;&amp;lt;p&amp;lt;&amp;lt;endl;
 }
void test(int num)
{
    cout&amp;lt;&amp;lt;&quot;num is int &quot;&amp;lt;&amp;lt;num&amp;lt;&amp;lt;endl; 
}
int main(void)
{
    test(NULL); //NULL是int还是指针？
    return 0; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显，NULL存在二义性，它既是整数，也是一个指针，函数test()无法根据参数的数据类型判断应该调用哪一个实现。
这时使用nullptr的优越性就体现出来了，因为它可以很好地把空指针这一层意思给剥离出来。nullptr就是C++11为了解决这个痛点而推出的东西。&lt;/p&gt;
</content:encoded></item><item><title>C++杂记 - 3（一）：std::bind/function、call_once、future/promise 与 atomic</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B03/</guid><pubDate>Tue, 06 Sep 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++杂记 - 3&lt;/h1&gt;
&lt;h1&gt;std::bind, std::function 和 std::mem_fn&lt;/h1&gt;
&lt;h2&gt;std::bind 包括但不限于mem_fn的功能，更为通用的解决方案&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;什么是bind？我们可以把它看作一个通用的函数适配器，它接受一个可调用对象，生成一个新的可调用对象来“适应”原对象的参数列表。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;std::bind将可调用对象与其参数一起进行绑定**，绑定后的结果可以使用std::function保存**。std::bind主要有以下两个作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将可调用对象和其参数绑定成一个仿函数；&lt;/li&gt;
&lt;li&gt;只绑定部分参数，减少可调用对象传入的参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto newCallable = bind(callable, arg_list);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该形式表达的意思是：当调用&lt;code&gt;newCallable&lt;/code&gt;时，会调用&lt;code&gt;callable&lt;/code&gt;，并传给它&lt;code&gt;arg_list&lt;/code&gt;中的参数。&lt;/p&gt;
&lt;p&gt;需要注意的是：&lt;code&gt;arg_list&lt;/code&gt;中的参数可能包含形如&lt;code&gt;_n&lt;/code&gt;的名字。其中&lt;code&gt;n&lt;/code&gt;是一个整数，这些参数是占位符，表示&lt;code&gt;newCallable&lt;/code&gt;的参数，它们占据了传递给&lt;code&gt;newCallable&lt;/code&gt;的参数的位置。数值&lt;code&gt;n&lt;/code&gt;表示生成的可调用对象中参数的位置：&lt;code&gt;_1&lt;/code&gt;为&lt;code&gt;newCallable&lt;/code&gt;的第一个&lt;strong&gt;待填充&lt;/strong&gt;参数，&lt;code&gt;_2&lt;/code&gt;为第二个&lt;strong&gt;待填充&lt;/strong&gt;参数，以此类推。
注意这些占位符在&lt;code&gt;std::placeholders&lt;/code&gt;名称空间内。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;std::bind的返回值是可调用实体，可以直接赋给std::function&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;bind绑定类非静态成员函数时，第一个参数表示对象的成员函数的指针，第二个参数表示对象的地址，这是因为对象的成员函数需要有this指针&lt;/strong&gt;。并且编译器不会将对象的成员函数隐式转换成函数指针，需要通过&amp;amp;手动转换（符合成员函数指针的赋值语法）。静态成员函数无需此操作。因为没有this指针。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意传入对象地址也可以传入对象的引用或对象本身。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在类内使用该类的成员函数的时候，和类外使用一样。都需要取地址并且传入对象指针。只不过可以使用&lt;code&gt;this&lt;/code&gt;替代。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这个的常见案例是类内含有&lt;code&gt;thread&lt;/code&gt;的时候，比如这种情况：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;struct test{
    test():th1(bind(&amp;amp;test::threadfunc, this)){}; //因为是类内使用该类成员函数，所以需要绑定，但是可以使用this替代。
    ~test(){
        if(th1.joinable()){
            th1.join();
        }
        
    }
    private:
        void threadfunc(){ //成员函数。
            cout &amp;lt;&amp;lt;&quot;hello world&quot; &amp;lt;&amp;lt; endl;
        }
    thread th1;
};
int main(){
    test mytest;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class my_class{
    public:
    int val;
    my_class(int x):val(x){}

    void getval(){
        cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
    }
    void add(int another){
        val = val + another;
        cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
    }
    static void staticfunc(int val){
        cout &amp;lt;&amp;lt; &quot;static&quot; &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
    }
};
int main(){
    my_class obj1(10);
    auto task2 = bind(&amp;amp;my_class::getval, &amp;amp;obj1); 	//调用无参函数，this参数预绑定。成员函数指针遵循语法。
    auto task3 = bind(&amp;amp;my_class::add, &amp;amp;obj1, placeholders::_1);	//调用有参函数，this参数预绑定。
    auto task4 = bind(&amp;amp;my_class::add, placeholders::_1, placeholders::_2); //调用有参函数，this参数使用占位形式。
    auto task5 = bind(&amp;amp;my_class::staticfunc, placeholders::_1); //静态成员函数没有this指针。无需传入。
    task2();
    task3(3); 
    task4(&amp;amp;obj1, 3); //this使用占位形式，需要调用时传入。
    task5(3);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;默认情况下，bind的那些不是占位符的参数会被拷贝或移动（以值传递）到bind返回的可调用对象中。如果需要使用引用传递，必须使用&lt;code&gt;ref&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void mypred(int a, int b, vector&amp;lt;int&amp;gt;&amp;amp; c){ 尽管这里函数头使用了引用，但是bind在预绑定的时候(也就是非placeholder参数）默认会拷贝一份原参数进行传入，也就是默认是值传递。所以这里的容器c其实是一个局部变量。
    if(a &amp;lt; b){
        cout &amp;lt;&amp;lt;&quot;yes&quot; &amp;lt;&amp;lt; endl;
        c.push_back(a);
        
    }
}

int main(){
    vector&amp;lt;int&amp;gt; test = {1};
    vector&amp;lt;int&amp;gt; final;
    final.reserve(10);
    for_each(test.begin(), test.end(), bind(mypred, placeholders::_1, 3, ref(final))); 所以这里我们如果要使用引用来传递final，则必须要使用ref函数来获取其引用。
    for(auto i = final.begin(); i != final.end(); i++){
        cout &amp;lt;&amp;lt; *i &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用std::bind搭配priority_queue 和 vector的自定义排序。&lt;/h2&gt;
&lt;p&gt;请参考STL-1笔记&lt;/p&gt;
&lt;h2&gt;慎用std::bind, 如果可能的话使用lambda&lt;/h2&gt;
&lt;p&gt;https://lefticus.gitbooks.io/cpp-best-practices/content/08-Considering_Performance.html&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/49246242/efficiency-of-stdbind-vs-lambda&lt;/p&gt;
&lt;p&gt;https://youtu.be/ZlHi8txU4aQ&lt;/p&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/VOqPjW48DG3gp60EqkYrTQ&lt;/p&gt;
&lt;h2&gt;mem_fn 成员函数适配器 把成员函数转为函数对象，使用对象指针或对象(引用)进行绑定&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;个人实验得出的结论：&lt;code&gt;mem_fn&lt;/code&gt;是&lt;code&gt;bind&lt;/code&gt;的子集&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;注意&lt;code&gt;mem_fn&lt;/code&gt;不能调用类静态成员函数。因为没有this指针。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mem_fn&lt;/code&gt;就是强制给你把类对象塞进去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mem_fn&lt;/code&gt;无法接受额外参数。也就是无法使用&lt;code&gt;placeholder&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mem_fn&lt;/code&gt;的核心功能是把类成员函数转换成不需要类成员就可以调用的形式。就是把&lt;code&gt;this&lt;/code&gt;指针绑定到类成员函数的隐藏&lt;code&gt;this&lt;/code&gt;参数上。但是调用的时候依旧需要传入一个对象地址。。就很废物。&lt;/p&gt;
&lt;p&gt;比较：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class my_class{
    public:
    int val;
    my_class(int x):val(x){}

    void getval(){
        cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
    }
    void add(int another){
        val = val + another;
        cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
    }
    static void staticfunc(int val){
        cout &amp;lt;&amp;lt; &quot;static&quot; &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
    }
};
int main(){
    my_class obj1(10);

    auto task1 = mem_fn(&amp;amp;my_class::getval); 
    task1(&amp;amp;obj1); //把对象地址传入

    auto task2 = mem_fn(&amp;amp;my_class::add);
    task2(&amp;amp;obj1, 5);

    //auto task3 = mem_fn(&amp;amp;my_class::staticfunc); 错误！静态成员函数不行。

    auto task1_1 = bind(&amp;amp;my_class::getval, placeholders::_1); //bind就得多写个参数
    task1_1(&amp;amp;obj1);
    
    auto task1_2 = bind(&amp;amp;my_class::getval, &amp;amp;obj1);
    task1_2();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;std::placeholders&lt;/h2&gt;
&lt;p&gt;我们提到了，我们使用&lt;code&gt;bind&lt;/code&gt;的时候，&lt;code&gt;placeholder&lt;/code&gt;是待填充参数。什么叫待填充？这是一种具象解释。其实&lt;code&gt;bind&lt;/code&gt;相当于生成了一个新的可调用对象，拥有两个参数。但是结合原本的可调用对象来看，我们有三个参数。所以站在原本的可调用对象角度来看，这原来的可调用对象中多出来的一个参数相当于已填充参数。剩下的两个参数相当于待填充。&lt;/p&gt;
&lt;p&gt;语法和例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void testfunc(int x, int y, int z){
    cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; y &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; z &amp;lt;&amp;lt; endl;
}
int main(){

    auto callable1 = bind(testfunc, placeholders::_1, placeholders::_2, 5);
    callable1(8,80);
    //输出8 80 5
    auto callable2 = bind(testfunc, placeholders::_2, placeholders::_1, 5);
    callable2(8,80);
    //输出80 8 5

    auto callable3 = bind(testfunc, placeholders::_1, 5, placeholders::_2);
    callable3(8,80);
    //输出8 5 80
    auto callable4 = bind(testfunc, placeholders::_1, 5, placeholders::_3); //错误。新的调用对象只有两个参数。这里3超了。并placeholders且必须按序。不可跨越。不能没有2的时候直接使用3，尽管可能待填充参数一共有3个。
    callable4(8,80);
    
    auto callable5 = bind(testfunc, placeholders::_1, 5, placeholders::_1); //极端情况。占位符可以重复。
    callable5(8);
    //输出8 5 8
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;placeholder&lt;/code&gt;编号必须按序。此处按序指的是不能没有&lt;code&gt;2&lt;/code&gt;的时候直接使用&lt;code&gt;3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;并且编号不能大于待填充参数的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考资料：https://elloop.github.io/c++/2015-12-15/learning-using-stl-12-std-bind&lt;/p&gt;
&lt;h2&gt;std::function&lt;/h2&gt;
&lt;p&gt;大家都用过函数指针。具体就不赘述了。稍微回忆一下函数指针的语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void(*another_a_ptr_ptr)(void) = (void(*)(void))*(long long*)(*a_ptr);
void(*another_a_ptr_ptr)(void); 这个函数指针的类型是void(*)(void)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成员函数指针的用法和声明在杂记2。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;std::function是一个多态可调用对象包装器，是一个类模板，可以容纳除所有可调用对象（类成员函数和指针需要bind一下），它可以用统一的方式处理函数、函数对象、函数指针，lambda并允许保存和延迟它们的执行。基本上任何有函数调用运算符重载&lt;code&gt;operator()&lt;/code&gt;的对象都可以被function包装。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意，std::function支持多态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个例子让你知道&lt;code&gt;function&lt;/code&gt;怎么用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int myfunc(int a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    return a;
}

int myfunc1(const int&amp;amp; a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    return a;
}

int myfunc2(int&amp;amp;&amp;amp; a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    return a;
}

int main(){
    function&amp;lt;int(int)&amp;gt; func = myfunc;
    func(4);

    function&amp;lt;int(const int&amp;amp;)&amp;gt; func1 = myfunc1;
    func1(4);

    function&amp;lt;int(int&amp;amp;&amp;amp;)&amp;gt; func2 = myfunc2;
    func1(4);

    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说白了就是类型是你的函数返回值类型+参数类型。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为&lt;code&gt;std::function&lt;/code&gt;可以保存lambda。所以当函数签名的参数是&lt;code&gt;std::function&lt;/code&gt;对象的时候，可以直接传入一个类型匹配的lambda对象。有隐式类型转换&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们前面提到过 &lt;code&gt;bind&lt;/code&gt;的返回值是可调用实体，可以直接赋给&lt;code&gt;std::function&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function&amp;lt;void(void)&amp;gt; task1_2 = bind(&amp;amp;my_class::getval, &amp;amp;obj1);
task1_2();

function&amp;lt;void(int)&amp;gt; task2 = bind(&amp;amp;my_class::add, &amp;amp;obj1, placeholders::_1);
task2(5);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;有一个问题是，这里必须要显式把对象地址直接塞进去，不能把对象地址用占位符。因为参数对不上了。（个人实验）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;故而，std::function的作用可以归结于：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;std::function对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的封装，形成一个新的可调用的std::function对象，简化调用；&lt;/li&gt;
&lt;li&gt;std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(如：函数指针这类可调用实体，是类型不安全的)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;慎用std::function 性能极差&lt;/h3&gt;
&lt;p&gt;虚函数的开销是常规函数的5倍，&lt;code&gt;std::function&lt;/code&gt;是6倍以上。&lt;/p&gt;
&lt;p&gt;原因是&lt;code&gt;std::function&lt;/code&gt;的底层还是使用虚函数，进行多态调用。因为使用了类型擦除。同时，&lt;code&gt;std::function&lt;/code&gt;是通用的，并且它不知道包装的可调用对象多大（比如lambda就是动态大小的，我们在lambda一节中提到了lambda的大小取决于捕获参数的数量和方式）。但是可调用对象必须要被&lt;code&gt;std::function&lt;/code&gt;所保存，这样才能在&lt;code&gt;std::function&lt;/code&gt;对象被移动或复制的时候正确的移动或复制。它的内部有一个缓冲区。如果可调用对象足够小，则不需要新开内存。所以当可调用对象大于某个阈值的时候，它需要使用动态内存分配来存储。同时，有几种方法可能避免。比如使用&lt;code&gt;std::ref&lt;/code&gt;包装参数，或者在使用lambda函数初始化&lt;code&gt;std::function&lt;/code&gt;对象时，对应的lambda可以使用引用捕获参数避免lambda的大小膨胀。&lt;/p&gt;
&lt;p&gt;https://wizmann.tk/cpp-type-erasure-and-std-function.html&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/5057382/what-is-the-performance-overhead-of-stdfunction&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/18453145/how-is-stdfunction-implemented&lt;/p&gt;
&lt;p&gt;https://blog.demofox.org/2015/02/25/avoiding-the-performance-hazzards-of-stdfunction/&lt;/p&gt;
&lt;h3&gt;关于std::function和多态&lt;/h3&gt;
&lt;p&gt;std::function既然叫做多态函数包装器，那么就可以正确处理多态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class father{
    public:
        father()= default;

        virtual void func(){
            cout &amp;lt;&amp;lt;&quot;father&quot; &amp;lt;&amp;lt; endl;
        }
};

class child:public father{
    public:
        child() = default;
        void func(){
            cout &amp;lt;&amp;lt; &quot;child&quot; &amp;lt;&amp;lt; endl;
        }
};
int main(){

    father* fptr = new child;
    function&amp;lt;void(void)&amp;gt; funcobj = bind(&amp;amp;father::func, fptr);
    funcobj(); //输出child
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关于源码解析&lt;/h3&gt;
&lt;p&gt;脸都看绿了。&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/zdy0_2004/article/details/50652934&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/weixin_43798887/article/details/116571325&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/560964284&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/66301236&lt;/p&gt;
&lt;h2&gt;在函数中传递&lt;code&gt;std::function&lt;/code&gt;对象，搭配&lt;code&gt;bind&lt;/code&gt;并考虑使用&lt;code&gt;placeholders&lt;/code&gt;。&lt;/h2&gt;
&lt;p&gt;为了方便起见。此处仅使用全局函数为例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void test(int a, int b, const string&amp;amp; c){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; b &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
}

void funcinput(function&amp;lt;void(int, int, const string&amp;amp;)&amp;gt; in_func){//注意函数入参签名
    in_func(1,2,&quot;abc&quot;);
}

void another_funcinput(function&amp;lt;void(int, const string&amp;amp;)&amp;gt; in_func){ //注意函数入参签名
    in_func(2,&quot;abc&quot;);
}
int main(){
    funcinput(bind(&amp;amp;test, placeholders::_1, placeholders::_2, placeholders::_3)); //输出12abc
    another_funcinput(bind(&amp;amp;test, 1, placeholders::_1, placeholders::_2));//输出ab123
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们打算将&lt;code&gt;std::function&lt;/code&gt;对象传入函数。&lt;/p&gt;
&lt;p&gt;注意在搭配&lt;code&gt;bind&lt;/code&gt;使用的时候。注意&lt;code&gt;function&lt;/code&gt;的类型。[下方例子全部忽略第一个实际参数（为函数地址）]。&amp;lt;u&amp;gt;&lt;strong&gt;&lt;code&gt;function&lt;/code&gt;的类型为函数返回类型+除预先绑定的参数以外的全部参数类型（所有&lt;code&gt;placeholders&lt;/code&gt;代表的类型）。&lt;/strong&gt;&amp;lt;/u&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;bind(&amp;amp;test, placeholders::_1, placeholders::_2, placeholders::_3)&lt;/code&gt;的时候，由于三个参数全部为&lt;code&gt;placeholders&lt;/code&gt;，所以此时&lt;code&gt;function&lt;/code&gt;的类型为目标函数的返回值类型+全部参数类型。为&lt;code&gt;function&amp;lt;void(int, int, const string&amp;amp;)&amp;gt; &lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;(bind(&amp;amp;test, 1, placeholders::_1, placeholders::_2)&lt;/code&gt;的时候，由于第一个参数为预绑定，只有两个&lt;code&gt;placeholders&lt;/code&gt;，所以此时&lt;code&gt;function&lt;/code&gt;的类型为目标函数的返回值类型+除第一个外，也就是剩余的参数类型。为&lt;code&gt;function&amp;lt;void(int, const string&amp;amp;)&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一个稍微复杂的包裹类练习。使用了模板和bind&lt;/h2&gt;
&lt;p&gt;我们这一段代码的目的是想简单模拟一下unique_ptr调用自定义删除器的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myInt{
    public:
    int val1;
    myInt() = default; //要点1
    myInt(int x):val1(x){}
    void operator()(int a){ //要点2
        cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt;endl;
        cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
    }
    
};
void global_func(int x){
    cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt;&quot;global&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T1, typename T2&amp;gt;
class anotherone{
    public:
        T1 callable; //要点3 
        T2 value;
        anotherone() = default;
        anotherone(T2 val):value(val){} //要点4
        anotherone(T1 func, T2 val): callable(func), value(val){} //要点5
        void operator()(){
            callable(value);
        }
};
int main()
{
    myInt tests; //要点6
    auto objbind = bind(&amp;amp;myInt::operator(), &amp;amp;tests, placeholders::_1); //要点7
    typedef decltype(objbind) mytype; //要点8
    anotherone&amp;lt;mytype, int&amp;gt; myobj2(objbind, 200); //要点9 T1是bind类型
    anotherone&amp;lt;myInt, int&amp;gt; myobj(234); //要点10 T1 是 myInt类型
    myobj2();
    myobj();
    
    anotherone&amp;lt;void(*)(int), int&amp;gt; myobj3(global_func, 345);
    myobj3();
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;要点1 &lt;code&gt;myInt&lt;/code&gt;必须提供默认构造。原因是我们的&lt;code&gt;anotherone&lt;/code&gt;在不接受函数对象做为可调用对象的时候，会利用第一个模板参数的类型构建一个 第一个模板参数类型的对象。也就是&lt;code&gt;myInt&lt;/code&gt;对象。如果没有默认构造则会失败&lt;/li&gt;
&lt;li&gt;要点2 &lt;code&gt;operator()&lt;/code&gt;没有固定实现。返回值可以依靠不同需求做决定&lt;/li&gt;
&lt;li&gt;要点3等同于要点1。在不接受函数对象做为可调用对象的时候，这个东西是第一个模板参数的类型的对象。然后我们会调用这个对象的&lt;code&gt;operator()&lt;/code&gt;来进行调用。如果是一个可调用对象，则利用这个可调用对象进行调用。比如&lt;code&gt;bind&lt;/code&gt;和&lt;code&gt;function&lt;/code&gt;或函数指针。&lt;/li&gt;
&lt;li&gt;要点4是为了匹配不提供调用对象只提供类型的情况&lt;/li&gt;
&lt;li&gt;要点5是为了匹配提供调用对象的情况&lt;/li&gt;
&lt;li&gt;要点6和要点7是bind成员函数的时候必须传入一个对象地址做为this指针&lt;/li&gt;
&lt;li&gt;要点8是因为我们必须要显式指明类型，所以要有一个decltype辅助我们判断类型。&lt;/li&gt;
&lt;li&gt;要点9和要点10就是实际模拟传入一个可调用对象或不传入可调用对象，只通过类型进行调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;lambda， bind和function的简单准则&lt;/h2&gt;
&lt;p&gt;在性能关键点，一定要避免使用&lt;code&gt;std::function&lt;/code&gt;或函数指针调用函数。尽可能避免使用&lt;code&gt;bind&lt;/code&gt;和lambda。如果一定要使用，优先选择lambda，其次是&lt;code&gt;bind&lt;/code&gt;，然后是函数指针，最后才是&lt;code&gt;std::function&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/49246242/efficiency-of-stdbind-vs-lambda&lt;/p&gt;
&lt;h1&gt;std::call_once 和 std::once_flag&lt;/h1&gt;
&lt;p&gt;原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class Callable, class... Args &amp;gt;
void call_once( std::once_flag&amp;amp; flag, Callable&amp;amp;&amp;amp; f, Args&amp;amp;&amp;amp;... args );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;call_in_once&lt;/code&gt;执行的可调用对象可以保证在多线程的状态下仅被（一个线程）执行一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;once_flag my_flag1; //这个flag是一次性使用的。必须要全局。而且如果有两个东西需要call_once就要两个flag
once_flag my_flag2;
void func1(){
    cout &amp;lt;&amp;lt;&quot;func1&quot; &amp;lt;&amp;lt; endl;
}

void func2(int s){
    cout &amp;lt;&amp;lt;&quot;func2&quot; &amp;lt;&amp;lt; s &amp;lt;&amp;lt; endl;
}
void thread_exe(){
    //once_flag my_flag1; //错误 放在这里就是线程函数的局部变量。这样每个线程就会有自己的flag
    //once_flag my_flag2;
    call_once(my_flag1, func1);
    call_once(my_flag2, func2, 1);
}

int main(){
    thread mythread1(thread_exe);
    thread mythread2(thread_exe);
    mythread1.join();
    mythread2.join();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;once_flag&lt;/code&gt;对象对于所有线程来说应该是全局变量（或等同于全局变量的形式）。因为&lt;code&gt;once_flag&lt;/code&gt;对象只能被使用一次。如果是局部变量，则放在这里就是线程函数的局部变量。这样每个线程就会有自己的&lt;code&gt;once_flag&lt;/code&gt;对象，就无法起作用。&lt;/li&gt;
&lt;li&gt;因为每一个&lt;code&gt;once_flag&lt;/code&gt;对象是一次性的。所以如果有两个东西需要call_once就要两个&lt;code&gt;once_flag&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;flag&lt;/code&gt;没有标记为&lt;code&gt;done&lt;/code&gt;，所有线程都会阻塞在&lt;code&gt;once_flag&lt;/code&gt;这里，此时：
&lt;ul&gt;
&lt;li&gt;如果某个&lt;code&gt;Callable&lt;/code&gt;抛出异常，则&lt;code&gt;call_once&lt;/code&gt;会选择下一个等候的线程重新执行&lt;code&gt;call_once&lt;/code&gt;动作。&lt;/li&gt;
&lt;li&gt;如果成功执行了&lt;code&gt;Callable&lt;/code&gt;，解除所有等候线程的阻塞并给&lt;code&gt;flag&lt;/code&gt;标记为&lt;code&gt;done&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考：https://blog.csdn.net/qq_31175231/article/details/77916028&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/XJF199001/article/details/51435845&lt;/p&gt;
&lt;h1&gt;异步API。std::future, std::promise, std::packaged_task, std::async&lt;/h1&gt;
&lt;p&gt;想要进行多线程编程，这四个看起来高级的API是必不可少的。我们来看一看这一套异步编程套组里的四个组件到底有什么关系，他们是干什么的。&lt;/p&gt;
&lt;p&gt;参考资料：https://segmentfault.com/a/1190000039201271&lt;/p&gt;
&lt;p&gt;https://murphypei.github.io/blog/2019/04/cpp-concurrent-4&lt;/p&gt;
&lt;h2&gt;std::future / std::shared_future&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt; future &amp;gt;&lt;/code&gt;头文件功能允许对特定提供者设置的值进行异步访问，可能在不同的线程中。
这些&lt;strong&gt;提供程序&lt;/strong&gt;(要么是&lt;code&gt;promise&lt;/code&gt; 对象，要么是&lt;code&gt;packaged_task&lt;/code&gt;对象，或者是对异步的调用&lt;code&gt;async&lt;/code&gt;)与&lt;code&gt;future&lt;/code&gt;对象共享&lt;em&gt;&lt;strong&gt;共享状态&lt;/strong&gt;&lt;/em&gt;：提供者使共享状态就绪的点与&lt;code&gt;future&lt;/code&gt;对象访问共享状态的点同步。&lt;/p&gt;
&lt;p&gt;上面这段话翻译过来就是我们可以理解为future是一个时间胶囊, 一个对象**。这个对象的提供程序（如promise)里面储存了我们希望以后拿到的东西。然后我们用future来和提供程序连接起来，一旦共享状态转变为就绪，就可以拿到提供程序里面的东西。**&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以说，future对象提供了一种让我们访问异步操作的结果的机制。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们看一下future的正式定义和具体细节。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;future 是一个对象，&lt;strong&gt;可以从某个提供对象或函数中检索值&lt;/strong&gt;，如果在不同线程中，则可以正确同步此访问。&lt;/li&gt;
&lt;li&gt;future的模板参数类型是其异步操作结果的类型。&lt;/li&gt;
&lt;li&gt;它提供了一种访问异步操作结果的机制。从字面意思上看它表示未来，这个意思就非常贴切，因为它不是立即获取结果但是可以在某个时候以同步的方式来获取结果。我们可以通过查询&lt;code&gt;future&lt;/code&gt;的状态来获取异步操作的结果。&lt;strong&gt;&lt;code&gt;future_status&lt;/code&gt;有三种状态：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;**&lt;code&gt;deferred&lt;/code&gt;：**异步操作还未开始。共享状态包含一个延迟函数，因此只有在明确请求时才会计算结果。（主要用于&lt;code&gt;async&lt;/code&gt;函数的&lt;code&gt;std::launch::deferred&lt;/code&gt;参数。) （&lt;strong&gt;共享状态持有的函数正在延迟运行，结果将仅在显式请求时计算&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;**&lt;code&gt;ready&lt;/code&gt;：**异步操作已经完成（&lt;strong&gt;共享状态就绪&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/strong&gt;：异步操作超时。在指定的超时持续时间过去之前，共享状态尚未准备好。（&lt;strong&gt;共享状态在经过指定的等待时间内仍未就绪&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“有效” future 对象，只能通过调用以下函数之一来构造：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;async&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;promise::get_future&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packaged_task::get_future&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;默认构造的 future 对象是无效的（除非移动（move）分配一个有效的 future）。&lt;/li&gt;
&lt;li&gt;在有效的 future 上调用 &lt;code&gt;future::get&lt;/code&gt; 会阻塞线程，直到提供程序准备好共享状态（通过设置值或异常）。这样，两个线程可以通过一个线程同步，等待另一个线程设置值。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;get()&lt;/code&gt; 调用会改变其共享状态，不再可用，也就是说 &lt;code&gt;get()&lt;/code&gt; 只能被调用一次，多次调用会触发异常。如果想要在多个线程中多次获取产出值需要使用 &lt;code&gt;shared_future&lt;/code&gt;。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;共享状态的生存期至少要持续到与之关联的最后一个对象释放它或销毁它为止。因此，如果与 future 相关联，共享状态可以在最初获得它的对象（如果有的话）之后继续存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/20190506204825200.png&quot; alt=&quot;20190506204825200&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;什么叫共享状态？ -- 整理自EMC++ 条款38&lt;/h2&gt;
&lt;p&gt;我们先来思考一下。我们知道了，我们可以通过promise，packaged_task拿到一个future对象。future对象是一个句柄，用于让我们访问异步操作的结果。在我们的直观想象中，可能调用方和被调用方的关系是这个样子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230224205647.png&quot; alt=&quot;QQ截图20230224205647&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这时候有了第一个问题。被调用方的结果储存在哪比较好？在调用方使用&lt;code&gt;get&lt;/code&gt;之前，可能这个异步操作已经执行完毕。同时，我们只能给&lt;code&gt;promise&lt;/code&gt;设定值，而不能从&lt;code&gt;promise&lt;/code&gt;中获取值。&lt;strong&gt;因此结果不会储存在被调用方的&lt;code&gt;promise&lt;/code&gt;对象中。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同时，这个结果也不能存在调用方的&lt;code&gt;future&lt;/code&gt;对象中&lt;/strong&gt;。因为我们可能通过&lt;code&gt;future&lt;/code&gt;对象创建其他的&lt;code&gt;shared_future&lt;/code&gt;对象。而&lt;code&gt;shared_future&lt;/code&gt;对象可能会有多个。那么这么多对应同一个结果中的&lt;code&gt;future&lt;/code&gt;对象中，哪一个应该包含结果呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以&lt;code&gt;future&lt;/code&gt;和结果应该储存在两个位置。这个位置被称之为共享状态。共享状态通常使用堆上的对象来表示，但是其型别、接口和实现标准皆未指定。标准库作者可以自由地用他们喜好的方法去实现共享状态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以说，调用方和被调用方的关系是这个样子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230224212325.png&quot; alt=&quot;QQ截图20230224212325&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;共享状态是有引用计数的。&lt;code&gt;future&lt;/code&gt;，&lt;code&gt;~packaged_task&lt;/code&gt;和&lt;code&gt;promise&lt;/code&gt;对象的析构函数对其负责。所以在一些情况下，&lt;code&gt;future&lt;/code&gt;对象的析构函数会由于共享状态的原因被阻塞。请查看&lt;code&gt;async&lt;/code&gt;部分。同时部分资料来自&lt;a href=&quot;https://stackoverflow.com/questions/43383530/who-is-responsible-for-the-shared-state-of-futures-and-promises&quot;&gt;这里&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;但是注意，只有&lt;code&gt;future&lt;/code&gt;对象的析构会阻塞，其他的不会。因为剩下两个对象的析构函数是抛弃（abandon）共享状态。而&lt;code&gt;future&lt;/code&gt;对象的析构是释放（release）共享状态。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于到底什么是共享状态，这个答案不太好。https://stackoverflow.com/questions/62241240/how-to-comprehend-stdfuture-shared-state&lt;/p&gt;
&lt;h3&gt;future::valid()&lt;/h3&gt;
&lt;h4&gt;检查有效的共享状态&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;返回 future 对象当前是否与共享状态关联。&lt;/li&gt;
&lt;li&gt;对于默认构造的 future 对象，此函数返回 false (除非将有效的 future 分配给移动对象)。&lt;/li&gt;
&lt;li&gt;future 只能由某些提供函数（如， &lt;code&gt;async&lt;/code&gt;, &lt;code&gt;promise::get_future&lt;/code&gt; 或 &lt;code&gt;packaged_task::get_future&lt;/code&gt;）使用有效的共享状态进行初始化。&lt;/li&gt;
&lt;li&gt;一旦使用 future::get 检索了共享状态的值，则调用此函数返回 false (除非移动分配了一个新的 future).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;返回值&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果对象与共享状态关联，则为 ture。&lt;/li&gt;
&lt;li&gt;否则为假。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;特别注意。返回值为true不代表此时共享状态已经就绪。只能表明对象与共享状态关联。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;future::wait&lt;/h3&gt;
&lt;h4&gt;阻塞并等待共享状态就绪（结果可用）&lt;/h4&gt;
&lt;h3&gt;future::get&lt;/h3&gt;
&lt;p&gt;可以理解为包含了&lt;code&gt;wait&lt;/code&gt;的操作。因为他调用了&lt;code&gt;wait&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;阻塞并等待共享状态就绪（结果可用），返回存储在共享状态中的值（或引发其异常）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;当共享状态就绪时，返回存储在共享状态中的值（或引发其异常）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果共享状态尚未准备好（即提供程序尚未设置其值或异常），则该函数将阻塞调用线程直到准备就绪。在此前，调用了get的函数不会返回。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;共享状态就绪后，该函数将取消阻塞并返回（或引发异常）以释放其共享状态。&lt;strong&gt;这时 future 对象不再有效&lt;/strong&gt;：&lt;strong&gt;对于每个 future 的共享状态，此成员函数最多应被调用一次&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;提供者准备好共享状态和返回此函数之间是同步的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;shared_future&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;std::shared_future&lt;/code&gt; 与 &lt;code&gt;std::future&lt;/code&gt; 类似，但是 &lt;code&gt;std::shared_future&lt;/code&gt; 可以拷贝、多个 &lt;code&gt;std::shared_future&lt;/code&gt; 可以共享某个共享状态的最终结果(即共享状态的某个值或者异常)。&lt;strong&gt;&lt;code&gt;shared_future&lt;/code&gt; 可以通过某个 &lt;code&gt;std::future&lt;/code&gt; 对象隐式转换（参见 &lt;code&gt;std::shared_future&lt;/code&gt; 的构造函数），或者通过 &lt;code&gt;std::future::share()&lt;/code&gt; 显示转换，无论哪种转换，被转换的那个 std::future 对象都会变为 not-valid&lt;/strong&gt;。&lt;code&gt;std::shared_future&lt;/code&gt; 的成员函数和 &lt;code&gt;std::future&lt;/code&gt; 大部分相同，这个地方就不一一展开了，需要的请查阅官方文档。&lt;/p&gt;
&lt;p&gt;我们可以看到，想要创建一个有效的future对象必须依靠剩下的三个API。我们就来进一步看看剩下的几块内容。&lt;/p&gt;
&lt;h2&gt;std::promise&lt;/h2&gt;
&lt;p&gt;promise是剩下三个当中最为“原始，底层“的API。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;promise&lt;/code&gt; 本质是一个类似我们打印输出中占位符的东西，你可以理解它就是一个等待数据装填的坑，它是一个“承诺”，承诺未来会有相应的数据（模板实现）&lt;/strong&gt;。因为这是一个“承诺”，所以创建的时候是没有东西的，所以我们需要知道这个异步操作什么时候能有东西，好实现“承诺”，所以 &lt;code&gt;promise&lt;/code&gt; 可以通过调用 &lt;code&gt;get_future()&lt;/code&gt; 返回一个 &lt;code&gt;future&lt;/code&gt; 对象，让你去了解这个承诺是否完成了。因此，&lt;code&gt;promise&lt;/code&gt; 是存放异步操作产出值的坑，而 &lt;code&gt;future&lt;/code&gt; 是从其中获取异步操作结果，二者都是模板类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里理解为我们的future对象是一个接口，建立起与promise对象的联系。我们使用promise对象来进行异步操作，所以能看到我们并不需要把future对象传入线程。但是我们会通过future对象去获知这个异步操作的结果是否就绪，也就是promise对象是否已经包含了我们期望的结果。然后我们可以通过future来获取结果。通过搭配future和promise，我们可以安全的进行线程间通信，不需要显式同步&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;promise和future就是一个异步操作的两个端点。我们把结果储存在promise，然后通过future提取。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;promise&lt;/code&gt; 是一个对象，由 &lt;code&gt;future&lt;/code&gt; 对象（可能在另一个线程中）检索，并提供一个同步点。&lt;/li&gt;
&lt;li&gt;通过调用成员&lt;code&gt;get_future&lt;/code&gt;，可以将该共享状态与 &lt;code&gt;future &lt;/code&gt;对象关联。 调用之后，两个对象共享相同的共享状态：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;promise&lt;/code&gt;对象是异步提供程序，应在某个时候为共享状态设置一个值。&lt;code&gt;set_value&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;future&lt;/code&gt; 对象是一个异步返回对象，可以检索共享状态的值，并在必要时等待其准备就绪。&lt;/li&gt;
&lt;li&gt;理解为我们通过&lt;code&gt;promise&lt;/code&gt;的&lt;code&gt;get_future&lt;/code&gt;来创建我们的&lt;code&gt;future&lt;/code&gt;对象。之后通过&lt;code&gt;future&lt;/code&gt;对象（来检索）获得一个共享状态（结果）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意 每个&lt;code&gt;std::promise&lt;/code&gt; 对象只应当使用一次。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/20190506203234388.png&quot; alt=&quot;20190506203234388&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;promise::set_value&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;以原子方式&lt;/strong&gt;将值存储到共享状态（就是把promise对象储存的值设置好），&lt;strong&gt;并且改变该状态变为就绪状态&lt;/strong&gt;。（设置共享状态）&lt;/li&gt;
&lt;li&gt;如果与同一共享状态关联的future对象当前正在等待对&lt;code&gt;future::get&lt;/code&gt;的调用，则它将取消阻塞并返回val。
&lt;ul&gt;
&lt;li&gt;因为future的get/wait会阻塞等待共享状态被设置完毕。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看看简单代码例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//使用promise进行异步
void accumulate2(std::vector&amp;lt;int&amp;gt;::iterator first, //注意不太需要返回值了。因为异步返回的值被储存在了promise对象中
                std::vector&amp;lt;int&amp;gt;::iterator last,
                std::promise&amp;lt;int&amp;gt; accumulate_promise) //函数头要有promise对象定义。
{
    int sum = std::accumulate(first, last, 0);
    accumulate_promise.set_value(sum);  // 将结果存入，并让共享状态变为就绪以提醒future
    //如果不set_value,那么调用了future::get/wait的线程将一直被阻塞。
}
 
int main()
{
    // 演示用 promise&amp;lt;int&amp;gt; 在线程间传递结果。
    std::vector&amp;lt;int&amp;gt; numbers = { 1, 2, 3, 4, 5, 6 };
    std::promise&amp;lt;int&amp;gt; accumulate_promise; //声明promise对象
    std::future&amp;lt;int&amp;gt; accumulate_future(accumulate_promise.get_future()); //通过promise对象的get_future来初始化(获取）future对象
    
    std::thread work_thread(accumulate2, numbers.begin(), numbers.end(),
                            std::move(accumulate_promise)); //启动线程。设置好执行函数和传入参数。注意必须要把promise对象一并传入（移入）
    //!这里也可以用ref。但是我们accumulate2的函数头要了个值。promise又禁用了拷贝构造所以这里目前只可以move。如果accumulate2函数头改为&amp;amp;就可以用ref
    // !可以用ref但是不推荐。因为如果你不转移所有权，万一你多个线程并发访问了promise，可能会有问题，
    !而且这也不符合设计，一般来说只应该让一个线程持有promise
    //accumulate_future.wait();  //等待结果 这个可以去掉，直接用get就可以
    
    std::cout &amp;lt;&amp;lt; &quot;result=&quot; &amp;lt;&amp;lt; accumulate_future.get() &amp;lt;&amp;lt; &apos;\n&apos;; //get阻塞住等待共享对象变为ready。然后获取结果。
    work_thread.join();  //阻塞等待线程执行完成
 
    getchar();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;std::packaged_task&lt;/h2&gt;
&lt;p&gt;packaged_task比promise高级一点。直观来看就是参数少一点，操作少一点。稍后我们会有一个简单的对比。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;packaged_task&lt;/code&gt; 是对一个任务的抽象，我们可以给其传递一个函数来完成其构造。相较于 &lt;code&gt;promise&lt;/code&gt;，它应该算是更高层次的一个抽象，同样地，我们可以将任务投递给任何线程去完成，然后通过 &lt;code&gt;packaged_task::get_future()&lt;/code&gt; 方法获取的 &lt;code&gt;future&lt;/code&gt; 对象来获取任务完成后的产出值。总结来说，&lt;strong&gt;&lt;code&gt;packaged_task&lt;/code&gt; 是连数据操作（比如set_value)都封装进去了的 &lt;code&gt;promise&lt;/code&gt;&lt;/strong&gt;。&lt;code&gt;packaged_task&lt;/code&gt; 也是一个类模板，模板参数为函数签名，也就是传递函数的类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt; packaged_task;     // undefined
template &amp;lt;class Ret, class... Args&amp;gt; class packaged_task&amp;lt;Ret(Args...)&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;std::packaged_task包装可调用对象，并允许异步检索其结果。&lt;/strong&gt;（可调用对象是重点）&lt;/li&gt;
&lt;li&gt;类似于 std::function, 但是会自动将其结果传输到 future 对象。&lt;/li&gt;
&lt;li&gt;对象内部包含两个元素：
&lt;ul&gt;
&lt;li&gt;存储的任务是一些可调用对象（例如，函数指针，成员或函数对象的指针），其调用签名应采用 Args... 中类型的参数，并返回 Ret 类型的值。&lt;/li&gt;
&lt;li&gt;共享状态，该状态能够存储调用存储的任务（类型为 Ret）的结果，并且可以通过 future 来异步访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过调用成员 get_future 将共享状态与 future 对象关联。调用之后，两个对象共享相同的共享状态：
&lt;ul&gt;
&lt;li&gt;packaged_task 对象是异步提供程序，通过调用存储的任务，可以在某个时刻将共享状态设置为就绪。&lt;/li&gt;
&lt;li&gt;future 对象是一个异步返回对象，可以检索共享状态的值，并在必要时等待其准备就绪。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/2019050621124831.png&quot; alt=&quot;2019050621124831&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看看代码实例，做一下比较：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//? 使用packaged_task进行异步
int accumulate(std::vector&amp;lt;int&amp;gt;::iterator first,
                std::vector&amp;lt;int&amp;gt;::iterator last) //注意函数头，我们没有像promise那样需要传入promise对象。
{
    int sum = std::accumulate(first, last, 0);
    return sum;  //所以也不需要promise的set_value
}
 
int main()
{
    // 演示用 packaged_task 在线程间传递结果。
    std::vector&amp;lt;int&amp;gt; numbers = { 1, 2, 3, 4, 5, 6 };
    std::packaged_task&amp;lt;int(std::vector&amp;lt;int&amp;gt;::iterator,std::vector&amp;lt;int&amp;gt;::iterator)&amp;gt; accumulate_task(accumulate);
    //创建packaged_task对象。注意这里有区别，下面详说。
    std::future&amp;lt;int&amp;gt; accumulate_future = accumulate_task.get_future(); //通过packaged_task对象的get_future来创建future对象
    std::thread work_thread(std::move(accumulate_task), numbers.begin(), numbers.end()); //启动线程。注意这里有区别了。
    /*
    @ 使用move的主要原因还有一个就是按值传递函数参数会调用拷贝构造。packaged_task禁用了拷贝构造，要么传引用要么换成右值引用转移所有权。
    */
    //accumulate_future.wait();  //等待结果 可以和下面的get合并
    std::cout &amp;lt;&amp;lt; &quot;result=&quot; &amp;lt;&amp;lt; accumulate_future.get() &amp;lt;&amp;lt; &apos;\n&apos;;
    work_thread.join();  //阻塞等待线程执行完成
 
    getchar();
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们来说一说几点具体区别&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们提到了，&lt;code&gt;packaged_task&lt;/code&gt;包装的是可调用对象。但是&lt;code&gt;promise&lt;/code&gt;仅仅是一个包装着异步执行结果的对象。所以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;packaged_task&lt;/code&gt;对象的模板类型是函数签名，因为他包装了可调用对象。&lt;/li&gt;
&lt;li&gt;所以线程执行的时候只需要移入&lt;code&gt;packaged_task&lt;/code&gt;对象即可，无需传入函数本身。因为已经被包装了。&lt;/li&gt;
&lt;li&gt;所以包装的函数无需额外参数，函数内无需额外动作。&lt;/li&gt;
&lt;li&gt;因为&lt;code&gt;promise&lt;/code&gt;会储存异步任务的结果，所以函数的返回值可以去掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其实理解了上面那一点，就理解了&lt;code&gt;packaged_task&lt;/code&gt;和&lt;code&gt;promise&lt;/code&gt;的区别。可以简单理解为&lt;code&gt;promise&lt;/code&gt;是一个包装了执行结果的对象。而&lt;code&gt;packaged_task&lt;/code&gt;是一个包装了整个任务的对象，它不仅包装任务执行结果，而且包装任务本身。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意它的模板头参数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class _Ret, class... _ArgTypes&amp;gt;
class packaged_task&amp;lt;_Ret(_ArgTypes...)&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的目的是让它的类型看起来更像函数。&lt;/p&gt;
&lt;h2&gt;std::async&lt;/h2&gt;
&lt;p&gt;async是最高级的一个API，代码简单，比较高层。它其实是封装了&lt;code&gt;thread &lt;/code&gt;和 &lt;code&gt;packged_task&lt;/code&gt;的功能，使异步执行一个任务更为方便&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;async是函数模板，不是类模板。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unspecified policy (1)    
template &amp;lt;class Fn, class... Args&amp;gt;
  future&amp;lt;typename result_of&amp;lt;Fn(Args...)&amp;gt;::type&amp;gt;
    async (Fn&amp;amp;&amp;amp; fn, Args&amp;amp;&amp;amp;... args);

specific policy (2)    
template &amp;lt;class Fn, class... Args&amp;gt;
  future&amp;lt;typename result_of&amp;lt;Fn(Args...)&amp;gt;::type&amp;gt;
    async (launch policy, Fn&amp;amp;&amp;amp; fn, Args&amp;amp;&amp;amp;... args);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;异步调用函数在某个时刻调用 &lt;code&gt;fn&lt;/code&gt; (以 &lt;code&gt;args&lt;/code&gt; 作为参数)，返回时无需等待 &lt;code&gt;fn&lt;/code&gt; 执行完成。(都说了是异步)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以通过返回的 future 对象（通过调用其成员 &lt;code&gt;future::get&lt;/code&gt;）来访问 &lt;code&gt;fn&lt;/code&gt; 返回的值。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;第二个版本 （2） 允许调用者选择特定的启动策略，而第一个版本 （1） 使用自动选择，就好像调用 (2) 并将 &lt;code&gt;launch::aysnc | launch::deferred&lt;/code&gt; 作为策略。&lt;/li&gt;
&lt;li&gt;该函数在共享状态下临时存储使用的线程处理程序。一旦完成 fn 的执行，共享状态将包含 fn 返回的值并准备就绪。&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;launch::async&lt;/th&gt;
&lt;th&gt;启动一个新的线程以调用 fn （就像使用 fn 和 args 作为参数构造线程对象，并访问返回的 future 的共享状态将其联接）&lt;code&gt;std::thread(std::forward&amp;lt;F&amp;gt;(f), std::forward&amp;lt;Args&amp;gt;(args)...)&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;launch::deferred&lt;/td&gt;
&lt;td&gt;意味着函数可能只会在std::async返回的future对象&lt;strong&gt;调用get或wait时执行&lt;/strong&gt;。（函数在future对象调用了get或wait的时候才会开始执行）。**不会产出新的线程。该任务会在调用线程中执行。**当调用get或wait时，函数会同步执行，即调用者会阻塞直到函数运行结束。&lt;strong&gt;如果get或wait没有被调用，函数就绝对不会执行&lt;/strong&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launch::async | launch::deferred&lt;/td&gt;
&lt;td&gt;该功能自动（在某个时候）选择策略。这取决于系统和库的实现，他们通常会针对系统中当前的并发可用性进行优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;看一下细节&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//? 使用async进行异步

int accumulate3(std::vector&amp;lt;int&amp;gt;::iterator first, std::vector&amp;lt;int&amp;gt;::iterator last){
    std::cout &amp;lt;&amp;lt;&quot;async begin&quot; &amp;lt;&amp;lt; std::endl;
    int sum = std::accumulate(first, last, 0);
    std::cout &amp;lt;&amp;lt;&quot;async end&quot; &amp;lt;&amp;lt; std::endl;
    return sum;
}

int main(){
    std::cout &amp;lt;&amp;lt;&quot;main begin&quot; &amp;lt;&amp;lt; std::endl;
    std::vector&amp;lt;int&amp;gt; numbers = { 1, 2, 3, 4, 5, 6 };
    std::future&amp;lt;int&amp;gt; accumulate_future = std::async(std::launch::async, accumulate3, numbers.begin(), numbers.end()); //ver1
    std::future&amp;lt;int&amp;gt; accumulate_future = std::async(std::launch::deferred, accumulate3, numbers.begin(), numbers.end()); //ver2
    Sleep(50); //睡眠五秒
    std::cout &amp;lt;&amp;lt; &quot;result=&quot; &amp;lt;&amp;lt; accumulate_future.get() &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt;&quot;main end&quot; &amp;lt;&amp;lt; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ver1 输出：
main begin
async begin
async end
result=21
main end

ver2 输出：
main begin
result=async begin
async end
21
main end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用方式和thread很像，没什么区别。没有花里胡哨的东西。但是注意下细节。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在async模式中，一旦开始了调用，则会立刻创建子线程开始任务执行。所以我们看到尽管主线程睡眠了一下，但是并不影响异步线程的执行。所以async begin， async end和result=21是可以打印的。因为get的时候任务已经完成（或者是阻塞至任务完成）。&lt;/li&gt;
&lt;li&gt;在deferred模式中，只有get/wait调用的时候任务才开始执行。所以result=先打印出来，然后直到调用get函数的时候，才开始执行任务，任务执行完毕后结果才能拿到，所以会先打印result=, 然后在调用线程（此处是主线程）中执行函数，拿到结果后打印21。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传参的时候如果需要使用只支持移动的参数比如unique_ptr，则必须使用move显式移动。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;unique_ptr&amp;lt;int&amp;gt; testfunc(unique_ptr&amp;lt;int&amp;gt; ptr){
    *ptr = *ptr + 1;
    return ptr;
}

int main(){
    unique_ptr&amp;lt;int&amp;gt; myptr = make_unique&amp;lt;int&amp;gt; (5);
    future&amp;lt;unique_ptr&amp;lt;int&amp;gt;&amp;gt; my_future = async(std::launch::async, testfunc, move(myptr)); //显示使用move转移所有权。
    cout &amp;lt;&amp;lt; *(my_future.get()) &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果async函数的返回值没有一个东西接住他（显式获取返回值），因为async会创建临时的future对象, 所以这个临时对象的析构函数会阻塞住调用线程直至异步线程执行完毕。因为我们不能让异步线程返回值的时候，这个临时对象已经被销毁了。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unique_ptr&amp;lt;int&amp;gt;  testfunc(unique_ptr&amp;lt;int&amp;gt; ptr){
    *ptr = *ptr + 1;
	sleep(5); //睡眠五秒
    return ptr;
}

int main(){
    unique_ptr&amp;lt;int&amp;gt; myptr = make_unique&amp;lt;int&amp;gt; (5);
	cout &amp;lt;&amp;lt; &quot;running&quot; &amp;lt;&amp;lt; endl;
    async(std::launch::async, testfunc, move(myptr)); //卡在这里5秒。因为没有获取返回值。直到任务完成后赋值给临时future对象之后才能执行future临时对象的析构。
    cout &amp;lt;&amp;lt;&quot;blocked&quot; &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码执行的时候，调用线程会由于异步线程没有执行完毕，而且因为没有获取返回值，所以卡在async这行。直到异步线程执行完毕后赋值给临时future对象之后才能执行future临时对象的析构。才会继续输出blocked。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果async函数的返回值有&lt;code&gt;future&lt;/code&gt;对象接住他（显式获取返回值），而且没有对&lt;code&gt;future&lt;/code&gt;对象用使用&lt;code&gt;get&lt;/code&gt;来阻塞主线程，因为满足下面的条件，所以主线程依旧会等待异步线程结束。因为该&lt;code&gt;future&lt;/code&gt;对象的析构函数被阻塞了&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;注意：此条件仅限于如下条件为真：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;future&lt;/code&gt;对象以 &lt;code&gt;std::async&lt;/code&gt; 的调用创建，且&lt;strong&gt;共享状态仍未就绪&lt;/strong&gt;，且&lt;code&gt; this&lt;/code&gt; 是到共享状态的最后引用。
&lt;ul&gt;
&lt;li&gt;最后面那个条件原文是：this was the last reference to the shared state. 我的理解其实是：这个&lt;code&gt;future&lt;/code&gt;对象是&lt;code&gt;async&lt;/code&gt;调用创建的那个对象的最后的引用。
&lt;ul&gt;
&lt;li&gt;也就是如果当前的&lt;code&gt;future&lt;/code&gt;对象此时是&lt;code&gt;async&lt;/code&gt;调用创建的那个&lt;code&gt;future&lt;/code&gt;对象，唯一与其有关联的实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int accumulate3(std::vector&amp;lt;int&amp;gt;::iterator first, std::vector&amp;lt;int&amp;gt;::iterator last){
    std::cout &amp;lt;&amp;lt;&quot;async begin&quot; &amp;lt;&amp;lt; std::endl;
    int sum = std::accumulate(first, last, 0);
    sleep(5); //异步线程睡眠5秒
    std::cout &amp;lt;&amp;lt;&quot;async end&quot; &amp;lt;&amp;lt; std::endl;
    return sum;
}


int exe(){
    std::cout &amp;lt;&amp;lt;&quot;exe begin&quot; &amp;lt;&amp;lt; std::endl;
    std::vector&amp;lt;int&amp;gt; numbers = { 1, 2, 3, 4, 5, 6 };
    std::future&amp;lt;int&amp;gt; accumulate_future = std::async(std::launch::async, accumulate3, numbers.begin(), numbers.end()); //通过async调用创建的future对象，且共享状态仍未就绪，且除了这个future对象以外，没有其他的对共享状态的引用。所以会阻塞。
    //std::cout &amp;lt;&amp;lt; &quot;result=&quot; &amp;lt;&amp;lt; accumulate_future.get() &amp;lt;&amp;lt; &apos;\n&apos;; 没有对`future`对象用使用`get`来阻塞主线程
    std::cout &amp;lt;&amp;lt;&quot;exe end&quot; &amp;lt;&amp;lt; std::endl;
    return 200;
}

int main(){
    int a = exe();
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    return 0;
}
/*
输出：
exe begin
exe end
async begin
(睡眠五秒...)
async end
200
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到，启用了异步线程，并且有&lt;code&gt;future&lt;/code&gt;对象做为返回值，所以没有阻碍临时对象的析构。同时我们没有对&lt;code&gt;future&lt;/code&gt;对象用使用&lt;code&gt;get&lt;/code&gt;来阻塞主线程。但是&lt;strong&gt;虽然exe end被打印，但是因为满足上面的条件，所以该&lt;code&gt;future&lt;/code&gt;对象的析构函数被阻塞了，这个调用线程依旧会被阻塞。所以这个函数的执行没有结束，所以&lt;code&gt;main&lt;/code&gt;函数内的函数调用不会返回。所以调用线程依旧会等待异步线程结束而结束。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这种阻塞行为可以理解为该future临时对象的析构函数是对底层异步执行任务的线程实施了一次隐式的&lt;code&gt;join&lt;/code&gt;&lt;/strong&gt; --emc++ 条款38
&lt;ul&gt;
&lt;li&gt;在future的析构函数不被阻塞时（不满足上述条件的时候），可以理解为对底层线程实施了一次隐式的&lt;code&gt;detach&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;future, packaged_tast, promise三者的关系&lt;/h2&gt;
&lt;p&gt;std::future用于访问异步操作的结果，而std::promise和std::packaged_task包住了future，它们内部都有一个future，promise包装的是一个值，packaged_task包装的是一个函数（异步操作本身），当需要获取线程中的某个值，可以使用std::promise，当需要获取线程函数返回值，可以使用std::packaged_task。&lt;/p&gt;
&lt;p&gt;这三个东西和async都是互相搭配使用的。具体在哪个线程阻塞，哪个线程获取，哪个先哪个后，包括是传引用还是move都是依照情况而定的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;packaged_task ≈ promise + function&lt;/li&gt;
&lt;li&gt;async ≈ thread + packaged_task&lt;/li&gt;
&lt;li&gt;通过promise的get_future()可拿到future&lt;/li&gt;
&lt;li&gt;通过future的share()可拿到shared_future&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/v2-4ef189e8c5a3473494ab9855002b312d_r.jpg&quot; alt=&quot;v2-4ef189e8c5a3473494ab9855002b312d_r&quot; /&gt;&lt;/p&gt;
&lt;p&gt;https://www.zhihu.com/question/547132461/answer/2657296340&lt;/p&gt;
&lt;h2&gt;杂项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;std::future提供访问异步操作结果的机制。std::future 只能与指定事件相关联，而 std::shared_future 就能关联多个事件。future对象本身并不提供同步访问（需要使用get/wait)。future的get()函数的设计包含移动语义，即只能调用一次，第二次调用时会报异常。shared_future的get()函数的设计包含复制语义，可以多次调用。std::shared_future对象可以通过std::future对象隐式转换，也可以通过显示调用std::future::share显示转换，在这两种情况下，原std::future对象都将变得无效。&lt;/li&gt;
&lt;li&gt;当不着急让任务结果时，可以使用 std::async 启动一个异步任务。std::async 会返回一个 std::future 对象。get()等价与先调用wait()再调用get()。 std::launch::defered 表明函数调用延迟到wait()或get()函数调用时才执行，std::launch::async 表明函数必须在其所在的独立线程上执行。&lt;/li&gt;
&lt;li&gt;std::packaged_task&amp;lt;&amp;gt; 会将future与函数或可调用对象进行绑定。当 std::packaged_task 作为函数调用时，实参将由函数调用操作符传递至底层函数，并且返回值作为异步结果存储在 std::future 中。&lt;/li&gt;
&lt;li&gt;std::promise/std::future 对提供一种机制：future可以阻塞等待线程，提供数据的线程可以使用promise对相关值进行设置，并将future的状态置为“就绪”。&lt;/li&gt;
&lt;li&gt;任何情况下，当future的状态还不是“就绪”时，调用 std::promise 或 std::packaged_task 的析构函数，将会存储一个与 std::future_errc::broken_promise 错误状态相关的 std::future_error 异常。
&lt;ul&gt;
&lt;li&gt;当调用抛出一个异常时，这个异常就会存储到future中，之后调用get()会抛出已存储的异常。&lt;/li&gt;
&lt;li&gt;std::current_exception() 来检索抛出的异常，可用 std::copy_exception() 作为替代方案， std::copy_exception() 会直接存储新的异常而不抛出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;因为 std::future 是只移动的，所以其所有权可以在不同的实例中互相传递，但只有一个实例可以获得特定的同步结果，而 std::shared_future 实例是可拷贝的，所以多个对象可以引用同一关联期望值的结果。&lt;/li&gt;
&lt;li&gt;当多线程 在没有额外同步的情况下，访问一个独立的 std::future 对象时，就会有数据竞争和未定义的 行为。这是因为： std::future 模型独享同步结果的所有权，并且通过调用get()函数，一次性 的获取数据，这就让并发访问变的毫无意义——只有一个线程可以获取结果值，因为在第一 次调用get()后，就没有值可以再获取了。&lt;/li&gt;
&lt;li&gt;在每一个 std::shared_future 的独立对象上成员函数调用返回的结果还是不同步的，所以为 了在多个线程访问一个独立对象时，避免数据竞争，可以使用两种方式：
&lt;ul&gt;
&lt;li&gt;锁&lt;/li&gt;
&lt;li&gt;每一个线程有自己的shared_future对象，然后都通过自己的这个shared_future对象来获取结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;async future搭配数组进行多线程accumulate的小例子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;struct custom_acc{
    int operator()(int a, future&amp;lt;int&amp;gt;&amp;amp; b){ //使用自定义累加。注意第一个参数是int。因为是把第二个参数作用域第一个参数上
        //其次，这里future不要忘记指定模板参数。
        //第三，这里future对象不可设置为const。因为get参数会更改future的内部状态。所以get不是const函数。const对象不能调用非const函数
        return a + b.get();//使用get取出计算结果。
    }
};

int acc_calc(std::vector&amp;lt;int&amp;gt;::iterator begin_iter, std::vector&amp;lt;int&amp;gt;::iterator end_iter){
    return std::accumulate(begin_iter, end_iter,0); //这里老规矩。和async解析一样。任务函数。
}

int main(){
    vector&amp;lt;int&amp;gt;my_vec(2000, 3); //要计算的数组
    vector&amp;lt;future&amp;lt;int&amp;gt;&amp;gt; my_futures; //装有future对象的数组
    int curpos = 0;
    for(int i = 0; i &amp;lt; 10; i++){
        my_futures.emplace_back(std::async(std::launch::async, acc_calc, my_vec.begin()+curpos, my_vec.begin()+curpos+200));
        //这里我们在future对象数组内原地构造一个async对象。async对象传入启动方式，任务函数，和函数参数。
        curpos+=200;
    }
    int ret = accumulate(my_futures.begin(), my_futures.end(), 0, custom_acc()); //这里我们逐个取出future对象数组并使用我们自定义的方法进行累加。
    cout &amp;lt;&amp;lt; ret &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;atomic&lt;/h1&gt;
&lt;p&gt;https://youtu.be/ZQFzMfHIxng&lt;/p&gt;
&lt;h2&gt;无锁编程不一定会让程序更快，甚至会有副作用。它并不具有加速行为。&lt;/h2&gt;
&lt;p&gt;无锁编程不保证性能，不保证程序更快。&lt;/p&gt;
&lt;p&gt;证明一个无锁代码的正确性非常难，如果必须使用无锁，最好使用库而不要自己搓轮子。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;基本上如果架构没有太大问题，整个系统的瓶颈不会出现在某个锁上，换句话说，出现因为锁的性能瓶颈时大都是因为架构的问题，很多时候根本没必要上lock-free，一般的同步手段完全可以满足我们的需求。但是因为这样我们就可以不了解这个问题吗？我想作为一个IT行业未来的从业者不应该这样想，我们应该很清楚我们的代码经过了哪些变化，指令重排有时会对代码造成影响。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;打算使用无锁编程提升性能之前，先检查其他部分，比如算法。&lt;/h2&gt;
&lt;h2&gt;std::atomic_flag&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::atomic_flag&lt;/code&gt; 是原子布尔类型。不同于所有 std::atomic 的特化，&lt;strong&gt;它保证是免锁的&lt;/strong&gt;。不同于 &lt;code&gt;std::atomic&amp;lt;bool&amp;gt;&lt;/code&gt; ， &lt;code&gt;std::atomic_flag&lt;/code&gt; 不提供加载或存储操作。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::atomic_flag&lt;/code&gt;是最简单的原子类型，这个类型的对象可以在两个状态间切换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;清除&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;默认构造函数&lt;/h3&gt;
&lt;p&gt;构造一个新&lt;code&gt;std::atomic_flag&lt;/code&gt;对象，不过未指明状态。这里未指定默认构造出来的&lt;code&gt;std::atomic_flag&lt;/code&gt;实例是&lt;code&gt;clear&lt;/code&gt;状态，还是&lt;code&gt;set&lt;/code&gt;状态（c++20后默认构造函数初始化的状态为clear）。因为对象存储过程是静态的，所以初始化必须是静态的。&lt;strong&gt;&lt;code&gt;std::atomic_flag&lt;/code&gt; 必须使用&lt;code&gt;ATOMIC_FLAG_INIT&lt;/code&gt;进行初始化&lt;/strong&gt;，这样构造出来的实例状态为&lt;code&gt;clear&lt;/code&gt;。&lt;strong&gt;另外，&lt;code&gt;atomic_flag&lt;/code&gt;不能被拷贝，也不能 &lt;code&gt;move&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;std::atomic_flag::test_and_set&lt;/h3&gt;
&lt;p&gt;原子地更改 &lt;code&gt;std::atomic_flag&lt;/code&gt;的状态为设置（ true ）并返回它先前保有的值。&lt;/p&gt;
&lt;h3&gt;std::atomic_flag::clear&lt;/h3&gt;
&lt;p&gt;原子地更改 &lt;code&gt;std::atomic_flag&lt;/code&gt;的状态为清除（ false ）。&lt;/p&gt;
&lt;h3&gt;std::atomic_flag::wait/notify_one/notify_all&lt;/h3&gt;
&lt;p&gt;C++20内容&lt;/p&gt;
&lt;h2&gt;std::atomic&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::atomic&lt;/code&gt;模板的每个实例化和专门化都定义了一个原子类型。如果一个线程在另一个线程读取它时写入一个原子对象，那么行为就会被明确定义(参见关于数据竞争的详细信息的内存模型)。此外，对原子对象的访问可以建立线程间的同步，并按照&lt;code&gt;std::memoryorder&lt;/code&gt;指定非原子性的内存访问。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;std::atomic&lt;/code&gt;可以用任何简单的可复制的&lt;code&gt;t&lt;/code&gt;实例化。同时&lt;code&gt;std::atomic&lt;/code&gt;是&lt;strong&gt;不可复制的，不可移动的。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意初始化方式只能使用直接初始化。因为拷贝赋值和拷贝构造被禁用且不提供移动构造和移动赋值。&lt;strong&gt;这个初始化方式非原子&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;c++17后可以使用拷贝初始化。强制使用了复制省略技术。前提是右侧必须是prvalue临时对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;atomic&lt;/code&gt;不可以使用聚合初始化（aggregate initialization）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;int&amp;gt; a = 5; //c++14错误, c++17 OK
atomic&amp;lt;int&amp;gt; a(5);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体特化类型参考https://zh.cppreference.com/w/cpp/atomic/atomic&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特化成员函数操作，操作符重载加减之类的不讲了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意原子操作没有乘法和除法&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意带有赋值操作符的时候不能保证整条语句的原子性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; atomic&amp;lt;int&amp;gt; a(5);
 a = a + 5; //只保证读取a和a+5并且赋值分别是原子的。也就是原子读 + 原子写。不能保证原子读+写。
//也就是此时在原子操作：把A读出来 和原子操作：把A+5然后写回A 之间，任何指令都可以插入。
//可能存在把A读出来，然后其他操作把A修改为100000，然后A被修改为A+5然后写回。这样的情况。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;c++20前，浮点类型不支持自增操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主 &lt;code&gt;std::atomic&lt;/code&gt; 模板可用任何满足&lt;a href=&quot;https://zh.cppreference.com/w/cpp/named_req/CopyConstructible&quot;&gt;&lt;em&gt;可复制构造&lt;/em&gt; &lt;em&gt;(CopyConstructible)&lt;/em&gt; &lt;/a&gt;及&lt;a href=&quot;https://zh.cppreference.com/w/cpp/named_req/CopyAssignable&quot;&gt;&lt;em&gt;可复制赋值&lt;/em&gt; &lt;em&gt;(CopyAssignable)&lt;/em&gt; &lt;/a&gt;的&lt;a href=&quot;https://zh.cppreference.com/w/cpp/named_req/TriviallyCopyable&quot;&gt;&lt;em&gt;可平凡复制&lt;/em&gt; &lt;em&gt;(TriviallyCopyable)&lt;/em&gt; &lt;/a&gt;类型 &lt;code&gt;T&lt;/code&gt; 特化。若下列任何值为 &lt;code&gt;false&lt;/code&gt; 则程序为非良构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://zh.cppreference.com/w/cpp/types/is_trivially_copyable&quot;&gt;std::is_trivially_copyable&lt;/a&gt;&amp;lt;T&amp;gt;::value&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://zh.cppreference.com/w/cpp/types/is_copy_constructible&quot;&gt;std::is_copy_constructible&lt;/a&gt;&amp;lt;T&amp;gt;::value&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://zh.cppreference.com/w/cpp/types/is_move_constructible&quot;&gt;std::is_move_constructible&lt;/a&gt;&amp;lt;T&amp;gt;::value&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://zh.cppreference.com/w/cpp/types/is_copy_assignable&quot;&gt;std::is_copy_assignable&lt;/a&gt;&amp;lt;T&amp;gt;::value&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://zh.cppreference.com/w/cpp/types/is_move_assignable&quot;&gt;std::is_move_assignable&lt;/a&gt;&amp;lt;T&amp;gt;::value&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;针对原子对象的操作并不一定全是“原子”的。也就是不一定保证无锁。需要看内存是否对齐了。可以使用&lt;code&gt;is_lock_free()&lt;/code&gt;进行判断针对该原子对象的操作是否是无锁的。详细查看&lt;a href=&quot;https://youtu.be/ZQFzMfHIxng?t=1724&quot;&gt;这个视频&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;std::atomic&amp;lt;T&amp;gt;::store&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void store( T desired, std::memory_order order = std::memory_order_seq_cst ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原子地&lt;/strong&gt;以 &lt;code&gt;val&lt;/code&gt; 替换当前值。按照 &lt;code&gt;order&lt;/code&gt; 的值影响内存。&lt;code&gt;order&lt;/code&gt; 必须是 &lt;code&gt;std::memory_order_relaxed&lt;/code&gt; 、 &lt;code&gt;std::memory_order_release&lt;/code&gt; 或 &lt;code&gt;std::memory_order_seq_cs&lt;/code&gt;t 之一。否则行为未定义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;T&amp;gt; x;
x.store(y); //次操作在含以上等同于x = y;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;std::atomic&amp;lt;T&amp;gt;::load&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;T load( std::memory_order order = std::memory_order_seq_cst ) const noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原子地加载并返回原子变量的当前值。按照 &lt;code&gt;order&lt;/code&gt; 的值影响内存。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;order&lt;/code&gt; 必须是 &lt;code&gt;std::memory_order_relaxed&lt;/code&gt;、 &lt;code&gt;std::memory_order_consume&lt;/code&gt; 、 &lt;code&gt;std::memory_order_acquire&lt;/code&gt; 或 &lt;code&gt;std::memory_order_seq_cst&lt;/code&gt; 之一。否则行为未定义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;T&amp;gt; x;
T y = x.load(); //此操作在含义上等同于 T y = x;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;std::atomic&amp;lt;T&amp;gt;::operator=&lt;/h3&gt;
&lt;p&gt;等于&lt;code&gt;store() &lt;/code&gt;注意用法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;int&amp;gt; a;
a = 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这个等号不是拷贝赋值的那个等号。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;std::atomic&amp;lt;T&amp;gt;::operator T&lt;/h3&gt;
&lt;p&gt;等于&lt;code&gt;load()&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;std::atomic&amp;lt;T&amp;gt;::exchange&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;T exchange( T desired, std::memory_order order = std::memory_order_seq_cst )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原子地以 &lt;code&gt;desired&lt;/code&gt; 替换底层值。操作为读-修改-写操作。根据 &lt;code&gt;order&lt;/code&gt; 的值影响内存。&lt;/p&gt;
&lt;p&gt;返回调用前原子对象的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;T&amp;gt; x;
T z = x.exchange(y); //此操作在含义上等同 z = x; x = y;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;std::atomic&amp;lt;T&amp;gt;::compare_exchange_weak/strong&lt;/h3&gt;
&lt;p&gt;上面的exchange只是原子交换。但是这个就是著名的CAS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool compare_exchange_weak( T&amp;amp; expected, T desired,
                            std::memory_order success,
                            std::memory_order failure ) noexcept;
bool compare_exchange_strong( T&amp;amp; expected, T desired,
                              std::memory_order success,
                              std::memory_order failure ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;原子地比较 &lt;code&gt;*this&lt;/code&gt; 和&lt;code&gt; expected&lt;/code&gt; 的对象表示，而若它们逐位相等，则以 &lt;code&gt;desired&lt;/code&gt; 替换前者（进行读修改写操作）。否则，将 &lt;code&gt;*this&lt;/code&gt; 中的实际值加载进 &lt;code&gt;expected&lt;/code&gt; （进行加载操作）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若成功更改底层原子值则返回 &lt;code&gt;true&lt;/code&gt; ，否则为&lt;code&gt; false&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;归纳一下这个函数的使用要点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前值与期望值**(expect)&lt;strong&gt;相等时，修改当前值为设定值&lt;/strong&gt;(desired)**，返回&lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当前值与期望值**(expect)&lt;strong&gt;不等时，将期望值&lt;/strong&gt;(expect)**修改为当前值，返回&lt;code&gt;false&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;这一行非常关键。如果当前值和期望值不相等，会修改期望值而不是修改当前值。所以下面我们实现简单的自旋锁的时候必须要搭配&lt;code&gt;while&lt;/code&gt;，而且&lt;code&gt;while&lt;/code&gt;内需要把值改回去。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这个函数可能在满足&lt;code&gt;true&lt;/code&gt;的情况下仍然返回&lt;code&gt;false&lt;/code&gt;，所以只能在循环里使用，否则可以使用它的strong版本
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;weak版和strong版的区别：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;weak版本的CAS允许偶然出乎意料的返回（比如在当前值和期待值一样的时候却返回了false），不过在一些循环算法中，这是可以接受的。通常它比起strong有更高的性能。&lt;/li&gt;
&lt;li&gt;如果此事偶然发生，可以对期望值(expect)进行检查。期望值应该不会改变。&lt;/li&gt;
&lt;li&gt;用人话说就是&lt;code&gt;compare_exchange_weak&lt;/code&gt; 有可能在当前值与 &lt;code&gt;expected&lt;/code&gt; 相等时仍然不执行交换并返回 &lt;code&gt;false&lt;/code&gt;; &lt;code&gt;compare_exchange_strong&lt;/code&gt; 则不会有这个问题. weak 版本能让编译器在一些平台下生成一些更优的代码, 在 x86 下是没区别的.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;T&amp;gt; x;
bool success = x.compare_exchange_weak(y, z);
//此操作含义为：
//如果x == y, 则让 x = z 并且返回true
//如果x != y, 则让 y = x 并且返回false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有没有注意到为啥这个函数有俩&lt;code&gt;memory_order&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E6%9D%82%E8%AE%B03.assets/QQ%E6%88%AA%E5%9B%BE20230116025214.png&quot; alt=&quot;QQ截图20230116025214&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了支持指定两个内存顺序: 成功时的内存顺序和失败时的内存顺序. 一个用于读取一个用于写入&lt;/p&gt;
&lt;p&gt;在 x86 下 &lt;code&gt;compare_exchange_*&lt;/code&gt; 会被编译成一条 &lt;code&gt;cmpxchgl&lt;/code&gt; 指令, 因此操作是原子且无锁的.&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/feikudai8460/article/details/107035480&lt;/p&gt;
&lt;p&gt;https://luyuhuang.tech/2022/10/30/lock-free-queue.html&lt;/p&gt;
&lt;h3&gt;额外说一下fetch系列操作。这里就说一下fetch_add&lt;/h3&gt;
&lt;p&gt;指针特化的fetch_add在下面。这里针对普通类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T fetch_add( T arg,
             std::memory_order order = std::memory_order_seq_cst ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;原子地以值和 &lt;code&gt;arg&lt;/code&gt; 的算术加法结果替换当前值。运算是读修改写操作。按照 &lt;code&gt;order&lt;/code&gt; 的值影响内存。&lt;/li&gt;
&lt;li&gt;其实就是 &lt;code&gt;+=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但是返回值是原值！！！！！！&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;int&amp;gt; x(200);
int z = x.fetch_add(20); //z = x; x += y。
cout &amp;lt;&amp;lt; z &amp;lt;&amp;lt; endl; //z是x修改前的值。200
cout &amp;lt;&amp;lt; x.load() &amp;lt;&amp;lt; endl; //x是220
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;std::atomic&amp;lt;T*&amp;gt;特化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;针对指针类型的特化，代表指针指向的对象&lt;strong&gt;不是&lt;/strong&gt;原子的。&lt;strong&gt;而是&lt;/strong&gt;代表这个指针本身是原子的。&lt;/li&gt;
&lt;li&gt;同样不可拷贝构造和拷贝赋值。&lt;/li&gt;
&lt;li&gt;但是他可以通过合适的指针类型（不一定非得是原子的）进行构造和赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;针对指针特化类型的特殊操作&lt;/h2&gt;
&lt;p&gt;特化的函数这里主要简单介绍&lt;code&gt;fetch_add&lt;/code&gt;, &lt;code&gt;fetch_sub&lt;/code&gt;, &lt;code&gt;operator++&lt;/code&gt;, &lt;code&gt;operator--&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;自增自减没啥好说的。这里就举个&lt;code&gt;fetch_add&lt;/code&gt;的例子&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fetch_add&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;T* fetch_add( std::ptrdiff_t arg,
              std::memory_order order = std::memory_order_seq_cst ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原子地以值和 &lt;code&gt;arg&lt;/code&gt; 的算术加法结果替换当前值。运算是读修改写操作。按照 &lt;code&gt;order&lt;/code&gt; 的值影响内存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其实就是&lt;code&gt;+=&lt;/code&gt;操作。注意这个加法是指针加法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;std::atomic&amp;lt;T&amp;gt;::wait/notify_one/notify_all&lt;/h3&gt;
&lt;p&gt;C++20内容&lt;/p&gt;
&lt;h2&gt;atomic 支持操作总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;&lt;code&gt;atomic_flag&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;atomic&amp;lt;bool&amp;gt;&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;atomic&amp;lt;T*&amp;gt;&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;atomic&amp;lt;integral_type&amp;gt;&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;atomic&amp;lt;other_type&amp;gt;&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_and_set&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clear&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is_lock_free&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;load&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;store&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exchange&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;compare_exchange_weak/strong&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch_add, +=&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch_sub, -=&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch_or, |=&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch_and, &amp;amp;=&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch_xor, ^=&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;++,--&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;关于用户定义类型，有非常严格要求。建议去官方文档看。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>c++并发编程实战-笔记</title><link>https://sterben-01.github.io/posts/%E5%B9%B6%E5%8F%91%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E5%B9%B6%E5%8F%91%E7%AC%94%E8%AE%B0/</guid><pubDate>Mon, 05 Sep 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;第二章&lt;/h1&gt;
&lt;h3&gt;&lt;code&gt;std::thread&lt;/code&gt;的析构函数在线程对象没有调用join或detach的时候会调用&lt;code&gt;std::terminate()&lt;/code&gt;。&lt;/h3&gt;
&lt;p&gt;如果一个线程我们没有调用&lt;code&gt;join&lt;/code&gt;或&lt;code&gt;detach&lt;/code&gt;，也就是说并没有分离出去或阻塞直至新线程执行完毕并回收资源，（也就是&lt;code&gt;joinable() == true&lt;/code&gt;)。那么当这个线程被析构的时候，&lt;code&gt;std::thread&lt;/code&gt;的析构函数会调用&lt;code&gt;std::terminate()&lt;/code&gt;。这是非常不好的行为。因为是异常！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有四种情况在调用&lt;code&gt;thread&lt;/code&gt;析构函数的时候是正常销毁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;被默认构造&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认构造的线程对象不认为是激活的线程对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;被移动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;已调用&lt;code&gt;join()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;已调用 &lt;code&gt;detach()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;为何要这么设计（effective c++ 条款37部分）&lt;/h4&gt;
&lt;p&gt;因为剩下两种选项：隐式&lt;code&gt;join&lt;/code&gt;或隐式&lt;code&gt;detach&lt;/code&gt;会更糟糕。&lt;/p&gt;
&lt;p&gt;如果隐式join，则析构函数会阻塞等待至线程结束。这听上去好像不错，但是会导致难以追踪的性能问题。&lt;/p&gt;
&lt;p&gt;如果隐式detach，假设线程使用了某些局部变量，或者是使用的资源已被释放，会导致很多问题。同时可能此时被释放的内存被其他资源或程序占用，而一直运行的线程会无意间使用这部分本来就毫无相关的资源。&lt;/p&gt;
&lt;h3&gt;注意在将仿函数（函数对象）作为参数传入&lt;code&gt;thread&lt;/code&gt;对象的时候，避免语法歧义。&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class obj{
    //其他东西
};

int main(){
    thread mythread(obj()); //错误
    mythread.join();
    
    thread mythread1((obj())); //可以
    mythread1.join();
    
    thread mythread2{obj()}; //可以
    mythread2.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;传参的时候需要使用引用就需要用&lt;code&gt;std::ref/cref&lt;/code&gt;。因为默认一律按照拷贝/移动传值。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个实参从主线程传递到子线程的线程函数中，&lt;strong&gt;需要经过两次传递&lt;/strong&gt;。&lt;strong&gt;第1次发生在&lt;code&gt;std::thread&lt;/code&gt;构造时，此次参数按值并以副本形式被保存在&lt;code&gt;thread&lt;/code&gt;对象内部&lt;/strong&gt;。&lt;strong&gt;第2次发生在向线程函数传递时&lt;/strong&gt;，此次传递是由子线程发起，并将之前&lt;code&gt;std::thread&lt;/code&gt;内部保存的副本&lt;strong&gt;以右值的形式(std::move())传入线程函数&lt;/strong&gt;中的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当我们使用了&lt;code&gt;ref&lt;/code&gt;去完成引用传参的时候，其实我们会先创建一个&lt;code&gt;std::ref&lt;/code&gt;的临时对象，这里保存着我们要传入的那个值的引用。然后这个&lt;code&gt;std::ref&lt;/code&gt;再以副本的形式保存在&lt;code&gt;thread&lt;/code&gt;对象中。随后这个副本被&lt;code&gt;move&lt;/code&gt;到线程函数，由于&lt;code&gt;std::ref&lt;/code&gt;重载了&lt;code&gt;operator T&amp;amp;()&lt;/code&gt;,因此会隐式转换为对应的类型。因此看起来就像是真的按照引用个传递进来了一样。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此处参考：https://en.cppreference.com/w/cpp/thread/thread/thread&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果线程函数的形参为T、const T&amp;amp;或T&amp;amp;&amp;amp;类型时&lt;/strong&gt;，std::thread的构造函数可以接受左值或右值实参。因为不管是左值还是右值，在std::thread中均是以副本形式被保存，并在第2次向线程函数传参时以右值方式传入，而以上三种形参均可接受右值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;而如果线程函数的形参为T&amp;amp;时&lt;/strong&gt;，&lt;strong&gt;不管是左值还是右值的T类型实参，都是无法直接经std::thread传递给形参为T&amp;amp;的线程函数&lt;/strong&gt;，因为该实参数的副本会被std::move成右值并传递线程函数，但T&amp;amp;无法接受右值类型。&lt;strong&gt;因此，需要以std::ref形式传入&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当向线程函数传参时，可能发生隐式类型转换，这种转换是在子线程中进行的。需要注意，由于隐式转换会构造临时对象，并将该对象（是个右值）传入线程函数，因此线程函数的形参应该是可接受右值类型的T、const T&amp;amp;或T&amp;amp;&amp;amp;类型，但不能是T&amp;amp;类型。此外，如果源类型是指针或引用类型时，还要防止可能发生悬空指针和悬空引用的现象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为thread函数使用了完美转发，所以在某些只支持移动的参数中，必须显示使用move来转移所有权。比如智能指针。（异步操作API也一样）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参考资料：https://www.cnblogs.com/5iedu/p/11633683.html&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;传递成员函数做为线程执行函数注意语法&lt;/h2&gt;
&lt;p&gt;需要传入成员函数指针。语法参见杂记2（显式指定作用域并取地址）。并且要传入类对象地址做为类成员函数的隐藏参数- this指针做为第二个参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class obj{
    public:
    void execute(){
        cout &amp;lt;&amp;lt; &quot;start&quot; &amp;lt;&amp;lt; endl;
    }
};



int main(){
    obj classobj;
    thread mythread(&amp;amp;obj::execute, &amp;amp;classobj); //需要传入成员函数指针。语法参见杂记2（显式指定作用域并取地址）。并且要传入类对象地址做为类成员函数的隐藏参数- this指针。
   
    thread mythread(&amp;amp;obj::execute, ref(classobj)); //引用
    
    thread mythread(&amp;amp;obj::execute, classobj); //对象
    mythread.join();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;此处第二个参数传递引用，指针或者是对象本身都可以。具体情况需要具体分析。比如当禁用了X类的拷贝构造的时候，第三种按值传递就不可以。因为是拷贝。 最好是传递指针。因为类成员函数的第一个参数是this指针。所以thread的第二个参数应该最好是这个类对象的指针。 还有，如果按照值传递，会变成副本。可能会导致原对象被销毁。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;线程对象不可以被复制。但是可以被移动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2
t1=std::thread(some_other_function); // 3
std::thread t3; // 4
t3=std::move(t2); // 5
t1=std::move(t3); // 6 赋值操作将使程序崩溃
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并发编程P32&lt;/p&gt;
&lt;p&gt;前面几个都没问题。包括t1。因为在给t1赋值前，其所有权已经转移至t2。此时t1就空了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;赋值给一个已经有关联线程的std::thread，系统直接调用 std::terminate() 终止程序继续运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不能通过赋新值给 std::thread 对象的方式来&quot;丢弃&quot;一个线程。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果这个容器是移动敏感的(比如，标准中的 &lt;code&gt;std::vector&amp;lt;&amp;gt;&lt;/code&gt; )，那么移动操作同样适用于这些容器。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小心局部变量导致的悬空引用&lt;/h2&gt;
&lt;p&gt;比如我们有这样一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func2(int&amp;amp; x){
    while(true){
        cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
        Sleep(1000);
    }
}
void func1(){
    int x = 10;
    thread th2(func2, ref(x)); //按照引用传递
    th2.detach();
    Sleep(1000);
    cout &amp;lt;&amp;lt;&quot;thread 1 finished&quot; &amp;lt;&amp;lt; endl;
}



int main(){
    thread th1(func1);
    th1.join();
    Sleep(10000); //一定要sleep
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在这个例子中，输出了两次&lt;code&gt;x&lt;/code&gt;后，输出&lt;code&gt;thread 1 finished&lt;/code&gt;。这时候马上会有&lt;code&gt;segmentation fault&lt;/code&gt;。因为这时候&lt;code&gt;x&lt;/code&gt;已经被销毁因为线程1已经结束。所以这时候&lt;code&gt;func2&lt;/code&gt;引用的&lt;code&gt;x&lt;/code&gt;就是悬空引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;避免应用被抛出的异常所终止。通常，在无异常的情况下使用join()时，需要在异常处理过程中调用join()，从而避免生命周期的问题。最好使用RAII&lt;/h2&gt;
&lt;h3&gt;使用RAII包装线程对象避免在某些可能抛出异常的函数中忘记&lt;code&gt;join&lt;/code&gt;或&lt;code&gt;detach&lt;/code&gt;线程对象&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个简单的线程对象包装类。这里的语义是持有一个线程对象的引用。如果想写为值语义，也就是包装类本身持有线程对象，可以参考effective modern C++的条款37&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class thread_guard{
    thread&amp;amp; t; //持有一个线程对象的引用
    public:
        explicit thread_guard (thread&amp;amp; rhs):t(rhs){}; //构造函数要explicit避免隐式类型转换

        ~thread_guard(){
            if(t.joinable()){ //析构函数中如果线程是joinable的
                cout &amp;lt;&amp;lt; &quot;析构&quot; &amp;lt;&amp;lt; endl;
                t.join();
            }
        }
        thread_guard(const thread&amp;amp;) = delete; //禁用拷贝构造和拷贝赋值
        thread_guard&amp;amp; operator=(const thread&amp;amp;) = delete;

};

int main(){
    thread my_thread(func);

    thread_guard my_guard(my_thread); //使用RAII对象管理线程对象
    Sleep(5000);

    cout&amp;lt;&amp;lt;&quot;abc&quot;&amp;lt;&amp;lt;endl;
    //安全join。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在给thread调用的线程函数传递参数的时候，注意参数是否是不可拷贝的&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void threadfunc(mutex &amp;amp;mylock){ //注意必须传引用
    Sleep(1000);
    mylock.unlock();
}
void testfunc(){
    mylock.lock();
    thread th(threadfunc, ref(mylock)); //注意必须传引用
    mylock.lock();
    mylock.unlock();
    th.join();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;mutex&lt;/code&gt;是不可拷贝的。所以必须引用传参。&lt;/p&gt;
&lt;h1&gt;第三章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;尽可能不要使用原始锁，而使用&lt;code&gt;lock_guard&lt;/code&gt;。因为使用原始锁意味着必须在函数的每一个出口都手动解锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个指针或引用，也会让这种保护形同虚设。切勿将受保护数据的指针或引用传递到互斥锁作用域之外。因为指针或引用可以在锁的作用域外面直接修改数据。锁是管不到那些的。所以切勿将受保护数据的指针或引用传递到互斥锁作用域之外，无论 是函数返回值，还是存储在外部可见内存，亦或是以参数的形式传递到用户提供的函数中去。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class list_wrapper{
    list&amp;lt;int&amp;gt; my_list;
    mutex m;

    void add_to_list(int x){
        lock_guard&amp;lt;mutex&amp;gt; lcg(m);
        my_list.push_front(x);
    }

    list&amp;lt;int&amp;gt;* get_list(){
        return &amp;amp;my_list;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上面所示，我们有一个函数会返回一个指向受保护数据（&lt;code&gt;my_list&lt;/code&gt;）的指针。然而有可能有别人通过这个指针直接修改我们的&lt;code&gt;my_list&lt;/code&gt;。这样做就完全绕过了我们的函数&lt;code&gt;add_to_list&lt;/code&gt;所添加的锁的保护。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免死锁
&lt;ul&gt;
&lt;li&gt;方式之一就是让两个互斥量总以相同的顺序上锁。不要一个函数先锁A再锁B，另一个先锁B再锁A。&lt;/li&gt;
&lt;li&gt;我们也可以使用&lt;code&gt;std::lock&lt;/code&gt;或&lt;code&gt;std::scoped_lock&lt;/code&gt;(c++17起)来同时上锁多个互斥量。（由于设计的原因，不会发生死锁。因为一旦一个锁报异常就会把另一个锁也解锁 -- 要么两个都锁，要么一个都不锁。）同时，我们可以用&lt;code&gt;lock_guard&lt;/code&gt; 的可选参数&lt;code&gt;std::adopt_lock&lt;/code&gt;来接管已经上锁了的互斥量。&lt;/li&gt;
&lt;li&gt;避免死锁的进阶指导：
&lt;ul&gt;
&lt;li&gt;避免嵌套锁。&lt;/li&gt;
&lt;li&gt;避免在持有锁时调用外部代码。&lt;/li&gt;
&lt;li&gt;使用固定顺序获取锁。&lt;/li&gt;
&lt;li&gt;使用层次锁结构。当代码试图对互斥量上锁，而低层已持有该层锁时，不允许锁定。因此锁的顺序只能先锁层级高的锁再锁层级低的锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unique_lock&lt;/code&gt;比&lt;code&gt;lock_guard&lt;/code&gt;更灵活，但是成本会更高。&lt;/li&gt;
&lt;li&gt;一般情况下，尽可能将持有锁的时间缩减到最小。&lt;/li&gt;
&lt;li&gt;单例模式也可以使用&lt;code&gt;call_once&lt;/code&gt;来解决线程安全问题。&lt;/li&gt;
&lt;li&gt;可以使用读写锁来减少一定场合下普通锁的开销。c中可以使用&lt;code&gt;pthread_rwlock_t&lt;/code&gt;，c++可以使用&lt;code&gt;shared_lock&lt;/code&gt;(c++17起）/&lt;code&gt;shared_mutex&lt;/code&gt;（c++14起）
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;std::unique_lock&lt;/code&gt; 与 &lt;code&gt;std::lock_guard&lt;/code&gt; 管理排他性锁定。(写锁)&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;std::shared_lock&lt;/code&gt; 管理共享锁定。（读锁）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第四章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;同步操作可使用条件变量。此处不赘述用法。&lt;/li&gt;
&lt;li&gt;剩下的都在杂记3.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;thread源码解析&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;
// 管理线程的类
class thread {
public:
	class id;                   // 内部ID类
	typedef void *native_handle_type;
 
	thread() _NOEXCEPT {	    // 构造函数，专门为空线程设计。这就是为什么默认构造的线程对象并不是有效的
		_Thr_set_null(_Thr);    // 宏定义，原型为：#define _Thr_set_null(thr) (thr._Id = 0)
	}
 
	template&amp;lt;class _Fn, class... _Args&amp;gt;
    explicit thread(_Fn&amp;amp;&amp;amp; _Fx, _Args&amp;amp;&amp;amp;... _Ax) {	// 带参模板构造函数_Fx(_Ax...)
		_Launch(&amp;amp;_Thr, _STD bind(_Decay_copy(_STD forward&amp;lt;_Fn&amp;gt;(_Fx)), _Decay_copy(_STD forward&amp;lt;_Args&amp;gt;(_Ax))...));
        //一堆乱七八糟连带着完美转发和退化的。你看到copy了。所以说了thread是一种必拷贝传值的。引用的话得用ref包一下。
    }
 
	~thread() _NOEXCEPT {	    // 析构函数
		if (joinable())         // 如果析构线程对象的时候，线程依旧是可结合的（没有调用join或detach）的话会调用terminate
			_XSTD terminate();  // terminate会调用abort()来终止程序
	}
 
	thread(thread&amp;amp;&amp;amp; _Other) _NOEXCEPT : _Thr(_Other._Thr) {	    // 拷贝构造函数，调用move
		_Thr_set_null(_Other._Thr);
	}
 
	thread&amp;amp; operator=(thread&amp;amp;&amp;amp; _Other) _NOEXCEPT {	// 赋值函数，调用move
		return (_Move_thread(_Other));
	}
 
	thread(const thread&amp;amp;) = delete;                 // 禁用 拷贝构造函数
	thread&amp;amp; operator=(const thread&amp;amp;) = delete;      // 禁用 赋值函数
 
	void swap(thread&amp;amp; _Other) _NOEXCEPT {	        // 交换两线程
		_STD swap(_Thr, _Other._Thr);
	}
 
	bool joinable() const _NOEXCEPT {	            // 若线程可结合程，返回 true；否则，返回flase
		return (!_Thr_is_null(_Thr));               // 宏定义，原型为：#define _Thr_is_null(thr) (thr._Id == 0)
	}
 
	void join();                                    // 线程结合，阻塞的
 
	void detach() {	                                // 线程分离
		if (!joinable())                            // 若线程是不可结合的，则异常
			_Throw_Cpp_error(_INVALID_ARGUMENT);
		_Thrd_detachX(_Thr);					//detach的核心
		_Thr_set_null(_Thr);					//设置线程id为0
	}
 
	id get_id() const _NOEXCEPT;                    // 获取线程唯一 id
 
	static unsigned int hardware_concurrency() _NOEXCEPT {	    // 返回硬件线程上下文数量
		return (::Concurrency::details::_GetConcurrency());
	}
 
	native_handle_type native_handle() {	        // 以 void* 形式返回线程的 Win32 句柄
		return (_Thr._Hnd);
	}
 
private:
	thread&amp;amp; _Move_thread(thread&amp;amp; _Other) {	        // move from _Other
		if (joinable())
			_XSTD terminate();
		_Thr = _Other._Thr;
		_Thr_set_null(_Other._Thr);
		return (*this);
	}
 
	_Thrd_t _Thr;            // 私有成员变量，_Thrd_t是一个结构体
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;里面的&lt;code&gt;_Thrd_t&lt;/code&gt;结构体长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_Thrd_t _Thr; //其实_Thrd_t 是类型的别名
 
typedef _Thrd_imp_t _Thrd_t;    // 而_Thrd_imp_t是一个结构体
 
typedef struct {	/* 线程 Win32 标识符 */
    void *_Hnd;	    /* Win32 句柄 */
    unsigned int _Id;    // 线程id
} _Thrd_imp_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;join&lt;/code&gt;长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
inline void thread::join(){	// join thread
	if (!joinable())            // 如果线程不可结合，调用join就报错。
		_Throw_Cpp_error(_INVALID_ARGUMENT);
	if (_Thr_is_null(_Thr))     // 如果是空线程，调用join继续报错。
		_Throw_Cpp_error(_INVALID_ARGUMENT);
	if (get_id() == _STD this_thread::get_id()) // 线程不能自己join自己。自己join自己就死锁了。
		_Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
	if (_Thrd_join(_Thr, 0) != _Thrd_success)   // 线程结合（_Thrd_join()是join方法的核心），是阻塞的
		_Throw_Cpp_error(_NO_SUCH_PROCESS); //如果结合失败就报错
	_Thr_set_null(_Thr);        // 设置线程id为0
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们看到这几种情况不可以&lt;code&gt;join&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认构造的线程对象（空对象）&lt;/li&gt;
&lt;li&gt;是非joinable的 (比如已经&lt;code&gt;join&lt;/code&gt;或者&lt;code&gt;detach&lt;/code&gt;过的，&lt;code&gt;move&lt;/code&gt;过的和空的）&lt;/li&gt;
&lt;li&gt;自己和自己。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果一个可结合的线程经过&lt;code&gt;join&lt;/code&gt;后（等线程执行完毕后），会将线程id置为0。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们查看&lt;code&gt;_Move_thread&lt;/code&gt;函数可以发现，如果尝试给一个仍有效的线程对象再次用一个线程对象赋值，则会触发&lt;code&gt;terminate&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么析构一个非joinable的线程会报错？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在实例化了线程对象之后，它们的状态默认都是可结合的，如果现在直接调用它们的析构函数来析构它们，那么在析构的时候线程处于什么状态呢？是执行完了吗？还是正在执行呢？注意，如果一个在没有结合（join）的情况下，就算它先于主线程执行完毕，其id依然是不为0的。所以我们是不能确定其状态的，所以我们只能析构明确了id为0的线程。因为id为0的线程要么已经执行完毕，要么是空线程，要么是分离后的线程。&lt;/li&gt;
&lt;li&gt;另外，一个线程分离（&lt;code&gt;detach&lt;/code&gt;）后，该线程对象便不能控制该线程，而是交由系统接管。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;学习，整理自&lt;a href=&quot;https://blog.csdn.net/hujingshuang/article/details/70208443&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;join和detach区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;join&lt;/code&gt;在主线程和新线程中引入了一个同步点。
&lt;ul&gt;
&lt;li&gt;主线程调用&lt;code&gt;join&lt;/code&gt;后，会阻塞直到新线程完成并返回。只有当&lt;code&gt;join&lt;/code&gt;返回后，主线程才能回收被调线程资源，并继续运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当使用&lt;code&gt;detach&lt;/code&gt;函数时，主线程继续运行，被调线程驻留后台运行。二者同时运行。主调线程无法再取得该被调线程的控制权。当主调线程结束时，由运行时库负责清理与被调线程相关的资源。（&lt;code&gt;thread&lt;/code&gt;对象已和底层执行线程分离）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们对一个线程对象使用&lt;code&gt;join&lt;/code&gt;或&lt;code&gt;detach&lt;/code&gt;后，这个线程对象就不再是&lt;code&gt;joinable&lt;/code&gt;的了。这时候线程对象可以被安全销毁。&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;detach&lt;/code&gt;函数被调用后，执行的线程从线程对象中被分离，已不再被一个线程对象所表达--这是两个独立的事情。**C++线程对象可以被销毁，同时OS执行的线程可以继续。**如果程序想要知道执行的线程何时结束，就需要一些其它的机制。&lt;code&gt;join&lt;/code&gt;函数在那个thread对象上不能再被调用，因为它已经不再和一个执行的线程相关联。
&lt;strong&gt;去销毁一个仍然可以“joinable”的C++线程对象会被认为是一种错误。为了销毁一个C++线程对象，要么&lt;code&gt;join&lt;/code&gt;函数需要被调用（并结束），要么&lt;code&gt;detach&lt;/code&gt;函数被调用。如果一个C++线程对象当销毁时仍然可以被&lt;code&gt;join&lt;/code&gt;，异常会被抛出。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;提防表面的线程安全&lt;/h1&gt;
&lt;p&gt;假设我们有这样一个表面线程安全的&lt;code&gt;stack&lt;/code&gt;包装器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class not_good_stack{
    public:
    stack&amp;lt;T&amp;gt; my_stk;
    mutex m;
        void push(const T&amp;amp; element){
            lock_guard&amp;lt;mutex&amp;gt;lcg (m);
            my_stk.push(element);
        }
        void pop(){
            lock_guard&amp;lt;mutex&amp;gt;lcg (m);
            my_stk.pop();
        }
        bool empty(){
            lock_guard&amp;lt;mutex&amp;gt;lcg (m);
            return my_stk.empty();
        }
        T&amp;amp; top(){
            lock_guard&amp;lt;mutex&amp;gt;lcg (m);
            return my_stk.top();
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它很简单，因为只是给每一个标准库实现的函数外面加了个锁。而且现在看起来很安全。不会同时又&lt;code&gt;push&lt;/code&gt;又&lt;code&gt;pop&lt;/code&gt;。但是有一个问题。在&lt;code&gt;empty&lt;/code&gt;和&lt;code&gt;pop&lt;/code&gt;中间，&lt;code&gt;top&lt;/code&gt;和&lt;code&gt;pop&lt;/code&gt;中间都会有间接的线程不安全问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void race1(){
    not_good_stack&amp;lt;int&amp;gt; mystk;

    if(!mystk.empty()){
        int value = mystk.top();
        mystk.pop();
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下，假设我们现在&lt;code&gt;stack&lt;/code&gt;只有一个元素。有可能有两个线程并发的进入了该函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个函数检查是否为空，答案是不为空，所以进入&lt;code&gt;if&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二个函数此时刚好也进来了，检查是否为空。由于第一个函数没有来得及进行下一步，则此时依旧不为空。也进入&lt;code&gt;if&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候就已经有问题了。可能第一个元素已经移除元素，这时候&lt;code&gt;top&lt;/code&gt;会出现错误。&lt;/p&gt;
&lt;p&gt;甚至，因为两个线程必定会执行两次&lt;code&gt;pop&lt;/code&gt;。但是此时只有一个元素。所以会出现错误。&lt;/p&gt;
&lt;p&gt;如果并不是只有一个元素，也会额外的多删除一个元素。也是错误。&lt;/p&gt;
&lt;h2&gt;如何解决&lt;/h2&gt;
&lt;p&gt;一般有两种方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种方式是针对整个函数&lt;code&gt;race1&lt;/code&gt;加锁。&lt;/li&gt;
&lt;li&gt;另一种方式是把有竞争关系的函数整合为一个。例如&lt;code&gt;empty&lt;/code&gt;和&lt;code&gt;pop&lt;/code&gt;合起来&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void pop(){
    lock_guard&amp;lt;mutex&amp;gt;lcg (m);
    if(my_stk.empty()){
        cout &amp;lt;&amp;lt; &quot;already empty&quot; &amp;lt;&amp;lt;endl;
        return;
    }
    my_stk.pop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;信号量&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;这个&lt;code&gt;sem_init&lt;/code&gt;最后的值意思是初始值是几。也不能完全理解为物品容量。信号量后续操作是单纯地对那个数字进行增减。而这个数字只有初始值而没有顶（所以会溢出）。&lt;code&gt;wait&lt;/code&gt;会让这个数字减掉1。如果&lt;strong&gt;相减后&lt;/strong&gt;小于0了就阻塞**（所以这个数字理论上可以到-1，因为我们如果现在是1，则拿一个资源就是0。因为现在我拿到了资源所以不会被阻塞，然后下一个线程进来了-1，发现结果变成了-1，就会阻塞）**。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择题中，这个值可以为负。负数就是目前有多少个进程/线程在等待该资源&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这个值可能会溢出。也就是初始值假如5，可能变成6，溢出，这样会返回错误 一个错误代码是&lt;code&gt;EOVERFLOW&lt;/code&gt;。但是一般操作系统会忽略。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post&lt;/code&gt;会让睡眠的进程唤醒，如果&lt;strong&gt;相加后&lt;/strong&gt;发现信号量值&amp;lt;=0，意味着有程序被阻塞，则会唤醒对应的线程或者是进程。如果&amp;gt;0则意味着没有进程睡眠&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也就是为什么信号量是&lt;strong&gt;先等待（-1），后加锁&lt;/strong&gt;。因为&lt;code&gt;wait&lt;/code&gt;本身是阻塞的，如果小于0了就阻塞。如果先加锁，发现小于0了直接阻塞就没办法解锁了。这也是为什么条件变量要反过来。&lt;/p&gt;
&lt;h2&gt;如何理解信号量&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;**信号量理解为需要控制的资源数量。**比如经典的更衣室场景。我们不应该只限制一个人在更衣室内，假如更衣室的容量是50，则应该允许高达50人同时在更衣室内。所以使用信号量，让信号量的初始值为50是非常合理的选择，也满足语义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所以如果初始值是1，那么这就是一种互斥锁。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;**同时，我们反复强调，只有当信号量数值小于等于0的时候才会引起阻塞，所以由于这个特殊性，信号量不需要broadcast。**因为信号量数值增加（有人从更衣室出来）的方式只可以是一个一个增加，所以只需要挑选一个睡眠的进程（线程）唤醒即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;例子&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;假设我们使用的是一个信号量，即消费者和生产者共享信号量。
&lt;ul&gt;
&lt;li&gt;一个信号量的时候就是，如果数字不为0，就该消费消费该生产生产。如果数字为0了，那么消费者就等着，等生产者生产完了通知后继续消费。&lt;/li&gt;
&lt;li&gt;首先，初始化的时候，我们不能让消费者直接消费，所以初始化的值一定是0。（如果生产者消费者区分信号量，则生产者信号量初始值应为队列的最大值，消费者信号量初始值仍旧应为0。&lt;/li&gt;
&lt;li&gt;生产者：
&lt;ul&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;生产&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;li&gt;post [+1]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;消费者：
&lt;ul&gt;
&lt;li&gt;wait [-1] 一定要先等待。如果上来就锁了，因为wait是阻塞的如果是0就阻塞等待，那生产者也拿不到锁也没办法生产了。&lt;/li&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;消费&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;假设我们使用的是两个信号量，即消费者和生产者&lt;strong&gt;不&lt;/strong&gt;共享信号量。（其实和条件变量差不多）
&lt;ul&gt;
&lt;li&gt;首先，初始化的时候，我们不能让消费者直接消费，所以初始化的值一定是0。但是生产者可以直接生产。所以初始化的值可以为队列最大值，比如8。&lt;/li&gt;
&lt;li&gt;生产者：
&lt;ul&gt;
&lt;li&gt;wait[-1] 注意这个时候是减掉的生产者自己的空位。也就是每生产一次，减掉一个。他最多生产8个，生产多了就停止等待让这个数字不为0。（消费者会+1）&lt;/li&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;生产&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;li&gt;post [+1]注意这个时候是添加的消费者的消费。让消费者的信号量不为1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;消费者：
&lt;ul&gt;
&lt;li&gt;wait[-1] 注意这个时候是减掉的消费者自己的空位。也就是记录有多少可以消费的&lt;/li&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;消费&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;li&gt;post[+1]注意这个是告诉生产者+1，也就是可生产的空位+1.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;信号量可以有多种同步方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实现一次临时的happens-before 语义
&lt;ul&gt;
&lt;li&gt;初始信号量为&lt;code&gt;0&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A; V(S)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;V就是&lt;code&gt;post&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;P(S); B&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt;就是&lt;code&gt;wait&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;假设S只被使用一次，则保证A happens-before B。因为如果A没有执行完毕后把信号量的值增加1，则B无法执行，因为前面一直在等待信号量的值增加。&lt;/li&gt;
&lt;li&gt;举个例子就是标准库的线程的&lt;code&gt;join&lt;/code&gt;操作。如果使用这个方法，则每一个子线程会&lt;code&gt;V(Sn)&lt;/code&gt;。主线程会&lt;code&gt;P(Sn)&lt;/code&gt;。比如第一个线程会&lt;code&gt;V(S1)&lt;/code&gt;，第二个线程会&lt;code&gt;V(S2)&lt;/code&gt;。主线程就是&lt;code&gt;P(S1); P(S2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E5%B9%B6%E5%8F%91%E7%AC%94%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20230508221734.png&quot; alt=&quot;QQ截图20230508221734&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现计数型同步
&lt;ul&gt;
&lt;li&gt;初始信号量为&lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行线程每完成一次工作，&lt;code&gt;V(S)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;主线程等候，&lt;code&gt;P(S) * 工作数量&lt;/code&gt;来检查是否完成全部工作。&lt;/li&gt;
&lt;li&gt;举个例子就是标准库的线程的&lt;code&gt;join&lt;/code&gt;操作。如果使用这个方法，则每一个子线程会V(S)。主线程会&lt;code&gt;P(S)&lt;/code&gt;。比如第一个线程会&lt;code&gt;V(S)&lt;/code&gt;，第二个线程会&lt;code&gt;V(S)&lt;/code&gt;。主线程就是&lt;code&gt;P(S)*n&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E5%B9%B6%E5%8F%91%E7%AC%94%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20230508221739.png&quot; alt=&quot;QQ截图20230508221739&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;条件变量&lt;/h1&gt;
&lt;p&gt;条件变量的语义是：&lt;em&gt;&lt;strong&gt;wait until&lt;/strong&gt;&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;条件变量一定要先加锁而且必须是可以手动解锁的锁比如&lt;code&gt;unique_lock&lt;/code&gt;而不能使用&lt;code&gt;lock_guard&lt;/code&gt;&lt;/strong&gt;。也可以用一个条件变量也可以用两个。核心是&lt;code&gt;pthread_cond_wait&lt;/code&gt;。原理是首先调用方抢锁，然后发现需要等待，所以调用方会被阻塞（睡眠并加入等待队列），然后互斥锁解锁，让其余线程抢锁。其余线程如果抢到锁执行完了任务，然后就可以调用&lt;code&gt;notify&lt;/code&gt;通知。通知后调用方的&lt;code&gt;wait&lt;/code&gt;会重新加锁并唤醒当前进程（之后&lt;code&gt;wait&lt;/code&gt;函数返回）。&lt;strong&gt;系统保证解锁并睡眠是原子操作。系统也保证加锁并唤醒是原子操作。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了防止虚假唤醒，必须要使用&lt;code&gt;while&lt;/code&gt;而不能使用wait。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为首先，我们的生产者和消费者线程全部都是&lt;code&gt;wait&lt;/code&gt;同一个条件变量。我们在&lt;code&gt;wait&lt;/code&gt;函数返回之前，当前进程必须执行 &lt;strong&gt;拿到锁--&amp;gt;加锁并唤醒&lt;/strong&gt; 这两步。加锁并唤醒是原子的，但是并不一定能拿得到这个锁。假如我们消费者1在等待，然后生产者生产完毕，通知消费者。假如这个时候消费者2进来了，直接就拿了锁（因为生产者释放锁到&lt;code&gt;wait&lt;/code&gt;函数拿锁这两步不是原子的。存在这种第三方插进来的情况）然后消费了生产的数据。然后释放锁。这时候我们消费者1才终于拿到锁了，但是这个时候抢到的锁在语义上可能并不是被原始生产者释放的锁，而是被其他消费者释放的锁。所以发现数据已经被消费了（或者是不满足某一条件），这样再去拿数据会有错误。所以必须用&lt;code&gt;while&lt;/code&gt;。也就是使用判别式。&lt;/p&gt;
&lt;p&gt;**再详细解释一下第三方插进来的情况。其实这样解释并不准确。因为我们提到过，我们的生产者和消费者线程全部都是&lt;code&gt;wait&lt;/code&gt;同一个条件变量。所以我们&lt;code&gt;notify&lt;/code&gt;不一定是&lt;code&gt;notify&lt;/code&gt;到具体哪一个生产者或者是哪个消费者。如果不使用&lt;code&gt;while&lt;/code&gt;再次判断的话，就会发生错误。提到的不满足某一条件，比如队列为空等等。**举个例子，此时两个消费者都卡在&lt;code&gt;wait&lt;/code&gt;。生产者生产了一个，&lt;code&gt;notify&lt;/code&gt;。这时候&lt;code&gt;notify&lt;/code&gt;了一个消费者，消费者拿到了锁，消费过后队列为空。这时候再解锁，&lt;code&gt;notify&lt;/code&gt;一个生产者，表示可以生产了。但是此时可能&lt;code&gt;notify&lt;/code&gt;到了另一个消费者。因为消费者已经卡在&lt;code&gt;wait&lt;/code&gt;了（已经判断过条件了），它拿到了锁。然后再想消费但是队列为空。这时候就错了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E5%B9%B6%E5%8F%91%E7%AC%94%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20230506004803.png&quot; alt=&quot;QQ截图20230506004803&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意条件变量的信号丢失问题。&lt;strong&gt;因为如果没有&lt;code&gt;notify()&lt;/code&gt;，则&lt;code&gt;wait()&lt;/code&gt;的线程永远不会唤醒。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看看例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::condition_variable cv;
std::mutex gMtx;

void Sender(){	
    std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
    std::cout &amp;lt;&amp;lt; &quot;Ready Send notification.&quot; &amp;lt;&amp;lt; std::endl;
    lck.unlock(); //先解锁
    cv.notify_one();   // 发送通知
 }

void Receiver(){
    std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
    std::cout &amp;lt;&amp;lt; &quot;Wait for notification.&quot; &amp;lt;&amp;lt; std::endl;
    cv.wait(lck);    // 等待通知并唤醒继续执行下面的指令
    std::cout &amp;lt;&amp;lt; &quot;Process.&quot; &amp;lt;&amp;lt; std::endl;
}

int main(){
    std::thread sender(Sender);
    std::thread receiver(Receiver);
    sender.join();
    receiver.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程随机启动导致的唤醒丢失，即：通信线程先启动并调用通知函数（&lt;code&gt;notify_one&lt;/code&gt;），但是接收线程还没有开始执行等待（&lt;code&gt;wait&lt;/code&gt;）函数，如果不再次调用函数通知，等待会一直持续下去。这个是最容易发现和验证的问题，上面的主线程中启动线程的顺序就会概率性出现唤醒丢失的问题。&lt;/p&gt;
&lt;p&gt;解决方案也比较简单。也是搭配&lt;code&gt;while&lt;/code&gt;和判断式。其实和上面解决虚假唤醒的道理一致。只不过要注意使用&lt;code&gt;while&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::condition_variable::wait&lt;/code&gt;后面的判断式（谓词）的意思是，只要后面的谓词返回&lt;code&gt;false&lt;/code&gt;，则前面无论如何都不会解锁。尽管可能已经被通知到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断式法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::condition_variable cv;
std::mutex gMtx;

bool send = false;
void Sender(){
    std::cout &amp;lt;&amp;lt; &quot;Ready Send notification.&quot; &amp;lt;&amp;lt; std::endl;
    std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
    send = true;
    lck.unlock(); //先解锁
    cv.notify_one();   // 发送通知
}

void Receiver(){
    std::cout &amp;lt;&amp;lt; &quot;Wait for notification.&quot; &amp;lt;&amp;lt; std::endl;
    std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
    cv.wait(lck, [](){return send;});    // 等待通知并唤醒继续执行下面的指令
    std::cout &amp;lt;&amp;lt; &quot;Process.&quot; &amp;lt;&amp;lt; std::endl;
}

int main(){
    std::thread sender(Sender);
    std::thread receiver(Receiver);
    sender.join();
    receiver.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面的谓词可以等同于这种写法：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void Receiver1()
{
     std::cout &amp;lt;&amp;lt; &quot;Wait for notification.&quot; &amp;lt;&amp;lt; std::endl;
     std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
     while(send == false){
        cv.wait(lck); // 等待通知并唤醒继续执行下面的指令
     }
     std::cout &amp;lt;&amp;lt; &quot;Process.&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有没有发现和解决虚假唤醒的很像？都是&lt;code&gt;while&lt;/code&gt;内有一个判别式，然后循环判断等待。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这个方法可以解决唤醒丢失的原因：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;我们的问题在于&lt;code&gt;sender&lt;/code&gt;先发送了&lt;code&gt;notify&lt;/code&gt;，然后&lt;code&gt;receiver&lt;/code&gt;才开始&lt;code&gt;wait&lt;/code&gt;。导致丢失&lt;/li&gt;
&lt;li&gt;所以在使用上述方法后，就算&lt;code&gt;sender&lt;/code&gt;先发送了&lt;code&gt;nofity&lt;/code&gt;，但是此时&lt;code&gt;send&lt;/code&gt;已经被&lt;code&gt;sender&lt;/code&gt;变更为&lt;code&gt;true&lt;/code&gt;。所以此时就算唤醒&lt;code&gt;receiver&lt;/code&gt;的信号已经丢失，由于&lt;code&gt;send&lt;/code&gt;是&lt;code&gt;true&lt;/code&gt;，所以不会被阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二者的解决方式都是&lt;code&gt;while&lt;/code&gt;+判别式。&lt;/li&gt;
&lt;li&gt;为什么要先解锁，再通知？也就是先&lt;code&gt;unlock()&lt;/code&gt;，再&lt;code&gt;notify_one()&lt;/code&gt;？
&lt;ul&gt;
&lt;li&gt;语义上没什么区别，但是性能会有一点点提升。原因是如果先通知再解锁，在通知的那一刻，&lt;code&gt;wait&lt;/code&gt;中的被通知线程会尝试获取锁，但是这时候通知线程还没有释放锁，所以被通知线程又会被阻塞一次然后睡眠，加入等待队列。如果是先释放锁，则不会发生这种情况。也就是它可能会直接获取锁成功。
&lt;ul&gt;
&lt;li&gt;这时候可能会问，我要是先解锁不就有可能通知线程又抢了一次吗？没错，但是就算是后解锁也有可能发生这种情况。也就是所有情况下，都可能发生虚假唤醒。这种情况其实并不要紧。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;来自&lt;a href=&quot;https://youtu.be/F6Ipn7gCOsY?t=2200&quot;&gt;这里&lt;/a&gt; 和 &lt;a href=&quot;https://stackoverflow.com/questions/55154462/c-condition-variable-notify-one-before-or-after-release-lock&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;简要分析源码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 阻塞等待条件。进入该函数前，已经获得了互斥锁mutex
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
{
    volatile pthread_t self = thread_self();
    
    acquire(&amp;amp;cond-&amp;gt;c_spinlock);// 加锁操作队列

    enqueue(&amp;amp;cond-&amp;gt;c_waiting, self); // 插入条件的等待队列

    release(&amp;amp;cond-&amp;gt;c_spinlock); // 操作完释放锁
    
    pthread_mutex_unlock(mutex); // 释放互斥变量，否则别人无法操作资源，导致条件一直无法满足
    
    suspend_with_cancellation(self); // 挂起，然后等待条件满足后被唤醒
	//上面这行和下面这行不是原子的，所以虚假唤醒发生在这里。
    pthread_mutex_lock(mutex); // 被唤醒后重新获取互斥锁
    /* This is a cancellation point */
    // 取消点，等待期间被取消了
    if (self-&amp;gt;p_canceled &amp;amp;&amp;amp; self-&amp;gt;p_cancelstate == PTHREAD_CANCEL_ENABLE) {
        /* Remove ourselves from the waiting queue if we&apos;re still on it */
        acquire(&amp;amp;cond-&amp;gt;c_spinlock);
        // 线程准备退出，从条件阻塞队列中移除
        remove_from_queue(&amp;amp;cond-&amp;gt;c_waiting, self);
        release(&amp;amp;cond-&amp;gt;c_spinlock);
        pthread_exit(PTHREAD_CANCELED);
    }
    return 0;
}
static inline void suspend_with_cancellation(pthread_t self)
{
  sigset_t mask;
  sigjmp_buf jmpbuf;
  // 获取当前的信号屏蔽码
  sigprocmask(SIG_SETMASK, NULL, &amp;amp;mask); /* Get current signal mask */
  // 清除PTHREAD_SIG_RESTART的信号掩码，即允许处理该信号
  sigdelset(&amp;amp;mask, PTHREAD_SIG_RESTART); /* Unblock the restart signal */
  /* No need to save the signal mask, we&apos;ll restore it ourselves */
  /*
    直接调用返回0，从siglongjump回来返回非0,这里支持线程挂起时，
    收到restart信号被唤醒，或者在取消信号的处理函数中，通过siglongjmp返回这里
  */
  if (sigsetjmp(jmpbuf, 0) == 0) {
    self-&amp;gt;p_cancel_jmp = &amp;amp;jmpbuf;
    // 已经被取消并且是可取消的则直接返回，否则挂起等待唤醒
    if (! (self-&amp;gt;p_canceled &amp;amp;&amp;amp; self-&amp;gt;p_cancelstate == PTHREAD_CANCEL_ENABLE)) {
      do {
        // 挂起等待restart信号
        sigsuspend(&amp;amp;mask);               /* Wait for a signal */
      } while (self-&amp;gt;p_signal != PTHREAD_SIG_RESTART);
    }
    self-&amp;gt;p_cancel_jmp = NULL;
  } else {
    // 从cancel信号的处理函数中的siglongjmp返回，重新设置信号掩码,屏蔽restart信号
    sigaddset(&amp;amp;mask, PTHREAD_SIG_RESTART); /* Reblock the restart signal */
    sigprocmask(SIG_SETMASK, &amp;amp;mask, NULL);
  }
}
// 条件满足，唤醒线程
int pthread_cond_signal(pthread_cond_t *cond)
{
  pthread_t th;

  acquire(&amp;amp;cond-&amp;gt;c_spinlock);
  th = dequeue(&amp;amp;cond-&amp;gt;c_waiting); // 取出一个被被阻塞的线程
  release(&amp;amp;cond-&amp;gt;c_spinlock);

  if (th != NULL) restart(th); // 发送信号唤醒他
  return 0;
}
// 给pid进程发送唤醒信号
static inline void restart(pthread_t th)
{
  kill(th-&amp;gt;p_pid, PTHREAD_SIG_RESTART);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整理自&lt;a href=&quot;https://zhuanlan.zhihu.com/p/374385534&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;生产者消费者模型中，到底使用几个条件变量？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;针对条件变量和生产者消费者模型到底应该用几个条件变量的问题，比较看情况。有可能生产者和消费者各有一个，也可能同时使用一个。为什么呢？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是一个非常不好解答的问题。因为这个和死锁息息相关。比如下面的例子。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::condition_variable cvS;
std::condition_variable cvR;
std::condition_variable cv;
std::mutex gMtx;
int countm = 0;
void SenderS(){
    std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
    while(countm &amp;gt;= 1){
        std::cout &amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt;&quot;exceedin S&quot; &amp;lt;&amp;lt; std::endl;
        cv.wait(lck); //or cvS.wait(lck);
    }
    countm++;
    std::cout&amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt; &quot;S&quot;&amp;lt;&amp;lt; countm &amp;lt;&amp;lt; std::endl;
    lck.unlock();
    cv.notify_one();  //or cvR.notify_one(); or cv.notify_all();
}
void ReceiverS(){
    std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
    while(countm &amp;lt;= 0){
        std::cout &amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt;&quot;exceedin R&quot; &amp;lt;&amp;lt; std::endl;
        cv.wait(lck); //or cvR.wait(lck);
    }
    countm--;
    std::cout &amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt;&quot;R&quot; &amp;lt;&amp;lt; countm &amp;lt;&amp;lt; std::endl;
    lck.unlock();
    cv.notify_one();  //or cvS.notify_one(); or cv.notify_all();
}
void Sender(){
    while(1){
        SenderS();
    }
}
void Receiver(){
    while(1){
        ReceiverS();
    }
}
int main(){
    std::thread sender1(Sender);
    std::thread receiver1(Receiver);
    std::thread sender2(Sender);
    std::thread receiver2(Receiver);
    sender1.join();
    receiver1.join();
    sender2.join();
    receiver2.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面的代码死锁了。为何会死锁？现在我们只使用1个条件变量&lt;code&gt;cv&lt;/code&gt;，并且使用的是&lt;code&gt;cv.notify_one()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;在极限情况下，假设现在有一个&lt;code&gt;ReceiverS&lt;/code&gt;执行了，卡在&lt;code&gt;cv.wait&lt;/code&gt;。然后这时候是第二个&lt;code&gt;ReceiverS&lt;/code&gt;执行，也卡在&lt;code&gt;cv.wait&lt;/code&gt;了。然后这时候只能从两个&lt;code&gt;SenderS&lt;/code&gt;中选。其中一个&lt;code&gt;SenderS&lt;/code&gt;成功执行，&lt;strong&gt;然后这时候非常不巧。&lt;code&gt;notify&lt;/code&gt;的还是一个&lt;code&gt;SenderS&lt;/code&gt;&lt;/strong&gt;。然后卡在&lt;code&gt;cv.wait&lt;/code&gt;。然后最后只剩下一个&lt;code&gt;SenderS&lt;/code&gt;，又卡在&lt;code&gt;cv.wait&lt;/code&gt;了。此时所有线程都卡在了&lt;code&gt;cv.wait&lt;/code&gt;。死锁发生。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;但是为什么改成两个条件变量，也就是&lt;code&gt;cvS&lt;/code&gt;和&lt;code&gt;cvR&lt;/code&gt;就没事呢？
&lt;ul&gt;
&lt;li&gt;假设现在有一个&lt;code&gt;ReceiverS&lt;/code&gt;执行了，卡在&lt;code&gt;cvR.wait&lt;/code&gt;。然后这时候是第二个&lt;code&gt;ReceiverS&lt;/code&gt;执行，也卡在&lt;code&gt;cvR.wait&lt;/code&gt;了。然后这时候只能从两个&lt;code&gt;SenderS&lt;/code&gt;中选。其中一个&lt;code&gt;SenderS&lt;/code&gt;成功执行，&lt;strong&gt;然后这时候&lt;code&gt;notify&lt;/code&gt;一定且只能通知到等待&lt;code&gt;cvR&lt;/code&gt;的线程，也就是&lt;code&gt;ReceiverS&lt;/code&gt;&lt;/strong&gt;。就算现在唤醒的这个&lt;code&gt;ReceiverS&lt;/code&gt;没有成功抢锁，被一个&lt;code&gt;SenderS&lt;/code&gt;抢到了，&lt;code&gt;SenderS&lt;/code&gt;卡在&lt;code&gt;cvS.wait&lt;/code&gt;了。然后此时又没有成功抢锁，第二个&lt;code&gt;SenderS&lt;/code&gt;抢到了，&lt;code&gt;SenderS&lt;/code&gt;也卡在&lt;code&gt;cvS.wait&lt;/code&gt;了。不过没关系。这时候抢到锁的一定是仅剩的一个&lt;code&gt;ReceiverS&lt;/code&gt;。**因为&lt;code&gt;ReceiverS&lt;/code&gt;此时已经被唤醒，只是没抢到锁，只要能抢到锁程序就可以继续。**然后此时&lt;code&gt;notify&lt;/code&gt;一定且只能通知到等待&lt;code&gt;cvS&lt;/code&gt;的线程，也就是&lt;code&gt;SenderS&lt;/code&gt;。程序继续。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;或者改成&lt;code&gt;cv.notify_all();&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;这样可以确保唤醒所有等候的线程。因为这种情况死锁的核心是非常不巧的&lt;code&gt;notify&lt;/code&gt;到了自己类型的线程。也就是发送者通知发送者，接收者通知接收者。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以说，具体使用一个或两个不是那么简单就可以决定的。这是我的&lt;a href=&quot;https://stackoverflow.com/questions/76187529/how-many-conditoinal-variables-should-be-used-in-producer-consumer-problem&quot;&gt;提问&lt;/a&gt;。&lt;strong&gt;根据我的个人分析，如果使用一个条件变量，为了避免唤醒到错误的线程，则应该使用&lt;code&gt;notify_all()&lt;/code&gt;唤醒所有等候线程。如果使用两个条件变量，则使用&lt;code&gt;notify_one()&lt;/code&gt;唤醒对方的一个线程即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**同时，具体的&lt;code&gt;while&lt;/code&gt;判断的&lt;code&gt;wait&lt;/code&gt;的条件也很重要。比如现在是两个接收者和两个发送者。如果不更改原有死锁代码，只把&lt;code&gt;countm &amp;gt;= 1&lt;/code&gt;更改为&lt;code&gt;countm &amp;gt;= 2&lt;/code&gt;便没有问题。但是，如果这时候又加了一个发送者，三个发送者的话此时又会死锁。**所以为了根本性避免这个问题，就应该使用上一段提到的，自我总结的规则。因为一般来说，具体的&lt;code&gt;while&lt;/code&gt;判断的&lt;code&gt;wait&lt;/code&gt;的条件是不确定的，比如在任务队列场景下，具体应该大于几是可以更改的，同时，具体由几个线程也是可以更改的。但是。任务队列本身的逻辑不应该变动。&lt;/p&gt;
&lt;h1&gt;杂项&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;注意，线程对象在创建时就已经启动。而非调用&lt;code&gt;join&lt;/code&gt;或&lt;code&gt;detach&lt;/code&gt;时启动，这里和异步API有些不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func(){
    cout&amp;lt;&amp;lt;&quot;我的线程开始执行了&quot;&amp;lt;&amp;lt;endl;
    //...
    cout&amp;lt;&amp;lt;&quot;我的线程结束执行了&quot;&amp;lt;&amp;lt;endl;
}

int main(){
    thread my_thread(func);
    Sleep(5000);
    my_thread.join();//等待子线程执行结束

    cout&amp;lt;&amp;lt;&quot;abc&quot;&amp;lt;&amp;lt;endl;
    return 0;
}
/*
我的线程开始执行了
我的线程结束执行了

然后睡眠直到时间到达。开始继续执行join
abc

*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++11原语和C++20原语的比较。来自&lt;a href=&quot;https://youtu.be/F6Ipn7gCOsY?t=2573&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E5%B9%B6%E5%8F%91%E7%AC%94%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20230506032808.png&quot; alt=&quot;QQ截图20230506032808&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-09-05-%E5%B9%B6%E5%8F%91%E7%AC%94%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20230506032738.png&quot; alt=&quot;QQ截图20230506032738&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>深度探索 C++ 对象模型 笔记</title><link>https://sterben-01.github.io/posts/%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B/</guid><pubDate>Mon, 22 Aug 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;深度探索 C++ 对象模型 笔记&lt;/h1&gt;
&lt;h1&gt;1.1&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;类的封装不会给C++带来额外成本。&lt;/li&gt;
&lt;li&gt;额外成本（时间，空间）来自于虚机制&lt;/li&gt;
&lt;li&gt;类内只包含非静态成员变量的大小 和 虚机制导致的虚指针（如果有的话）。静态成员变量，所有成员函数都不算作类的大小。此处不再赘述&lt;/li&gt;
&lt;li&gt;关于虚函数和虚指针，RTTI在单独的笔记有。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1.2&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;C++优先判断一个语句为声明：当语言无法区分一个语句是声明还是表达式时，就需用用一个 超越语言范围的规则 —— C++优先判断为声明。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;小例子就是在声明对象的时候，使用无参构造函数不能加括号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test test1(); 	//这是声明了一个返回test类型的无参函数
test test1; 	//这才是是声明了一个test对象并使用了无参构造
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;C++只保证处于同一个 access section 的数据，一定会以声明的次序出现在内存布局当中。 C++标准只提供了这一点点的保证&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以说变长数组（柔性数组，动态数组）这种东西一般只出现在单纯的class/struct里面。&lt;/p&gt;
&lt;h1&gt;2.1构造函数语义学&lt;/h1&gt;
&lt;h2&gt;注意，默认构造函数的意思是，我们没写，编译器认为需要，编译器生成的。这个叫默认构造。 但是我们写了一个构造，无论是有参还是无参，假如我们现在只有一个有参构造，则我们需要无参初始化的时候就要再写一个无参构造。因为只要我们写了任意一个构造，编译器就不会为我们合成默认构造。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所以一般情况下我们只要写了构造函数，就要写一个默认无参构造函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果构造函数参数有默认值，这个构造函数就成了默认构造函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;默认构造函数就是要么没有参数，要么所有参数都有默认值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只有编译器认为需要默认构造的时候才会被合成出来。任何被程序需要的东西，编译器都不负责合成，这是程序员的责任&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只有全局变量和静态变量会保证被初始化&lt;/strong&gt;，因为他们都是保存在BSS段（如果未初始化或者是初始化为0值）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果 class A 内含一个或以上的 member objects，那么 A 的 constructor 必须调用每一个 member class 的（默认或对应的）构造函数。没有显式调用，编译器则会扩张构造函数，帮我们调用。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这句话看起来很抽象，什么意思呢？举个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    test(){ cout &amp;lt;&amp;lt; &quot;test const&quot; &amp;lt;&amp;lt; endl;}
    int val;
};
class test1{
    public:
    int t1val; 		
    test tt;			//test1 内涵test 而不是继承
    test1(){			//显式定义无参构造函数，但是没有显式初始化我们的test对象。
        t1val = 10;
        cout &amp;lt;&amp;lt;&quot;t1const &quot; &amp;lt;&amp;lt; endl;
    }
};
int main()
{
    test1 t1;			//声明test1
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们这里&lt;code&gt;test1&lt;/code&gt;内涵了一个&lt;code&gt;test&lt;/code&gt;对象（&lt;strong&gt;如果 class A 内含一个或以上的 member objects&lt;/strong&gt;），我们&lt;code&gt;test1&lt;/code&gt;有自己显式的无参构造函数。但是我们的构造函数没有初始化&lt;code&gt;test&lt;/code&gt;对象，这怎么办？编译器会帮我们。在我们&lt;code&gt;test1&lt;/code&gt;的构造函数中会被插入形如&lt;code&gt;tt.test::test()&lt;/code&gt;这样的一条代码来调用对应的构造函数来初始化&lt;code&gt;test&lt;/code&gt;对象。（&lt;strong&gt;那么 A 的 constructor 必须调用每一个 member class 的（默认或对应的）构造函数。&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;如果有多个class member objects 都要求constructor初始化操作，将如何? &lt;strong&gt;C++语言要求以“member objects 在 class中的声明顺序”来调用各个constructors。这就是声明 的次序决定了初始化次序（构造函数初始化列表一直要求以声明顺序来初始化）的根本原因！&lt;/strong&gt;（当然你实际写的时候逆序也可以，但是编译器依旧会按照声明的顺序去初始化，和你写的顺序没关系，但是可能你这么写会造成一些错误）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;整体来说，就是编译器会扩张 constructors ，在其中安插代码使得在 user code 被调用之前先调用 member objects 的默认构造函数（当然如果需要调用基类的默认构造函数，则放在基类的默认构造函数调用之后：基类构造函数 -&amp;gt; 成员构造函数 -&amp;gt;user code）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;带有有 虚函数 的类的默认构造函数毫无疑问是 notrivial 的，需要编译器安插额外的 成员 vptr 并在构造函数中正确的设置好 vptr，这是编译器的重要职责之一。 带有 虚基类 的类的默认构造函数同样也毫无疑问的 notrivial，编译器需要正确设 置相关的信息以使得这些虚基类的信息能够在执行时准备妥当，这些设置取决于编译实现虚基类 的手法。（&lt;strong&gt;一句话：虚基表和虚函数表的存在导致含有虚机制的类在没有构造函数的情况下必须被合成一个默认构造函数&lt;/strong&gt;）&lt;/p&gt;
&lt;h2&gt;总结：&lt;/h2&gt;
&lt;p&gt;编译器&lt;strong&gt;有 4 种情况会使得编译器真正的为 class 生成 nontrivial 的默认构造函数&lt;/strong&gt;，这个 nontrivial 的默认构造函数只满足编译器的需要。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 member objects 的默认构造函数&lt;/li&gt;
&lt;li&gt;调用 base class 的默认构造函数、&lt;/li&gt;
&lt;li&gt;初始化 virtual function （虚机制）&lt;/li&gt;
&lt;li&gt;初始化 virutal base class （虚机制）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其它情况时，类在概念上拥有默认构造函数，但是实际上根本不会被产生出来&lt;/strong&gt;（前面的区分 trivial 和 notrivial）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;C++新手常见的 2 个误区：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;如果 class 没有定义 default constructor 就会被合成一个； 首先定义了其它的 constructor 就不会合成默认构造函数，再次即使没有定义任何构造函数 也不一定会合成 default constructor，可能仅仅是概念上有，但实际上不合成出来。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编译器合成出来的默认构造函数会明确设定每一个 data member 的默认值； 明显不会，区分了 Global objects, Stack objects, Heap objects 就非常明白了只有在 Global 上的 objects （全局或静态变量）会被清 0，其它的情况都不会保证被清 0。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2.2 拷贝构造函数语义学&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;bitwise copy = 浅拷贝&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;memberwise copy = 深拷贝&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;拷贝构造函数 和默认构造函数一样，&lt;strong&gt;只有在必须的时候才会被产生出来&lt;/strong&gt;，对于大部分的 class 来说，拷贝构造函数仅仅需要按位拷贝就可以。 满足 bitwise copy semantics 的拷贝构 造函数是 trivial 的，就不会真正被合成出来（与默认构造函数一样，只有 nontrivial 的拷贝构 造函数才会被真正合成出来）。&lt;strong&gt;注意，深拷贝和浅拷贝，也就是拷贝构造函数的正确与否在这里我们是不关心的。我们只关心生成与否。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么时候不满足浅拷贝语义呢？也就是什么时候拷贝构造会被真正的合成出来呢？四种情况&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当class内含一个member object而后者的class声明有一个copy constructor时（不论是被显式地声明，或是被编译器合成）。**一句话：当类内含有另一个类成员，而另一个类成员含有了拷贝构造，无论是声明出来的或者是被合成的。**因为这个类的拷贝构造必须要调用包含的类成员的拷贝构造，执行这个过程则必须生成拷贝构造，然后隐式插入其中。&lt;/li&gt;
&lt;li&gt;当class继承自一个base class而后者存在一个copy constructor时（再次强调,不论是被显式声明或是被合成而得）。&lt;strong&gt;一句话：当子类继承父类，父类含有拷贝构造的时候，无论是声明出来的或者是被合成的。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当class声明了一个或多个virtual functions 时。&lt;strong&gt;（有虚函数时）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这个就是我们说的，当我们用子类对象给父类对象赋值的时候，会产生切割（slice）。而且在赋值的时候编译器会禁止赋值任何和虚机制相关的东西&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220823200622.png&quot; alt=&quot;QQ截图20220823200622&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;也就是说，合成出来的 父类拷贝构造会显式设定对象的虚函数表指针指向父类自己的虚函数表,而不是直接从右手边的对象 中将其虚函数表指针现值拷贝过来。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当class派生自一个继承串链，其中有一个或多个virtual base classes时。&lt;strong&gt;（有虚继承时）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译器会合成一个拷贝构造函数，安插一些代码用来设定虚基类指针和偏移的初值以及执行其他的内存相关工作&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2.3 程序转化语义学&lt;/h1&gt;
&lt;p&gt;NRV优化 杂记2提到了。不赘述&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然后就是，在不需要深拷贝语义的情况下不要瞎鸡巴写拷贝构造。效率低没意义。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;2.4 构造函数初始化列&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造函数初始化列在初始化的时候就将值做直接初始化。 而不是先默认构造后再赋值 assignment。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不用初始化列表，则会先initialize初始化再做赋值assignment（拷贝）动作。 先调用默认无参构造，再调用拷贝赋值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比如如果构造函数中存在string，我们在普通情况下会调用默认构造，再调用拷贝赋值。 如果使用初始化列表，则会&lt;strong&gt;调用参数最匹配的构造函数进行直接初始化&lt;/strong&gt;。也就是直接调用拷贝构造&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;test(const string&amp;amp; a){ //先调用默认构造函数构造val, 然后使用拷贝赋值把a赋值给val
  val = a;
}
test(const string&amp;amp; a):val(a){} //直接调用string类变量val的拷贝构造，把a复制进去。因为val是个string
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;类的构造函数后面跟冒号：找到最匹配的构造函数直接构建对象。也就是系统创建类成员变量的同时初始化（本例中直接调用拷贝构造函数）。&lt;/li&gt;
&lt;li&gt;类的构造函数里面等号赋值的方式：使用默认构造构建对象后再进行拷贝赋值。也就是系统创建成员变量后（调用默认构造函数后），再进行拷贝赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四种情况必须使用初始化列进行成员的初始化：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;常量成员&lt;code&gt; const&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为如果不用初始化列，会先调用默认构造函数生成&lt;code&gt;const&lt;/code&gt;对象，再去赋值。但是&lt;code&gt;const&lt;/code&gt;对象不允许更改值。（&lt;code&gt;const&lt;/code&gt;对象不允许定义和初始化分离。）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用成员&lt;code&gt; reference&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原因同上。引用不允许定义和初始化分离。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用父类的有参构造的时候&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用类内其他类成员的有参构造的时候&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当某一个成员没有默认构造的时候&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于初始化列是直接调用对应构造，所以没有默认构造但是能找到其他的构造也可以。&lt;/li&gt;
&lt;li&gt;但是写在函数体内就是先默认构造再拷贝赋值。这样就必须要求对应成员拥有默认构造。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;构造函数初始化列的初始化顺序和初始化列的顺序无关，只和类内变量声明的顺序有关。但是如果顺序错乱，虽然编译层面不会报错，但会导致不可预知的错误&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class test{
    int i;
    int j;
    test(int val):j(val), i(j){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;上面的代码看起来像是要把&lt;code&gt;j&lt;/code&gt;设初值为&lt;code&gt;val&lt;/code&gt;，再把&lt;code&gt;i&lt;/code&gt;设初值为&lt;code&gt;j&lt;/code&gt;。问题在于，由于声明顺序的缘故，初始化列中的&lt;code&gt;i(j)&lt;/code&gt;其实比&lt;code&gt;j(val)&lt;/code&gt;更早执行。但因为&lt;code&gt;j&lt;/code&gt;一开始未有初值，所以&lt;code&gt;i (j)&lt;/code&gt;的执行结果导致i无法预知其值。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;除非改成这样：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    int i;
    int j;
    test(int val):j(val){
        i = j;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;strong&gt;初始化列会被放在用户代码之前执行&lt;/strong&gt;，也就是这里，我们由于初始化列只初始化&lt;code&gt;j&lt;/code&gt;，没有&lt;code&gt;i&lt;/code&gt;, 所以他跳过了&lt;code&gt;i&lt;/code&gt;先初始化了&lt;code&gt;j&lt;/code&gt;，然后进入到用户代码块去给&lt;code&gt;i&lt;/code&gt;初始化。&lt;/p&gt;
&lt;p&gt;在初始化列中使用成员函数是合法的，因为和这个对象相关的&lt;code&gt;this&lt;/code&gt;指针已经被构建。但是要考虑依赖性来避免发生错误。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int i;
int value;
int j;
A():i(99),j(100),value(foo());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会不会产生错误取决于成员函数&lt;code&gt;foo()&lt;/code&gt;是依赖于&lt;code&gt;i&lt;/code&gt;还是&lt;code&gt;j&lt;/code&gt;:
如果&lt;code&gt;foo&lt;/code&gt;依赖于&lt;code&gt;i&lt;/code&gt;，由于&lt;code&gt;i&lt;/code&gt;声明在&lt;code&gt;value&lt;/code&gt;之前，所以不会产生错误﹔
如果&lt;code&gt;foo&lt;/code&gt;依赖于&lt;code&gt;j&lt;/code&gt;，由于&lt;code&gt;j&lt;/code&gt;声明在&lt;code&gt;value&lt;/code&gt;之后，就产生了使用未初始化成员的错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话总结：编译器会一一操作初始化列表，把其中的初始化操作以 member 声明的次序在 constructor 内安插初始化操作，并且在任何 explicit user code 之前。 以 member 声明的次序来决定初始化次序 和 初始化列表中的排列次序 之间的外观错乱，可 能会导致一些不明显的 Bug。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;3.3 类成员变量的存取速度&lt;/h1&gt;
&lt;p&gt;类静态对象无论是通过对象调用还是通过指针调用，没有差距。因为静态对象不属于类。&lt;/p&gt;
&lt;p&gt;普通的类对象通过指针调用还是对象调用也没有区别。因为变量地址可以在编译器确定。&lt;/p&gt;
&lt;p&gt;当对象地址无法在编译期被确定，比如在有虚机制发生的时候，这时候通过指针调用和对象调用就会有速度差异。这时候对象调用会更快。因为对象调用不会触发多态，而指针调用会触发多态。编译的时候无法确定到底是哪一种类型。&lt;/p&gt;
&lt;h1&gt;3.4 C++ Standard 保证：“出现在派生类中的 base class subobject 有其完整原样性！”&lt;/h1&gt;
&lt;p&gt;子类会被放在父类的对齐空白字节之后，因为父类的完整性必须得以保证，父类的对齐空白字节也是父类的一部分，也是不可分割的。&lt;/p&gt;
&lt;p&gt;举个例子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220826025225.png&quot; alt=&quot;QQ截图20220826025225&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们这里可以用如下语句来进行深拷贝（如果定义了深拷贝的拷贝构造/拷贝赋值）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*pc1_2 = *pc1_1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前提是他俩实际上都指向&lt;code&gt;Concrete1&lt;/code&gt;。如果&lt;code&gt;pc1_1&lt;/code&gt;实际指向了&lt;code&gt;Concrete2&lt;/code&gt;或者&lt;code&gt;Concrete3&lt;/code&gt;，则上述操作应该吧2和3里面的父类部分（1自己的部分）复制给1。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果没有padding，那么父类对象这里现在是空白，而子类对象现在这里有值。那么一旦发生子类对象给父类对象赋值的话，父类对象本来应该空的位置会被子类的数据填充，这样就乱套了。所以先补齐是为了保证每一个类的完整性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以是先补齐，再继承。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;关于多态的虚函数表指针应该放在父类的屁股还是放在开头的小讨论&lt;/h2&gt;
&lt;p&gt;我们讨论过。父类指针指向子类对象，父类指针可以解析的范围仅是子类的父类范围。因为父类指针仅有权限读取并正确解释那么大的区块。而且我们讨论过，虚函数表指针是可以理解成继承（复制）自父类的。属于子类的父类部分。所以在有虚函数表指针的时候，如果放在基类的屁股，则无论是在屁股后增添子类数据成员还是赋值回去造成的slice都比较自然。如果放在开头，则没有多态和有多态的时候，类成员的起始地址会有差异。&lt;/p&gt;
&lt;h2&gt;关于类的成员对象指针可能会在实际偏移量上+1 也就是取类成员地址的偏移量。&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220828073243.png&quot; alt=&quot;QQ截图20220828073243&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们如果想拿到一个类的成员的地址可以如下面操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    int a;
};


int main() {
    A obj;
    cout &amp;lt;&amp;lt; &amp;amp;A::a &amp;lt;&amp;lt; endl; //输出1
    cout &amp;lt;&amp;lt; &amp;amp;obj.a &amp;lt;&amp;lt; endl;//输出0x61fe1c
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这里第一种叫做取一个类的非静态成员的地址。这里会输出相对地址。因为这是废话。真实地址要依靠对象来实现。所以这里会输出1。为啥是1不是0？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二种才是叫做取一个类的对象身上的类的成员地址。这里才会输出真实地址。因为是绑定在对象身上的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里的主要意思我想就是，一个空指针会指向0。但是类的成员指针其实是按照对象起始地址的偏移量来计算的（实际储存的是相对于对象起始地址的偏移）。假如刚好偏移量为0，为了区分是空指针还是指向了成员的头部，可能编译器会在指向成员头部指针的具体offset上面+1.&lt;/p&gt;
&lt;h1&gt;4.1 各种成员函数的调用方式&lt;/h1&gt;
&lt;h3&gt;注意 类静态成员函数不可以被声明为const。也就是类成员函数不可同时用const和static修饰。&lt;/h3&gt;
&lt;p&gt;因为const必须是成员函数（需要this指针）。然而static修饰的成员不属于类，（没有this指针）。&lt;/p&gt;
&lt;h2&gt;非静态成员函数和普通函数在调用层面没有性能差别&lt;/h2&gt;
&lt;p&gt;因为编译器会把每所有的非虚函数（此处不确定是否正确。有的笔记里写的是普通函数，普通成员函数和静态成员函数。书里此处没有提及静态成员函数。）进行处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先是把对象调用的方式改为值调用。也就是在形参的第一个位置放一个this指针（指向对象的指针）做为入参&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其次是把函数内对于非静态数据成员的直接操作变成用this指针的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后是名称重新编译成独特名称（这一步的目的是实现重载。注意，C++重载是依靠名称和参数，C只有名称）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后是实施NRV优化（如果有）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;类成员函数在处理后可以理解为变成了带作用域的全局函数。虚函数也只不过是多了查表的步骤。&lt;/p&gt;
&lt;h2&gt;虚函数在调用层面的优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只有触发多态的时候（引用或指针调用虚函数）才会被编译器处理成进行查表（我们提到过虚函数调用其实就是查虚函数表）。&lt;strong&gt;如果没有触发多态（对象调用）则会被编译器按照正常成员函数处理。因为没有触发多态，不查表。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在虚函数内调用虚函数的时候，可能会有一个非常大的优化空间。也就是因为第一层虚函数已经被查表到了，所以自己内部的调用虚函数的操作可能会被直接处理为通过作用域运算符调用对应类的函数。&lt;/strong&gt;（这里不太确定）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;使用类作用域访问运算符显式调用虚函数会压制虚函数的调用方法（不会查表），调用法会被处理成调用非静态成员函数。&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220828220848.png&quot; alt=&quot;QQ截图20220828220848&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;静态成员函数的调用法&lt;/h2&gt;
&lt;p&gt;在进行静态成员函数的调用时，编译器依旧会把它变成一般的非成员函数进行调用。和类的非静态成员函数唯一的区别就是，静态成员函数没有this指针，不需要添加这个形参。&lt;/p&gt;
&lt;h1&gt;4.2 虚成员函数（就是虚函数）&lt;/h1&gt;
&lt;h3&gt;单继承部分&lt;/h3&gt;
&lt;p&gt;第一部分讲的是虚函数表。已经在别的地方说过了此处不赘述。&lt;/p&gt;
&lt;p&gt;需要补充的部分：&lt;strong&gt;虚函数表是编译期确定的原因是，我们已经按照顺序对所有的虚函数进行了排列。也就是知道了每一个虚函数表的下标对应的函数是什么。但是唯一一个需要运行期确定的是，我要找哪个表？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220829005232.png&quot; alt=&quot;QQ截图20220829005232&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;多继承部分&lt;/h3&gt;
&lt;p&gt;在虚函数笔记有写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要在一个 virtual base class 中声明 nonstatic data members。如果一定要这么做，那么你会距离复杂的深渊愈来愈近，终不可拔。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;4.3 函数的效能&lt;/h1&gt;
&lt;p&gt;函数性能测试表明， inline 函数的性能如此之高，比其它类型的函数高的不是一个等级。因为 inline 函数不只能够节省一般函数调用所带来的额外负担，也给编译器提供了程序优化的额外机会&lt;/p&gt;
&lt;p&gt;内联函数就是把调用函数的部分换成内联函数的函数体。会造成代码膨胀。而且inline关键词仅仅是建议，编译器不一定会执行。&lt;/p&gt;
&lt;h1&gt;4.4 指向成员函数的指针&lt;/h1&gt;
&lt;h2&gt;非虚函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们提到过，直接提取非静态成员数据的地址只能拿到类内的偏移量。只有通过对象调用才可访问到真实地址。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成员函数虽然直接提取可以提取到真正的地址，但是此时无法调用，必须绑定到一个对象上之后通过对象调用，因为会被编译器转化成一个带有this指针的函数。需要传入this指针，所以需要对象地址。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220831044250.png&quot; alt=&quot;QQ截图20220831044250&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体在杂记2有介绍。&lt;/p&gt;
&lt;h2&gt;虚函数&lt;/h2&gt;
&lt;p&gt;对一个 虚函数取其地址，所能获得的只是一个 虚函数表中的索引值。&lt;/p&gt;
&lt;h2&gt;多重继承或虚继承&lt;/h2&gt;
&lt;p&gt;过于复杂不说了。&lt;/p&gt;
&lt;h1&gt;4.5 inline&lt;/h1&gt;
&lt;p&gt;inline 函数扩展时的实际参数取代形式参数的过程，会聪明地引入临时变量来避免重复求值。&lt;/p&gt;
&lt;p&gt;假如有如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline int minval(int i, int j){
    return  i &amp;lt; j ? i : j;
}

inline int bar(){
    int minval;
    int val1 = 1024;
    int val2 = 2048;
    
    minval = min(val1, val2);		//1
    minval = min(1024, 2048);		//2
    minval = min(foo(), bar()+1);	//3
    return minval;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于第一种，会发生参数替换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minval = val1 &amp;lt; val2 : val1, val2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于第二种，替换后直接常量计算。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minval = 1024;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于第三种，会引发副作用。所以会引入临时变量避免了对这个函数的多次调用。因此，知道编译器会自动的做这些优化，就没有必须自己去画蛇添足的手动引入临时变量了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int temp1;
int temp2;
minval = (t1 = foo), (t2 = bar()+1), t1 &amp;lt; t2 ? t1 : t2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;inline 中再调用 inline 函数，可能使得表面上一个看起来很平凡的 inline 却因连锁的复杂性而没有办法扩展开来。在inline函数中的局部变量和有副作用的参数也会导致大量临时对象的产生。编译器不一定可以把他们移除。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;5.1 无继承情况下的对象构造&lt;/h1&gt;
&lt;h2&gt;纯虚函数&lt;/h2&gt;
&lt;p&gt;纯虚函数介绍在vptr。这里仅做补充。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要把所有函数都声明为虚函数，然后依赖于编译器优化，这样不好。&lt;strong&gt;如果这个函数不需要多态调用，则不要把他声明为虚函数。这样会提升效率。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;一个虚函数该不该被定义为 const 呢？一个虚函数在基类中不需要修改 data member 并不意味着派生类改写它时一定不会修改 data member&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;对象生存周期&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220831081947.png&quot; alt=&quot;QQ截图20220831081947&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个对象的生命，是该对象的一个执行期属性。局部对象 的生命从L5的定义开始，到L10为止。全局对象 的生命和整个程序的生命相同。堆对象的生命从它被new运算符配置出来开始，到它被delete运算符摧毁为止。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;记住：理论上，编译器会为每一个类产生四个（六个）默认函数：构造，析构，拷贝构造，拷贝赋值，（移动构造，移动赋值）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是，并不一定真正产生。因为有的时候他们是trivial的。此处前面提到过，不赘述。2.1提到了何时生成non-trivial的构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;POD类型可以使用列表初始化（聚合初始化），C++11后限制放宽。参见聚合初始化笔记。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;POD类型是聚合类型的子集。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;针对可以使用列表初始化的类型，使用列表初始化会比一个一个赋值要快一些。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;针对移动构造和移动赋值，有更严格的要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译器只会针对满足如下情况的类生成移动构造和移动赋值
&lt;ul&gt;
&lt;li&gt;没有用户定义的移动构造/移动赋值&lt;/li&gt;
&lt;li&gt;没有用户声明的拷贝构造/拷贝赋值&lt;/li&gt;
&lt;li&gt;没有用户声明的析构函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以会有0/3/5法则。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;引入虚函数会给对象的构造，拷贝，析构带来负担。&lt;/h2&gt;
&lt;p&gt;我们说过，在多态调用中，我们构建子类对象的时候会伴随着虚函数表指针的切换。父类部分构造期间指向父类，子类部分构造期间指向子类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（严格来说虚函数表指针的切换应该是在构造函数后，用户代码前）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而且也提到过为何对象调用无法触发多态，因为虚函数表指针不会被复制。编译器优化后就是不查表。（本笔记关键词搜索：对象调用）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这一切都需要编译器隐式的在我们的构造函数中添加代码。尤其是对象调用的时候，为了防止错误的拷贝虚函数表指针，还需要额外合成拷贝构造和拷贝赋值。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;5.2 继承情况下的对象构造&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;C++ Standard 要求尽量延迟 nontrival members 的实际合成操作，直到真正遇到其使用场合为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;构造函数可能会被安插大量代码：&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;记录在成员初始化列表中的数据成员初始化操作会被放进构造函数的函数本体，并以数据成员的声明顺序为顺序。&lt;strong&gt;（初始化列表的部分被声明）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果有一个成员并没有出现在成员初始化列之中，但它有一个默认构造函数，那么该d默认构造函数必须被调用。&lt;strong&gt;（前面提到过，编译器会帮助我们调用对应的构造函数来构造出在类内但是没有在初始化列中的数据。看2.1）。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在那之前，如果这个类有虚函数指针，则需要被设定至指向正确的虚函数表**（设置虚函数表指针）**。&lt;/li&gt;
&lt;li&gt;在那之前，所有的父类构造函数必须被调用。以父类的声明顺序为顺序**（调用父类构造）**。
&lt;ol&gt;
&lt;li&gt;如果父类被放在了子类的初始化列表中，则所有的显式指定的参数都要被传递**（这块看一下频道的子类构造中调用父类构造）**&lt;/li&gt;
&lt;li&gt;如果父类没有被放在子类的初始化列表中，但是有默认构造，则调用默认构造。&lt;/li&gt;
&lt;li&gt;如果这个父类是多重继承下的非第一基类，则&lt;code&gt;this&lt;/code&gt;指针需要调整。&lt;strong&gt;（这部分看vptr笔记的多继承thunk技术）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;在那之前，所有的虚基类构造函数必须被调用，从左到右，从深到浅。同时设置好所有的虚基类所需要的各种机制。&lt;strong&gt;（虚继承部分）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总结：顺序就是&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚继承部分&lt;/li&gt;
&lt;li&gt;继承部分&lt;/li&gt;
&lt;li&gt;虚函数表指针&lt;/li&gt;
&lt;li&gt;数据成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220831091426.png&quot; alt=&quot;QQ截图20220831091426&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220831091431.png&quot; alt=&quot;QQ截图20220831091431&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;记得在拷贝赋值中要进行自我赋值检测。&lt;/h2&gt;
&lt;h2&gt;虚继承中，共享基类（虚基类）必须由最底层的类负责初始化操作。&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-22-%E6%B7%B1%E5%BA%A6%E6%8E%A2%E7%B4%A2%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B.assets/QQ%E6%88%AA%E5%9B%BE20220901000848.png&quot; alt=&quot;QQ截图20220901000848&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里，point是point3d和vertex虚继承的父类（共享基类）。然后只有vertec3D才会负责初始化虚基类的内容。这也是为什么虚基类的元素总会在屁股。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通常，中间类调用这个共享基类初始化的操作会被编译器压制住。具体做法是增加一个&lt;code&gt;most_derived&lt;/code&gt;参数判断是否是最底层的类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们已经说过了，但还要再重复一次。构造函数中的代码是不具有多态性的。编译器在构造函数中安插代码时会保证： 先调用所有基类的构造函数，再设置 vptr，然后再调用 member initialization 操作。这是构造函数中没有多态性的根本原因！&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;5.3 对象复制语义学&lt;/h1&gt;
&lt;h2&gt;拷贝赋值函数在如下四种情况不会表现出浅拷贝语义（bitwise copy）。此处和拷贝构造一样。看2.2&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类内包含一个有拷贝赋值的对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类继承自一个有拷贝赋值的类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚继承。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 标准没有规定在虚继承时 拷贝赋值 中是否会多次调用共享基类的 拷贝赋值。这样就有可能造成共享基类被赋值多次，造成一些错误，所以程序员应该在使用了 虚基类 时小心检验 拷贝赋值 里的代码,以确保这样的多次赋值没有问题或者查看编译器是否已经提供了解决方案**。因此，尽可能不要允许一个 虚基类 的拷贝操作，甚至根本不在要任何 虚基类 中声明数据。**&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般而言，我个人觉得在继承中，最好显式调用基类的拷贝构造。使用&lt;code&gt;this-&amp;gt;base::operator=()&lt;/code&gt;的方式调用。（个人理解，关于调用参考笔记拷贝构造）&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;5.5 析构语义学&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;析构函数如果没有被定义，则只有两种情况下才会被编译器真正合成出来。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在类内含有一个其他拥有析构函数的类的对象的情况下。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类拥有析构函数的情况下。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他情况下，析构函数被视为trivial所以不会合成也不会被调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有虚函数不代表需要析构函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;要思考析构函数的作用。析构函数的作用旨在帮助你执行你认为当对象被销毁后应该执行的操作。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对象销毁会调用析构函数，所以调用时机是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;栈对象离开其作用域&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;堆对象被手动 delete&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;析构函数执行顺序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先开始执行析构函数本体。谨记虚函数指针会在用户代码执行前被重新设置。&lt;/li&gt;
&lt;li&gt;如果类中拥有其他类的对象，并且对应类拥有析构函数。则他们会以其声明顺序的相反顺序被调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重新设置虚函数指针，让它指向当前这个构造函数所处的类。也就是子类析构期间，析构函数中的虚函数一定执行的是子类的。父类析构期间，因为子类已经被析构，则更不可能调用到子类的虚函数。所以必须在做其他事情之前先重新正确设置虚函数指针。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果有自己的非虚拟继承的父类，则会以其声明顺序的相反顺序被调用。也就是析构顺序是先子类析构再父类析构。和构造顺序相反。&lt;/li&gt;
&lt;li&gt;如果有任何的虚拟继承的父类，而且当前类是最底层的。则他们会按照原来的构造顺序相反的顺序进行调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;由于析构函数中的重设 vptr 会在任何代码之前被执行，这样就保证了在析构函数中也不具有多态性，从而不会调用非本类的函数。因为此时对象已经不完整了，有些成员已经不存在了，而函数有可能需要使用这些成员。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;构造函数和析构函数中（的函数调用）都不具有多态性（也就是调用对应类型的虚函数一定是本类的）：这并不是语言的弱点，而是正确的语意所要求的（因为那个时候的对象不完整）&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;6.1 执行期语义学 - 对象的构造和析构&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为c++有很多的隐式类型转换，所以不太容易从程序代码中看出表达式的复杂度（时间和空间）。因为涉及到隐式类型转换的时候大概率会需要临时对象。所以其实一个简单的表达式可能会被编译器扩展为具有多次函数调用和多个临时对象的代码段。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为局部变量的生存周期是作用域（代码区块（花括号））。所以在这个区块内，可能会有隐式的多个地方可能调用这个对象的析构函数。换句话讲，如果一个区块内有多个退出点，则每一个退出点之前都要有这个区块内所有局部对象的析构函数。因为已经到了对象的生存期。&lt;strong&gt;所以尽量在需要对象的时候才声明对象，不要在一开始就声明所有的对象。这样可以在一些情况下减少非必要的对象的创建和销毁成本。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++保证全局对象会被在第一次使用之前构建完毕，在main结束之前析构掉。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建议不要使用需要使用静态初始化的全局对象。（不理解）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;现在的 C++ Standard 已经强制要求局部静态对象在第一次被使用时才被构造出来。&lt;strong&gt;这也是mayer’s 单例模式所用到的特性。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;6.1 new 和 delete 运算符 搭配memory1/2笔记一起看&lt;/h1&gt;
&lt;p&gt;我一再强调。new和delete是运算符。malloc和free是C库函数。具体的区别看memory1笔记。&lt;/p&gt;
&lt;p&gt;那就再提一遍。new是先分配内存在调用构造。delete是先析构再释放内存。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次调用new都需要传回一个独一无二的指针。为了解决这个问题，哪怕new的空间是0，也要为其分配1个字节然后传回这1字节内存地址的指针。&lt;/li&gt;
&lt;li&gt;new中是有一个&lt;code&gt;new_handler&lt;/code&gt;的，用来给内存不足的时候进行一次补救的机会。看memory6。&lt;/li&gt;
&lt;li&gt;一般来说new和free底层都用的是C库的malloc和free，尽管没有强制规定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;针对array new 而言：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一般来说，如果new一个数组，数组里面储存的对象没有定义默认析构或者默认构造函数，则不需要调用对象的构造或析构函数。&lt;/li&gt;
&lt;li&gt;我们说过 array new 和 array delete必须成对出现。否则数组中储存的元素的析构函数只会被调用一次。这是为什么？
&lt;ul&gt;
&lt;li&gt;因为获取数组的大小是有额外开销的。所以只有在&lt;code&gt;[]&lt;/code&gt;出现的时候才会去寻找这个维度。否则他就假设只有一个对象是需要被删除的，也就是只调用一次元素的析构函数。&lt;/li&gt;
&lt;li&gt;如果使用array delete去释放单个对象，会进入死循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原始数组和多态天生不兼容。当你对一个指向派生类的基类指针进行 &lt;code&gt;delete [] pbase;&lt;/code&gt; 操作时，它是不会有正确的语意的。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这是由于 delete []实际上会使用 vec_delete()类似的函数调用代替，而在 vec_delete()的参数中已经传递了元素的大小，&lt;strong&gt;在 vec_delete 中的迭代删除时，会在删除一个指针之后将指针向后移动 item_size 个位置，如果 DerivedClass 的 size 比 BaseClass 要大的话（通常都是如此），指针就已经指向了一个未知的区域了&lt;/strong&gt;（如果 Derived 与 Base 大小相同，那碰巧不会发生错误，delete []可以正确的执行）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;针对placement new的语义。&lt;/h2&gt;
&lt;p&gt;我们在memory的笔记中已经了解了。placement operator new是一种可以在已分配内存中放置对象的操作。&lt;strong&gt;也就是它本身并不申请内存，只进行对象构造。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如 我们他的实现可能长这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void* operator new(size_t, void* p){
  return p
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能这个看起来没有逼用，因为他的另一半我们写不出来。&lt;/p&gt;
&lt;p&gt;另一半的可能实现可能长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test* ptr = (test*) obj;
if(ptr != nullptr){
  ptr-&amp;gt;test::test();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，如果此时这段内存上本身有对象，也就是在原来存在的对象上构造新的对象，此时这个原有的对象不会被销毁掉。我们的第一反应可能是这样直接调用delete：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delete ptr;
ptr = new(obj) test;
//obj是已分配的内存的指针。test是构建的对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这个时候有问题：&lt;strong&gt;delete是不仅进行析构，而且释放内存。所以这时候其实已经新分配内存了。所以是错误的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;理论上来讲，应该需要这么写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ptr-&amp;gt;~test(); //显式调用析构函数。
ptr = new(obj) test;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理论上可以使用&lt;code&gt;placement delete&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以placement operator new 应该和placement operator delete 搭配使用。也可以在 placement operator new 出来的对象上显式的调用它的析构函数使得原来的内存又可以被再次使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般而言，placement operator new 并不支持多态。因为子类一般比父类要大。所以已经存在的父类的内存不一定可以装得下子类对象。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;6.3 临时对象&lt;/h1&gt;
&lt;h2&gt;我们已经多次说过临时对象的三种产生时机：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;以值形式传递函数参数&lt;/li&gt;
&lt;li&gt;隐式类型转换&lt;/li&gt;
&lt;li&gt;函数返回一个对象（值返回）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;临时对象的摧毁时机：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;临时对象应该在产生它的完整的表达式的最后一个步骤后被摧毁。切记是完整的表达式，比如一连串的逗号或一堆的括号，只有在完整的表达式最后才能保证这个临时对象在后面不会再被引用到。&lt;/li&gt;
&lt;li&gt;如果一个临时性对象被绑定于一个引用，则对象将残留，直到被这个引用的生命结束，或者直到临时对象的生命范围结束。视哪一种情况先到达而定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：临时性对象的确在一些场合、一定程度上影响了C++的效率。&lt;strong&gt;但是这些影响完全可以通过良好的编码和编译器的积极优化而解决掉临时性对象带来的问题（至少在很大的程度上），所以对临时性对象的影响不能大意但也不必太放在心上。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;7.1 模板&lt;/h1&gt;
&lt;h2&gt;模板的二段式查找和实例化行为&lt;/h2&gt;
&lt;p&gt;首先我们来看一段正常的，没有用模板的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void c(){
    cout&amp;lt;&amp;lt;&quot;global c()&quot;&amp;lt;&amp;lt;endl;
}
class C{
    public: 
        void c() { 
            cout &amp;lt;&amp;lt; &quot;C::c()&quot; &amp;lt;&amp;lt; endl; 
        }
};

class D:public C
{
  public: 
    void g(){
        ::c();//gloabl c()
        c(); 	//C::c()
        this-&amp;gt;c(); //C::c()
      	C::c();	//C::c()
   }
};

int main(){
    D d;
    d.g();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码我们可以清楚地看到。子类可以调用父类函数，也就是他找得到。如果想调用全局函数需要加&lt;code&gt;::&lt;/code&gt;作用域解析运算符强制使用。&lt;/p&gt;
&lt;p&gt;那么我们再看看有模板类的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f(){
    cout&amp;lt;&amp;lt;&quot;global f()&quot;&amp;lt;&amp;lt;endl;
}
template&amp;lt;typename T&amp;gt;
class A{
    public: 
        void f() { 
            cout &amp;lt;&amp;lt; &quot;A::f()&quot; &amp;lt;&amp;lt; endl; 
        }
};

template&amp;lt;typename T&amp;gt;
class B:public A&amp;lt;T&amp;gt;
{
  public: 
    void g(){
        f();//gloabl f() 如果global没有会报错。
        this-&amp;gt;f();//A::f() 查找被延迟了
        A&amp;lt;T&amp;gt;::f();//A::f() 使用限定名来引入依赖性。
        B::f(); //  B::f() 当上面这种方法模板参数很长的时候可以使用这个方法引入依赖性
        // 或者直接引入名称空间
        using space = typename B::A;
        space::f();
   }
};

int main(){
    B&amp;lt;int&amp;gt; b;
    b.g();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现了几个奇怪现象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果全局函数没有&lt;code&gt;f()&lt;/code&gt;，则编译期间会报错。&lt;/li&gt;
&lt;li&gt;如果有了全局函数&lt;code&gt;f()&lt;/code&gt;，默认直接调用会使用全局函数而不是父类函数。&lt;/li&gt;
&lt;li&gt;只有使用了&lt;code&gt;this&lt;/code&gt;指针或者是限定作用域才能调用父类的&lt;code&gt;f()&lt;/code&gt;函数s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编译器在看到一个模板的声明时会做出什么反映呢？实际上编译器没有任何反映！（这部分不同编译器不同。有的会报错有的不会。因为有的没有语法检查。）编译器的反映只有在真正具现化(实例化/使用）时才会发生。明白了这个，就明白了为什么在模板内部有明显的语法错误，编译器也不会报错，除非你要具现化出这个模板的一具实体时编译器才会发出抱怨。但是，凡是和模板参数有关的部分，都必须延迟到真正的实例化发生时才可以。比如由于我们不知道&lt;code&gt;T&lt;/code&gt;类型是什么，&lt;code&gt;T obj = 1024;&lt;/code&gt;这句可能是对的，也可能是错的。所以必须等到我们确定了&lt;code&gt;T&lt;/code&gt;是什么的时候我们才会知道是对的还是错的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是模板实例化的宗旨是&lt;strong&gt;延迟定义&lt;/strong&gt;。也就是&lt;strong&gt;在模板类中，只有某个成员函数真正被使用的时候才会被实例化。这样做有至少两个好处：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空间和时间上的效率。假设我们有一百个函数，我们使用了两种类型，其中一种类型使用了其中三个，另一种使用了其中五个。如果实例化全部的200个（100+100不同类型是不同的实例) 则会消耗很多资源。&lt;/li&gt;
&lt;li&gt;如果使用的类型并不完全支持所有的函数，但是只需要不去用那些不支持函数，这样的部分 具现化就能得以通过编译。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;额外注意：声明一个模板类型的指针是不会引起模板的具现化操作的，因为仅仅声明指针不需要知道class的实际内存布局&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般来说类的名称查找有如下规则&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;１．对于两个非模板继承是可直接继承．
２．对于模板类继承非模板类时，在模板声明进行解析的时候就会进行查找
３．对于继承父类是模板类的会进行名称二段式查找。&lt;/p&gt;
&lt;p&gt;二段式查找有如下两个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板定义阶段（&lt;strong&gt;scope of the template definition&lt;/strong&gt;）：刚被定义时，只有模板中独立的名字（可以理解为和模板参数无关的名字，非依赖名）参加查找&lt;/li&gt;
&lt;li&gt;模板实例化阶段（&lt;strong&gt;scope of the template instantiation&lt;/strong&gt;）：实例化模板代码时，非独立的名字（和模板参数有关的名字，依赖名）才参加查找。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;依赖名和非依赖名在萃取笔记有。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;模板相关的东西非常复杂。可以参考这篇文章：https://www.cnblogs.com/yyxt/p/5150449.html&lt;/p&gt;
&lt;p&gt;所以我们回顾一下为什么在模板类中会出现如此特殊的现象。&lt;/p&gt;
&lt;p&gt;首先进入 &lt;code&gt;B&lt;/code&gt; 的模板定义阶段，此时 &lt;code&gt;B&lt;/code&gt; 的基类 &lt;code&gt;A&amp;lt;T&amp;gt; &lt;/code&gt;依赖于模板参数&lt;code&gt; T&lt;/code&gt;，所以是一个「非独立」的名字。所以在这个阶段，对于 &lt;code&gt;B &lt;/code&gt;来说 &lt;code&gt;A&amp;lt;T&amp;gt; &lt;/code&gt;这个名字是不存在的，因为无法确定其父类类型，于是&lt;code&gt;A&amp;lt;T&amp;gt;::f()&lt;/code&gt;也不存在。因为不会去父类查找。但此时这段代码仍旧是合法的，因为此时编译器可以认为&lt;code&gt; f()&lt;/code&gt; 是一个非成员函数（我们有全局函数&lt;code&gt;f()&lt;/code&gt;的定义）。当稍晚些时候进入 &lt;code&gt;B &lt;/code&gt;的模板实例化阶段时，编译器已经坚持认为&lt;code&gt;f()&lt;/code&gt; 是非成员函数，纵使此时已经可以查到 &lt;code&gt;A&amp;lt;T&amp;gt;::f()&lt;/code&gt;，编译器也不会去这么做。因为他不会去基类查找。&lt;strong&gt;可以理解为在模板定义阶段，对于非依赖名称，链接是静态的，也就是现场决议。没有被实例化出来的父类是不存在的。一个模板子类其实是不能在实例化之前就知道他的模板父类到底是谁。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们如何显示让一个名字从非依赖名变成依赖名？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加&lt;code&gt;this&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用作用域运算符（引入限定名）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上两种方式可以把对应的查找延迟到第二阶段，也就是把名称替换为依赖名。说白了就是显式告诉编译器，东西在父类里。不要在第一阶段查找。&lt;/p&gt;
&lt;p&gt;针对这个问题简而言之:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于数据成员和成员函数，显式地为它们添加前缀，&lt;code&gt;this-&amp;gt;&lt;/code&gt;使它们依赖于模板类的完整定义。&lt;/li&gt;
&lt;li&gt;对于静态成员和成员类型，请使用模板类的名称显式对它们进行范围限定，如果要查找成员类型，请使用&lt;code&gt;typename&lt;/code&gt;前缀 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（如果您想访问静态数据成员或静态成员函数，这两个选项都可用，因为您可以使用&lt;code&gt;this&lt;/code&gt;来访问静态数据成员或静态成员函数。）&lt;/p&gt;
&lt;p&gt;来自:&lt;a href=&quot;https://devblogs.microsoft.com/oldnewthing/20240517-00/?p=109774&quot;&gt;关于模板依赖性的解决办法&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;7.2 异常，有时间再看。&lt;/h1&gt;
&lt;h1&gt;7.3 RTTI 在虚函数表说过了&lt;/h1&gt;
&lt;p&gt;多态类就是有虚函数的类（直接声明或继承）&lt;/p&gt;
&lt;h1&gt;特别注意：&lt;/h1&gt;
&lt;h2&gt;C++隐式生成的 4 大成员函数，在不是真正需要的情况下都不要自己去声明。&lt;/h2&gt;
&lt;p&gt;因为如果是 trivial 的，这些函数不会被真正的合成出来（只存在于概念上），当然也就没有调用的成本了，去提供一个 trivial 的成员反而是不符合效率的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要觉得有构造就一定要有析构。要看实际是否需要。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;构造函数和析构函数中都不具有多态性：这并不是语言的弱点，而是正确的语意所要求的（因为那个时候的对象不完整）&lt;/h2&gt;
</content:encoded></item><item><title>HTTP</title><link>https://sterben-01.github.io/posts/HTTP/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/HTTP/</guid><pubDate>Sat, 20 Aug 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;HTTP请求报文格式&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220809091212.png&quot; alt=&quot;QQ截图20220809091212&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意请求体（请求数据）部分&lt;code&gt;GET&lt;/code&gt;请求没有。&lt;/p&gt;
&lt;h1&gt;HTTP请求方法&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;GET：向指定的资源发出“显示”请求。使用 GET 方法应该只用在读取数据，而不应当被用于产生“副
作用”的操作中，例如在 Web Application 中。其中一个原因是 GET 可能会被爬虫等随意访
问。所以GET请求不应被定义为带有副作用的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HEAD：与 GET 方法一样，都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文
部分。它的好处在于，使用这个方法可以在不必传输全部内容的情况下，就可以获取其中“关于该
资源的信息”（元信息或称元数据）。（&lt;strong&gt;只返回请求头&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;POST：向指定资源提交数据，请求服务器进行处理（例如提交表单或者上传文件）。数据被包含
在请求本文中。这个请求可能会创建新的资源或修改现有资源，或二者皆有。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PUT：向指定资源位置上传其最新内容。但是鉴于PUT方法自身不带验证机制，任何人都可以上传文件，存在安全性问题，因此一般网站都不采用该方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DELETE：请求服务器删除 Request-URI 所标识的资源。同样不带验证机制，存在安全性问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TRACE：回显服务器收到的请求，主要用于测试或诊断。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OPTIONS：这个方法可使服务器传回该资源所支持的所有 HTTP 请求方法。用&apos;*&apos;来代替资源名称，
向 Web 服务器发送 OPTIONS 请求，可以测试服务器功能是否正常运作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CONNECT：HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服
务器的链接（经由非加密的 HTTP 代理服务器）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;GET和POST区别&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;GET 的语义是从服务器获取指定的资源&lt;/strong&gt;， 而&lt;strong&gt;POST 的语义是根据请求负荷（报文body）对指定的资源做出处理（通常导致状态变化或副作用，比如增添或删除数据等）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET&lt;strong&gt;一般&lt;/strong&gt;没有请求体，POST有请求体&lt;/li&gt;
&lt;li&gt;GET参数通过URL传递，而POST参数会放在请求体中，这导致：
&lt;ul&gt;
&lt;li&gt;由于URL有长度限制，所以GET的参数长度有限制，POST没有&lt;/li&gt;
&lt;li&gt;GET参数暴露在URL中，不安全。不可传递敏感信息。这个不安全在传输中因为是HTTPS所以不是很担心，主要担心的是存在在历史记录内。&lt;/li&gt;
&lt;li&gt;由于URL只支持ASCII编码，所以GET编码种类受限。&lt;/li&gt;
&lt;li&gt;由于URL含有参数，历史记录内会保存GET请求参数，但是POST不会&lt;/li&gt;
&lt;li&gt;所以GET的URL可以被当做书签&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GET请求在浏览器回退的时候是无害的，但是POST会再次提交请求，所以会出现浏览器问你是否要继续&lt;/li&gt;
&lt;li&gt;GET请求发送的时候，请求行和请求头和请求体是一起发送的。（理解为没有请求体）。然后服务器一并接受并相应。而POST请求发送的时候，会先发送请求行和请求头，服务器响应100 continue后，再次发送请求体。然后服务器响应。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;HTTP1.1&lt;/h1&gt;
&lt;h2&gt;优点：&lt;/h2&gt;
&lt;h3&gt;支持长连接&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;支持长连接，无需每一个请求都对应一次TCP握手和挥手。可以在一个TCP连接中进行多次请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220810064353.png&quot; alt=&quot;QQ截图20220810064353&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;支持管线化。解决了请求端队头阻塞，但是没有解决响应端的队头阻塞。&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;支持管线化。意思是客户端发出的请求不必排队发送（也就是前一个请求未响应的时候下一个请求无法发送）。&lt;strong&gt;但是服务器必须按照接受请求的顺序返回管线化的请求响应。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;举个例子，非管线化就是1发送，1接受，然后2发送，2接受。管线化就是1发送，2发送。但是服务器返回必须也先返回1再返回2。不可乱序返回。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所以HTTP1.1的管线化解决了请求的队头阻塞（可以同时发送多个请求），但是没有解决响应的队头阻塞（服务器必须按照顺序返回，如果第一个阻塞了，后面的就阻塞了）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220810064729.png&quot; alt=&quot;QQ截图20220810064729&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;引入了host域名&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址，因此，请求消息中的URL并没有传递主机名（hostname）。但随着虚拟主机技术的发展，在一台物理服务器上可以存在多个虚拟主机（Multi-homed Web Servers），并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域，且请求消息中如果没有Host头域会报告一个错误（400 Bad Request）。实现了在一台WEB服务器上可以在同一个IP地址和端口号上使用不同的主机名来创建多个虚拟WEB站点&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;新增了几个状态码所以支持：&lt;/h3&gt;
&lt;h3&gt;支持 Range(断点续传)&lt;/h3&gt;
&lt;h3&gt;支持先发送请求头，等服务器确认后返回100再次发送请求体&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HTTP/1.1加入了一个新的状态码100（Continue）。客户端事先发送一个只带头域的请求，如果服务器因为权限拒绝了请求，就回送响应码401（Unauthorized）；如果服务器接收此请求就回送响应码100，&lt;/li&gt;
&lt;li&gt;客户端就可以继续发送带实体的完整请求了。100 (Continue) 状态代码的使用，允许客户端在发request消息body之前先用request header试探一下server，看server要不要接收request body，再决定要不要发request body。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点：&lt;/h2&gt;
&lt;h3&gt;无状态&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;意思就是没有记忆能力，如果一个数据依靠其他请求，则每次都需要验证。理解为从登陆到下单，每一个页面都需要知道用户身份，则每一次都要登陆一下。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;明文传输&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;没用HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;请求头和响应头无法压缩，只能压缩请求体和响应体&lt;/h3&gt;
&lt;p&gt;因为首部信息是JSON的。没有压缩。所以冗长浪费性能&lt;/p&gt;
&lt;h3&gt;没有请求优先级控制&lt;/h3&gt;
&lt;h3&gt;请求只能由客户端主动发起&lt;/h3&gt;
&lt;h1&gt;HTTP2.0&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220810065919.png&quot; alt=&quot;QQ截图20220810065919&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;优点：&lt;/h2&gt;
&lt;h3&gt;支持头部压缩&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HTTP/2 会&lt;strong&gt;压缩头&lt;/strong&gt;（Header）如果你同时发出多个请求，他们的头是一样的或是相似的，那么，协议会帮你&lt;strong&gt;消除重复的部分&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这就是所谓的 &lt;code&gt;HPACK&lt;/code&gt; 算法：在客户端和服务器同时维护一张头信息表，所有字段都会存入这个表，生成一个索引号，以后就不发送同样字段了，只发送索引号，这样就&lt;strong&gt;提高速度&lt;/strong&gt;了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二进制格式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文，而是全面采用了&lt;strong&gt;二进制格式&lt;/strong&gt;，头信息和数据体都是二进制，并且统称为帧（frame）：&lt;strong&gt;头信息帧（Headers Frame）和数据帧（Data Frame）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据流&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HTTP/2 的数据包不是按顺序发送的，同一个连接里面连续的数据包，可能属于不同的回应。因此，必须要对数据包做标记，指出它属于哪个回应。&lt;/li&gt;
&lt;li&gt;在 HTTP/2 中每个请求或响应的所有数据包，称为一个数据流（&lt;code&gt;Stream&lt;/code&gt;）。每个数据流都标记着一个独一无二的编号（Stream ID），&lt;strong&gt;不同 Stream 的帧是可以乱序发送的（因此可以并发不同的 Stream ）&lt;/strong&gt;，因为每个帧的头部会携带 Stream ID 信息，所以接收端可以通过 Stream ID 有序组装成 HTTP 消息&lt;/li&gt;
&lt;li&gt;客户端和服务器&lt;strong&gt;双方都可以建立 Stream&lt;/strong&gt;， Stream ID 也是有区别的，客户端建立的 Stream 必须是奇数号，而服务器建立的 Stream 必须是偶数号。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;因为使用了数据流，所以可以指定数据流的优先级。支持了优先级控制&lt;/h3&gt;
&lt;h3&gt;多路复用 解决了响应端的队头阻塞&lt;/h3&gt;
&lt;p&gt;HTTP/2 是可以在&lt;strong&gt;一个连接中并发多个请求或回应，而不用按照顺序一一对应&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;移除了 HTTP/1.1 中的串行请求，不需要排队等待，也就不会再出现「队头阻塞」问题，&lt;strong&gt;降低了延迟，大幅度提高了连接的利用率， 解决了响应端的队头阻塞&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举例来说，在一个 TCP 连接里，服务器收到了客户端 A 和 B 的两个请求，如果发现 A 处理过程非常耗时，于是就回应 A 请求已经处理好的部分，接着回应 B 请求，完成后，再回应 A 请求剩下的部分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220810070205.png&quot; alt=&quot;QQ截图20220810070205&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;支持服务器推送&lt;/h3&gt;
&lt;p&gt;HTTP/2 还在一定程度上改善了传统的「请求 - 应答」工作模式，服务端不再是被动地响应，可以&lt;strong&gt;主动&lt;/strong&gt;向客户端发送消息。&lt;/p&gt;
&lt;p&gt;比如，客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件，而 HTML 可能还需要依赖 CSS 来渲染页面，这时客户端还要再发起获取 CSS 文件的请求，需要两次消息往返，如下图左边部分：&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220810070402.png&quot; alt=&quot;QQ截图20220810070402&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图右边部分，在 HTTP/2 中，客户端在访问 HTML 时，服务器可以直接主动推送 CSS 文件，减少了消息传递的次数。&lt;/p&gt;
&lt;h2&gt;缺点：&lt;/h2&gt;
&lt;h3&gt;虽然解决了HTTP层面的发送队头阻塞和响应队头阻塞，但是有TCP层面的队头阻塞&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;HTTP/2 是基于 TCP 协议来传输数据的，TCP 是字节流协议，TCP 层必须保证收到的字节数据是完整且连续的，这样内核才会将缓冲区里的数据返回给 HTTP 应用，那么当「前 1 个字节数据」没有到达时，后收到的字节数据只能存放在内核缓冲区里，只有等到这 1 个字节数据到达时，HTTP/2 应用层才能从内核中拿到数据，这就是 HTTP/2 队头阻塞问题。&lt;/strong&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220810070455.png&quot; alt=&quot;QQ截图20220810070455&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;HTTPS&lt;/h1&gt;
&lt;h2&gt;HTTP 与 HTTPS 有哪些区别？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;HTTP 是超文本传输协议，信息是明文传输，存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷，在 &lt;strong&gt;TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议，使得报文能够加密传输。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;HTTP 连接建立相对简单， TCP 三次握手之后便可进行 HTTP 的报文传输。&lt;strong&gt;而 HTTPS 在 TCP 三次握手之后，还需进行 SSL/TLS 的握手过程，才可进入加密报文传输。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;HTTP 的端口号是 80，HTTPS 的端口号是 443。&lt;/li&gt;
&lt;li&gt;HTTPS 协议需要向 CA（证书权威机构）申请数字证书，来保证服务器的身份是可信的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;HTTPS 解决了 HTTP 的哪些问题？&lt;/h2&gt;
&lt;p&gt;HTTP 由于是明文传输，所以安全上存在以下三个风险：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;窃听风险&lt;/strong&gt;，比如通信链路上可以获取通信内容，用户号容易没。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;篡改风险&lt;/strong&gt;，比如强制植入垃圾广告，视觉污染，用户眼容易瞎。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冒充风险&lt;/strong&gt;，比如冒充淘宝网站，用户钱容易没。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTPS 优点：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;安全性高（废话），不存在窃听，篡改和冒充风险&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTPS缺点：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;延迟较高，因为TCP握手后还需要进行SSL握手。&lt;/li&gt;
&lt;li&gt;部署成本高，需要进行加密计算，部署证书等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;HTTPS是怎么解决风险的？&lt;/h1&gt;
&lt;h2&gt;HTTPS采用了混合加密 和 摘要算法来解决风险&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;混合加密&lt;/strong&gt;的方式实现信息的&lt;strong&gt;机密性&lt;/strong&gt;，解决了窃听的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;摘要算法&lt;/strong&gt;的方式来实现&lt;strong&gt;完整性&lt;/strong&gt;，它能够为数据生成独一无二的「指纹」，指纹用于校验数据的完整性，解决了篡改的风险。&lt;/li&gt;
&lt;li&gt;将服务器公钥放入到&lt;strong&gt;数字证书&lt;/strong&gt;中，解决了冒充的风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;混合加密&lt;/h3&gt;
&lt;p&gt;HTTPS 采用的是&lt;strong&gt;对称加密&lt;/strong&gt;和&lt;strong&gt;非对称加密&lt;/strong&gt;结合的「混合加密」方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在通信建立前采用&lt;strong&gt;非对称加密&lt;/strong&gt;的方式交换「会话秘钥」，后续就不再使用非对称加密。&lt;/li&gt;
&lt;li&gt;在通信过程中全部使用&lt;strong&gt;对称加密&lt;/strong&gt;的「会话秘钥」的方式加密明文数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;采用「混合加密」的方式的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对称加密&lt;/strong&gt;只使用一个密钥，运算速度快，但是密钥必须保密，无法做到安全的密钥交换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非对称加密&lt;/strong&gt;使用两个密钥：公钥和私钥，公钥可以任意分发而私钥保密，解决了密钥交换问题但速度慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;摘要算法+数字签名&lt;/h3&gt;
&lt;p&gt;摘要就是他妈的哈希。&lt;strong&gt;服务器把数据和哈希值一起发出去，然后客户端拿到数据后再算一遍哈希值，如果和服务器发来的一样，能说明数据没有被篡改&lt;/strong&gt;。但是你没办法确认这个数据真的是服务器发的。有可能是别人发的。就是中间人攻击。所以我们会有一个数字证书。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器把自己的公钥注册到CA，CA用自己的私钥给服务器的公钥签名，然后颁发数字证书。数字证书包含着服务器的公钥。CA的公钥是预先保存在浏览器或操作系统内的。所以客户端收到了数字证书后，用CA的公钥给CA私钥加密的数字证书解密**（公钥解密，私钥加密证明不会被冒充）**。这就证明了这个证书确实是CA颁发的。所以也就知道了这个数字证书里面的服务器公钥确实是服务器的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;公钥和私钥 （非对称加密）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;公钥，这个是可以公开给所有人的；&lt;/li&gt;
&lt;li&gt;私钥，这个必须由本人管理，不可泄露。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个密钥可以&lt;strong&gt;双向加解密&lt;/strong&gt;的，比如可以用公钥加密内容，然后用私钥解密，也可以用私钥加密内容，公钥解密内容。&lt;/p&gt;
&lt;p&gt;流程的不同，意味着目的也不相同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;公钥加密，私钥解密&lt;/strong&gt;。这个目的是为了&lt;strong&gt;保证内容传输的安全&lt;/strong&gt;，因为被公钥加密的内容，其他人是无法解密的，只有持有私钥的人，才能解密出实际的内容；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私钥加密，公钥解密&lt;/strong&gt;。这个目的是为了&lt;strong&gt;保证消息不会被冒充&lt;/strong&gt;，因为私钥是不可泄露的，如果公钥能正常解密出私钥加密的内容，就能证明这个消息是来源于持有私钥身份的人发送的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;HTTPS建立流程：&lt;/h1&gt;
&lt;h2&gt;一句话版本：先TCP三次握手，然后TLS四次握手。所以HTTPS首次通信是握手7次&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/unknown.png&quot; alt=&quot;unknown&quot; /&gt;&lt;/p&gt;
&lt;p&gt;SSL/TLS 协议建立的详细流程：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;1. ClientHello&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;首先，由客户端向服务器发起加密通信请求，也就是 &lt;code&gt;ClientHello&lt;/code&gt; 请求。&lt;/p&gt;
&lt;p&gt;在这一步，客户端主要向服务器发送以下信息，&lt;strong&gt;是明文传输的&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;（1）客户端支持的 SSL/TLS 协议版本，如 TLS 1.2 版本。&lt;/p&gt;
&lt;p&gt;（2）客户端生产的随机数（&lt;code&gt;Client Random&lt;/code&gt;），后面用于生成「会话秘钥」条件之一。&lt;/p&gt;
&lt;p&gt;（3）客户端支持的密码套件列表，如 RSA 加密算法。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;2. SeverHello&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;服务器收到客户端请求后，向客户端发出响应，也就是 &lt;code&gt;SeverHello&lt;/code&gt;。服务器回应的内容有如下内容：&lt;/p&gt;
&lt;p&gt;（1）确认 SSL/ TLS 协议版本，如果浏览器不支持，则关闭加密通信。&lt;/p&gt;
&lt;p&gt;（2）服务器生产的随机数（&lt;code&gt;Server Random&lt;/code&gt;），也是后面用于生产「会话秘钥」条件之一。&lt;/p&gt;
&lt;p&gt;（3）确认的密码套件列表，如 RSA 加密算法。&lt;/p&gt;
&lt;p&gt;（4）&lt;strong&gt;服务器的数字证书。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;3.客户端回应&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;客户端收到服务器的回应之后，首先通过浏览器或者操作系统中的 CA 公钥，确认服务器的数字证书的真实性。&lt;/p&gt;
&lt;p&gt;如果证书没有问题，客户端会&lt;strong&gt;从数字证书中取出服务器的公钥&lt;/strong&gt;，然后使用它加密报文，向服务器发送如下信息：&lt;/p&gt;
&lt;p&gt;（1）一个随机数（&lt;code&gt;pre-master key&lt;/code&gt;）。该随机数会被服务器公钥加密。&lt;/p&gt;
&lt;p&gt;（2）&lt;strong&gt;加密通信算法改变通知，表示随后的信息都将用「会话秘钥」加密通信。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（3）客户端握手结束通知，表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要，用来供服务端校验。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上面第一项的随机数是整个握手阶段的第三个随机数&lt;/strong&gt;，会发给服务端，所以这个随机数客户端和服务端都是一样的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务器和客户端有了这三个随机数（Client Random、Server Random、pre-master key），接着就用双方协商的加密算法，各自生成本次通信的「会话秘钥」。因为客户端随机数+服务器随机数+pre-master和加密算法都是协商好一样的，所以会话秘钥也是一样的，所以开始对称加密&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;4. 服务器的最后回应&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;服务器收到客户端的第三个随机数（&lt;code&gt;pre-master key&lt;/code&gt;）之后，通过服务器的私钥解密，拿到&lt;code&gt;pre-master key&lt;/code&gt;。然后计算出本次通信的「会话秘钥」。&lt;/p&gt;
&lt;p&gt;然后，向客户端发送最后的信息：&lt;/p&gt;
&lt;p&gt;（1）加密通信算法改变通知，表示随后的信息都将用「会话秘钥」加密通信。&lt;/p&gt;
&lt;p&gt;（2）服务器握手结束通知，表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要，用来供客户端校验。&lt;/p&gt;
&lt;p&gt;至此，整个 SSL/TLS 的握手阶段全部结束。接下来，客户端与服务器进入加密通信，就完全是使用普通的 HTTP 协议，只不过用「会话秘钥」加密内容。&lt;/p&gt;
&lt;h2&gt;总结：&lt;/h2&gt;
&lt;p&gt;TCP三次握手后，开始TLS四次握手。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发送：客户端随机数，客户端TLS版本，和支持的加密协议。&lt;/li&gt;
&lt;li&gt;服务器发送：服务器随机数，确认的TLS版本，确认的加密协议和服务器证书
&lt;ul&gt;
&lt;li&gt;客户端通过证书验证身份后，拿到服务器公钥。然后生成一个pre-master key。此时客户端可以用客户端随机数+服务器随机数+pre-master key生成会话秘钥。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;客户端发送：被服务器公钥加密过的pre-master key， 加密算法改变通知和客户端握手结束通知
&lt;ul&gt;
&lt;li&gt;服务器端通过私钥解密拿到pre-master key。此时服务器也可以用客户端随机数+服务器随机数+pre-master key生成会话秘钥。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;服务器发送：加密算法改变通知，服务器握手结束通知&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;杂项问题：&lt;/h1&gt;
&lt;h2&gt;HTTPS会加密URL吗 -- 会&lt;/h2&gt;
&lt;p&gt;因为 URL 的信息都是保存在 HTTP Header 中的，而 HTTPS 是会对 HTTP Header + HTTP Body 整个加密的，所以 URL 自然是会被加密的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-09-HTTP.assets/QQ%E6%88%AA%E5%9B%BE20220814072853.png&quot; alt=&quot;QQ截图20220814072853&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;那么HTTPS可以看到域名吗？ -- 可以&lt;/h2&gt;
&lt;p&gt;TLS 第一次握手的 “Client Hello” 消息中，有个 server name 字段，它就是请求的域名地址。&lt;/p&gt;
&lt;h2&gt;关闭keepalive&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;keepalive&lt;/code&gt;必须双方都开启才会生效，只要一方不开启就不生效。&lt;/p&gt;
&lt;p&gt;但是是双方哪方先收到&lt;code&gt;Connection: Close&lt;/code&gt; 则由收到方关闭。&lt;/p&gt;
&lt;p&gt;比如服务器关闭&lt;code&gt;keepalive&lt;/code&gt;，那么写回的请求就是 Connection: Close。浏览器接收到这个后就会调用&lt;code&gt;close&lt;/code&gt;发送&lt;code&gt;FIN&lt;/code&gt;包关闭连接&lt;/p&gt;
</content:encoded></item><item><title>单例模式</title><link>https://sterben-01.github.io/posts/%E5%8D%95%E4%BE%8B/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E5%8D%95%E4%BE%8B/</guid><pubDate>Sat, 20 Aug 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;单例模式&lt;/h1&gt;
&lt;h2&gt;优点：&lt;/h2&gt;
&lt;p&gt;只有一个实例所以不需要每次都创建和销毁，可以在启动的时候就创建对象然后永久驻留在内存中。&lt;/p&gt;
&lt;h2&gt;缺点：&lt;/h2&gt;
&lt;p&gt;有的实现方式是线程不安全的。&lt;/p&gt;
&lt;h2&gt;懒汉式：&lt;/h2&gt;
&lt;p&gt;懒汉式的意思是，&lt;strong&gt;只有类被实例化的时候才会创建这个单例实例&lt;/strong&gt;。意思就是你不能在类里面放一个静态实例了，那样就是饿汉式了。普通的懒汉式是线程不安全的。&lt;/p&gt;
&lt;h3&gt;线程不安全版本：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;numeric&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;map&amp;gt;
#include &amp;lt;set&amp;gt;
using namespace std;

class Student{
	private:
		Student() :age(99) {									//私有化构造
			cout &amp;lt;&amp;lt; &quot;lan_han construct!this age=&quot; &amp;lt;&amp;lt; age &amp;lt;&amp;lt; endl;
		};

		~Student() {											//私有化析构
			cout &amp;lt;&amp;lt; &quot;lan_han destory!&quot; &amp;lt;&amp;lt; endl; 
		};

		Student(const Student&amp;amp;);								//私有化拷贝构造
		Student&amp;amp; operator=(const Student&amp;amp;);						//私有化拷贝赋值
		int age;
		static Student* myInstance;								// 这里是单例对象指针，不是一个实例化对象了。因为是懒汉式，只有用到的时候才会实例化。

	public:
		static Student* getInstance()							//静态函数，返回一个单例实例的指针。
		{
			if (myInstance == nullptr)							//!此处线程不安全。因为可能有多个线程同时到达这一行后时间片切换，其他线程可能已经实例化单例，切换回去后其余线程又会new出来实例。
			{
				myInstance = new Student();							
			}
			return myInstance;//返回对象指针
		}
		void printAge()
		{
			cout &amp;lt;&amp;lt; this-&amp;gt;age &amp;lt;&amp;lt; endl;
		}
	private:
		// 定义一个内部类
		class CGarbo {
		public:
			CGarbo() {};
			// 定义一个内部类的静态对象
			static CGarbo m_garbo;													

			~CGarbo()//对象程序结束析构时对主类指针释放内存
			{
				if (myInstance != nullptr)
				{
					delete myInstance;
					myInstance = nullptr;
				}
			}
		};
};
//记得要初始化静态对象，里面只是定义了他，没有分配内存空间
Student* Student::myInstance = nullptr;						//为静态对象分配内存
Student::CGarbo Student::CGarbo::m_garbo;					//为静态对象分配内存，类的私有静态变量可以通过作用域访问运算符直接访问
int main()
{
	Student* stu_1 = Student::getInstance();
	stu_1-&amp;gt;printAge();

	Student* stu_2 = Student::getInstance();
	stu_2-&amp;gt;printAge();

	return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;私有化构造，析构，拷贝赋值，拷贝构造。&lt;/li&gt;
&lt;li&gt;类内放置一个静态的对象指针。&lt;/li&gt;
&lt;li&gt;类内放置一个静态的成员函数，用作接口来使用户调用，来实例化单例。&lt;/li&gt;
&lt;li&gt;类内放置一个辅助内部类用于回收单例实例资源。
&lt;ul&gt;
&lt;li&gt;辅助类内部有一个静态辅助类实例。&lt;/li&gt;
&lt;li&gt;类外初始化该辅助类实例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;由于单例的意义是，从程序开始到结束，只有一个实例而且一般不提前销毁。静态变量符合该需求。所以开始时初始化，结束时静态对象被析构。静态对象析构的同时判断单例实例是否已经被销毁，如果没有被销毁则去销毁单例实例。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类外初始化单例的静态对象指针。&lt;/li&gt;
&lt;li&gt;注意，这个版本线程不安全。
&lt;ul&gt;
&lt;li&gt;首先判断我们类外初始化的静态变量是否是nullptr，如果是的话证明该单例从未被实例化过，则可以new出实例然后返回指针。&lt;/li&gt;
&lt;li&gt;因为可能有多个线程同时到达这一行后时间片切换，其他线程可能已经实例化单例，切换回去后其余线程又会new出来实例。所以不安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所以，我们直观地想到，加锁不就完了吗？对，但是不全对。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程安全版本：&lt;/h3&gt;
&lt;p&gt;我们只需要修改一下获取实例的代码部分即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static Student* getInstance()							//静态函数，返回一个单例实例的指针。
{	
    my_mutex.lock();									//加锁
    if (myInstance == nullptr)							//此时线程安全。
    {
        myInstance = new Student();							
    }
    my_mutex.unlock();									//解锁
    return myInstance;//返回对象指针
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们发现一个问题，也就是我们使用实例也是通过&lt;code&gt;getinstance&lt;/code&gt;来的。也就是我们如果每次想要新建一个指针来获取到指向实例的指针，都需要调用&lt;code&gt;getInstance&lt;/code&gt;。然后每次都加锁释放锁是非常非常浪费资源的。&lt;/p&gt;
&lt;p&gt;我们同时发现，出现线程不安全的时机几乎只是一开始创建的时候。所以我们发明了双检锁&lt;/p&gt;
&lt;h3&gt;线程安全版本，双检锁：&lt;/h3&gt;
&lt;p&gt;我们只需要修改获取示例代码的部分即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static Student* getInstance()							//静态函数，返回一个单例实例的指针。
{	
    if (myInstance == nullptr)							//先判断是否为空
    {
        my_mutex.lock();								//确定空后再加锁
        if(myInstance == nullptr){						//再次判断是否为空
            myInstance = new Student();					//实例化
        }	
        my_mutex.unlock();								//解锁
    }

    return myInstance;//返回对象指针
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们分析一下这段代码。首先，我们外层的判断目的是为了在已经实例化后，直接返回对象指针。**意思就是，我们已经实例化后，不需要加锁去检查是否已经实例化了。再次加锁就是浪费资源。**所以如果已经实例化了，我们再次调用的时候发现非空就可以直接返回对象指针了。&lt;/p&gt;
&lt;p&gt;然后内层的原因就是，我们会有多个线程想去实例化单例。这时候加锁，实例化，解锁。第二个等候锁的线程拿到锁后，发现已经为非空了，此时可以直接返回。所以线程安全。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是，双检锁依旧可能失效，原因就是指令重排。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myInstance = new Student();	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行代码，背后是三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为单例对象分配空间&lt;/li&gt;
&lt;li&gt;调用单例对象的构造函数来构造对象。&lt;/li&gt;
&lt;li&gt;让&lt;code&gt;myInstance&lt;/code&gt; 指向分配的空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;但是编译器并不保证这三者的顺序。2和3可能会被交换顺序。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以有可能发生下面的事情：线程A先分配了空间，然后交换了2和3导致直接让指针指向了分配的空间，这时时间片到期！此时分配的空间并没有实际构造出单例的实例！然后线程B进来了！它直接判断了现在指针是否为空，非常可惜，此时指针不为空。所以线程B直接返回了一个指向空内存的指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解决这个的办法有很多，比如&lt;code&gt;pthread_once&lt;/code&gt;，内存屏障（&lt;code&gt;atomic&lt;/code&gt; + &lt;code&gt;std::memory_order_acquire&lt;/code&gt;)，volatile（非跨平台）等等。&lt;/p&gt;
&lt;p&gt;但是这种内存屏障的代码会非常复杂，有没有什么其他的方式允许我们实现一个线程安全的单例模式呢？&lt;/p&gt;
&lt;h2&gt;饿汉式：&lt;/h2&gt;
&lt;p&gt;饿汉式的意思就是，一开始就有一个实例被创建出来，而不是等待类被实例化的时候才创建。意思就是你要在类里面放一个该类的静态实例。这样程序一旦启动就会有实例创建。&lt;strong&gt;线程安全&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student
{
private:
    Student() :age(99) {                                            //私有化构造
        cout &amp;lt;&amp;lt; &quot;e_han construct! age=&quot; &amp;lt;&amp;lt; age &amp;lt;&amp;lt; endl;             
    };

    ~Student() {                                                    //私有化析构
        cout &amp;lt;&amp;lt; &quot;e_han destory!&quot; &amp;lt;&amp;lt; endl; 
    };

    
    Student(const Student&amp;amp;);                                        //私有拷贝构造
    Student&amp;amp; operator=(const Student&amp;amp;);                             //私有拷贝赋值
    static Student myInstance;                                      //单例对象在这里！


    int age;

public:
    static Student* getInstance()
    {
        return &amp;amp;myInstance;                                         //返回单例实例的指针，此处是取地址
    }
    void printAge()
    {
        cout &amp;lt;&amp;lt; this-&amp;gt;age &amp;lt;&amp;lt; endl;
    }
};

Student Student::myInstance;                                        //为静态对象分配内存
int main()
{
    Student* stu_1 = Student::getInstance();
    stu_1-&amp;gt;printAge();
    //对象是全局一开始就建立好的，两个函数获取到的是同一个对象（地址相同）
    Student* stu_2 = Student::getInstance();
    stu_2-&amp;gt;printAge();
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个就简单很多，因为不涉及到&lt;code&gt;new&lt;/code&gt;所以也不涉及资源回收。而且是线程安全的。&lt;/p&gt;
&lt;p&gt;但是潜在问题在于 &lt;strong&gt;no-local&lt;/strong&gt; static对象（函数外的static对象）在不同编译单元（可理解为cpp文件和其包含的头文件）中的初始化顺序是未定义的。如果在初始化完成之前调用 &lt;code&gt;getInstance()&lt;/code&gt; 方法会返回一个未定义的实例，比如一个全局变量的构造函数中调用了此方法。&lt;/p&gt;
&lt;h2&gt;Meyers Singleton （也是懒汉式的一种）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;
class Student {
   private:
    Student() : age(99) {  // 私有构造
        cout &amp;lt;&amp;lt; &quot;Student construct! age=&quot; &amp;lt;&amp;lt; age &amp;lt;&amp;lt; endl;
    };
    ~Student() {  // 私有析构
        cout &amp;lt;&amp;lt; &quot;Student destory!&quot; &amp;lt;&amp;lt; endl;
    };
    int age;
    Student(const Student&amp;amp;) = delete;             // 禁用拷贝构造
    Student(Student&amp;amp;&amp;amp;) = delete;                  // 禁用移动构造
    Student&amp;amp; operator=(const Student&amp;amp;) = delete;  // 禁用拷贝赋值

   public:
    static Student&amp;amp; getInstance()  // 注意是返回单例实例对象的引用
    {
        static Student
            myInstance;  // 注意这里，这里是局部静态变量而不是类成员变量。局部静态变量的生存周期是从声明起至程序结束。
        return myInstance;  // 返回实例对象
    }
    void printAge() { cout &amp;lt;&amp;lt; this-&amp;gt;age &amp;lt;&amp;lt; endl; }
};
int main() {
    Student&amp;amp; stu_1 = Student::getInstance();  // 注意这里要用引用去接。
    stu_1.printAge();
    Student&amp;amp; stu_2 = Student::getInstance();
    stu_2.printAge();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意。这里私有析构会造成如果实例化对象，编译时会提示对象无法析构&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这里禁用拷贝构造。这样的话调用拷贝构造的时候编译器会提示错误。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果提供了拷贝构造的定义，就算是私有，也可以调用成功。因为调用的函数是类的成员函数，尽管是静态。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在c++11后，当变量在初始化的时候，如果多线程并发同时进入声明语句，并发线程将会阻塞等待初始化结束。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;具体原因可以参考这里。https://stackoverflow.com/questions/17712001/how-is-meyers-implementation-of-a-singleton-actually-a-singleton&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;类内的函数内的变量是局部变量，不算做类的成员，也不占用类的大小。静态也是这样。而且局部静态变量的生存周期是从声明（但是必须程序执行到该对象的声明处）起至程序结束。在这里就是从函数调用开始到程序结束。所以正是利用了这个特性既保证了线程安全，又是懒汉式，又是全局生存周期。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;如果针对这种单例模式，我们返回对象会发生什么？也就是局部静态变量的拷贝&lt;/h2&gt;
&lt;p&gt;静态局部对象会正常拷贝。&lt;/p&gt;
&lt;p&gt;假设我们把单例模式的所有函数都设置为public。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static Student getInstance()				
{   
    static Student myInstance;  //除第一次外，其余调用编译器会忽略
    return myInstance;	//正常拷贝。调用拷贝构造    		
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道，局部静态变量只允许也只会被初始化一次。如果发现已经被初始化了则会跳过。所以&lt;code&gt;static Student myInstance;&lt;/code&gt;只有在第一次经过的时候会初始化。剩下的调用中会忽略这一行。但是&lt;code&gt;return myInstance;	&lt;/code&gt;会正常拷贝。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student stu_1 = Student::getInstance();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们用值去接。则这个过程会拷贝构造两次。有编译器优化就是一次。&lt;/p&gt;
&lt;p&gt;第一次是作为临时对象拷贝出函数。第二次是临时对象拷贝至外部对象。然后临时对象销毁。&lt;/p&gt;
&lt;h3&gt;其他实现方式：&lt;/h3&gt;
&lt;p&gt;https://www.cnblogs.com/liyuan989/p/4264889.html&lt;/p&gt;
&lt;h3&gt;类的私有静态变量可以通过作用域访问运算符直接访问&lt;/h3&gt;
&lt;h3&gt;main函数结束也会为栈对象调用析构，但是不会为堆对象调用。&lt;/h3&gt;
&lt;h3&gt;单例模式可以把构造函数设置为protected以允许子类继承&lt;/h3&gt;
&lt;h3&gt;现在的 C++ Standard 已经强制要求局部静态对象在第一次被使用时才被构造出来。&lt;/h3&gt;
&lt;h3&gt;局部静态变量只允许也只会被初始化一次。如果发现已经被初始化了则会跳过。&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在&lt;strong&gt;块作用域&lt;/strong&gt;声明且带有 &lt;code&gt;static&lt;/code&gt; 或 &lt;code&gt;thread_local&lt;/code&gt; (C++11 起) 说明符的变量拥有静态或线程 (C++11 起)存储期，但在控制首次经过它的声明时才会被初始化（除非它被&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/zero_initialization&quot;&gt;零初始化&lt;/a&gt;或&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/constant_initialization&quot;&gt;常量初始化&lt;/a&gt;，这可以在首次进入块前进行）。在其后所有的调用中，声明都会被跳过。&lt;/p&gt;
&lt;p&gt;如果初始化&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/throw&quot;&gt;抛出异常&lt;/a&gt;，那么不认为变量被初始化，且控制下次经过该声明时将再次尝试初始化。&lt;/p&gt;
&lt;p&gt;如果初始化递归地进入正在初始化的变量的块，那么行为未定义。&lt;/p&gt;
&lt;p&gt;如果多个线程试图同时初始化同一静态局部变量，那么初始化严格发生一次（类似的行为也可对任意函数以 &lt;a href=&quot;https://zh.cppreference.com/w/cpp/thread/call_once&quot;&gt;std::call_once&lt;/a&gt; 来达成）。
&lt;strong&gt;注意：此功能特性的通常实现均使用双检查锁定模式的变体，这使得对已初始化的局部静态变量检查的运行时开销减少为单次非原子的布尔比较。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意，块作用域（block scope）不是类作用域（class scope）。只有块作用域声明且带有&lt;code&gt;static&lt;/code&gt;的才叫局部静态变量。静态类成员变量不是局部静态变量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态成员变量的生存周期是从程序开始到程序结束。和全局变量，全局静态变量一致。而静态局部变量是在控制首次经过它的声明时才会被初始化，然后到程序结束为止。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>项目相关</title><link>https://sterben-01.github.io/posts/%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3/</guid><pubDate>Sat, 20 Aug 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;项目相关&lt;/h1&gt;
&lt;h1&gt;UDP TCP&lt;/h1&gt;
&lt;p&gt;UDP不需要监听，自然服务端没有listen，UDP是无连接，&lt;strong&gt;在无连接需求下&lt;/strong&gt;自然客户端没有connect，服务端也没有accept&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UDP可以调用connect函数&lt;/strong&gt;。但是和TCP意义不同。**UDP使用connect之后，内核仅仅会把对端ip&amp;amp;port记录下来。这样链接就变成了一对一的。**也就是可以使用TCP的那一套系统调用了。**但是UDP自己本身还是UDP，依旧不可靠。但是此时效率会变高，**因为普通的UDP在一对多的时候因为没有记录端口和地址，则每次需要调用&lt;code&gt;sendto&lt;/code&gt;函数。参数更多。但是在一对一的时候只需要使用&lt;code&gt;send&lt;/code&gt;。这样传入的参数变少了，这意味着不需要重复分配和释放内存储存连接信息，可以永久维护一个链接信息的结构。&lt;/li&gt;
&lt;li&gt;https://blog.51cto.com/u_15346415/3674455&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/unknown.png&quot; alt=&quot;unknown&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;同步模型&lt;/h1&gt;
&lt;p&gt;例子：你是一个老师，让学生做作业，学生做完作业后收作业。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同步阻塞：逐个收作业，先收A，再收B，接着是C、D，如果有一个学生还未做完，则你会&lt;strong&gt;等到他写完，然后才继续收下一个&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;同步非阻塞：逐个收作业，先收A，再收B，接着是C、D，如果有一个学生还未做完，则你会&lt;strong&gt;跳过该学生，继续去收下一个，收完了一圈再过来问学生写完没，如果没写完就再问一圈。&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;select/poll：&lt;strong&gt;学生写完了作业会举手，但是你不知道是谁举手，需要一个个的去询问&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;epoll：&lt;strong&gt;学生写完了作业会举手，你知道是谁举手，你直接去收作业&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;同步阻塞：&lt;/h1&gt;
&lt;p&gt;服务器执行到&lt;code&gt;accept&lt;/code&gt;的时候，会阻塞，等待直到建立连接为止。服务器执行到&lt;code&gt;receive（Read）&lt;/code&gt;部分的时候，也会阻塞，等待客户端发送数据。直到客户端发送完数据了才能继续。在这期间所有其他客户端想要建立链接都是不可以的，因为被阻塞在了客户端的&lt;code&gt;connect&lt;/code&gt;这里。对于这种情况我们可以为每一个&lt;code&gt;connect&lt;/code&gt;新建一个线程/进程。但是会消耗大量资源。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;写的部分也一样。会阻塞。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;单线程：某个 socket 阻塞，会影响到其他 socket 处理。&lt;/p&gt;
&lt;p&gt;多线程：客户端较多时，会造成资源浪费，全部 socket 中可能每个时刻只有几个就绪。同时，线程的调度、上下文切换乃至它们占用的内存，可能都会成为瓶颈。&lt;strong&gt;多线程解决的方式是在主线程&lt;code&gt;accept&lt;/code&gt;后，新开一个线程对应一个客户端，读和写都在新的线程执行。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个socket（文件描述符）是否设置为阻塞模式，只会影响到&lt;code&gt;connect/accept/send/recv&lt;/code&gt;等四个&lt;code&gt;socketAPI&lt;/code&gt;函数，不会影响到 &lt;code&gt;select/poll/epoll_wait&lt;/code&gt;函数，后三个函数的超时或者阻塞时间是由其函数自身参数控制的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;同步非阻塞（忙轮询）&lt;/h1&gt;
&lt;p&gt;我们可以把&lt;strong&gt;文件描述符&lt;/strong&gt;设为非阻塞。文件描述符设置为非阻塞可以解决&lt;code&gt;accept/receive(read)/send(write)&lt;/code&gt;的阻塞。在非阻塞模式，所有的建立链接/读/写如果不能执行，则不会阻塞而是继续（循环）执行。所以我们可以把监听文件描述符设置为非阻塞来解决accept阻塞，也可以把读写文件描述符设置为非阻塞解决read write的阻塞。当监听文件描述符检测到连接后，既可以在主线程处理后续逻辑，也可以单开新线程进行处理。但是这种循环执行有个问题。每次循环都需要调用系统调用来向内核询问是否有数据就绪，会频繁切换内核态和用户态。浪费资源。&lt;/p&gt;
&lt;h1&gt;同步非阻塞和异步的区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;非阻塞I/O要不停的调用I/O函数来检查数据是否来，如果数据来了，就得卡在I/O函数这里把数据从内核缓冲区复制到用户缓冲区，然后这个函数才能返回&lt;/li&gt;
&lt;li&gt;异步I/O根本不需要不停的调用I/O函数来检查数据是否到来，只需要调用一次，然后就可以干别的事情去了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没用用到特殊API的都是同步。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;select，poll， epoll都是同步IO&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;IO多路复用&lt;/h1&gt;
&lt;h2&gt;SELECT 底层是数组&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;首先，SELECT会拷贝一份我们需要监听的文件描述符集合到内核空间。然后内核帮助我们进行遍历，判断对应的文件描述符是否有修改动作。如果有就给对应的位置置为1，没有就会置为0哪怕原来是1。最后返回有几个事件就绪&lt;/strong&gt;。所以这个文件描述符集合每次会被修改。这就是为什么我们代码有两份文件描述符集合。但是假如我们有4个客户端 100 101 102 103需要检测。所以100-103的位置都是1。我们如果此时只有100 和 101 被修改了，那么这个数组里面将只有100 和 101是1。 102和103将会被置为0，不会被继续监听。所以我们有两个数组。一个是原始的只可以被set和clr进行手动设置和归零的。另一个是给内核的，内核可以修改的。所以我们select的时候给内核可修改版本。set和clr还是修改原始版本。然后在每一次的while循环一开始，更新内核版本为原来的需要监听的那几个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由于是内核帮助我们进行遍历，然后修改原文件描述符数组。所以最后我们还是要自己遍历一遍文件描述符数组找到到底哪几个文件描述符就绪了。也就是会重复遍历一次。最后我们还需要将自己原来的监听文件描述符数组拷贝回给让内核修改的那份（入参的那一份）&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;优点：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不需要每个 FD 都进行一次系统调用，解决了我们自己使用同步非阻塞的时候频繁使用系统调用导致的频繁的用户态内核态切换问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缺点：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;有文件描述符数组的拷贝动作，（从用户空间拷贝到内核空间）消耗很大。而且是直接修改后拷贝回用户空间（整个数组全都拷贝）。所以需要两个数组，每次循环后都需要把我们自己保留的一份拷贝给内核的那一份里面。&lt;/li&gt;
&lt;li&gt;有最大描述符限制&lt;/li&gt;
&lt;li&gt;内核帮助我们遍历文件描述符数组的时候是线性遍历。所以效率较低&lt;/li&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt;调用后，我们还是需要再次遍历一次文件描述符数组，找出具体修改的文件描述符。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SELECT的文件描述符数组其实是bitmap。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;请注意。我们所谓的拷贝至内核指的是，将函数的参数拷贝至内核栈。普通参数传递的时候是直接把参数值入栈（用户态或者内核态）。但是系统调用的时候，由于内核不能相信任何用户空间的指针，所以会先把参数写入至寄存器，然后再把参数从寄存器拷贝至内核栈。所以我们SELECT会先把数组拷贝到内核空间，修改后再拷贝回用户空间。&lt;/h3&gt;
&lt;h2&gt;POLL 底层是链表&lt;/h2&gt;
&lt;p&gt;和&lt;code&gt;select&lt;/code&gt;的两个区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去掉了最大监听描述符数量的1024限制&lt;/li&gt;
&lt;li&gt;不再需要每次重置监听描述符数组（重新赋值）&lt;strong&gt;因为POLL的监听文件描述符数组实际上每个元素是储存了多种信息的结构体，类似&lt;code&gt;epoll&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POLL依旧有把FD数组拷贝至内核 和 回参需要遍历的问题。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct pollfd{
    int		fd;			//委托检测的文件描述符
    short	events;		//委托检测的事件
    short	revents;	//实际发生的事件
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;内核会修改&lt;code&gt;revents&lt;/code&gt;而不会修改&lt;code&gt;events&lt;/code&gt;。这是主要区别&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;EPOLL 底层是红黑树&lt;/h1&gt;
&lt;p&gt;使用EPOLL的时候我们会先使用&lt;code&gt;epoll_create&lt;/code&gt;创建一个epoll实例（eventpoll)。&lt;strong&gt;但是这个实例被创建在了内核区&lt;/strong&gt;。创建后会返回一个epoll的文件描述符。我们正是通过这个文件描述符操作这个epoll实例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;我们所有要求epoll监听的文件描述符被储存在红黑树内。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有一个就绪列表，用来添加所有已经就绪的文件描述符。这是一个双向链表（也有人说是队列）。这里之后会把里面的东西复制回用户空间的一个用于接收就绪文件描述符的数组（&lt;code&gt;epoll_wait&lt;/code&gt;的第二个参数）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有一个等待队列。如果调用时没有时间就绪，就会阻塞，放入等待队列让出CPU以便后续唤醒。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们每次使用&lt;code&gt;epoll_ctl&lt;/code&gt;将一个需要监听的文件描述符添加至&lt;code&gt;eventpoll&lt;/code&gt;的时候，会被封装成&lt;code&gt;epitem&lt;/code&gt;后拷贝至内核空间，仅需拷贝一次。因为他会一直存在在里面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一个&lt;code&gt;epitem&lt;/code&gt;也有一个等待队列，它会关联至一个回调函数(&lt;code&gt;ep_callback&lt;/code&gt;)。也就是这个&lt;code&gt;epitem&lt;/code&gt;对应的文件描述符就绪时，会调用这个回调函数来唤醒进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epoll_wait&lt;/code&gt;的作用是获取就绪的文件描述符。所以项目里面这个函数是放在while里面的。因为每次获取就绪的文件。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;epoll_wait&lt;/code&gt;形参没有poll或者是select的那种传入的监听文件描述符数组了。而是我们之前创建的&lt;code&gt;epoll&lt;/code&gt;文件描述符。所以没有数组拷贝这个流程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;接收流程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;服务端通过网卡接收到客户端消息。&lt;/li&gt;
&lt;li&gt;网卡通过DMA写入内存。&lt;/li&gt;
&lt;li&gt;发送中断信号给CPU，表示有数据到达。&lt;/li&gt;
&lt;li&gt;CPU调用中断处理程序处理。通过数据包的IP和端口号找到对应的socket套接字（文件描述符）。&lt;/li&gt;
&lt;li&gt;将数据放入对应socket（文件描述符）的接收队列。&lt;/li&gt;
&lt;li&gt;处理后会找到对应&lt;code&gt;epitem&lt;/code&gt;的等待队列关联的回调函数（&lt;code&gt;ep_poll_callback&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;回调函数会将&lt;code&gt;epitem&lt;/code&gt;添加至就绪列表。&lt;/li&gt;
&lt;li&gt;唤醒在等待队列中的进程。（如果进程处于睡眠状态）&lt;/li&gt;
&lt;li&gt;进程判断就绪列表是否有就绪事件。&lt;/li&gt;
&lt;li&gt;如果有就绪事件，&lt;strong&gt;拷贝&lt;/strong&gt;至用户空间的一个用于接收就绪文件描述符的数组（&lt;code&gt;epoll_wait&lt;/code&gt;的第二个参数）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;epoll仅当添加监听的文件描述符（拷贝至内核空间） 和 有对应的就绪事件时（拷贝回用户空间）会发生拷贝。&lt;/h3&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;epoll对象一直被维护在内核态，所以仅有添加文件描述符时需要进行拷贝&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有就绪列表，所以可直接获知就绪事件（文件描述符）。无需重复遍历&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监听文件描述符数组储存在红黑树，搜索/添加/删除速度快&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;仅支持linux。跨平台性差。&lt;/li&gt;
&lt;li&gt;比select复杂，移植性差。&lt;/li&gt;
&lt;li&gt;在监听连接数和事件较少的情况下，select/poll可能更优。因为比较简单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;LT（水平）/ET（边缘）触发区别（其实就是回调函数触发次数的区别）&lt;/h2&gt;
&lt;p&gt;LT模式的时候，只要&lt;code&gt;epoll_wait&lt;/code&gt;检测到事件没有被处理完毕（比如没有读完），那么后续每次&lt;code&gt;epoll_wait&lt;/code&gt;调用都会通知。（只要缓冲区有数据就一直触发）&lt;/p&gt;
&lt;p&gt;ET模式的时候，&lt;code&gt;epoll_wait&lt;/code&gt;检测到事件后，仅通知一次。直到下次再次检测到事件后才继续通知。（就算没读完，也要等到下次新的事件到来后才能继续处理）（直到缓冲区数据有增加（变化）才会触发）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ET模式下可以通知很多次。监听socket不用设置为oneshot是因为只存在一个主线程去操作这个监听socket&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;我们为什么使用ET&lt;/h2&gt;
&lt;p&gt;ET减少了重复触发次数，效率会高一些。&lt;/p&gt;
&lt;h2&gt;ONESHOT&lt;/h2&gt;
&lt;p&gt;oneshot指的某socket对应的fd事件最多只能被检测一次，不论你设置的是读写还是异常。&lt;/p&gt;
&lt;p&gt;因为可能存在这种情况：如果epoll检测到了读事件，数据读完交给一个子线程去处理，&lt;/p&gt;
&lt;p&gt;如果该线程处理的很慢，在此期间epoll在该socket上又检测到了读事件，则又给了另一个线程去处理，&lt;/p&gt;
&lt;p&gt;则在同一时间会存在两个工作线程操作同一个socket。&lt;/p&gt;
&lt;p&gt;EPOLLONESHOT这种方法，可以在epoll上注册这个事件，注册这个事件后，如果在处理完毕当前的socket后不再重新注册相关事件，&lt;/p&gt;
&lt;p&gt;那么这个事件就不再响应了或者说触发了。&lt;/p&gt;
&lt;p&gt;当处理完毕想要通知epoll可以再次处理的时候就要调用epoll_ctl重新注册(重置)文件描述符上的事件。这样前面的socket就不会出现竞态&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;也就是说注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕， 该线程就应该立即重置这个 socket 上的 EPOLLONESHOT 事件，以确保这个 socket 下一次可读时，其 EPOLLIN 事件能被触发，进而让其他工作线程有机会继续处理这个 socket。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;https://www.jxhs.me/2021/04/08/linux%E5%86%85%E6%A0%B8Epoll-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/&lt;/p&gt;
&lt;h2&gt;为什么使用ET一定要设置为非阻塞&lt;/h2&gt;
&lt;p&gt;因为我们ET是一个事件只通知一次，所以为了效率我们必须一次性读完或者写完。我们会在一个单独的while里面进行循环读或者循环写。但是如果我们是阻塞的文件描述符，假设我们在读。我们循环了四次读完了，然后发现没有东西读了。就会一直卡在这直到有新的东西能读。也就是阻塞读的while循环没有退出条件。我们如果设置为非阻塞的话，发现没有数据了会返回一个ERRNO，这样就是有退出条件了。我们就可以&lt;code&gt;break&lt;/code&gt;退出循环。写也是一个道理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;尤其是我们这里模拟了preactor模式，用的是主线程进行消息的读取主线程把消息读完了交给子线程去解析，子线程写好了打包好了数据后交给主线程发送。所以read的时候如果用了阻塞的文件描述符，读完了就一直卡在那，也无法接受新链接。彻底卡死。所以设置为非阻塞读，当没有数据的时候不会卡在那，会返回一个错误代码，我们根据错误代码来退出读取的while循环。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而且，如果是阻塞的，&lt;strong&gt;不循环读&lt;/strong&gt;的话会干扰下一次发送的数据，会有上次读不完的和下一次的数据混在一起的事情发生。注意，阻塞与否是要看有没有while。阻塞的核心原因是read函数在阻塞模式下，&lt;strong&gt;循环读&lt;/strong&gt;的话没有退出条件。所以：&lt;/p&gt;
&lt;p&gt;ET模式下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞不循环：干扰下一次发送的数据&lt;/li&gt;
&lt;li&gt;阻塞循环：卡死&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以一定要设置非阻塞，然后循环读取。这样既可以保证一次性读完，不会发生数据混乱，也保证了有退出条件。&lt;/p&gt;
&lt;p&gt;LT模式下：&lt;/p&gt;
&lt;p&gt;因为LT是每次都触发，所以我们不需要设置循环读。我们只要告诉他每次读取几个字节即可。他会一直每次都读取对应的字节数量直到读取完毕。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞：一直读直到读完。没问题&lt;/li&gt;
&lt;li&gt;非阻塞：一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以LT模式没什么特别区别&lt;/p&gt;
&lt;h2&gt;如果把监听文件描述符&lt;code&gt;listenFD&lt;/code&gt;设置为边缘触发(ET)的话应该怎么办&lt;/h2&gt;
&lt;p&gt;如果这样的话，我们也需要把&lt;code&gt;listenFD&lt;/code&gt;外面加一个&lt;code&gt;while&lt;/code&gt;循环来循环&lt;code&gt;accept&lt;/code&gt;所有的链接，直到返回-1而且&lt;code&gt;errno == EAGAIN&lt;/code&gt;。不然的话高并发的时候你只处理一个之后他直到新的链接进来都不会通知。服务器无法处理，会堵在这，这样会丢链接。&lt;/p&gt;
&lt;p&gt;LT模式的监听文件描述符只要&lt;code&gt;accept&lt;/code&gt;返回不是错误就可以直接建立链接，然后等待下次通知。（只要存在就通知）&lt;/p&gt;
&lt;h2&gt;recv和send函数作用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;recv和send的作用只是按照规则拷贝而已。从socket缓冲区拷贝到用户缓冲区而已。不管进程是否调用recv()读取socket，对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。recv()所做的工作，就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面，并返回，仅此而已。send（）也一样。所以send将数据拷贝至socket内核发送缓冲区（协议的缓冲区）后会直接返回，而这个时候数据不一定已经成功发送，因为send只负责拷贝。发送是TCP的事情。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;read/write readv/writev 和 recv/send 都是IO函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;recv&lt;/code&gt;/&lt;code&gt;send&lt;/code&gt;比&lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;的功能稍多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意看好了这里和TCP的联系！！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;socket内核接受缓冲区被TCP用来缓存网络上接收到的数据，一直保存到应用进程读走为止。如果应用进程一直没有调用&lt;code&gt;read&lt;/code&gt;或者&lt;code&gt;recv&lt;/code&gt;读取，数据会一直缓存在socket的接受缓冲区内。**接受缓冲区满了以后，发生的动作是：接收端通知发送端，接收窗口关闭（win=0）。这个便是滑动窗口上的实现。保证TCP套接口接受缓冲区不会溢出，从而保证了TCP是可靠传输。**因为对方不允许发出超过所通告窗口大小的数据。这就是TCP的流量控制，如果对方无视窗口大小而发出了超过窗口大小的数据，则接收方TCP将丢弃它。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;I/O缓冲区在每个TCP套接字中单独存在；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;I/O缓冲区在创建套接字时自动生成；&lt;/li&gt;
&lt;li&gt;即使关闭套接字也会继续传送输出缓冲区中遗留的数据；&lt;/li&gt;
&lt;li&gt;关闭套接字将丢失输入缓冲区中的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;recv的工作原理：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;recv先检查套接字的接收缓冲区，如果该接收缓冲区中没有数据或者&lt;strong&gt;协议正在接收数据&lt;/strong&gt;，那么recv就一直等待，直到协议把数据接收完毕。当协议把数据接收完毕，recv函数就把套接字的接收缓冲区中的数据拷贝到用户层的buffer中，（注意：&lt;strong&gt;协议接收到的数据可能大于用户区的buffer的长度，所以在这种情况下，要调用几次recv函数才能把套接字接收缓冲区中的数据拷贝完。或者是需要接收的数据比socket的buffer还要大，那么需要循环读取，每次读取空出socket的buffer后才能继续接收。&lt;/strong&gt;）recv函数仅仅是拷贝数据，真正的接收数据是协议来完成的。&lt;/li&gt;
&lt;li&gt;recv函数返回其实际拷贝的字节数。如果recv在拷贝时出错，那么就返回SOCKET_ERROR；如果recv函数在等待协议接收数据时网络中断了，那么它返回0。对方优雅的关闭socket并不影响本地recv的正常接收数据，如果协议缓冲区内没有数据，recv返回0，指示对方关闭；如果协议缓冲区有数据，则返回对应数据（可能需要多次recv），在最后一次recv时，返回0，指示对方关闭。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以我们项目里面在读取的时候有一个下标&lt;code&gt;m_read_index = m_read_index + bytes_read;&lt;/code&gt;是因为可能多次调用recv读取然后拼接到缓冲区上。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;循环recv的原因是我们不知道具体有多少东西要发送过来。我们需要非阻塞循环读取直到返回-1即没有东西可以读取了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;recv返回值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;0 表示读到了EOF也就是对端关闭&lt;/li&gt;
&lt;li&gt;-1 表示发生错误
&lt;ul&gt;
&lt;li&gt;我们项目中因为是非阻塞所以错误号为&lt;code&gt;EAGAIN&lt;/code&gt;或&lt;code&gt;EWOULDBLOCK&lt;/code&gt;或&lt;code&gt;EINTR&lt;/code&gt;表示读完了就跳出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;其他就是接受的数据的字节数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;send的工作原理&lt;/h3&gt;
&lt;p&gt;send()函数只负责将数据提交给协议层。当调用该函数时，send()先比较待发送数据的长度和套接字的发送缓冲区的长度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当待拷贝数据的长度大于发送缓冲区的长度时，该函数返回SOCKET_ERROR；&lt;/li&gt;
&lt;li&gt;当待拷贝数据的长度小于或等于发送缓冲区的长度时，那么send先检查协议是否正在发送发送套接字的发送缓冲区中的数据：
&lt;ul&gt;
&lt;li&gt;如果是就等待协议把数据发送完，再进行拷贝；&lt;/li&gt;
&lt;li&gt;如果协议还没有开始发送套接字的发送缓冲区中的数据或者该发送缓冲区中没有数据，那么send就比较该发送缓冲区中的剩余空间和待拷贝数据的长度：
&lt;ul&gt;
&lt;li&gt;如果待拷贝数据的长度大于剩余空间的大小，send就一直等待协议把该发送缓冲区中的数据发完；&lt;/li&gt;
&lt;li&gt;如果待拷贝数据的长度小于剩余空间大小，send就仅仅把buf中的数据拷贝到剩余空间中。（注意：并不是send把该套接字的发送缓冲区中数据传到连接的另一端，而是协议传的，send仅仅是把数据拷贝到该发送缓冲区的剩余空间里面。）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果send函数拷贝成功，就返回实际拷贝的字节数；如果拷贝的过程中出现错误，send就返SOCKET_ERROR；如果send在等待协议传送数据时网络断开的话，那么send函数也返回SOCKET_ERROR。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要注意，send函数把buffer中的数据成功拷贝到套接字的发送缓冲区中的剩余空间里面后，它就返回了，但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传输过程中出现网络错误的话，那么下一个socket函数就会返回SOCKET_ERROR&lt;/strong&gt;。（每一个除send外的socket函数在执行的最开始总要先等待套接字的发送缓冲区的数据被协议传送完毕才能继续，如果在等待时出现网络错误，那么该socket函数就返回SOCKET_ERROR。）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;服务器模型 Proactor和Reactor模式 （事件处理模式）&lt;/h2&gt;
&lt;h3&gt;Reactor&lt;/h3&gt;
&lt;p&gt;核心就是主线程&lt;strong&gt;只负责监听&lt;/strong&gt;文件描述上是否有事件发生，有的话就立即将该事件通知工作线程。除此之外，主线程不做任何其他实质性的工作。读写数据，接受新的连接，以及处理客户请求均在工作线程中完成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220731171623.png&quot; alt=&quot;QQ截图20220731171623&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Proactor&lt;/h3&gt;
&lt;p&gt;俩字：异步&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220731171903.png&quot; alt=&quot;QQ截图20220731171903&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;模拟Proactor模式&lt;/h2&gt;
&lt;p&gt;主线程负责监听链接 + 读 + 写。读出来的数据交给子线程处理。子线程处理完毕后交给主线程写出（发送）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220731172002.png&quot; alt=&quot;QQ截图20220731172002&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;并发模式 - 半同步半反应堆。就是主线程监听+读写，然后封装成任务对象后插入队列交给子线程处理其余的事情&lt;/h2&gt;
&lt;p&gt;请求队列用的是链表。因为只需要顺序访问并且需要经常删除和添加。链表速度比较快。&lt;/p&gt;
&lt;h3&gt;缺点&lt;/h3&gt;
&lt;p&gt;主线程和子线程共用一个任务队列。主线程和子线程对队列操作都需要加锁，消耗资源较大。&lt;/p&gt;
&lt;p&gt;子线程同一时间只能处理一个客户端连接，如果连接数很大，队列会堆积任务导致响应速度越来越慢。就算使用子线程，切换线程也会消耗资源。&lt;/p&gt;
&lt;h2&gt;客户端断开连接，服务端epoll会监听到EPOLLRDHUP&lt;/h2&gt;
&lt;p&gt;没有epoll的时候，我们在NET1笔记写了，客户端close发送FIN包，服务器的协议栈会给它替换成一个EOF放到文件描述符里面。我们使用recv读到这个EOF的时候会返回0，我们拿到了这个返回值0就知道需要关闭了，就可以关闭连接了。也就是我们是代码层操作。&lt;/p&gt;
&lt;p&gt;有epoll的时候，我前面都一样也会替换成EOF，但是这个时候epoll会检测到。所以会触发EPOLLRDHUP这个事件。也就是现在epoll替我们做了这个事情。&lt;/p&gt;
&lt;h2&gt;项目存在的问题：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;多线程取处理数据用的是信号量。而且信号量只有一个，没有给生产者消费者两个信号量，而是让他俩使用的一个。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这两个可以替换。信号量+锁和条件变量+锁都可以实现对应功能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;recv的时候出现严重问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假如我的socketbuffer非常小。只有10，header是100。我读完了10之后才能有空间接收下一个10。但是假如现在网卡了，读完了10后while循环读发现没东西，errorno = eagain然后就break跳出了。所以其实根本读不完，所以只能依靠外面解析的时候发现badline之后报错&lt;/li&gt;
&lt;li&gt;所以大文件传输应该在传输前设置一个文件大小。如果小于文件大小而且是eagain就继续读取。如果等于文件大小了还是eagain就break跳出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;报错有问题。processread返回多种错误，但是process的时候只判断了NO_REQUEST&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果链接关闭的比定时器早，比如HTTP关闭了keep_alive，则链接会被直接关闭，但是依旧会被放到超时链表，也会检测一遍，最后会关闭一个已经关闭的文件描述符（我们项目里的close_connect中，如果关闭一个对象的文件描述符，会把这个对象的文件描述符关闭后设置为-1）并且返回一个（-1）。虽然没啥事吧但是不太优雅。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;信号量&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这个&lt;code&gt;sem_init&lt;/code&gt;最后的值意思是初始值是几。也不能完全理解为物品容量。信号量后续操作是单纯地对那个数字进行增减。而这个数字只有初始值而没有顶（所以会溢出）。&lt;code&gt;wait&lt;/code&gt;会让这个数字减掉1。如果&lt;strong&gt;相减后&lt;/strong&gt;小于0了就阻塞**（所以这个数字理论上可以到-1，因为我们如果现在是1，则拿一个资源就是0。因为现在我拿到了资源所以不会被阻塞，然后下一个线程进来了-1，发现结果变成了-1，就会阻塞）**。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择题中，这个值可以为负。负数就是目前有多少个进程/线程在等待该资源。（操作系统概念6.6.1）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这个值可能会溢出。也就是初始值假如5，可能变成6，溢出，这样会返回错误 一个错误代码是&lt;code&gt;EOVERFLOW&lt;/code&gt;。但是一般操作系统会忽略。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post&lt;/code&gt;会让睡眠的进程唤醒，如果&lt;strong&gt;相加后&lt;/strong&gt;发现信号量值&amp;lt;=0，意味着有程序被阻塞，则会唤醒对应的线程或者是进程。如果&amp;gt;0则意味着没有进程睡眠&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也就是为什么信号量是&lt;strong&gt;先等待（-1），后加锁&lt;/strong&gt;。因为&lt;code&gt;wait&lt;/code&gt;本身是阻塞的，如果小于0了就阻塞。如果先加锁，发现小于0了直接阻塞就没办法解锁了。这也是为什么条件变量要反过来。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设我们使用的是一个信号量，即消费者和生产者共享信号量。
&lt;ul&gt;
&lt;li&gt;一个信号量的时候就是，如果数字不为0，就该消费消费该生产生产。如果数字为0了，那么消费者就等着，等生产者生产完了通知后继续消费。&lt;/li&gt;
&lt;li&gt;首先，初始化的时候，我们不能让消费者直接消费，所以初始化的值一定是0。（如果生产者消费者区分信号量，则生产者信号量初始值应为队列的最大值，消费者信号量初始值仍旧应为0。&lt;/li&gt;
&lt;li&gt;生产者：
&lt;ul&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;生产&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;li&gt;post [+1]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;消费者：
&lt;ul&gt;
&lt;li&gt;wait [-1] 一定要先等待。如果上来就锁了，因为wait是阻塞的如果是0就阻塞等待，那生产者也拿不到锁也没办法生产了。&lt;/li&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;消费&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;假设我们使用的是一个信号量，即消费者和生产者&lt;strong&gt;不&lt;/strong&gt;共享信号量。（其实和条件变量差不多）
&lt;ul&gt;
&lt;li&gt;首先，初始化的时候，我们不能让消费者直接消费，所以初始化的值一定是0。但是生产者可以直接生产。所以初始化的值可以为队列最大值，比如8。&lt;/li&gt;
&lt;li&gt;生产者：
&lt;ul&gt;
&lt;li&gt;wait[-1] 注意这个时候是减掉的生产者自己的空位。也就是每生产一次，减掉一个。他最多生产8个，生产多了就停止等待让这个数字不为0。（消费者会+1）&lt;/li&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;生产&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;li&gt;post [+1]注意这个时候是添加的消费者的消费。让消费者的信号量不为1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;消费者：
&lt;ul&gt;
&lt;li&gt;wait[-1] 注意这个时候是减掉的消费者自己的空位。也就是记录有多少可以消费的&lt;/li&gt;
&lt;li&gt;加锁&lt;/li&gt;
&lt;li&gt;消费&lt;/li&gt;
&lt;li&gt;解锁&lt;/li&gt;
&lt;li&gt;post[+1]注意这个是告诉生产者+1，也就是可生产的空位+1.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;条件变量&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;条件变量一定要先加锁而且必须是可以手动解锁的锁比如unique_lock而不能使用lock_guard&lt;/strong&gt;。也可以用一个条件变量也可以用两个。核心是&lt;code&gt;pthread_cond_wait&lt;/code&gt;。原理是首先调用方抢锁，然后发现需要等待，所以调用方会被阻塞（睡眠并加入等待队列），然后互斥锁解锁，让其余线程抢锁。其余线程如果抢到锁执行完了任务，然后就可以调用&lt;code&gt;notify&lt;/code&gt;通知。通知后调用方的&lt;code&gt;wait&lt;/code&gt;会重新加锁并唤醒当前进程（之后wait函数返回）。系统保证解锁和睡眠是原子操作。系统也保证加锁和唤醒是原子操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了防止虚假唤醒，必须要使用&lt;code&gt;while&lt;/code&gt;而不能使用wait。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为我们在wait函数返回之前，当前进程必须执行 拿到锁—&amp;gt;加锁并唤醒 这两步。加锁和唤醒是原子的，但是并不一定能拿得到这个锁。假如我们消费者1在等待，然后生产者生产完毕，通知消费者。假如这个时候消费者2进来了，直接就拿了锁（因为生产者释放锁到wait函数拿锁这两步不是原子的。存在这种第三方插进来的情况）然后消费了生产的数据。然后释放锁。这时候我们消费者1终于拿到锁了，但是发现数据已经被消费了，这样再去拿数据会有错误。所以必须用while。也就是判空。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意条件变量的信号丢失问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看看例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::condition_variable cv;
 std::mutex gMtx;

void Sender()
{
     std::cout &amp;lt;&amp;lt; &quot;Ready Send notification.&quot; &amp;lt;&amp;lt; std::endl;
     cv.notify_one();   // 发送通知
 }

void Receiver()
{
     std::cout &amp;lt;&amp;lt; &quot;Wait for notification.&quot; &amp;lt;&amp;lt; std::endl;
     std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
     cv.wait(lck);    // 等待通知并唤醒继续执行下面的指令
     std::cout &amp;lt;&amp;lt; &quot;Process.&quot; &amp;lt;&amp;lt; std::endl;
}

 int main()
 {
     std::thread sender(Sender);
     std::thread receiver(Receiver);
     sender.join();
     receiver.join();
     return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程随机启动导致的唤醒丢失，即：通信线程先启动并调用通知函数（&lt;code&gt;notify_one&lt;/code&gt;），但是接收线程还没有开始执行等待（&lt;code&gt;wait&lt;/code&gt;）函数，如果不再次调用函数通知，等待会一直持续下去。这个是最容易发现和验证的问题，上面的主线程中启动线程的顺序就会概率性出现唤醒丢失的问题。&lt;/p&gt;
&lt;p&gt;解决方案也比较简单。也是搭配&lt;code&gt;while&lt;/code&gt;和判断式。其实和上面解决虚假唤醒的道理一致。只不过要注意使用&lt;code&gt;while&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::condition_variable::wait&lt;/code&gt;后面的判断式（谓词）的意思是，只要后面的谓词返回&lt;code&gt;false&lt;/code&gt;，则前面无论如何都不会解锁。尽管可能已经被通知到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断式法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::condition_variable cv;
std::mutex gMtx;

bool send = false;
void Sender()
{
     std::cout &amp;lt;&amp;lt; &quot;Ready Send notification.&quot; &amp;lt;&amp;lt; std::endl;
     send = true;
     cv.notify_one();   // 发送通知
 }

void Receiver()
{
     std::cout &amp;lt;&amp;lt; &quot;Wait for notification.&quot; &amp;lt;&amp;lt; std::endl;
     std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
     cv.wait(lck, [](){return send;});    // 等待通知并唤醒继续执行下面的指令
     std::cout &amp;lt;&amp;lt; &quot;Process.&quot; &amp;lt;&amp;lt; std::endl;
}

int main()
{
    std::thread sender(Sender);
    std::thread receiver(Receiver);
    sender.join();
    receiver.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面的谓词可以等同于这种写法：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void Receiver1()
{
     std::cout &amp;lt;&amp;lt; &quot;Wait for notification.&quot; &amp;lt;&amp;lt; std::endl;
     std::unique_lock&amp;lt;std::mutex&amp;gt; lck(gMtx);
     while(send == false){
        cv.wait(lck); // 等待通知并唤醒继续执行下面的指令
     }
     std::cout &amp;lt;&amp;lt; &quot;Process.&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有没有发现和解决虚假唤醒的很像？都是&lt;code&gt;while&lt;/code&gt;内有一个判别式，然后循环判断等待。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;这个方法可以解决唤醒丢失的原因：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们的问题在于&lt;code&gt;sender&lt;/code&gt;先发送了&lt;code&gt;notify&lt;/code&gt;，然后&lt;code&gt;receiver&lt;/code&gt;才开始&lt;code&gt;wait&lt;/code&gt;。导致丢失&lt;/li&gt;
&lt;li&gt;所以在使用上述方法后，就算&lt;code&gt;sender&lt;/code&gt;先发送了&lt;code&gt;nofity&lt;/code&gt;，但是此时&lt;code&gt;send&lt;/code&gt;已经被&lt;code&gt;sender&lt;/code&gt;变更为&lt;code&gt;true&lt;/code&gt;。所以此时就算唤醒&lt;code&gt;receiver&lt;/code&gt;的信号已经丢失，由于&lt;code&gt;send&lt;/code&gt;是&lt;code&gt;true&lt;/code&gt;，所以不会被阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二者的解决方式都是&lt;code&gt;while&lt;/code&gt;+判别式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时参考 effective modern c++ 条款39&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;信号，管道，定时器&lt;/h2&gt;
&lt;p&gt;我们使用&lt;code&gt;SIGALRM&lt;/code&gt; 和&lt;code&gt;SIGTERM&lt;/code&gt;来侦测定时器信息和服务器停止信息。那么我们如何传递呢？&lt;/p&gt;
&lt;h3&gt;使用管道&lt;/h3&gt;
&lt;p&gt;我们用&lt;code&gt;socketpair&lt;/code&gt;创建管道。然后从管道写端写入信号值（&lt;code&gt;send&lt;/code&gt;函数），管道读端注册至&lt;code&gt;epoll&lt;/code&gt;，通过&lt;code&gt;epoll&lt;/code&gt;监测读事件。&lt;strong&gt;设置管道写入端为非阻塞。因为如果设置为阻塞，信号套接字（文件描述符）缓冲区满了的话会阻塞。会增加信号处理的时间。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;https://cloud.tencent.com/developer/article/1603781&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;socketpair&lt;/code&gt;创建的管道是双向都可读写。&lt;/p&gt;
&lt;h3&gt;定时器逻辑。&lt;/h3&gt;
&lt;p&gt;我们main里的第一次&lt;code&gt;alert&lt;/code&gt;会在五秒后触发，然后信号捕捉函数捕捉到信号，调用&lt;code&gt;sig_handler&lt;/code&gt;。&lt;code&gt;sig_handler&lt;/code&gt;会往信号管道写入一个&lt;code&gt;SIGALRM&lt;/code&gt;数据。然后我们&lt;code&gt;epoll&lt;/code&gt;监听到信号文件描述符有事件，放入就绪数组。我们遍历至信号文件描述符的时候判断是&lt;code&gt;SIGALRM&lt;/code&gt;或者是&lt;code&gt;SIGTRM&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是&lt;code&gt;SIGTRM&lt;/code&gt;则设置&lt;code&gt;stop&lt;/code&gt;为&lt;code&gt;true&lt;/code&gt;后停止服务器。然后我们会&lt;code&gt;delete pool&lt;/code&gt;删除线程池，调用线程池析构函数。析构函数内设置线程停止标识为&lt;code&gt;true&lt;/code&gt;然后子线程停止。因为是&lt;code&gt;detach&lt;/code&gt;所以资源自动回收。&lt;/li&gt;
&lt;li&gt;如果是&lt;code&gt;SIGALRM&lt;/code&gt;则设置定时变量标志为&lt;code&gt;true&lt;/code&gt;。我们不立即执行定时任务因为优先级并不高，先处理其余任务比如文件的读写。当一轮文件描述符遍历处理完毕后再处理定时任务。
&lt;ul&gt;
&lt;li&gt;定时任务即调用&lt;code&gt;timer_handler&lt;/code&gt;。**由于&lt;code&gt;alarm&lt;/code&gt;**调用一次只会触发一次，所以函数内仍要设置新的定时器来不断触发信号
&lt;ul&gt;
&lt;li&gt;第一次在while外面的alarm做为引火器。让我们执行第一次处理信号，因为我们收到了是SIGALRM所以返回true调用此函数。调用此函数后再次设置五秒倒计时。五秒后又会处理信号，又是SIGALRM所以再次调用此函数。如此循环&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;执行到&lt;code&gt;timer_handler&lt;/code&gt;后进入&lt;code&gt;tick&lt;/code&gt;。此函数是判断链表里的任务是否超时。遍历链表。因为我们的链表是升序链表。如果当前链表头的任务仍未超时，则&lt;code&gt;break&lt;/code&gt;。如果有超时任务，则删除节点并调用回调函数，这个回调函数执行的是：
&lt;ul&gt;
&lt;li&gt;客户端文件描述符移出&lt;code&gt;epoll&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;关闭文件描述符&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​				然后一直查找超时任务，直到链表头的任务不超时为止。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更改时间
&lt;ul&gt;
&lt;li&gt;注意，我们在&lt;code&gt;accept&lt;/code&gt;的时候就设置定时器了。所以我们检测到了一个文件描述符有任务了，则把该文件描述符的超时时间重新设置为当前时间+3倍的timeslot。然后调用&lt;code&gt;adjust_timer&lt;/code&gt;重新设置。设置逻辑也是按照升序的顺序插入到原链表内。&lt;strong&gt;定时器相关操作由主线程执行，所以不会产生共享资源，无需上锁。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;复杂度
&lt;ul&gt;
&lt;li&gt;从实现上看，主要涉及双向链表的插入，删除操作，其中&lt;strong&gt;添加定时器的事件复杂度是O(n),删除定时器的事件复杂度是O(1)。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;需要补充的地方，项目没写
&lt;ul&gt;
&lt;li&gt;写的时候也要重新设置定时器。但是咱们只有单次写入就没事&lt;/li&gt;
&lt;li&gt;异常的时候也要从链表中删除对应文件描述符。懒得弄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;管道传递的是什么类型？&lt;/h3&gt;
&lt;p&gt;信号本身是整型数值，管道中传递的是ASClI码表中整型数值对应的字符。&lt;/p&gt;
&lt;p&gt;switch的变量一般为字符或整型，当switch的变量为字符时，case中可以是字符，也可以是字符对应的ASClIl码。&lt;/p&gt;
&lt;h3&gt;杂项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;统一事件源
&lt;ul&gt;
&lt;li&gt;具体的，信号处理函数使用管道将信号传递给主循环，信号处理函数往管道的写端写入信号值，主循环则从管道的读端读出信号值，使用I/O复用系统调用来监听管道读端的可读事件，这样&lt;strong&gt;信号事件与其他文件描述符都可以通过epoll来监测，从而实现统一处理。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;定时事件失效也允许，因为定时任务不是必须立刻处理的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;异步日志&lt;/h2&gt;
&lt;p&gt;其实我这个异步日志就是单纯的开了一个线程不断的从阻塞队列取出数据然后写入磁盘。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用了单例模式。多次使用同一个对象不会重复创建，内存中只有一份，大家共享。防止频繁创建导致的内存消耗。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;懒汉：真正使用的时候才创建
&lt;ul&gt;
&lt;li&gt;懒汉在多线程会出现创建多份的情况。虽然后面创建的会覆盖掉前面的，但还要避免。
&lt;ul&gt;
&lt;li&gt;解决：加锁+双检测。双检锁也可能失效
&lt;ul&gt;
&lt;li&gt;只用单检测锁的时候每次调用实例的时候都要加锁，影响性能，双检锁的第一层只有在实例没有创建的时候会调用，创建实例后，不是&lt;code&gt;NULL&lt;/code&gt;所以不用加锁可以直接返回实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;或者是使用静态局部变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;饿汉：加载的时候就创建。会消耗更多资源，因为就算没使用到这个实例，但是只要加载了这个类就会创建。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阻塞队列使用了生产者消费者模型，使用了锁+条件变量。条件变量上面提到了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再次强调：条件变量先加锁。因为&lt;code&gt;wait&lt;/code&gt;函数要解锁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里必须只能使用&lt;code&gt;unique_lock&lt;/code&gt;不能使用&lt;code&gt;lock_guard&lt;/code&gt;。&lt;code&gt;lock_guard&lt;/code&gt;是阉割版的&lt;code&gt;unique_lock&lt;/code&gt;，不支持手动解锁。但是&lt;code&gt;wait&lt;/code&gt;函数要解锁，所以只可以使用&lt;code&gt;unique_lock&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;伪唤醒相关。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写入使用了&lt;code&gt;fflush &lt;/code&gt; + &lt;code&gt;fputs&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fflush()&lt;/code&gt;会强迫将缓冲区内的数据写回参数&lt;code&gt;stream&lt;/code&gt;指定的文件中，如果参数&lt;code&gt;stream&lt;/code&gt;为&lt;code&gt;NULL&lt;/code&gt;, &lt;code&gt;fflush()&lt;/code&gt;会将所有打开的文件数据更新。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在使用多个输出函数连续进行多次输出到控制台时，有可能下一个数据再上一个数据还没输出完毕，还在输出缓冲区中时，下一个printf就把另一个数据加入输出缓冲区，结果冲掉了原来的数据，出现输出错误。在prinf()后加上fflush(stdout);强制马上输出到控制台，可以避免出现上述错误。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可变参数宏。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  #define LOG_BASE(level, format, ...) \
      do {\
          Log* log = Log::Instance();\
          if (log-&amp;gt;IsOpen() &amp;amp;&amp;amp; log-&amp;gt;GetLevel() &amp;lt;= level) {\
              log-&amp;gt;write(level, format, ##__VA_ARGS__); \
              log-&amp;gt;flush();\
          }\
      } while(0);
  
  #define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0);
  #define LOG_INFO(format, ...) do {LOG_BASE(1, format, ##__VA_ARGS__)} while(0);
  #define LOG_WARN(format, ...) do {LOG_BASE(2, format, ##__VA_ARGS__)} while(0);
  #define LOG_ERROR(format, ...) do {LOG_BASE(3, format, ##__VA_ARGS__)} while(0);
  
  /*
  这里的do while意思是确保宏可以正确调用。因为宏有先替换再计算的特性。可能会出错
  ...这个可变参数列表在宏中和##__VA_ARGS__搭配使用。
  举例子：
  #define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0);
  这个意思是你输入LOG_DEBUG(&quot;HELLO %d\n&quot;, 100)
  会被替换成LOG_BASE(0, &quot;HELLO %d\n&quot;, 100)
  
  输入LOG_DEBUG(&quot;HELLO %d %s %d\n&quot;, 100, &quot;test&quot;, 10000)
  会被替换成LOG_BASE(0, &quot;HELLO %d %s %d\n&quot;, 100, &quot;test&quot;, 10000)
  
  
  可变参数的宏里的’##’操作说明带有可变参数的宏(Macros with a Variable Number of Arguments)
  __VA_ARGS__宏前面加上##的作用在于，当可变参数的个数为0时，这里printf参数列表中的##会把前面多余的”,”去掉，否则会编译出错，建议使用后面这种，使得程序更加健壮。
  https://blog.csdn.net/bat67/article/details/77542165
  */
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;日志分文件
&lt;ul&gt;
&lt;li&gt;日志写入前会判断&lt;strong&gt;当前day&lt;/strong&gt;是否为创建日志的时间，行数是否超过最大行限制。
&lt;ul&gt;
&lt;li&gt;若为创建日志时间，写入日志，&lt;strong&gt;否则按当前时间创建新log&lt;/strong&gt;，更新创建时间和行数。&lt;/li&gt;
&lt;li&gt;若行数超过最大行限制，在当前日志的末尾加count/max_lines为后缀创建新log将系统信息格式化后输出，具体为:格式化时间＋格式化内容&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;为什么要异步？和同步的区别是什么？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;同步方式写入日志时会产生较多的系统调用，如果某条日志信息过大，会阻塞日志系统，造成系统瓶颈。异步系统采用生产者--消费者模型，具有较高的并发能力。&lt;/p&gt;
&lt;h2&gt;单例模式：&lt;/h2&gt;
&lt;p&gt;优点：只有一个实例所以不需要每次都创建和销毁，可以在启动的时候就创建对象然后永久驻留在内存中。&lt;/p&gt;
&lt;p&gt;实现方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;私有化构造函数和拷贝构造，拷贝赋值&lt;/li&gt;
&lt;li&gt;定义类内静态指针并且设置为私有化&lt;/li&gt;
&lt;li&gt;定义一个获取指针的静态函数，返回指向实体的指针。&lt;/li&gt;
&lt;li&gt;初始化的时候在类外进行，然后new一个对象赋值给这个指针。&lt;/li&gt;
&lt;li&gt;使用的时候使用对应的静态函数，获取指针，访问实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：有的实现方式是线程不安全的。&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;accept&lt;/code&gt;后拿到的读写文件描述符对应的是TCP请求三次握手后服务器接受连接了。然后这个时候不一定有数据送达。所以真正的报文送达是epoll检测到的读写文件描述符的事件。所以&lt;code&gt;accept&lt;/code&gt;只是负责把客户端信息封装后放到&lt;code&gt;epoll&lt;/code&gt;监听队列内。&lt;strong&gt;注意，我们的定时器为了监测链接，所以是从accept的时候就把接收到的读写文件描述符的定时器设置好了。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CHECK_STATE_CONTENT&lt;/code&gt;这个主状态机的状态在咱们项目没用。这个只给POST请求用的。GET没请求体。没数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;项目难点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;异步日志的加锁。粒度控制&lt;/li&gt;
&lt;li&gt;日志的拼接。&lt;/li&gt;
&lt;li&gt;生成响应体的时候计算偏移量&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么时候ET 什么时候LT&lt;/h2&gt;
&lt;p&gt;个人整理：&lt;/p&gt;
&lt;p&gt;ET：连接数量较大的时候。假设5000个链接。我们为了不让第1个人速度很快然后队伍越往后延迟越高，（当然应该用消息队列）。我们可以轮询5000个链接，先每一个读1MB，然后循环。&lt;/p&gt;
&lt;p&gt;LT：实时性较高，应该尽力处理完毕每一个链接及时返回。&lt;/p&gt;
&lt;h2&gt;客户端什么时候异常断开&lt;/h2&gt;
&lt;p&gt;客户端出BUG，段错误之类的，或者是接收到一半/发送到一半没信号了等等。&lt;/p&gt;
&lt;h2&gt;为什么用线程池&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;避免创建和销毁线程的开销。（为每个线程的栈分配内存）&lt;/li&gt;
&lt;li&gt;削峰。如果没有线程池，那么大量链接涌入的时候服务器会同时开启大量的线程，会占用大量内存空间导致内存空间不足，影响服务器稳定性。而且会进行大量的线程切换，开销极大。如果线程数量是固定的，每个线程从队列中取出任务，这样大量涌入的时候不会影响服务器稳定性。而且线程切换是固定的，不会因为频繁新建和销毁线程导致切换不固定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程池中线程数量&lt;/h2&gt;
&lt;p&gt;一般是看是IO密集型还是CPU密集型。如果是IO密集型则大小是2N+1, CPU密集型是N+1。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IO密集型，大部分时间在处理I/O请求，不需要CPU提供多大算力，这时候频繁切换可以提高并发性，所以I/O密集型的处理，希望在高并发下进行，多线程并发消耗资源少。&lt;/li&gt;
&lt;li&gt;CPU密集型，占用CPU算力大，希望能获得更长的时间轮片而不是经常切换；这样使用进程较好，进程本身优于线程，只是切换调度消耗的资源多。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如果请求过大，线程池处理不过来怎么办？&lt;/h2&gt;
&lt;p&gt;可以增加线程数量或使用集群。&lt;/p&gt;
&lt;p&gt;tomcat的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;判断如果当前线程数小于核心线程池，则新建一个线程来处理提交的任务&lt;/li&gt;
&lt;li&gt;如果当前当前线程池数大于核心线程池，小于最大线程数，则创建新的线程执行提交的任务&lt;/li&gt;
&lt;li&gt;如果当前线程数等于最大线程数，则将任务放入任务队列等待执行&lt;/li&gt;
&lt;li&gt;如果队列已满，则执行拒绝策略&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么时候用多进程什么时候用多线程&lt;/h2&gt;
&lt;p&gt;具体情况具体分析。比如游戏服务器需要用多进程，因为进程有隔离性，我们不希望一个线程挂掉影响整个进程。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;对比维度&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;多进程&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;多线程&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据共享、同步&lt;/td&gt;
&lt;td&gt;数据共享复杂，需要用IPC；数据是分开的，同步简单&lt;/td&gt;
&lt;td&gt;因为共享进程数据，数据共享简单，但也是因为这个原因导致同步复杂&lt;/td&gt;
&lt;td&gt;各有优势&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存、CPU&lt;/td&gt;
&lt;td&gt;占用内存多，切换复杂，CPU利用率低&lt;/td&gt;
&lt;td&gt;占用内存少，切换简单，CPU利用率高&lt;/td&gt;
&lt;td&gt;线程占优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建销毁、切换&lt;/td&gt;
&lt;td&gt;创建销毁、切换复杂，速度慢&lt;/td&gt;
&lt;td&gt;创建销毁、切换简单，速度很快&lt;/td&gt;
&lt;td&gt;线程占优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;编程、调试&lt;/td&gt;
&lt;td&gt;编程简单，调试简单&lt;/td&gt;
&lt;td&gt;编程复杂，调试复杂&lt;/td&gt;
&lt;td&gt;进程占优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可靠性&lt;/td&gt;
&lt;td&gt;进程间不会互相影响&lt;/td&gt;
&lt;td&gt;一个线程挂掉将导致整个进程挂掉&lt;/td&gt;
&lt;td&gt;进程占优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分布式&lt;/td&gt;
&lt;td&gt;适应于多核、多机分布式；如果一台机器不够，扩展到多台机器比较简单&lt;/td&gt;
&lt;td&gt;适应于多核分布式&lt;/td&gt;
&lt;td&gt;进程占&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以简单来说，比如我们的web server，是一个模拟Proactor模式，我们需要主线程把东西读进来放到队列然后子线程从队列拿东西。这种频繁的数据共享使用多线程会轻松很多。而且我们对于可靠性来说，单纯的进行请求的响应并不复杂，所以不太可能会导致线程挂掉。所以线程可能更好一点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用多线程：任务之间独立，不需要数据交互的。需要强健壮性的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;服务器怎么知道断开连接了&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;浏览器关闭了页面，浏览器会调用&lt;code&gt;close&lt;/code&gt;。发送&lt;code&gt;FIN&lt;/code&gt;包&lt;/li&gt;
&lt;li&gt;服务器接收到了&lt;code&gt;FIN&lt;/code&gt;包，TCP协议栈会把这个&lt;code&gt;FIN&lt;/code&gt;包换成EOF结束符然后放到对应客户端读写描述符的接收缓冲区中。
&lt;ul&gt;
&lt;li&gt;我们知道每一个socket都是文件描述符。&lt;/li&gt;
&lt;li&gt;我们知道每个socket都有一个自己的缓冲区。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过读取（可能是epoll，可能是read，可能是select等等）我们能读取到这个&lt;code&gt;EOF&lt;/code&gt;，我们就知道了客户端想要关闭，不会再发送数据了。我们就可以这边准备进行关闭。&lt;/li&gt;
&lt;li&gt;我们读取到了&lt;code&gt;EOF&lt;/code&gt;可以调用&lt;code&gt;close&lt;/code&gt;进行关闭了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意我们这里HTTP开启了keep-alive，所以在&lt;code&gt;http_connection&lt;/code&gt;文件里面的&lt;code&gt;read&lt;/code&gt;函数，我们读取的时候不会走到&lt;code&gt;bytes_read == 0&lt;/code&gt;。因为客户端没主动断开链接。除非我们把定时器检测去掉等着让他出来。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么socket有了内核缓冲区还需要一个用户缓冲区?&lt;/h2&gt;
&lt;p&gt;https://blog.csdn.net/farmwang/article/details/64450170&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;假设应用程序需要发送40kB数据,但是操作系统的TCP发送缓冲区只有25kB剩余空间,那么剩下的15kB数据怎么办?如果等待OS缓冲区可用,会阻塞当前线程,因为不知道对方什么时候收到并读取数据。因此网络库应该把这15kB数据缓存起来,放到这个TCP连接的应用层发送缓冲区中,等socket变得可写的时候立刻发送数据,这样“发送”操作不会阻塞。如果应用程序随后又要发送50kB数据,而此时发送缓冲区中尚有未发送的数据(若干kB),那么网络库应该将这50kB数据追加到发送缓冲区的末尾,而不能立刻尝试write(),因为这样有可能打乱数据的顺序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个地方,等剩余的数据收到之后再一并处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;简而言之，就是，如果没有用户态的缓冲区，那么我们直接把数据放入内核缓冲区，会有非常多的自己判断的地方。所以我们设计出用户态的缓冲区，IO函数辅助我们进行读取和写入。换句话说，这个用户缓冲区是帮助我们对想要发送的信息和已经接收到的信息进行处理的地方。比如排序，等待。等等&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们的线程池+任务队列就是一种用户缓冲区设计。我们自己的readbuf和writebuf就是缓冲区&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为我们有了这个缓冲区，我们可以在读取的时候用循环读的方式等到读取完毕后再一起处理数据。如果没有这个缓冲区，我们直接从socket中读取的数据如果不够处理，那么我们就永远没法处理这个数据了。&lt;/p&gt;
&lt;p&gt;写入也是，如果我们直接写入socket的缓冲区，那么我们会需要非常多的判断机制&lt;/p&gt;
&lt;h2&gt;主从状态机（HTTP解析）&lt;/h2&gt;
&lt;p&gt;主状态机从内部调用从状态机，从状态机驱动主状态机。每解析一部分就将状态改变，来完成状态机的解析跳转，最后得到一个完整的HTTP请求。&lt;/p&gt;
&lt;h3&gt;为什么用主从状态机&lt;/h3&gt;
&lt;p&gt;为了封装逻辑，使得代码逻辑清晰，编程效率高。&lt;/p&gt;
&lt;h3&gt;主状态机&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CHECK_STATE_REQUESTLINE&lt;/code&gt;，解析请求行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主状态机的初始状态，调用&lt;code&gt;parse_line&lt;/code&gt;函数解析行数据&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;process_read_line&lt;/code&gt;解析&lt;strong&gt;请求行&lt;/strong&gt;获得请求方法、目标URL及HTTP版本号&lt;/li&gt;
&lt;li&gt;解析完成后主状态机的状态变为&lt;code&gt;CHECK_STATE_HEADER&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CHECK_STATE_HEADER&lt;/code&gt;，解析请求头&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;process_read_headers&lt;/code&gt;函数解析请求头部信息&lt;/li&gt;
&lt;li&gt;判断是空行还是请求头，若是空行，进而判断&lt;code&gt;content-length&lt;/code&gt;是否为0，如果不是0，表明是&lt;code&gt;POST&lt;/code&gt;请求，则状态转移到&lt;code&gt;CHECK_STATE_CONTENT&lt;/code&gt;，否则说明是&lt;code&gt;GET&lt;/code&gt;请求，则报文解析结束。&lt;/li&gt;
&lt;li&gt;若解析的是请求头部字段，则主要分析&lt;code&gt;connection&lt;/code&gt;字段，&lt;code&gt;content-length&lt;/code&gt;字段，和&lt;code&gt;host&lt;/code&gt;字段。其他字段可以直接跳过，各位也可以根据需求继续分析。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CHECK_STATE_CONTENT&lt;/code&gt;，解析消息体，&lt;strong&gt;仅用于解析POST请求&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;主状态机的逻辑&lt;/h3&gt;
&lt;p&gt;主状态机的函数是&lt;code&gt;process_read&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这里的判断条件是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主状态机转移到&lt;code&gt;CHECK_STATE_CONTENT&lt;/code&gt;，该条件涉及解析消息体。(本项目没用到)&lt;/li&gt;
&lt;li&gt;从状态机转移到&lt;code&gt;LINE_OK&lt;/code&gt;，该条件涉及解析请求行和请求头部&lt;/li&gt;
&lt;li&gt;两者为或关系，当条件为真则继续循环，否则退出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;首先我们调用过了&lt;code&gt;parse_line&lt;/code&gt;。这个函数帮我们把&lt;code&gt;\r\n&lt;/code&gt;换成了\0也就是字符串结束符。这样方便读取。主状态机初始状态是&lt;code&gt;CHECK_STATE_REQUESTLINE&lt;/code&gt;，解析请求行。通过调用从状态机驱动主状态机。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解析完请求行后，主状态机继续分析请求头。具体方法上文写了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;从状态机&lt;/h3&gt;
&lt;p&gt;三种状态，标识解析一行的读取状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LINE_OK&lt;/code&gt;，完整读取一行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LINE_BAD&lt;/code&gt;，报文语法有误&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LINE_OPEN&lt;/code&gt;，读取的行不完整&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;webbench原理&lt;/h2&gt;
&lt;p&gt;父进程fork若干个子进程，每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求，父子进程通过管道进行通信，子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息，父进程通过管道读端读取子进程发来的相关信息，子进程在时间到后结束，父进程在所有子进程退出后统计并给用户显示最后的测试结果，然后退出。&lt;/p&gt;
&lt;h2&gt;mmap 零拷贝&lt;/h2&gt;
&lt;h3&gt;传统方式进行数据读取和发送&lt;/h3&gt;
&lt;p&gt;整个过程发生了&lt;strong&gt;4次用户态和内核态的上下文切换&lt;/strong&gt;和&lt;strong&gt;4次拷贝&lt;/strong&gt;，具体流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户进程通过&lt;code&gt;read()&lt;/code&gt;方法向操作系统发起调用，此时上下文从用户态转向内核态&lt;/li&gt;
&lt;li&gt;DMA控制器把数据从硬盘中拷贝到读缓冲区&lt;/li&gt;
&lt;li&gt;CPU把读缓冲区数据拷贝到应用缓冲区，上下文从内核态转为用户态，&lt;code&gt;read()&lt;/code&gt;返回&lt;/li&gt;
&lt;li&gt;用户进程通过&lt;code&gt;write()&lt;/code&gt;方法发起调用，上下文从用户态转为内核态&lt;/li&gt;
&lt;li&gt;CPU将应用缓冲区中数据拷贝到socket缓冲区&lt;/li&gt;
&lt;li&gt;DMA控制器把数据从socket缓冲区拷贝到网卡，上下文从内核态切换回用户态，&lt;code&gt;write()&lt;/code&gt;返回&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220804011214.png&quot; alt=&quot;QQ截图20220804011214&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;mmap零拷贝&lt;/h3&gt;
&lt;p&gt;零拷贝并非真的是完全没有数据拷贝的过程，只不过是减少用户态和内核态的切换次数以及CPU拷贝的次数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mmap+write简单来说就是使用&lt;code&gt;mmap&lt;/code&gt;替换了read+write中的read操作，减少了一次CPU的拷贝。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mmap&lt;/code&gt;主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射，内核缓冲区和应用缓冲区共享，从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;也就是说，使用了mmap之后，我们不再需要读了。我们拿到了mmap函数返回的指针（内存首地址）之后我们可以直接在这里修改或者是获取文件了。（mmap函数的形参有被映射文件的文件描述符）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在这里我们用mmap直接拿到了这个文件映射区的首地址，我们会直接把这个文件地址放到分散写的结构体内，然后用writev直接写入文件读写描述符（客户端文件描述符）即可。&lt;/p&gt;
&lt;p&gt;所以整个过程发生了&lt;strong&gt;4次用户态和内核态的上下文切换&lt;/strong&gt;和&lt;strong&gt;3次拷贝&lt;/strong&gt;，具体流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户进程通过&lt;code&gt;mmap()&lt;/code&gt;方法向操作系统发起调用，上下文从用户态转向内核态&lt;/li&gt;
&lt;li&gt;DMA控制器把数据从硬盘中拷贝到读缓冲区&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文从内核态转为用户态，mmap调用返回&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;用户进程通过&lt;code&gt;write()&lt;/code&gt;方法发起调用，上下文从用户态转为内核态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU将读缓冲区中数据拷贝到socket缓冲区&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;DMA控制器把数据从socket缓冲区拷贝到网卡，上下文从内核态切换回用户态，&lt;code&gt;write()&lt;/code&gt;返回&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;mmap&lt;/code&gt;的方式节省了一次CPU拷贝，同时由于用户进程中的内存是虚拟的，只是映射到内核的读缓冲区，所以可以节省一半的内存空间，比较适合大文件的传输。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220804011220.png&quot; alt=&quot;QQ截图20220804011220&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;sendfile&lt;/h3&gt;
&lt;p&gt;sendfile简单说就是把write也拿掉了。类似于完美转发。数据不经过用户空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但这种操作仅适用于不需要用户空间读写的情况。比如我们这就不行，我们要写入额外的响应头。这种方法适合于静态文件服务器那种。放到这个项目来说，就是sendfile直接把我们的网站资源发出去了，但是我们的响应数据没办法加进去。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220804012822.png&quot; alt=&quot;QQ截图20220804012822&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;进程间通信方式：&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;无名管道（pipe）：管道允许一个进程和另一个与它有共同祖先的进程之间进行通信。&lt;/li&gt;
&lt;li&gt;命名管道（FIFO）：类似于管道，但是它可以用于任何两个进程之间通信，命名管道在文件系 统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。&lt;/li&gt;
&lt;li&gt;信号（signal）：信号是比较复杂的通信方式，用于通知接收进程有某种事情发生。除了用于 进程间通信外，进程还可以发送信号给进程本身；Linux除了支持UNIX早期信号语义函数signal 外，还支持语义符合POSIX.1标准的信号函数sigaction。（实际上，该函数是基于BSD的，BSD即 能实现可靠信号机制，又能够统一对外接口，用sigaction函数重新实现了signal函数的功能）&lt;/li&gt;
&lt;li&gt;内存映射（mapped memory）：内存映射允许任何多个进程间通信，每一个使用该机制的进 程通过把一个共享的文件映射到自己的进程地址空间来实现它；&lt;/li&gt;
&lt;li&gt;消息队列（message queue）：消息队列是消息的连接表，包括POSIX消息队列和System V 消息队列。有足够权限的进程可以向队列中添加消息，被赋予读权限的进程则可以读走队列中的消 息。消息队列克服了信号承载信息量少，管道只能成该无格式字节流以及缓冲区大小受限等缺点；&lt;/li&gt;
&lt;li&gt;信号量（semaphore）：信号量主要作为进程间以及同进程不同线程之间的同步手段。&lt;/li&gt;
&lt;li&gt;共享内存 （shared memory）：它使得多个进程可以访问同一块内存空间，是最快的可用IPC 形式。这是针对其他通信机制运行效率较低而设计的。它往往与其他通信机制，如信号量结合使用，以达到进程间的同步及互斥；&lt;/li&gt;
&lt;li&gt;套接字（Socket）：它是更为通用的进程间通信机制，可用于不同机器之间的进程间通信。起 初是由UNIX系统的BSD分支开发出来的，但现在一般可以移植到其他类UNIX系统上：Linux和 System V的变种都支持套接字。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;进程间同步方式：&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;信号量&lt;/li&gt;
&lt;li&gt;管程 https://www.cnblogs.com/Keeping-Fit/p/15064039.html 不是很懂&lt;/li&gt;
&lt;li&gt;互斥量（锁）（基于共享内存的快速用户态 ）&lt;/li&gt;
&lt;li&gt;文件锁（通过 fcntl 设定，针对文件）&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;线程间同步方式：&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;锁机制：&lt;strong&gt;（可以设置初始化条件让其可以在进程间使用）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;读写锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;信号量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原子操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存屏障&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++的&lt;code&gt;future&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Windos上的临界区&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个linux没有，只能用互斥锁替代。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;临界区：通过对多线程的串行化来访问公共资源或一段代码，速度快，适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问，如果有多个线程试图访问公共资源，那么在有一个线程进入后，其他试图访问公共资源的线程将被阻塞，并一直等到进入临界区的线程离开，临界区在被释放后，其他线程才可以抢占。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虽然临界区同步速度很快，但却&lt;em&gt;只能用来同步本进程内的线程，而不可用来同步多个进程中的线程&lt;/em&gt;。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;互斥量：采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限，因为互斥对象只有一个，所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享，还能实现不同应用程序的公共资源安全共享&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;信号量：它允许多个线程在同一时刻访问同一资源，但是需要限制在同一时刻访问此资源的最大线程数目&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事件： 通过通知操作的方式来保持线程的同步，还可以方便实现对多个线程的优先级比较的操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;临界区和互斥量的区别：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;临界区只能用于对象在同一进程里线程间的互斥访问；互斥体可以用于对象进程间或线程间的互斥访问。&lt;/li&gt;
&lt;li&gt;临界区是非内核对象，只在用户态进行锁操作，速度快；互斥体是内核对象，在核心态进行锁操作，速度慢。&lt;/li&gt;
&lt;li&gt;临界区和互斥体在Windows平台都下可用；Linux下只有互斥体可用。&lt;/li&gt;
&lt;li&gt;临界区: 通过对多线程的串行化来访问公共资源或一段代码，速度快，适合控制数据访问。&lt;/li&gt;
&lt;li&gt;互斥量: 为协调共同对一个共享资源的单独访问而设计的&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;如何理解进程间通信，进程间同步和进程间互斥&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;通信是多个进程访问同一个资源。&lt;/li&gt;
&lt;li&gt;同步是让他们按照顺序来。&lt;/li&gt;
&lt;li&gt;互斥是不能一起来。&lt;/li&gt;
&lt;li&gt;所以互斥是一种特殊的同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以进程间的同步建立在访问上面。我如果没有访问同一个资源就没有同步的意义。同时我们也可以通过通信来告知其他线程同步和互斥的状态。所以进程间同步和进程间通信也可以算进程间通信。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以说，我们可以理解为，我们想要通过7种进程间通信的方式去访问共享资源，比如多进程操作一个文件，操作一块共享内存。&lt;/p&gt;
&lt;p&gt;但是我们需要他们按照顺序同步或互斥，我们就可以使用进程间同步的方法，比如互斥锁+信号量，互斥锁+共享内存，互斥锁+文件映射等等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们也知道，进程间是独立的。所以我这个进程不可能被其他进程看见，那么我这个进程的东西（比如锁）更不可能被其他进程看见。所以我们如果加锁，一定是给共享资源加锁。同时为了让进程都可以访问锁，锁自己也必须可以在进程间访问。所以给共享资源加锁自然是要针把锁放到如共享内存，内存映射上。这样多个进程访问同一个资源的时候才可以找到锁，然后再进行下一步动作。&lt;/li&gt;
&lt;li&gt;那么线程呢？我们知道线程是可以访问全局资源的。（全局资源在数据段。数据段是共享的）。所以我们可以给线程本身上锁，也就是使用线程间同步的方式，比如条件变量之类的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意一下锁的含义。为什么锁叫互斥量？不要认为锁和资源是挨着的。也就是资源不一定和锁连着。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;锁（互斥量）和资源是独立的。什么意思？假设我们有规定：想要打开抽屉，必须从桌子上拿走令牌。如果没有令牌则不能打开抽屉。所以说资源（抽屉）和锁（互斥量）不一定在一起。资源在抽屉里，锁在桌子上。两者是分离的。我只要确保每个人都可以访问到令牌（锁）和抽屉（资源）即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用共享内存和互斥锁进行多进程通信的例子：&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_35382207/article/details/106627826&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/weixin_44618297/article/details/124411195&lt;/p&gt;
&lt;h1&gt;共享内存 shmget 和 内存映射 mmap的区别&lt;/h1&gt;
&lt;p&gt;Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是&lt;strong&gt;虚拟内存&lt;/strong&gt;上，运行着进程), &lt;strong&gt;通过对这段内存的读取和修改, 实现对文件的读取和修改&lt;/strong&gt;。mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存。&lt;strong&gt;普通文件映射到进程地址空间后，进程可以像访问内存的方式对文件进行访问，不需要其他内核态的系统调用(read,write)去操作。用户对这段内存区域的修改可以直接反映到内核空间&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里是将设备或者硬盘存储的一块空间映射到物理内存，然后操作这块物理内存就是在操作实际的硬盘空间，不需要经过内核态传递。比如你的硬盘上有一个文件，你可以使用linux系统提供的mmap接口，将这个文件映射到进程一块虚拟地址空间，这块空间会对应一块物理内存，当你读写这块物理空间的时候，就是在读取实际的磁盘文件，就是这么直接高效。&lt;strong&gt;通常诸如共享库的加载都是通过内存映射的方式加载到物理内存的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;mmap系统调用并不完全是为了共享内存来设计的，它本身提供了不同于一般对&lt;strong&gt;普通文件&lt;/strong&gt;的访问的方式，&lt;strong&gt;进程可以像读写内存一样对普通文件进行操作（无需系统调用），IPC的共享内存是纯粹为了共享。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;详细点说就是**&lt;code&gt;mmap()&lt;/code&gt;中没有进行数据拷贝，真正的数据拷贝是在缺页中断处理时进行的**，**由于&lt;code&gt;mmap()&lt;/code&gt;将文件直接映射到用户空间，所以中断处理函数根据这个映射关系，直接将文件从硬盘拷贝到用户空间，只进行了 一次数据拷贝 。**因此，内存映射的效率要比read/write效率高。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;mmap是在磁盘上&lt;strong&gt;建立（或打开已有的）一个文件&lt;/strong&gt;，每个进程地址空间中开辟出一块空间进行映射(&lt;strong&gt;mmap每个进程都会有自己的内存映射区&lt;/strong&gt;)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说mmap操作的是文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而shm共享内存，每个进程最终会&lt;strong&gt;映射到同一块物理内存&lt;/strong&gt;。&lt;strong&gt;shm保存在物理内存，这样读写的速度肯定要比磁盘要快，但是存储量不是特别大。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享内存的方式原理就是将一份物理内存映射到不同进程各自的虚拟地址空间上，这样每个进程都可以读取同一份数据，从而实现进程通信。因为是通过内存操作实现通信，因此是一种最高效的数据交换方法。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;也就是说shm操作的是内存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相对于shm来说，mmap更加简单，调用更加方便，所以这也是大家都喜欢用的原因。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另外mmap有一个好处是当机器重启，因为mmap把文件保存在磁盘上，这个文件还保存了操作系统同步的映像，所以mmap不会丢失，但是shmget在内存里面就会丢失。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总之，shm是在内存中创建空间，每个进程映射到此处。内存映射是创建一个文件，并且映射到每个进程开辟的空间中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于mmap是文件，shm是内存，所以mmap比shm慢但是容量大。shm比mmap快但是容量小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;共享内存必须搭配锁来使用&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为系统不会给共享内存提供互斥和同步。比如如果一个进程读的同时另一个进程在写，就会出现问题。&lt;/li&gt;
&lt;li&gt;那我们是给进程上锁还是内存上锁？
&lt;ul&gt;
&lt;li&gt;进程是给共享资源（内存）上锁。&lt;/li&gt;
&lt;li&gt;线程可以给线程上锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;一些EPOLL的细节&lt;/h1&gt;
&lt;p&gt;参考：https://mp.weixin.qq.com/s/QYxRwfe_OI9LTv5QqL7Exw&lt;/p&gt;
&lt;h2&gt;epoll在什么时候被触发？也就是协议栈什么时候触发回调函数来通知epoll有事件？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;针对监听文件描述符，在三次握手完成之后，会往全连接队列中添加一个TCB线程控制块结点，然后触发一个回调函数，通知到epoll里面有个EPOLLIN事件。&lt;strong&gt;（三次握手完成后）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220901013121.png&quot; alt=&quot;QQ截图20220901013121&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;针对读写文件描述符，客户端发送一个数据包，协议栈接收后回复ACK，之后触发一个回调函数，通知到epoll里面有个EPOLLIN事件（可读）&lt;strong&gt;（接收数据回复ACK后）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220901013152.png&quot; alt=&quot;QQ截图20220901013152&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个连接的TCB里面都有一个sendbuf，在对端接收到数据并返回ACK以后，sendbuf就可以将这部分确认接收的数据清空，此时sendbuf里面就有剩余空间，此时触发一个回调函数，通知到epoll里面有个EPOLLOUT事件（可写）&lt;strong&gt;（发送数据收到ACK后）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220901013241.png&quot; alt=&quot;QQ截图20220901013241&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当对端发送close，在接收到fin后回复ACK，此时会调用回调函数，通知到epoll有个EPOLLIN事件（可读。客户端断开连接，服务端epoll会监听到EPOLLRDHUP）&lt;strong&gt;（接受FIN回复ACK后）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220901013351.png&quot; alt=&quot;QQ截图20220901013351&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当接收到rst标志位的时候，回复ack之后也会触发回调函数，通知epoll有一个EPOLLERR事件。&lt;strong&gt;（接收RST回复ACK后）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-26-%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220901013356.png&quot; alt=&quot;QQ截图20220901013356&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;EPOLL在哪里加锁了？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;epoll_ctl()&lt;/code&gt; 对红黑树加锁。因为是对同一颗红黑树进行，增删改，这就涉及到资源竞争需要加锁了，此时我们对整棵树进行加锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epoll_wait()&lt;/code&gt;对就绪队列加锁。因为操作的是就绪队列，所以需要对就绪队列进行加锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;回调函数() &lt;/code&gt; 对红黑树加锁,对就绪队列加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般来说红黑树在节点多的时候用互斥锁。就绪队列用自旋锁。&lt;/p&gt;
&lt;h1&gt;nginx坑点：&lt;/h1&gt;
&lt;p&gt;默认是http1.0转发。结果我的服务器只支持1.1&lt;/p&gt;
&lt;p&gt;默认轮询不知道为何不起作用，只有加权轮询才起作用。&lt;/p&gt;
&lt;p&gt;调用服务器日志发现会同时给两台服务器都发送请求。&lt;/p&gt;
&lt;h1&gt;项目介绍&lt;/h1&gt;
&lt;p&gt;我实现的linux webserver有如下几个模块。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心部分：基于epoll的多路复用来进行连接的监听，读取和发送。&lt;/li&gt;
&lt;li&gt;任务队列模块搭配线程池：epoll把客户端发送的数据读取进来后，封装成对象送入任务队列，线程池内的子线程不断从任务队列中获取任务。&lt;/li&gt;
&lt;li&gt;http解析：采用了主从状态机进行HTTP的解析和封装。封装完毕后通知主线程epoll任务可以发送了，主线程进行数据发送&lt;/li&gt;
&lt;li&gt;日志：同步和异步日志模块。异步日志也是有一个任务队列。单独开设一个日志线程。其他部分将日志内容送入任务队列，日志线程不断从任务队列中拿去日志然后写入文件。&lt;/li&gt;
&lt;li&gt;超时检测：基于链表的一个超时监测机制。主线程定时发送信号至管道，epoll也监听管道文件描述符。epoll监听到定时器信号后开始进行链表遍历，如果发现有链接一定时间没有操作，就关闭文件描述符释放资源。每次监听到文件描述符有操作后会更新绝对时间戳。也就是当前时间+3倍的timeslot。进行链接遍历的时候拿当前时间和这个时间比对，如果超时了就关闭连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;更新&lt;/h1&gt;
&lt;p&gt;map是为了防止sql注入。&lt;/p&gt;
</content:encoded></item><item><title>数据库</title><link>https://sterben-01.github.io/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/</guid><pubDate>Sat, 06 Aug 2022 14:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;数据库&lt;/h1&gt;
&lt;h2&gt;MYSQL结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;应用层&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链接处理&lt;/li&gt;
&lt;li&gt;鉴权&lt;/li&gt;
&lt;li&gt;安全管理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务层&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安全管理
&lt;ul&gt;
&lt;li&gt;备份恢复&lt;/li&gt;
&lt;li&gt;集群管理&lt;/li&gt;
&lt;li&gt;复制管理&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接口
&lt;ul&gt;
&lt;li&gt;DML&lt;/li&gt;
&lt;li&gt;DDL&lt;/li&gt;
&lt;li&gt;储存过程&lt;/li&gt;
&lt;li&gt;视图&lt;/li&gt;
&lt;li&gt;触发器&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;解析器&lt;/li&gt;
&lt;li&gt;优化器&lt;/li&gt;
&lt;li&gt;缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;储存引擎层 (索引在这里，所以不同引擎有不同的索引实现方式)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;储存引擎&lt;/li&gt;
&lt;li&gt;物理文件
&lt;ul&gt;
&lt;li&gt;redolog&lt;/li&gt;
&lt;li&gt;undolog&lt;/li&gt;
&lt;li&gt;binlog&lt;/li&gt;
&lt;li&gt;errorlog&lt;/li&gt;
&lt;li&gt;querylog&lt;/li&gt;
&lt;li&gt;slowlog&lt;/li&gt;
&lt;li&gt;data&lt;/li&gt;
&lt;li&gt;index&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;逻辑架构&lt;/h2&gt;
&lt;h3&gt;MYSQL执行流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先连接到数据库服务器后，&lt;strong&gt;查询缓存&lt;/strong&gt;。（8.0已弃用缓存。因为命中率很低）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有（命中），直接返回&lt;/li&gt;
&lt;li&gt;没有命中，开始查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用解析器进行语句分析&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;词法分析（关键字）&lt;/li&gt;
&lt;li&gt;语法分析（语法是否有误）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行查询优化然后得到执行计划。（优化器）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一条查询可以有很多执行方式，优化器作用是分析哪个效率比较高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遵循执行计划开始执行查询操作。（调用执行引擎）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询后返回结果，同时缓存。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以整体流程：&lt;/p&gt;
&lt;p&gt;SQL语句-&amp;gt;查询缓存（8.0弃用）-&amp;gt; 解析器 -&amp;gt; 优化器 -&amp;gt; 执行器&lt;/p&gt;
&lt;h2&gt;索引&lt;/h2&gt;
&lt;p&gt;索引是一种数据结构，可以帮助我们&lt;strong&gt;快速的进行数据查找。&lt;/strong&gt; &lt;strong&gt;可以理解为是个目录&lt;/strong&gt;。如果没有目录，我们就需要从头开始找。如果有了目录，可以直接找到对应位置。（不够完全准确）。索引是在引擎层实现的，不同引擎有不同的实现索引的方式。&lt;/p&gt;
&lt;h3&gt;索引即数据&lt;/h3&gt;
&lt;h3&gt;使用索引的目的：&lt;/h3&gt;
&lt;p&gt;优点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;减少&lt;strong&gt;磁盘&lt;/strong&gt;IO。因为MYSQL是磁盘读取的。&lt;/li&gt;
&lt;li&gt;创建唯一索引可以保证数据库表中每一行的数据唯一性&lt;/li&gt;
&lt;li&gt;可以加速和表之间的链接。&lt;/li&gt;
&lt;li&gt;使用分组和排序（group，orderby）的时候减少操作时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建和维护索引耗费时间。因为操作数据也会操作索引。&lt;/li&gt;
&lt;li&gt;索引也会占用磁盘空间。&lt;/li&gt;
&lt;li&gt;虽然索引加速查询时间，但是因为操作数据的时候会操作索引，所以对数据表更新的时候（添加，修改，删除）速度会变慢。因为索引也需要同时操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们可以在进行数据操作的时候，先删除索引，操作完数据后再添加索引。&lt;/p&gt;
&lt;p&gt;索引可能使用B树， B+树或hash等。&lt;/p&gt;
&lt;h3&gt;索引的分类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;功能分类：
&lt;ul&gt;
&lt;li&gt;普通索引
&lt;ul&gt;
&lt;li&gt;在创建普通索引时，不附加任何限制条件，只是用于提高查询效率。这类索引可以创建在任何数据类型中，其值是否唯一和非空，要由字段本身的完整性约束条件决定。建立索引以后，可以通过索引进行查询。例如，在表student的字段name上建立一个普通索引，查询记录时就可以根据该索引进行查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;唯一索引
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;UNIQUE&lt;/code&gt;参数可以设置索引为唯一性索引，在创建唯一性索引时，限制该索引的值必须是唯一的，但&lt;strong&gt;允许有空值&lt;/strong&gt;。在一张数据表里可以有多个唯一索引。&lt;/li&gt;
&lt;li&gt;例如，在表student的字段email中创建唯一性索引，那么字段email的值就必须是唯一的。通过唯一性索引，可以更快速地确定某条记录。|&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;主键索引
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主键索引就是一种特殊的唯一性索引&lt;/strong&gt;，在&lt;strong&gt;唯一索引的基础上增加了不为空的约束&lt;/strong&gt;，也就是&lt;code&gt;NOTNULL+UNIQUE&lt;/code&gt;，一张表里最多只有一个主键索引。&lt;/li&gt;
&lt;li&gt;这是由主键索引的物理实现方式决定的，因为数据存储在文件中只能按照一种顺序进行存储。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;全文索引
&lt;ul&gt;
&lt;li&gt;全文索引(也称全文检索）是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性，然后按照一定的算法规则智能地筛选出我们想要的搜索结果。全文索引非常适合大型数据集，对于小的数据集，它的用处比较小。使用参数FULLTEXT可以设置索引为全文索引。在定义索引的列上支持值的全文查找，允许在这些索引列中插入重复值和空值。全文索引只能创建在CHAR、VARCHAR或TEXT类型及其系列类型的字段上，查询数据量较大的字符串类型的字段时，使用全文索引可以提高查询速度。例如，表student的字段information是TEXT类型，该字段包含了很多文字信息。在字段information上建立全文索引后，可以提高查询字段information的速度。&lt;/li&gt;
&lt;li&gt;全文索引典型的有两种类型:自然语言的全文索引和布尔全文索引。&lt;/li&gt;
&lt;li&gt;自然语言搜索引擎将计算每一个文档对象和查询的相关度。这里，相关度是基于匹配的关键词的个数，以及关键词在文档中出现的次数。在整个索引中出现次数越少的词语，匹配时的相关度就越高。相反，非常常见的单词将不会被搜索，如果一个词语的在超过50%的记录中都出现了，那么自然语言的搜索将不会搜索这类词语。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;物理实现方式：
&lt;ul&gt;
&lt;li&gt;聚簇索引
&lt;ul&gt;
&lt;li&gt;聚簇索引是指的针对&lt;strong&gt;主键&lt;/strong&gt;构建的索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;非聚簇索引
&lt;ul&gt;
&lt;li&gt;非聚簇索引是指的针对非&lt;strong&gt;非主键&lt;/strong&gt;构建的索引。也叫二级索引或者辅助索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;作用字段个数：
&lt;ul&gt;
&lt;li&gt;单列索引
&lt;ul&gt;
&lt;li&gt;在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引，也可以是唯一性索引，还可以是全文索引。&lt;strong&gt;只要保证该索引只对应一个字段即可&lt;/strong&gt;。一个表可以有多个单列索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;联合索引
&lt;ul&gt;
&lt;li&gt;联合索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段，可以通过这几个字段进行查询，但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。例如，在表中的字段id、name和gender上建立一个多列索引` idx_id_name_gender’，只有在查询条件中使用了字段id时该索引才会被使用。如果一开始使用了字段name则不会被使用。&lt;strong&gt;使用联合索引时遵循最左匹配（前缀）原则。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;HASH结构&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;复杂度O(1)。所以从检索效率来看，HASH比树快。树是O(log2N)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是为什么我们还用树呢？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哈希索引只可以判断 &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;!=&lt;/code&gt;, 和 &lt;code&gt;IN&lt;/code&gt;查询。范围查询就不行了，复杂度会变成O(N)&lt;/li&gt;
&lt;li&gt;哈希索引数据存储是没有顺序的。&lt;code&gt;order by&lt;/code&gt; 会导致重新排序&lt;/li&gt;
&lt;li&gt;联合索引的情况下，哈希是把索引键合并后计算的，无法对单独一个或多个索引键查询&lt;/li&gt;
&lt;li&gt;索引列重复值很多，会导致哈希冲突，就会导致需要遍历桶内链表来进行比较。非常耗时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;树&lt;/h3&gt;
&lt;p&gt;单纯使用树的话，极端情况下树会退化成链表，会导致深度很大。我们知道树的每一层在数据库内代表一次IO操作。所以效率很差&lt;/p&gt;
&lt;h3&gt;AVL树（包括红黑树）&lt;/h3&gt;
&lt;p&gt;为了解决树深度过大的问题，我们有AVL树，也就是平衡二叉树。左右两侧树深度差不超过1.&lt;/p&gt;
&lt;p&gt;但是这个时候，虽然深度会降低，但是依旧深度很大，因为他是&lt;strong&gt;二叉树&lt;/strong&gt;。为此，为了降低深度，我们可以用&lt;strong&gt;多叉树&lt;/strong&gt;。来减少树的深度，增加树的广度。&lt;/p&gt;
&lt;h3&gt;B树&lt;/h3&gt;
&lt;p&gt;进一步优化AVL树且实施我们的多叉树策略，我们有B树&lt;/p&gt;
&lt;p&gt;B树作为多路平衡查找树，它的每一个节点最多可以包括M个子节点，M称为B树的阶。每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块中包括了x个关键字，那么指针数就是x+1。对于一个100阶的B树来说，如果有3层的话最多可以存储约100万的索引数据。对于大量的索引数据来说，采用B树的结构是非常适合的，因为树的高度要远小于二叉树的高度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-02-%E6%95%B0%E6%8D%AE%E5%BA%93.assets/QQ%E6%88%AA%E5%9B%BE20220803013831.png&quot; alt=&quot;QQ截图20220803013831&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;B树和B+树的区别：&lt;/h3&gt;
&lt;p&gt;关键字 = 索引（此处索引不仅可以是聚簇索引代表的主键，也可以是非聚簇索引当中创建索引的那个值本身）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最大的区别就是B+树只有叶子结点储存真实数据，其余节点只储存目录。但是B树不仅叶子节点可以储存真实数据，其余节点也可以储存。&lt;/li&gt;
&lt;li&gt;从上图看到了，B树的叶子结点之间没有联系。但是B+树的叶子结点使用的是双链表链接。&lt;/li&gt;
&lt;li&gt;B+树中，子节点数量和关键字数量相同。B树中子节点数量是关键字数量+1.&lt;/li&gt;
&lt;li&gt;B+树中，关键字（索引）自己本身不仅存在于目录节点中，也存在数据节点中。而且这个节点是子节点中所有关键字中最大或者最小的值。这也就是为什么我们子节点数量和关键字数量相同。而B树则不是，他是那种间隔形的。所以会+1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;B树的优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;B树可以在内部节点同时存储键（目录）和值（数据），因此，把&lt;strong&gt;频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率&lt;/strong&gt;。这种特性使得B树在特定数据重复多次查询的场景中更加高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;B+树的优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;（页大小是固定的）由于B+树的内部节点只存放键（目录），不存放值（数据），因此，在一页中可以存放更多的键（目录）。一次读取，可以在内存页中获取更多的键，有利于更快地缩小查找范围。&lt;/li&gt;
&lt;li&gt;B+树的叶节点由一条链相连，因此，当需要进行一次全数据遍历的时候，B+树只需要使用O(logN)时间找到最小的一个节点，然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历，这会需要更多的内存置换次数，因此也就需要花费更多的时间&lt;/li&gt;
&lt;li&gt;查询效率更稳定。因为B树可能有的时候节点距离根节点近，速度快，离得远则速度慢。而B+树不存在这个问题，因为数据都在叶子结点上面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;B+树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;B+树的每一个节点都是一个数据页。页内数据用链表链接。&lt;/li&gt;
&lt;li&gt;数据页之间可以不连续。所以数据页之间的实现方式是一个双向链表。&lt;/li&gt;
&lt;li&gt;叶子结点（第0层，最下层）储存的是我们的真实数据。数据之间是单向链表链接。
&lt;ul&gt;
&lt;li&gt;每一个叶子结点之间是&lt;strong&gt;链表&lt;/strong&gt;链接。因为可能有页分裂这种情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所有的非叶子结点储存的都是目录页（也是数据页）。我们理解为一种大目录套小目录。非叶子结点只储存键值（目录索引信息）。里面每一个值是一个指针（指向一个子节点）和一个索引值（子节点中最小的关键字）
&lt;ul&gt;
&lt;li&gt;目录和数据通过一个&lt;code&gt;record_type&lt;/code&gt;字段判断。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数据层都会为所有的主键生成一个页目录&lt;code&gt;page directory&lt;/code&gt;。所以用主键查找的时候可以使用二分法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般来讲，B+树不会超过4层。&lt;strong&gt;树的层次越低，我们IO次数越少&lt;/strong&gt;。因为我们理解为，在最上层进行IO，即加载数据页，找到第二层目录，然后把第二层目录IO进来，再次查找。以此类推。所以是树的层数越少，IO次数越少。然后，假设真实数据每个页是100个，然后每个目录页存1000个目录，那么树两层就是十万，三层就是一个亿。四层就是一千亿。&lt;/p&gt;
&lt;h3&gt;B+树分为聚簇索引和非聚簇索引&lt;/h3&gt;
&lt;h2&gt;聚簇索引&lt;/h2&gt;
&lt;p&gt;聚簇索引是指的针对&lt;strong&gt;主键&lt;/strong&gt;构建的索引。&lt;/p&gt;
&lt;p&gt;聚簇索引不是一种单独索引类型。而是一种数据存储方式。也就是我们上面B+树举的例子（所有真实数据都在叶子结点）。**以主键作为B+树索引的键值而构建的B+树索引，我们称之为聚集索引。换言之，在B+树中，叶子节点存储整条记录的数据，这样的索引为聚集索引。 **&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-02-%E6%95%B0%E6%8D%AE%E5%BA%93.assets/QQ%E6%88%AA%E5%9B%BE20220802180820.png&quot; alt=&quot;QQ截图20220802180820&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意这张图有缺陷。一个框（一个节点）的两个页之间有双向链表。这里表示的是页和页可以不连续。需要注意这一点。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;特点：&lt;/h3&gt;
&lt;p&gt;1.使用主键值的大小进行数据和页的排序，这包括三个方面的含义:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页内&lt;/strong&gt;的数据是按照主键的大小顺序排成一个单向链表。&lt;/li&gt;
&lt;li&gt;各个存放用户数据的页（叶子节点）也是根据页中用户数据的主键值大小顺序排成一个双向链表。&lt;/li&gt;
&lt;li&gt;存放目录数据的页（其他节点）分为不同的层次，在同一层次中的页也是根据页中目录数据的主键大小顺序排成一个双向链表。&lt;/li&gt;
&lt;li&gt;不需要显式使用&lt;code&gt;index&lt;/code&gt;创建索引。因为我们的数据表一定会有主键。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;优点：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;数据访问更快，因为聚簇索引将索引和数据保存在同一个B+树中，因此从聚簇索引中获取数据比非聚簇索引更快&lt;/li&gt;
&lt;li&gt;聚簇索引对于主键的&lt;strong&gt;排序查找&lt;/strong&gt;和&lt;strong&gt;范围查找&lt;/strong&gt;速度非常快&lt;/li&gt;
&lt;li&gt;按照聚簇索引排列顺序，查询显示一定范围数据的时候，由于数据都是紧密相连，数据库不用从多个数据块中提取数据，所以节省了大量的IO操作。（构建索引的优点）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缺点：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;插入速度&lt;strong&gt;严重依赖于插入顺序&lt;/strong&gt;，按照主键的顺序插入是最快的方式，否则将会出现页分裂，严重影响性能。因此，对于InnoDB表，我们一般都会定义一个自增的ID列为主键。&lt;/li&gt;
&lt;li&gt;更新主键的代价很高，因为将会导致被更新的行移动。因此，对于InnoDB表，我们一般定义主键为不可更新·二级索引访问需要两次索引查找，第一次找到主键值，第二次根据主键值找到行数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;限制：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对于MySQL数据库目前只有InnoDB数据引擎支持聚簇索引，而MylSAM并不支持聚簇索引。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;由于数据物理存储排序方式只能有一种，所以每个MysQL的表只能有一个聚簇索引。一般情况下就是该表的主键。所以一个表只可以有一个主键。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果没有定义主键，Innodb会选择非空的唯一索引代替。如果没有这样的索引，Innodb会隐式的定义一个主键来作为聚簇索引。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;为了充分利用聚簇索引的聚簇的特性，所以innodb表的主键列&lt;strong&gt;尽量选用有序的顺序id，而不建议用无序的id&lt;/strong&gt;，比如UUID、MD5、HASH、字符串列作为主键无法保证数据的顺序增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;非聚簇索引&lt;/h2&gt;
&lt;p&gt;非聚簇索引是指的针对非&lt;strong&gt;非主键&lt;/strong&gt;构建的索引。也叫二级索引或者辅助索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;针对主键进行构建索引是聚簇索引。如果对非主键进行索引构建就需要构建非聚簇索引。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非聚集索引的叶子节点不存储表中的数据，而是存储该列对应的主键，&lt;strong&gt;想要查找数据我们还需要根据主键再去聚集索引中进行查找&lt;/strong&gt;，这个再根据聚集索引查找数据的过程，我们称为&lt;strong&gt;回表&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-02-%E6%95%B0%E6%8D%AE%E5%BA%93.assets/QQ%E6%88%AA%E5%9B%BE20220802182154.png&quot; alt=&quot;QQ截图20220802182154&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们这里看到叶子结点的蓝色部分储存的都是我们数据的非主键值。我们对此创建了索引的时候这里就是非聚簇索引。这里我们蓝色部分也是按照升序排列的。对应的黄色部分是主键的值。而且我们发现，这里叶子结点仅仅储存了我们创建索引的那一列的值（蓝色部分），和该索引对应的主键（黄色部分）。没有其余数据列（紫色的值）。因为非聚簇索引不储存表中的数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假设我们现在是&lt;code&gt;select *&lt;/code&gt;。意思是返回的时候必须返回所有的值，即黄色+蓝色+紫色。但是假设我们现在是通过非聚簇索引查找，也就是通过蓝色值查找。那么我们就会用到上面的这个树。我们会首先通过蓝色的值拿到主键（黄色的值），然后再次通过主键去找到对应的数据条目。这个过程就叫做&lt;strong&gt;回表&lt;/strong&gt;。这个过程会查两次B+树。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非聚簇索引不影响数据在聚簇索引中的组织。所以可以有多份。也就是你可以用&lt;code&gt;index&lt;/code&gt;建多个索引。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么非聚簇索引不保存数据（为何要回表）&lt;/h3&gt;
&lt;p&gt;因为每一个表可以有一个聚簇索引和多个非聚簇索引。如果非聚簇索引也保存全部表数据，那么整个表的数据会储存很多份。是无意义的。&lt;/p&gt;
&lt;h3&gt;使用聚簇索引和非聚簇索引的场景&lt;/h3&gt;
&lt;p&gt;使用聚簇索引查询效率高。因为无需回表操作。但是修改效率低。可能导致页分裂。&lt;/p&gt;
&lt;p&gt;使用非聚簇索引修改效率高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-02-%E6%95%B0%E6%8D%AE%E5%BA%93.assets/%E6%9C%AA%E5%91%BD%E5%90%8D%E5%9B%BE%E7%89%87.png&quot; alt=&quot;未命名图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;联合索引&lt;/h2&gt;
&lt;p&gt;MySQL可以使用多个字段同时建立一个索引，叫做联合索引。&lt;strong&gt;在联合索引中，如果想要命中索引，需要按照建立索引时的字段顺序挨个使用，否则无法命中索引。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体原因为:&lt;/p&gt;
&lt;p&gt;MySQL使用索引时需要索引有序，假设现在建立了&quot;name，age，school&quot;的联合索引，那么索引的排序为: &lt;strong&gt;先按照name排序，如果name相同，则按照age排序，如果age的值也相等，则按照school进行排序。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**当进行查询时，此时索引仅仅按照name严格有序，因此必须首先使用name字段进行等值查询，之后对于匹配到的列而言，其按照age字段严格有序，此时可以使用age字段用做索引查找，以此类推。**因此在建立联合索引的时候应该注意索引列的顺序，一般情况下，将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-02-%E6%95%B0%E6%8D%AE%E5%BA%93.assets/QQ%E6%88%AA%E5%9B%BE20220802184453.png&quot; alt=&quot;QQ截图20220802184453&quot; /&gt;&lt;/p&gt;
&lt;p&gt;联合索引把使用频繁，区分度高的放在最左侧。为了可以复用索引而不是新建索引。比如把a拓展成(a,b) 而不是新建(a,b)&lt;/p&gt;
&lt;h2&gt;覆盖索引&lt;/h2&gt;
&lt;p&gt;我们说过在使用非聚簇索引的时候需要回表。因为非聚簇索引索引储存的是索引和其对应的主键值。但是如果我们使用的是联合索引呢？我们上文也提到了，联合索引可以把两个字段对应的信息+对应主键的值存起来。（只储存对应字段+主键，并不储存全表数据）。假如我们的联合索引（非聚簇索引）在使用的时候，刚好满足了我们的查询需求，是不是就不用回表查询了呢。或者是我们不使用联合索引，但是我们在非聚簇索引查找的时候，刚好要查找的数据是主键。是不是也不用回表了呢？&lt;/p&gt;
&lt;p&gt;举个例子，假如我们现在表有&lt;code&gt;C1 C2 C3 C4 C5&lt;/code&gt;。&lt;code&gt;C1&lt;/code&gt;是主键。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们假如想要依照&lt;code&gt;C2&lt;/code&gt;来查找数据，我们会给&lt;code&gt;C2&lt;/code&gt;加一个非聚簇索引。然后找到了对应的主键然后再去聚簇索引中查找（回表）。假如我们现在是&lt;code&gt;SELECT C1 FROM table WHERE C2 = ...&lt;/code&gt;。我们通过&lt;code&gt;C2&lt;/code&gt;的非聚簇索引查找。但是我们发现&lt;code&gt;C1&lt;/code&gt;刚刚好也在索引中，则不用回表。&lt;/li&gt;
&lt;li&gt;假如我们现在创建了联合索引。&lt;code&gt;C2&lt;/code&gt;和&lt;code&gt;C3&lt;/code&gt;。我们现在是&lt;code&gt;SELECT C1, C2, C3 FROM table WHERE C2 = ... AND C3 =... &lt;/code&gt;(注意遵循最左匹配)。我们通过&lt;code&gt;C2&lt;/code&gt;和&lt;code&gt;C3&lt;/code&gt;的联合索引查找，然后发现需要返回的数据&lt;code&gt;C1 C2 C3&lt;/code&gt;全部都在这个联合索引内。所以不用回表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最左匹配&lt;/h2&gt;
&lt;p&gt;我们上面提到了，联合索引需要符合最左匹配。什么意思？&lt;/p&gt;
&lt;p&gt;假如我们创建的是&lt;code&gt;C2 C3 C4&lt;/code&gt;的联合索引，那么我们使用的时候，必须按照这个顺序来。也就是下列三种情况可以触发索引：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C2&lt;/li&gt;
&lt;li&gt;C2 C3&lt;/li&gt;
&lt;li&gt;C2 C3 C4&lt;/li&gt;
&lt;li&gt;C2 C4 这种情况叫索引截断，会触发索引下推功能。&lt;/li&gt;
&lt;li&gt;C2% C3同上&lt;/li&gt;
&lt;li&gt;C2% C4同上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为储存排序的时候是按照&lt;code&gt;C2&lt;/code&gt;排序，如果&lt;code&gt;C2&lt;/code&gt;相等则按照&lt;code&gt;C3&lt;/code&gt;排序，如果&lt;code&gt;C3&lt;/code&gt;相等按照&lt;code&gt;C4&lt;/code&gt;排序。也就是按照前一个字段的排序基础上再进行下一个字段的排序。也就是整体上，只有第一个字段的索引是绝对有序的，剩下字段的索引是相对有序。这个最左匹配可以是最左侧的N个字段或者是N个字符。&lt;/p&gt;
&lt;h2&gt;索引下推&lt;/h2&gt;
&lt;p&gt;假设我们有一个市民表。我们有一个联合索引（name, age）。如果现在有一个需求：检索出表中“名字第一个字是张，而且年龄是10岁的所有男孩”。那么，SQL语句是这么写的&lt;code&gt;select * from tuser where name like &apos;张%&apos; and age=10 and ismale=1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意这里用了模糊匹配。虽然模糊匹配在通配符后置的时候可以用到索引，但是模糊匹配后面的联合索引也会失效（推测叫索引截断）。所以个语句在搜索索引树的时候，只能用 “张”，找到第一个满足条件的记录后，一个个回表然后对比字段。（我们这里是select*，不满足索引覆盖）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-02-%E6%95%B0%E6%8D%AE%E5%BA%93.assets/QQ%E6%88%AA%E5%9B%BE20220802233135.png&quot; alt=&quot;QQ截图20220802233135&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们注意到，因为是模糊匹配了（产生了索引截断），数据库不关心age是什么。他会返回所有模糊匹配的情况然后回表查询四次。&lt;/p&gt;
&lt;p&gt;但是我们有了索引下推后，数据库在联合索引(name, age)内部就判断了age是否是10。如果不是10就跳过，所以只需要回表两次。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-02-%E6%95%B0%E6%8D%AE%E5%BA%93.assets/QQ%E6%88%AA%E5%9B%BE20220802233445.png&quot; alt=&quot;QQ截图20220802233445&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;索引失效场景：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;对索引使用了左模糊匹配或者左右模糊匹配（like)
&lt;ul&gt;
&lt;li&gt;%data 或%data%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;使用函数&lt;/li&gt;
&lt;li&gt;进行表达式计算&lt;/li&gt;
&lt;li&gt;范围条件右边的列索引失效。（&amp;gt;,&amp;lt;)&lt;/li&gt;
&lt;li&gt;不等于（！=， &amp;lt;&amp;gt;)&lt;/li&gt;
&lt;li&gt;隐式类型转换&lt;/li&gt;
&lt;li&gt;没有遵循最左匹配原则。&lt;/li&gt;
&lt;li&gt;where字句中的or
&lt;ul&gt;
&lt;li&gt;如果or前的条件列是索引列，or后的不是索引列，索引会失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字符串条件没有使用 &apos;&apos;&lt;/li&gt;
&lt;li&gt;两列数据做比较，即便两列都创建了索引，索引也会失效。&lt;/li&gt;
&lt;li&gt;查询条件使用is null时正常走索引，使用is not null时，不走索引。&lt;/li&gt;
&lt;li&gt;查询条件使用not in时，如果是主键则走索引，如果是普通索引，则索引失效。&lt;/li&gt;
&lt;li&gt;查询条件使用not exists时，索引失效。&lt;/li&gt;
&lt;li&gt;当查询条件涉及到order by、limit等条件时，是否走索引情况比较复杂，而且与Mysql版本有关，通常普通索引，如果未使用limit，则不会走索引。order by多个索引字段时，可能不会走索引。其他情况，建议在使用时进行expain验证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;适合创建索引的情况：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字段数值有唯一性限制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;索引本身有约束作用。比如唯一索引和主键索引。所以如果某个字段是唯一的，可以直接创建唯一性索引或者是主键索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;频繁做为&lt;code&gt;WHERE&lt;/code&gt;查询条件的列&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;做为外键的数据列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;经常&lt;code&gt;GROUP BY&lt;/code&gt; 或 &lt;code&gt;ORDER BY&lt;/code&gt;的列&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;UPDATE DELETE&lt;/code&gt;的&lt;code&gt;WHERE&lt;/code&gt;条件列&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DISTINCT&lt;/code&gt;字段需要创建索引&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多表JOIN链接（查询）操作，注意创建索引的细节。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，连接表的数量尽量不要超过3张，因为每增加一张表就相当于增加了一次嵌套的循环，数量级增长会非常快，严重影响查询的效率。&lt;/li&gt;
&lt;li&gt;其次，对 WHERE 条件创建索引，因为WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下，没有WHERE条件过滤是非常可怕的。&lt;/li&gt;
&lt;li&gt;最后，对用于连接的字段创建索引，并且该字段在多张表中的类型必须一致。比如course_id在student_info表和course表中都为int(11)类型，而不能一个为int另一个为varchar类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用列的类型小的创建索引。我们这里所说的类型大小指的就是该类型表示的数据范围的大小。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们在定义表结构的时候要显式的指定列的类型，以整数类型为例，有TINYINT、MEDIUMINT、INT，BIGINT等，它们占用的存储空间依次递增，能表示的整数范围当然也是依次递增。如果我们想要对某个整数列建立索引的话，在表示的整数范围允许的情况下，尽量让索引列使用较小的类型，比如我们能使用INT就不要使用BIGINT，能使用MEDIUMINT 就不要使用INT。这是因为&lt;strong&gt;数据类型越小，在查询时进行的比较操作越快 。数据类型越小，索引占用的存储空间就越少，在一个数据页内就可以放下更多的记录，从而减少磁盘I/0带来的性能损耗，也就意味着可以把更多的数据页缓存在内存中，从而加快读写效率。&lt;/strong&gt; 这个建议对于表的主键来说更加适用，因为不仅是聚簇索引中会存储主键值，其他所有的二级索引的节点处都会存储一份记录的主键值，如果主键使用更小的数据类型，也就意味着节省更多的存储空间和更高效的I/0。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用字符串前缀创建索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设我们的字符串很长，那存储一个字符串就需要占用很大的存储空间。在我们需要为这个字符串列建立索引时，那就意味着在对应的B+树中有这么两个问题:
&lt;ul&gt;
&lt;li&gt;·B+树索引中的记录需要把该列的完整字符串存储起来，更费时。而且字符串越长，在索引中占用的存储空间越大。&lt;/li&gt;
&lt;li&gt;如果B+树索引中索引列存储的字符串很长，那在做字符串比较时会占用更多的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;我们可以通过截取字段的前面一部分内容建立索引，这个就叫&lt;strong&gt;前缀索引&lt;/strong&gt;。这样在查找记录时虽然不能精确的定位到记录的位置，但是能定位到相应前缀所在的位置，然后根据前缀相同的记录的主键值回表查询完整的字符串值。既节约空间，又减少了字符串的比较时间，还大体能解决排序的问题。例如，TEXT和BLOG类型的字段，进行全文检索会很浪费时间，如果只检索字段前面的若干字符，这样可以提高检索速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;区分度高的（重复值少的）列适合做为索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列的基数指的是某一列中不重复数据的个数，比方说某个列包含值2，5，8，2，5，8，2，5，8，虽然有9条记录，但该列的基数却是3。也就是说，在记录行数一定的情况下，列的基数越大，该列中的值越分散;列的基数越小，该列中的值越集中。这个列的基数指标非常重要，直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引，为基数太小列的建立索引效果可能不好。&lt;/li&gt;
&lt;li&gt;联合索引把区分度高(散列性高)的列放在前面。这样可以提升性能。&lt;/li&gt;
&lt;li&gt;所以比如像性别，这种值只有几个种类的列，不适合做为索引。反而会降低数据更新速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用最频繁的列放到联合索引的左侧&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这样也可以较少的建立一些索引。同时，由于&quot;最左前缀原则&quot;，可以增加联合索引的使用率.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在多个字段都要创建索引的情况下，联合索引优于单值索引。做到了索引复用。&lt;strong&gt;尽量扩展索引而不是新建索引&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;不适合创建索引的情况：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;where&lt;/code&gt;中使用不到的字段，不要设置索引。（包括&lt;code&gt;GROUP BY, ORDER BY&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;数据量小的表不要使用索引
&lt;ul&gt;
&lt;li&gt;因为效率替身不大，反而索引占据磁盘空间，而且可能触发回表操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有大量重复值的列不适合作为索引（高于10%）
&lt;ul&gt;
&lt;li&gt;上面提到过，比如性别就不适合。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;避免对经常更新的表（字段）创建&lt;strong&gt;过多的&lt;/strong&gt;索引
&lt;ul&gt;
&lt;li&gt;更新字段的时候也要更新索引，浪费性能。&lt;/li&gt;
&lt;li&gt;更新的表创建过多的索引会导致表更新的速度降低。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;尽量不要使用无序的值作为索引
&lt;ul&gt;
&lt;li&gt;因为索引是有序的。使用无序值的话，插入的时候，更新索引会导致大量的中间插入，导致页分裂（数据转移）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;删除很少使用的索引&lt;/li&gt;
&lt;li&gt;不要定义冗余/重复索引
&lt;ul&gt;
&lt;li&gt;也就是尽量做到索引复用，使用联合索引。比如&lt;code&gt;index(a)&lt;/code&gt;, &lt;code&gt;index(a,b)&lt;/code&gt; 和 &lt;code&gt;index(a,b,c)&lt;/code&gt; 这三个索引根据最左匹配原则相当于&lt;code&gt;index(a,b,c)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重复索引比如给主键增加了一个唯一索引。没有意义。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建索引的注意事项（附加）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;尽量设置为&lt;code&gt;NOT NULL&lt;/code&gt;，可以让优化器优化。&lt;/li&gt;
&lt;li&gt;使用数据类型小的字段。&lt;/li&gt;
&lt;li&gt;取离散值大的字段。&lt;/li&gt;
&lt;li&gt;索引不宜太多，一般5个以内。
&lt;ul&gt;
&lt;li&gt;索引并不是越多越好，虽其提高了查询的效率，但却会降低插入和更新的效率；&lt;/li&gt;
&lt;li&gt;索引可以理解为一个就是一张表，其可以存储数据，其数据就要占空间；&lt;/li&gt;
&lt;li&gt;索引表的数据是排序的，排序也是要花时间的；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert&lt;/code&gt;或&lt;code&gt;update&lt;/code&gt;时有可能会重建索引，如果数据量巨大，重建将进行记录的重新排序，所以建索引需要慎重考虑，视具体情况来定；&lt;/li&gt;
&lt;li&gt;一个表的索引数最好不要超过5个，若太多需要考虑一些索引是否有存在的必要；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;索引优化&lt;/h2&gt;
&lt;h3&gt;物理查询优化（比如使用索引，表连接）&lt;/h3&gt;
&lt;h3&gt;逻辑查询优化（换一种写法）&lt;/h3&gt;
&lt;h2&gt;EXPLAIN&lt;/h2&gt;
&lt;p&gt;EXPLAIN是我们提到的优化器部分的东西。可以&lt;strong&gt;模拟优化器执行SQL查询语句，并不会去真正的执行这条SQL&lt;/strong&gt;，从而知道 MySQL 是如何处理你的SQL语句的。可用来分析你的查询语句或是表结构的性能瓶颈。&lt;/p&gt;
&lt;h3&gt;它能做什么？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表的读取顺序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据读取操作的操作类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哪些索引可以使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哪些索引被实际使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表之间的引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每张表有多少行被优化器查询&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;它里面有什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;id&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在一个大的查询语句中每个SELECT关键字都对应一个唯一的id。在有多条记录的时候可能会出现id相同的情况。因为他们用的是一个SELECT关键字。也就是几个SELECT，几个不同的ID&lt;/li&gt;
&lt;li&gt;id相同的时候，执行顺序从上到下。&lt;/li&gt;
&lt;li&gt;id不同的时候，如子查询（嵌套查询会有新的select）的时候，序号会递增。ID值越大的越先执行，因为需要先执行子查询，再执行外部查询。很好理解。&lt;/li&gt;
&lt;li&gt;id相同+不同，id越大越先执行+从上到下顺序执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;select_type&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表示该条SQL查询的类型，如子查询、联合查询等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;simple&lt;/code&gt;
简单的 select 查询，查询中不包含子查询或 union&lt;/li&gt;
&lt;li&gt;&lt;code&gt;primary&lt;/code&gt;
查询中若包含任何的子查询，最外层的查询被标记为 primary&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subquery&lt;/code&gt;
在 select 或 where 中包含子查询&lt;/li&gt;
&lt;li&gt;..太多了不是重点不写了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;table&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表名。表示用到了哪几张表，若出现了 derived 的情况，则表示产生了中间表，常见产生中间表的情况有子查询和联合查询等。&lt;/li&gt;
&lt;li&gt;查询的每一行记录都对应一个单表。使用了子查询或联合查询的时候就会有多条记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;partitions&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;匹配的分区信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;type&lt;/code&gt;&lt;/strong&gt; 重点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;针对单个表的访问方法。下面的这些种类靠前的性能越好，靠后的性能越差。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;system&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;表中只有一行记录（等同于系统表），这是const 类型的特列，平时不会出现，可以忽略不计&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;表示通过索引一次就查找到了这条记录，当我们根据主键索引或者是&lt;strong&gt;唯一&lt;/strong&gt;辅助索引与常数进行等值匹配的时候，对单表的访问方法就是const。因为只匹配一行数据，所以很快。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eq_ref&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;唯一索引扫描，对于每个索引建，表中只有一条记录与之匹配。在连接查询时，如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话，所有的索引列都必须进行等值比较），则对该被驱动表的访问方法就是eq ref&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;非唯一索引扫描，返回匹配某个单独值的所有行，本质也是一种索引访问，它返回某个匹配值的多行数据。当通过普通的二级索引列与常量进行等值匹配时来查询某个表，那么对该表的访问方法就可能是ref&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fulltext&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref_or_null&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;当对普通二级索引进行等值匹配查询，该索引列的值也可以是&lt;code&gt;NULL&lt;/code&gt;值时，那么对该表的访问方法就可能是ref or null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index_merge&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unique_subquery&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;是针对在一些包含IN子查询的查询语句中，如果查询优化器决定将IN子查询转换为EXISTS子查询，而且子查询可以使用到主键进行等值匹配的话，就是unique_subquery&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index_subquery&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range&lt;/code&gt;
只检索指定范围的行，使用一个索引来选择行，一般就是在你的where语句中出现了between、&amp;lt;、&amp;gt;、in等的查询，这种范围扫描索引比全表扫描要好，因为它只需要开始于索引的某一点，而结束于另一点，不用扫描全部索引.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt;
full index scan，index 与 all 的区别为 index 类型只遍历索引树，这通常比 all 快，因为索引文件通常比数据文件小，也就是说 index 和 all 虽然都是读全表，但index 是从索引中读取的，而all使用硬盘中读取&lt;/li&gt;
&lt;li&gt;&lt;code&gt;all&lt;/code&gt;
full table scan，将遍历全表以找到匹配的行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;possible_keys&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可能用到的索引&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;key&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实际用到的索引。每张表的一次查询只会用到一个索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;key_Len&lt;/code&gt;重点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实际用到的索引的长度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当使用索引列等值查询时，与索引列进行等职匹配的对象信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rows&lt;/code&gt;重点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预估需要读取的记录条数，越少越好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;filtered&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个表经过某个搜索条件过滤后剩余的记录条数百分比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;extra&lt;/code&gt;重点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;额外信息字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;事务&lt;/h2&gt;
&lt;p&gt;一组逻辑操作单元。使数据从一种状态转换为另外一种。&lt;/p&gt;
&lt;h3&gt;ACID&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;原子性：事务所有操作要么全部完成要么全部不完成。主要依靠&lt;code&gt;undo.log&lt;/code&gt;日志实现，即在事务失败时执行回滚。&lt;code&gt;undo.log&lt;/code&gt;日志会记录事务执行的SQL，当事务需要回滚时，通过反向补偿回滚数据库状态。&lt;strong&gt;（依靠事务日志）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;持久性：&lt;strong&gt;主要依靠事务日志&lt;/strong&gt;。修改的时候先修改日志，再对磁盘的数据修改。这样可以保证发生意外了，也可以进行恢复。
&lt;ul&gt;
&lt;li&gt;主要依靠&lt;code&gt;redo.log&lt;/code&gt;日志实现。首先，&lt;code&gt;mysql&lt;/code&gt;持久化通过缓存来提高效率，即在&lt;code&gt;select&lt;/code&gt;时先查缓存，再查磁盘；在&lt;code&gt;update&lt;/code&gt;时先更新缓存，再更新磁盘。以减少磁盘IO次数，提高效率。但由于缓存断电就没了，所以需要&lt;code&gt;redo.log&lt;/code&gt;日志。在执行修改操作时，SQL会先写入到&lt;code&gt;redo.log&lt;/code&gt;日志，再写入缓存中。这样即使断电，也能保证数据不丢失，达到持久性。&lt;/li&gt;
&lt;li&gt;redo日志是顺序写，在文件尾部进行添加操作。这样也算侧面实现了顺序写。但是数据库文件（innodb文件不是顺序写）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;隔离性：一个事物的执行不能被其他事务干扰。比如一个事物内部操作的数据不能被同时（并发）执行的其他事务所干扰。&lt;strong&gt;（就是加锁。线程安全）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;一致性：数据前后是从一个合法状态转变为另一个合法状态。不会出现非法状态的转移。具体是和业务有关的。比如余额不能小于0，因为我们规定他不能小于0.&lt;strong&gt;（依靠事务日志）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据并发问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;脏写
&lt;ul&gt;
&lt;li&gt;事务A修改了另外一个&lt;strong&gt;未提交的&lt;/strong&gt;事务B修改过的数据。&lt;/li&gt;
&lt;li&gt;比如原来的数据叫张三，事务B先把数据修改为李四，然后事务A把数据修改为王五。假如此时事务B进行了回滚，那么事务A的修改也将没有作用。也就是发现事务A修改了也提交了事务，但是数据没有变化。&lt;/li&gt;
&lt;li&gt;默认隔离级别下，事务A的更新语句会被阻塞（等待状态）。数据库默认会避免此情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;脏读
&lt;ul&gt;
&lt;li&gt;事务A读取了已经被事务B更新但是还未提交的数据。&lt;strong&gt;（读到了其他事务未提交的数据）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;假如事务B现在把余额从100改为200，但是此时没有提交。这时候事务A进行数据读取，他会读到一个200的余额。但是如果此时事务B发生了回滚，余额将会被还原至100。此时事务A在此前读取到的余额200将会是临时而且无效的。（过期的）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不可重复读
&lt;ul&gt;
&lt;li&gt;事务A在一个期间内重复多次读取同一个数据，但是数据发生了变化。&lt;strong&gt;（前后读取数据不一致）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;假如事务A现在读取了一个字段，假如是100。此时事务B修改并提交了这个字段为200。这个时候A再次读取发现这个值变为了200。这就是不可重复读。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;幻读
&lt;ul&gt;
&lt;li&gt;事务A在一个期间内多次查询符合条件的记录数量，出现了前后查询到的数据条数不一致。&lt;strong&gt;（前后读取记录数量不一致[多了]）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;假如事务A现在查询账户余额大于100的记录，发现有五条。这时，事务B对这个表进行了&lt;strong&gt;插入&lt;/strong&gt;。假设插入了三条账户余额大于100的记录，并且提交了（隐式）事务。此时事务A再次查询账户余额大于100的记录则会发现数据条数从五条变为八条。记录数量前后不一致了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt; 删除不算幻读。删除算作每一条记录都发生了不可重复读。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据隔离级别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;读未提交（read uncommitted)
&lt;ul&gt;
&lt;li&gt;事务可以看到其他未提交的事务的执行结果。此隔离级别不能避免脏读，不可重复读和幻读。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;读已提交（read committed)
&lt;ul&gt;
&lt;li&gt;事物只可以看到已经提交的事物的执行结果。次级别满足基础隔离级别。可以避免脏读，但是不可以避免不可重复读和幻读。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可重复读（repeatable read）
&lt;ul&gt;
&lt;li&gt;事务A读取到一条数据后，如果这时事务B对该数据进行了修改并提交，那么事务A再次读取该数据将不会发生改变。读取到的依旧是原来的数据。&lt;strong&gt;是MySQL InnoDB引擎的默认隔离级别&lt;/strong&gt;。可以避免脏读，不可重复读。不可避免幻读。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;串行化（serializable）
&lt;ul&gt;
&lt;li&gt;会对记录加上读写锁，在多个事务对这条记录进行读写操作时，如果发生了读写冲突的时候，后访问的事务必须等前一个事务执行完成，才能继续执行。也就是事务执行期间，禁止其他事物对该表进行修改，插入和删除操作。所有问题都可以避免，但是性能很低。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;redo log&lt;/code&gt;重做日志&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;提供了再写入操作。恢复提交事务修改的页操作。&lt;strong&gt;保证持久性。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;我们访问页面之前，需要让磁盘把页面数据缓存到内存中的&lt;code&gt;buffer pool&lt;/code&gt;中后在可以访问。所有的变更都必须先更新缓冲池的数据。然后缓冲池内的脏页（修改过的数据）会用一定的频率更新至磁盘。&lt;/li&gt;
&lt;li&gt;所以我们修改操作会先在内存内修改数据，然后同时记录日志。如果在持久化到磁盘之前发生错误，则可以通过重做日志重做之前丢失的操作。所以是操作是&lt;strong&gt;在内存操作，并写入日志，然后系统等待一个合适的时机再写入磁盘。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;重做日志也有自己的缓存。也是先写入重做日志缓存。然后再写入日志磁盘文件。&lt;/li&gt;
&lt;li&gt;重做日志是循环写的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;为什么需要&lt;code&gt;redolog&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;恢复在内存中但突发故障没有刷入磁盘导致丢失的操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只要修改就把数据写入磁盘会导致大量的IO操作。哪怕修改一个字节也要读取整个页面然后再写回整个页面，非常耗费性能。&lt;/li&gt;
&lt;li&gt;随机IO刷新较慢。因为一个事务可能包含多个语句，就算一条语句也可能修改多个页面。这些页面可能不相邻。不相邻就会意味着数据库在把内存中（缓冲池）中的数据刷入硬盘的的时候会进行很多随机IO。比如机械硬盘需要巡道。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;优点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;降低刷盘频率。&lt;/li&gt;
&lt;li&gt;日志占用空间小。因为只储存表ID，页号，偏移量和需要更新的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;特点&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;redo日志是顺序写入磁盘的&lt;/strong&gt;。因为每条日志都是在日志文件末尾进行追加。所以是顺序写入磁盘的。也就是用了顺序IO，速度较快。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;undo log&lt;/code&gt;回滚日志&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;回滚记录到某个特定版本。&lt;strong&gt;保证原子性和一致性。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在事务中&lt;strong&gt;更新数据之前&lt;/strong&gt;，需要先把对应操作的逆向操作写入回滚日志。select操作不写入回滚日志。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undo log&lt;/code&gt;会产生&lt;code&gt;redo log&lt;/code&gt;。因为回滚日志也需要持久性保护。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;为什么需要&lt;code&gt;undo log&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;回滚数据。
&lt;ul&gt;
&lt;li&gt;注意，回滚数据是&lt;strong&gt;逻辑层面恢复而不会物理性恢复&lt;/strong&gt;，也就是不会恢复原来的数据结构或者是页面具体的物理特性，比如开辟内存空间等，仅仅是把操作进行逆向操作。比如删除的数据，恢复回去。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MVCC
&lt;ul&gt;
&lt;li&gt;InnoB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时，若该记录已经被其他事务占用，当前事务可以通过undo读取之前的行版本信息，以此实现非锁定读取。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;两个日志底层的区别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redolog&lt;/code&gt;是储存引擎层生成的日志。记录的是&lt;strong&gt;物理级别&lt;/strong&gt;的页修改操作。比如在页号A，偏移量B的位置写入了C。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undolog&lt;/code&gt;是储存引擎层生成的日志。记录的是&lt;strong&gt;逻辑操作&lt;/strong&gt;。比如&lt;code&gt;INSERT&lt;/code&gt;了一行数据，那么&lt;code&gt;undolog&lt;/code&gt;内会记录一个相反的&lt;code&gt;DELETE&lt;/code&gt;操作。（每一个修改的逆向操作）&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;redo log跟bin log 的区别，redo log是存储引擎层产生的，而bin log是数据库层产生的。假设一个事务，对表做10万行的记录插入，在这个过程中，一直不断的往redo logl顶序记录，而bin log不会记录，直到这个事务提交，才会一次写入到bin log文件中。&lt;/h5&gt;
&lt;h2&gt;锁&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从数据操作类型划分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享锁（读锁）
&lt;ul&gt;
&lt;li&gt;两个事务一起读没问题。不阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;排他锁（写锁）
&lt;ul&gt;
&lt;li&gt;在一个事物写入没有完成之前，&lt;strong&gt;禁止其他事务进行写入和读取操作。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对于InnoDB来说，读锁和写锁可以加在表上也可以加在行上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从数据操作粒度划分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;锁定的数据量越少，发生锁争用的可能就越小，系统的并发程度就越高。但锁的各种操作(获取锁、释放锁、检查锁状态)都会增加系统开销。因此锁的粒度越小，系统开销就越大。因此在选择锁的粒度时，需要在锁开销和并发程度做平衡。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个数据库全部锁定。&lt;/li&gt;
&lt;li&gt;一般用于全库的逻辑备份&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表（级）锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表锁响应的是非索引字段。&lt;/li&gt;
&lt;li&gt;表锁会锁定整张表，是最基本的策略，&lt;strong&gt;不依赖存储引擎&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;因为粒度大，所以开销小，加锁快。&lt;strong&gt;发生锁冲突几率高&lt;/strong&gt;。 &lt;strong&gt;不会死锁。但是并发性能低&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;意向锁
&lt;ul&gt;
&lt;li&gt;为了协调行锁和表锁的关系。支持多个粒度的锁并存。&lt;/li&gt;
&lt;li&gt;举个例子。假如T1对表的某一行加锁了。如果此时T2要对整张表加锁，则需要遍历（假如有10万条，开销很大）这张表有没有某个部分已经被加锁了。如果有则需要等待。意向锁的作用就是，如果我们加了行锁，系统会在他的上一层加一个意向锁，告诉系统这张表/这个页已经有锁了。可以达到快速判断。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自增锁。
&lt;ul&gt;
&lt;li&gt;很好理解。一个字段设置为自增的时候我们不用显式赋值。系统会自动增添。为了保证增添这个自增字段的顺序，会有一个自增锁防止多条插入的时候对这个自增值同时修改。好理解。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;元数据锁。
&lt;ul&gt;
&lt;li&gt;保证表结构一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页（级）锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁定一个数据页。开销介于表锁和行锁之间。会发生死锁，并发度一般。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;行（级）锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MYSQL的行锁依赖于索引。因为行锁要加在索引对应的行上。&lt;/li&gt;
&lt;li&gt;锁住具体的某一行（某条记录）。&lt;strong&gt;依赖储存引擎。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;锁粒度小，开销大，加锁慢。发生锁冲突几率低。会出现死锁情况，并发性能高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MyISAM不支持行锁。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;记录锁
&lt;ul&gt;
&lt;li&gt;仅仅锁住一条记录&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;间隙锁
&lt;ul&gt;
&lt;li&gt;主要是为了在RR（可重复读取）隔离级别下的幻读问题。幻读是记录前后不一致。也可用MVCC解决。&lt;strong&gt;间隙锁锁定一个范围，不包括记录本身&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;因为事务执行第一次操作的时候，幻读导致的记录还不存在，导致没办法给幻读的数据加记录锁。所以我们可以在这个区间设置一个间隙锁，禁止在某个区间插入。可以避免幻读。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;临键锁
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;相当于记录锁+间隙锁。锁住一个范围包括记录本身&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;死锁&lt;/h3&gt;
&lt;p&gt;死锁是指两个或多个事务在同一资源上相互占用，并请求锁定对方的资源，从而导致恶性循环的现象。&lt;/p&gt;
&lt;h4&gt;解决方案：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;一直等待直到超时。&lt;/li&gt;
&lt;li&gt;判断如果一个操作会导致死锁，则进行回滚。选择成本最低的（持有最少的行级排他锁的事物回滚）。&lt;/li&gt;
&lt;li&gt;尽可能的一次锁定所需要的所有资源。&lt;/li&gt;
&lt;li&gt;容易产生死锁的部分可以提升锁粒度，比如使用表锁。&lt;/li&gt;
&lt;li&gt;使用分布式或乐观锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;隔离级别和锁的关系&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在Read Uncommitted级别下，读取数据不需要加共享锁，这样就不会跟被修改的数据上的排他锁冲突&lt;/li&gt;
&lt;li&gt;在Read Committed级别下，读操作需要加共享锁，但是在语句执行完以后释放共享锁；&lt;/li&gt;
&lt;li&gt;在Repeatable Read级别下，读操作需要加共享锁，但是在事务提交之前并不释放共享锁，也就是必须等待事务执行完毕以后才释放共享锁。&lt;/li&gt;
&lt;li&gt;SERIALIZABLE 是限制性最强的隔离级别，因为该级别锁定整个范围的键，并一直持有锁，直到事务完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MVCC&lt;/h2&gt;
&lt;p&gt;多版本并发控制。解决并发问题（一致性读），总加锁性能低，所以用这个。&lt;/p&gt;
&lt;p&gt;一致性读也就是快照读。也就是我读取的时候如果某个数据被更新了，我需要读取更新之前的值。这样查询的时候不需要等到另外的事务释放锁。如果读到了其他事务更新的值，那就是脏读。如果一直在等，那就是串行化了。&lt;/p&gt;
&lt;p&gt;不加锁的简单SELECT都叫快照读。也就是不加锁的非阻塞读。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MVCC = 隐藏字段+ undolog + readview&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只有InnoDB支持&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;零碎知识点：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;数据库的一个页一般是16KB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据页物理上不连续，逻辑连续。是双向链表。数据页的编号也可能不是顺序的。&lt;/strong&gt;（不然一个大数据库上哪儿弄那么多连续空间）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据页里面的记录按照主键顺序组成单向链表。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;视图：将重复使用的联结查询的表创建为一个虚拟表，方便后续查询使用。&lt;/li&gt;
&lt;li&gt;游标：数据缓冲区，用于存放语句执行结果。可以上下查看前后的行&lt;/li&gt;
&lt;li&gt;储存过程：函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;可能的题：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;为了减少IO，索引树会一次性加载吗?
&lt;ul&gt;
&lt;li&gt;数据库索引是存储在磁盘上的，如果数据量很大，必然导致索引的大小也会很大，超过几个G。&lt;/li&gt;
&lt;li&gt;当我们利用索引查询时候，是不可能将全部几个G的索引都加载进内存的，我们能做的只能是:逐一加载每一个磁盘页，因为磁盘页对应着索引树的节点。所以我们选用了B+树。让磁盘IO尽可能低。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B+树的存储能力如何?为何说一般查找行记录，最多只需1~3次磁盘IO
&lt;ul&gt;
&lt;li&gt;InnoDB存储引擎中页的大小为16KB，一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节)，指针类型也一般为4或8个字节，也就是说一个页(B+Tree 中的一个节点)中大概存储&lt;/li&gt;
&lt;li&gt;16KB/(8B+8B)=1K个键值(因为是估值，为方便计算，这里的K取值为10^3。也就是说一个深度为3的B+Tree索引可以维护10^3 &lt;em&gt;10^3&lt;/em&gt;103= 10亿条记录。(这里假定一个数据页也存储10^3条行记录数据了)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;为什么说B+树比B树更适合实际应用中操作系统的文件索引和数据库索引?
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B+树的磁盘读写代价更低。B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小&lt;/strong&gt;。如果把所有同一内部结点的关键字存放在同一盘块中，那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说Io读写次数也就降低了。&lt;strong&gt;树高更低，磁盘IO次数更少。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;B+树的查询效率更加稳定。由于非终结点并不是最终指向文件内容的结点，而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同，导致每一个数据的查询效率相当。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B+树叶子结点用双向链表链接。适合范围查询。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Hash索引B+树索引的区别
&lt;ul&gt;
&lt;li&gt;我们之前讲到过B+树索引的结构，Hash索引结构和B+树的不同，因此在索引使用上也会有差别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hash索引不能进行范围查询&lt;/strong&gt;，而B+树可以。这是因为Hash索引指向的数据是无序的，而B+树的叶子节点是个有序的链表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hash索引不支持联合索引的最左侧原则&lt;/strong&gt;(即联合索引的部分索引无法使用)，而B+树可以。对于联合索引来说，Hash索引在计算Hash值的时候是将索引键合并后再一起计算Hash值，所以不会针对每个索引单独计算Hash值。因此如果用到联合索引的一个或者几个索引时，联合索引无法被利用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hash索引不支持ORDER BY排序&lt;/strong&gt;，因为Hash索引指向的数据是无序的，因此无法起到排序优化的作用，而B+树索引数据是有序的，可以起到对该字段ORDER BY排序优化的作用。&lt;/li&gt;
&lt;li&gt;同理，我们也&lt;strong&gt;无法用Hash索引进行模糊查询&lt;/strong&gt;，而B+树使用LIKE进行模糊查询的时候，LIKE后面后模糊查询（比如%结尾）的话就可以起到优化作用。&lt;/li&gt;
&lt;li&gt;InnoDB和MyISAM不支持哈希索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Redis&lt;/h1&gt;
&lt;h2&gt;redis主机宕机后，从机变成主机的同时 主机恢复了，这时候会发生什么？&lt;/h2&gt;
&lt;p&gt;原有主机会变成新的主机的从节点，成为从节点之后就是清空原有数据，然后再重新和主机进行RDB文件的数据同步。&lt;/p&gt;
</content:encoded></item><item><title>Effective STL 笔记（二）：关联容器、迭代器与容器使用杂项</title><link>https://sterben-01.github.io/posts/EFFSTL-2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/EFFSTL-2/</guid><pubDate>Fri, 22 Jul 2022 06:56:00 GMT</pubDate><content:encoded>&lt;h1&gt;map/unordered_map不允许更改key&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;原因是我们依靠的是key来进行排序。
&lt;ul&gt;
&lt;li&gt;而且我们会把key和data合成成为一个value。形式是&lt;code&gt;pair&amp;lt;const key_type, data_type&amp;gt;&lt;/code&gt;。然后把这个传入红黑树/哈希表。指定他从中抽取出来pair的第一个值也就是key来排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所以换句话说就是红黑树/哈希表存的是map的key和value合成的pair。然后通过key排序。这也是为什么map的迭代器返回的是pair。因为我们就是按照pair存进去的。也是为什么用&lt;code&gt;insert&lt;/code&gt;存入的时候需要&lt;code&gt;make_pair&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;map不允许更改key是因为保存至红黑树的&lt;code&gt;pair&lt;/code&gt;里面的&lt;code&gt;key&lt;/code&gt;是&lt;code&gt;const&lt;/code&gt;的。
&lt;ul&gt;
&lt;li&gt;也就是如果有&lt;code&gt;map&amp;lt;k,v&amp;gt;&lt;/code&gt; 或 &lt;code&gt;multimap&amp;lt;k,v&amp;gt;&lt;/code&gt; 类型的对象，其中的元素类型会是&lt;code&gt;pair&amp;lt;const k, v&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/QQ%E6%88%AA%E5%9B%BE20230111183843.png&quot; alt=&quot;QQ截图20230111183843&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;map的特殊的重载的operator[]&lt;/h2&gt;
&lt;p&gt;如果这个括号里面的元素不存在，他会帮你创建一个以括号内元素为key，等号后面为value的键值对。&lt;/p&gt;
&lt;p&gt;所以会有陷阱 比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (my_map[4] == 7){
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这行表面是判断&lt;code&gt;key&lt;/code&gt; 4的值是否为7， 但是这行执行的时候，如果&lt;code&gt;key&lt;/code&gt;4不存在，他会创建一个&lt;code&gt;4,0&lt;/code&gt;的键值对。&lt;/p&gt;
&lt;p&gt;因为它访问了&lt;code&gt;my_map[4]&lt;/code&gt;，此时发现该元素不存在，就使用该类型的&lt;strong&gt;默认构造函数&lt;/strong&gt;构造出一个value插入进去。这里我们用的是&lt;code&gt;int&lt;/code&gt;，&lt;code&gt;int&lt;/code&gt;的默认是0，所以会有一个&lt;code&gt;4,0&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;若键不存在：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;则默认构造的对象的key_type（也就是key）必须满足可复制/移动构造。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mapped_type（也就是value）必须满足可默认构造&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;来自&lt;a href=&quot;https://zh.cppreference.com/w/cpp/container/map/operator_at&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;对于自定义的对象，例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj(){
            val = 12; //注意这里
        }
        myobj(int x){
            val = x;
        }
    int val;
};

class comp1{ //自定义排序
    public:
    bool operator()(const int&amp;amp; lhs, const int&amp;amp; rhs) const{ //注意map的自定义排序是key而不是整个pair。所以自定义排序的函数签名是key
        return lhs &amp;gt; rhs;
    }
};
int main()
{
    map&amp;lt;int,myobj, comp1&amp;gt; we;  
    myobj a(20);
    myobj b(30);
    if(we[2].val == a.val){ //key为2的元素的val是否等于a的val
        cout &amp;lt;&amp;lt; &quot;true1&quot; &amp;lt;&amp;lt; endl;
    }
    for(auto t:we){
        cout &amp;lt;&amp;lt; t.first &amp;lt;&amp;lt; t.second.val &amp;lt;&amp;lt; endl; //输出2 12
    }
    cout &amp;lt;&amp;lt; we.size() &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于判断&lt;code&gt;key&lt;/code&gt;为&lt;code&gt;2&lt;/code&gt;的元素的&lt;code&gt;val&lt;/code&gt;是否等于&lt;code&gt;a&lt;/code&gt;的&lt;code&gt;val&lt;/code&gt;的时候会先访问&lt;code&gt;key&lt;/code&gt;为&lt;code&gt;2&lt;/code&gt;的元素，这时候发现元素不存在。所以会使用默认（无参）构造函数或默认值构造对应的键值对。所以会出现一个 &lt;code&gt;2,12&lt;/code&gt;的元素。&lt;/p&gt;
&lt;p&gt;所以为了避免陷阱，我们应该：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向map容器插入元素时直接使用insert函数&lt;/li&gt;
&lt;li&gt;遍历map容器时使用iterator迭代器&lt;/li&gt;
&lt;li&gt;修改或者删除元素时使用find函数找到元素后再进行操作&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    unordered_map &amp;lt;string,int&amp;gt; map;
    unordered_map &amp;lt;string,int&amp;gt; map2;
    vector&amp;lt;string&amp;gt; str = {&quot;a&quot;,&quot;a&quot;,&quot;abc&quot;,&quot;abcd&quot;};
    for(string a : str){
        cout &amp;lt;&amp;lt; map[a] &amp;lt;&amp;lt; endl;
        map[a] = map[a] + 1;//map[a]是你key对应的value。 a才是你的key。所以这里的意思是 你map[a]的这个val+1。也就是key为a的地方把他的value在原来的基础上加1
        //如果map[a]有这个key 比如map里面有a 那么这个val+1。简单来说就是key为a对应的value = key为a对应的value + 1
        //如果map[a] 没有这个key。那就加这个key 比如里面没有abc 那就把abc加进去
        //map[num];对于这个下标操作，如果存在num关键字，则返回对应的值；如果不存在num关键字，则创建一个键值对，键为num，值为值类型的默认初始化值。
       那么这句话的意思是。如果有a这个key，那么把这个key对应的value+1
       如果没有，那就先新建a这个key，value设为0+1.
       所以map[4] = &quot;ffff&quot; 这一句，如果有4这个key，那就把这个key对应的value改为&quot;ffff&quot;
       如果没有，就新建4， &quot;ffff&quot;这个键值对放入map
        //等号右侧的map[a]先执行。先在map里面找有没有元素为a的。如果没有就创建一个key=a value=0的元素。然后返回value= 0。之后value+1赋值给key为a的元素的value上面
        //先重载=返回自己。然后重载[]返回second也就是value。因为东西是按照pair存的。
    }
    for(auto x = map.begin(); x!= map.end(); x++){
        cout &amp;lt;&amp;lt; x-&amp;gt;first &amp;lt;&amp;lt; x-&amp;gt;second &amp;lt;&amp;lt; endl;
    }
    
    pair&amp;lt;string,int&amp;gt; ss = *map.begin();
    //map里面的每一个元素可以看为一个pair。注意这里迭代器需要解引用。
    因为map的底层是把key和value合成起来成为真正的value。是用一个pair包起来的。
    用pair包起来之后，把key和这个包起来的pair放到红黑树里面。
    其实这个value应该叫data。没所谓了。
    所以map的每一个元素存起来应该长这个样子
    key = string
    value = pair&amp;lt;string, int&amp;gt;。
    所以迭代器的first其实是下面value存的pair的first
    迭代器的second是下面的value存的pair的second


    所以说才会有select1st从value里面取key。所以你自己要insert元素的时候需要传一个pair进去。他会把key从pair里面提出来变成key

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么要这么设计&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;这么设计的原因，我的个人思考是，我们使用&lt;code&gt;x[5]&lt;/code&gt;的时候，其实就是&lt;code&gt;x.operator[](5)&lt;/code&gt;，但是我们既有可能要给这个地方赋值，如&lt;code&gt;x[5] = 1&lt;/code&gt;，又有可能是获取一个这个地方的值，如&lt;code&gt;int a = x[5]&lt;/code&gt;。但是这个函数本身无法做出这样的判断。同时STL容器是值语义 。所以它必须当做所有&lt;code&gt;operator[]&lt;/code&gt;的调用都是写入。所以在用下标运算符访问不存在的元素的时候，会默认构造出这个元素，然后返回这个元素的引用。 -- 同时参考more effective c++ 条款29 P.190&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们查看文档&lt;/p&gt;
&lt;blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;T&amp;amp; operator[]( const Key&amp;amp; key );&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;(1)&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T&amp;amp; operator[]( Key&amp;amp;&amp;amp; key );&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(2)&lt;/td&gt;
&lt;td&gt;(C++11 起)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们发现没有一个&lt;code&gt;const&lt;/code&gt;成员函数&lt;/p&gt;
&lt;p&gt;所以下面这样是不可以的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const map&amp;lt;int, int&amp;gt; op{{1,2}};
op[1];//不可以。
op.at(1);//可以
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;operator[]&lt;/code&gt;总会有潜在的对象修改语义。所以针对&lt;code&gt;const map&lt;/code&gt;而言，访问元素的唯一方法便是使用&lt;code&gt;at()&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;set不允许更改值&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为set的&lt;code&gt;key&lt;/code&gt;和&lt;code&gt;data&lt;/code&gt;是同一个东西，也就是&lt;code&gt;value&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们是依靠这个值进行红黑树排序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/QQ%E6%88%AA%E5%9B%BE20230111183159.png&quot; alt=&quot;QQ截图20230111183159&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是set不允许更改值是因为迭代器返回的是常量迭代器也就是&lt;code&gt;const iterator&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;map不允许更改key是因为保存至红黑树的pair里面的key是const的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;set不会重复插入且不允许重复元素的原因&lt;/h1&gt;
&lt;p&gt;set使用的红黑树的&lt;code&gt;insert_unique&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;multi_set使用的红黑树的&lt;code&gt;insert_equal&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;map不会重复插入且不允许重复key的原因&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt;使用的红黑树的&lt;code&gt;insert_unique&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;multi_map&lt;/code&gt;使用的红黑树的&lt;code&gt;insert_equal&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;set容器 和 map容器在插入时会自动排序。所以在使用自定义数据类型的时候就必须要直接指定排序规则仿函数来进行自定义排序。&lt;/h1&gt;
&lt;h2&gt;set 不多赘述&lt;/h2&gt;
&lt;p&gt;自定义排序比较常见&lt;/p&gt;
&lt;h2&gt;map的自定义排序排序的是&lt;code&gt;key&lt;/code&gt;而不是&lt;code&gt;pair&lt;/code&gt;。所以自定义排序的函数签名是key&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj(){
            val = 12; //注意这里
        }
        myobj(int x){
            val = x;
        }
    int val;
};

class comp1{ //自定义排序
    public:
    bool operator()(const int&amp;amp; lhs, const int&amp;amp; rhs) const{ //注意map的自定义排序是key而不是整个pair。所以自定义排序的函数签名是key
        return lhs &amp;gt; rhs;
    }
};
int main()
{
    map&amp;lt;int, myobj, comp1&amp;gt; we;  
    myobj a(20);
    myobj b(30);
    if(we[2].val == a.val){ //key为2的元素的val是否等于a的val
        cout &amp;lt;&amp;lt; &quot;true1&quot; &amp;lt;&amp;lt; endl;
    }
    for(auto t:we){
        cout &amp;lt;&amp;lt; t.first &amp;lt;&amp;lt; t.second.val &amp;lt;&amp;lt; endl; //输出2 12
    }
    cout &amp;lt;&amp;lt; we.size() &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;unordered_map/unordered_set 哈希表没有自定义排序。只有自定义哈希和自定义等比。想使用自定义的键类型，必须实现hash函数和等比函数。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;自定义哈希的意义不多说。&lt;/li&gt;
&lt;li&gt;自定义等比的意义是在两个对象哈希值相同的时候，需要告诉容器一个办法让其判断这两个对象是真的一模一样，还是只是哈希出来的值是一样但本身是不同的，&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;c++的map是基于红黑树实现的，它不需要对key进行哈希，所以可以用vector做为key。但是unordered map是基于哈希表实现的，它需要对key进行哈希，但是c++ 默认不对vector提供哈希，所以要么自己写一个哈希函数对vector进行哈希，或不要在unordered map中使用vector做为key。&lt;/h1&gt;
&lt;h1&gt;注意！随机访问迭代器支持&amp;gt;和&amp;lt;比较！！&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/5740965-0d91567d9f259cce.png&quot; alt=&quot;5740965-0d91567d9f259cce&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;对容器使用下标访问运算符访问或者是对其迭代器解引用返回的是元素的引用。front，back成员函数也是。&lt;/h1&gt;
&lt;p&gt;正因为是引用，我们才可以有如下操作，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_vec[0] = 1;
*my_vec.begin() = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;正因为上面那条，针对容器的常量引用或如果容器本身是常量，提取出来的元素也是常量性质的。&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;因为常量引用不能绑定到非常量引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const vector&amp;lt;int&amp;gt; a {1,2,3};
int&amp;amp; val = a[0]; //错误
const int&amp;amp; vala = a[0]; //正确
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void functest(const vector&amp;lt;int&amp;gt;&amp;amp; vec){
    int&amp;amp; val = vec[0]; //错误
    const int&amp;amp; vala = vec[0]; //正确
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;为什么没有引用容器，除非使用std::reference_wrapper&lt;/h1&gt;
&lt;p&gt;1：容器内的元素必须是可赋值的。引用是不可赋值的（引用只能在声明的同时初始化）&lt;/p&gt;
&lt;p&gt;2：没有指向引用的指针。容器有迭代器，迭代器是指针。元素如果是引用，迭代器无法工作，&lt;/p&gt;
&lt;p&gt;除非使用&lt;code&gt;std::reference_wrapper&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;for_each&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;//这里函数对象对应的函数的形参应该是迭代器指向的容器对象的对应类型。
//比如vector&amp;lt;int&amp;gt;的话这里就要int, vector&amp;lt;pair&amp;lt;int,int&amp;gt;&amp;gt;的话就要pair&amp;lt;int, int&amp;gt;
void printvector01(int a){ 
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
}

//注意这里pair&amp;lt;int a, int b&amp;gt;是错的！！！
//pair&amp;lt;&amp;gt;括号里头的是数据类型，外面的才是名字！你不能vector&amp;lt;int a&amp;gt;吧
void printvector02(pair&amp;lt;int, int&amp;gt; a){ 
    cout &amp;lt;&amp;lt; a.first &amp;lt;&amp;lt; &quot; , &quot; &amp;lt;&amp;lt; a.second &amp;lt;&amp;lt; endl;
}


class printvector03{
public:
    void operator()(int a){
        cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;

    }
};

/*
template &amp;lt;class _InIt, class _Fn&amp;gt;
_CONSTEXPR20 _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
    _Adl_verify_range(_First, _Last);
    auto _UFirst      = _Get_unwrapped(_First);
    const auto _ULast = _Get_unwrapped(_Last);
    for (; _UFirst != _ULast; ++_UFirst) {
        _Func(*_UFirst); //这里已经对迭代器对象解引用了，所以直接传入的是迭代器对应的值。
        // 注意这里调用函数的方式是直接把参数塞进去。所以传递函数对象就行 不用() 加了括号叫调用
    }

    return _Func; //看好了！！有返回值！！
}
*/




&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt; test = {1,2,3,4,5};
    for_each(test.begin(), test.end(), printvector01); //注意这里传递的是函数对象，不是函数。因为printvector01是个函数不是类，所以printvector01()加了括号的叫调用！！
    for_each(test.begin(), test.end(), printvector03()); //注意这里传递的是函数对象，不是类型名。因为printvector03是个类(仿函数)不是函数。所以printvector01()加了括号的是类的匿名对象，这里因为是仿函数所以叫函数对象，而不是调用！！
    //注意区分类和普通函数的对象。普通函数不加括号叫对象，加了括号叫调用。仿函数（类）要加括号。加了括号叫类的匿名对象，不加括号叫类型名。
    vector&amp;lt;pair&amp;lt;int,int&amp;gt;&amp;gt; test2{ {1,1}, {2,2}, {3,3}, {4,4}, {5,5} };
    for_each(test2.begin(),test2.end(), printvector02);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;for_each&lt;/code&gt;是可以有返回值的。也就是可以施加一个有状态的函数对象。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;比如下面的代码目的是获取所有性别为1的人的id，并放入容器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Person{
    public:
        int id;
        int sex;
        Person() = default;
        Person(int x, int y):id(x), sex(y){};

};

struct myfind{
    vector&amp;lt;int&amp;gt; persons;
    void operator()(const Person&amp;amp; item){ //这里不需要bool了。void即可。
        if(item.sex == 1){
            persons.push_back(item.sex);
        }
    }
};

int main(){
    vector&amp;lt;Person&amp;gt; my_vec{Person(1,1),Person(2,1),Person(3,1),Person(4,2),Person(5,2)};
    myfind t = for_each(my_vec.begin(), my_vec.end(), myfind());
    
    for(auto&amp;amp;i:t.persons){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;迭代器种类&lt;/h1&gt;
&lt;p&gt;迭代器分为五种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入迭代器（Input Iterator）：通过对输入迭代器解除引用，它将引用对象，而对象可能位于集合中。最严格的输入迭代只能以只读方式访问对象。例如：istream。&lt;/li&gt;
&lt;li&gt;输出迭代器（Output Iterator）：该类迭代器和Input Iterator极其相似，也只能单步向前迭代元素，不同的是该类迭代器对元素只有写的权力。例如：ostream, inserter。&lt;/li&gt;
&lt;li&gt;前向迭代器（Forward Iterator）：该类迭代器可以在一个正确的区间中进行读写操作，它拥有Input Iterator的所有特性，和Output Iterator的部分特性，以及单步向前迭代元素的能力。
&lt;ul&gt;
&lt;li&gt;假设 p 是一个前向迭代器，则 p 支持 ：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;++p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p++&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;复制&lt;/li&gt;
&lt;li&gt;赋值&lt;/li&gt;
&lt;li&gt;可以用&lt;code&gt; ==&lt;/code&gt; 和 &lt;code&gt;!= &lt;/code&gt;运算符进行比较。&lt;/li&gt;
&lt;li&gt;此外，两个正向迭代器可以互相赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;双向迭代器（Bidirectional Iterator）：该类迭代器是在Forward Iterator的基础上提供了单步向后迭代元素的能力。例如：list, set, multiset, map, multimap。
&lt;ul&gt;
&lt;li&gt;假设 p 是一个双向迭代器，则 p 支持 ：
&lt;ul&gt;
&lt;li&gt;正向迭代器的全部功能 和&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p--&lt;/code&gt; （即一次向后移动一个位置）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;随机迭代器（Random Access Iterator）：该类迭代器能完成上面所有迭代器的工作，它自己独有的特性就是可以像指针那样进行算术计算，而不是仅仅只有单步向前或向后迭代。例如：vector, deque, string, array。
&lt;ul&gt;
&lt;li&gt;假设 p 是一个随机迭代器，则 p 支持 ：
&lt;ul&gt;
&lt;li&gt;双向迭代器的全部功能 和&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p+=i&lt;/code&gt;：使得 p 往后移动 &lt;code&gt;i &lt;/code&gt;个元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p-=i&lt;/code&gt;：使得 p 往前移动&lt;code&gt;i&lt;/code&gt;个元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p+i&lt;/code&gt;：返回 p 后面第&lt;code&gt;i&lt;/code&gt;个元素的迭代器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p-i&lt;/code&gt;：返回 p 前面第&lt;code&gt;i&lt;/code&gt;个元素的迭代器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p[i]&lt;/code&gt;：返回 p 后面第 &lt;code&gt;i&lt;/code&gt; 个元素的引用。&lt;/li&gt;
&lt;li&gt;此外，两个随机访问迭代器&lt;code&gt; p1&lt;/code&gt;、&lt;code&gt;p2 &lt;/code&gt;还可以用 &lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;=&lt;/code&gt;、&lt;code&gt;&amp;gt;= &lt;/code&gt;运算符进行比较。&lt;/li&gt;
&lt;li&gt;另外，表达式 &lt;code&gt;p2-p1&lt;/code&gt; 也是有定义的，其返回值表示&lt;code&gt; p2&lt;/code&gt; 所指向元素和 &lt;code&gt;p1&lt;/code&gt; 所指向元素的序号之差（也可以说是&lt;code&gt;p2&lt;/code&gt;和&lt;code&gt;p1&lt;/code&gt;之间的元素个数减一）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;容器类型&lt;/th&gt;
&lt;th&gt;迭代器类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;随机访问迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vector&lt;/td&gt;
&lt;td&gt;随机访问迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deque&lt;/td&gt;
&lt;td&gt;随机访问迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;双向迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set / multiset&lt;/td&gt;
&lt;td&gt;双向迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;map / multimap&lt;/td&gt;
&lt;td&gt;双向迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forward_list&lt;/td&gt;
&lt;td&gt;前向迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unordered_map / unordered_multimap&lt;/td&gt;
&lt;td&gt;前向迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unordered_set / unordered_multiset&lt;/td&gt;
&lt;td&gt;前向迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stack&lt;/td&gt;
&lt;td&gt;不支持迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;不支持迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;迭代器定义方式&lt;/th&gt;
&lt;th&gt;具体格式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;正向迭代器&lt;/td&gt;
&lt;td&gt;容器类名::iterator 迭代器名;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;常量正向迭代器&lt;/td&gt;
&lt;td&gt;容器类名::const_iterator 迭代器名;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反向迭代器&lt;/td&gt;
&lt;td&gt;容器类名::reverse_iterator 迭代器名;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;常量反向迭代器&lt;/td&gt;
&lt;td&gt;容器类名::const_reverse_iterator 迭代器名;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;所以我们说，除了vector，deque，string和array以外，其他的容器都不支持如&lt;code&gt;begin()+n&lt;/code&gt;的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;为什么迭代器的类型不是简单的T*，而一般都带了很长的包装器&lt;/h1&gt;
&lt;p&gt;https://quuxplusone.github.io/blog/2022/03/03/why-isnt-vector-iterator-just-t-star/&lt;/p&gt;
&lt;h1&gt;不同容器不同操作的&lt;em&gt;平均&lt;/em&gt;时间复杂度&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;容器&lt;/th&gt;
&lt;th&gt;[]下标访问&lt;/th&gt;
&lt;th&gt;push_back&lt;/th&gt;
&lt;th&gt;pop_back&lt;/th&gt;
&lt;th&gt;insert&lt;/th&gt;
&lt;th&gt;erase&lt;/th&gt;
&lt;th&gt;find&lt;/th&gt;
&lt;th&gt;sort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;std::array&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n logn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::vector&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n logn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::deque&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n logn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::list&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n logn)*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::forward_list&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::set/std::multiset&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::map/std::multimap&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::unorderd_set/std::unorderd_multiset&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::unorderd_map/std::unorderd_multimap&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;*list的sort是成员函数。因为list不提供随机访问迭代器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要随机访问或只在尾部进行插入或删除使用&lt;code&gt;vector&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;需要随机访问和头尾插入使用&lt;code&gt;deque&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果不需要随机访问但频繁插入和删除使用&lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果内存格外受限则使用&lt;code&gt;forward_list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果需要搜索且保持排序则使用&lt;code&gt;map/set&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果需要搜索但不需要排序使用&lt;code&gt;unordered_map/unordered_set&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;原始数组的初始化&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;静态数组 &lt;code&gt;int array[100];&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;定义了数组array，但并未对数组初始化；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态数组&lt;code&gt; int array[100] = {0};&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;定义了数组array，并将数组元素全部初始化为0；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态数组 &lt;code&gt;int array[100] = {1}; &lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;定义了数组array，并将数组第一个元素初始化为1，后面99个元素初始化为0；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态数组&lt;code&gt;int array[100] = {4,5};&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;定义数组array，并初始化前两个元素为4,5，后面剩余元素初始化为0；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;int a[5] = { 1 }; &lt;/code&gt; 曾经我认为这是把数组全部初始化为１，事实却是，只有数组的第一个元素被初始化为１，其他全为０；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数组初始化列表中的元素个数小于指定的数组长度时，不足的元素补以默认值。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对应基本类型&lt;code&gt;int&lt;/code&gt;来说，就是补0&lt;/li&gt;
&lt;li&gt;非基本类型的数组就是调用默认构造函数。如&lt;code&gt;string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string a[5] = { &quot;foo&quot; };&lt;/code&gt; 等同于 &lt;code&gt;string a[5] = { &quot;foo&quot;, &quot;&quot;, &quot;&quot;, &quot;&quot;, &quot;&quot; };&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果不明确指出初始化列表，那么基本类型是不会被初始化的（除全局变量和静态变量外），所有的内存都是“脏的”；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;而类类型则会为每个元素调用默认构造函数或使用默认值进行初始化。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testnum{
    public:
        testnum(){
        }
        int val = 200;
};
class testnum1{
    public:
        testnum1(){
            val = 23333;
        }
        int val;
};
int main() 
{
    testnum arr[10]; //全是200
    testnum1 arr1[10]; //全是23333
    int arr2[10]; //没有初始化，脏数据，乱糟糟
    int arr3[10]; //初始化为默认值 10个0
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意，在C++11中，中间的赋值号可以省略，即&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt; int a[5]{1};&lt;/code&gt;是合法的，但依旧是只初始化第一个元素为1，剩下的都是默认0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并且，如果初始化列表为空，如&lt;code&gt; int a[5]{}&lt;/code&gt;，那将初始化所有元素为默认值，即等同于&lt;code&gt;int a[5]{0};&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;再次重申容器是值语义。拷贝进来拷贝出去。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt;my_vec;
    int a = 100;
    my_vec.push_back(a); //拷贝进
    a = 40000;
    cout &amp;lt;&amp;lt; my_vec[0] &amp;lt;&amp;lt;endl; //还是100
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么呢？我们看看push_back的源代码就好了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void push_back(const T&amp;amp; x) {
    if (finish != end_of_storage) {
        construct(finish, x);
        ++finish;
    } else
        insert_aux(end(), x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先不管其他的，我们只看&lt;code&gt;construct&lt;/code&gt;这个部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T1, class T2&amp;gt;
inline void construct(T1* p, const T2&amp;amp; value) {
    new (p) T1(value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;new (p) T1(value);&lt;/code&gt;是一个&lt;code&gt;placement new&lt;/code&gt;的用法，&lt;code&gt;new&lt;/code&gt;的这个用法是在一个已分配好内存的地方调用构造函数来初始化一下。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们这里看到了，我们拿了一个&lt;code&gt;value&lt;/code&gt;的值来在一块特定的地方&lt;strong&gt;调用拷贝构造来构造&lt;/strong&gt;了一个元素。这就是为什么我们说是拷贝进。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://blog.csdn.net/jmh1996/article/details/77968364&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;针对是否拷贝出来，这个地方要综合分析&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt;my_vec;
    int a = 100;
    my_vec.push_back(a); //拷贝进
    int b = my_vec.front();
    b = 12345;
    cout &amp;lt;&amp;lt; my_vec[0] &amp;lt;&amp;lt;endl; //肯定还是100
    
    int&amp;amp; c = my_vec.front();
    c = 12345;
    cout &amp;lt;&amp;lt; my_vec[0] &amp;lt;&amp;lt;endl; //变成12345
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;front&lt;/code&gt;返回第一个元素的引用。&lt;strong&gt;我们如果用一个引用来初始化一个值，那么会调用其拷贝构造。因为需要独立开来。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;容器不能储存常量对象&lt;/h1&gt;
&lt;p&gt;没有&lt;code&gt;vector&amp;lt;const int&amp;gt;&lt;/code&gt;。因为常量对象不是&lt;em&gt;可拷贝赋值&lt;/em&gt;的。而且使用&lt;code&gt;cosnt vector&amp;lt;int&amp;gt;&lt;/code&gt;可以达到同一效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int a = 200;
const int b = 300;
b = a; //不可修改常量对象。所以是不可拷贝赋值的。
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;deque的使用提示&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;在空容器上对 &lt;code&gt;deque.front()/back()&lt;/code&gt; 的调用是未定义的。
&lt;ul&gt;
&lt;li&gt;因为它返回的是容器首元素的&lt;strong&gt;引用&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;map不同接口的使用推荐&lt;/h1&gt;
&lt;p&gt;依旧来自Raymond Chen的&lt;a href=&quot;https://devblogs.microsoft.com/oldnewthing/20241118-00/?p=110535&quot;&gt;这篇文章&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read, throw if missing / 读取，如果缺失则抛出异常&lt;/td&gt;
&lt;td&gt;&lt;code&gt;m.at(key)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read, allow missing / 读取，允许缺失&lt;/td&gt;
&lt;td&gt;&lt;code&gt;m.find(key)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read, create if missing / 读取，如果缺失则创建&lt;/td&gt;
&lt;td&gt;&lt;code&gt;m[key]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write, nop if exists, discard value / 写入，如果存在则nop，丢弃值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;m.insert({ key, value })&lt;/code&gt; /&lt;code&gt;m.emplace(key, value)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write, nop if exists / 写入，如果存在则nop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;m.emplace(std::piecewise_construct, ...)&lt;/code&gt; &lt;code&gt;m.try_emplace(key, params)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write, overwrite if exists / 写入，如果存在则覆盖。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;m.insert_or_assign(key, value)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这么多接口，想实现如果没有就插入，如果有不要浪费构造这种逻辑，怎么做？&lt;/p&gt;
&lt;p&gt;依旧是Raymond的&lt;a href=&quot;https://devblogs.microsoft.com/oldnewthing/20241118-00/?p=110535&quot;&gt;这篇文章&lt;/a&gt;，来自&lt;a href=&quot;https://github.com/wanghenshui/cppweeklynews/blob/dev/posts/173.md&quot;&gt;这里&lt;/a&gt;。 代码就是一坨。&lt;/p&gt;
&lt;h1&gt;实现&lt;code&gt;vector&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;https://quantdev.blog/posts/implementing_vector/index.html&lt;/p&gt;
</content:encoded></item><item><title>Effective STL 笔记（一）：条款与 vector、emplace、迭代器失效</title><link>https://sterben-01.github.io/posts/EFFSTL/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/EFFSTL/</guid><pubDate>Fri, 22 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;Effective STL 笔记&lt;/h1&gt;
&lt;h2&gt;第二条：不要试图编写独立于容器类型的代码。&lt;/h2&gt;
&lt;p&gt;STL 是以泛化原则为基础的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组被泛化为”以其包含的对象的类型为参数“的容器；&lt;/li&gt;
&lt;li&gt;函数被泛化为”以其使用的迭代器的类型为参数“的算法；&lt;/li&gt;
&lt;li&gt;指针被泛化为”以其指向的对象的类型为参数“的迭代器；&lt;/li&gt;
&lt;li&gt;容器被泛化为”序列式和关联式“容器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;个人理解一句话：别给多个容器写一个通用的函数。没有意义而且效率低下。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;第三条：确保容器中的对象副本正确而高效。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;当(通过如 insert 或 push_back 之类的操作)向容器中加入对象时，存入容器的是你所指定的对象的拷贝。&lt;/li&gt;
&lt;li&gt;当(通过如front或back之类的操作)从容器中取出一个对象时，你所得到的是容器中所保存的对象的拷贝。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;拷贝进来拷贝出去是STL的工作方式&lt;/p&gt;
&lt;p&gt;在存在继承关系的情况下，拷贝动作会导致剥离(slicing)。也就是说，如果你创建了一个存放基类对象的容器，却向其中插入派生类的对象，那么在派生类对象(通过基类的拷贝构造函数)被拷贝进容器时，它所特有的部分(即派生类中的信息)将会丢失。”剥离”问题意味着向基类对象的容器中插入派生类对象几乎总是错误的。使拷贝动作高效、正确，并防止剥离问题发生的一个简单办法是使容器包含指针而不是对象。&lt;/p&gt;
&lt;p&gt;上一句的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base{
 	//...   
}
class Derived : public Base{
  //...  
};
vector&amp;lt;Base&amp;gt; father; //容器类型为父类对象
Derived child;
father.push_back(child); //子类对象通过基类复制构造函数复制进类型为父类对象的容器时，子类独有的部分会丢失。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们为了实现多态，必须是容器保存父类指针。不能是对象也不能是引用。因为指针是一样大的。对象不是一样大的。而且也没有引用容器。而且对象也没法多态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;Base*&amp;gt; father;
Derived* child = new child(); //子类指针指向子类对象
Base* child = new child(); //父类指针指向子类对象两种都可以。
father.push_back(child);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第四条：调用empty而不是检查size()是否为0。&lt;/h2&gt;
&lt;p&gt;个人理解：因为比如链表这种，size()需要线性时间查找一遍之后告诉你size。但是empty是O(1)。所以用empty不要用size更快速。&lt;/p&gt;
&lt;h2&gt;第五条：区间成员函数优先于与之对应的单元素成员函数。&lt;/h2&gt;
&lt;p&gt;个人总结：能用区间函数的就不要使用循环一个一个拷贝。增加效率。尤其是序列容器的头插。一个一个插入到头部会造成大量的拷贝和析构。因为每插入一个，后面的元素都要移动一位。&lt;/p&gt;
&lt;p&gt;那么，都有哪些区间成员函数？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;区间创建函数、insert、erase、assign等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第六条 C++非常煞笔的分析机制&lt;/h2&gt;
&lt;p&gt;C++非常煞笔的分析机制。函数和函数指针。加括号不加括号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注意：围绕参数名的括号(比如对f2中d)与独立的括号的区别：围绕参数名的括号被忽略，而独立的括号则表明参数
// 列表的存在：它们说明存在一个函数指针参数
int f1(double d); // 声明了一个带double参数并返回int的函数
int f2(double(d)); // 同上，d两边的括号被忽略,可以给参数名加上圆括号
int f3(double); // 同上，参数名被忽略
 
int g1(double(*pf)()); // 参数是一个指向不带任何参数的函数的指针，该函数返回double值；g1以指向函数的指针为参数
int g2(double pf()); // 同上，pf为隐式指针
int g3(double()); // 同上，省去参数名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list&amp;lt;int&amp;gt; data(istream_iterator&amp;lt;int&amp;gt;(dataFile), istream_iterator&amp;lt;int&amp;gt;());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行代码的解释是：这声明了一个函数&lt;code&gt;data&lt;/code&gt;，其返回值是&lt;code&gt;list&amp;lt;int&amp;gt;&lt;/code&gt;。这个&lt;code&gt;data&lt;/code&gt;函数有两个参数：&lt;/p&gt;
&lt;p&gt;■　第一个参数的名称是&lt;code&gt;dataFile&lt;/code&gt;。它的类型是&lt;code&gt;istream_iterator&amp;lt;int&amp;gt;&lt;/code&gt;。&lt;code&gt;dataFile&lt;/code&gt;两边的括号是多余的，会被忽略。&lt;/p&gt;
&lt;p&gt;■　第二个参数没有名称。它的类型是指向不带参数的函数的指针，该函数返回一个&lt;code&gt;istream_iterator&amp;lt;int&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;经典错误&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Widget{
    //...假设Widget有默认构造函数
}
Widget w(); //这是声明了一个名字叫w的函数，该函数没有参数，返回一个Wedget
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第七条：如果容器中包含了通过new操作创建的指针，切记在容器对象析构前将指针delete掉。&lt;/h2&gt;
&lt;p&gt;前提是你真的希望容器内指针指向的对象被析构。在构建二叉树的时候我们并不希望容器内指针指向的对象被析构，因为我们会返回一个根节点。如果析构了东西就都没了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class test;
class myfunc{
    public:
        void operator()(vector&amp;lt;int*&amp;gt;&amp;amp; vec, int p){
            vec.push_back(new int(p)); //新建一个指向int的指针并放入容器。
        }  
};

struct DEL{
    template&amp;lt;typename T&amp;gt;
    void operator()(T* ptr){ //创建删除函数利用foreach释放tt容器内的每一个指针指向的资源
        delete ptr;
        ptr = nullptr;
    }
};



class test{
    public:
    int val;
    test(){}
    test(int x):val(x){};
    void getbug(vector&amp;lt;int&amp;gt;&amp;amp; vec){
        vector&amp;lt;int*&amp;gt; tt; //函数内创建指针容器
        for_each(vec.begin(), vec.end(), bind(myfunc(), ref(tt), placeholders::_1)); //使用foreach和bind。记得传入容器需要加ref
        for_each(tt.begin(), tt.end(), [](int* content){cout &amp;lt;&amp;lt;*content &amp;lt;&amp;lt; endl; }); //使用lambda表达式打印tt容器的每一个值。[]捕获列表没有参数因为我们没有用到上下文变量
        for_each(tt.begin(), tt.end(), DEL()); //如果这里不用foreach搭配释放函数释放，那么tt容器在离开函数的时候会被销毁。里面的指针全部都会被移除，但是指向的资源没有释放。导致内存泄漏。因为tt是局部变量。不属于class。
    }
};

int main()
{

    vector&amp;lt;int&amp;gt; rrr = {1,2,3,4,5,6,7};
    test obj;
    obj.getbug(rrr);

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这一条的个人解释：容器确实会调用容器内每个元素的析构。但是调用的是对应类型的析构。举个例子&lt;code&gt;vector&amp;lt;int&amp;gt; a;&lt;/code&gt; 和 &lt;code&gt;vector&amp;lt;int*&amp;gt; b&lt;/code&gt;。&lt;code&gt;a&lt;/code&gt;析构的时候调用&lt;code&gt;int&lt;/code&gt;的析构，&lt;code&gt;b&lt;/code&gt;析构的时候调用&lt;code&gt;int*&lt;/code&gt;的析构。但是指针不是类，它没有析构函数，所以什么都没做，仅仅是删掉了指针而没有释放指针所指向的元素。所以容器中所有指针指向的数据全部泄漏。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;容器析构的时候会为每个元素调用&lt;code&gt;destroy&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/QQ%E6%88%AA%E5%9B%BE20220824172240.png&quot; alt=&quot;QQ截图20220824172240&quot; /&gt;&lt;/p&gt;
&lt;p&gt;他为每个元素调用其析构函数。这里是因为传入的是迭代器。所以长这样。&lt;/p&gt;
&lt;p&gt;所以我们普通的比如&lt;code&gt;myclass&lt;/code&gt;可以有&lt;code&gt;~myclass()&lt;/code&gt;这样的析构函数调用，但是&lt;code&gt;myclass*&lt;/code&gt;这种指针类型是没有析构函数的，没有（其实是这种类型的析构函数是trivial的）&lt;code&gt;~myclass*()&lt;/code&gt;。&lt;strong&gt;所以要谨记，容器析构的时候并不是delete，而是调用其对应类类型的析构函数！。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个简单明显的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int*&amp;gt;myvec;
    myvec.emplace_back(new int(6));
    myvec.emplace_back(new int(7));
    delete myvec[0]; //去掉这两行会内存泄漏。
    delete myvec[1];
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;所以，当容器储存的是普通对象的时候，并无大碍。但是如果储存的是指针，则必须要手动调用析构函数。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;第 八 条：切勿创建包含 &lt;code&gt;auto_ptr&lt;/code&gt; 的容器对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;auto_ptr&lt;/code&gt; 的容器(简称COAP) 是被禁止的。当你拷贝一个 &lt;code&gt;auto_ptr&lt;/code&gt; 时，它所指向的对象的所有权被移交到拷入的 &lt;code&gt;auto_ptr&lt;/code&gt; 上，而它自身被置为&lt;code&gt; NULL&lt;/code&gt;。如果你的目标是包含智能指针的容器，这并不意味着你要倒霉，包含智能指针的容器是没有问题的。&lt;strong&gt;但 &lt;code&gt;auto_ptr &lt;/code&gt;非智能指针&lt;/strong&gt;。顺带提一句，&lt;code&gt;auto_ptr&lt;/code&gt;在c++11已被摒弃。应使用&lt;code&gt;unique_ptr&lt;/code&gt;或&lt;code&gt;shared_ptr&lt;/code&gt;做替代。&lt;/p&gt;
&lt;h2&gt;第 九 条：慎重选择删除元素的方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;要删除容器中有特定值的所有对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果容器是vector、string或deque，则使用erase-remove习惯用法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;v.erase(remove(v.begin(), v.end(), VALUE), c.end);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面这句的要点是。remove仅是移除。他会把把每一个不和指定value相等的元素轮番赋值给first之后的空间。假设现在是112211要移除2，那么remove后应该是长成111111。最后两个数据是脏数据。remove返回的迭代器就指向倒数第二个1，也就是最开始的脏数据。然后erase使用这个迭代器为起点移除所有到结尾位置的数据。因为这一段全都是脏数据。记住，erase后，size改变，capacity不改变。如果需要改变capacity还需要配合swap或匿名对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果容器是list，则使用list::remove。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果容器是一个标准关联容器，则使用它的erase成员函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要删除容器中满足特定判别式（条件）的所有对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果容器是vector、string或deque，则使用erase-remove_if习惯用法。&lt;/li&gt;
&lt;li&gt;如果容器是list，则使用list::remove_if。&lt;/li&gt;
&lt;li&gt;如果容器是一个标准关联容器，则使用remove_copy_if和swap，或者写一个循环来遍历容器中的元素，记住当把迭代器传给erase时，要对它进行后缀递增。防止迭代器失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要在循环内部做某些（除了删除对象之外的）操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果容器是一个标准序列容器，则写一个循环来遍历容器中的元素，记住每次调用erase时，要用它的返回值更新迭代器。防止迭代器失效。&lt;/li&gt;
&lt;li&gt;如果容器是一个标准关联容器，则写一个循环来遍历容器中的元素，记住当把迭代器传给erase时，要对迭代器做后缀递增。防止迭代器失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第 十 条：了解分配子(allocator)的约定和限制 -- 罄待深入了解&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;你的分配子是一个模板，模板参数T代表你为它分配内存的对象的类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提供类型定义pointer和reference，但是始终让pointer为T* ，reference为T&amp;amp;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;千万别让你的分配子拥有随对象而不同的状态（per-object state）。通常，分配子不应该有非静态的数据成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记住，传给分配子的allocate成员函数的是那些要求内存的对象的个数，而不是所需的字节数。同时要记住，这些函数返回T* 指针（通过pointer类型定义），即使尚未有T对象被构造出来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一定要提供嵌套的rebind模板，因为标准容器依赖该模板。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这里有一点 使用operator new的时候，给的大小是字节数。使用自定义allocator的时候，给的大小是对象的数量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第 十二 条：切勿对STL容器的线程安全性有不切实际的依赖。&lt;/h2&gt;
&lt;p&gt;对一个STL实现你最多只能期望：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多个线程读是安全的&lt;/strong&gt; 。多个线程可以同时读同一个容器的内容，并且保证是正确的。自然地，在读的过程中，不能对容器有任何写入操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多个线程对不同的容器做写入操作是安全的&lt;/strong&gt; 。多个线程可以同时对不同的容器做写入操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现完全的容器线程安全性时可能采取的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对容器成员函数的每次调用，都锁住容器直到调用结束。&lt;/li&gt;
&lt;li&gt;在容器所返回的每个迭代器的生存期结束前，都锁住容器（比如通过begin或end调用）。&lt;/li&gt;
&lt;li&gt;对于作用于容器的每个算法，都锁住该容器，直到算法结束。（实际上这样做没有意义。因为，如同在第32条中解释的，算法无法知道它们所操作的容器。尽管如此，在这里我们仍要讨论这一选择。因为即便这是可能的，我们也会发现这种做法仍不能实现线程安全性，这对于我们的讨论是有益的。）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;个人理解：加锁就完事了。&lt;/p&gt;
&lt;p&gt;下面的例子是一个不加锁也安全的例子。&lt;code&gt;vec&lt;/code&gt;虽然是一个容器，但是这个容器里有10个小容器。我开了10个线程给这10个小容器进行多线程同时写入，是安全的。符合要求。也就是所谓的多个线程对不同的容器做写入操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void push(vector&amp;lt;int&amp;gt;&amp;amp; vec){
    for(int j = 0; j &amp;lt; 100; j++){
        vec.push_back(j);
    }
}
int main()
{   
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; vec(10, vector&amp;lt;int&amp;gt;());
    thread th[10];
    for(int i = 0; i &amp;lt; 10; i++){
        th[i] = thread(push, ref(vec[i])); //注意，这里是核心。我vec虽然是一个容器，但是我这个容器里有10个小容器。我开了10个线程给这10个小容器进行多线程同时写入，是安全的。符合要求
    }
    for(int i = 0; i &amp;lt; 10; i++){
        th[i].join();
    }
    for(int i = 0; i &amp;lt; 10; i++){
        for(int j = 0; j &amp;lt; vec[i].size(); j++){
            cout &amp;lt;&amp;lt; vec[i][j];
        }
        cout&amp;lt;&amp;lt;endl;
    }
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第十四条：使用reserve来避免不必要的重新分配。&lt;/h2&gt;
&lt;p&gt;对于 vector 和 string，增长过程是这样来实现的：每当需要更多空间时，就调用与 realloc类似的操作。这一类似于 realloc 的操作分为四部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分配一块大小为当前容量的某个倍数的新内存。在大多数实现中，vector 和 string 的容量每次以 2 的倍数增长，即，每当容器需要扩张时，它们的容量即加倍。&lt;/li&gt;
&lt;li&gt;把容器的所有元素从旧的内存拷贝到新的内存中。&lt;/li&gt;
&lt;li&gt;析构掉旧内存中的对象。&lt;/li&gt;
&lt;li&gt;释放旧内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;每当这些步骤发生时，vector或string中所有的指针、迭代器和引用都将变得无效。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通常有两种方式来使用reserve以避免不必要的重新分配。 第一种方式是，若能确切知道或大致预计容器中最终会有多少元素，则此时可以使用reserve。第二种方式是，先预留足够大的空间(根据你的需要而定)，然后，当把所有数据都加入以后，再去除多余的容量。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;第十七条：使用“swap技巧”除去多余的容量&lt;/h2&gt;
&lt;p&gt;讲过很多次了。不再赘述了。看下面杂记。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;typename&amp;gt;(container).swap(container);
vector&amp;lt;int&amp;gt;(c1).swap(c1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表达式&lt;code&gt;vector&amp;lt;typename&amp;gt;(container)&lt;/code&gt;创建一个临时的矢量，它是&lt;code&gt;container&lt;/code&gt;的副本：这是由&lt;code&gt;vector&lt;/code&gt;的&lt;strong&gt;复制构造函数&lt;/strong&gt;来完成的。然而，&lt;code&gt;vector&lt;/code&gt;的复制构造函数只为所复制的元素分配所需要的内存，所以这个临时矢量没有多余的容量。然后我们把临时矢量中的数据和&lt;code&gt;container&lt;/code&gt;中的数据做&lt;code&gt;swap&lt;/code&gt;操作，在这之后，&lt;code&gt;container&lt;/code&gt;具有了被去除之后的容量，即原先临时变量的容量，而临时变量的容量则变成了原先&lt;code&gt;container&lt;/code&gt;臃肿的容量。到这时（在语句结尾），临时&lt;code&gt;vector&lt;/code&gt;被析构，从而释放了先前为&lt;code&gt;container&lt;/code&gt;所占据的内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;swap两个vector可简单理解为交换tag。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int a = 5;
    int b = 10;
    int c = 15;
    int*pa = &amp;amp;a;
    int*pb = &amp;amp;b;
    int*pc = &amp;amp;c;
    cout &amp;lt;&amp;lt; &amp;amp;a &amp;lt;&amp;lt; endl; 		//0x61fdac
    cout &amp;lt;&amp;lt; &amp;amp;*pa &amp;lt;&amp;lt; endl;		//0x61fdac
    cout &amp;lt;&amp;lt; &amp;amp;b &amp;lt;&amp;lt; endl;			//0x61fda8
    cout &amp;lt;&amp;lt; &amp;amp;*pb &amp;lt;&amp;lt; endl;		//0x61fda8
    vector&amp;lt;int*&amp;gt; va = {pa, pb};	
    vector&amp;lt;int*&amp;gt; vb = {pb, pc};
    cout &amp;lt;&amp;lt; &amp;amp;va[0] &amp;lt;&amp;lt; endl;		//0xde1490
    cout &amp;lt;&amp;lt; &amp;amp;*va[0] &amp;lt;&amp;lt; endl;	//0x61fdac
    cout &amp;lt;&amp;lt; &amp;amp;vb[0] &amp;lt;&amp;lt; endl;		//0xde14d0
    cout &amp;lt;&amp;lt; &amp;amp;*vb[0] &amp;lt;&amp;lt; endl;	//0x61fda8
    swap(va, vb);
    cout &amp;lt;&amp;lt; &amp;amp;va[0] &amp;lt;&amp;lt; endl;		//0xde14d0
    cout &amp;lt;&amp;lt; &amp;amp;*va[0] &amp;lt;&amp;lt; endl;	//0x61fda8
    cout &amp;lt;&amp;lt; &amp;amp;vb[0] &amp;lt;&amp;lt; endl;		//0xde1490
    cout &amp;lt;&amp;lt; &amp;amp;*vb[0] &amp;lt;&amp;lt; endl;	//0x61fdac
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到。VA和VB交换后，VA容器装的指针自己的地址由原来的&lt;code&gt;0x61fdac&lt;/code&gt;换成了&lt;code&gt;0xde1490&lt;/code&gt;。VB亦然。所以可理解为仅仅是把VA的铭牌摘下来换给了VB，VB换给了VA&lt;/p&gt;
&lt;h2&gt;第十八条：避免使用&lt;code&gt;vector&amp;lt;bool&amp;gt;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;同时参考more effective 条款30。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作为一个STL容器，&lt;code&gt;vector&amp;lt;bool&amp;gt;&lt;/code&gt;只有两点不对。首先，它不是一个STL容器。其次，它并不存储&lt;code&gt;bool&lt;/code&gt;。除此以外，一切正常。&lt;/p&gt;
&lt;p&gt;一个对象并不因为有人说它是一个STL容器，所以它就是了。一个对象要成为STL容器，就必须满足C++标准的第23.1节列出的所有条件。其中的一个条件是，如果&lt;code&gt;c&lt;/code&gt;是包含对象&lt;code&gt;T&lt;/code&gt;的容器，而且&lt;code&gt;c&lt;/code&gt;支持&lt;code&gt;operator[]&lt;/code&gt;，那么下面的代码必须能够被编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T* P = &amp;amp;C[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换句话说，如果你用&lt;code&gt;operator[]&lt;/code&gt;取得了&lt;code&gt;Container&amp;lt;T&amp;gt;&lt;/code&gt;中的一个T对象，那么你可以通过取它的地址得到一个指向该对象的指针。（这里假定&lt;code&gt;T&lt;/code&gt;没有用非常规的方式对&lt;code&gt;operator&amp;amp;&lt;/code&gt;做重载。）所以，如果&lt;code&gt;vector&amp;lt;bool&amp;gt;&lt;/code&gt;是一个容器，那么下面这段代码必须可以被编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;bool&amp;gt;v;
bool* pb = &amp;amp;v[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是它不能编译。不能编译的原因是，&lt;code&gt;vector&amp;lt;bool&amp;gt;&lt;/code&gt;是一个假的容器，它并不真的储存&lt;code&gt;bool&lt;/code&gt;，相反，为了节省空间，它储存的是&lt;code&gt;bool&lt;/code&gt;的紧凑表示。在一个典型的实现中，储存在&lt;code&gt;vector&lt;/code&gt;中的每个&lt;code&gt;bool&lt;/code&gt;仅占一个二进制位，一个8位的字节可容纳8个&lt;code&gt;bool&lt;/code&gt;。在内部，&lt;code&gt;vector&amp;lt;bool&amp;gt;&lt;/code&gt;使用了与位域（bitfield）一样的思想，来表示它所存储的那些&lt;code&gt;bool&lt;/code&gt;；实际上它只是假装存储了这些&lt;code&gt;bool&lt;/code&gt;。&lt;strong&gt;位域与bool相似，它只能表示两个可能的值，但是在bool和看似bool的位域之间有一个很重要的区别:你可以创建一个指向bool的指针，而指向单个位的指针则是不允许的。所以你以为vector存了bool，但其实他储存的不是bool，自然使用[]下标访问返回的也不会是bool*类型的指针，自然无法给bool*类型的变量赋值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/QQ%E6%88%AA%E5%9B%BE20230514181512.png&quot; alt=&quot;QQ截图20230514181512&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们在more effective 条款30的笔记中写的例子就和这个差不多，类内套了一个类。同时&lt;code&gt;operator[]&lt;/code&gt;返回的是代理类对象。所以现在我们可以很好的理解这个例子了。&lt;/p&gt;
&lt;p&gt;当你需要 vector时，标准库提供了两种选择，可以满足绝大多数情况下的需求。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种是 deque。deque 几乎提供了 vector 所提供的一切(没有reserve和capacity)，但deque 是一个 STL 容器，而且它确实存储 bool。当然 deque 中元素的内存不是连续的，所以你不能把 deque 中的数据传递给一个期望 bool 数组的 C API。&lt;/li&gt;
&lt;li&gt;第二种可以替代 vector 的选择是 bitset。bitset 不是 STL 容器，但它是标准 C++ 库的一部分。与 STL 容器不同的是，它的大小(即元素的个数)在编译时就确定了，所以它不支持插入和删除元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;细节&lt;/h3&gt;
&lt;p&gt;我们有如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func1() {
    std::vector&amp;lt;bool&amp;gt; v;
    v.push_back(false);
    std::cout &amp;lt;&amp;lt; v[0] &amp;lt;&amp;lt; &quot; &quot;;  // 0
    const auto b = v[0];
    auto c = b;
    c = true;
    std::cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;       // 1 1
    cout &amp;lt;&amp;lt; &amp;amp;c &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; &amp;amp;b &amp;lt;&amp;lt; endl;          // 0x7ffd1f5c73c0 0x7ffd1f5c73b0
    cout &amp;lt;&amp;lt; c._M_p &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; b._M_p &amp;lt;&amp;lt; endl;  // 0x5578bbd8eeb0 0x5578bbd8eeb0
}
void func2() {
    std::vector&amp;lt;int&amp;gt; v;
    v.push_back(0);
    std::cout &amp;lt;&amp;lt; v[0] &amp;lt;&amp;lt; &quot; &quot;;  // 0
    const auto b = v[0];
    auto c = b;
    c = 1;
    std::cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;  // 1 0
    cout &amp;lt;&amp;lt; &amp;amp;c &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; &amp;amp;b &amp;lt;&amp;lt; endl;     // 0x7ffdc06bffac 0x7ffdc06bffa8
}
void func3() {
    std::vector&amp;lt;int*&amp;gt; v;
    v.push_back(new int(0));
    std::cout &amp;lt;&amp;lt; *v[0] &amp;lt;&amp;lt; &quot; &quot;;  // 0
    const auto b = v[0];
    auto c = b;
    *c = 1;
    std::cout &amp;lt;&amp;lt; *c &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; *b &amp;lt;&amp;lt; endl;  // 1 1
    cout &amp;lt;&amp;lt; &amp;amp;c &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; &amp;amp;b &amp;lt;&amp;lt; endl;       // 0x7ffdc06bffa8 0x7ffdc06bffa0
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;         // 0x563d96d19eb0 0x563d96d19eb0
    auto del = [](int* p) {
        delete p;
        p = nullptr;
    };
    std::for_each(v.begin(), v.end(), del);
}
int main() {
    func1();
    func2();
    func3();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现，&lt;code&gt;func1&lt;/code&gt;修改了元素的值。但是理论上不应该被修改，如&lt;code&gt;func2&lt;/code&gt;一样。原因就在于&lt;code&gt;b&lt;/code&gt;和&lt;code&gt;c&lt;/code&gt;的类型并不是&lt;code&gt;bool&lt;/code&gt;，而是&lt;code&gt;std::vector&amp;lt;bool&amp;gt;::reference&lt;/code&gt;。所以说他的行为和&lt;code&gt;func3&lt;/code&gt;相似。我们从其元素地址和元素底层的值地址中就可以看出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241018222823.png&quot; alt=&quot;微信图片_20241018222823&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241018222812.png&quot; alt=&quot;微信图片_20241018222812&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;第十九条：理解相等（equality）和等价（equivalence）的区别。&lt;/h2&gt;
&lt;p&gt;标准关联容器（如set, map）总是保持排列顺序的，所以每个容器必须有一个比较函数（默认为less）来决定保持怎样的顺序。等价的定义正是通过该比较函数而确定的，因此，标准关联容器的使用者要为所使用的每个容器指定一个比较函数（用来决定如何排序）。如果该关联容器使用相等来决定两个对象是否有相同的值，那么每个关联容器除了用于排序的比较函数外，还需要另一个比较函数来决定两个值是否相等。（默认情况下，该比较函数应该是equal_to，但有趣的是，equal_to从来没有被用作STL的默认比较函数。当STL中需要相等判断时，一般的惯例是直接调用operator==。&lt;/p&gt;
&lt;p&gt;个人理解：假如我们让使用set去忽略大小写地插入字符串。则我们会基于set一个仿函数告知他这一点。随后我们插入&lt;code&gt;&quot;abc&quot;&lt;/code&gt; 和&lt;code&gt;&quot;ABC&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;会发现只有第一个可以插入。是因为在这时候，&lt;code&gt;&quot;abc&quot;&lt;/code&gt; 和&lt;code&gt;&quot;ABC&quot;&lt;/code&gt;是&lt;strong&gt;等价&lt;/strong&gt;的。但是&lt;code&gt;&quot;abc&quot;&lt;/code&gt; 和&lt;code&gt;&quot;ABC&quot;&lt;/code&gt;并&lt;strong&gt;不相等&lt;/strong&gt;。等价是我们认为告知的。&lt;/p&gt;
&lt;h2&gt;第二十条：为包含指针的容器（关联或序列式容器）指定自定义比较器。&lt;/h2&gt;
&lt;p&gt;如下代码，我们有一个装有指针的容器。如果按照默认排序 当我们使用如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;string*&amp;gt; myvec;
sort(myvec.begin(), myvec.end());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实会被隐含地推导为如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sort(myvec.begin(), myvec.end(), less&amp;lt;string*&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是明显错误的。此处会对指针排序。所以必须自定义比较器。如下。顺便练习删除器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void deleter(string* item){ //自定义删除器。
    if (item!=nullptr){
        delete item;
        item = nullptr;
    }
}

class mydeleter{ //自定义删除器。
    public:
        bool operator()(string* item){
            if(item != nullptr){
                delete item;
                item = nullptr;
            }
        }
};
class mysort{
    public:
        bool operator()(const string* a, const string* b) const{
            if(*a &amp;lt; *b){
                return true;
            }
            return false;
        }   
};

int main(){
    vector&amp;lt;string*&amp;gt; myvec;
    myvec.push_back(new string(&quot;bcd&quot;));
    myvec.push_back(new string(&quot;cde&quot;));
    myvec.push_back(new string(&quot;abc&quot;));


    sort(myvec.begin(), myvec.end()); //这种排序方式是错误的。这会按照指针的值来排序。错误的。
    sort(myvec.begin(), myvec.end(), mysort()); //必须使用自定义比较器。
    
    for_each(myvec.begin(), myvec.end(), mydeleter()); //记住释放指针容器内的指针。上下两种都可以
    for_each(myvec.begin(), myvec.end(), deleter); //记住释放指针容器内的指针。
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;尤其是关联容器在创建之时就需要提供比较器。而默认创建的指针关联式容器的比较器是对指针排序，这明显不是我们希望的。所以创建指针关联式容器的时候就必须指定好我们的自定义比较器。&lt;/li&gt;
&lt;li&gt;包含智能指针或迭代器的容器亦如此。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二十一条：总是让比较函数在等值情况下返回false。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class myfunc{
    public:
    bool operator()(const int&amp;amp;a, const int&amp;amp; b) const{
        return a&amp;lt;=b; //注意这里
    }
};


int main(){
    set&amp;lt;int, myfunc&amp;gt; s;
    s.insert(10);
    s.insert(10);
    s.insert(11);
    for(auto i:s){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个set会储存两个10和一个11。这破坏了规则。&lt;/p&gt;
&lt;p&gt;因为set会先判断两个参数是否&lt;strong&gt;等价&lt;/strong&gt;。这意味着是根据我们的规则来的&lt;/p&gt;
&lt;p&gt;他会这么判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!(10 &amp;lt;= 10) &amp;amp;&amp;amp; !(10 &amp;gt;= 10);
!(true) &amp;amp;&amp;amp; !(true);
false &amp;amp;&amp;amp; false;
false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样他会返回不等价。于是就塞进去了。这不应该。&lt;/p&gt;
&lt;p&gt;所以永远记住，在任何情况下都要让自定义比较函数在两个变量相等的情况下返回&lt;code&gt;false&lt;/code&gt;。术语叫严格弱序化。&lt;/p&gt;
&lt;h2&gt;第二十二条：切勿直接修改set或multiset中的键。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;我们已经知道了无法修改map和multimap的key。是因为保存至红黑树的&lt;code&gt;pair&lt;/code&gt;里面的&lt;code&gt;key&lt;/code&gt;是&lt;code&gt;const&lt;/code&gt;的。(下面提到了)。&lt;/li&gt;
&lt;li&gt;set不允许更改值是因为迭代器返回的是常量迭代器也就是&lt;code&gt;const iterator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设我们现在有个&lt;code&gt;student&lt;/code&gt;，里面包含了&lt;code&gt;id&lt;/code&gt;和&lt;code&gt;name&lt;/code&gt;。我们有个装有它的&lt;code&gt;set&lt;/code&gt;。并且使用&lt;code&gt;ID&lt;/code&gt;来进行排序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;假设我们想更改名字，可以吗？可以。因为针对&lt;code&gt;set&lt;/code&gt;的排序是&lt;code&gt;student&lt;/code&gt;的&lt;code&gt;id&lt;/code&gt;。也就是说，实际上，&lt;code&gt;student&lt;/code&gt;的&lt;code&gt;ID&lt;/code&gt;是这个&lt;code&gt;set&lt;/code&gt;中元素的键(key), 而其他数据比如&lt;code&gt;name&lt;/code&gt;只不过和这个键绑在一起而已。所以没有理由不能更改非key的部分。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是实际做起来有些麻烦。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class student{
    public:
        student(int x, const string&amp;amp; y):id(x), name(y){};
        int id;
        string name;

        void change_name(const string&amp;amp; s){
            name = s;
        }
};

class mycomp{
    public:
        bool operator()(const student a, const student b) const{
            if(a.id &amp;lt; b.id){
                return true;
            }
            return false;
        }   
};


int main(){

    set&amp;lt;student, mycomp&amp;gt; my_set;
    my_set.insert(student(1,&quot;miku&quot;));
    my_set.insert(student(2,&quot;luka&quot;));
    my_set.insert(student(3,&quot;nozomi&quot;));

    auto t = my_set.begin();			
    cout &amp;lt;&amp;lt; t-&amp;gt;name &amp;lt;&amp;lt; endl; //输出miku
    const_cast&amp;lt;student&amp;amp;&amp;gt;(*t).change_name(&quot;changedmiku&quot;); //1
    static_cast&amp;lt;student&amp;gt;(*t).change_name(&quot;changedmiku&quot;); //2
    cout &amp;lt;&amp;lt; t-&amp;gt;name &amp;lt;&amp;lt; endl; //经过1输出changedmiku, 经过2不变，输出miku。



    return 0;


}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里的1是对的。2是错的。我来解释为什么。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;首先，我们提到了，&lt;code&gt;set&lt;/code&gt;拿到的迭代器是常量迭代器也就是&lt;code&gt;const iterator&lt;/code&gt;。所以我们需要把它的&lt;code&gt;const&lt;/code&gt;弄掉才行。
&lt;ul&gt;
&lt;li&gt;注意为什么我们使用了&lt;code&gt;student&amp;amp;&lt;/code&gt;做为新的类型？第一是因为&lt;code&gt;const_cast&lt;/code&gt;只接受指针或引用类型的转换。不支持转换对象。&lt;/li&gt;
&lt;li&gt;其次，我们需要作用到原来的对象上。所以必须使用引用的方式。来让这个临时cast出来的对象是一个原对象的引用，这样才能施加操作到原来的对象上。&lt;/li&gt;
&lt;li&gt;记得我们在杂记2说的，四种cast不改变原对象，会生成一个新的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果使用2的方式，我们会cast出一个临时的匿名对象出来。然后把&lt;code&gt;change_name&lt;/code&gt;施加到了临时对象上。所以没用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果我们想自由，安全的对四种关联容器进行元素修改。应使用如下几步：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到你想修改的容器的元素。如果你不能肯定最好的做法，第45条介绍了如何执行一次恰当的搜索来找到特定的元素。&lt;/li&gt;
&lt;li&gt;为将要被修改的元素做一份副本。在map或multimap的情况下，请记住，不要把该副本的第一个部分声明为const。毕竟，你想要改变它。&lt;/li&gt;
&lt;li&gt;修改该副本，使它具有你期望它在容器中的值。&lt;/li&gt;
&lt;li&gt;把该元素从容器中删除，通常是通过调用erase来进行的（见第9条）。&lt;/li&gt;
&lt;li&gt;把新的值插入到容器中。如果按照容器的排列顺序，新元素的位置可能与被删除元素的位置相同或紧邻，则使用“提示”（hint）形式的insert，以便把插入的效率从对数时间提高到常数时间。把你从第1步得来的迭代器作为提示信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;student target(2,&quot;luka&quot;); //目标对象
auto t = my_set.find(target); // 1 寻找目标对象 
if(t != my_set.end()){ //如果找到了
    student temp(*t);  // 2 获取一个目标副本
    temp.change_name(&quot;changed_luka&quot;); //3 对副本施加更改操作
    t = my_set.erase(t); //4 把容器内的对应源元素删除。注意set的erase会返回一个指向删除的元素后面的第一个元素的迭代器。
    my_set.insert(t, temp); //5 在指定位置插入。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第二十三条：考虑使用排序的vector替换关联容器&lt;/h2&gt;
&lt;p&gt;这个建议的前提是这个容器会有如下三个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个新的数据结构，并插入大量元素，在这个阶段，几乎所有的操作都是插入和删除操作。很少或几乎没有查找操作。&lt;/li&gt;
&lt;li&gt;查找阶段：查询该数据结构找到特点的信息，在这个阶段，几乎所有的操作都是查找很少或几乎没有删除。&lt;/li&gt;
&lt;li&gt;重组阶段：改变数据结构的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么这样的容器最好使用vector？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关联式容器针对保存的每一个对象都需要额外的指针。比如map和set使用红黑树，unordered使用哈希表。都需要额外指针。这也就导致需要更多的内存。&lt;/li&gt;
&lt;li&gt;关联式容器不保证是否在物理内存中相邻，所以在进行如二分查找时，会遇到更多的缺页错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是这里必须保证vector有序，但是在插入或删除元素的时候，或重新分配内存的时候都会进行移动或拷贝。这开销极大，但是关联容器却没有这个缺点。&lt;strong&gt;所以对数据结构的使用方式是：查找操作几乎从不跟插入和删除操作混在一起”时，再考虑使用排序的vector而不是关联容器才是合理的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;第四十五条：正确区分count、find、binary_search、lower_bound、upper_bound和equal_range。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在使用&lt;code&gt;序列容器&lt;/code&gt;和&lt;code&gt;multimap&lt;/code&gt;, &lt;code&gt;multiset&lt;/code&gt;的时候，&lt;code&gt;find&lt;/code&gt;效率比&lt;code&gt;count&lt;/code&gt;快。因为&lt;code&gt;count&lt;/code&gt;总是会遍历完整个区间 或 检查容器中的每一个对象。而&lt;code&gt;find&lt;/code&gt;找到后就可以返回。
&lt;ul&gt;
&lt;li&gt;但是在&lt;code&gt;set&lt;/code&gt;和&lt;code&gt;map&lt;/code&gt;里面不是问题，因为&lt;code&gt;set&lt;/code&gt;不允许重复值，&lt;code&gt;map&lt;/code&gt;不允许重复键。所以&lt;code&gt;count&lt;/code&gt;总是会返回0或1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/QQ%E6%88%AA%E5%9B%BE20220922003210.png&quot; alt=&quot;QQ截图20220922003210&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;杂记&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;remove的原理是把每一个不和指定value相等的元素轮番赋值给first之后的空间。 假如我们有{0,1,0,2,0,3,0,4}，要移除所有的0， remove后会变成 {1,2,3,4,0,3,0,4} 。你使用auto打印或者是迭代器遍历的话，也会输出{1,2,3,4,0,3,0,4}。所以他什么都不删除。从下标[4]开始后的是脏数据。 所以&lt;strong&gt;remove不改变size，也不改变capacity&lt;/strong&gt; 所以要利用返回的迭代器配合erase使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS31.assets/QQ%E6%88%AA%E5%9B%BE20220723013129.png&quot; alt=&quot;QQ截图20220723013129&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**可以把capacity理解为STL为整个容器分配的数据存储空间的大小，size理解为尾迭代器和头迭代器的区间距离。**所以erase和remove因为都不改变capacity的大小（new出来的内存没办法动态扩缩容，只能搬移），所以数据不会真正删除，而是覆盖式赋值后，如果size改变，就改变end迭代器位置，如果不改变则不动
&lt;ul&gt;
&lt;li&gt;这就是为什么remove不改变size，因为覆盖式赋值后end迭代器指向位置不变&lt;/li&gt;
&lt;li&gt;这也是为什么erase改变size，因为覆盖式赋值后end迭代器指向的位置变化了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;unique也一样，是轮番赋值，不改变size和capacity。&lt;/li&gt;
&lt;li&gt;erase会&lt;strong&gt;调用析构&lt;/strong&gt;移除数据（意味着没有析构就啥也不干）。同时将容器size缩短（更改end迭代器位置），但是依旧不改变capacity。 &lt;strong&gt;改变capacity可以在清除完需要的数据后使用swap或者是直接用匿名对象赋值&lt;/strong&gt;。注意，erase依旧是和remove一样，覆盖式赋值[&lt;strong&gt;注意这里只针对序列式容器&lt;/strong&gt;]（把vector中要删除位置后面的所有数据移动到要删除的位置）。但是erase会把后面的脏数据删掉 &lt;strong&gt;[注意：用int做实验的时候因为int没有析构函数，所以数据还在那。]（但是自定义类型会调用析构函数。注意！调用析构函数不代表数据会被删除或内存被释放！！释放内存依靠的是delete。）&lt;/strong&gt;。又因为capacity不改变，所以依旧可以强制访问。
&lt;ul&gt;
&lt;li&gt;注意，erase调用的析构函数是自定义对象的析构。自定义对象一般在类成员有指针的时候才会有显式析构。这时候，erase调用析构仅仅是把元素的指针变量指向的数据释放掉，但是元素的指针啊，普通变量啊都还在。这时候如果强制访问了指针，会出现危险访问。这也是为什么析构函数里面，释放掉指针指向的变量后一定要将指针显式置空。&lt;/li&gt;
&lt;li&gt;这里依旧注意指针容器。指针类型没有我们心中的析构函数。仅仅是指针自己没了而已。资源还在。所以此处有潜在可能导致内存泄漏。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这也是为什么没有任何一个函数可以改变容器的capacity。容器的capacity是容器的分配器给容器元素分配的内存，也就是说过的&lt;code&gt;new&lt;/code&gt;了一块区域。这块的大小不能变。所以唯一的办法就是把整个容器析构然后新建。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt;t1 = {1,2,2,3,4,5};
t1.erase(t1.begin() + 1); //erase了，数据清除改变size但是不改变capacity
t1 = vector&amp;lt;int&amp;gt;(t1); //临时对象释放法
vector&amp;lt;int&amp;gt;(t1).swap(t1); //swap释放法。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;clear直接调用的erase&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;reserve是容器预留空间，但并不真正创建元素对象，在创建对象之前，不能访问容器内的元素，因此当加入新的元素时，需要用push_back()/insert()函数。reserve不修改size大小，只修改capacity大小。而且&lt;strong&gt;只增不减&lt;/strong&gt;。reserve强迫容器把它的容量变为至少是n，前提是n不小于当前的大小。这通常会导致重新分配，因为容量需要增加。（如果n比当前的容量小，则vector忽略该调用，什么也不做；而string则可能把自己的容量减为size()和n中的最大值，但是string的大小肯定保持不变。）。&lt;strong&gt;记住，在reserve后不可以直接使用下标的方式为其赋值，例如&lt;code&gt;vector[1] = 4&lt;/code&gt;这样的形式，因为reserve不构建元素。没有赋值之前，直接去访问元素会导致访问到未初始化的元素。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resize是改变容器的大小，并且创建元素对象，因此，调用这个函数之后，就可以引用容器内的对象了，因此当加入新的元素时，用operator[]操作符，或者用迭代器来引用元素对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;resize&lt;strong&gt;不改变&lt;/strong&gt;capacity大小。&lt;/li&gt;
&lt;li&gt;resize的容器，如果n比当前的大小（size)要小，元素并没有被删除。依旧可以被访问到。在调用resize之后，size将返回n。&lt;s&gt;如果n比当前的大小（size）要小，则容器尾部的元素将会被析构。&lt;/s&gt; （这段应该是不同编译器实现不一样）。&lt;/li&gt;
&lt;li&gt;如果n比当前的**大小(size)**要大，则通过默认构造函数创建的新元素（就是用 你给的用来填充的容器的值 创建的元素）将被添加到容器的末尾。&lt;/li&gt;
&lt;li&gt;如果n比当前的**容量（capacity)**要大，那么在添加元素之前，将先重新分配内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上一条举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt;a;
    a.reserve(10);
    cout &amp;lt;&amp;lt; a.size()&amp;lt;&amp;lt;endl; 		//0
    cout &amp;lt;&amp;lt; a.capacity()&amp;lt;&amp;lt;endl;		//10
    for(int i = 0; i &amp;lt; 10; i++){
        a.push_back(i);
    }
    a.push_back(22);
    cout &amp;lt;&amp;lt; a.size()&amp;lt;&amp;lt;endl;			//11
    cout &amp;lt;&amp;lt; a.capacity()&amp;lt;&amp;lt;endl;		//20
    cout &amp;lt;&amp;lt; &amp;amp;a[0] &amp;lt;&amp;lt; endl;			//0xd618c0
    a.resize(15, 6);				//此时resize的大小，大于size，小于capacity，不会发生内存重新分配
    cout &amp;lt;&amp;lt; a.size()&amp;lt;&amp;lt;endl;			//15
    cout &amp;lt;&amp;lt; a.capacity()&amp;lt;&amp;lt;endl;		//20
    cout &amp;lt;&amp;lt; &amp;amp;a[0] &amp;lt;&amp;lt; endl;			//0xd618c0
    a.resize(25,6);					//此时resize的大小，大于capacity，发生内存重新分配
    cout &amp;lt;&amp;lt; a.size()&amp;lt;&amp;lt;endl;			//25
    cout &amp;lt;&amp;lt; a.capacity()&amp;lt;&amp;lt;endl;		//30
    cout &amp;lt;&amp;lt; &amp;amp;a[0] &amp;lt;&amp;lt; endl;			//0xd61940
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新增例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt; myvec{0,1,0,2,0,3,0,4};
    cout &amp;lt;&amp;lt; myvec.size() &amp;lt;&amp;lt;endl;		//8
    cout &amp;lt;&amp;lt; myvec.capacity() &amp;lt;&amp;lt;endl;	//8
    myvec.resize(2);					//resize缩小了
    cout &amp;lt;&amp;lt; myvec.size() &amp;lt;&amp;lt;endl;		//size变为2
    cout &amp;lt;&amp;lt; myvec.capacity() &amp;lt;&amp;lt;endl;	//capacity不变，仍是8
    for(auto i:myvec){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }
    for(auto i = myvec.begin(); i != myvec.end(); i++){
        cout &amp;lt;&amp;lt; *i &amp;lt;&amp;lt; endl;
    }
    auto i = myvec.begin() + 5;			//因为capacity不变，所以依旧可以访问数据，但是因为size改变了，end位置改变了，所以无法通过遍历的方式访问。
    cout &amp;lt;&amp;lt; *i &amp;lt;&amp;lt; endl;
    
    
    
    myvec.resize(100);
    cout &amp;lt;&amp;lt; myvec.size() &amp;lt;&amp;lt;endl;		//size变为100。因为resize会创建元素
    cout &amp;lt;&amp;lt; myvec.capacity() &amp;lt;&amp;lt;endl;	//capacity变为100。因为扩张了发生了内存重新分配。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;capacity是vector和string独有的。它是预分配的内存容量。使用reserve时分配的就是这个。预分配也是分配了。所以也占用空间。&lt;/li&gt;
&lt;li&gt;STL是拷贝进来，拷贝出去的执行方式。即便是形参是引用，也会拷贝实际数据成员至容器。形参是引用只是防止入参时候的拷贝，但是放入容器依旧是拷贝。如果形参不是引用，则会拷贝两次。第一次是入参时，第二次是放入容器时。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void addval(const myclass&amp;amp; obj){
   myvec.push_back(obj); //obj会被拷贝至容器。
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;容器本身在栈上，容器的数据在堆上。也就是&lt;code&gt;vector&amp;lt;int&amp;gt; a(100, 5);&lt;/code&gt;。&lt;code&gt;vector a&lt;/code&gt;本身在栈上，但是这100个5在堆上。STL帮我们执行内存分配和释放。&lt;code&gt;a&lt;/code&gt;的本身大小只有三根指针。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;array&lt;/code&gt;是个例外。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;原始数组也就是 &lt;code&gt;T a[]&lt;/code&gt;这种静态数组是栈上的&lt;/li&gt;
&lt;li&gt;原始数组&lt;code&gt;new &lt;/code&gt;出来的在堆上（啥玩意new的都在堆）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;STL的array也是在栈上的。并且和原始数组一样是编译时创建。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/QQ%E6%88%AA%E5%9B%BE20220803230048.png&quot; alt=&quot;QQ截图20220803230048&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;vector&lt;/code&gt;的构造函数初始化&lt;code&gt;vector&lt;/code&gt;的时候，是会&lt;strong&gt;创建元素&lt;/strong&gt;的。&lt;code&gt;push_back&lt;/code&gt;会直接在尾部添加元素，所以这是&lt;code&gt;reserve&lt;/code&gt;存在的意义。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt; myvec(2);
    myvec.push_back(444);
    for(auto i:myvec){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段代码会输出&lt;code&gt;0 0 444&lt;/code&gt;。因为使用了&lt;code&gt;vector&lt;/code&gt;的有参构造默认初始化了元素，也就是&lt;strong&gt;创建了元素&lt;/strong&gt;，这样在尾部添加的话自然会添加在后面而不是覆盖。这也是为什么&lt;code&gt;reserve&lt;/code&gt;不可以用下标访问（因为只分配储存空间，而不创建元素），而有参构造可以。&lt;/p&gt;
&lt;h1&gt;vector扩容后调用移动构造的问题&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vector扩容后，如果储存的元素有移动构造且移动构造函数声明为&lt;code&gt;noexcept&lt;/code&gt;，那么就会触发移动构造&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;注意这一条仅适用于扩容的操作。在不是扩容的场景下，非&lt;code&gt;noexcept&lt;/code&gt;也可以触发移动。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;vector创建时使用构造函数或reserve&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;使用有参构造函数的两种方式都会创建元素。所以&lt;code&gt;push_back&lt;/code&gt;会在创建的元素后添加。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt;v1(5);
vector&amp;lt;int&amp;gt;v2(5,0);
v1.push_back(100);
v2.push_back(100);
for(auto i:v1){
    cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl; //输出0 0 0 0 0 100
}
for(auto i:v2){
    cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl; //输出0 0 0 0 0 100
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用无参构造函数不会创建元素。所以必须使用&lt;code&gt;push_back&lt;/code&gt;类函数来进行插入。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt;v3;
v3.push_back(100);
for(auto i:v3){
    cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl; //只输出100
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们提到过，&lt;code&gt;reserve&lt;/code&gt;只预留空间，不创建元素。所以必须使用&lt;code&gt;push_back&lt;/code&gt;类函数来进行插入。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt;v4;
v4.reserve(5);
v4.push_back(100);
for(auto i:v4){
        cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl; //只输出100
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们提到过，&lt;code&gt;resize&lt;/code&gt;是改变容器的大小，并且创建元素对象，因此，调用这个函数之后，就可以引用容器内的对象了。所以&lt;code&gt;push_back&lt;/code&gt;会在创建的元素后添加。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt;v5;
v5.resize(5);
v5.push_back(100);
for(auto i:v5){
    cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl; //输出0 0 0 0 0 100
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;emplace_back 和 push_back 区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;emplace_back()&lt;/code&gt;在容器尾部添加一个元素时，可以直接传入构造函数需要的参数来直接在容器内原地构造。因为&lt;code&gt;emplace_back&lt;/code&gt;使用了完美转发。也就时使用&lt;code&gt;emplace_back()&lt;/code&gt;可以不显式调用对象的构造函数来避免一次额外的构造。&lt;strong&gt;就是把所需参数完美转发至元素类型的构造函数。注意，是储存元素类型的构造函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;具体触发移动构造或拷贝构造的规则
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;emplace_back&lt;/code&gt;直接传入默认构造需要的参数时，不论是否有移动构造函数，都是原地构造，只会调用一次&lt;strong&gt;对应的构造函数&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;emplace_back&lt;/code&gt;不可以使用参数列表。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;emplace_back&lt;/code&gt;以左值对象的形式传入时，不论是否有移动构造函数，都是只调用一次&lt;strong&gt;拷贝构造&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;emplace_back&lt;/code&gt;以显式调用对象构造函数形式传入时（临时对象，右值）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有移动构造，则先调用&lt;strong&gt;对应的构造函数&lt;/strong&gt;构造临时对象，然后因为是临时对象触发&lt;strong&gt;移动构造&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果没有移动构造，则先调用&lt;strong&gt;对应的构造函数&lt;/strong&gt;构造临时对象然后调用&lt;strong&gt;拷贝构造&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;emplace_back&lt;/code&gt;以右值对象（例如&lt;code&gt;move&lt;/code&gt;（左值对象），或者就是右值）的形式传入时 :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果有移动构造则只触发&lt;strong&gt;移动构造&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果没有移动构造，则调用&lt;strong&gt;拷贝构造&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj(int x, int y):vala(x), valb(y){ cout &amp;lt;&amp;lt;&quot;constructor&quot; &amp;lt;&amp;lt; endl;}

        myobj(const myobj&amp;amp; obj){
            this-&amp;gt;vala = obj.vala;
            this-&amp;gt;valb = obj.valb;
            cout &amp;lt;&amp;lt;&quot;copy constructor&quot; &amp;lt;&amp;lt; endl;
        }

        myobj(myobj&amp;amp;&amp;amp; obj){
            this-&amp;gt;vala = obj.vala;
            this-&amp;gt;valb = obj.valb;
            cout &amp;lt;&amp;lt;&quot;mv constructor&quot; &amp;lt;&amp;lt; endl;
        }

        int vala;
        int valb;
};
int main(){
    vector&amp;lt;myobj&amp;gt; vec;
    vec.reserve(10);
  
    vec.emplace_back(3,3);          //1直接传入默认构造需要的参数时，不论是否有移动构造函数，都是原地构造，只会调用一次默认构造函数
    vec.emplace_back({3,3});		//1.1不可以，不能使用参数列表。
    
    myobj mm(1,2);
    vec.emplace_back(mm);           //2 左值对象的形式传入时，不论是否有移动构造函数，因为对象已经存在，所以只调用一次拷贝构造。
    
	vec.emplace_back(myobj(1,2)); 	//3 显式使用构造函数时，先调用对应的构造函数构造临时对象，然后因为是临时对象触发移动构造
    							  //如果此时没有移动构造，则先调用对应的构造函数构造临时对象然后调用拷贝构造
	vec.emplace_back(move(mm));			//4 右值对象传入时（如使用move)，如果有移动构造则只触发移动构造。
										//如果此时没有移动构造，则调用拷贝构造

	vec.push_back(myobj(1,2));      //先调用默认构造，然后因为是临时对象触发移动构造
	vec.push_back(1,2);             //不可以

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;push_back&lt;/code&gt;,必须先调用对象的默认构造然后根据是临时对象与否（右值或左值）来决定调用拷贝构造还是移动构造。
&lt;ul&gt;
&lt;li&gt;具体触发移动构造或拷贝构造的规则:
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;push_back&lt;/code&gt;以参数列表的形式传入时&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有移动构造函数，先调用&lt;strong&gt;对应的构造函数&lt;/strong&gt;构造临时对象，再使用&lt;strong&gt;移动构造&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;没有移动构造函数，先调用&lt;strong&gt;对应的构造函数&lt;/strong&gt;构造临时对象，然后使用&lt;strong&gt;拷贝构造&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push_back&lt;/code&gt;不可以直接传入构造函数所需参数。必须使用参数列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;push_back&lt;/code&gt;以左值对象的形式传入时，不论是否有移动构造函数，都是调用一次&lt;strong&gt;拷贝构造&lt;/strong&gt;函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;push_back&lt;/code&gt;以显式调用对象构造函数形式传入时（临时对象，右值）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有移动构造，则先调用&lt;strong&gt;对应的构造函数&lt;/strong&gt;构造临时对象，然后因为是临时对象触发&lt;strong&gt;移动构造&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果没有移动构造，则先调用&lt;strong&gt;对应的构造函数&lt;/strong&gt;构造临时对象然后调用&lt;strong&gt;拷贝构造&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;push_back&lt;/code&gt;以右值对象（例如&lt;code&gt;move&lt;/code&gt;（左值对象），或者就是右值）的形式传入时 :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有移动构造则只触发&lt;strong&gt;移动构造&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果没有移动构造，则调用&lt;strong&gt;拷贝构造&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vec.push_back({1,2});			//1使用参数列表的时候，先调用对应的构造函数构造临时对象，如果有移动构造则直接使用移动构造。
							//如果没有，先调用对应的构造函数构造临时对象，然后使用拷贝构造。
vec.push_back(1,2);             //1.1不可以

myobj mm(1,2);
vec.push_back(mm);  		//2以左值对象的形式传入时，不论是否有移动构造函数，都是调用一次拷贝构造函数

vec.push_back(myobj(1,2));      //3显式使用构造函数时，先调用对应的构造函数构造临时对象，然后因为是临时对象触发移动构造
    							  //如果此时没有移动构造，则先调用对应的构造函数构造临时对象然后调用拷贝构造
vec.push_back(move(mm));		//4右值对象传入时（如使用move)，如果有移动构造则只触发移动构造。
								//如果此时没有移动构造，则调用拷贝构造

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;emplace_back&lt;/code&gt;传入&lt;code&gt;pair&lt;/code&gt;的时候不要忘记，&lt;code&gt;emplace_back&lt;/code&gt;是原地构造。就是把所需参数完美转发至&lt;strong&gt;元素类型&lt;/strong&gt;的构造函数。&lt;/p&gt;
&lt;p&gt;所以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;pair&amp;lt;string,int&amp;gt;&amp;gt; history;
string s = &quot;1b23&quot;;
history.emplace_back({s,0}); //错误。这时候错误地传入了pair构造完毕的临时对象。
history.emplace_back(s,0); //正确，传入构造pair对象所需要的参数。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;针对花括号初始化器的问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不推导语境在模板笔记&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::initializer_list&lt;/code&gt;的笔记在杂记3&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main() { 
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; myvec;
    myvec.push_back({1, 3}); // OK
    myvec.emplace_back({1, 3}); //不行.
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么这个地方用&lt;code&gt;emplace_back&lt;/code&gt;不可以? 因为你以为&lt;code&gt;{1,3}&lt;/code&gt;是&lt;code&gt;initializer_list&lt;/code&gt;, 但是其实它啥都不是. 因为看见这种花括号列表, 想让它变成&lt;code&gt;initializer_list&lt;/code&gt;是有条件的. 要么是我们在做函数调用且该函数入参恰好需要个&lt;code&gt;initializer_list&lt;/code&gt;, 要么是用&lt;code&gt;auto&lt;/code&gt;推导一下.&lt;/p&gt;
&lt;p&gt;同时, &lt;code&gt;push_back&lt;/code&gt;是函数, 而&lt;code&gt;emplace_back&lt;/code&gt;是函数模板. 所以说它需要依照类型推导来合成. 也就是&lt;code&gt;emplace_back&lt;/code&gt;接受一个以&lt;code&gt;initializer_list&lt;/code&gt;为入参的前提是&lt;code&gt;{1,3}&lt;/code&gt;是一个&lt;code&gt;initializer_list&lt;/code&gt;. 但是&lt;code&gt;{1,3}&lt;/code&gt;变成&lt;code&gt;initializer_list&lt;/code&gt;的前提是&lt;code&gt;emplace_back&lt;/code&gt;有接受一个&lt;code&gt;initializer_list&lt;/code&gt;的版本. 然后就死锁了.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;顺带一提, 形参非&lt;code&gt;initializer_list&lt;/code&gt;的时候且实参是花括号初始化器列表的时候, 这个是不推导语境.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以应该怎么办? 有三种办法.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myvec.emplace_back(initializer_list&amp;lt;int&amp;gt;{1, 3}); // 手动构造initializer_list
myvec.emplace_back&amp;lt;initializer_list&amp;lt;int&amp;gt;&amp;gt;({1, 3}); // 显式指明模板参数, 放弃自动推导
auto input = {1,3}; // 使用auto推导, 此时auto类型推导为initializer_list&amp;lt;int&amp;gt;
myvec.emplace_back(input); // 利用游戏规则
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;所以上面我们发现了，&lt;code&gt;emplace_back&lt;/code&gt; 和&lt;code&gt;push_back&lt;/code&gt;主要的使用场景区别就在于是否可以被原地构造。也就是可以被原地构造的时候（直接传入对象构造函数所需要的参数的时候）使用&lt;code&gt;emplace_back&lt;/code&gt;。其余场合没有区别。另外就是需要注意用花括号初始化器的问题.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话：push_back总是先创建对象，然后拷贝/移动到容器。emplace则可以选择直接在容器内构造对象。所以只有当我们想原地构造（直接放入容器 （直接传入构造函数参数，调用构造函数来直接构建对象））的时候选用emplace才是最佳方法。如果是想要&lt;em&gt;移入&lt;/em&gt;一个已存在的对象，则没有区别。因为二者遇到右值对象的时候都会调用移动构造。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;map的emplace原地构造和forward_as_tuple&lt;/h1&gt;
&lt;p&gt;先看一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myobj{
    myobj(int x){
        cout &amp;lt;&amp;lt; &quot;myobj constructor with arg&quot; &amp;lt;&amp;lt; endl;
    }
    myobj(int x, int y){
        cout &amp;lt;&amp;lt; &quot;myobj constructor with arg2&quot; &amp;lt;&amp;lt; endl;
    }
    myobj(){
        cout &amp;lt;&amp;lt; &quot;myobj constructor&quot; &amp;lt;&amp;lt; endl;
    }
    ~myobj(){
        cout &amp;lt;&amp;lt; &quot;myobj destructor&quot; &amp;lt;&amp;lt; endl;
    }
    myobj&amp;amp; operator=(const myobj&amp;amp; obj){
        cout &amp;lt;&amp;lt; &quot;myobj copy assignment&quot; &amp;lt;&amp;lt; endl;
        return *this;
    }
    myobj(const myobj&amp;amp; obj){
        cout &amp;lt;&amp;lt; &quot;myobj copy constructor&quot; &amp;lt;&amp;lt; endl;
    }
    myobj&amp;amp;&amp;amp; operator=(myobj&amp;amp;&amp;amp; obj){
        cout &amp;lt;&amp;lt; &quot;myobj move assignment&quot; &amp;lt;&amp;lt; endl;
        return std::move(*this);
    }
    myobj(myobj&amp;amp;&amp;amp; obj){
        cout &amp;lt;&amp;lt; &quot;myobj move constructor&quot; &amp;lt;&amp;lt; endl;
    }
};

int main(){
    map&amp;lt;int, myobj&amp;gt; mymap;
    map&amp;lt;int, myobj&amp;gt; mymap1;
    map&amp;lt;int, myobj&amp;gt; mymap11;
    map&amp;lt;int, myobj&amp;gt; mymap2{{1, myobj()}};
    cout &amp;lt;&amp;lt; &quot;-----&quot; &amp;lt;&amp;lt; endl;
    mymap.emplace(1, 2);
    mymap.emplace(1, 2, 3); // 不可能，别想了
    cout &amp;lt;&amp;lt; &quot;-----&quot; &amp;lt;&amp;lt; endl;
    mymap11.emplace(1, myobj());
    cout &amp;lt;&amp;lt; &quot;-----&quot; &amp;lt;&amp;lt; endl;
    mymap1.emplace(std::piecewise_construct, std::forward_as_tuple(1), std::forward_as_tuple());
    cout &amp;lt;&amp;lt; &quot;-----&quot; &amp;lt;&amp;lt; endl;
    return 0;
}
/*
mymap2:
myobj constructor
myobj move constructor
myobj copy constructor
myobj destructor
myobj destructor
----- mymap
myobj constructor with arg
----- mymap11
myobj constructor
myobj move constructor
myobj destructor
----- mymap1
myobj constructor
-----
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现。&lt;code&gt;mymap2&lt;/code&gt;构造一次，移动一次，拷贝一次。&lt;code&gt;mymap&lt;/code&gt;用了有参构造一次。&lt;code&gt;mymap11&lt;/code&gt; 构造一次 移动一次。而&lt;code&gt;mymap1&lt;/code&gt;只构造一次&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mymap2&lt;/code&gt;我们使用了初始化列表。构造是构造原始对象，移动是移动构造到&lt;code&gt;initializer_list&lt;/code&gt;, 拷贝是拷贝构造到容器内。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mymap&lt;/code&gt;我们使用了有参构造。有参构造的时候&lt;code&gt;emplace&lt;/code&gt;就地构造可以把参数转发给对应的构造函数，自然只构造一次。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mymap11&lt;/code&gt;我们也使用了&lt;code&gt;emplace&lt;/code&gt;就地构造。构造是构造原始对象，移动是&lt;code&gt;emplace&lt;/code&gt;移动构造到容器内。我们要理解就地构造，就地构造也是使用了参数就地构造。此时参数是右值的原始对象。所以调用了移动构造。为什么一定要一次拷贝或移动？原因是我们如果想用默认构造，那么就不能有参数。如果不传参数那么&lt;code&gt;emplace&lt;/code&gt;怎么办？没办法。所以如果想用无参构造则必须先构造临时对象然后移动进去。&lt;/p&gt;
&lt;p&gt;如果是多个参数呢？咋办？同时无参的时候需要拷贝或移动，是不是看起来很烦？下面就是如何解决这个问题。&lt;/p&gt;
&lt;h2&gt;std::piecewise_construct&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::piecewise_construct_t&lt;/code&gt; 是一个空类标签类型，用于区分接受两个元组实参的不同函数。也就是说，这个&lt;code&gt;tag&lt;/code&gt;表明后面的两个 &lt;code&gt;tuple&lt;/code&gt; 应当被分别传递给 &lt;code&gt;std::pair&lt;/code&gt; 的两个成员的构造函数（&lt;code&gt;first&lt;/code&gt; 和 &lt;code&gt;second&lt;/code&gt;），而不是将整个&lt;code&gt;tuple&lt;/code&gt; 作为一个单独的参数传递。&lt;/p&gt;
&lt;p&gt;这个东西会匹配到一个&lt;code&gt;pair&lt;/code&gt;针对&lt;code&gt;tuple&lt;/code&gt;的特化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// tuple.cpp
template &amp;lt;class _T1, class _T2&amp;gt;
template &amp;lt;typename... _Args1, typename... _Args2&amp;gt;
_GLIBCXX20_CONSTEXPR inline pair&amp;lt;_T1, _T2&amp;gt;::pair(piecewise_construct_t, tuple&amp;lt;_Args1...&amp;gt; __first,
                                                 tuple&amp;lt;_Args2...&amp;gt; __second)
    : pair(__first, __second, typename _Build_index_tuple&amp;lt;sizeof...(_Args1)&amp;gt;::__type(),
           typename _Build_index_tuple&amp;lt;sizeof...(_Args2)&amp;gt;::__type()) {} // 委托构造给下面的

template &amp;lt;class _T1, class _T2&amp;gt;
template &amp;lt;typename... _Args1, size_t... _Indexes1, typename... _Args2, size_t... _Indexes2&amp;gt;
_GLIBCXX20_CONSTEXPR inline pair&amp;lt;_T1, _T2&amp;gt;::pair(tuple&amp;lt;_Args1...&amp;gt;&amp;amp; __tuple1,
                                                 tuple&amp;lt;_Args2...&amp;gt;&amp;amp; __tuple2,
                                                 _Index_tuple&amp;lt;_Indexes1...&amp;gt;,
                                                 _Index_tuple&amp;lt;_Indexes2...&amp;gt;)
    : first(std::forward&amp;lt;_Args1&amp;gt;(std::get&amp;lt;_Indexes1&amp;gt;(__tuple1))...),
      second(std::forward&amp;lt;_Args2&amp;gt;(std::get&amp;lt;_Indexes2&amp;gt;(__tuple2))...) {}
// stl_pair.h
template &amp;lt;typename... _Args1, typename... _Args2&amp;gt;
_GLIBCXX20_CONSTEXPR pair(piecewise_construct_t, tuple&amp;lt;_Args1...&amp;gt;, tuple&amp;lt;_Args2...&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;std::forward_as_tuple&lt;/h2&gt;
&lt;p&gt;这个东西很简单，就是把一对参数包装为一个&lt;code&gt;tuple&lt;/code&gt;。&lt;strong&gt;该元组在以右值为实参时拥有右值引用数据成员，否则拥有左值引用数据成员。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class... Types &amp;gt;
tuple&amp;lt;Types&amp;amp;&amp;amp;...&amp;gt; forward_as_tuple( Types&amp;amp;&amp;amp;... args ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;，不可以和&lt;code&gt;make_tuple&lt;/code&gt;混用，下面的是绝对错误的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto tup = forward_as_tuple(1, 2, std::string(&quot;abc&quot;));
string a = get&amp;lt;2&amp;gt;(tup);
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
static_assert(std::is_same_v&amp;lt;decltype(tup), std::tuple&amp;lt;int&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, string&amp;amp;&amp;amp;&amp;gt;&amp;gt;); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个&lt;code&gt;tuple&lt;/code&gt;内的所有对象全是临时对象的引用。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;所以整体来说，就是我们遇到无参或多参构造的时候，也就是需要把构造函数参数传递给&lt;code&gt;emplace&lt;/code&gt;但是受限于语法支持时，我们选择用&lt;code&gt;forward_as_tuple&lt;/code&gt;包装一下。第一个参数是&lt;code&gt;tag&lt;/code&gt;，第二个参数是&lt;code&gt;key&lt;/code&gt;的构造函数参数包装成的&lt;code&gt;tuple&lt;/code&gt;，第三个参数是&lt;code&gt;value&lt;/code&gt;的构造函数参数包装成的&lt;code&gt;tuple&lt;/code&gt;。然后在构造&lt;code&gt;pair&lt;/code&gt;的时候把第一个&lt;code&gt;tuple&lt;/code&gt;的参数解包转发给&lt;code&gt;key&lt;/code&gt;的构造函数，第二个&lt;code&gt;tuple&lt;/code&gt;的参数解包转发给&lt;code&gt;value&lt;/code&gt;的构造函数。这样一来就可以解决多参或无参的问题，来原地构造。 &lt;strong&gt;所有&lt;code&gt;pair&lt;/code&gt;的情况，包括其他容器内存&lt;code&gt;pair&lt;/code&gt;的情况都可以用这个方法进行原地构造&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;构造元组的一般性建议&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;要创建返回值元组，尽可能使用构造函数。如果某些参数需要是引用类型，请显式使用 &lt;code&gt;cref/ref&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仅使用&lt;code&gt;std::tie&lt;/code&gt;将一组变量临时表示为元组：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;std::tie(it, inserted) = map.insert({x, y});  // tuple unpacking 更多解释查看cppreference
std::tie(x1, y1, z1) == std::tie(x2, y2, z2); // component-wise comparison
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;仅在传递参数时&lt;/strong&gt;使用&lt;code&gt;std::forward_as_tuple&lt;/code&gt;。不要将其返回值保存在任何地方。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;来自&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1156/&quot;&gt;本文的&lt;em&gt;Errors in object lifetime: tuples that shoot at your feet&lt;/em&gt;章节&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;小贴士：&lt;/h1&gt;
&lt;h2&gt;使用emplace_back的语法：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;temp;
temp.emplace_back(vector&amp;lt;int&amp;gt;{...,...});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在vector中判断特定元素是否存在的方法：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find&lt;/code&gt;会在查找到指定值后立即返回，所以它一般比&lt;code&gt;count&lt;/code&gt;更快（因为&lt;code&gt;count&lt;/code&gt;总是要遍历整个容器）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find_if&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;支持判别式（复杂查找）,返回第一个元素的迭代器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;查找元素个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;any_of&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;和&lt;code&gt;find_if&lt;/code&gt;类似，但是返回&lt;code&gt;bool&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binary_search&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;可以先排序再查找。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于已经排序的vector，使用&lt;code&gt;binary_search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;仅判断是否存在某元素，使用&lt;code&gt;find&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要某元素总个数时，使用&lt;code&gt;count&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;支持复杂条件的查找时，使用&lt;code&gt;any_of&lt;/code&gt;(仅知道是否存在)/&lt;code&gt;find_if&lt;/code&gt;(返回了第一个元素的迭代器)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用哈希表的容器，增删改查都是O(1)。使用红黑树的容器，增删改查都是O(nlogn)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;, &lt;code&gt;multimap&lt;/code&gt;, &lt;code&gt;multiset&lt;/code&gt; 上述四种容器采用红黑树实现，红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入: &lt;code&gt;O(logN)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看:&lt;code&gt;O(logN) &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除:&lt;code&gt;O(logN)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;unordered_map&lt;/code&gt;,&lt;code&gt;unordered_set&lt;/code&gt;,&lt;code&gt;unordered_multimap&lt;/code&gt;, &lt;code&gt;unordered_multiset&lt;/code&gt; 上述四种容器采用哈希表实现，不同操作的时间复杂度为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入:&lt;code&gt;O(1)&lt;/code&gt;，最坏情况&lt;code&gt;O(N)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;查看:&lt;code&gt;O(1)&lt;/code&gt;，最坏情况&lt;code&gt;O(N)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;删除:&lt;code&gt;O(1)&lt;/code&gt;，最坏情况&lt;code&gt;O(N)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;map和unordered map区别&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-EFFSTL.assets/QQ%E6%88%AA%E5%9B%BE20220824210104.png&quot; alt=&quot;QQ截图20220824210104&quot; /&gt;&lt;/p&gt;
&lt;p&gt;右下角那句话：要求结果有序（按&lt;code&gt;key&lt;/code&gt;排序）&lt;/p&gt;
&lt;h2&gt;关键点：&lt;/h2&gt;
&lt;h2&gt;&lt;strong&gt;指针容器的复制是浅拷贝。不能正确复制指针指向的内容。&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int*&amp;gt; v1 = ...;
vector&amp;lt;int*&amp;gt; v2 = v1;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;此时不会调用拷贝构造。因为和析构一样，容器调用的拷贝构造或拷贝赋值是帮助你调用。意思就是他会帮助你调用容器内储存的类型的对应的拷贝构造或拷贝赋值。所以说深拷贝浅拷贝完全依赖于你的拷贝构造和拷贝赋值的实现方式。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这也是为什么指针容器无法深拷贝。因为指针容器储存的是指针，任何类型的指针的类型都是指针。指针类型的拷贝构造和拷贝赋值都是浅拷贝，所以他无法调用到int类型的拷贝构造，只会调用指针类型的拷贝构造。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为此时假设&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v1[0] = 0x100;
*v1[0] = 0x999;
//我们复制
v2[0] = 0x200;
*v2[0] = 0x999;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vector可以复制每一个元素。就好比&lt;code&gt;int x = y&lt;/code&gt;一样。但是这个元素如果是指针的话，他不会进行深拷贝。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;指针给指针赋值仅仅是将一个指针指向的地址，复制给了另一个指针，导致两个指针指向一个地址。但是并没有生成新的对象。因为对象和对象指针不一样。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;指向对象的指针本质是指针。遵循指针的一般规则。&lt;/p&gt;
&lt;h3&gt;指针容器的double-free&lt;/h3&gt;
&lt;p&gt;上面我们提到了，&lt;strong&gt;指针容器的互相拷贝是浅拷贝。我们也知道了指针容器必须手动释放容器内每一个指针指向的对象&lt;/strong&gt;。（第七条）那么有一个问题，我们知道容器是值语义。容器会在传入元素的时候拷贝一份放入容器内。从里面取出的时候又会拷贝一份出来。这导致了一个非常严重的问题，尤其是针对指针容器而言。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int*&amp;gt;vec1;
int* b = new int(5);
vec1.push_back(b);
delete b;
delete vec1[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们这个指针&lt;code&gt;b&lt;/code&gt;在放入容器的一瞬间，会被拷贝一份。所以此时有两个指针指向了&lt;code&gt;new&lt;/code&gt;的部分。一个在容器外，一个在容器内。所以假设我们释放了&lt;code&gt;b&lt;/code&gt;指向的资源，则此时容器内的这个指针指向的资源也会被释放。如果我们此时害怕没有正确释放，再次释放了容器内指针指向的对象，会有double free错误。因为此时资源已经被释放。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以指针容器一定要慎重！既要防止没有释放，也要防止重复释放，因为容器是值语义！&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;再次重申，指针容器内的元素（指针）被移除或容器被析构的时候&lt;strong&gt;不会调用指向元素的析构函数&lt;/strong&gt;。也就是实际对象不会被销毁。因为容器**只是调用指针自己的析构函数而不是指针指向对象的析构函数。**然而指针类型的析构函数什么也不做。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;迭代器失效&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.cppreference.com/w/cpp/container#.E8.BF.AD.E4.BB.A3.E5.99.A8.E5.A4.B1.E6.95.88&quot;&gt;官方文档参考&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我总是把迭代器失效理解成错误的含义。我们简单讲一下真正的含义是什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;迭代器失效，不仅仅意味着报错，也意味着你会拿到不是你想要的数据。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;报错一般指的是由于插入元素，导致容器扩容而致使容器迁移导致指向原空间的迭代器失效。&lt;/li&gt;
&lt;li&gt;拿不到想要的数据一般指的是由于删除元素，导致元素移位致使迭代器不再指向期望的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; a = {1,2,3,4,5,6,7,8,9,10};
auto it = a.end();
cout &amp;lt;&amp;lt; *(it-1) &amp;lt;&amp;lt; endl; //输出10
cout &amp;lt;&amp;lt; a.size()&amp;lt;&amp;lt;endl; //输出10
cout &amp;lt;&amp;lt; a.capacity()&amp;lt;&amp;lt;endl; //输出10
a.insert(a.begin() + 3, 3333); //插入后扩容，数据转移至新区域，原来区域被释放
cout &amp;lt;&amp;lt; *(it-1) &amp;lt;&amp;lt; endl; //输出无效数据，迭代器失效。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们要知道。&lt;code&gt;begin()&lt;/code&gt; 和&lt;code&gt;end()&lt;/code&gt;每次都会动态的返回对应的迭代器，所以这俩不会失效。&lt;/p&gt;
&lt;p&gt;啥时候会失效呢？&lt;strong&gt;当有一个额外的手动赋值的迭代器出现的时候&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这句话是啥意思呢&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for(iter = a.begin();;)&lt;/code&gt; 这里你声明了一个新的&lt;code&gt;iter&lt;/code&gt;来接收这个调用&lt;code&gt;begin()&lt;/code&gt;返回的迭代器。这就是另一个迭代器&lt;/p&gt;
&lt;p&gt;或者如上图的：&lt;code&gt;auto it = a.end()&lt;/code&gt;。这里也是新的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以为什么在for循环里对迭代器操作的时候会失效。因为你操作的迭代器不是动态获取的，而是你已经获取到之后对迭代器进行加减操作的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;另外一个简单例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; a;
a.reserve(20);
for(int i = 0 ;i &amp;lt; 10; i++){
    a.push_back(i);
}
//这时候a是{0,1,2,3,4,5,6,7,8,9}
auto it = a.end();
cout &amp;lt;&amp;lt; *(it-1) &amp;lt;&amp;lt; endl; //输出9
a.insert(a.begin() + 3, 3333);
//这时候a是{0,1,2,3333,3,4,5,6,7,8,9}
cout &amp;lt;&amp;lt; *(it-1) &amp;lt;&amp;lt; endl; //输出8。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为迭代器已经被赋值。对迭代器的加减操作我们可以理解为原始数组对指针的加减操作。所以原来的时候，最后一位指向的内存地址存的是9，我们可以理解为&lt;code&gt;end-1&lt;/code&gt;是&lt;code&gt;begin+9&lt;/code&gt;。我们执行&lt;code&gt;insert&lt;/code&gt;之后，由于迭代器没有重新赋值，所以&lt;code&gt;begin+9&lt;/code&gt;指向的内存地址不变。但是&lt;code&gt;insert&lt;/code&gt;会让插入位置后的元素后移，8会被移到原来9的内存地址上。这样就是迭代器失效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下面我们看看下标访问的情况&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
    vector&amp;lt;int&amp;gt; myvec{ 0,1,2,3,4,5,6 };
    for (int i = 0; i &amp;lt; myvec.size(); i++) {
        if (myvec[i] == 3) {
            myvec.erase(myvec.begin() + i);
        }
        cout &amp;lt;&amp;lt; myvec[i] &amp;lt;&amp;lt; endl; //注意在erase后面
    }
    cout&amp;lt;&amp;lt;*(myvec.end()) &amp;lt;&amp;lt; endl; //这里是能看到最后一个6的。
//输出0 1 2 4 5 6

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里看上去好像皆大欢喜，但是我们要摸清楚一下原理&lt;/p&gt;
&lt;p&gt;这里是先把3删掉。erase是覆盖式赋值，把后面元素从删除的元素开始赋值。所以这时候变成了&lt;code&gt;0 1 2 4 5 6 6&lt;/code&gt;（虽然最后一个6不可见，但依旧可强制访问）。&lt;/p&gt;
&lt;p&gt;所以这时候index依旧为3，但是此时index = 3的值变成了4，又因为&lt;code&gt;erase&lt;/code&gt;会改变&lt;code&gt;size&lt;/code&gt;（也就是改变end迭代器的位置）所以会正常输入&lt;code&gt;0 1 2 4 5 6&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是一旦我们把打印放在erase前面，那就出问题了&lt;/strong&gt;。会输出&lt;code&gt;0 1 2 3 5 6&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;为什么呢？显而易见，打印3的原因是3此时还没有删除，但是4怎么没了呢？&lt;/p&gt;
&lt;p&gt;因为我们删掉3之后，是 &lt;code&gt;0 1 2 4 5 6&lt;/code&gt;。但是这时候下标已经从3移动到了4。所以打印下标为4的元素自然是5。&lt;/p&gt;
&lt;h2&gt;再次强调序列式容器的erase是覆盖式删除&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;erase&lt;/code&gt;是覆盖式删除，也就是把要删除数据位置后面的数据依次拷贝至要删除的数据位置和后面的位置上，并且调用脏数据的析构，但是调用析构函数不代表数据会被真正意义上的删除或内存释放。比如指针容器。指针容器只会调用指针类型自己的析构而不会调用指针指向对象的析构。内存释放依靠的是&lt;code&gt;delete&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;erase&lt;/code&gt;调整的是&lt;code&gt;size&lt;/code&gt;，就是调整&lt;code&gt;end&lt;/code&gt;迭代器位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
    int val;
    myclass(int x):val(x){};
    ~myclass(){
        val = 8964;
    }
};
int main() {
    vector&amp;lt;myclass&amp;gt; myvec{0,1,2,3,4,5,6};
    myvec.erase(myvec.begin());
    myvec.erase(myvec.begin());
    myvec.erase(myvec.begin());
    myvec.erase(myvec.begin());
    for(auto i:myvec){
        cout &amp;lt;&amp;lt; i.val &amp;lt;&amp;lt; endl; //输出4 5 6
    }
    cout &amp;lt;&amp;lt; (*myvec.end()).val &amp;lt;&amp;lt; endl; 	//输出8964
    cout &amp;lt;&amp;lt; (*(myvec.end()+1)).val &amp;lt;&amp;lt; endl;	//输出8964
    cout &amp;lt;&amp;lt; (*(myvec.end()+2)).val &amp;lt;&amp;lt; endl;	//输出8964
    cout &amp;lt;&amp;lt; (*(myvec.end()+3)).val &amp;lt;&amp;lt; endl;	//输出8964
    cout &amp;lt;&amp;lt; (*(myvec.end()+4)).val &amp;lt;&amp;lt; endl; //已经越界。所以是非法数据。
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们分析一下过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 1 2 3 4 5 6 覆盖式删除
1 2 3 4 5 6 6 删除后，调用最末尾元素的析构。注意我们的析构，只是进行一个值的覆盖。因为是栈对象，又没有delete所以依旧能访问到元素
1 2 3 4 5 6 8964 调用析构后。

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上这一部分是调用了一次erase的情况。如果我们扩展下就是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 2 3 4 5 6 8964
2 3 4 5 6 8964 8964
3 4 5 6 8964 8964 8964
4 5 6 8964 8964 8964 8964
		↑end 在这
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个期望很符合我们上面代码的注释部分。&lt;/p&gt;
&lt;h2&gt;看一下vector在erase的情况下的迭代器失效到底是什么意思&lt;/h2&gt;
&lt;p&gt;先看代码，这是for循环删除的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
    int val;
    myclass(int x):val(x){};
    ~myclass(){
        val = 8964;
    }
};
int main() {
    vector&amp;lt;myclass&amp;gt; myvec{ 0,1,2,3,4,5,6 };
    //for(auto iter = myvec.begin(); iter != myvec.end(); iter++){ //注意这一行
    for(auto iter = myvec.begin(); iter &amp;lt; myvec.end(); iter++){
        myvec.erase(iter);
    }
    for(auto i:myvec){
        cout &amp;lt;&amp;lt; i.val &amp;lt;&amp;lt; endl; //输出 1 3 5
    }
    return 0;

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意！随机访问迭代器支持&amp;gt;和&amp;lt;比较！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 1 2 3 4 5 6 开始
↑iter
1 2 3 4 5 6 8964 删除后
↑iter

1 2 3 4 5 6 8964 第二次for循环，iter++
  ↑iter
1 3 4 5 6 8964 8964 删除后
  ↑iter

1 3 4 5 6 8964 8964  第三次for循环，iter++
	↑iter
1 3 5 6 8964 8964 8964 删除后

1 3 5 6 8964 8964  第三次for循环，iter++
	  ↑iter
1 3 5 8964 8964 8964 8964 删除后
		↑end 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们已经通过上面的删除样例了解了，删除后数据会被覆盖过来，然后再进行&lt;code&gt;iter++&lt;/code&gt;就会跨越一个数据。就会出现迭代器失效。&lt;/p&gt;
&lt;p&gt;假如我们用的不是大小比较，用的是&lt;code&gt;end&lt;/code&gt;比较，那么会出现大问题。for循环一直到天荒地老合法内存的尽头。&lt;/p&gt;
&lt;p&gt;因为我们在最后一次删除的时候，由于是先删除的，所以&lt;code&gt;end&lt;/code&gt;会往前缩一格，会缩到第四位也就是当前iter的位置。然后&lt;code&gt;iter++&lt;/code&gt;移动到第五位。这时候进入&lt;code&gt;for&lt;/code&gt;循环判断&lt;code&gt;++&lt;/code&gt;后的&lt;code&gt;iter&lt;/code&gt;，这时候发现&lt;code&gt; iter&lt;/code&gt;在第五位，&lt;code&gt;end&lt;/code&gt;在第四位，当然不相等。于是就会一直走下去。&lt;/p&gt;
&lt;h2&gt;各种容器迭代器失效场景&lt;/h2&gt;
&lt;h3&gt;vector&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;重新分配内存时（超出capacity大小时），&lt;strong&gt;所有&lt;/strong&gt;都失效。&lt;/li&gt;
&lt;li&gt;在当前指向元素之前进行&lt;strong&gt;插入&lt;/strong&gt;操作，当前迭代器&lt;strong&gt;以后的&lt;/strong&gt;迭代器都会失效。&lt;/li&gt;
&lt;li&gt;在&lt;strong&gt;删除&lt;/strong&gt;元素时，指向被删除元素&lt;strong&gt;以后的&lt;/strong&gt;迭代器都失效&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;deque&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;插入&lt;/strong&gt;元素使deque的&lt;strong&gt;所有&lt;/strong&gt;迭代器失效。（因为可能中控器个数不够，需要开辟更大空间容纳中控器）&lt;/li&gt;
&lt;li&gt;在deque的&lt;strong&gt;中间删除&lt;/strong&gt;元素将使&lt;strong&gt;所有&lt;/strong&gt;迭代器失效。（涉及到缩减缓冲区数量）&lt;/li&gt;
&lt;li&gt;在deque的&lt;strong&gt;头或尾删除&lt;/strong&gt;元素时，&lt;strong&gt;只有指向该元素的&lt;/strong&gt;迭代器失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;queue, stack, priority_queue 这三个容器适配器没有迭代器&lt;/h3&gt;
&lt;h3&gt;list, map, set，unordered_map, unordered_set&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增加&lt;/strong&gt;任何元素都&lt;strong&gt;不会&lt;/strong&gt;使迭代器失效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除&lt;/strong&gt;元素时，除了指向&lt;strong&gt;当前被删除元素的迭代器失效&lt;/strong&gt;外，其它迭代器都不会失效。&lt;/li&gt;
&lt;li&gt;**格外注意在关联式容器中当进行&lt;code&gt;my_set.erase(iter)&lt;/code&gt;后，&lt;code&gt;iter&lt;/code&gt;已经失效。任何针对&lt;code&gt;iter&lt;/code&gt;的操作如自增或自减操作都是失效的。所以要么使用&lt;code&gt;erase&lt;/code&gt;函数返回的新迭代器，要么使用&lt;code&gt;my_set.erase(iter++)&lt;/code&gt;**具体解析在下面。&lt;/li&gt;
&lt;li&gt;针对哈希容器如unordered系列：
&lt;ul&gt;
&lt;li&gt;若插入&lt;strong&gt;引起rehash，则所有迭代器失效&lt;/strong&gt;。若不rehash，依然有效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;迭代器失效的自增操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;student target(2,&quot;luka&quot;); 
auto t = my_set.find(target); 
if(t != my_set.end()){ 
    student temp(*t); 
    temp.change_name(&quot;changed_luka&quot;); 
    my_set.erase(t++); //注意这里
    my_set.insert(t, temp); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**格外注意在关联式容器中当进行&lt;code&gt;my_set.erase(iter)&lt;/code&gt;后，&lt;code&gt;iter&lt;/code&gt;已经失效。任何针对&lt;code&gt;iter&lt;/code&gt;的操作如自增或自减操作都是失效的。**所以我们要么使用&lt;code&gt;erase&lt;/code&gt;函数传回的新迭代器，要么使用&lt;code&gt;my_set.erase(iter++)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意这里发生了几个步骤。首先，&lt;code&gt;t++&lt;/code&gt;会被先运算。然后我们知道&lt;code&gt;t++&lt;/code&gt;是先赋值后自增。所以此时传入&lt;code&gt;erase&lt;/code&gt;的迭代器指向的是我们要删除的那一个。也就是&lt;code&gt;t&lt;/code&gt;。但是&lt;code&gt;erase&lt;/code&gt;会在&lt;code&gt;t&lt;/code&gt;完成自增后再进行删除操作。也就是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先把&lt;code&gt;t&lt;/code&gt;传入&lt;code&gt;erase&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行&lt;code&gt;t++&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;因为上面两步合起来才是&lt;code&gt;t++&lt;/code&gt;的运算（参考杂记2）。所以在这之后才开始进行删除操作，处理传入的迭代器。也就是&lt;code&gt;erase&lt;/code&gt;原来的&lt;code&gt;t&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;操作等同于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;student target(2,&quot;luka&quot;); 
auto t = my_set.find(target); 
if(t != my_set.end()){ 
    student temp(*t); 
    temp.change_name(&quot;changed_luka&quot;);
    auto t2 = t; //拿到一个新的迭代器
    t2++; //新的迭代器自增，注意不可t++或者++t。这都是对t操作，我们要对t2操作
    my_set.erase(t); //删掉t指向的内容
    t = t2; //把t2赋值回t。
    my_set.insert(t, temp); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://stackoverflow.com/questions/41959511/erase-set-iterator-value-and-increment-iterator&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;或者使用&lt;code&gt;erase&lt;/code&gt;返回的新迭代器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;student target(2,&quot;luka&quot;); 
auto t = my_set.find(target); 
if(t != my_set.end()){ 
    student temp(*t); 
    temp.change_name(&quot;changed_luka&quot;); 
    t = my_set.erase(t); //注意这里
    my_set.insert(t, temp); 
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C++杂记 - 2（三）：const 重载、函数签名与 final</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B02-3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B02-3/</guid><pubDate>Wed, 20 Jul 2022 06:57:00 GMT</pubDate><content:encoded>&lt;h1&gt;关于&lt;code&gt;const&lt;/code&gt;函数重载。修饰（成员）函数的&lt;code&gt;const&lt;/code&gt;算作函数签名的一部分&lt;/h1&gt;
&lt;h2&gt;&lt;code&gt;const&lt;/code&gt;修饰函数参数&lt;strong&gt;一般&lt;/strong&gt;不作为函数签名的一部分。除非修饰的参数是指针或引用。并且需要是修饰的是底层。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt;修饰的函数参数是值的时候不可以进行重载。如下代码编译错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public: 
        myobj(int x):cal(x){}
        int cal;
};
void func1(myobj a){
        cout &amp;lt;&amp;lt; &quot;non const&quot; &amp;lt;&amp;lt; endl;
        }
void func1(const myobj  a){
    cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;const&lt;/code&gt;修饰的函数参数是指针或引用的时候，并且修饰的是底层的时候可以进行重载。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;volatile&lt;/code&gt;也适用于此规则。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以上例外&lt;code&gt;const&lt;/code&gt;必须修饰的是指针指向的内容， 即ponter-to-const;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是指针本身是&lt;code&gt;const&lt;/code&gt;的，则不能重载&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原因是函数签名&lt;strong&gt;不包括&lt;/strong&gt;形参列表参数的&lt;strong&gt;顶层&lt;/strong&gt;CV限定&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;顶层&lt;code&gt;const&lt;/code&gt;表示指针本身是个常量。顶层修饰的是对象本身。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;myobj* const b = new myobj(20);&lt;/code&gt; &lt;code&gt;const&lt;/code&gt;指针&lt;code&gt;b&lt;/code&gt;指向&lt;code&gt;myobj&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;底层&lt;code&gt;const&lt;/code&gt;表示指针所指向的对象是个常量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const myobj* b = new myobj(20);&lt;/code&gt;指针&lt;code&gt;b&lt;/code&gt;指向&lt;code&gt;const myobj&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//指针
void func(myobj* a){
        cout &amp;lt;&amp;lt; &quot;non const&quot; &amp;lt;&amp;lt; endl;
        }
void func(const myobj* a){
    cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
}
//引用
void func1(myobj&amp;amp; a){
        cout &amp;lt;&amp;lt; &quot;non const&quot; &amp;lt;&amp;lt; endl;
        }
void func1(const myobj&amp;amp; a){
    cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
}


int main(){
    myobj* a = new myobj(10);
    const myobj* b = new myobj(20);
    func(a); //输出non const
    func(b); //输出const


    myobj obj(10);
    myobj&amp;amp; refa = obj;
    const myobj&amp;amp; refb = obj;

    func1(refa); //输出 non const
    func1(refb); //输出 const


    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;const引用可以绑定到非const对象，但是非const引用不可绑定const对象。因为不能保证其不被修改。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;string t = &quot;234&quot;; //非const对象
const string&amp;amp; y = t; //可以 const引用

const string s = &quot;234&quot;; //const对象
string&amp;amp; b = s; //不可以 非const引用

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以函数入参为引用的时候，非const对象可以传入const引用参数。也就是const引用绑定到非const对象。比如作用是防止修改。但是const对象不可传入非const引用参数。也就是非const引用不可以绑定到const对象。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void test(const string&amp;amp; a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
}

void test2(string&amp;amp; a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
}
int main(){
    const string str1 = &quot;234&quot;;
    string str2 = &quot;234&quot;;
    test(str1);
    test2(str1); //不可以。const对象不能传入非const参数
    test(str2);
    test2(str2);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;值传递不受影响&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;const如果放在函数后面（修饰成员函数）只能放在类成员函数后面&lt;/h1&gt;
&lt;p&gt;先看代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
public:
    int vala;
    int valb;
    myclass(int x, int y):vala(x), valb(y){}

    void changethis(myclass&amp;amp; obj)const {
        cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
    }
    // void changethis(const myclass* const this, myclass&amp;amp; obj) const{ 
    //     cout &amp;lt;&amp;lt;&quot;non const&quot; &amp;lt;&amp;lt; endl;
    // }
    void changethis(myclass&amp;amp; obj) {
        cout &amp;lt;&amp;lt;&quot;non const&quot; &amp;lt;&amp;lt; endl;
    }
    // void changethis(myclass* const this, myclass&amp;amp; obj) {
    //     cout &amp;lt;&amp;lt;&quot;non const&quot; &amp;lt;&amp;lt; endl;
    // }
};

int main(){
    myclass obj(10,20);
    const myclass obj1(10,20);
    obj.changethis(obj); //调用不带const的
    obj1.changethis(obj); //调用带const的
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt;放在类成员函数后面的意义是，&lt;strong&gt;不可以修改当前调用对象的数据成员的值&lt;/strong&gt;。切记注意，入参是否可以被修改是不归函数管的。函数加&lt;code&gt;const&lt;/code&gt;只是负责&lt;strong&gt;禁止修改调用方&lt;/strong&gt;。在这里也就是禁止修改调用方&lt;code&gt;obj1&lt;/code&gt;的值。&lt;/p&gt;
&lt;h2&gt;const修饰成员函数修饰的是什么？是this指针&lt;/h2&gt;
&lt;p&gt;我们在上面代码段看到了两段被注释掉的函数，里面写出了如果显式&lt;code&gt;this&lt;/code&gt;指针会是什么样。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意，由于&lt;code&gt;this&lt;/code&gt;指针不能改变指向，所以&lt;code&gt;this&lt;/code&gt;指针是一个常量指针。也就是&lt;code&gt;myclass* const this&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果我们不想修改一个参数的值，办法就是让这个参数被&lt;code&gt;const&lt;/code&gt;修饰。在这里就是把this指针也用&lt;code&gt;const&lt;/code&gt;修饰。则会变成&lt;code&gt;const myclass* const this&lt;/code&gt;。这也说明了为什么&lt;code&gt;const&lt;/code&gt;只能修饰成员函数，因为非成员函数没有this指针。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;又由于&lt;code&gt;this&lt;/code&gt;指针会被编译器根据对象来隐式传入。所以&lt;code&gt;const&lt;/code&gt;对象的&lt;code&gt;this&lt;/code&gt;指针必然是也是常量指针。所以自然&lt;code&gt;const&lt;/code&gt;对象无法调用非&lt;code&gt;const&lt;/code&gt;函数。因为&lt;code&gt;this&lt;/code&gt;指针的不匹配。&lt;code&gt;const&lt;/code&gt;可以被隐式（函数调用）加上但是不能被去除。也就是权限的缩紧是可以的，放松是不可以的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;简单理解就是因为&lt;code&gt;const&lt;/code&gt;成员无法进行修改。如果&lt;code&gt;const&lt;/code&gt;成员变量调用了非&lt;code&gt;const&lt;/code&gt;函数，函数不能保证不修改&lt;code&gt;const&lt;/code&gt;成员，如果修改了会报错，所以直接加以限制。当然了非&lt;code&gt;const&lt;/code&gt;成员因为可以修改也可以不修改，都不会报错。所以自然可以调用&lt;code&gt;const&lt;/code&gt;函数也可以调用非const函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt;成员函数不能调用非&lt;code&gt;const&lt;/code&gt;成员函数。因为在&lt;code&gt;const&lt;/code&gt;成员函数里面的&lt;code&gt;this&lt;/code&gt;指针被&lt;code&gt;const&lt;/code&gt;修饰，在&lt;code&gt;const&lt;/code&gt;成员函数里面调用非&lt;code&gt;const&lt;/code&gt;成员函数时，相当于将&lt;code&gt;const&lt;/code&gt;修饰的&lt;code&gt;this&lt;/code&gt;指针传给非&lt;code&gt;const&lt;/code&gt;成员函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也可以理解为权限的扩张是禁止的。&lt;strong&gt;不可以修改（整体）-&amp;gt; 可以修改（局部）是不可以的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非&lt;code&gt;const&lt;/code&gt;成员函数可以调用&lt;code&gt;const&lt;/code&gt;成员函数的原因是：权限的缩紧是可以的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是：&lt;strong&gt;可以修改（整体）-&amp;gt;不可以修改（局部）这样是可以的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;无意间发生的const与非const冲突&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
public:
    int vala;
    int valb;
    myclass(int x, int y):vala(x), valb(y){}
    void func(){
		//一些内容
    }
    void changethis(const myclass&amp;amp; obj){
        obj.func(); //错误。const对象（引用）不能调用非const函数
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们看到了。经常性的我们会使用常量左值引用做为函数入参签名，一是防止拷贝二是防止修改。但是我们可能需要通过这个对象调用其内部的一些函数。此时就不可以通过&lt;code&gt;const&lt;/code&gt;引用调用非&lt;code&gt;const&lt;/code&gt;函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;注意 this指针并不是对象本身的一部分。&lt;/h2&gt;
&lt;p&gt;注意 &lt;code&gt;this&lt;/code&gt;指针并不是对象本身的一部分。也不会影响&lt;code&gt;sizeof&lt;/code&gt;的结果。它的作用域是在类的内部。&lt;code&gt;this&lt;/code&gt;指针只不过会在调用非静态函数的时候被编译器隐式加入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-%E6%9D%82%E8%AE%B02.assets/unknown.png&quot; alt=&quot;unknown&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关于const函数重载。&lt;code&gt;const&lt;/code&gt;算作函数签名的一部分&lt;/h2&gt;
&lt;p&gt;我们在上面代码可以看到，我们有一个函数重载。因为&lt;code&gt;const&lt;/code&gt;算作函数签名的一部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是编译器如何判断我们调用的是哪个函数呢？虽然表面参数一样，但其实隐藏的&lt;code&gt;this&lt;/code&gt;形参是不同的。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以如果我们调用方是&lt;code&gt;const&lt;/code&gt;对象，则会调用&lt;code&gt;const&lt;/code&gt;版本的函数。如果是非&lt;code&gt;const&lt;/code&gt;对象就调用非&lt;code&gt;const&lt;/code&gt;版本的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-%E6%9D%82%E8%AE%B02.assets/unknown%20(1).png&quot; alt=&quot;unknown (1)&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;为什么返回值类型不能做为函数重载的依据？也就是为什么仅返回值不同的函数不可重载？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;理论上来说就是函数的返回值不是&lt;strong&gt;普通&lt;/strong&gt;函数签名的一部分。&lt;/li&gt;
&lt;li&gt;实际上来说是有二义性。比如&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int func();
string func();

int main(){
    func(); //调用哪个func?
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上面代码，调用&lt;code&gt;func&lt;/code&gt;的时候编译器无法确定到底调用哪个&lt;code&gt;func&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;虽然返回值不是普通函数签名一部分，但是在链接期，匹配的不仅是函数的签名，还包括其类型。所以返回值依旧要匹配（个人理解）&lt;/p&gt;
&lt;h1&gt;函数签名总结&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;普通函数的函数签名包括：
&lt;ul&gt;
&lt;li&gt;函数名（非限定名）或者 生成该函数的函数模板名称&lt;/li&gt;
&lt;li&gt;参数类型和个数（参数列表）。&lt;strong&gt;如果函数是从函数模板中生成的，那么指的是替换前的模板参数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;函数名称所属的类或命名空间作用域；如果函数名称拥有内部链接，还包括该名称声明所在的编译单元。&lt;/li&gt;
&lt;li&gt;如果是类成员函数则还包括函数的：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt;限定符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volatile&lt;/code&gt;限定符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref&lt;/code&gt;限定符&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数模板的签名除了上面的以外还包括：
&lt;ul&gt;
&lt;li&gt;返回值类型&lt;/li&gt;
&lt;li&gt;模板参数和模板实参&lt;/li&gt;
&lt;li&gt;格外注意，函数模板的签名包括返回值类型导致的表面说“函数模板的重载“其实是一种重载决议。被不同类型实例化的函数模板是不同的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数参数的const一般不算做函数签名。除非该参数为引用或指针
&lt;ul&gt;
&lt;li&gt;普通参数的const不算做函数签名是为了兼容C。&lt;/li&gt;
&lt;li&gt;C语言并不支持函数重载，因此编译C语言代码的函数时不会带上函数的参数类型，一般只包括函数名。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;普通函数的函数签名不包括返回值类型。也就是无法仅通过返回值类型的不同进行函数重载&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;关键字 final&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;final关键字的意义是&lt;strong&gt;阻止子类重写自身（父类）的虚函数或禁止继承&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;一般不用在父类，因为没有意义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一般用在某个子类防止子类的子类重写子类的虚函数 或者是 禁止继承。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>智能指针（二）：使用总结、侵入式智能指针与设计</title><link>https://sterben-01.github.io/posts/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88-2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88-2/</guid><pubDate>Wed, 20 Jul 2022 06:56:00 GMT</pubDate><content:encoded>&lt;h1&gt;常用部分总结&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;比较&lt;/th&gt;
&lt;th&gt;shared_ptr&lt;/th&gt;
&lt;th&gt;unique_ptr&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;初始化&lt;/td&gt;
&lt;td&gt;①&lt;code&gt;shared_ptr&amp;lt;T&amp;gt; sp;&lt;/code&gt;&amp;lt;br /&amp;gt;②&lt;code&gt; sp.reset(new T());&lt;/code&gt;&amp;lt;br /&amp;gt;②&lt;code&gt;shared_ptr&amp;lt;T&amp;gt; sp(new T());&lt;/code&gt;&amp;lt;br /&amp;gt;③&lt;code&gt;shared_ptr&amp;lt;T&amp;gt; sp1 = sp; &lt;/code&gt;&lt;strong&gt;//拷贝构造&lt;/strong&gt;&amp;lt;br /&amp;gt;④&lt;code&gt;auto sp = make_shared&amp;lt;int&amp;gt;(10);&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;①&lt;code&gt;unique_ptr&amp;lt;T&amp;gt; up;&lt;/code&gt; &amp;lt;br /&amp;gt;&lt;code&gt;up.reset(new T());&lt;/code&gt;&amp;lt;br /&amp;gt;②&lt;code&gt;unique_ptr&amp;lt;T&amp;gt; up(new T());&lt;/code&gt;&amp;lt;br /&amp;gt;③&lt;code&gt;unique_ptr&amp;lt;T&amp;gt; up1 = std::move(up);&lt;/code&gt;&lt;strong&gt;//移动构造&lt;/strong&gt;&amp;lt;br /&amp;gt;④&lt;code&gt;auto up = make_unique&amp;lt;int&amp;gt;(10);&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;两者的构造函数将声明为**explicit，即不允许隐式类型转换，**如&lt;code&gt;shared_ptr&amp;lt;int&amp;gt; sp = new int(10);&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;条件判断&lt;/td&gt;
&lt;td&gt;如，&lt;code&gt;if(sp){…}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如，&lt;code&gt;if(up){…}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;两都均&lt;strong&gt;重载&lt;code&gt;operator bool()&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;解引用(间接寻址运算符 &lt;code&gt;*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*sp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;解引用，获得它所指向的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;类成员访问运算符（&lt;code&gt;-&amp;gt;&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sp-&amp;gt;mem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;up-&amp;gt;mem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重载&lt;code&gt;-&amp;gt;&lt;/code&gt;运算符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get()&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sp.get()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;up.get()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回智能指针中保存的裸指针，要小心使用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p.swap(q)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sp.swap(q);&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;up.swap(q);&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;交换p和q指针&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;独有操作&lt;/td&gt;
&lt;td&gt;①&lt;code&gt;shared_ptr&amp;lt;T&amp;gt; p(q);&lt;/code&gt;//拷贝构造&amp;lt;br /&amp;gt;②&lt;code&gt;p = q;&lt;/code&gt;//赋值&amp;lt;br /&amp;gt;③&lt;code&gt;p.unique();&lt;/code&gt;若&lt;code&gt;p.use_count()&lt;/code&gt;为1，返回&lt;code&gt;true&lt;/code&gt;,否则返回&lt;code&gt;false&lt;/code&gt;。④&lt;code&gt;p.use_count()&lt;/code&gt;//返回强引用计数&lt;/td&gt;
&lt;td&gt;①&lt;code&gt;up=nullptr;&lt;/code&gt;释放up指向的对象，并将up置空。&amp;lt;br /&amp;gt;②&lt;code&gt;up.release();&lt;/code&gt;//up&lt;strong&gt;放弃对指针的控制权，返回裸指针，并将up置空&lt;/strong&gt;&amp;lt;br /&amp;gt;③&lt;code&gt;up.reset();&lt;/code&gt;释放up指向的对象。 &lt;code&gt;up.reset(q);&lt;/code&gt;其中q为裸指针。令up指向q所指对象。&amp;lt;br /&amp;gt;&lt;code&gt; up.reset(nullptr);&lt;/code&gt;置空&lt;/td&gt;
&lt;td&gt;注意:&amp;lt;br /&amp;gt;①&lt;code&gt;unique_ptr&lt;/code&gt;&lt;strong&gt;不可拷贝和赋值，但可以被移动&lt;/strong&gt;。&amp;lt;br /&amp;gt;②&lt;code&gt;release&lt;/code&gt;会切断&lt;code&gt;unique_ptr&lt;/code&gt;和它原来管理的对象间的联系。&lt;strong&gt;通常用来初始化另一个智能指针&lt;/strong&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;强烈建议以对待普通指针的方式对待智能指针。&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;如按值传递，&lt;strong&gt;按值返回&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;按值传递&lt;code&gt;unique_ptr&lt;/code&gt;显示了所有权的转移。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;关于侵入式和非侵入式智能指针。&lt;/h1&gt;
&lt;h2&gt;什么是非侵入式智能指针&lt;/h2&gt;
&lt;p&gt;STL的&lt;code&gt;shared_ptr&lt;/code&gt;属于非侵入式智能指针。从原理上来说就是&lt;strong&gt;它的实现完全放在智能指针模板里&lt;/strong&gt;。也就是控制块是和指针在一个类里。&lt;/p&gt;
&lt;p&gt;模板类有一个用于保存资源类对象的指针变量，和一个用于记录资源对象使用计数（控制块）的指针变量。&lt;/p&gt;
&lt;h2&gt;什么是侵入式智能指针&lt;/h2&gt;
&lt;p&gt;ITK, VTK和boost里的智能指针是侵入式的。从原理来说就是**它的实现分散在智能指针模板和使用智能指针模板的类中：模板类只有一个用于保存对象的指针变量，对象的计数放在了资源类中。**举例来说就是把控制块比如引用计数和对象绑定到一起。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/QQ%E6%88%AA%E5%9B%BE20221118211750.png&quot; alt=&quot;QQ截图20221118211750&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;非侵入式智能指针，它的引用计数变量为了保证所有对象共享，需要用堆里的内存，所以需要用new，这个都一样，不一样的是使用new的次数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;侵入式智能指针的引用计数变量保存在对象里，因为对象是唯一的，所以引用计数也是唯一的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非侵入式智能指针的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在不使用&lt;code&gt;make_shared&lt;/code&gt;的时候，会为控制块单独分配一次内存。这导致性能问题。而如果使用&lt;code&gt;make_shared&lt;/code&gt;，则在某些时候会导致整块内存都无法释放，比如弱引用计数仍不为0的情况下。&lt;/li&gt;
&lt;li&gt;引用计数的内存区域和数据区域不一致，缓存失效导致性能问题。&lt;/li&gt;
&lt;li&gt;编写代码不善，将导致同一个数据，绑定到了两个引用计数，从而导致双重删除问题。比如&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;obj *resource = new obj(100);
shared_ptr&amp;lt;obj&amp;gt; ptr1(resource); //ptr1有控制块
shared_ptr&amp;lt;obj&amp;gt; ptr2(resource); //ptr2还有一个控制块。double free
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相比非侵入式智能指针，侵入式智能指针的好处是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个资源对象无论被多少个侵入式智能指针包含，从始至终只有一个引用计数变量，不需要在每一个使用智能指针对象的地方都new一个计数对象，这样子效率比较高，使用内存也比较少，也比较安全；
&lt;ul&gt;
&lt;li&gt;这句话理解就是如果在使用非侵入的时候我们有一个原始指针指向资源，我们使用这个原始指针初始化共享指针的时候会创建多个控制块。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;因为引用计数存储在对象本身，所以在函数调用的时候可以直接传递资源对象地址，而不用担心引用计数值丢失（非侵入式智能指针对象的拷贝，必须带着智能指针模板，否则就会出现对象引用计数丢失）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源类必须有引用计数变量，并且该变量的增减可以被侵入式智能指针模板基类操作，这显得麻烦。也就是资源类必须继承自某个引用计数变量的基类或者是编写的时候将其编写进去。b
&lt;ul&gt;
&lt;li&gt;比如在&lt;code&gt;boost&lt;/code&gt;库中，需要继承&lt;code&gt;boost::intrusive_ptr_base&lt;/code&gt;基类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果该类并不想使用智能指针，它还是会带着引用计数变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能的实现方法：&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/460983966&lt;/p&gt;
&lt;p&gt;https://zhiqiang.org/coding/boost-intrusive-ptr.html&lt;/p&gt;
&lt;h1&gt;可能有用的参考资料&lt;/h1&gt;
&lt;p&gt;https://developer.aliyun.com/article/582674&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/5iedu/p/11622401.html&lt;/p&gt;
&lt;p&gt;https://kingsamchen.github.io/2018/03/16/demystify-shared-ptr-and-weak-ptr-in-msvc-stl/ sharedptr源码分析 包含enable_shared_from_this&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/lgp88/article/details/7529254 不完整类型&lt;/p&gt;
&lt;h1&gt;如果自己设计的话，如何让原始指针为形参的函数接受智能指针为实参？&lt;/h1&gt;
&lt;p&gt;可以使用转换构造函数。如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;operator T*() const{
    //return static_cast&amp;lt;T*&amp;gt;(this-&amp;gt;object); 这行是VTK源码但不知道为何要这么做。
    return this-&amp;gt;object;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考&lt;code&gt;vtkNew&lt;/code&gt;和&lt;code&gt;vtkSmartpointer&lt;/code&gt;。VTK8以前的&lt;code&gt;vtkNew&lt;/code&gt;有意禁用了自动转换。但是8以后新增了转换构造所以可以进行转换。转换构造参见杂记2。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，这样做非常危险。因为你现在可以&lt;code&gt;delete&lt;/code&gt;一个智能指针了。&lt;code&gt;delete&lt;/code&gt;会调用这个类型转换函数。所以这样设计的危险性就在于这里。-----Modern C++ design 7.7 和 more effective 条款28&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;为何智能指针支持 和继承有关的类型转换？ more effective c++ -条款28（部分）&lt;/h1&gt;
&lt;p&gt;回顾我们曾经实现过的&lt;code&gt;uptr&lt;/code&gt;，代码不在这里贴了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先我们知道，原始指针中，指向完整派生类类型的（可有 cv 限定的）空指针可转换成指向它的（有相同 cv 限定的）基类的指针。如果基类不可访问或有歧义，那么转换非良构（不能编译）。转换结果是指向原被指向对象内的基类子对象的指针。空指针值转换成目标类型的空指针值。&lt;strong&gt;也就是子类类型指针可以天然自然地转换为父类类型指针。（这是隐式类型转换中的第五级的指针转换。）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设我们有如下场景&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct b{

};

struct d:b{

};

void func(area2::unique_ptr&amp;lt;b&amp;gt; ptr){
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}
void formalfunc(unique_ptr&amp;lt;b&amp;gt; ptr){
    cout &amp;lt;&amp;lt;&quot;formal&quot; &amp;lt;&amp;lt; endl;
}
int main(){
    area2::unique_ptr&amp;lt;b&amp;gt; ptr(new b());
    func(move(ptr)); //可以。因为此时类型是b
    area2::unique_ptr&amp;lt;b&amp;gt; ptr2(new d());
    func(move(ptr2)); //可以。因为此时类型是b

    area2::unique_ptr&amp;lt;d&amp;gt; ptr2(new d());
    func(move(ptr2)); //不可以。因为此时类型是d

    unique_ptr&amp;lt;d&amp;gt; uptr(new d()); //针对标准库的智能指针，这样可以，因为有隐式类型转换。
    formalfunc(move(uptr)); //可以。有隐式类型转换。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们发现我们实现的简易版和标准库的相比，我们无法做到和继承有关的类型转换。原因非常明显。就算&lt;code&gt;b&lt;/code&gt;和&lt;code&gt;d&lt;/code&gt;有继承关系，但是&lt;code&gt;unique_ptr&amp;lt;b&amp;gt;&lt;/code&gt;和&lt;code&gt;unique_ptr&amp;lt;d&amp;gt;&lt;/code&gt;毫无关联。但是为什么标准库的智能指针可以让我们把一个子类类型的智能指针传入（赋值）给接受基类类型的智能指针的函数中呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;标准库的做法我们从文档中可以窥见一斑：&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class U, class E &amp;gt;
unique_ptr( unique_ptr&amp;lt;U, E&amp;gt;&amp;amp;&amp;amp; u ) noexcept; //(6)	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过从 &lt;code&gt;u&lt;/code&gt; 转移所有权给 &lt;code&gt;*this&lt;/code&gt; 构造 &lt;code&gt;unique_ptr&lt;/code&gt; ，其中 &lt;code&gt;u&lt;/code&gt; 以指定的删除器（ &lt;code&gt;E&lt;/code&gt; ）构造。它依赖于 &lt;code&gt;E&lt;/code&gt; 是否是引用类型，如下：&lt;/p&gt;
&lt;p&gt;a) 若 &lt;code&gt;E&lt;/code&gt; 是引用类型，则从 &lt;code&gt;u&lt;/code&gt; 的删除器复制构造此删除器（要求此构造不抛出）&lt;/p&gt;
&lt;p&gt;b) 若 &lt;code&gt;E&lt;/code&gt; 不是引用类型，则从 &lt;code&gt;u&lt;/code&gt; 的删除器移动构造此删除器（要求此构造不抛出）&lt;/p&gt;
&lt;p&gt;此构造函数仅若下列皆为真才参与重载决议：&lt;/p&gt;
&lt;p&gt;a) &lt;code&gt;unique_ptr&amp;lt;U, E&amp;gt;::pointer&lt;/code&gt; 可隐式转换为 &lt;code&gt;pointer&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;b) &lt;code&gt;U&lt;/code&gt; 不是数组类型&lt;/p&gt;
&lt;p&gt;c) &lt;code&gt;Deleter&lt;/code&gt; 是引用类型且 &lt;code&gt;E&lt;/code&gt; 与 &lt;code&gt;D&lt;/code&gt; 为同一类型，或 &lt;code&gt;Deleter&lt;/code&gt; 不是引用类型且 &lt;code&gt;E&lt;/code&gt; 可隐式转换为 &lt;code&gt;D&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;std::unique_ptr&amp;lt;Derived&amp;gt;&lt;/code&gt; 通过重载 (6) 可隐式转换为 &lt;code&gt;std::unique_ptr&amp;lt;Base&amp;gt;&lt;/code&gt; （因为被管理指针和 &lt;code&gt;std::default_delete&lt;/code&gt;都可隐式转换）。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据 more effective c++ 条款28，在简单层面，选择是添加一个简单的用户定义转换函数模板。但是如果照着标准库的方法模拟，则是这样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp&amp;gt;
    struct default_delete
    {
      /// Default constructor
      constexpr default_delete() noexcept = default; //默认删除器的默认构造

      /** @brief Converting constructor.
       *
       * Allows conversion from a deleter for objects of another type, `_Up`,
       * only if `_Up*` is convertible to `_Tp*`.
       */
      template&amp;lt;typename _Up,
	       typename = _Require&amp;lt;is_convertible&amp;lt;_Up*, _Tp*&amp;gt;&amp;gt;&amp;gt;
	_GLIBCXX23_CONSTEXPR
        default_delete(const default_delete&amp;lt;_Up&amp;gt;&amp;amp;) noexcept { } //一个构造函数模板用来在查看默认删除器是否可以被类型转换后进行类型转换。这里很重要

      /// Calls `delete __ptr`
      _GLIBCXX23_CONSTEXPR
      void
      operator()(_Tp* __ptr) const
      {
	static_assert(!is_void&amp;lt;_Tp&amp;gt;::value,
		      &quot;can&apos;t delete pointer to incomplete type&quot;);
	static_assert(sizeof(_Tp)&amp;gt;0,
		      &quot;can&apos;t delete pointer to incomplete type&quot;);
	delete __ptr;
      }
    };

//unique_ptr的构造函数6
template&amp;lt;typename _Up, typename _Ep, typename = _Require&amp;lt;
               __safe_conversion_up&amp;lt;_Up, _Ep&amp;gt;,
	       __conditional_t&amp;lt;is_reference&amp;lt;_Dp&amp;gt;::value,
			       is_same&amp;lt;_Ep, _Dp&amp;gt;,
			       is_convertible&amp;lt;_Ep, _Dp&amp;gt;&amp;gt;&amp;gt;&amp;gt;
	_GLIBCXX23_CONSTEXPR
	unique_ptr(unique_ptr&amp;lt;_Up, _Ep&amp;gt;&amp;amp;&amp;amp; __u) noexcept
	: _M_t(__u.release(), std::forward&amp;lt;_Ep&amp;gt;(__u.get_deleter())) //非常单纯的赋值
	{ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/unique_ptr.h&quot;&gt;源代码&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;上面的代码看起来复杂，其实用自己写的版本可以化简为这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void pivot(T p){
    std::puts(__PRETTY_FUNCTION__); //查看函数签名
}
class myobj{
    public:
    int* ptrs;
    myobj(int val):ptrs(new int(val)){}
    ~myobj(){
        if(ptrs != nullptr){
            delete ptrs;
            ptrs = nullptr;
        }
    }
};

namespace area2{
    template&amp;lt;typename T&amp;gt;
    class default_deleter{ //默认形式的删除器
        public:
        default_deleter() = default;
        //模拟默认删除器的构造函数模板
        template&amp;lt;typename _Up, typename = typename enable_if&amp;lt;is_convertible&amp;lt;_Up*, T*&amp;gt;::value&amp;gt;::type&amp;gt; //注意必须是指针类型的可转换性
        default_deleter(const default_deleter&amp;lt;_Up&amp;gt;&amp;amp;){
            std::puts(__PRETTY_FUNCTION__); //2
        }
        default_deleter&amp;amp; operator=(const default_deleter&amp;amp; rhs){
            cout &amp;lt;&amp;lt;&quot;operator= called&quot; &amp;lt;&amp;lt; endl; //4
            return *this;
        }
        void operator()(T *p) const {
            std::puts(__PRETTY_FUNCTION__); //6
            if(p != nullptr){
                delete p;
                p = nullptr; 
            }
        }
    };

    template&amp;lt;typename T, typename Deleter = default_deleter&amp;lt;T&amp;gt;&amp;gt; //模板表示接受T类型一个接受T类型的删除器
    class unique_ptr{
        public:
        T* p_ = nullptr; //T类型指针
        Deleter d_; //删除器
		//.....
        //模拟构造函数6
        template&amp;lt;typename U, typename E&amp;gt;
        unique_ptr(unique_ptr&amp;lt;U,E&amp;gt;&amp;amp;&amp;amp; u){
            this-&amp;gt;p_ = u.p_; //指针赋值。自然转换
            //----------------
            pivot(u.d_); //1
            default_deleter&amp;lt;T&amp;gt; temp(u.d_);
            pivot(temp); //3
            this-&amp;gt;d_ = temp;
            //----上面的区域可以换成下面一行------------
            //this-&amp;gt;d = u.d_
            u.p_ = nullptr;
        }
		//......
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void pivot(T) [with T = area2::default_deleter&amp;lt;d&amp;gt;] //1
area2::default_deleter&amp;lt;T&amp;gt;::default_deleter(const area2::default_deleter&amp;lt;_Up&amp;gt;&amp;amp;) [with _Up = d; &amp;lt;template-parameter-2-2&amp;gt; = void; T = b] //2
void pivot(T) [with T = area2::default_deleter&amp;lt;b&amp;gt;] //3
operator= called //4
called
void area2::default_deleter&amp;lt;T&amp;gt;::operator()(T*) const [with T = b] //6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心原理就是把指针直接赋值过来。&lt;/p&gt;
&lt;p&gt;当我们有&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;area2::unique_ptr&amp;lt;d&amp;gt; ptr3(new d());
area2::unique_ptr&amp;lt;b&amp;gt; ptr(move(ptr3));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;的时候，首先我们发现这时候传入的指针的删除器类型是&lt;code&gt;area2::default_deleter&amp;lt;d&amp;gt;&lt;/code&gt;。我们发现子类删除器和父类删除器&lt;code&gt;area2::default_deleter&amp;lt;d&amp;gt;&lt;/code&gt;不一样，这就是为什么默认删除器要添加一个构造函数模板。&lt;/p&gt;
&lt;p&gt;然后我们拆开写了，可以直接写&lt;code&gt;this-&amp;gt;d_ = u.d_&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第一步是查找合适的赋值方式。先查找&lt;code&gt;d_&lt;/code&gt;有没有合适的&lt;code&gt;operator=&lt;/code&gt;，明显没有，那么查看&lt;code&gt;u.d_&lt;/code&gt;能不能转换为本类类型的默认拷贝赋值的参数类型，也就是&lt;code&gt;area2::default_deleter&amp;lt;b&amp;gt;&lt;/code&gt;。我们发现&lt;code&gt;default_deleter&lt;/code&gt;拥有一个构造函数模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Up, typename = typename enable_if&amp;lt;is_convertible&amp;lt;_Up*, T*&amp;gt;::value&amp;gt;::type&amp;gt;
    default_deleter(const default_deleter&amp;lt;_Up&amp;gt;&amp;amp;){
    std::puts(__PRETTY_FUNCTION__); //2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它可以帮助我们转换。这个其实非常简单。先查看&lt;code&gt;_Up*&lt;/code&gt;(子类指针，也就是本类中的&lt;code&gt;d&lt;/code&gt;）能不能转换为&lt;code&gt;T*&lt;/code&gt;（父类指针，也就是本类中的&lt;code&gt;b&lt;/code&gt;）。然后就是构造了一个对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里必须要用&lt;code&gt;is_convertible&amp;lt;_Up*, T*&amp;gt;&lt;/code&gt;而不是&lt;code&gt;is_convertible&amp;lt;_Up, T&amp;gt;&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;因为智能指针只有父类指针对子类指针进行&lt;code&gt;delete&lt;/code&gt;的时候才有意义。但是如果直接两个对象之间判断是否可以转换，语义不对。比如两个没有继承关系的但是有转换函数的对象是convertible的，&lt;code&gt;int&lt;/code&gt;和&lt;code&gt;long&lt;/code&gt;之间也是convertible的。但是&lt;code&gt;int&lt;/code&gt;指针可以用&lt;code&gt;long&lt;/code&gt;指针析构吗？显然不可以。所以必须使用指针类型进行判断是否convertible。&lt;/li&gt;
&lt;li&gt;所以这里只有在有继承关系的&lt;strong&gt;子转父&lt;/strong&gt;的时候，或者是**转换CV限定（non-const to const）**的时候才是true。语义才正确。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二步就是我们得到了一个父类类型的默认删除器的临时对象。&lt;/p&gt;
&lt;p&gt;第三部就是把这个删除器赋值即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为默认删除器这个类没有任何数据。所以这样直接转换是符合我们直观意图的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现在子类指针已经被转为父类。那么子类的删除器也应该变为父类。这样才正确。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct b{

};

struct d:b{

};

void func(area2::unique_ptr&amp;lt;b&amp;gt; ptr){
    cout &amp;lt;&amp;lt;&quot;called&quot; &amp;lt;&amp;lt; endl;
}

int main(){
    area2::unique_ptr&amp;lt;d&amp;gt; ptr3(new d()); //现在不会报错了
    func(move(ptr3));

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;智能指针的大师问答&lt;/h1&gt;
&lt;p&gt;来自&lt;a href=&quot;https://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. 如何决定是否使用和使用哪一种智能指针？-- 默认使用unique_ptr&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;优先使用标准智能指针，默认情况下使用&lt;code&gt;unique_ptr&lt;/code&gt;，如果需要共享，则使用&lt;code&gt;shared_ptr&lt;/code&gt;&lt;/strong&gt;。它们是所有 C++ 库都可以理解的通用类型。仅在需要与其他库进行互操作时，或者在需要使用标准指针上的删除器和分配器无法实现的自定义行为时，才使用其他智能指针类型。&lt;/p&gt;
&lt;h2&gt;2. shared_ptr应该如何构造？make_shared还是直接构造？&lt;/h2&gt;
&lt;p&gt;除了我们上面提到的那些关于&lt;code&gt;make_shared&lt;/code&gt;的优缺点和区别以外，想在此补充一些。&lt;/p&gt;
&lt;p&gt;第一个，C++20开始其实有了&lt;a href=&quot;https://zh.cppreference.com/w/cpp/memory/shared_ptr/allocate_shared&quot;&gt;&lt;code&gt;allocate_shared&lt;/code&gt;&lt;/a&gt;。 顾名思义就是自定义内存分配器，但是我们其实可以视&lt;code&gt;make_shared&lt;/code&gt;是一种狭义的&lt;code&gt;allocate_shared&lt;/code&gt;（因为实际上没有自定义自己的内存分配器）&lt;/p&gt;
&lt;p&gt;第二个，除了上面说到的make方法的使用以外，有一个额外的工程层面问题可能会需要直接构造，也就是&lt;strong&gt;如果你采用指向从其他（通常是遗留的）代码传递给你的对象的原始指针，则可以直接从该原始指针构造一个&lt;code&gt;shared_ptr&lt;/code&gt;。&lt;/strong&gt; 其余情况下，应当尽可能的使用&lt;code&gt;make_shared&lt;/code&gt;。因为不仅使代码更清晰，而且从内存角度来讲，一般情况下，局部性，碎片和分配次数比实际的大小对系统的影响更大&lt;/p&gt;
&lt;h1&gt;其他文章&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://andreasfertig.blog/2024/10/smart-pointers-and-the-pointer-to-implementation-idiom/&quot;&gt;智能指针和Pinpl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tonni.nl/blog/shared-ptr-overuse-cpp&quot;&gt;过度使用shared_ptr的危害及如何避免&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;设计智能指针 -- more effective C++ 第七章&lt;/h1&gt;
&lt;p&gt;因为这本书非常老，所以我们可以从最原始的角度理解智能指针到底需要满足什么需求，有哪些设计方式，为什么最后成为了现在的这个样子。&lt;/p&gt;
&lt;h2&gt;7.2 语义&lt;/h2&gt;
&lt;p&gt;7.2讲的是语义。智能指针到底应该如何保有资源。这部分我想就是后面拆分的&lt;code&gt;unique_ptr&lt;/code&gt;和&lt;code&gt;shared_ptr&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;7.3 智能指针的存储&lt;/h2&gt;
&lt;p&gt;7.3讲的是智能指针的存储。应该保留一个什么类型的原始指针？同时提到了重载&lt;code&gt;operator-&amp;gt;&lt;/code&gt;的技巧。&lt;/p&gt;
&lt;h2&gt;7.4 成员函数&lt;/h2&gt;
&lt;p&gt;7.4讲的是成员函数。提到了智能指针不应该拥有如release()这样的成员函数。但是目前，它确实有。书中给到的不应该有的理由是：必须区分&lt;code&gt;ptr-&amp;gt;release()&lt;/code&gt;和&lt;code&gt;ptr.release()&lt;/code&gt;的区别。因为可能智能指针保有的指针类型恰好也含有一个对应名字的函数。&lt;/p&gt;
&lt;h2&gt;7.5 所有权&lt;/h2&gt;
&lt;p&gt;7.5讲的是所有权。 相当于7.2的延伸。这部分涉及到了侵入式和非侵入式指针的设计。侵入式如VTK的智能指针，好处是智能指针本身不需要操心引用计数器。因为引用计数是和对象绑定在一起的。缺点是对象必须要继承引用计数类。同时，提到了几个引用计数的设计。这部分在我们上面提到的&lt;code&gt;make&lt;/code&gt;方法中有所体现。同时，额外提到了一种利用双向链表来做为引用计数的方法（感觉没啥用）。最后提到了关于循环引用的问题。&lt;/p&gt;
&lt;h2&gt;7.6 取址操作符&lt;/h2&gt;
&lt;p&gt;7.6讲的是取址操作符。这一部分非常经典？理论上从不应该重载取址操作符，当然了，在某些代理类的时候除外。书中提到了重载了取址操作符的类不太好用于STL容器，因为所有&lt;code&gt;&amp;amp;T&lt;/code&gt;都应该返回一个&lt;code&gt;T*&lt;/code&gt;。当然了我觉得这是因为当时还没有&lt;code&gt;std::address_of&lt;/code&gt;这个东西。&lt;strong&gt;不过也确实，STL的智能指针都没有重载&lt;code&gt;operator&amp;amp;&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;7.7 隐式转换&lt;/h2&gt;
&lt;p&gt;这部分就是上面提到的：&lt;em&gt;如果自己设计的话，如何让原始指针为形参的函数接受智能指针为实参？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;答案是看情况。需要权衡利弊。&lt;/p&gt;
&lt;h2&gt;7.8 7.9 比较操作&lt;/h2&gt;
&lt;p&gt;STL目前是拥有智能指针的比较函数的。也就是重载了对应的函数。并且不是成员函数。&lt;/p&gt;
&lt;h2&gt;7.10 错误检测&lt;/h2&gt;
&lt;p&gt;比如是否可以把空值传入智能指针做为初始值？目前是可以的。但是书上说不太应该。但是也是case by case。&lt;/p&gt;
&lt;p&gt;另一个就是关于是否要进行解引用前检测是否为空？我个人认为应该把这个留给用户。应该让智能指针在智能的同时，尽可能简约。&lt;/p&gt;
&lt;h2&gt;7.11 常量性&lt;/h2&gt;
&lt;p&gt;确实有一个很有意思的地方。无论指针的常量性是顶层还是底层还是皆有，&lt;code&gt;delete&lt;/code&gt;都可正常使用。从某个角度来说是废话，但是从所有权的语义上理解可能就有了微妙的地方。为啥要在智能指针上面提到这种呢？因为智能指针也应该支持顶层和底层的const的语义区别。确实支持。毕竟原理就是&lt;code&gt;type trait&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;7.12 数组类型&lt;/h2&gt;
&lt;p&gt;其实不太应该使用堆上的数组。当然了，标准库也对智能指针提供了&lt;code&gt;[]&lt;/code&gt;数组特化的版本。&lt;/p&gt;
&lt;h2&gt;7.13 多线程&lt;/h2&gt;
&lt;p&gt;对于智能指针的多线程访问一直是一个比较令人困扰的问题。&lt;/p&gt;
&lt;p&gt;一般来说，对于智能指针的多线程访问我们需要分成两个方向去讨论。第一个方向是指针保有的对象的多线程访问。另一个是指针的引用计数的多线程访问。&lt;/p&gt;
&lt;p&gt;针对第一个，我们能做的东西不大。语义上我们希望智能指针帮我们做的事情越少越好，越少也就越透明，越自由。所以说在这一方面我们不应替用户操心太多。书中提到了自动锁定的应用，但我觉得不怎么样。用户自己会去针对并发访问进行处理。&lt;/p&gt;
&lt;p&gt;针对第二个，我们可以肯定的是目前的&lt;code&gt;shared_ptr&lt;/code&gt;的引用计数块的并发访问是不安全的。所以需要进行控制。原理还是一样，我们不应该在这个情况上替用户做过多决定。&lt;/p&gt;
</content:encoded></item><item><title>C++杂记 - 2（二）：inline 细节、编译过程、对象内存模型、指针与引用</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B02-2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B02-2/</guid><pubDate>Wed, 20 Jul 2022 06:56:00 GMT</pubDate><content:encoded>&lt;h1&gt;inline 额外的区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inline&lt;/code&gt; 只意味着在程序中函数的定义可以出现很多次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;直接转载至此处&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作者：吼姆桑
链接：https://www.zhihu.com/question/24185638/answer/2404153835
来源：知乎&lt;/p&gt;
&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;我曾在很长一段时间对inline关键字的认识都维持在相当朴素的程度：用来建议编译器将函数内联展开至调用处。这种理解在早期的C++标准（C++98之前）里问题不大，但在C++98标准以后就完全不对了。首先，现代的编译器在决定是否将函数调用进行内联展开时，几乎不参考函数声明中inline修饰符；其次，inline关键字不仅能修饰函数，也可修饰变量（C++17以后），甚至能修饰命名空间（C++11以后）；此外，inline更为关键的作用是允许同一个函数或变量的定义出现在多个编译单元之中；最后，修饰命名空间的inline关键字为程序库的版本控制提供了语言层面上的支持，这与最初的函数内联的含义更是相去甚远。&lt;/p&gt;
&lt;h2&gt;inline函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;**我们知道，若一个非static函数在多个编译单元中被重复定义，那么在链接阶段会发生multiple definition的错误，这是因为面对同一符号的多个定义，链接器并不知道使用哪个。但是对于header-only的程序库来说，所有的函数定义都位于头文件，若不加特殊处理被多个源文件包含，则一定会发生multiple definition的错误。解决这个问题的方法是在函数声明处加上inline修饰，这样的话编译器在处理该函数时就会认为它是一个弱符号，链接器在面对多个名字相同的弱符号时只会保留其中一个的定义（具体的保留规则视编译器而定）。**我用一个例子来说明inline函数的编译行为：inline函数&lt;code&gt;foo&lt;/code&gt;定义在头文件foo.h中，且函数内部定义了&lt;a href=&quot;https://www.zhihu.com/search?q=%E5%B1%80%E9%83%A8%E9%9D%99%E6%80%81%E5%8F%98%E9%87%8F&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;局部静态变量&lt;/a&gt;&lt;code&gt;factor&lt;/code&gt;；源文件bar1.cc和bar2.cc分别包含了foo.h，并在&lt;code&gt;bar1&lt;/code&gt;和&lt;code&gt;bar2&lt;/code&gt;函数中都调用了&lt;code&gt;foo&lt;/code&gt;函数；最后，源文件main.cc中的&lt;code&gt;main&lt;/code&gt;函数调用了&lt;code&gt;bar1&lt;/code&gt;和&lt;code&gt;bar2&lt;/code&gt;函数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/* foo.h */
inline int foo(int x) {
    static int factor = 1;
    return x * (factor++);
}

/* bar1.cc */
#include &quot;foo.h&quot;
int bar1() {
    return foo(1);
}

/* bar2.cc */
#include &quot;foo.h&quot;
int bar2() {
    return foo(2);
}

/* main.cc */
int Bar1(), Bar2();
int main() {
    return Bar1() + Bar2();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用gcc编译这三个源文件并链接生成可执行文件main，链接过程中并不会发生multiple definition的错误，并且&lt;a href=&quot;https://www.zhihu.com/search?q=main%E5%87%BD%E6%95%B0&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;main函数&lt;/a&gt;的返回值表明两次调用使用了同一个局部静态变量factor。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ -c main.cc bar1.cc bar2.cc -fno-gnu-unique  # ok
g++ -o main main.o bar1.o bar2.o    # ok
./main; echo $? # 5
readelf -s main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后我们用readelf查看输出的可执行main文件的符号表，可以发现main中确实只有一份符号Foo的定义，同时Foo中的静态变量factor也同样只保留了一份。此时函数Foo和内部定义的局部静态变量factor都是weak符号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Num:    Value          Size Type    Bind   Vis      Ndx Name
...
49: 0000000000004010     4 OBJECT  WEAK   DEFAULT   23 _ZZ3FooiE6factor
...
53: 000000000000115f    32 FUNC    WEAK   DEFAULT   14 _Z3Fooi
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，没有规定强制多个编译单元中的同名inline函数的定义必须一致，链接器并不会对这种定义不一致的行为报错，但你却无法保证生成的可执行文件中调用了哪个版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* bar1.cc */
inline int Foo(int x) {
    static int factor = 1;
    return x * (factor++);
}
int Bar1() {
    return Foo(1);
}

/* bar2.cc */
inline int Foo(int x) {
    static int factor = 2;
    return x * (factor++);
}
int Bar2() {
    return Foo(2);
}

/* main.cc */
int Bar1(), Bar2();
int main() {
    return Bar1() + Bar2();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于上述例子，9.3.0版本的gcc面对这种情况很可能是根据源文件的编译顺序而决定使用哪个Foo：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ -o main main.cc bar1.cc bar2.cc -fno-gnu-unique # ok
./main; echo $? # 5
g++ -o main main.cc bar2.cc bar1.cc -fno-gnu-unique # ok
./main; echo $? # 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以面对这种其它编译单元可能定义了同名inline函数的情况，不要轻易定义该名字的inline函数，因为你既无法保证对方的定义与你相同，也无法保证链接器最终选择的定义。如果非要定义，应该将其声明为static或者将其声明定义在另一个不冲突的命名空间中。当然，不使用任何关键字修饰该函数也不行，因为这时你定义的版本对应的符号是全局的强符号，链接器在面对多个弱符号和一个强符号时一定会采用强符号对应的定义，因此你定义的版本会覆盖其它单元所定义的inline版本。除非你知道这样的后果是你所需的（确实我们有时候会这么做，例如覆盖掉程序库提供的默认版本），否则不要这样做。&lt;/p&gt;
&lt;h2&gt;inline变量&lt;/h2&gt;
&lt;p&gt;inline用于修饰变量定义是在C++17之后的事情。当inline用于修饰变量定义时，你很难说它具有&lt;a href=&quot;https://www.zhihu.com/search?q=%E5%86%85%E8%81%94%E5%B1%95%E5%BC%80&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;内联展开&lt;/a&gt;的作用，因为将变量的定义内联展开到引用处没有意义，它更多地只是允许在多个编译单元对同一个变量进行定义，并且在链接时只保留其中的一份作为该符号的定义。当然，同时在多个源文件中定义同一个inline变量必须保证它们的定义都相同，否则和inline函数一样，你没办法保证链接器最终采用的是哪个定义。inline变量除了允许我们在头文件中定义全局变量，也允许我们在类定义中直接初始化静态数据成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* foo.h */
struct Foo {
    inline static int foo = 1;
};
/* bar1.cc */
#include &quot;foo.h&quot;
int Bar1() { return Foo::foo + 1; }
/* bar2.cc */
#include &quot;foo.h&quot;
int Bar2() { return Foo::foo + 2; }
/* main.cc */
int Bar1(), Bar2();
int main() {
    return Bar1() + Bar2();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们用gcc编译上述文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ -std=c++17 -c main.cc bar1.cc bar2.cc # ok
g++ -std=c++17 -o main main.o bar1.o bar2.o # ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并用readelf分别查看各个目标文件，我们会发现这个内联的静态数据成员在bar1.o和bar2.o中都以&lt;a href=&quot;https://www.zhihu.com/search?q=%E5%BC%B1%E7%AC%A6%E5%8F%B7&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;弱符号&lt;/a&gt;的形式单独存在于一个段中，在链接后main只会包含一个foo符号，并且该符号对应的数据被合并到了.data段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 以下为 readelf -sS bar1.o | grep foo 和readelf -sS bar2.o的内容 
Section Headers:
[Nr] Name               Type             Address           Offset
...
[ 6] .data._ZN3Foo3fooE PROGBITS         0000000000000000  0000005c
...
Symbol table &apos;.symtab&apos; contains 13 entries:
Num:    Value          Size Type    Bind   Vis      Ndx Name
...
11: 0000000000000000     4 OBJECT  WEAK   DEFAULT    6 _ZN3Foo3fooE
# 以下为readelf -s main | grep foo
Num:    Value          Size Type    Bind   Vis      Ndx Name
...
0000000000004010     4 OBJECT  WEAK   DEFAULT   23 _ZN3Foo3fooE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见在bar1.o和bar2.o中内联的静态数据成员被单独放到了名为.data._ZN3Foo3fooE的段中，这种将内联变量单独放置一个段的原因主要是为了在链接时消除重复代码，在链接合并段的时候面这些特殊的同名段会选择性地保留其中一个。&lt;/p&gt;
&lt;h2&gt;inline命名空间&lt;/h2&gt;
&lt;p&gt;inline命名空间是C++11标准中引入的关键词，对于一个用inline修饰的内嵌命名空间而言，它所包含的成员在可见性上如同声明在外围&lt;a href=&quot;https://www.zhihu.com/search?q=%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;命名空间&lt;/a&gt;中一样，所以inline之于命名空间更具有字面上的含义：将内嵌命名空间在外围命名空间中“展开”。inline命名空间最主要的用途是为&lt;strong&gt;程序库的版本控制提供语言上的支持&lt;/strong&gt;，有点类似于共享库中的[符号版本控制](https://zhuanlan.zhihu.com/&lt;a href=&quot;https://gcc.gnu.org/wiki/SymbolVersioning&quot;&gt;SymbolVersioning - GCC Wiki (gnu.org)&lt;/a&gt;)（将程序使用的接口版本信息记录到可执行文件，&lt;a href=&quot;https://www.zhihu.com/search?q=%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;动态链接&lt;/a&gt;时再根据这些版本信息从&lt;a href=&quot;https://www.zhihu.com/search?q=%E5%85%B1%E4%BA%AB%E5%BA%93&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;共享库&lt;/a&gt;调用正确版本的接口），一般来说库的作者会为不同的版本放置到专属的命名空间，再用一个与版本无关的外围命名空间将它们包含，并通过&lt;a href=&quot;https://www.zhihu.com/search?q=%E9%A2%84%E7%BC%96%E8%AF%91%E6%8C%87%E4%BB%A4&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2404153835%7D&quot;&gt;预编译指令&lt;/a&gt;选择性地将开发环境支持的库版本对应的命名空间暴露给用户。例如下面的例子中库&lt;code&gt;libfoo&lt;/code&gt;根据宏&lt;code&gt;some_predefined_macro&lt;/code&gt;的值将不同版本的接口暴露给用户程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define some_predefined_macro   2022L

namespace libfoo {
#if some_predefined_macro &amp;gt;= 2022L
    inline
#endif
    namespace libfoo_2022 {
        int foo(int);       // better foo
        float foo(float);   // new foo
    }
#if some_predefined_macro &amp;gt;= 2019L &amp;amp;&amp;amp; some_predefined_macro &amp;lt; 2022L
    inline
#endif
    namespace libfoo_2019 {
        int foo(int);   // old foo
    }
// other versions ...
}

using namespace libfoo;
int main() {
    printf(&quot;foo(5) = %d\n&quot;, foo(5));    // refer to int libfoo::libfoo_2022::foo(int);
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，这种选择性地暴露内嵌命名空间成员乍一看也可以通过using namespace来完成，在C++11之前库的版本控制就是通过它来实现的。但用这种方式实现库的版本控制是有两个明显的缺陷：（1）在内嵌命名空间声明的模板无法在外围命名空间中进行特化，（2）不支持[ADL](https://zhuanlan.zhihu.com/&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/adl&quot;&gt;Argument-dependent lookup - cppreference.com&lt;/a&gt;)。一般来说库作者都不希望将版本相关的命名空间暴露给用户，而缺陷（1）又要求用户必须在模板所在的命名空间中对其进行特化，例如说下面这段代码就不行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace libfoo {
    namespace libfoo_2022 {
        template &amp;lt;typename T&amp;gt;
        T &amp;amp;foo(T &amp;amp;);
    }
    using namespace libfoo_2022;
}

namespace libfoo {
    template &amp;lt;&amp;gt;
    float &amp;amp;foo&amp;lt;float&amp;gt;(float &amp;amp;); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ADL的意思是在函数名字查找时自动将调用参数所属的命名空间包含进来，这样在函数调用时便无需显示指定作用域。那缺陷（2）意思是用内嵌命名空间的类型变量作为参数调用外围命名空间的函数，或者用外围命名空间的类型变量作为参数调用内嵌命名空间的函数是行不通的，例如下面这段代码也不行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace libfoo {
    class Bar1 {};
    namespace libfoo_2022 {
        void foo1(Bar1);
        class Bar2 {};
    }
    using namespace libfoo_2022;
    void foo2(Bar2);
}

int main()
{
    libfoo::Bar1 bar1;
    libfoo::Bar2 bar2;
    (void)foo1(bar1);    // compile error
    (void)foo2(bar2);    // compile error
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;inline命名空间可以解决using namespace在实现库版本控制时的缺陷，inline与using的区别在于用inline修饰的内嵌命名空间的成员表现得更像声明在外围命名空间：你不仅可以在外围命名空间中对inline的内嵌命名空间的模板进行特化，而且ADL也会将参数类型所在命名空间所包含的inline命名空间，以及自身作为inline命名空间所在的外围命名空间一并考虑进来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace libfoo {
    class Bar1 {};
    inline namespace libfoo_2022 {
        void foo1(Bar1);
        class Bar2 {};
        template &amp;lt;typename T&amp;gt;
        T &amp;amp;foo(T &amp;amp;);
    }
    void foo2(Bar2);
}

namespace libfoo {
    template &amp;lt;&amp;gt;
    float &amp;amp;foo&amp;lt;float&amp;gt;(float &amp;amp;);
}

int main() {
    libfoo::foo(3.2);       // call specialized libfoo::libfoo_2022::foo
    libfoo::Bar1 bar;
    libfoo::Bar2 bar2;
    foo1(bar1);             // call lib::libfoo_2022::foo1
    foo2(bar2);             // call lib::foo2
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;inline在修饰命名空间时还有一些别的特点，比如若inline命名空间与外围命名空间包含了名字重复的符号时，使用外围命名空间作为作用域对该符号进行引用将会导致编译错误，因为编译器无法确定你究竟引用了哪个命名空间的符号。但如果使用using namespace的话上述情况则总是指向外围命名空间中的符号。此外inline命名空间还具有传递性，这些具体的特性详见[cppreference](https://zhuanlan.zhihu.com/&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/namespace#Inline_namespaces&quot;&gt;Namespaces - cppreference.com&lt;/a&gt;)，但我认为inline修饰符引入到命名空间最主要的作用是在库版本控制时解决ADL和命名空间外模板特化的问题。&lt;/p&gt;
&lt;h3&gt;核心解析 inline namespace：&lt;/h3&gt;
&lt;p&gt;把一个命名空间标成 &lt;code&gt;inline&lt;/code&gt; 后，&lt;strong&gt;其中的名字在外层作用域里像“直接声明在外层”一样可见&lt;/strong&gt;（对未限定名查找/ADL可见），但&lt;strong&gt;符号名仍会带上这个命名空间做区分（影响符号mangling）&lt;/strong&gt;。这让库可以同时提供多个版本的接口，并指定一个“默认版本”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace mylib {
    namespace v1 {
        struct Foo { void old_api(); };
    }

    inline namespace v2 {         // 当前默认版本
        struct Foo { void new_api(); };
    }
}

// 用户代码：
mylib::Foo x;    // 实际是 mylib::v2::Foo
x.new_api();     // 未限定查找能直接用 v2 的名字
// 仍可显式用老版本：
mylib::v1::Foo y;
y.old_api();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查找行为&lt;/strong&gt;：&lt;code&gt;inline namespace&lt;/code&gt; 里的名字对外层是&lt;strong&gt;透明&lt;/strong&gt;的，像直接在 &lt;code&gt;mylib&lt;/code&gt; 里声明一样可见（未限定名查找、ADL 都能找到）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ABI 行为&lt;/strong&gt;：符号仍会被编码为 &lt;code&gt;mylib::v2::Foo&lt;/code&gt;（带版本名），因此你可以同时提供 &lt;code&gt;v1&lt;/code&gt; 和 &lt;code&gt;v2&lt;/code&gt; 两套符号，&lt;strong&gt;老二进制&lt;/strong&gt;继续链接 &lt;code&gt;v1&lt;/code&gt;，&lt;strong&gt;新编译&lt;/strong&gt;默认用 &lt;code&gt;v2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;切换默认版本&lt;/strong&gt;：只需把别的版本标成 &lt;code&gt;inline&lt;/code&gt; 即可：&lt;/p&gt;
&lt;h1&gt;typedef和define（宏）有什么区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;用法不同：typedef用来定义一种数据类型的别名，增强程序的可读性。define主要用来定义常量，以及书写复杂使用频繁的宏。&lt;/li&gt;
&lt;li&gt;执行时间不同：&lt;strong&gt;typedef是编译期处理&lt;/strong&gt;，有类型检查的功能。define是宏定义，是预编译的部分，其发生在编译之前，只是简单的进行字符串的替换，不进行类型的检查。&lt;/li&gt;
&lt;li&gt;作用域不同：typedef有作用域限定。define不受作用域约束，只要是在define声明后的引用都是正确的。&lt;/li&gt;
&lt;li&gt;对指针的操作不同：typedef和define定义的指针时有很大的区别。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：typedef定义是语句，因为句尾要加上分号。而define不是语句，千万不能在句尾加分号。&lt;/p&gt;
&lt;h1&gt;如何加深理解移动构造和拷贝构造&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj(int x):val(new int(x)){}
        int&amp;amp; getval(){
            return *val;
        }
        myobj(const myobj&amp;amp; obj){						//拷贝构造
            val = new int(*obj.val);					
        }
        myobj&amp;amp; operator=(const myobj &amp;amp; rhs){			//拷贝赋值
            if(this == &amp;amp;rhs){
                return *this;
            }
            delete this-&amp;gt;val;
            val = new int(*rhs.val);
            return *this;
        }
        myobj(myobj&amp;amp;&amp;amp; obj){								//移动构造
            cout &amp;lt;&amp;lt;&quot;mv&quot; &amp;lt;&amp;lt; endl;
            val = obj.val;
            obj.val = nullptr;
        }
        myobj&amp;amp; operator=(myobj&amp;amp;&amp;amp; rhs){					//移动赋值
            cout &amp;lt;&amp;lt;&quot;mv=&quot; &amp;lt;&amp;lt; endl;
            if(this == &amp;amp;rhs){
                return *this;
            }
            if(this-&amp;gt;val != nullptr){
                delete val;
            }
            val = rhs.val;
            rhs.val = nullptr;
            return *this;

        }
        ~myobj(){
            delete val;
            val = nullptr;
        }


        int* val;
        
};

int main(){
    myobj a(5);
    myobj b(10);
    cout &amp;lt;&amp;lt; a.getval() &amp;lt;&amp;lt; endl;					
    myobj c(a);							//拷贝构造
    cout &amp;lt;&amp;lt; c.getval() &amp;lt;&amp;lt; endl;
    myobj d = c;						//拷贝构造。因为d对象之前不存在。
    cout &amp;lt;&amp;lt; d.getval() &amp;lt;&amp;lt; endl;
    d = b;								//拷贝赋值
    cout &amp;lt;&amp;lt; d.getval() &amp;lt;&amp;lt; endl;
    myobj e = move(a);					//移动构造。因为e对象之前不存在。在这之后不可访问a，因为a所有权已被转移
    cout &amp;lt;&amp;lt; e.getval() &amp;lt;&amp;lt; endl;
    d = move(b);						//移动赋值。在这之后不可访问b,因为b所有权已被转移
    cout &amp;lt;&amp;lt; d.getval() &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先我们要强化记忆，什么叫构造函数。构造函数就是创建对象的时候调用的。我们这里有一个类，里面有一根指针。首先，我们既然是构造，那么拷贝构造和默认构造都要对变量（指针）初始化（分配内存）。所以这里拷贝构造和默认构造都是&lt;code&gt;new&lt;/code&gt;的。因为我们都是&lt;strong&gt;无中生有&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么移动构造和拷贝构造的区别在哪呢？&lt;/p&gt;
&lt;p&gt;什么时候我们想要移动？假如我们要&lt;strong&gt;转移所有权&lt;/strong&gt;，我们这时候会调用移动构造。也是无中生有。但是由于转移所有权，所以我们&lt;strong&gt;不需要new&lt;/strong&gt;。我们只需要把原来指针指向的地址赋值给我们新变量的对应变量上即可。&lt;strong&gt;移动之后，因为我们的目的是转移所有权，所以应当对原对象的对应指针变量置空。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;拷贝赋值和移动赋值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;拷贝赋值我们首先要检测自我赋值。通过地址来判断。如果是一个地址互相赋值，那么什么都不做。然后我们首先把自己的东西清理掉，也就是自己指针指向的资源释放掉。然后，由于我们是拷贝，不是移动。所以我们依旧要&lt;strong&gt;开辟新空间，把原来对象的指针指向的值复制过来。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动赋值，我们首先也要检测自我赋值。然后也要先把自己的东西清理掉，也就是自己指针指向的资源释放掉。然后，由于我们是&lt;strong&gt;移动&lt;/strong&gt;，所以我们直接复制指针而不是指针指向的值。然后把&lt;strong&gt;源对象的指针置空，因为我们是转移所有权。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构造函数，无论是默认构造，拷贝构造还是移动构造，函数都无返回值。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;赋值操作符，无论是拷贝赋值还是移动赋值，&lt;strong&gt;我们为了链式调用，也避免不必要的拷贝&lt;/strong&gt;，都应该返回对象的引用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;形参：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;拷贝构造和拷贝赋值，应该接受对象的常引用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动构造和移动赋值因为涉及到对源对象的修改，即 对源成员变量（指针）置空，所以不能是常量。而且要接收右值。因为是移动。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;什么时候该触发移动构造呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果临时对象即将消亡，并且它里面的资源是需要被再利用的，这个时候我们就可以触发移动构造。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如&lt;code&gt;vector.push_back(obj(8));&lt;/code&gt;这就是往容器添加一个临时对象。这时候如果此临时对象有移动构造，则调用完默认构造后，直接触发移动构造进行搬移省去一次内存分配。否则需要调用拷贝构造。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动构造在使用vector的情况下，一般是vector扩容的时候重新分配内存的时候使用，如果元素的自定义类型有移动构造就会用移动构造。没有就是拷贝构造。&lt;strong&gt;这里的移动构造必须是&lt;code&gt;noexcept&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;针对自定义类强制生成移动构造&lt;/h2&gt;
&lt;p&gt;针对自定义类强制生成移动构造会满足member-wise move。也就是移动。它会帮我们调用每一个成员的移动构造。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        int val; //基本数据类型
        string str; //自定义类型可移动
        unique_ptr&amp;lt;int&amp;gt;u_ptr = nullptr; //智能指针，也算自定义类型可移动

        myobj(int x, const string&amp;amp; rhsstr, int num): val(x), str(rhsstr), u_ptr(make_unique&amp;lt;int&amp;gt;(num)){
            cout &amp;lt;&amp;lt; &quot;const&quot; &amp;lt;&amp;lt; endl;
        }
        myobj(myobj&amp;amp;&amp;amp; rhs) = default; //强制生成移动构造。
};
int main(){
    myobj obj(4, &quot;abc&quot;, 22);
    cout &amp;lt;&amp;lt; *obj.u_ptr &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; obj.str &amp;lt;&amp;lt; endl;
    return 0;
}
/*
输出
const
22
abc
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果决定移动它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    myobj obj(4, &quot;abc&quot;, 22);
    cout &amp;lt;&amp;lt; *obj.u_ptr &amp;lt;&amp;lt; endl;
    myobj obj1(move(obj)); //移动后
    if(obj.u_ptr != nullptr){
        cout &amp;lt;&amp;lt;&quot;not null&quot; &amp;lt;&amp;lt; endl;
    }
    else{
        cout &amp;lt;&amp;lt;&quot;null&quot; &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt; obj.str &amp;lt;&amp;lt; endl;
    return 0;
}
/*
输出
const
22
null
(这行是空字符串)

*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们看到，&lt;code&gt;unique_ptr&lt;/code&gt;和&lt;code&gt;string&lt;/code&gt;都被正确移动了。原对象的智能指针为空，字符串也为空。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;但是，裸指针（指针类型）并没有移动构造，所以必须写出我们自己的移动构造。包括两个部分，一个部分是设置为&lt;code&gt;default&lt;/code&gt;强制生成构造函数可以帮我们完成的member wise move。另一个部分是给原始指针进行置空&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        int val; //基本数据类型
        string str; //自定义类型可移动
        int* ptr = nullptr; //注意此处替换为原始指针

        myobj(int x, const string&amp;amp; rhsstr, int* num): val(x), str(rhsstr), ptr(num){
            cout &amp;lt;&amp;lt; &quot;const&quot; &amp;lt;&amp;lt; endl;
        }
        myobj(myobj&amp;amp;&amp;amp; rhs) = default;
        ~myobj(){
            delete ptr;
            ptr = nullptr;
        }
};
int main(){
    myobj obj(4, &quot;abc&quot;, new int(22));
    cout &amp;lt;&amp;lt; *obj.ptr &amp;lt;&amp;lt; endl;
    if(obj.ptr != nullptr){
        cout &amp;lt;&amp;lt;&quot;not null&quot; &amp;lt;&amp;lt; endl;
    }
    else{
        cout &amp;lt;&amp;lt;&quot;null&quot; &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt; obj.str &amp;lt;&amp;lt; endl;
    return 0;
}
/*
输出
const
22
notnull
abc
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们决定移动它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    myobj obj(4, &quot;abc&quot;, new int(22));
    cout &amp;lt;&amp;lt; *obj.ptr &amp;lt;&amp;lt; endl;
    myobj obj1(move(obj));
    cout &amp;lt;&amp;lt; *obj1.ptr &amp;lt;&amp;lt; endl;
    if(obj.ptr != nullptr){
        cout &amp;lt;&amp;lt;&quot;not null&quot; &amp;lt;&amp;lt; endl;
    }
    else{
        cout &amp;lt;&amp;lt;&quot;null&quot; &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt; obj.str &amp;lt;&amp;lt; endl;
    return 0;
}
/*
输出
const
22
22 //此行为obj1的输出。也是22
notnull
(这行是空字符串)
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们看到，&lt;code&gt;string&lt;/code&gt;被正确移动了。原对象的字符串为空。但是由于原始指针没有自己的移动构造，所以编译器只能帮助我们进行member wise move，无法帮助我们给原始指针进行置空。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们要这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj(myobj&amp;amp;&amp;amp; rhs): 
    val(move(rhs.val)), //基本数据类型，这么写永远不会错。虽然没什么用。
    str(move(rhs.str)),
    ptr(move(rhs.ptr)){ //原始指针类型，这么写永远不会错。虽然没什么用。
        rhs.ptr = nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是就会有正确结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    myobj obj(4, &quot;abc&quot;, new int(22));
    myobj obj1(move(obj));
    cout &amp;lt;&amp;lt; *obj1.ptr &amp;lt;&amp;lt; endl;
    if(obj.ptr != nullptr){
        cout &amp;lt;&amp;lt;&quot;not null&quot; &amp;lt;&amp;lt; endl;
    }
    else{
        cout &amp;lt;&amp;lt;&quot;null&quot; &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt; obj1.str &amp;lt;&amp;lt; endl;
    return 0;
}
/*
输出
const
22 //此行为obj1的输出。也是22
null //obj的指针已经空了。
abc //此行为obj1的字符串。正确移动了。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://youtu.be/St0MNEU5b0o&lt;/p&gt;
&lt;h1&gt;for循环&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;for ( init; condition; increment )
{
   statement(s);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for循环的更新条件（第三个条件）是迭代之后执行的，也就是最后执行的。&lt;/p&gt;
&lt;h2&gt;重要优化 for循环的判断表达式（第二个条件）是每一次循环都会执行。如果判断表达式中有函数调用则每次都会调用&lt;/h2&gt;
&lt;p&gt;如下面代码，for循环调用了五次，则&lt;code&gt;func&lt;/code&gt;函数也会被调用5次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int func(int x) {
    cout &amp;lt;&amp;lt;&quot;call&quot; &amp;lt;&amp;lt; endl;
    return 5;
}
int main() {

    for (int i = 0; i &amp;lt; func(i); i++) {
        cout &amp;lt;&amp;lt; &quot;for&quot; &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;i++和++i&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;++i
++i 先自加，再赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;stdio.h&amp;gt;
int main()
{
    int i = 0;
    int j = ++i;    
    cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; j &amp;lt;&amp;lt;endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时输出 i=1 j=1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;i++&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​		i++ 先赋值，再自加。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;stdio.h&amp;gt;
int main()
{
    int i = 0;
    int j = i++;
    cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; j &amp;lt;&amp;lt;endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时输出 i=1 j=0&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;i++是右值，++i是左值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在内置类型的情况下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = i++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这句话在编译器眼里是下面这样的两句话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = i;
i++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = ++i;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在编译器眼里则是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;i++;
int a = i;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;i++和++i在非自定义类型的情况下，只要不发生赋值，则原理没有差别。但是性能有差别。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们深入看一下&lt;strong&gt;自定义类型的情况&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct mybase
{
    // 前置++
    mybase&amp;amp; operator++()
    {
        data++;
        return *this;
    }

    // 后置++
    const mybase operator++(int) //注意返回const
    {
        auto tmp = *this;
        // 执行++有两种选择, data++或者调用上面的前置++
        data++;

        return tmp; //注意这里注意这里temp是局部变量，不可返回引用！！
    }

    int data = 0;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里后置的&lt;code&gt;++&lt;/code&gt;我们有一个非常奇怪的写法。我们用一个临时变量保存了自己，然后再把自己的值&lt;code&gt;+1&lt;/code&gt;，然后返回一个自己的拷贝。&lt;strong&gt;不能返回引用！因为这里的temp是临时变量，返回引用就炸了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同时应为其添加&lt;code&gt;const&lt;/code&gt;。原因是&lt;code&gt;i++++&lt;/code&gt;这样的调用并不应该合法。首先因为它不符合如&lt;code&gt;int&lt;/code&gt;这样的内置类型的语义。其次，&lt;code&gt;i++++&lt;/code&gt;的第二个&lt;code&gt;++&lt;/code&gt;是作用于&lt;code&gt;i++&lt;/code&gt;返回值的临时对象身上的，所以最终&lt;code&gt;i&lt;/code&gt;也只会被&lt;code&gt;++&lt;/code&gt;一次。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这也就是为什么&lt;code&gt;i++&lt;/code&gt;先赋值再自增了。因为我们赋值过去的是一个没有自增的自己。&lt;/p&gt;
&lt;p&gt;这也是为什么&lt;code&gt;i++&lt;/code&gt;是一个右值。因为他符合了右值的定义之一：返回了一个非引用类型的 函数调用。因为&lt;code&gt;++&lt;/code&gt;本身也可以重载，也是函数调用。他返回了一个临时对象的自己。&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;++i&lt;/code&gt;反而看起来非常简单。我们先给自己&lt;code&gt;+1&lt;/code&gt;然后返回自己的引用。这也是为什么&lt;code&gt;++i&lt;/code&gt;是先自增再赋值，也是为何&lt;code&gt;++i&lt;/code&gt;是左值。&lt;/p&gt;
&lt;h2&gt;性能区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内置类型（比如&lt;code&gt;int&lt;/code&gt;)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译器会优化，所以前置++和后置++性能无差别。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义类型（STL迭代器）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前置++操作的性能优于后置++&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;__normal_iterator&amp;amp; operator++(){  // 前置操作
    ++_M_current;
    return *this;
}

 __normal_iterator operator++(int){ // 后置操作
     return __normal_iterator(_M_current++); 
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面是STL中iterator的源码。从上面代码可以看出，迭代器的前置和后置操作主要有以下两个区别：
&lt;ul&gt;
&lt;li&gt;返回值：前置操作返回对象的引用，后置操作返回类型为对象，&lt;/li&gt;
&lt;li&gt;拷贝：前置操作无拷贝操作，后置操作存在一次对象拷贝&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;正因为这两个原因，前置操作符就地修改对象，而后置操作符将导致创建临时对象，调用构造函数和析构函数（某些情况下编译器会做优化，此处不做讨论），导致了前置操作和后置操作的性能差异。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他自定义类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前置++操作的性能优于后置++。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;编译过程&lt;/h1&gt;
&lt;p&gt;记住ISO即可&lt;/p&gt;
&lt;p&gt;.h/c/cpp通过 预处理器处理为.i的预处理代码&lt;/p&gt;
&lt;p&gt;.i的预处理代码通过编译器处理为.s的汇编代码&lt;/p&gt;
&lt;p&gt;.s的汇编代码通过汇编器处理为.o的目标代码（可重定位二进制）&lt;/p&gt;
&lt;p&gt;.o的目标代码通过链接器生成可执行文件.exe/.out （如果没有外部代码或者dll/lib库文件就直接生成可执行文件）&lt;/p&gt;
&lt;h1&gt;从cpp源代码到可执行程序经过了哪些过程?&lt;/h1&gt;
&lt;p&gt;A: 在Linux系统下从.cpp开始, 经过&lt;/p&gt;
&lt;p&gt;1.预处理阶段, 在这部分主要处理include的文件包含关系, 还有宏的替换, 做完.cpp变为.i
2.编译阶段会做语法分析还有优化之类的, 将代码转换为汇编, 做完.i变.s
3.汇编阶段会把前面得到的汇编代码转换为二进制的机器代码, 做完.s变.o
4.最后运行链接器将所有可重定位文件合并为一个可执行文件, 做完.o变prog. 对于可执行文件可以调用系统中被称为加载器的函数来将文件复制到内存并开始执行.&lt;/p&gt;
&lt;p&gt;其中自己了解较多的是最后的链接阶段. 链接器为了将输入的多个可重定位文件组合为一个可执行文件, 要做的事主要有两个, 一是符号解析, 二是重定位. 其中符号解析意思是说一个文件里会有很多对符号的定义和引用, 所谓符号就是全局变量,函数和静态变量这些. 然后符号解析就是将每个符号引用与一个符号定义关联起来. 而重定位做的事是把每个符号定义与一个内存地址关联起来, 然后将所有对这个符号的引用都指向这个地址.&lt;/p&gt;
&lt;p&gt;详细说符号解析的话需要先说链接器的输入, 也就是可重定位文件的文件格式. 虽然它是二进制文件, 但实际上是有各种分段的, 比如有个固定的头部, 描述了文件类型, 机器类型等等一些主要信息. 还有放机器代码的段, 再往下还有放全局变量的段等等. 而文件中会有一个放符号表的段, 其中存放了这个文件定义和引用的全局变量以及函数这些信息.&lt;/p&gt;
&lt;p&gt;符号表中有三类不同的符号主.分别是1.由当前文件定义的全局符号(定义的全局变量,函数等) 2.引用的其他文件定义的全局符号, 3.只被当前文件定义和引用的符号(static全局变量和函数). 每个符号条目并不是只有一个名字, 而是有一个固定的格式结构. 这个结构会标明符号名字, 大小, 类型(变量还是函数), 链接性(全局变量是GLOBAL, static全局变量大概会是LOCAL)等等.(之前看到过一个问题是说为什么static全局变量可以把作用域限制在本文件, 讲到这里就可以回答这个问题了. 因为static符号被单独分了一类, 链接器不会拿static符号去跟别的文件做解析.)&lt;/p&gt;
&lt;p&gt;符号解析就是利用这些符号表信息去做解析的. 对于那些定义和引用都在同一文件的符号解析很简单. 但是本文件引用了其他文件定义的符号就会复杂一些. 编译器会把这样的只有引用的符号交给链接器, 由链接器去其他文件找这个符号的定义, 如果找不到就会报错. 符号解析还有一个问题是重名问题, 编译器在编码符号的时候会给他一个强弱属性, 已初始化的全局变量就是强, 未初始化的就是弱. 然后如果重名, 链接器去选择的时候会优先选择强符号, 如果都是弱符号, 那就随便选一个. 但如果两个重名符号都是强那会直接报错.&lt;/p&gt;
&lt;p&gt;符号链接还可以跟静态库链接(静态库就是提前写好的一些函数文件打包为一个单独文件, 可以用它来当作链接器输入). 在链接静态库的时候, 链接器会维护三个列表: 1.可重定位文件的列表, 2.未解析的符号(有引用但还没定义的符号) 3.已定义的符号. 链接器会从左向右扫描文件然后用这三个表来做符号解析. 这其中要注意的问题是文件的顺序很重要, 顺序不对了解析会失败.&lt;/p&gt;
&lt;p&gt;上面就是符号解析的说明了, 接下来是重定位部分. 重定位要做的就是把文件合并一下,然后给每个指令和符号分配内存地址. 再然后就是把对符号的引用指向刚才分配好的地址.&lt;/p&gt;
&lt;p&gt;重定位做完就可以得到一个可执行的目标文件.文件由三个大部分组成: 1.只读代码段, 2.读写数据段, 3.不加载进内存的符号表和调试信息这些. 而将一个可执行文件加载到内存后所呈现的布局主要有四个大的部分. 从下到上分别是:1.只读代码段, 2.读写数据段(用来放全局变量, 细分为已初始化和未初始化的两部分), 3.堆, 4.栈. 然后在堆栈之间还有预留给动态库的内存映射区域. 最后在栈的上面还有用户不可见的内核内存.&lt;/p&gt;
&lt;h1&gt;整数相除&lt;/h1&gt;
&lt;p&gt;两个整数相除，无论用什么接都是整数。因为会自动窄化。所以想要让两个整数相除保留小数点，则必须转换成double或float再进行除法操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 3;
int b = 6;
double c = a/b;
// C = 0;
double a = 3;
double b = 6;
double c = a/b;
// C = 0.5
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;形参（parameter）和实参（argument）区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;形参 parameter是在函数头的参数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func(int parameter){
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们明显看到了parameter的位置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实参 argument是传递的参数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int argument = 10;
func(argument);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们明显看到了argument的位置&lt;/p&gt;
&lt;h1&gt;函数参数（parameter）的默认值的继承&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;c++的函数参数的默认值是不可以被继承的。表面上可以使用只是“借用”，但不是继承。同时会有奇怪调用。&lt;/strong&gt; 我们看下面的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
  virtual void f(int a = 7){
    cout &amp;lt;&amp;lt; a;
    cout &amp;lt;&amp;lt;&quot;A&quot; &amp;lt;&amp;lt; endl;
  }
};
struct B : public A {
  void f(int a){
    cout &amp;lt;&amp;lt; a;
    cout &amp;lt;&amp;lt; &quot;B&quot; &amp;lt;&amp;lt; endl;
  }
};
int main() {
    A* pa = new A;
    B* pb = new B;
    A* m = new B;

    pa-&amp;gt;f();    //7A
    pa-&amp;gt;f(10);  //10A

    pb-&amp;gt;f();    //error: no matching function for call to ‘B::f()’
    pb-&amp;gt;f(10);  //10B

    m-&amp;gt;f();     //7B
    m-&amp;gt;f(10);   //10B
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**虚函数调用中使用的默认参数（argument，参数的默认值）由对象的（指针或引用）的静态类型决定。 派生类中的重写的虚函数不会从被它重写的虚函数中获取默认参数。**来自&lt;a href=&quot;https://eel.is/c++draft/dcl.fct.default#10&quot;&gt;标准库文档&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表面上的参数默认值的继承其实是借用，而不是继承。&lt;code&gt;pb-&amp;gt;f()&lt;/code&gt;，参数的默认值从静态类型决定。所以此处参数的默认值应该是从&lt;code&gt;B&lt;/code&gt;类寻找。但是&lt;code&gt;B&lt;/code&gt;类重写的函数的参数没有默认值，导致没有合适的值放入函数调用。函数签名不匹配，无法调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**当父类虚函数和子类虚函数参数都有默认值，但是值不同的时候，如果产生了多态调用，则此时会忽略子类默认值，使用父类默认值，但是实际调用的是子类函数。**也就是在使用父类指针指向的对象调用虚函数时，函数会按实际指向的对象调用对应的成员函数（动态绑定的结果），而函数的默认参数依旧为父类对象中的默认参数（静态绑定的结果） 所以永远不要重新定义一个继承而来的默认参数值，因为默认参数值是静态绑定的，而虚函数——你应该重新定义的唯一的函数——是动态绑定的。effective C++ 37条&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;类(对象）内存模型：&lt;/h1&gt;
&lt;h2&gt;大小&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类内存大小有关的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;非静态数据成员（包括常量）。也就是常量会对类大小有影响。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚函数表指针（虚函数产生）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚基表指针（虚继承产生）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类内存大小无关的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成员函数&lt;/li&gt;
&lt;li&gt;静态成员函数&lt;/li&gt;
&lt;li&gt;静态数据成员&lt;/li&gt;
&lt;li&gt;静态常量数据成员&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空类大小为1，为了分配一个内存起始地址&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空类被继承后，会有空基类优化，所以空基类的占用大小会变成0。前提是当前继承空基类的类不是空类。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class x {};
class y:public x{
    int a;
};
//sizeof(x) = 1
//sizeof(y) = 4
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;类内如果含有空类的对象，则空类对象大小依旧会被算为1。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class x {};
class y:public x{
    x obj
    int a;
};
//sizeof(x) = 1
//sizeof(y) = 8
//基类对象 1 + int 4 + 补齐 = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果是链式空基类继承，则所有的空类大小都为1。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class x {};
class y:public x{};
class z:public y{};
//sizeof(x) = 1
//sizeof(y) = 1
//sizeof(z) = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;内存布局&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://meetingcpp.com/mcpp/slides/2023/Class%20Layout%20-%20meeting%20c++271911.pdf&quot;&gt;资料&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;内存对齐&lt;/h2&gt;
&lt;p&gt;C++类成员变量的内存分布是：按照声明顺序，&lt;strong&gt;从上到下&lt;/strong&gt;，按照&lt;strong&gt;内存对齐&lt;/strong&gt;的原则进行分布的。&lt;/p&gt;
&lt;p&gt;用预编译命令&lt;code&gt;#pragma pack(n)&lt;/code&gt; 或结构体名字前加 &lt;code&gt;alignas(n)&lt;/code&gt;用来指定对齐系数(单位)为n个字节。&lt;code&gt;n&lt;/code&gt;的取值范围为&lt;code&gt;1, 2, 4, 8, 16&lt;/code&gt;。gcc默认对齐系数是&lt;code&gt;4&lt;/code&gt;，msvc默认对齐系数是&lt;code&gt;8&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct alignas(4) EbaInfo {
    uint8_t eba_switch;
    uint8_t eba_state;
    uint8_t eba_direction;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;内存分配的顺序是按照声明的顺序。&lt;/li&gt;
&lt;li&gt;类中第一个成员的偏移量（offset）为&lt;code&gt;0&lt;/code&gt;，以后每个成员（该成员的数据类型长度为&lt;code&gt;k&lt;/code&gt;）相对于结构体首地址的offset为&lt;code&gt;min(k, s)&lt;/code&gt;的整数倍。&lt;/li&gt;
&lt;li&gt;如果一个类里有结构体成员，则结构体成员要从其内部最宽基本类型成员的整数倍地址开始存储。&lt;/li&gt;
&lt;li&gt;整体对齐规则：整个结构体的大小应是对齐单位&lt;code&gt;s&lt;/code&gt;的整数倍。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;检查结构体对齐单位可以使用&lt;code&gt;alignof()&lt;/code&gt;。 使用&lt;code&gt;g++ -fdump-lang-class *.cpp&lt;/code&gt;生成布局信息文件。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
  char a;
  int b;
  double c;
};

class B {
  char a;
  double b;
  int c;
};

int main() {
  cout &amp;lt;&amp;lt; sizeof(A) &amp;lt;&amp;lt; endl; //16
  cout &amp;lt;&amp;lt; sizeof(B) &amp;lt;&amp;lt; endl; //24
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A类的大小：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;char a&lt;/code&gt;占1个字节，偏移值为0，目前A类占1字节。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int b&lt;/code&gt;占4个字节，根据内存对齐原则的第二条，b起始位置的偏移量应该4的整数倍，所以b要在a后面空3个字节，然后放入，因此偏移值为4，目前为止A类占用8个字节（3个padding）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double c&lt;/code&gt;占8个字节，可以直接从b后的第一个字节开始放入，偏移值为8。此时A类占用16字节，符合内存对齐的第三条原则，整个类的大小（16）是对齐单位（8）的整数倍，因此不需要补齐。所以A类总共占用16字节。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同理，B类的大小：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;char a&lt;/code&gt;占1个字节，偏移值为0，目前B类占1字节。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double b&lt;/code&gt;占8个字节，根据第二条规则，b要在a后面空7个字节，然后开始放入，因此偏移值为8，目前为止B类占用16个字节（7个padding）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int c&lt;/code&gt;占4个字节，可以直接从b后的第一个字节开始放入，偏移值为16，此时B类占用20个字节。根据内存对齐的第三条原则，整个类的大小应该是对齐单位（8）的整数倍，因此需要补齐3个字节到24字节。所以B类总共占用24字节。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;带有虚函数的情况：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class D {
public:
  virtual void funA();

private:
  char a;
  int b;
  double c;
};

int main() {
    cout &amp;lt;&amp;lt; sizeof(D) &amp;lt;&amp;lt; endl; //24
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;vptr&lt;/code&gt;占8个字节，偏移值为0，目前D类占8字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char a&lt;/code&gt;占1个字节，偏移值为8，目前D类占9字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int b&lt;/code&gt;占4个字节，根据内存对齐原则的第二条，b起始位置的偏移量应该4的整数倍，所以b要在a后面空3个字节，然后放入，因此偏移值为12，目前为止D类占用16个字节（3个padding）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double c&lt;/code&gt;占8个字节，可以直接从b后的第一个字节开始放入，偏移值为16。此时D类占用24字节，符合内存对齐的第三条原则，整个类的大小（24）是对齐单位（8）的整数倍，因此不需要补齐。所以D类总共占用16字节。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;所以在某种极端情况，如这样&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class a{
    char a;
    int b;
    char c;
    int d;
    char e;
    int f;
    char g;
    int h;
};

class b{
    char a,b,c,d;
    int e,f,g,h;
};


int main() {
    cout &amp;lt;&amp;lt; sizeof(a) &amp;lt;&amp;lt; endl; 	//32
    cout &amp;lt;&amp;lt; sizeof(b) &amp;lt;&amp;lt; endl;	//20
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为根据规则，a的每一个char后都需要补3位为了满足每一个变量的起始偏移地址必须为该变量大小的整数倍。这里int是4，所以必须要补到4这样是1倍。所以每一个char都占据了一个int空间。所以整个class相当于是4*8 = 32。&lt;/p&gt;
&lt;p&gt;然而b的四个char连续放置。1是1的整数倍所以不需要补齐操作。所以只有4+4*4 = 20。&lt;/p&gt;
&lt;p&gt;由此可见，变量的声明顺序会极大地影响类的内存布局和类对象的大小。&lt;/p&gt;
&lt;h3&gt;出现在派生类中的 base class subobject 有其完整原样性&lt;/h3&gt;
&lt;p&gt;无论是否是多继承下，每一个继承的类，会先进行对齐，然后再进行下一个继承。&lt;/p&gt;
&lt;p&gt;假设类A是（8+1）9 + 补齐 = 16，类B也是9 + 补齐 = 16， 类C继承了类A和类B，所以类C是32  （多继承）&lt;/p&gt;
&lt;p&gt;假设类A是（4+1）5 +补齐 = 8，类B继承了类A但是类B自己也是5，则会变成类A的8 + （4+1）+ 补齐的3 = 16， 类C继承了类B然后自己是4，就是 16 + 4 = 20。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为C++ Standard 保证：“出现在派生类中的 base class subobject 有其完整原样性！” 子类会被放在父类的对齐空白字节之后，因为父类的完整性必须得以保证，父类的对齐空白字节也是父类的一部分，也是不可分割的。所以会保持继承-&amp;gt;对齐-&amp;gt;继承第二个-&amp;gt;对齐....&lt;/strong&gt; 深入探索C++对象模型&lt;/p&gt;
&lt;h3&gt;类内函数内声明的变量（局部变量）作用域是函数而不是类&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;所以类的函数内的变量不会算作类的大小。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    void funcs(){
        int val;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大小一定是1。&lt;/p&gt;
&lt;h1&gt;NRV优化&lt;/h1&gt;
&lt;p&gt;NRV优化一句话就是，在函数返回值是值传递的时候，按理来说需要有临时变量调用拷贝构造。编译器对这些事情有优化。&lt;/p&gt;
&lt;h2&gt;内置类型：&lt;/h2&gt;
&lt;p&gt;如&lt;code&gt;int&lt;/code&gt; &lt;code&gt;double&lt;/code&gt; ,指针等内置类型，使用eax寄存器直接储存，然后访问&lt;/p&gt;
&lt;h2&gt;自定义类型：&lt;/h2&gt;
&lt;p&gt;一句话：编译器会把函数返回值当成形参，然后直接写回。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void myfunc(solution&amp;amp; ret, solution&amp;amp; a, solution&amp;amp; b);

solution myfunc1(solution&amp;amp;a, solution&amp;amp; b);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的&lt;code&gt;myfunc&lt;/code&gt;和&lt;code&gt;myfunc1&lt;/code&gt;起到了同样作用，只不过我们人为写成两种形式，一种是返回值，一种是写回。编译器也是优化成了这样，把我们外部接着返回值的变量以**引用（这样才能写回）**方式当做函数形参，然后函数仅需写回即可。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;p&gt;原函数和调用代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vector add(Vector&amp;amp; a, Vector &amp;amp; b)
{
    Vector v;
    v.x = a.x + b.x;
    v.y = a.y + b.y;
    return v;
}
int main(){
    Vector a, b;
	Vector c = add(a, b);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器优化过的函数和调用代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void add(Vector&amp;amp; __result, Vector&amp;amp; a, Vector &amp;amp; b)
{
    __result.x = a.x + b.x;
    __result.y = a.y + b.y;
    return;
}
int main(){
    Vector a, b;
    Vector c;
    add(c, a, b);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://www.cnblogs.com/autosar/archive/2011/10/09/2204181.html&lt;/p&gt;
&lt;h1&gt;类的私有静态变量可以通过作用域访问运算符直接访问&lt;/h1&gt;
&lt;h1&gt;main函数结束也会为栈对象调用析构，但是不会为堆对象调用。&lt;/h1&gt;
&lt;h1&gt;类内函数内声明的变量（局部变量）作用域是函数而不是类&lt;/h1&gt;
&lt;p&gt;所以类的函数内的变量不会算作类的大小。&lt;/p&gt;
&lt;h1&gt;指针和引用的区别：&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;引用从来都不是所谓的“指针包装器”。它们的行为完全不同。是完全不同的两种数据类型。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以把引用看成一个const指针。因为是const指针所以必须定义时初始化，也不能更改指向。&lt;strong&gt;但是这个并不全对。比如&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;double p = 3.33;
int&amp;amp; ref = p; //错误
int&amp;amp;&amp;amp; ref = p;//可以。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们上面提到了。隐式类型转换会创建临时对象。这里就是调用了&lt;code&gt;int&lt;/code&gt;的形参为&lt;code&gt;double&lt;/code&gt;的构造函数创建了一个临时对象。临时对象是右值。右值没有地址。我们这里用的又是左值引用，自然会产生错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以我们注意到了，隐式转换的临时变量是右值。所以可以用右值引用去接。&lt;strong&gt;但是！！这时候这个ref并不是p的引用，而是一个临时变量的引用。ref此时代表的并不是p而是一个临时对象！&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指针是一个单独的实体，引用则是别名&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以指针有地址&lt;/li&gt;
&lt;li&gt;引用虽然（可能）也有地址（也分配内存），但是引用和引用所引用的变量共用一块内存空间。当然了，引用本身确实有自己的内存空间，和指针一样。但是经过编译器处理后，访问这块内存时将直接转而访问其指向的内存。因此在程序中无法读取到这块内存本身。（依靠实现）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用不可以赋值，所以引用只能在定义时初始化一次，之后不可更改。指针可以更改该并且可以把定义和初始化分开。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用不能为空，指针可以为空。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sizeof&lt;/code&gt;指针的大小是指针自己的大小。&lt;code&gt;sizeof&lt;/code&gt;引用是对象的大小。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有引用的引用，没有引用数组，也没有指向引用的指针。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下面我们的&lt;code&gt;b&lt;/code&gt;和&lt;code&gt;c&lt;/code&gt;都是&lt;code&gt;a&lt;/code&gt;的引用。&lt;code&gt;c&lt;/code&gt;并不是&lt;code&gt;b&lt;/code&gt;的引用。因为没有引用的引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int a = 10;
    int &amp;amp;b = a;
    int &amp;amp;c = b;
    c = 20;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; b &amp;lt;&amp;lt; c &amp;lt;&amp;lt;endl; //20 20 20 
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;指针有自增操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;引用不可以重新赋值（不能更改指向）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int a = 10;
    int b = 20;
    int c = 30;
    int&amp;amp; aref = a;
    cout &amp;lt;&amp;lt; aref &amp;lt;&amp;lt; endl; //10
    aref = c; //注意这里！
    cout &amp;lt;&amp;lt; aref &amp;lt;&amp;lt; endl; //30
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; //30
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们看到&lt;code&gt;aref&lt;/code&gt;是&lt;code&gt;a&lt;/code&gt;的引用，也就是它俩是一个东西。执行&lt;code&gt;aref = c&lt;/code&gt;的时候，并不是把&lt;code&gt;aref&lt;/code&gt;变成&lt;code&gt;c&lt;/code&gt;的引用，而是把&lt;code&gt;c&lt;/code&gt;的值赋给&lt;code&gt;aref&lt;/code&gt;也就是赋给&lt;code&gt;a&lt;/code&gt;的引用。所以最后&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;aref&lt;/code&gt;都会变成30。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用一个引用给一个变量赋值，则会调用其拷贝构造构造出一个新的变量。因为引用的行为和普通变量一样&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a = 10;
int&amp;amp; b = a;
int c = b; //在这里a会被拷贝然后给c。也就是c会被拷贝构造。因为如果这里换成int c = a,也是会被拷贝构造。引用就相当于变量的别名。所以这里引用和普通变量行为一样
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;自然而然的，函数的值传递和引用传递可以解释通了&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func(int&amp;amp; a);
void func1(int a);

int var = 100;
int&amp;amp; ref = var;

func(var);
func(ref);
func1(var);
func1(ref);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个调用：标准的引用传递。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量&lt;code&gt;var&lt;/code&gt;传递进函数&lt;code&gt;func&lt;/code&gt;的时候，函数内部会创建一个叫&lt;code&gt;a&lt;/code&gt;的引用，引用的对象是外部的&lt;code&gt;var&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二个调用：引用的引用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用&lt;code&gt;ref&lt;/code&gt;传递进函数&lt;code&gt;func&lt;/code&gt;的时候，函数内部会创建一个叫&lt;code&gt;a&lt;/code&gt;的引用，引用的对象是引用&lt;code&gt;ref&lt;/code&gt;的引用对象也就是&lt;code&gt;var&lt;/code&gt;。因为没有引用的引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三个调用：标准的值传递。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量&lt;code&gt;var&lt;/code&gt;传递进函数&lt;code&gt;func1&lt;/code&gt;的时候，函数内部会创建一个叫&lt;code&gt;a&lt;/code&gt;的变量，变量拷贝构造自外部的&lt;code&gt;var&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第四个调用：也是值传递。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用&lt;code&gt;ref&lt;/code&gt;传递进函数&lt;code&gt;func1&lt;/code&gt;的时候，函数内部会创建一个叫&lt;code&gt;a&lt;/code&gt;的变量。我们刚提到了，使用引用初始化一个普通变量会调用拷贝构造，拷贝自引用的引用对象。所以这里&lt;code&gt;a&lt;/code&gt;的变量是拷贝自引用&lt;code&gt;ref&lt;/code&gt;所引用的对象，也就是&lt;code&gt;var&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;移动一个元素的引用就等同于移动一个元素本身&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    myobj a(200);
    myobj&amp;amp; b = a; //b是a的引用
    myobj c = move(b); //这里移动b和移动a效果相同
    cout &amp;lt;&amp;lt; *a.val &amp;lt;&amp;lt; endl; //无论访问a还是b都是无效。因为移动b等同于移动a。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;引用的可能实现&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;C++ 标准只说明它应该如何表现，而不是它应该如何实现。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一般来说编译器会把引用实现为指针。并采用特殊优化。比如&lt;code&gt;int&amp;amp; i = j&lt;/code&gt;。编译器在编译的时候会把所有遇到&lt;code&gt;i&lt;/code&gt;的地方替换为&lt;code&gt;j&lt;/code&gt;的内存地址。也就是在符号表内添加类似注释的东西通知&lt;code&gt;i&lt;/code&gt;是&lt;code&gt;j&lt;/code&gt;的别名。所以在底层，引用确实可能会被分配内存，也可能不分配。&lt;/p&gt;
&lt;h3&gt;内置类型使用引用传递不会提高性能&lt;/h3&gt;
&lt;p&gt;这之所以不能提高性能，&lt;strong&gt;是因为在底层实现上，按引用传递还是通过传递参数的地址实现的。地址会被简单编码，这样可以提高从调用者向被调用者传递地址的效率。不过按地址传递可能会使编译器在编译调用者的代码时有一些困惑：被调用者会怎么处理这个地址？理&lt;/strong&gt;
**论上被调用者可以随意更改该地址指向的内容。这样编译器就要假设在这次调用之后，所有缓存在寄存器中的值可能都会变为无效。**而重新载入这些变量的值可能会很耗时（可能比拷贝对象的成本高很多）。你或许会问在按 const 引用传递参数时：为什么编译器不能推断出
被调用者不会改变参数的值？不幸的是，确实不能，&lt;strong&gt;因为调用者可能会通过它自己的非 const引用修改被引用对象的值（这个解释太好，另一种情况是被调用者可以通过 const_cast 移除参数中的 const）&lt;/strong&gt;。----C++模板第二版 7.2.1&lt;/p&gt;
&lt;h1&gt;继承权限和访问权限&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-%E6%9D%82%E8%AE%B02.assets/QQ%E6%88%AA%E5%9B%BE20220828052432.png&quot; alt=&quot;QQ截图20220828052432&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;默认继承方式&lt;/h1&gt;
&lt;p&gt;继承的默认类型取决于继承（派生）类型，而不是正在继承的类型（基类）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;struct，默认为public&lt;/li&gt;
&lt;li&gt;class，默认为private&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A {};
struct B: /* public */ A {}; //派生类型是struct，默认为public

struct A {};
class B: /* private */ A {}; //派生类型是class，默认为private
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;函数指针和类成员函数指针&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;//静态的成员函数指针语法:
void (*ptrStaticFun)() = &amp;amp;ClassName::staticFun;
//成员函数指针语法:
void (ClassName::*ptrNonStaticFun)() = &amp;amp;ClassName::nonStaticFun; //必须加作用域
&amp;lt;返回值类型&amp;gt;(&amp;lt;类名&amp;gt;::*&amp;lt;指针名称&amp;gt;)(&amp;lt;参数列表&amp;gt;) = &amp;amp;&amp;lt;类名&amp;gt;::&amp;lt;非静态成员函数名称&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来通过具体例子来说明其中的坑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

void normal_func(){
    cout &amp;lt;&amp;lt; &quot;func&quot; &amp;lt;&amp;lt;endl;
}

void call_back_test(int&amp;amp; a, int&amp;amp; b){
    cout &amp;lt;&amp;lt; a + b &amp;lt;&amp;lt; endl;
}


class test{
    public:
        int val = 5;
        void class_nonstatic_func(){
            cout &amp;lt;&amp;lt; &quot;class_nonstatic_func&quot; &amp;lt;&amp;lt;endl;
        }
        static void class_static_func(){
            cout &amp;lt;&amp;lt; &quot;class_static_func&quot; &amp;lt;&amp;lt;endl;
        }
        void callback(void(*ptr)(int&amp;amp;, int&amp;amp;), int&amp;amp; a, int&amp;amp; b){
            ptr(a, b); //普通函数调用
            void(*normal_funcptr)() = &amp;amp;normal_func; //类内获取全局函数指针。注意全局函数必须写在这一行的前面。
            normal_funcptr(); //调用 语法糖
            (*normal_funcptr)(); //调用


            void(test::*class_nonstatic_func_ptr)() = &amp;amp;test::class_nonstatic_func; //类内获取类非静态成员函数指针
            (this-&amp;gt;*class_nonstatic_func_ptr)(); //调用。调用类成员函数指针必须加*解引用。而且需要绑定对象。类内就是用this。

            void(*class_static_func_ptr)() = &amp;amp;test::class_static_func; //类内获取类非静态成员函数指针
            class_static_func_ptr();    //调用 语法糖
            (*class_static_func_ptr)(); //调用


        }
};


int main(){
    void (*normal_funcptr)() = &amp;amp;normal_func;          //普通函数指针
    void (*normal_funcptr1)() = normal_func;          //也可以不取地址。语法糖。

    normal_funcptr(); //调用方式，不解引用也可以，语法糖
    normal_funcptr1();

    (*normal_funcptr)();    //解引用，正确方式。应该这么写，更加清晰。
    (*normal_funcptr1)();

    test classobj;
    test* classobj_ptr = new test();
    void (test::*class_nonstatic_func_ptr)() = &amp;amp;test::class_nonstatic_func; //获取类非静态成员函数指针，必须取地址
    //获取类获取类非静态成员函数指针 必须在指针名字前面加类作用域，用来辨别。
    (classobj.*class_nonstatic_func_ptr)(); //对象调用。调用类成员函数指针必须加*解引用。而且需要绑定对象（通过对象调用）。
    (classobj_ptr-&amp;gt;*class_nonstatic_func_ptr)(); //指针调用。调用类成员函数指针必须加*解引用。而且需要绑定对象（通过对象调用）



    void(*class_static_func_ptr)() = &amp;amp;test::class_static_func; //获取类静态成员函数指针 ，必须取地址
    //获取类获取类静态成员函数指针 不需要指针名字前面加类作用域。因为静态成员函数可以理解为在类作用域内的全局函数。
    //所以取地址还要加作用域，函数名字不需要。
    class_static_func_ptr(); //直接调用。语法糖
    (*class_static_func_ptr)(); //直接调用。解引用

    //!(classobj.*class_static_func_ptr)();     //对象调用错误
    //!(classobj_ptr-&amp;gt;*class_static_func_ptr)();//指针调用错误


    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;声明&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通函数指针
&lt;ul&gt;
&lt;li&gt;直接取名字即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类成员函数指针
&lt;ul&gt;
&lt;li&gt;必须在指针名字前面加类作用域，用来辨别。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类静态成员函数指针
&lt;ul&gt;
&lt;li&gt;和普通函数指针一样，不需要加作用域。因为静态成员函数可以理解为在类作用域内的全局函数。（所以赋值依旧需要取地址+作用域）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;赋值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通函数指针
&lt;ul&gt;
&lt;li&gt;可以直接用函数名赋值。语法糖。&lt;/li&gt;
&lt;li&gt;也可以取地址&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类成员函数指针和类静态成员函数指针
&lt;ul&gt;
&lt;li&gt;赋值必须要加作用域解析运算符并且取地址&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通函数指针
&lt;ul&gt;
&lt;li&gt;直接调用。语法糖&lt;/li&gt;
&lt;li&gt;解引用调用。注意解引用必须加括号。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类成员函数指针
&lt;ul&gt;
&lt;li&gt;不可以直接调用，必须解引用。同时必须绑定对象使用对象调用或对象指针调用&lt;/li&gt;
&lt;li&gt;类内必须使用this用来充当绑定对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类静态成员函数指针
&lt;ul&gt;
&lt;li&gt;和普通函数指针一样。&lt;/li&gt;
&lt;li&gt;直接调用。语法糖&lt;/li&gt;
&lt;li&gt;解引用调用。注意解引用必须加括号。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;成员函数模板 函数指针和类模板 成员函数指针&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt; //类模板
class myclass{
    public:
    void func(){
        cout &amp;lt;&amp;lt; &quot;class func&quot; &amp;lt;&amp;lt; endl;
    }
    int func2(int a){
        cout &amp;lt;&amp;lt; &quot;class func2&quot; &amp;lt;&amp;lt; endl;
        return a;
    }
};

class nclass{
    public:
        template &amp;lt;typename T&amp;gt;
        void func(T a){
            cout &amp;lt;&amp;lt;&quot;nclass func&quot; &amp;lt;&amp;lt; endl;
            cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
        }
        template &amp;lt;typename RT, typename T&amp;gt; //函数模板
        RT func2(T a){
            cout &amp;lt;&amp;lt;&quot;nclass func&quot; &amp;lt;&amp;lt; endl;
            cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
            return a;
        }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于模板本身没有地址（模板中详细介绍过），所以必须要依托其实例化后的东西才可以。所以需要显式指明模板参数。&lt;/li&gt;
&lt;li&gt;类模板成员函数指针
&lt;ul&gt;
&lt;li&gt;左侧指针声明的时候，不仅要依照类成员函数指针语法，同时要显式指明类模板参数。&lt;/li&gt;
&lt;li&gt;右侧进行取地址的时候，也必须要指定类模板参数。因为不指定参数不会引发实例化，无法取地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myclass&amp;lt;int&amp;gt; myclassobj; //实例化的对象

void(myclass&amp;lt;int&amp;gt;::*myptr)() = &amp;amp;myclass&amp;lt;int&amp;gt;::func; 
(myclassobj.*myptr)();

int(myclass&amp;lt;int&amp;gt;::*myptr2)(int) = &amp;amp;myclass&amp;lt;int&amp;gt;::func2;
(myclassobj.*myptr2)(4);

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;成员函数模板函数指针
&lt;ul&gt;
&lt;li&gt;左侧指针声明的时候，由于此时不涉及类模板，则语法和普通类成员函数指针一致&lt;/li&gt;
&lt;li&gt;右侧取地址的时候，由于函数模板可以自动推导类型。所以不需要显式指明模板参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nclass nclassobj; //实例化的对象

void(nclass::*nptr)(int) = &amp;amp;nclass::func;
(nclassobj.*nptr)(234);

float(nclass::*nptr2)(int) = &amp;amp;nclass::func2;
(nclassobj.*nptr2)(2343);

float(nclass::*nptr_noneed)(int) = &amp;amp;nclass::func2&amp;lt;float, int&amp;gt;; //不是必须显式指明模板参数。
(nclassobj.*nptr_noneed)(2343);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;类成员（变量）指针&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类成员变量指针与普通意义上的指针不一样。存放的是类内偏移量&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指向成员变量的指针并不指向某一个具体对象的内存地址。它指向的是一个类的特定成员，而不是指定某一个特定对象的成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指向类成员变量的指针”的实现方式是获取该类成员在类中的偏移值，这也同样印证了指向类成员变量的指针不可以单独访问 (依赖于某一特定对象) 的原因 —— 它只是偏移值，需要通过特定的对象来访问该对象此偏移值处的子对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在我们对一个 “指向类内数据成员的指针” 赋予初始值，实际上是获得了该数据成员在类内的偏移量。除非是对一个类内的 static 数据成员进行 &lt;code&gt;&amp;amp;&lt;/code&gt; 操作，否则不会带来一个实际的地址，而是一个偏移量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//静态的成员变量指针语法:
int *ptrStaticitem = &amp;amp;ClassName::Staticitem
&amp;lt;成员变量数据类型&amp;gt;*&amp;lt;指针名&amp;gt; = &amp;amp;&amp;lt;类名&amp;gt;::&amp;lt;静态数据成员名称&amp;gt;
//成员变量指针语法:
int ClassName::*ptrNonStaticitem = &amp;amp;ClassName::nonStaticitem
&amp;lt;成员变量数据类型&amp;gt;&amp;lt;类名&amp;gt;::*&amp;lt;指针名&amp;gt; = &amp;amp;&amp;lt;类名&amp;gt;::&amp;lt;非静态数据成员名称&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class person{
    public:
        int id = 20;
        static int info;
};
int person::info = 10;

int main()
{
    person human;
    person* human_ptr = new person();
    int person::*nonstatic_ptr = &amp;amp;person::id; //非静态成员变量指针
    cout &amp;lt;&amp;lt; human.*nonstatic_ptr &amp;lt;&amp;lt; endl; //必须对指针解引用后通过对象或指针访问
    cout &amp;lt;&amp;lt; human_ptr-&amp;gt;*nonstatic_ptr &amp;lt;&amp;lt; endl; //指针解引用访问。

    int* static_ptr = &amp;amp;person::info; //静态成员变量指针
    cout &amp;lt;&amp;lt; *static_ptr &amp;lt;&amp;lt; endl; //解引用
    cout &amp;lt;&amp;lt; person::info &amp;lt;&amp;lt; endl;//直接通过作用域运算符访问类静态成员变量。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;非静态成员变量：
&lt;ul&gt;
&lt;li&gt;和非静态成员函数相似。不可以直接使用，必须解引用后绑定对象使用对象调用或对象指针调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态成员变量：
&lt;ul&gt;
&lt;li&gt;和静态成员函数相似。可以直接使用，不必绑定对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://imzlp.com/posts/27615/&lt;/p&gt;
&lt;h1&gt;类成员（变量）指针和类成员函数指针的具体区分方式在模板的结尾。但是记住一点，类成员函数指针是类成员（变量）指针的子集。&lt;/h1&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/659510753&lt;/p&gt;
&lt;h1&gt;为构造函数提供默认值&lt;/h1&gt;
&lt;p&gt;两种方式。第一种是用构造函数初始化列的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
        test(int x = 5, int y = 10):_x(x), _y(y){};
        int _x, _y;
};
int main(){
    test t;
    cout &amp;lt;&amp;lt; t._x &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种是常规方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
        test(int x = 5, int y = 10){
            _x = x; 
            _y = y;
        }
        int _x, _y;
};
int main(){
    test t;
    cout &amp;lt;&amp;lt; t._x &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;指针名称解读&lt;/h1&gt;
&lt;h2&gt;顺序系列&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int* p[10] &lt;/code&gt;意思是&lt;code&gt;p&lt;/code&gt;是一个大小为&lt;code&gt;10&lt;/code&gt;的储存&lt;code&gt;int*&lt;/code&gt;类型指针的数组 。
&lt;ul&gt;
&lt;li&gt;它是一个指针数组&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int (*p)[10]&lt;/code&gt;意思是有一个指针&lt;code&gt;p&lt;/code&gt;指向一个大小为&lt;code&gt;10&lt;/code&gt;的储存&lt;code&gt;int&lt;/code&gt;类型的数组。
&lt;ul&gt;
&lt;li&gt;它是一个指向数组的指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int* p(int) &lt;/code&gt;是一个名为&lt;code&gt;p&lt;/code&gt;的函数，这个函数返回&lt;code&gt;int*&lt;/code&gt;，入参是&lt;code&gt;int &lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;它是一个函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int (*p)(int)&lt;/code&gt;是一个名为&lt;code&gt;p&lt;/code&gt;的函数指针，这个函数指针指向的函数返回值为&lt;code&gt;int&lt;/code&gt;，入参为&lt;code&gt;int&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;它是一个函数指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;const系列 const关键字总是优先作用于左侧的东西，除非左侧没东西。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const int *ptr; = int const *ptr; = const int* ptr; = int const* ptr&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;把&lt;code&gt;*&lt;/code&gt;念成指向, 这句话翻译过来就是：指针&lt;code&gt;ptr&lt;/code&gt;指向&lt;code&gt;const int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;这是一个常量指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int* const ptr&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;把&lt;code&gt;*&lt;/code&gt;念成指向 这句话翻译过来就是：&lt;code&gt;const&lt;/code&gt;指针&lt;code&gt;ptr&lt;/code&gt; 指向&lt;code&gt;int&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;``const int* const ptr`
&lt;ul&gt;
&lt;li&gt;把&lt;code&gt;*&lt;/code&gt;念成指向 这句话翻译过来就是：&lt;code&gt;const&lt;/code&gt;指针&lt;code&gt;ptr&lt;/code&gt;指向&lt;code&gt;const int&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;指针计算&lt;/h1&gt;
&lt;h2&gt;指针A给指针B赋值的意思是将指针A指向的地址赋给指针B&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int digit = 5;
int* a = &amp;amp;digit;
int* b = a; //指针a指向的地址赋给指针b 没有新对象产生所以不会调用拷贝构造。
int* c = &amp;amp;*a; //等同于将指针a先解引用（*a = 5），得到指针a指向的的值。然后把这个值取地址(&amp;amp;*a)赋值给指针c。

cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl; //打印b储存的地址。也就是b指向的地址。 等同于&amp;amp;digit和*&amp;amp;b
cout &amp;lt;&amp;lt; *b &amp;lt;&amp;lt; endl; //解引用b 打印b指向的地址的值。
cout &amp;lt;&amp;lt; &amp;amp;b &amp;lt;&amp;lt; endl; //打印b自己的地址。
cout &amp;lt;&amp;lt; &amp;amp;digit &amp;lt;&amp;lt; endl; //打印变量地址。

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 5;
int* ptr = &amp;amp;a;

cout &amp;lt;&amp;lt; &amp;amp;a &amp;lt;&amp;lt;endl;      //打印a的地址。0x61fe1c
cout &amp;lt;&amp;lt;ptr &amp;lt;&amp;lt; endl;     //打印指针ptr储存的值，也就是a的地址。0x61fe1c
cout &amp;lt;&amp;lt;&amp;amp;ptr &amp;lt;&amp;lt; endl;    //打印指针ptr自己的地址。0x61fe10
cout &amp;lt;&amp;lt;*&amp;amp;ptr &amp;lt;&amp;lt; endl;   //打印指针ptr储存的值。也就是a的地址。0x61fe1c
// *&amp;amp;ptr = ptr = &amp;amp;a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-%E6%9D%82%E8%AE%B02.assets/QQ%E6%88%AA%E5%9B%BE20220905085526.png&quot; alt=&quot;QQ截图20220905085526&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;指针加法&lt;/h2&gt;
&lt;h3&gt;指针直接和数字相加：意思是指针当前指向的地址 + 对应指针类型的大小 * 数字。&lt;/h3&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int * p;
p = (int*)malloc(100); //分配100字节。
cout &amp;lt;&amp;lt; p&amp;lt;&amp;lt; endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。此处输出 0x771410
auto y = p + 1; //把p指向的地址偏移4位（因为int是4字节）后赋值给y。
printf(&quot;%#x&quot;,y); //打印y储存的地址。也就是指向的地址。此处输出 0x771414
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;也就是说指针直接和数字相加，可以理解为用指针操作数组。加几就是将指针往后移动几位。具体偏移量也就是对应对象的大小*移动位数。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;如果想要指定一个具体的偏移量应该怎么办？&lt;/h3&gt;
&lt;p&gt;首先，可以使用转型将指针转换为对应类型。然后再加数字。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj* p;
p = (obj*)malloc(sizeof(int) * 10);
cout &amp;lt;&amp;lt; p&amp;lt;&amp;lt; endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。 此处输出 0x771410
auto x = (char*)p + 1; //把p指向的地址偏移1位后赋值给x。因为char是1位。
printf(&quot;%#x\n&quot;,x); //打印x储存的地址。也就是指向的地址。此处输出 0x771411
auto y = (obj*)p + 1; //把p指向的地址偏移8位后赋值给x。因为obj是指针类型。8位。
printf(&quot;%#x\n&quot;,y); //打印y储存的地址。也就是指向的地址。此处输出 0x771418
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：指针类型转换指的是指针转换为对应类型的指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此处不要使用&lt;code&gt;cout&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;auto x = (char*)p&lt;/code&gt;的意思是把指针&lt;code&gt;p&lt;/code&gt;转为&lt;code&gt;char*&lt;/code&gt;类型的指针。不可以&lt;code&gt;auto x = (char)p&lt;/code&gt;。 这样做的意思是直接转换成了&lt;code&gt;char&lt;/code&gt;。当然，所有的指针都是8字节。记住，指针类型指的是告诉指针应该读取指针指向的地址之后多大的数据。&lt;/p&gt;
&lt;p&gt;怎么理解这个操作呢。我们可以想一下。如果 &lt;code&gt;a&lt;/code&gt;是&lt;code&gt;int*&lt;/code&gt;类型，也就是&lt;code&gt;int&lt;/code&gt;类型指针。意思是指针&lt;code&gt;a&lt;/code&gt;指向的数据是&lt;code&gt;int&lt;/code&gt;，所以需要以&lt;code&gt;4&lt;/code&gt;字节为一段进行读取。也就是一次读取四个字节。那么&lt;code&gt;a+1&lt;/code&gt;其实是对地址进行了&lt;code&gt;+4&lt;/code&gt;操作。因为指针+1的意思是将指针往后移动一个&lt;strong&gt;对象位。也就是指针当前指向的地址 + 对应指针类型的大小（此处为4） * 数量（此处为1）&lt;/strong&gt;。但是我们如果想要对地址&lt;code&gt;+1&lt;/code&gt;的话怎么办？我们可以把指针&lt;code&gt;a&lt;/code&gt;转换为&lt;code&gt;char*&lt;/code&gt;类型也就是&lt;code&gt;char&lt;/code&gt;类型指针。而不是&lt;code&gt;int&lt;/code&gt;类型指针。这样相当于告诉编译器，指针&lt;code&gt;a&lt;/code&gt;指向的数据是&lt;code&gt;char&lt;/code&gt;。所以需要以1字节为一段进行读取。也就是一次读取一个字节。所以此时&lt;code&gt;a+1&lt;/code&gt;就是对地址进行了&lt;code&gt;+1&lt;/code&gt;操作。&lt;strong&gt;也就是指针当前指向的地址 + 对应指针类型的大小（此处为1） * 数量（此处为1）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;移动一个对象位也就是移动的偏移量为对象大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int digit = 5;
int* a = &amp;amp;digit;
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
auto x = (char*)a + 1;
printf(&quot;%#x\n&quot;,x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意！&lt;code&gt;(char*)a + 1&lt;/code&gt;往后移动1位的时候，&lt;code&gt;(char*)a&lt;/code&gt;的意思是把指针a看成指向&lt;code&gt;char&lt;/code&gt;类型的指针（告诉编译器一段是1）。所以&lt;code&gt;(char*)a + 1&lt;/code&gt;不是+2，还是+1。因为指针&lt;code&gt;a&lt;/code&gt;指向的地址根本没变。仅仅是看成了&lt;code&gt;char&lt;/code&gt;类型的指针后+1&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;成员函数的引用限定符&lt;/h1&gt;
&lt;p&gt;假如有些情况下，我们想限制某些成员函数只能被左值对象调用，或者只能被右值对象调用，C++11中引入了引用限定符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在函数声明后面加上&amp;amp;符号，表示该方法只能被左值对象调用&lt;/li&gt;
&lt;li&gt;在函数声明后面加上&amp;amp;&amp;amp;符号，表示该方法只能被右值对象调用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        // void func(int a, int b){
        //     cout &amp;lt;&amp;lt; &quot;normal func&quot; &amp;lt;&amp;lt; endl;
        // }
        void func(int a, int b) &amp;amp;{
            cout &amp;lt;&amp;lt; &quot;lref func&quot; &amp;lt;&amp;lt; endl;
        }
        void func(int a, int b) &amp;amp;&amp;amp;{
            cout &amp;lt;&amp;lt; &quot;rref func&quot; &amp;lt;&amp;lt; endl;
        }
};

int main(){
    int a = 10;
    int b = 100;
    myobj obj;
    obj.func(a, b); //左值对象 输出 lref func
    move(obj).func(a, b); //通过move把左值变为右值，此时是右值对象 输出rref func
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意事项：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用限定符只能用在成员函数上，而且不能用于&lt;code&gt;static&lt;/code&gt;函数。（this指针原因）&lt;/li&gt;
&lt;li&gt;带有一个&lt;code&gt;&amp;amp;&lt;/code&gt;和不带&lt;code&gt;&amp;amp;&lt;/code&gt;的函数只能保留一个。因为他们的含义有二义性。&lt;/li&gt;
&lt;li&gt;和&lt;code&gt;const&lt;/code&gt;搭配使用的时候，&lt;code&gt;const&lt;/code&gt;限定符必须写在引用限定符之前&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func(int a, int b) const &amp;amp;{ //可以
    cout &amp;lt;&amp;lt; &quot;lref func&quot; &amp;lt;&amp;lt; endl;
}
void func(int a, int b) &amp;amp; const{ //不可以
    cout &amp;lt;&amp;lt; &quot;lref func&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;引用限定符和const搭配的注意事项&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct test{
    void f() const &amp;amp;{ // 注意const
        std::cout &amp;lt;&amp;lt; &quot;lvalue&quot; &amp;lt;&amp;lt; std::endl;
    }
    void f() &amp;amp;&amp;amp;{
        std::cout &amp;lt;&amp;lt; &quot;rvalue&quot; &amp;lt;&amp;lt; std::endl;
    }
};
int main() {
    test a;
    a.f(); // lvalue
    test{}.f(); // rvalue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切正常。如果我们去掉右值版本的呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct test{
    void f() const &amp;amp;{ // 注意const
        std::cout &amp;lt;&amp;lt; &quot;lvalue&quot; &amp;lt;&amp;lt; std::endl;
    }

};
int main() {
    test a;
    a.f(); // lvalue
    test{}.f(); // lvalue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过编译，我们发现临时对象调用的也输出lvalue。&lt;/p&gt;
&lt;p&gt;但是如果我们去掉&lt;code&gt;const&lt;/code&gt;且去掉右值版本的呢&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct test{
    void f() &amp;amp;{ //无const
        std::cout &amp;lt;&amp;lt; &quot;lvalue&quot; &amp;lt;&amp;lt; std::endl;
    }
    // void f() &amp;amp;&amp;amp;{
    //     std::cout &amp;lt;&amp;lt; &quot;rvalue&quot; &amp;lt;&amp;lt; std::endl;
    // }
};
int main() {
    test a;
    a.f();
    test{}.f(); // 错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们需要把后面的&lt;code&gt;const&lt;/code&gt;和引用限定符的组合放在一起看。这一组修饰是限定了&lt;code&gt;this&lt;/code&gt;指针，和&lt;code&gt;const&lt;/code&gt;一样。&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;const&amp;amp;&lt;/code&gt;可以接一切，所以此时临时对象也可以调用这个&lt;code&gt;const&amp;amp;&lt;/code&gt;修饰的版本。如果不加&lt;code&gt;const&lt;/code&gt;，则不行。&lt;/p&gt;
</content:encoded></item><item><title>Effective Modern C++ 笔记</title><link>https://sterben-01.github.io/posts/EffectiveMC++/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/EffectiveMC++/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;Effective Modern C++ 笔记&lt;/h1&gt;
&lt;h1&gt;条款18需要显式所有权的资源管理时，用&lt;code&gt;std::unique_ptr&lt;/code&gt;&lt;/h1&gt;
&lt;h1&gt;条款26 避免重载万能引用&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;2023.2.21：直接看模板的6.2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一次看书给我看乐了。核心就一句话。根据C++的重载决议规则，万能引用版本总会被优先匹配。万能引用很jb贪。。它们会在具现过程中，和几乎任何实参型别都会产生精确匹配。而且在重载过程当中，万能引用模板还会和构造函数拷贝构造函数竞争。例子不写了，看书吧。反正别重载万能引用就行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;要点:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把万能引用作为重载候选型别，几乎总会让该重载版本在始料未及的情况下被调用到&lt;/li&gt;
&lt;li&gt;（完美转发）构造函数(使用了万能引用）的问题尤其严重，因为对于非常量的左值型别而言，它们一般都会形成相对于复制构造函数的更佳匹配，并且它们还会劫持子类中对父类的复制和移动构造函数的调用&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;条款27：26的解决方案&lt;/h1&gt;
&lt;h2&gt;1. 使用常量左值引用做为形参（&lt;code&gt;const &amp;amp;&lt;/code&gt;)&lt;/h2&gt;
&lt;p&gt;常量左值引用可以接受任意类型的参数。（常量左值，左值，常量右值，右值）。虽然效率低一些，但是可以正确使用。&lt;/p&gt;
&lt;h2&gt;2. 传值&lt;/h2&gt;
&lt;p&gt;有些人忽略一点。以值传递是可以接受右值的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void num(int a){
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
}
int main(){
    int digit = 5;
    num(digit);	//	OK 
    num(800); 	//	OK
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person {
public:
    explicit Person(std::string n) //值传递
    : name(std::move(n)) {}

    explicit Person(int idx)
    : name(nameFromIdx(idx)) {}
    ...
private:
    std::string name;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有效率损失的原因：如果实参是左值，那么实参到形参是一次复制，形参到&lt;code&gt;name&lt;/code&gt;是一次移动，相比普适引用只多了一次移动；如果实参是右值，那么实参到形参是一次移动，形参到&lt;code&gt;name&lt;/code&gt;还是一次移动，相比普适引用还是只多一次移动，可以认为没有效率损失。&lt;/p&gt;
&lt;h2&gt;3.使用标签。&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;2023.2.21：直接看模板的标记派发。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里很有意思。侯捷老师讲STL的时候提到过，当时没有理解。现在有点理解了。&lt;/p&gt;
&lt;p&gt;我们重新实现&lt;code&gt; logAndAdd&lt;/code&gt;把它委托给另外两个函数，&lt;strong&gt;一个接受整型值，另一个接受其他所有型别&lt;/strong&gt;。而&lt;code&gt; logAndAdd&lt;/code&gt; 本身则接受所有型别的实参，无论整型和非整型都来者不拒。&lt;/p&gt;
&lt;p&gt;改动前的原始版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::multiset&amp;lt;std::string&amp;gt; names;
template &amp;lt;typename T&amp;gt;
void logAndAdd(T&amp;amp;&amp;amp; name) {
    names.emplace(std::forward&amp;lt;T&amp;gt;(name)); //具体代码细节无须在意
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接近实现正确的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void logAndAdd(T&amp;amp;&amp;amp; name) {
    logAndAddImpl(std::forward&amp;lt;T&amp;gt;(name), 
                  std::is_integral&amp;lt;T&amp;gt;()//检查T的类型是否为整型
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的问题是：当实参是左值时，&lt;code&gt;T&lt;/code&gt;会被推导为左值引用，即如果实参类型是&lt;code&gt;int&lt;/code&gt;，那么&lt;code&gt;T&lt;/code&gt;就是&lt;code&gt;int&amp;amp;&lt;/code&gt;，（杂记的完美转发推导有写）&lt;code&gt;std::is_integral&amp;lt;T&amp;gt;()&lt;/code&gt;就会返回false（此函数判断是否为整型。但是&lt;strong&gt;所有的引用型别都不是整型&lt;/strong&gt;）。&lt;strong&gt;这里我们需要把&lt;code&gt;T&lt;/code&gt;可能的引用性去掉&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void logAndAdd(T&amp;amp;&amp;amp; name) {
    logAndAddImpl(
        std::forward&amp;lt;T&amp;gt;(name),
        std::is_integral&amp;lt;typename std::remove_reference&amp;lt;T&amp;gt;::type&amp;gt;() //检查T的类型是否为整型
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;code&gt;logAndAddImpl&lt;/code&gt;提供两个特化版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void logAndAddImpl(T&amp;amp;&amp;amp; name, std::false_type) { //如果不是整型
    names.emplace(std::forward&amp;lt;T&amp;gt;(name)); //具体代码细节无须在意
}

template &amp;lt;typename T&amp;gt;
void logAndAddImpl(T&amp;amp;&amp;amp; name, std::true_type) {	//如果是整型
    logAndAdd(nameFromIdx(idx)); //具体代码细节无须在意
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么用&lt;code&gt;std::true_type&lt;/code&gt;/&lt;code&gt;std::false_type&lt;/code&gt;而不用&lt;code&gt;true/false&lt;/code&gt;？前者是编译期值，后者是运行时值。&lt;/p&gt;
&lt;p&gt;注意这里我们都没有给&lt;code&gt;logAndAddImpl&lt;/code&gt;的第二个参数起名字，说明它就是一个Tag。这种方法常用于模板元编程。它们在运行期不起任何作用。&lt;/p&gt;
&lt;h1&gt;条款35 优先选用基于任务（std::async）而非基于线程（std::thread）的程序设计。&lt;/h1&gt;
&lt;p&gt;关于这一点其实我也困惑了比较久，尤其是基于&lt;code&gt;std::thread&lt;/code&gt;，&lt;code&gt;std::async&lt;/code&gt;这样的高级函数，和&lt;code&gt;POSIX&lt;/code&gt;系列的底层接口在多线程中的区别。&lt;/p&gt;
&lt;p&gt;一般来说，在使用现代C++的高级库的时候，异步操作可以选用&lt;code&gt;std::thread&lt;/code&gt;和&lt;code&gt;std::async&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基于任务的方法通常比基于线程实现的对应版本要好&lt;/strong&gt;。**主要原因是&lt;code&gt;async&lt;/code&gt;可以让你获取到异步执行的返回值。**因为我们要有一个&lt;code&gt;future&lt;/code&gt;对象做为句柄。而&lt;code&gt;thread&lt;/code&gt;则不会给你机会直接返回一个返回值。&lt;/p&gt;
&lt;p&gt;线程在C++软件的世界里有三种含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬件线程是实际执行计算的线程。现代计算机体系结构会为每个CPU内核提供一个或多个硬件线程。&lt;/li&gt;
&lt;li&gt;软件线程(又称操作系统线程或系统线程)是操作系统用以实施跨进程的管理，以及进行硬件线程调度的线程。通常，能够创建的软件线程会比硬件线程要多，因为当一个软件线程阻塞了（例如，阻塞在I/O操作上，或者需要等待互斥量或条件变量等)，运行另外的非阻塞线程能够提升吞吐率。&lt;/li&gt;
&lt;li&gt;**&lt;code&gt;std::thread&lt;/code&gt;是C++进程里的对象，用作底层软件线程的句柄。**有些&lt;code&gt;std::thread&lt;/code&gt;对象表示为“&lt;code&gt;null&lt;/code&gt;”句柄，对应于“无软件线程”，可能的原因有:
&lt;ul&gt;
&lt;li&gt;它们处于默认构造状态（因此没有待执行的函数)&lt;/li&gt;
&lt;li&gt;被移动了（作为移动目的的&lt;code&gt;std::thread&lt;/code&gt;对象成为了底层线程的句柄)&lt;/li&gt;
&lt;li&gt;被联结了(&lt;code&gt;join&lt;/code&gt;。待运行的函数已运行结束)，&lt;/li&gt;
&lt;li&gt;被分离了(&lt;code&gt;detach&lt;/code&gt; 。&lt;code&gt;std::thread&lt;/code&gt;对象与其底层软件线程的连接被切断了)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;那么什么时候使用&lt;code&gt;std::thread&lt;/code&gt;更好呢？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要访问底层线程实现的API。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::thread&lt;/code&gt;的 &lt;code&gt;native_handle&lt;/code&gt;函数提供了一个返回底层线程句柄的方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;需要且有能力优化线程用法&lt;/li&gt;
&lt;li&gt;需要实现超越C++并发API的线程技术。比如细化线程池。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意到我们必须时刻关注线程运行的数量。所以&lt;code&gt;async&lt;/code&gt;的默认启动方式是&lt;code&gt;std::launch::async | std::launch::deferred&lt;/code&gt;。也就是到底是真正的异步执行还是推迟执行是交给系统去选择的。这是下一条的讨论重点&lt;/strong&gt;。&lt;/p&gt;
&lt;h1&gt;条款36，如果异步是必要的，则指定std::launch::async&lt;/h1&gt;
&lt;p&gt;我们在杂记3中已经详细讨论过了async的启动方式。我感觉大多数情况下，只要使用了&lt;code&gt;async&lt;/code&gt;，应该都会使用异步方式启动。但是为了避免意外情况，在这里详细讨论一下，使用默认方式启动可能会遇到的问题。&lt;/p&gt;
&lt;p&gt;比如我们有&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto fut1 = std::async(f);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;无法预知&lt;code&gt;f&lt;/code&gt;是否会和调用线程并发运行，因为&lt;code&gt;f&lt;/code&gt;可能会被调度为推迟运行。&lt;/li&gt;
&lt;li&gt;无法预知&lt;code&gt;f&lt;/code&gt;是否运行在与调用&lt;code&gt;fut&lt;/code&gt;的&lt;code&gt;get&lt;/code&gt;或&lt;code&gt;wait&lt;/code&gt;函数的线程不同的某线程之上。&lt;/li&gt;
&lt;li&gt;连&lt;code&gt;f&lt;/code&gt;是否会运行这件起码的事情都是无法预知的，这是因为无法保证在程序的每条路径上，&lt;code&gt;fut&lt;/code&gt;的 &lt;code&gt;get&lt;/code&gt;或&lt;code&gt;wait&lt;/code&gt;都会得到调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;尤其是针对有&lt;code&gt;thread_local&lt;/code&gt;变量的情况下，我们无法确认f到底是使用调用线程的&lt;code&gt;thread_local&lt;/code&gt;变量还是新线程的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;基于future 的 wait_untill /wait_for 的循环以超时为条件的情况下可能导致永远无法退出循环&lt;/h2&gt;
&lt;p&gt;假设我们有如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f(){
    //一些操作
}
auto fut = async(f); //默认启动条件。表面上的异步运行

while(fut.wait_for(100ms) != std::future_status::ready){ //注意这里
    //其他操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;假设如果&lt;code&gt;f&lt;/code&gt;和调用线程是并发执行的，也就是&lt;code&gt;async&lt;/code&gt;真的异步启动了，那么这个操作没什么问题。&lt;/li&gt;
&lt;li&gt;但是假设&lt;code&gt;f&lt;/code&gt;并没有被&lt;code&gt;async&lt;/code&gt;异步启动，而是采用了推迟执行。由于我们从未对fut调用过&lt;code&gt;get&lt;/code&gt;或&lt;code&gt;wait&lt;/code&gt;，则&lt;code&gt;f&lt;/code&gt;操作永远不会被执行。该&lt;code&gt;future&lt;/code&gt;对象的状态将永远是&lt;code&gt;std::launch::deferred&lt;/code&gt;。所以这个&lt;code&gt;while&lt;/code&gt;永远都不会退出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;修正技巧：调用wait_untill 或 wait_for 并检查返回值&lt;/h3&gt;
&lt;p&gt;比如上面的代码可以修改为这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f(){
    //一些操作
}
auto fut = async(f); //默认启动条件。表面上的异步运行

if(fut.wait_for(0s) == std::future_status::deferred){ //实际上我们不需要等待任何事情，0s即可。然后查看是否被推迟
    fut.get();//如果被推迟，就显式的调用get或wait
}
else{
    while(fut.wait_for(100ms) != std::future_status::ready){ //如果未被推迟，确为异步执行。则正确执行。
    	//其他操作
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;wait_for&lt;/code&gt;中，实际上我们不需要等待任何事情，所以参数为&lt;code&gt;0s&lt;/code&gt;即可。然后查看是否被推迟。如果被推迟，就显式的调用&lt;code&gt;get&lt;/code&gt;或&lt;code&gt;wait&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;条款37 使thread对象在所有路径皆不可联结 -- 也就是让thread对象在离开其作用域范围之前被join或detach掉。&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;换句话说就是：thread对象在离开其作用域的时候，必须确保它的状态不是joinable的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;非joinable的thread对象包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认构造的（本来就无效）&lt;/li&gt;
&lt;li&gt;已被移动的（所有权被转移）&lt;/li&gt;
&lt;li&gt;已被&lt;code&gt;join&lt;/code&gt;的（废话）&lt;/li&gt;
&lt;li&gt;已被&lt;code&gt;detach&lt;/code&gt;的（thread对象已和底层执行线程分离）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在并发笔记中提到了，如果一个thread对象在离开作用域（调用析构函数）的时候，仍然是joinable的状态，则会直接调用&lt;code&gt;std::terminate&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决这个问题的方式是使用RAII管理线程对象。我们在并发笔记中写到了。此处不赘述。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;条款38 对变化多端的线程句柄析构函数行为保持关注&lt;/h1&gt;
&lt;p&gt;这一条主要讲的是future的共享状态到底是什么，和future对象的析构函数在什么时候会被阻塞。请查看杂记3。&lt;/p&gt;
&lt;h1&gt;条款39 考虑针对一次性事件通信使用以void为模板型别实参的future对象&lt;/h1&gt;
&lt;p&gt;这话听着贼诡异。前半部分讲的是条件变量的虚假唤醒和唤醒丢失。这一部分在项目相关里面写过了。&lt;/p&gt;
&lt;p&gt;后半部分是着重强调了一下&lt;strong&gt;一次性事件的通信&lt;/strong&gt;。**我们知道&lt;code&gt;future&lt;/code&gt;和&lt;code&gt;promise&lt;/code&gt;所保有的共享状态的状态是不可逆的。也就是一旦被设置好了值，&lt;code&gt;future&lt;/code&gt;和&lt;code&gt;promise&lt;/code&gt;是不可复用的。&lt;code&gt;future&lt;/code&gt;只能&lt;code&gt;get&lt;/code&gt;一次，&lt;code&gt;promise&lt;/code&gt;只能&lt;code&gt;set&lt;/code&gt;一次。它们之间的通信通道是一次性的。**所以这就是它们和条件变量的最大区别。这种&lt;code&gt;future&lt;/code&gt;通信方式有一个缺点，就是需要使用堆内存，可能造成性能影响。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是如果有一些事情刚好是只需要一次性通信的，比如创建线程后对线程暂停，进行亲和性或优先级的设置，这个时候它们就非常有用。因为此时搭配条件变量和互斥量的开销较大。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-EffectiveMC++.assets/QQ%E6%88%AA%E5%9B%BE20230224235914.png&quot; alt=&quot;QQ截图20230224235914&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个例子就是我们所谓的创建线程中对线程执行进行暂停。直到我们&lt;code&gt;set_value&lt;/code&gt;之前，&lt;code&gt;react&lt;/code&gt;不会被执行。所以我们可以在这个线程执行&lt;code&gt;react&lt;/code&gt;之前对它进行一些设置。注意，此处写成普通函数依旧可以。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;thread&lt;/code&gt;构造函数中会立刻新建线程，在新线程中对可调用对象进行&quot;启动&quot;然后立刻返回。（不要纠结于为什么创建新线程的函数可以立刻返回，如果非常纠结的话看&lt;em&gt;&lt;strong&gt;深入理解linux内核的第三章&lt;/strong&gt;&lt;/em&gt;。大概可以理解为创建线程用的&lt;code&gt;clone&lt;/code&gt;函数只负责创建，创建成功后会立刻返回，函数本身不会执行具体的任务。只是把任务当做参数传入然后把线程放入调度队列。然后交给调度器来决定什么时候新线程开始执行。比如在上面的例子中，整个lambda是参数传入给新的线程。当clone返回后，如果cpu决定执行这个线程了，就开始执行lambda内的函数。&lt;strong&gt;我们无法保证也没有必要保证&lt;code&gt;set_value&lt;/code&gt;到底会在&lt;code&gt;wait&lt;/code&gt;前还是&lt;code&gt;wait&lt;/code&gt;后被执行。因为&lt;code&gt;wait&lt;/code&gt;始终会等待值被设定好。和我们的条件变量为什么需要判断信号丢失的原理是一致的。&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-EffectiveMC++.assets/QQ%E5%9B%BE%E7%89%8720230225013643.png&quot; alt=&quot;QQ图片20230225013643&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回到我们的代码中，上面的代码有一个问题。因为我们在条款37中提到了尽可能的使用RAII管理线程对象，所以我们的新代码可能会像是这个样子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-EffectiveMC++.assets/QQ%E6%88%AA%E5%9B%BE20230225013828.png&quot; alt=&quot;QQ截图20230225013828&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这段代码有个非常严重的问题。所以我们剖析一下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;假如红框部分抛出了异常，则&lt;code&gt;p.set_value()&lt;/code&gt;永远都不会被执行。&lt;/li&gt;
&lt;li&gt;如果它不执行，则&lt;code&gt;ThreadRAII&lt;/code&gt;保有的线程对象会在新的线程中被卡在&lt;code&gt;p.get_future().wait()&lt;/code&gt;这一行。新线程无法执行完毕这个任务函数，也就无法返回。&lt;/li&gt;
&lt;li&gt;同时，在主线程中，我们如果要离开&lt;code&gt;detect&lt;/code&gt;函数，则&lt;code&gt;ThreadRAII&lt;/code&gt;对象必须析构。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThreadRAII&lt;/code&gt;的析构函数会调动&lt;code&gt;join&lt;/code&gt;函数。如果任务函数不返回，&lt;code&gt;join&lt;/code&gt;会一直等待任务函数返回。所以一旦红框抛出异常，则析构函数永远都不会执行完成。整个函数就会失去响应。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>王道操作系统笔记-内存（24）-- 内存管理6</title><link>https://sterben-01.github.io/posts/OS27/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS27/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存（24）-- 内存管理6&lt;/h1&gt;
&lt;h2&gt;TLB&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;页表保存在主内存中。&lt;/li&gt;
&lt;li&gt;Page-table base register (PTBR) points to the page table.(页表基址寄存器指向页表)&lt;/li&gt;
&lt;li&gt;Page-table length register (PRLR) indicates size of the page table.(页表限长寄存器表明页表的长度)&lt;/li&gt;
&lt;li&gt;在这个机制中，每一次的数据/指令存取需要两次内存存取，一次是存取页表，一次是存取数据
&lt;ul&gt;
&lt;li&gt;我们是首先查询页表得到物理地址，然后访问该物理地址读取指令、数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TLB translation look-aside buffers 通过一个联想寄存器,可以解决两次存取的问题。（快表）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们有了TLB之后，首先在TLB找，如果找不到就是miss，找到了就是hit。如果hit了直接把偏移量拿过来用。（这样就是一次访问即可）。如果miss了就再去页表里面找，然后再&lt;strong&gt;复制到&lt;/strong&gt;TLB里面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS27.assets/QQ%E6%88%AA%E5%9B%BE20220720043949.png&quot; alt=&quot;QQ截图20220720043949&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TLB虽然是cache，但是只存有页表项的副本。普通Cache则会有其他数据的副本。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;两级页表&lt;/h2&gt;
&lt;p&gt;单级页表有两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;页表必须连续存放，因此当页表很大时，需要占用很多个连续的页框。&lt;/li&gt;
&lt;li&gt;没有必要让整个页表常驻内存，因为进程在一段时间内可能只需要访问某几个特定的页面。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;针对第一个问题&lt;/strong&gt;，我们既然都可以使用页表来将进程的地址空间进行分页，那么我们也可以给页表再分页。这样也就可以让页表离散存放。这一个页表可能叫 页目录表，外层页表，或顶层页表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS27.assets/QQ%E6%88%AA%E5%9B%BE20220720044749.png&quot; alt=&quot;QQ截图20220720044749&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS27.assets/QQ%E6%88%AA%E5%9B%BE20220720044853.png&quot; alt=&quot;QQ截图20220720044853&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;针对第二个问题&lt;/strong&gt;，可以使用虚拟储存技术。也就是需要访问页面的时候，才把页面调入内存。如果想访问的页面不在内存中，会产生缺页中断（内中断），把目标页面从外存调入内存。后文详细介绍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS27.assets/QQ%E6%88%AA%E5%9B%BE20220720045040.png&quot; alt=&quot;QQ截图20220720045040&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-内存内存（25）-- 内存管理7</title><link>https://sterben-01.github.io/posts/OS28/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS28/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存内存（25）-- 内存管理7&lt;/h1&gt;
&lt;h3&gt;非连续分配管理方式（2）基本分段存储管理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;每一个进程都有一个自己的段表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个其实说回来和分页差不多。只不过分段机制是按照自身逻辑来划分的。每一个段有一个段名。每段从0开始编址。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程的地址空间:按照程序自身的逻辑关系划分为若干个段，每个段都有一个段名（在低级语言中，程序员使用段名来编程），每段从0开始编址。&lt;/li&gt;
&lt;li&gt;内存分配规则:以段为单位进行分配，每个段在内存中占据连续空间，但各段之间可以不相邻。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS28.assets/QQ%E6%88%AA%E5%9B%BE20220720054847.png&quot; alt=&quot;QQ截图20220720054847&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们也会有段表。结构和页表不太一样。段表项长度是相同的。储存了段长和段的基地址。又因为段表是连续存放的。所以段号可以隐含。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS28.assets/QQ%E6%88%AA%E5%9B%BE20220720054911.png&quot; alt=&quot;QQ截图20220720054911&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;页是信息的物理单位。分页的主要目的是为了实现离散分配，提高内存利用率。分页仅仅是系统管理上的需要，完全是系统行为，对用户是不可见的。&lt;/li&gt;
&lt;li&gt;段是信息的逻辑单位。分页的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的，用户编程时需要显式地给出段名。&lt;/li&gt;
&lt;li&gt;页的大小固定且由系统决定。段的长度却不固定，决定于用户编写的程序。&lt;/li&gt;
&lt;li&gt;分页的用户进程地址空间是一维的，程序员只需给出一个记忆符即可表示一个地址。（页表储存的是页号和块号）仅需通过页号找块号。&lt;/li&gt;
&lt;li&gt;分段的用户进程地址空间是二维的，程序员在标识一个地址时，既要给出段名，也要给出段内地址。（段表储存的是段名和起始地址，需要给出这两者来寻找段）。&lt;/li&gt;
&lt;li&gt;分段比分页更容易实现信息的共享和保护。不能被修改的代码称为纯代码或可重入代码（不属于临界资源），这样的代码是可以共享的。可修改的代码是不能共享的&lt;/li&gt;
&lt;li&gt;访问一个逻辑地址需要几次访存?&lt;/li&gt;
&lt;li&gt;分页（单级页表)﹔第一次访存――查内存中的页表，第二次访存――访问目标内存单元。总共两次访存&lt;/li&gt;
&lt;li&gt;分段:第一次访存――查内存中的段表，第二次访存――访问目标内存单元。总共两次访存&lt;/li&gt;
&lt;li&gt;与分页系统类似，分段系统中也可以引入快表机构，将近期访问过的段表项放到快表中，这样可以少一次访间，加快地址变换速度。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>王道操作系统笔记-内存内存（26）-- 内存管理8</title><link>https://sterben-01.github.io/posts/OS29/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS29/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存内存（26）-- 内存管理8&lt;/h1&gt;
&lt;h2&gt;非连续分配管理方式（3）基本段页式存储管理&lt;/h2&gt;
&lt;h3&gt;分页和分段的优缺点分析&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS29.assets/QQ%E6%88%AA%E5%9B%BE20220720055359.png&quot; alt=&quot;QQ截图20220720055359&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;如果系统采用分页式，则每个进程都有一个自己的页表。如果系统采用分段式，则每个进程都有一个自己的段表。如果系统采用段页式，则每个进程都有一个自己的段表。而每一个段都会有一个页表，所以是一个段表和多个页表。&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS29.assets/QQ%E6%88%AA%E5%9B%BE20220720055542.png&quot; alt=&quot;QQ截图20220720055542&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“分段”对用户是可见的，程序员编程时需要显式地给出段号、段内地址。而将各段“分页”对用户是不可见的。系统会根据段内地址自动划分页号和页内偏移量。因此段页式管理的地址结构是二维的。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;段页式分配的段表和页表都和原始的段式的段表，页式的页表长得不太一样。&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个段对应一个段表项，每个段表项由段号、页表长度、页表存放块号（页表起始oo也址）组成。每个段表项长度相等，段号是隐含的。&lt;/li&gt;
&lt;li&gt;每个页面对应一个页表项，每个页表项由页号、页面存放的内存块号组成。每个页表项长度相等，页号是隐含的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS29.assets/QQ%E6%88%AA%E5%9B%BE20220720055801.png&quot; alt=&quot;QQ截图20220720055801&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-内存内存（27）-- 内存管理9</title><link>https://sterben-01.github.io/posts/OS30%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS30%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存内存（27）-- 内存管理9&lt;/h1&gt;
&lt;h2&gt;虚拟内存1&lt;/h2&gt;
&lt;h3&gt;为什么需要虚拟内存&lt;/h3&gt;
&lt;p&gt;进程必须全部被放入内存后才可以运行。但是如果进程的大小大于内存的容量，或者是内存中同时运行多个进程的话怎么办？我们可以通过覆盖这种方式来解决（之前笔记有。类似于c++内存管理使用union实现的嵌入式指针），但是需要让程序员做很多工作。所以我们可以通过逻辑上去扩充内存。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从逻辑上去扩充内存，满足在小内存的机器上运行较多程序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为进程提供统一的地址空间，简化内存管理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个进程都假设系统中只有自己存在。我们给每一个进程的地址都限制为0-4G，这样可以简化内存管理。因为每个进程有自己的页表，可以通过自己的页表去查询物理页面映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保护进程地址空间不被其他进程破坏&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程的隔离性也是由此而来。我们不希望其他进程去干扰本进程的地址。为了做到这一点，我们让不同进程的相同虚拟地址映射到不同的物理地址。这样进程不会直接访问物理地址而是间接访问。这样就不会发生冲突或者是误操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常规存储器的特征和程序的局部性理论。&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一次性作业在运行前需要一次性的全部装入内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;驻留性作业装入内存后，便一直驻留在内存中，直到作业结束。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正是由于一次性和驻留性，使得程序中暂时不用的数据占用了大量的内存空间，从而需要运行的作业无法装入内存。但是这两个特性并不是必须的。因为程序不是所有的部分都被同时需要。&lt;/p&gt;
&lt;p&gt;一般来说，在一段时间内，程序的执行仅局限于某个部分;相应地，它所访问的存储空间也局限于某个区域内（局部性原理）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;因为程序在执行时，除了少部分的转移和过程调用指令外，大多数仍是顺序执行的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;即使子程序调用将会使程序的执行由一部分内存区域转至另一部分区域，但是跳转至另一区域后仍然大概率是顺序执行的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序中存在许多循环结构，循环体中的指令被多次执行。（刚刚访问过的地址空间很可能马上又要访问）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序中还包括许多对数据结构的处理，如对连续的存储空间——数组的访问，往往局限于很小的范围内。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;局部性分为时间局部性和空间局部性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间局部性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果程序中的某条指令一旦执行，则不久的将来该指令可能再次被执行;&lt;/li&gt;
&lt;li&gt;如果某个存储单元被访问，则不久以后该存储单元可能再次被访问。&lt;/li&gt;
&lt;li&gt;产生时间局限性的典型原因是在程序中存在着大量的循环操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间局部性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一旦程序访问了某个存储单元，则在不久的将来，其附近的存储单元也最有可能被访问。即程序在一段时间内所访问的地址，可能集中在一定的范围内，其典型原因是程序是顺序执行的。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;虚拟内存&lt;/h3&gt;
&lt;p&gt;虚拟内存是一种允许进程部分装入内存就可以执行的技术。因为只有运行的部分程序需要在内存中。正因为如此，逻辑地址空间可以比物理地址空间要大，因为可以换入和换出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于局部性原理，在程序装入时，可以将程序中很快会用到的部分装入内存，暂时用不到的部分留在外存，就可以让程序开始执行。&lt;/li&gt;
&lt;li&gt;在程序执行过程中，当所访问的信息不在内存时，由操作系统负责将所需信息从外存调入内存，然后继续执行程序。(请求调页/段)&lt;/li&gt;
&lt;li&gt;若内存空间不够，由操作系统负责将内存中暂时用不到的信息换出到外存。(页面/段置换)&lt;/li&gt;
&lt;li&gt;在操作系统的管理下，在用户看来似乎有一个比实际内存大得多的内存，这就是虚拟内存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虚拟内存有三个主要特征:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;多次性:无需在作业运行时一次性全部装入内存，而是允许被分成多次调入内存。&lt;/li&gt;
&lt;li&gt;对换性:在作业运行时无需一直常驻内存，而是允许在作业运行过程中，将作业换入、换出。&lt;/li&gt;
&lt;li&gt;虚拟性:从逻辑上扩充了内存的容量，使用户看到的内存容量，远大于实际的容量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虚拟内存技术，允许一个作业分多次调入内存。如果采用连续分配方式，会不方便实现。因此虚拟内存的实现需要建立在&lt;strong&gt;离散分配&lt;/strong&gt;的内存管理方式基础上。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;正如传统的非连续分配储存管理分为三种：基本分页，基本分段，基本段页式一样，虚拟内存也分为三种。即请求分页，请求分段，请求段页式&lt;/strong&gt;&lt;/h3&gt;
&lt;h3&gt;请求分页管理方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在程序执行过程中，当所访问的信息不在内存时，由操作系统负责将所需信息从外存调入内存，然后继续执行程序。（操作系统要提请求调页功能将缺失页面从存调入内存。）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与基本分页管理相比，请求分页管理中，为了实现“请求调页”，操作系统需要知道每个页面是否已经调入内存:如果还没调入，那么也需要知道该页面在外存中存放的位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若内存空间不够，由操作系统负责将内存中暂时用不到的信息换出到外存。（操作系统要提供页面置换的功能,将暂时用不到的页面换出外存）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当内存空间不够时，要实现“页面置换”，操作系统需要通过某些指标来决定到底换出哪个页面:有的页面没有被修改过，就不用再浪费时间写回外存。有的页面修改过，就需要将外存中的旧数据覆盖，因此，操作系统也需要记录各个页面是否被修改的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;请求分页管理方式的页表&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS30.assets/QQ%E6%88%AA%E5%9B%BE20220720061957.png&quot; alt=&quot;QQ截图20220720061957&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;缺页中断机构&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;在请求分页系统中，每当要访问的页面不在内存时，便产生一个缺页中断，然后由操作系统的缺页中断处理程序处理中断。
此时缺页的进程阻塞，放入阻塞队列，调页完成后再将其唤醒，放回就绪队列。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果此时内存中有空闲块，则为进程分配一个空闲块，将所缺页面装入该块，并修改页表中相应的页表项。&lt;/li&gt;
&lt;li&gt;如果此时内存中没有空闲块，则由页面置换算法选择一个页面淘汰，&lt;strong&gt;若该页面在内存期间被修改过，则要将其写回外存&lt;/strong&gt;。（不写回数据不就丢了么）。未修改过的页面不用写回外存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的，因此属于内中断的故障（fault）&lt;/strong&gt;。一条指令在执行期间，可能产生多次缺页中断。(如: copy A to B，即将逻辑地址A中的数据复制逻辑地址B，而A、B属于不同的页面，则有可能产生两次缺页中断)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-20-OS30.assets/QQ%E6%88%AA%E5%9B%BE20220720062408.png&quot; alt=&quot;QQ截图20220720062408&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;缺页中断于一般中断的区别&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;缺页中断在指令执行期间产生和处理中断信号，而一般中断在一条指令执行完后检查和处理中断信号。&lt;/li&gt;
&lt;li&gt;缺页中断返回到该指令的开始重新执行该指令，而一般中断返回到该指令的下一条指令执行。&lt;/li&gt;
&lt;li&gt;一条指令在执行期间，可能产生多次缺页中断。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;细节&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;只有“写指令”才需要修改“修改位”。并且，一般来说只需修改快表中的数据，只有要将快表项删除时才需要写回内存中的慢表。这样可以减少访存次数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;和普通的中断处理一样，缺页中断处理依然需要保留CPU现场。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要用某种“页面置换算法”来决定一个换出页面（下节内容)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;换入/换出页面都需要启动慢速的I/O操作，可见，如果换入/换出太频繁，会有很大的开销。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页面调入内存后，需要修改慢表，同时也需要将表项复制到快表中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在具有快表机构的请求分页系统中，访问一个逻辑地址时，若发生缺页，则地址变换步骤是:&lt;/p&gt;
&lt;p&gt;查快表(未命中)――查慢表(发现未调入内存)—―调页(调入的页面对应的表项会直接加入快表)――查快表(命中)――访问目标内存单元&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Malloc, brk 和 mmap</title><link>https://sterben-01.github.io/posts/brk-mmap/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/brk-mmap/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;malloc, brk 和 mmap&lt;/h1&gt;
&lt;h1&gt;这篇笔记可能存在错误，可能需要重新修改&lt;/h1&gt;
&lt;h1&gt;&lt;code&gt;malloc&lt;/code&gt;不是系统调用。是C库函数&lt;/h1&gt;
&lt;p&gt;看过侯捷老师的视频，我们都有内存池这个概念。但是&lt;code&gt;malloc&lt;/code&gt;如何向系统申请内存？&lt;/p&gt;
&lt;p&gt;我们仅用Linux下的方法说明。&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;malloc(1)&lt;/code&gt;  会分配多大的虚拟内存？&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;malloc()&lt;/code&gt;在分配内存的时候，并不是老老实实按用户预期申请的字节数来分配内存空间大小，而是&lt;strong&gt;会预分配更大的空间作为内存池。（这一段具体原理可以看侯捷老师视频）&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;malloc&lt;/code&gt;会使用两种方式向操作系统申请堆内存&lt;/h1&gt;
&lt;h2&gt;&lt;code&gt;brk&lt;/code&gt;系统调用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;brk&lt;/code&gt;的原理是把&lt;code&gt;edata&lt;/code&gt;指针(堆顶指针）往上（往栈的方向）推。来获得新的虚拟内存空间。使用&lt;code&gt;free&lt;/code&gt;释放的时候，&lt;strong&gt;&lt;code&gt;brk&lt;/code&gt;分配的内存不会交回给操作系统。而是会缓存在&lt;code&gt;malloc&lt;/code&gt;的内存池中&lt;/strong&gt;。等待下次使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-25-brk%20mmap.assets/QQ%E6%88%AA%E5%9B%BE20220725064954.png&quot; alt=&quot;QQ截图20220725064954&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;mmap&lt;/code&gt;系统调用&lt;/h2&gt;
&lt;p&gt;mmap是在堆和栈的中间也就是文件映射区**（注意这里使用的是私有匿名映射，所以不需要实体磁盘文件）**分配一块虚拟内存。使用&lt;code&gt;free&lt;/code&gt;释放的时候，&lt;code&gt;mmap&lt;/code&gt;分配的内存会交回给操作系统。得到真正的释放。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-25-brk%20mmap.assets/QQ%E6%88%AA%E5%9B%BE20220725065018.png&quot; alt=&quot;QQ截图20220725065018&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;何时决定使用何种系统调用？&lt;/h2&gt;
&lt;p&gt;一般来说，会有一个默认阈值（128KB）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果用户分配的内存小于 128 KB，则通过 &lt;code&gt;brk()&lt;/code&gt; 申请内存；&lt;/li&gt;
&lt;li&gt;如果用户分配的内存大于 128 KB，则通过 &lt;code&gt;mmap() &lt;/code&gt; 申请内存；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么&lt;code&gt;brk&lt;/code&gt;的内存不会交回操作系统？&lt;/h2&gt;
&lt;p&gt;因为&lt;code&gt;brk&lt;/code&gt;是推指针。假如我们有 A B C D 四块内存。我们指针指向D。我们此时释放了B，指针不可以往回推，因为C和D还在。而且B的区域是可以重用的。直到C D 被回收 也就是从&lt;strong&gt;指针地址开始算有连续的大于某一个阈值（128K）的空闲内存了&lt;/strong&gt;，edata指针才会紧缩。&lt;strong&gt;这就是内存碎片(这里不区分内部和外部碎片。和操作系统维护的物理内存的内部外部碎片不同。)产生的原因。&lt;/strong&gt; 而且，假设我释放的B的大小是10K。如果此时需要给新的E分配一个30K的大小，这个B的空间是不可用的。所以依旧需要继续推指针。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-25-brk%20mmap.assets/QQ%E6%88%AA%E5%9B%BE20220725065150.png&quot; alt=&quot;QQ截图20220725065150&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;为什么不统一使用&lt;code&gt;mmap&lt;/code&gt;申请内存？&lt;/h2&gt;
&lt;p&gt;因为向操作系统申请内存，是要通过系统调用的，执行系统调用是要进入内核态的，然后在回到用户态，运行态的切换会耗费不少时间。&lt;/p&gt;
&lt;p&gt;所以，申请内存的操作应该避免频繁的系统调用，如果都用 &lt;code&gt;mmap&lt;/code&gt; 来分配内存，等于每次都要执行系统调用。&lt;/p&gt;
&lt;p&gt;另外，因为 &lt;code&gt;mmap&lt;/code&gt; 分配的内存每次释放的时候，都会归还给操作系统，于是每次  &lt;code&gt;mmap&lt;/code&gt; 分配的虚拟地址都是缺页状态的，然后在第一次访问该虚拟地址的时候，就会触发缺页中断。&lt;/p&gt;
&lt;p&gt;也就是说，&lt;strong&gt;频繁通过&lt;code&gt; mmap&lt;/code&gt; 分配的内存话，不仅每次都会发生运行态的切换，还会发生缺页中断（在第一次访问虚拟地址后），这样会导致 CPU 消耗较大&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;**为了改进这两个问题，&lt;code&gt;malloc&lt;/code&gt; 通过&lt;code&gt;brk()&lt;/code&gt;系统调用在堆空间申请内存的时候，由于堆空间是连续的，所以直接预分配更大的内存来作为内存池，当内存释放的时候，就缓存在内存池中。**等下次再申请内存的时候，就直接从内存池取出对应的内存块就行了，而且可能这个内存块的虚拟地址与物理地址的映射关系还存在，这样不仅减少了系统调用的次数，也减少了缺页中断的次数，这将大大降低 CPU 的消耗。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再次记住，malloc不是系统调用&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么不统一使用&lt;code&gt;brk&lt;/code&gt;申请内存？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;因为上文提到的内存碎片&lt;/strong&gt;，随着系统频繁地 &lt;code&gt;malloc &lt;/code&gt;和 &lt;code&gt;free&lt;/code&gt; ，尤其对于小块内存，堆内将产生越来越多不可用的碎片，导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。&lt;/p&gt;
&lt;h3&gt;brk()和sbrk()&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;brk&lt;/code&gt;是系统调用，&lt;code&gt;sbrk&lt;/code&gt;是封装了&lt;code&gt;brk&lt;/code&gt;的库函数。
&lt;ul&gt;
&lt;li&gt;来自&lt;a href=&quot;https://man7.org/linux/man-pages/man2/brk.2.html&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;brk&lt;/code&gt;的作用是直接设置指针到某个地址。&lt;code&gt;sbrk&lt;/code&gt;的作用是移动一段距离。&lt;/li&gt;
&lt;li&gt;所以说&lt;code&gt;sbrk&lt;/code&gt;的入参可以为负数。这样就是往回缩。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;code&gt;free()&lt;/code&gt; 函数只传入一个内存地址，为什么能知道要释放多大的内存？&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;内存笔记的第一章。有cookie。cookie不仅会保存分配内存的大小，而且会用最后一个bit保存是否已分配。如果已分配，就是能被析构的。就是1，未分配也就是不能析构的就是0.&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;malloc&lt;/code&gt;分配的是虚拟内存&lt;/h1&gt;
&lt;p&gt;如果分配后的虚拟内存没有被访问的话，是不会将虚拟内存不会映射到物理内存，这样就不会占用物理内存了。&lt;/p&gt;
&lt;p&gt;只有在访问已分配的虚拟地址空间的时候，操作系统通过查找页表，发现虚拟内存对应的页没有在物理内存中，就会触发缺页中断，然后操作系统会建立虚拟内存和物理内存之间的映射关系。当真正有&lt;code&gt;memcpy&lt;/code&gt;这样的函数访问的时候才会触发缺页中断来分配物理内存&lt;/p&gt;
&lt;h2&gt;外部碎片(在分配单元间的未使用的内存)；内部碎片(在分配单元中未使用的内存)这里的碎片指的是物理内存。不是虚拟内存。&lt;/h2&gt;
&lt;p&gt;上面讲的内存碎片指的是虚拟内存碎片，OS是不管的，OS只管物理内存。&lt;/p&gt;
&lt;p&gt;平时我们说的内存碎片整理（defragment）或内存紧缩（memory compaction），是指OS对物理内存进行的碎片整理，把分开小的物理内存页移动在一起形成一个大的整块。OS整理完物理内存后，会用新的物理内存地址来更新虚拟内存与物理内存映射表，这些对于上层逻辑都是透明的。&lt;strong&gt;虚拟内存是不能进行碎片整理的，主要原因是碎片整理会移动内存，上层逻辑的指针地址确还是指向老的地址，这会导致致命错误。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;new一定陷入内核态吗？ -- 不一定&lt;/h1&gt;
&lt;p&gt;因为new的底层是malloc。malloc会选择使用：内存池 或 brk 或 mmap进行内存的分配。如果选择了brk或mmap则因为是系统调用会陷入内核。&lt;strong&gt;而brk申请的内存释放后会进入malloc自己的内存池&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，内存池是在malloc里面的而不是在brk或mmap里面的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以如果下次调用的时候发现brk归还的内存在内存池里（内存池有余量）则不会调用brk或mmap这种系统调用，而是直接拿出内存池的内存。这样就避免了陷入内核态。&lt;/p&gt;
&lt;h1&gt;杂项&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;munmap&lt;/code&gt;解除映射&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;mprotect&lt;/code&gt;修改映射权限&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;msync&lt;/code&gt;将对使用&lt;code&gt;mmap&lt;/code&gt;映射到内存中的文件的核内副本所做的更改刷新回文件系统。如果不使用此调用，则不能保证在调用&lt;code&gt;munmap&lt;/code&gt;之前将更改写回。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;STL的空间配置器&lt;/h1&gt;
&lt;h2&gt;一级空间配置器&lt;/h2&gt;
&lt;p&gt;一级空间配置器也就是封装了malloc和free&lt;/p&gt;
&lt;h2&gt;二级空间配置器&lt;/h2&gt;
&lt;p&gt;二级空间配置器就是内存池（自由链表）&lt;/p&gt;
&lt;h2&gt;申请&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;申请的时候，如果大于128字节直接调用一级配置器。如果小于128字节调用二级配置器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二级配置器就相当于我们说的，一次申请一大块内存，然后拆分成8字节大小的块，挂在链表上。申请就从链表拿走，归还就挂回链表。可以看memory4笔记。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;malloc源码讲解：https://www.52pojie.cn/thread-1581911-1-1.html&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考链接：https://mp.weixin.qq.com/s/HXRGr90baCvM-NQbPIgn-g&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>虚函数表</title><link>https://sterben-01.github.io/posts/vptr/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/vptr/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;虚函数表&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;每个包含了虚函数的类都包含一个虚函数表。&lt;strong&gt;虚函数表是一个指针数组，其元素是指向虚函数的指针，每个元素对应一个虚函数的函数指针。需要指出的是，普通的函数即非虚函数，其调用并不需要经过虚表，所以&lt;/strong&gt;虚表的元素并不包括普通函数的函数指针&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚表是属于类的，而不是属于某个具体的对象，一个类只需要一个虚表即可。&lt;strong&gt;同一个类的所有对象都使用同一个虚表。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;虚表内的条目，即指向虚函数的指针的赋值发生在编译器的编译阶段，也就是说在代码的编译阶段，虚函数表就可以构造出来了。&lt;/strong&gt;（只要发现有&lt;code&gt;virtual&lt;/code&gt;关键字就会塞进虚函数表）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;_vfptr&lt;/code&gt;对象中指向虚函数表的这个指针是创建对象的时候才会出现，因为这个东西属于对象。因为编译期不会为对象分配内存（new是运行期的东西），所以自然对象的虚函数表指针只有运行期才有。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以无论你的对象是栈上还是堆上，无论你是否用了虚函数，无论是否满足多态调用的条件。只要这个类内部用有虚函数，那么一定会有虚函数表。那么每一个对象则一定会用有一个虚函数表指针。所以对象会变大。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果一个基类有虚函数，那么这个基类地址的头部（偏移量为0，最开始的地方）是一个&lt;strong&gt;指向虚函数表的指针&lt;/strong&gt;。这个指向虚函数表的指针是在类对象创建时自动赋值的。&lt;strong&gt;会自动被设置为指向自己这个类的虚表&lt;/strong&gt;。我们可以管这个指针叫做&lt;code&gt;_vfptr&lt;/code&gt;。(具体的位置因平台而异)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;也就是说。在多态情况下，也就是父类有虚函数的情况下，每一个类（父类/子类）都有一个自己的虚函数表。子类是继承（复制）来的。&lt;strong&gt;子类在多重继承的时候可能会有多个虚函数表，和多重继承导致的多个&lt;code&gt;_vfptr&lt;/code&gt;&lt;/strong&gt;（&lt;strong&gt;此句理解为：由于虚函数表和虚函数表指针是继承（复制）而来的，所以子类每继承一个父类，就会有一套其父类的虚函数表和对应的虚函数表指针。&lt;/strong&gt;）子类中&lt;code&gt;_vfptr&lt;/code&gt;虚函数表指针的数量是所有继承的父类的虚函数表指针的总和。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;只要父类有虚函数表，子类无论是否有虚函数，也会有一个自己的虚函数表。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只要是虚函数，无论是否触发多态调用（比如对象调用），都会去查表。（最原始的情况）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;尽管编译器会优化此行为。这样如果不满足多态调用条件则不会查表。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象的虚表指针用来指向自己所属类的虚表，&lt;strong&gt;虚表中的指针会指向其继承的最近的一个类的虚函数&lt;/strong&gt;。这里的要点是，假如&lt;code&gt;C&lt;/code&gt;继承&lt;code&gt;B&lt;/code&gt;，&lt;code&gt;B&lt;/code&gt;继承&lt;code&gt;A&lt;/code&gt;。&lt;code&gt;B&lt;/code&gt;重写了&lt;code&gt;A&lt;/code&gt;的虚函数，如果&lt;code&gt;C&lt;/code&gt;没有重写，那么由于是&lt;code&gt;C&lt;/code&gt;继承的&lt;code&gt;B&lt;/code&gt;，所以&lt;code&gt;C&lt;/code&gt;的虚函数表中的指针会指向&lt;code&gt;B&lt;/code&gt;重写过的虚函数而不是&lt;code&gt;A&lt;/code&gt;的。即 继承的最近一个类的虚函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单继承中，无论继承链条有多长，无论子类是否有自己的虚函数，子类也只有一个虚函数表（假设基类有虚函数）。因为每个中间类的虚函数表都是继承自基类的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++中虚函数表位于只读数据段（.rodata），也就是C++内存模型中的常量区；而虚函数则位于代码段（.text），也就是C++内存模型中的代码区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当子类继承父类后，子类在编译期间就会继承（复制一份）父类的虚函数指针和虚函数表。但是如果子类重写了该方法(加virtual或者不加都可以，但是方法必须完全相同包括返回值)，那么子类的虚函数表中的函数则会改变，也就是把自己的条目补全或替换。（在子类对象构造函数调用后）会将自己对象的虚函数指针指向自己的虚函数表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;父类指针指向子类对象时，发生动态联编，如果虚函数表被重写，则调用父类虚函数会指向被重写的方法，否则父类指针指向自身的方法，是静态联编。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;虚基类表指针&lt;code&gt;_vbptr&lt;/code&gt;的含义是：该指针指向的虚基类表里面记录了对应的虚继承的类在本类里的位置。也就是对应虚基类距离本类头部的偏移量。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们知道虚基类的内容都是在屁股的，所以相当于用一个指针塞在了头部，然后查表找到屁股的位置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; class A {
     int a;
 };
 
 class B {
     int d;
 };
 
 class C : public virtual  B, public virtual A {
     int sss;
 };
 1&amp;gt;class C	size(24):
 1&amp;gt;	+---
 1&amp;gt; 0	| {vbptr}
 1&amp;gt; 8	| sss
 1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
 1&amp;gt;	+---
 1&amp;gt;	+--- (virtual base B)			//虚基类内容在屁股
 1&amp;gt;16	| d  
 1&amp;gt;	+---
 1&amp;gt;	+--- (virtual base A)			//虚基类内容在屁股
 1&amp;gt;20	| a
 1&amp;gt;	+---
 1&amp;gt;C::$vbtable@:
 1&amp;gt; 0	| 0
 1&amp;gt; 1	| 16 (Cd(C+0)B)				//虚基类B的起始地址是16。所以偏移量是16
 1&amp;gt; 2	| 20 (Cd(C+0)A)				//虚基类B的起始地址是20。所以偏移量是20
 1&amp;gt;vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
 1&amp;gt;               B      16       0       4 0
 1&amp;gt;               A      20       0       8 0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;虚基类表和虚函数表都是编译期确定，每个类共享一个。虚函数表指针和虚基类表指针都是跟着对象走的。但是具体数量会因为是虚继承或不是虚继承而导致差异。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚基类表在多重非菱形继承的时候，无论该类虚继承了几个类，该类都只有一个虚基类表。(如上面代码)&lt;/li&gt;
&lt;li&gt;如果是菱形继承的情况下，继承了几个虚继承的类，就会有几个虚基类表&lt;/li&gt;
&lt;li&gt;如果是链式虚继承，则虚基类表指针数量会累加。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class x {};
class y: public virtual x{};
class z: public virtual y{};
1&amp;gt;class z	size(16):
1&amp;gt;	+---
1&amp;gt; 0	| {vbptr} //z的虚基类表指针
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base x)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base y)
1&amp;gt; 8	| {vbptr} //y的虚基类表指针
1&amp;gt;	+---
1&amp;gt;z::$vbtable@z@:
1&amp;gt; 0	| 0
1&amp;gt; 1	| 8 (zd(z+0)x)
1&amp;gt; 2	| 8 (zd(z+0)y)
1&amp;gt;z::$vbtable@y@:
1&amp;gt; 0	| 0
1&amp;gt; 1	| 0 (zd(y+0)x)
1&amp;gt;vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1&amp;gt;               x       8       0       4 0
1&amp;gt;               y      
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;普通非虚单继承下，虚函数表中的指针会指向其继承的最近的一个类的虚函数&lt;/strong&gt;。比如B继承A，C继承B。则C会继承B的虚函数表。&lt;strong&gt;此时C的虚函数表会显示是自己的虚函数表（&lt;code&gt;C::$vftable@:&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;普通非虚多继承下，继承了几个含有虚函数的类就有几个虚函数指针和虚函数表&lt;/strong&gt;。比如C继承A，C又继承B。则C会含有两个虚函数表，然后自己的虚函数放入第一个继承的类的虚函数表。（假如先继承的B，就放到B，先继承的A就放到A）&lt;strong&gt;此时C的两个虚函数表不会显示是自己的虚函数表，而是A和B的 （&lt;code&gt;C::$vftable@B@: / C::$vftable@A@:&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果多继承有同名函数，则会有thunk。所有的虚函数都会被重定向至C重写后的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VC下，虚函数表指针排在虚基类表指针前面。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用类作用域访问运算符显式调用虚函数会压制虚函数的调用方法（不会触发多态不查表），调用法会被处理成调用非静态成员函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;严格来说虚函数表指针的切换应该是在构造函数后，用户代码前。&lt;strong&gt;也就是构造函数的最后几行&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base
{
public:
    virtual void function1() {};
    virtual void function2() {};
};
 
class D1: public Base
{
public:
    virtual void function1() {};
};
 
class D2: public Base
{
public:
    virtual void function2() {};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个最简单的例子。因为这里有三个类，所以编译器会创建三个虚函数表。&lt;/p&gt;
&lt;p&gt;编译器会在使用了虚函数的最上层的基类中的定义我们提到过的指向虚函数表的指针&lt;code&gt;_vfptr&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;加上之后长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base
{
public:
    FunctionPointer *__vptr; //注意这里
    virtual void function1() {};
    virtual void function2() {};
};
 
class D1: public Base
{
public:
    virtual void function1() {};
};
 
class D2: public Base
{
public:
    virtual void function2() {};
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是为什么说，这个&lt;code&gt;_vfptr&lt;/code&gt;指针的偏移量是0，在最开始。因为类的内存分布是按照声明顺序的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意这里为什么D1和D2没有添加&lt;code&gt;_vfptr&lt;/code&gt;，因为&lt;code&gt;_vfptr&lt;/code&gt;属于基类部分。子类是包含基类部分的。所以说这里不能再写了。也就是说，子类虽然包含自己的&lt;code&gt;_vfptr&lt;/code&gt;，但是它是继承自父类的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;_vfptr&lt;/code&gt; 在类对象创建的时候会设置成指向类的虚函数表。例如，类型 &lt;code&gt;Base&lt;/code&gt; 被实例化的时候，&lt;code&gt;_vfptr&lt;/code&gt; 就指向 &lt;code&gt;Base&lt;/code&gt; 的虚函数表。类型 &lt;code&gt;D1&lt;/code&gt; 或者 &lt;code&gt;D2&lt;/code&gt; 被实例化的时候，&lt;code&gt;_vfptr&lt;/code&gt; 就在子类调用父类构造期间，先指向 &lt;code&gt;Base&lt;/code&gt;的虚函数表，再在执行子类构造期间指向&lt;code&gt;D1&lt;/code&gt; 或者 &lt;code&gt;D2&lt;/code&gt; 的自己的虚函数表。&lt;/p&gt;
&lt;h2&gt;不应在构造和析构函数中调用虚函数，但是构造函数中可以调用非虚的类成员函数&lt;/h2&gt;
&lt;p&gt;https://pvs-studio.com/en/blog/posts/cpp/0891/&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们都知道，构造子类对象的时候，子类对象会先调用父类构造。调用父类构造的时候，我们可以理解为子类对象目前是父类类型。所以对象的&lt;code&gt;_vfptr&lt;/code&gt;指针此时会指向父类的虚函数表。所以说这个时候虚函数不是虚函数。当父类构造完毕，调用子类的构造函数之时，子类对象会被认为是子类类型。这时这个&lt;code&gt;_vfptr&lt;/code&gt;指针会指向子类自身的虚函数表，这时候虚函数才是虚函数&lt;/strong&gt;。**（严格来说虚函数表指针的切换应该是在构造函数后，用户代码前。构造函数的最后几行）**这就是为何不应在构造和析构函数中调用虚函数。因为此时的虚函数并不表现出虚函数的多态性质。举个例子，我们在父类的构造函数中使用了一个虚函数，因为这个虚函数被调用是在父类构造期间。所以即便是子类对象来调用，这个&lt;code&gt;_vfptr&lt;/code&gt;也是指向的父类的虚函数表。&lt;/p&gt;
&lt;p&gt;举例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    A(){
        p();
    }
    virtual void p(){
        cout &amp;lt;&amp;lt; &quot;A&quot; &amp;lt;&amp;lt;endl;
    }
};

class B: public A{
    public:
    B(){
        p();
    }
    void p(){
        cout &amp;lt;&amp;lt; &quot;B&quot;&amp;lt;&amp;lt;endl;
    }
};

class C :public B{
    public:
    C(){
        p();
    }
    void p(){
        cout &amp;lt;&amp;lt; &quot;C&quot; &amp;lt;&amp;lt; endl;
    }
};

int main(){
    A* a = new C; //会输出 ABC
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个程序会输出&lt;code&gt;ABC&lt;/code&gt;。因为构建子类对象会依次调用父类构造。表面上这里构造函数调用了虚函数，&lt;strong&gt;但是这个虚函数实际执行的是对应类的虚函数&lt;/strong&gt;。这就是我们说的，可以在构造函数中调用虚函数，但是不应调用。我们会发现，因为是依次构造，所以在调用对应类的构造的时候，调用的是对应类的虚函数。（C类对象在执行A类部分构造期间，调用的虚函数执行的是A类自己的。在执行B类部分构造期间，执行的是B类自己的。在执行C类部分构造期间，执行的是C类自己的）所以说并没有多态特性。&lt;strong&gt;在实际应用中，这种在构造函数中调用虚函数的时候，不会有这种每一个类的虚函数都调用了对应虚函数的情形&lt;/strong&gt;。 标准情况下，&lt;code&gt;B&lt;/code&gt;和&lt;code&gt;C&lt;/code&gt;类的构造函数是不会调用函数&lt;code&gt;P&lt;/code&gt;的。所以如果仅仅在父类&lt;code&gt;A&lt;/code&gt;中调用了&lt;code&gt;P&lt;/code&gt;，这个&lt;code&gt;P&lt;/code&gt;也会是类&lt;code&gt;A&lt;/code&gt;的&lt;code&gt;P&lt;/code&gt;，不是类&lt;code&gt;C&lt;/code&gt;的&lt;code&gt;P&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;记住：在自己类的构造函数中调用了虚函数，将会是自己类的函数。即，父类构造函数中调用的虚函数依旧是父类的，子类构造函数中调用的虚函数依旧是子类的。顺序是父类构造-&amp;gt;此时虚函数表指针指向父类虚函数表-&amp;gt;子类构造-&amp;gt;此时虚函数表指针指向子类虚函数表。（严格来说虚函数表指针的切换应该是在构造函数后，用户代码前。构造函数的最后几行）所以再次重申，父类指针指向子类对象，子类对象先调用了父类构造。父类构造中有虚函数，即便被子类重写过，但是此时子类对象仍在父类构造期间，虚函数表指针依旧指向父类的虚函数表。所以此时虚函数实际执行的不会具有多态性，会依旧执行父类虚函数而不是子类虚函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编译器会保证先调用所有基类的构造函数，然后再设置虚函数表指针，然后再调用成员的初始化操作。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;但是构造函数中可以调用非虚类成员函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;虚函数表长什么样&lt;/h2&gt;
&lt;p&gt;我们假设所有的函数都是虚函数&lt;/p&gt;
&lt;h3&gt;基类的虚函数表&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724044555.png&quot; alt=&quot;QQ截图20220724044555&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;一般继承（无虚函数覆盖*）的子类虚函数表&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724044712.png&quot; alt=&quot;QQ截图20220724044712&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724044722.png&quot; alt=&quot;QQ截图20220724044722&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时：&lt;/p&gt;
&lt;p&gt;1）虚函数按照其声明顺序放于表中。
2）父类的虚函数在子类的虚函数前面。&lt;/p&gt;
&lt;h3&gt;一般继承（有虚函数覆盖*）的子类虚函数表&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724044950.png&quot; alt=&quot;QQ截图20220724044950&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724044958.png&quot; alt=&quot;QQ截图20220724044958&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时：
1）覆盖的&lt;code&gt;f()&lt;/code&gt;函数被放到了虚表中原来父类虚函数的位置。
2）没有被覆盖的函数依旧。&lt;/p&gt;
&lt;p&gt;所以当如下代码执行时&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base *b = new Derive();
b-&amp;gt;f();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;由&lt;code&gt;b&lt;/code&gt;所指的内存中的虚函数表（子类构造结束后，子类&lt;code&gt;_vfptr&lt;/code&gt;指向自己的虚函数表）的&lt;code&gt;f()&lt;/code&gt;的位置已经被&lt;code&gt;Derive::f()&lt;/code&gt;函数地址所取代，于是在实际调用发生时，编译器检测到了&lt;code&gt;f&lt;/code&gt;是一个虚函数，所以不会静态的将&lt;code&gt;Base&lt;/code&gt;类的函数直接编译过来。，而是运行的时候，动态的根据&lt;code&gt;b&lt;/code&gt;指向的对象，找到这个对象的自己的&lt;code&gt;_vfptr&lt;/code&gt;指针找到这个对象(一个类的所有对象共用一个虚函数表)的虚函数表。然后调用虚函数表里面对应的函数。于是&lt;code&gt;Derive::f()&lt;/code&gt;被调用了。这就实现了多态。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;多重继承（无虚函数覆盖*）的子类虚函数表&lt;/h3&gt;
&lt;p&gt;假设有如下继承关系(无覆盖)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724045219.png&quot; alt=&quot;QQ截图20220724045219&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724045811.png&quot; alt=&quot;QQ截图20220724045811&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到：
1） **每个父类都有自己的虚表。**因为每一个继承，子类都会有一个对应的虚表。所以多继承，子类也会有多个虚表（虚表指针）。&lt;/p&gt;
&lt;p&gt;2） 所以子类有多重继承而来的多个&lt;code&gt;_vfptr&lt;/code&gt;以指向多个虚函数表&lt;/p&gt;
&lt;p&gt;3）&lt;strong&gt;子类的成员函数被放到了第一个父类的表中&lt;/strong&gt;。（所谓的第一个父类是按照声明顺序来判断的）
这样做就是为了解决不同的父类类型的指针指向同一个子类实例，而能够调用到实际的函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意，多重继承有多个虚函数表，而每一个虚函数表的开头都会有一个偏移量和一个RTTI信息&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;多重继承（有虚函数覆盖*）的子类虚函数表&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20221228023004.png&quot; alt=&quot;QQ截图20221228023004&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724050217.png&quot; alt=&quot;QQ截图20220724050217&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;注：覆盖又称override 重载&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/dengjin20104042056/article/details/80302193?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167221615916800182712211%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&amp;amp;request_id=167221615916800182712211&amp;amp;biz_id=0&amp;amp;utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-80302193-null-null.blog_rank_default&amp;amp;utm_term=%E8%99%9A%E5%87%BD%E6%95%B0&amp;amp;spm=1018.2226.3001.4450&quot;&gt;参考自这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;虚函数表的其他内容&lt;/h2&gt;
&lt;p&gt;虚函数表在第一个指向虚函数指针的前面还会有两个东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;一个指针，指向一个存有字符串的类(&lt;code&gt;type_info&lt;/code&gt;)。也就是储存了RTTI信息。c++运行时类型识别信息。&lt;strong&gt;我们就是靠这个来知道在有虚函数的情况下，父类指针指向子类对象的时候，子类到底是什么类型&lt;/strong&gt; 。我们知道只有带有虚函数的类才会生成虚函数表,因此&lt;strong&gt;动态类型强制转换只用于多态类型&lt;/strong&gt;，在进行动态类型转换时只要取虚函数表中的第-1个元素得到&lt;code&gt;type_info&lt;/code&gt;类对象判断其真正的类型再进行操作。这个指针是编译期设定的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个偏移量。指示类中这个&lt;code&gt;_vfptr&lt;/code&gt;离类的头指针有多远。一般情况下都是0。因为&lt;code&gt;_vfptr&lt;/code&gt;会在头部。&lt;strong&gt;但是在多继承的时候，如上图，会有多个&lt;code&gt;_vfptr&lt;/code&gt;。所以会有偏移量。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;RTTI 和虚函数的关系&lt;/h1&gt;
&lt;p&gt;https://www.jianshu.com/p/3b4a80adffa7&lt;/p&gt;
&lt;p&gt;在解释这个问题之前我们先回顾一下向上和向下转型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base{
    virtual void func(){
        //...虚函数有没有都行
    }
};
class Derive :public Base{
    
};
int main(){
    Base* b = new Derive(); //父类指针指向子类对象。这句话等同于下面这两行。也就是子转父。也就是向上转型。此行是对象切片
    Base* b = static_cast&amp;lt;Base*&amp;gt;(new Derive()); 
    Base* b = (Base*) new Derive(); //C风格
    
    Derive* d = new Base(); //错误，禁止隐式的向下转型。也就是子类指针指向父类对象。也就是父转子。下面这两行可以通过编译
    Derive* d = static_cast&amp;lt;Derive*&amp;gt;(new Base()); 
    Derive* d = (Derive*) new Base(); // C风格
    
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;static_cast&lt;/code&gt;强制类型转换不安全，因为没有类型检查。但是并不禁止编译。&lt;strong&gt;会产生未定义行为&lt;/strong&gt;。我们上一行的&lt;code&gt;Derive* d = static_cast&amp;lt;Derive*&amp;gt;(new Base());&lt;/code&gt;可能看得不够清楚。我们拆开来看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base* baseptr = new Base();
Derive* d = static_cast&amp;lt;Derive*&amp;gt;(baseptr); //不安全
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到问题了吗？我们仅仅初始化了父类成员，但是转成了子类对象。这代表会多出一块未定义的空间。&lt;/p&gt;
&lt;p&gt;但是如果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base* baseptr = new Derive(); //注意这里
Derive* d = static_cast&amp;lt;Derive*&amp;gt;(baseptr); //安全
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就是安全的，为什么？因为我们这里用的是指针。虽然子类对象隐式已经在第一行被转换成了父类类型，但是我们可以安全的手动转回来。因为我们第一行&lt;code&gt;new&lt;/code&gt;的时候初始化了全部的子类成员，隐式转换成父类后会被隐藏（&lt;strong&gt;此处使用的是指针。切记，指针类型表示编译器可以合法读取多大区域的内存。我分配了整个子类，但是我只让指针读取父类的部分，这是可以的，数据没有被清除，仅仅是指针访问不到而已。&lt;/strong&gt;），手动转回来不会出现未定义空间。&lt;/p&gt;
&lt;p&gt;在没有多态的时候，由于无法使用&lt;code&gt;dynamic cast&lt;/code&gt;导致没有类型检查导致风险较大。因为你必须要手动保证转型的指针&lt;code&gt;new&lt;/code&gt;的是子类对象而不是父类对象。但是&lt;code&gt;dynamic cast&lt;/code&gt;就帮助我们进行了一个类型检查。&lt;/p&gt;
&lt;p&gt;为了避免这种不安全的转换，所以我们需要&lt;code&gt;dynamic_cast&lt;/code&gt;来在运行时检查是否可以安全转换。这就用到了RTTI。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只有包含虚函数的类（多态类）才可以使用&lt;code&gt;dynamic_cast&lt;/code&gt;因为RTTI 依赖于虚表，所以用dynamic_cast 对应的类一定要有虚函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多态类就是有虚函数的类（直接声明或继承）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;strong&gt;没有虚函数&lt;/strong&gt;的前提下，如果父类和子类定义了相同名称的成员函数，那么通过对象指针调用成员函数时，&lt;strong&gt;到底调用那个函数要根据指针的原型来确定，而不是根据指针实际指向的对象类型确定。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Base{
  	//...没有虚函数  
};
class Derive:public Base{
    
};
int main{
    Base* ptr = new Derive(); //静态绑定。ptr实际上指向的类型是ptr的类型即父类类型
    cout &amp;lt;&amp;lt; typeid(*ptr).name() &amp;lt;&amp;lt; endl; //输出base
    return 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;strong&gt;有虚函数&lt;/strong&gt;的前提下，如果父类和子类定义了相同名称的成员函数（重写，动态绑定），那么通过对象&lt;strong&gt;指针调用&lt;/strong&gt;成员函数时，&lt;strong&gt;与当前指向类实例的父类指针类型无关，仅和类实例对象本身有关。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Base{
  	virtual void p(){
        //...
    }
};
class Derive:public Base{
    //...子类无论是否定义了相同名称的成员函数（重写），都会使父类指针指向的类型变为子类类型
};
int main{
    Base* ptr = new Derive(); //动态绑定。ptr实际上指向的类型是Derive的类型即子类类型。指针的静态类型和目标类型不同
    cout &amp;lt;&amp;lt; typeid(*ptr).name() &amp;lt;&amp;lt; endl; //输出Derive
    return 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;虽然此时&lt;code&gt;ptr&lt;/code&gt;可以被认为是指向了子类类型，但是我们不要忘记了，指针类型其实是告诉编译器应该正确解析多大的内存空间。所以此时依旧无法访问子类自己的部分 如变量和非虚函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以如果此时我们想要访问子类函数或变量应该怎么办？我们需要&lt;code&gt;dynamic_cast&lt;/code&gt;来在运行时检查是否可以安全转换。这就用到了RTTI。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;dynamic_cast&lt;/code&gt; 如果执行成功，会返回子类地址。如果转的是指针，失败会返回&lt;code&gt;nullptr&lt;/code&gt;, 如果转的是指针，失败会抛出&lt;code&gt;bad_cast&lt;/code&gt;因为引用无法为空&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;通过运行时类型信息（RTTI）能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型&lt;/h3&gt;
&lt;h3&gt;RTTI是一种概念，是一种机制。在虚函数表表头放置type_info是实现RTTI的一种方式。&lt;/h3&gt;
&lt;p&gt;我们说过，有虚函数表的开头是一个指向当前类对应的 &lt;code&gt;type_info &lt;/code&gt;对象。当程序在运行阶段获取类型信息时，可以通过对象指针找到对象自己的指向虚函数表的指针 &lt;code&gt;_vfptr&lt;/code&gt;，再通过 &lt;code&gt;_vfptr&lt;/code&gt;找到 &lt;code&gt;type_info&lt;/code&gt; 对象的指针，进而取得类型信息。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static_cast&lt;/code&gt;转型的话只要是有点关系的两个类型就可以转，转了会不会出问题他不管，用户自己负责。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dynamic_cast&lt;/code&gt;的话会根据虚表里信息去比对，这个基类指针指向的对象真的是派生类才会转换，否则就返回空指针，提醒用户不能转换。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;dynamic_cast&lt;/code&gt;的向下转换效率很低。因为需要使用一个类似于字符串比较的函数对两个类型描述器&lt;code&gt;type_info&lt;/code&gt;进行比较。但是这样却足够安全。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;一段话总结&lt;/h1&gt;
&lt;p&gt;父类指针指向子类对象，限制了父类指针读取的内存区域，只可以读取父类部分。所以我们认为这时候这个东西是父类类型。所以在没有虚函数的时候，父类指针是无法使用子类独有资源（成员变量，成员函数）的。&lt;/p&gt;
&lt;p&gt;当多态发生的时候，我们父类指针指向子类对象依旧会被编译器认为是父类类型（也可以理解为指向对象被隐式转换或者是切片）。所以只能访问子类对象的父类部分，无法访问子类独有的数据（变量，非虚函数和独有的虚函数）。但是我们说过，指向虚函数表的指针&lt;code&gt;_vfptr&lt;/code&gt;是在子类的父类部分中的。所以这个指针是可以访问的。当我们调用虚函数的时候，由于我们指向的是子类对象，也就是&lt;strong&gt;实例化的是子类对象&lt;/strong&gt;，（&lt;code&gt;Base* ptr = new Derive()&lt;/code&gt;这句话我们看到&lt;code&gt;new&lt;/code&gt;了一个&lt;code&gt;Derive&lt;/code&gt;，所以还是会进行正常的构造过程。也就是先执行父类构造，再执行子类构造。我们说过，父类构造期间，对象的&lt;code&gt;_vfptr&lt;/code&gt;会指向父类的虚函数表，然后执行子类构造期间，&lt;code&gt;_vfptr&lt;/code&gt;会指向子类虚函数表。所以&lt;strong&gt;子类构造完毕后，&lt;code&gt;_vfptr&lt;/code&gt;一定会指向自己的虚函数表&lt;/strong&gt;）。所以&lt;code&gt;_vfptr&lt;/code&gt;指向的是子类的虚函数表。所以可以通过这个指向了子类虚函数表的&lt;code&gt;_vfptr&lt;/code&gt;指针找到对应的虚函数表，然后对虚函数进行调用。**但是这个时候我们还是不可以通过父类指针访问子类的非虚函数和子类的成员变量。因为他还是被认为是父类类型。**所以这时候我们想访问子类独有资源就可以使用&lt;code&gt;dynamic_cast&lt;/code&gt;来进行安全的转换。&lt;/p&gt;
&lt;p&gt;这个&lt;code&gt;dynamic_cast&lt;/code&gt;是怎么用的呢？我们父类指针指向子类对象的时候，限制了指针仅可以读取子类对象的父类部分。我们进行&lt;code&gt;dynamic_cast&lt;/code&gt;的时候，会使用RTTI信息。这个信息是储存在虚函数表的头部。也就是编译器找到了虚函数表指针（子类对象的虚函数表指针一定指向自己的虚函数表，所以RTTI信息类型也是子类），然后问RTTI：”这人想要让我转换成子类，也就是多读点儿子类的东西，你帮我看看我指向的对象到底是不是子类，多读的那部分被正确初始化了没有？合法不合法？“。RTTI看了一眼回答：”哦，你指向的部分实际类型确实是一个子类的类型，因为他&lt;code&gt;new&lt;/code&gt;的时候&lt;code&gt;new&lt;/code&gt;的是一个子类对象。我检查了，都初始化了，也合法，那你就多读点儿吧“。这就是&lt;code&gt;dynamic_cast&lt;/code&gt;和RTTI的联系。&lt;/p&gt;
&lt;p&gt;所以虚函数表的RTTI应该就是进行一个安全检查。&lt;/p&gt;
&lt;h1&gt;多态中 this指针的切换&lt;/h1&gt;
&lt;p&gt;这里有了虚函数表指针就比较好理解。在一开始子类对象构造的时候，先构造父类部分，此时this指针的类型是父类，虚函数表指针也指向父类=虚函数表，因为此时这个对象是父类类型。等到构造到了子类部分，子类进行构造的时候，this指针切换成子类类型，虚函数表指针切换到子类虚函数表。此时对象才是子类类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意，this指针的切换是在执行期。（废话）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;还有，this指针的类型是要看具体在哪个类里面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base{
    public:
    void func(){
        cout &amp;lt;&amp;lt;&quot;base func&quot; &amp;lt;&amp;lt; endl;
    }
};

class Derive:public Base{
    public:
    void call(){
        func();
    }
};
int main() {
    Base* ptr = new Derive();
    Derive* ptr1 = new Derive();

    ptr-&amp;gt;func();        //1
    ptr1-&amp;gt;call();       //2
    ptr1-&amp;gt;func();       //3

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这段代码内，&lt;code&gt;func&lt;/code&gt;的this指针类型为base，call的this指针为derive&lt;/p&gt;
&lt;h1&gt;静态类型 动态类型和 为什么多态只能使用指针或者引用？&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;引用可以实现多态的原因是引用的底层也是指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;c++确定类型分两种，一种是对象，一种是指针或者是引用。如果是对象，那就是我们编译时确定它是什么玩意 即静态绑定。即便此时使用的是子类对象给父类对象赋值，但是对象没有动态类型。所以肯定是使用其静态类型。&lt;/p&gt;
&lt;p&gt;如果是指针或引用，在设计层面他们就被允许指向声明类型或者是声明类型的子类的对象。也就是进行了动态类型判定的动态绑定。**而对象永远不应该改变它的类型。这就是对象模型的工作方式。**所以只有在用指针和引用调用的时候才发生了这个动态类型确定（RTTI）和虚函数表查询并且执行正确的函数。&lt;/p&gt;
&lt;p&gt;而通过对象调用则不会有这种情况。所以在直接使用对象赋值的时候，拷贝赋值这种东西会忽略掉&lt;code&gt;_vfptr&lt;/code&gt;这种只有动态绑定才会激活的东西，只会拷贝赋值普通成员变量。所以哪怕子类的&lt;code&gt;_vfptr&lt;/code&gt;和虚函数表继承了父类的，也确实是父类的，但是因为他是个&lt;code&gt;_vfptr&lt;/code&gt;，规定只有在指针或者引用的情况才会被激活，所以会在拷贝赋值的时候忽略。&lt;/p&gt;
&lt;p&gt;深度探索C++对象模型中提到：&lt;/p&gt;
&lt;p&gt;“一个pointer或一个reference之所以支持多态，是因为它们并不引发内存任何“与类型有关的内存委托操作； 会受到改变的。只有它们所指向内存的大小和解释方式 而已”&lt;/p&gt;
&lt;p&gt;对这句话解释就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指针和引用类型只是要求了基地址和这种指针所指对象的内存大小，与对象的类型无关。&lt;strong&gt;指针和引用并不涉及内存中对象的类型转换，只是改变了解释方式。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;而把一个派生类对象直接赋值给基类对象，就牵扯到对象的类型问题（比如切割），编译器就会回避之前的的虚机制。从而无法实现多态。&lt;/strong&gt;（对象直接调用调用赋值&lt;code&gt;=&lt;/code&gt;会发生转型）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;每一个实体（对象，指针或引用）都有静态类型和动态类型两种。它们可能不一致。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态类型指的是不需要考虑表达式的执行期语义，仅分析程序文本而决定的表达式类型。静态类型仅依赖于包含表达式的程序文本的形式，而在程序运行时不会改变。通俗的讲，就是上下文无关，&lt;strong&gt;在编译时就可以确定其类型&lt;/strong&gt;。&lt;strong&gt;也就是声明时的类型。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对象、引用或指针的静态类型决定了该对象的哪些成员是可见的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;动态类型指的是变量或者表达式表示的内存中的对象的类型，动态类型直到运行时才可知。&lt;strong&gt;只有指针或引用才有动态类型。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在编译期间，所有变量（对象）的静态类型都是其声明时类型。如果是指针或者是引用，其静态类型依旧是其声明时类型。但是其动态类型会被推迟至运行时确定。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那么如何确定一个变量在运行时到底使用了其静态类型还是其动态类型呢？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先确定该对象（变量）确实有动态类型。也就是使用了指针或引用形态。（因为只有这种情况才有动态类型）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用场景为带虚函数的继承体系结构。（只有虚函数表里才有type_info）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须子类重写了父类的虚函数。因为父类指针无法访问子类独有的成员（变量，非虚函数和独有的虚函数）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查静态类型和动态类型是否相等。（运行时通过RTTI形式, 访问虚函数表的type_info确定其类型）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果该对象的静态类型和动态类型不相等，并且调用了（重写的）虚函数，采用其动态类型。否则采用静态类型（废话）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;所以如果&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用对象调用，不满足条件1，使用静态类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有虚函数或没有重写，不满足条件2，使用静态类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果类型一致，比如父类指针指向父类对象，不满足条件3，使用静态类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果使用对象或引用调用（满足条件1）虚函数（满足条件2），并且发生了父类指针指向子类对象（满足条件3）的时候才确认使用其动态类型。这样就可以触发多态。但是具体调用父类还是子类虚函数依旧要看表内的具体内容，也就是子类重写了父类虚函数。否则查表查到的依旧是父类的虚函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设有以下继承结构：&lt;code&gt;class Drived : public Base&lt;/code&gt;，然后有表达式&lt;code&gt;p-&amp;gt;mem()&lt;/code&gt;或&lt;code&gt;obj.mem()&lt;/code&gt;，其中&lt;code&gt;mem()&lt;/code&gt;是一个类成员函数，但具体是基类还是子类，现在可以不用管，而且我们也不用管&lt;code&gt;p&lt;/code&gt;或者&lt;code&gt;obj&lt;/code&gt;是指向哪个类。当程序的编译期，当编译器遇到表达式&lt;code&gt;p-&amp;gt;mem()&lt;/code&gt;或&lt;code&gt;obj.mem()&lt;/code&gt;，执行以下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先确定&lt;code&gt;p&lt;/code&gt;（或&lt;code&gt;obj&lt;/code&gt;）的静态类型，即声明类型；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后在&lt;code&gt;p&lt;/code&gt;（或&lt;code&gt;obj&lt;/code&gt;）的静态类型中查找&lt;code&gt;mem()&lt;/code&gt;函数，如果没有找到，按照作用域规则，这时编译器会到其直接基类中寻找，并依次向上，直到达到继承链的顶端，如果在这个过程中找到了，则继续下一步，如果没有找到，则报错；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一旦找到名字匹配的&lt;code&gt;mem()&lt;/code&gt;函数，则编译器会根据&lt;code&gt;mem()&lt;/code&gt;是否是虚函数，以及我们是通过指针或引用（如&lt;code&gt;p&lt;/code&gt;）来调用&lt;code&gt;mem()&lt;/code&gt;函数，还是通过类对象（如&lt;code&gt;obj&lt;/code&gt;）来调用&lt;code&gt;mem()&lt;/code&gt;函数，生成不同的代码。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;mem()&lt;/code&gt;是虚函数且是通过指针或引用调用（如&lt;code&gt;p-&amp;gt;mem()&lt;/code&gt;），编译器生成的代码会检查其动态类型，这就是动态绑定的过程，也就是说编译器生成的代码会直到运行期确定了动态类型才决定具体调用基类还是子类的虚函数&lt;code&gt;mem()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;mem()&lt;/code&gt;不是虚函数或是通过对象调用（如&lt;code&gt;obj.mem()&lt;/code&gt;），则编译器会产生一个常规的函数调用代码，可以直接确定调用哪一个&lt;code&gt;mem()&lt;/code&gt;函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实可以直接理解为：动态类型就是为动态绑定（C++继承的多态）准备的。只有当上述3个条件都满足了，动态类型才能发挥其威力（也就是采用对象的动态类型，即很好的支持虚函数动态绑定机制）。如果条件不全部满足，则依旧采用对象的静态类型。&lt;/p&gt;
&lt;p&gt;注意，这里的动态类型并不能代表我们可以通过父类指针调用子类独有的数据成员（变量，非虚函数和独有的虚函数）。如果需要调用独有的数据成员，依旧需要&lt;code&gt;dynamic_cast&lt;/code&gt;进行操作。&lt;/p&gt;
&lt;p&gt;**对于虚函数，执行时实际调用该函数的对象类型为该指针或引用所指对象的实际类型。也就是在此时，如果满足了使用动态类型的几个条件，则调用方（父类指针）在调用时可以被解释为所指对象的实际类型（子类类型）。**也就是p-&amp;gt;mem()的时候，p的类型在调用时（执行时）被解释为子类类型。但是p的静态类型依旧是父类类型。&lt;/p&gt;
&lt;p&gt;第二点，我们指针指向的对象没法互相赋值，赋值只能是交换指针。**指针的拷贝赋值是指针类型自己的，和指向的对象类型无关。**所以这一层面的拷贝赋值使用的是对象的拷贝赋值，而不是指针的拷贝赋值。所以自然忽略掉了&lt;code&gt;_vfptr&lt;/code&gt;对应的内容。&lt;/p&gt;
&lt;p&gt;我们梳理一下，假设我们有下面代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A  
{  
public:  
    virtual void T(){}  
    virtual void print()  
    cout&amp;lt;&amp;lt;&quot;A&quot;&amp;lt;&amp;lt;endl;
};  
class B:public A  
{  
public:  
    void print()  
    cout&amp;lt;&amp;lt;&quot;B&quot;&amp;lt;&amp;lt;endl;  
};  
int main()  
{  
  A a;  
  B b;  
  a = b;  
  a.print();                         //通过对象直接访问  
  b.print();  
  return 0;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的&lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt;已经被构造完毕，所以此时&lt;code&gt;_vfptr&lt;/code&gt;已经指向了自己的。&lt;strong&gt;哪怕我们没有使用多态调用，但是由于类内存在虚函数，则一定存在虚函数表。对象也一定存在&lt;code&gt;_vfptr&lt;/code&gt;指针。（我可以不用，但不能没有）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220823022745.png&quot; alt=&quot;QQ截图20220823022745&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们可以看到，对象&lt;code&gt;b&lt;/code&gt;的虚函数表指针和虚函数表被认为是在A里面的，也就是继承来的。但是继承是复制。所以才有那句话：每一个类都有一个自己的虚函数表，每一个对象都有一个自己的虚函数表指针&lt;code&gt;_vfptr&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当我们执行到&lt;code&gt;a = b&lt;/code&gt;的时候，虚函数表的内容并没有被替换。所以我们可以认为，拷贝赋值或者拷贝构造的时候会忽略掉任何和虚函数相关的东西，只针对可以拷贝的东西进行操作。默认的赋值运算符并不会操作虚函数表。一个类对象里面的&lt;code&gt;_vfptr&lt;/code&gt;永远不会变，永远都会指向所属类型的虚函数表，因为对象的语义就是类型永远不会改变。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220823024407.png&quot; alt=&quot;QQ截图20220823024407&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一句话总结：虽然子类的虚表指针和虚函数表是继承自父类，并且认为是父类的，但是在把子类对象拷贝给父类对象的时候，编译器会忽略掉所有虚机制的部分。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;记住：无论是否使用多态调用，只要这个对象的类里面有虚函数，他一定会有虚函数表并且会把虚函数放到虚函数表。~~只要是虚函数，我无论是否触发多态调用，他都会去查表。对象调用不会触发多态的原因是对象赋值忽略虚指针的部分，所以等号左手边的对象依旧会用自己类类型的虚函数表。会查到自己类的虚函数。~~所以不会有多态行为。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，这句*“无论是否触发多态调用，都会去查表。”*不准确。会有编译器优化，如上一段说的，如果是非虚函数或通过对象调用，直接不使用虚函数表。编译器会产生一个常规的函数调用代码，可以直接确定调用哪一个函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多态调用无法访问子类独有的虚函数。因为我们记得，子类独有的虚函数是放到虚函数表的后面的。也就是说，我们通过对象的虚函数表指针查表的时候，由于子类独有的数据成员不属于父类部分，也就是处于父类指针解释不到的位置。所以父类无法访问。假如父类的虚函数表是50字节，子类添加了独有的所以是80字节。但是父类通过虚函数表指针访问的时候，依旧是只能看到前50字节。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;又及，父类指针指向子类对象的时候，访问子类独有数据/子类独有函数/子类独有虚函数的时候都不满足多态调用，所以指针在调用时依旧解释为其静态类型。而且父类指针不含有子类独有数据，所以并不能满足要求所以无法调用。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更多可以看深度探索c++对象模型的1.3和虚函数章节。包括笔记的2.2拷贝构造函数部分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E5%9B%BE%E7%89%8720230108101950.jpg&quot; alt=&quot;QQ图片20230108101950&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从此图我们可以看到，由于是虚函数所以并不能直接call。需要首先拿到对象地址（6988），然后拿出对象的虚函数表地址（698B），然后找到虚函数在虚函数表的地址放入eax（6990）[这里因为这个函数是第二个虚函数，所以+4了。图里是32位所以+4, 64位正常+8]。然后调用函数（6993）&lt;/p&gt;
&lt;h1&gt;对一个指针解引用是否具备多态性？&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class father{
    public:
        father()= default;

        virtual void func(){
            cout &amp;lt;&amp;lt;&quot;father&quot; &amp;lt;&amp;lt; endl;
        }
};

class child:public father{
    public:
        child() = default;
        void func(){
            cout &amp;lt;&amp;lt; &quot;child&quot; &amp;lt;&amp;lt; endl;
        }
};

int main(){

    father* fptr = new child; //父类指针指向子类对象，注意指针自己是父类。也就是fptr的静态类型是父类
    auto s = *fptr; //解引用依靠的是静态类型。所以此时s是father类型。
    (*fptr).func();//输出child
    s.func(); //输出father。
    
    auto&amp;amp; ss = *fptr;
    ss.func(); //正确输出child
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对一个指针解引用，返回的是其静态类型的引用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/image-20230120024852905.png&quot; alt=&quot;image-20230120024852905&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所以说，我们&lt;code&gt;fptr&lt;/code&gt;静态类型是&lt;code&gt;father&lt;/code&gt;。它一直是&lt;code&gt;father&lt;/code&gt;。因为类型需要编译时解析。我们上面提到了，***在编译期间，所有变量（对象）的静态类型都是其声明时类型。如果是指针或者是引用，其静态类型依旧是其声明时类型。但是其动态类型会被推迟至运行时确定。***所以我们对它解引用得到的是一个&lt;code&gt;father&amp;amp;&lt;/code&gt;。这样做如果赋值，因为使用一个引用给一个变量赋值的时候会调用其拷贝构造（杂记2提到过）。那么等号左侧的&lt;code&gt;auto&lt;/code&gt;自然是&lt;code&gt;father&lt;/code&gt;类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果直接调用，因为解引用返回&lt;code&gt;father&amp;amp;&lt;/code&gt;，它是一个引用，那么基于这个引用自然可以正确施加多态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后，如果我们使用引用去接，则没有问题。正常多态。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/75181450/why-dereference-an-polymorphism-pointer-will-get-the-pointers-type-rather-than#75181533&lt;/p&gt;
&lt;h1&gt;符合条件的函数入参依旧可以触发多态&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class base{
    public:
    void func1(){
        cout &amp;lt;&amp;lt; &quot;base1&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void func2(){
        cout &amp;lt;&amp;lt; &quot;base2&quot; &amp;lt;&amp;lt; endl;
    }
};

class derive:public base{
    public:
    void func1(){
        cout &amp;lt;&amp;lt; &quot;derived1&quot; &amp;lt;&amp;lt; endl;
    }
    void func2(){
        cout &amp;lt;&amp;lt; &quot;derived2&quot; &amp;lt;&amp;lt; endl;
    }
};

void func(base&amp;amp; b){
    b.func1();
    b.func2();
}

int main(){
    derive obj;
    func(obj);
    //输出Base1, derived2
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的函数签名是传入一个父类的引用（指针也可以）。然而我们其实传入的是子类的对象（指针也可以），这样是因为函数的入参其实就是一个拷贝过程。意思就是一个derive类型的对象初始化了base类型的引用（符合引用多态）或者是一个derive类型的指针赋值给了base类型的指针（符合指针多态）&lt;/p&gt;
&lt;h1&gt;多重继承下的thunk 深度探索c++4.2&lt;/h1&gt;
&lt;p&gt;在了解什么是thunk之前，我们先明确三个事情。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;this指针永远指向调用方。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数形参的this指针的类型永远是类类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;this指针的类型判定依靠的是静态类型而不是动态类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意这个和虚函数表指针不一样。虚函数表指针在指针赋值的时候不会改变。只有对象赋值的时候会有额外操作即忽略掉虚机制的赋值。（虚函数表指针不会被赋值）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;什么叫this指针的类型判定依靠&lt;/strong&gt;的是静态类型而不是动态类型&lt;/h3&gt;
&lt;p&gt;这句话解释出来就是&lt;code&gt;Base* ptr = new Derive()&lt;/code&gt;这行代码，父类指针指向子类对象。但是此时这个对象被认为是父类类型。又因为this指针依靠静态类型判断，所以说此时子类对象的this指针会被认为是父类类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以这时我们想要进行多态或非多态函数调用，就必须将this调整为对应类型。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这句话的意思是，假如我们父类有一个函数是&lt;code&gt;func&lt;/code&gt;，那么我们调用这个函数的时候，this指针类型必须被转换为父类类型。如果此时子类有一个函数是&lt;code&gt;func1&lt;/code&gt;，那么调用这个函数的时候，this指针类型必须被转换为子类类型。&lt;/p&gt;
&lt;h2&gt;多重继承&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220829081138.png&quot; alt=&quot;QQ截图20220829081138&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class Base1 {
public:
    Base1() {};
    virtual ~Base1() {}; 				//虚析构
    void test() {
        cout &amp;lt;&amp;lt; &quot;Base1&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void func() {				//同名虚函数

    }
    int a;
};
class Base2 {
public:
    Base2() {};
    virtual ~Base2() {};				//虚析构
    virtual void Base2func(){}			//子类没有重写该函数。
    virtual void func() {				//同名虚函数

    }
    int b;
};

class Derive: public Base1, public Base2 {
public:
    Derive(){}
    virtual ~Derive(){}					//虚析构
    void test() {
        cout &amp;lt;&amp;lt; &quot;Derive&quot; &amp;lt;&amp;lt; endl;
    }
    virtual void func() {				//同名虚函数重写

    }
    int c;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;1&amp;gt;class Derive	size(40):
1&amp;gt;	+---
1&amp;gt; 0	| +--- (base class Base1)
1&amp;gt; 0	| | {vfptr}
1&amp;gt; 8	| | a
1&amp;gt;  	| | &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	| +---
1&amp;gt;16	| +--- (base class Base2)
1&amp;gt;16	| | {vfptr}
1&amp;gt;24	| | b
1&amp;gt;  	| | &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	| +---
1&amp;gt;32	| c
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;Derive::$vftable@Base1@:
1&amp;gt;	| &amp;amp;Derive_meta
1&amp;gt;	|  0
1&amp;gt; 0	| &amp;amp;Derive::{dtor}
1&amp;gt; 1	| &amp;amp;Derive::func
1&amp;gt; 2	| &amp;amp;Derive::func1
1&amp;gt;Derive::$vftable@Base2@:
1&amp;gt;	| -16
1&amp;gt; 0	| &amp;amp;thunk: this-=16; goto Derive::{dtor}		//thunk 出现在非第一基类的剩余的虚函数表内。且必须是发生同名虚函数重写
1&amp;gt; 1	| &amp;amp;Base2::Base2func						//子类没有重写且不是重名，不触发thunk
1&amp;gt; 2	| &amp;amp;thunk: this-=16; goto Derive::func		//thunk
1&amp;gt;Derive::{dtor} this adjustor: 0
1&amp;gt;Derive::func this adjustor: 0
1&amp;gt;Derive::func1 this adjustor: 0
1&amp;gt;Derive::__delDtor this adjustor: 0
1&amp;gt;Derive::__vecDelDtor this adjustor: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们可以非常清楚地看到三件事情：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base1和Base2互相不包含。因为是多继承。&lt;/li&gt;
&lt;li&gt;虚函数被重写。虚函数被编译器认为是同名虚函数&lt;/li&gt;
&lt;li&gt;同名虚函数func被重写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这三件事情隐含的信息有如下几条：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为是多继承，而且都有虚函数，所以Derive会有两张虚函数表。一张是Base1 的 一张是Base2 的&lt;/li&gt;
&lt;li&gt;因为是非虚多继承，所以子类自己的虚函数会被添加至第一基类的虚函数表上。&lt;/li&gt;
&lt;li&gt;子类同名虚函数重写会重写掉所有类的同名虚函数。此时这个类的两张虚函数表里的同名函数如果被子类重写了，则这两张虚函数表对应的同名虚函数都会被替换。也就是此时可以看做只有一个func。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还记得我们说过，调用成员函数时，this指针会被转为对应类型的成员吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们来看看多继承会有什么问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base2* ptr = new Derive();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们不用想都知道，这个时候这个ptr的this指针应该指向的是上图黄色框的部分。如果此时我们多态调用了子类虚函数，我们如何转型？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们需要把指针往上面推，推到子类的起始点，也就是推到头。黑色框是整个子类对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们发现，第一基类的this指针位置一定和子类的this指针位置一样。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以此时我们要有一个偏移量，这个偏移量就是Base1的大小。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是不仅仅多继承的函数调用需要调整this指针，只要是继承都需要。那么什么是thunk呢？&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;thunk是 调整this指针的位置 + 跳转至对应虚函数进行执行的整合体&lt;/h2&gt;
&lt;p&gt;我们既然需要一个地方保存this指针的偏移量，我们还要虚函数表保存对应的虚函数地址。那么就意味着要存两个东西。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是，我们知道，子类的this指针一定会被推到开头。因为子类对象是整个黑框。而且我们还知道，子类重写同名虚函数，所有虚函数执行哪个都一样。那么我们为什么不直接让其余基类的虚函数表里面，同名的虚函数部分只存一个this偏移量，然后直接回头从第一基类的虚函数表找到对应函数来执行呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**对 可以这样。这就是thunk。具体的跳转是汇编层面的优化。**所以我们看到了这样的东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1&amp;gt;Derive::$vftable@Base2@:
1&amp;gt;	| -16
1&amp;gt; 0	| &amp;amp;thunk: this-=16; goto Derive::{dtor}		
1&amp;gt; 1	| &amp;amp;thunk: this-=16; goto Derive::func		
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面不仅有一个this指针的偏移量，还有一个goto语句。这就是跳转到了要执行的目标函数。也就是子类的虚函数。&lt;/p&gt;
&lt;p&gt;这里-16是因为，第一基类现在有一个虚函数表指针，地址是+8，还有一个&lt;code&gt;int&lt;/code&gt;，地址是+8+4=+12。然后补齐到8的倍数，也就是+4 = +16。所以&lt;code&gt;Base2*&lt;/code&gt;指向的位置是+16。要推到头就是-16。&lt;/p&gt;
&lt;p&gt;thunk伪代码会长得像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base2_dtor_thunk:
	this = this - sizeof(base1);
	Derive::~Derive(this);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么单继承没有thunk&lt;/h3&gt;
&lt;p&gt;最简单的理由：&lt;strong&gt;单继承只有一张虚函数表。无论这个继承链有多长&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;所以什么时候会有thunk 和 thunk的规则&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;子类一定要重写多继承中的同名虚函数（此处包括虚析构的重写）。&lt;/li&gt;
&lt;li&gt;重写的函数理论上应该被覆盖至所有的继承的父类的虚函数表内。但是除第一基类外，其余基类的虚函数表的对应位置被优化了，优化为这个thunk。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;在上面两条的基础上，三种情况会导致触发thunk 而调整this指针&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Base2* ptr = new Derive();
delete ptr;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里需要调整的原因是，本来this指针指向黄色框部分，但是调用了子类重写的同名虚函数（析构函数）则需要把this转换为子类类型传入。所以this需要推到头部。也就是-16&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Derive* ptr = new Derive();
ptr-&amp;gt;Base2func();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此处是子类调用了一个没有重写且不同名的父类函数。此时因为该函数在Base2内，所以this指针必须转换为Base2。所以这里this需要往下推到黄色框也就是Base2部分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三种是协变，过于复杂 不看了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;虚继承：&lt;/h1&gt;
&lt;p&gt;在VC下面，有如下规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;虚继承的类元素一般会被放到类对象内存分布的最后尾。这里指的是直接虚继承。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为啥放到屁股？因为我们B虚继承A，C虚继承A。这时候B和C分别拥有一个虚基类表指针，虚基类表和A的部分。然后我们有D继承B和C的时候，我们依旧会继承B和C的虚基类表指针和虚基类表。然后这时候会有一个vib表记录基类A的偏移量之类的信息。通过这些vbi表和虚基类表我们可以做到将基类A只留存一份在D中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另外一个原因是，我们把虚继承的类分割成了两部分。非虚继承的部分做为不变区域，虚继承的部分是共享（动态区域）因为虚继承会导致偏移量的变化。所以要在开头放一个虚基类表指针来正确指向虚基类的内容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用这种虚基类表的好处在于虚基类表属于类，整个类共享。无论有多少个对象都只有一次开销。而且虚基类表指针是一定要有的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为啥不放到头？放到头了我指针放哪？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当子类虚继承了父类的时候，子类包含有父类的全部数据和自己的虚基类表。直到当有一个类继承了两个类，这两个类都虚继承了同一个类的时候，这部分相同的数据才会被合并。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;class A{
    //A数据
};
class B: virtual public A{
    //B数据
    //此时B类包含A类全部数据和自己的虚基类表
};
class C: virtual public A{
    //C数据
    //此时C类包含A类全部数据和自己的虚基类表    
};

class D: public B, public C{
    //D数据
    //此时，D继承了两个类，这两个类虚继承自同一个类，这个时候相同的部分也就是A类数据才会被合并。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;这是不同编译器不一样。编译器决定的。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在一个类虚继承多个类的时候（多重虚继承），无论虚继承了多少类，该类只会有一个虚基类表指针&lt;code&gt;_vbptr&lt;/code&gt;和一个虚基类表。因为此类是虚继承的开始类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在一个类虚继承了一个虚继承过的类的时候，会有两个虚基类表。一个是自己的，因为自己在虚继承。另一个是继承来的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以只有在本类虚继承了其他类的时候，本类才会生成一个虚基类表和虚基类表指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Z {
    int v;
};

class X {
    int v;
};

class Y : public virtual Z, public virtual X {
    int k;
};
1&amp;gt;class Y	size(24):
1&amp;gt;	+---
1&amp;gt; 0	| {vbptr}						//仅有一个虚基类表指针
1&amp;gt; 8	| k
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base Z)
1&amp;gt;16	| v
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base X)
1&amp;gt;20	| v
1&amp;gt;	+---
1&amp;gt;Y::$vbtable@:
1&amp;gt; 0	| 0
1&amp;gt; 1	| 16 (Yd(Y+0)Z)
1&amp;gt; 2	| 20 (Yd(Y+0)X)
1&amp;gt;vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1&amp;gt;               Z      16       0       4 0
1&amp;gt;               X      20       0       8 0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在链式虚继承中，虚基类表指针数量会累加。也就是查看继承链上发生了几次虚继承。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class x {};
class y: public virtual x{};
class z: public virtual y{};
1&amp;gt;class z	size(16):
1&amp;gt;	+---
1&amp;gt; 0	| {vbptr} //z的虚基类表指针
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base x)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base y)
1&amp;gt; 8	| {vbptr} //y的虚基类表指针
1&amp;gt;	+---
1&amp;gt;z::$vbtable@z@:
1&amp;gt; 0	| 0
1&amp;gt; 1	| 8 (zd(z+0)x)
1&amp;gt; 2	| 8 (zd(z+0)y)
1&amp;gt;z::$vbtable@y@:
1&amp;gt; 0	| 0
1&amp;gt; 1	| 0 (zd(y+0)x)
1&amp;gt;vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1&amp;gt;               x       8       0       4 0
1&amp;gt;               y      
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在虚继承中，&lt;strong&gt;无论是否是多重虚继承&lt;/strong&gt;，如果是&lt;strong&gt;只重写了对应虚继承的类的虚函数&lt;/strong&gt;，则继承了多少个含有虚函数的类，就有多少个虚函数表指针&lt;code&gt;_vfptr&lt;/code&gt;和对应数量的虚函数表。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Z {
    int v;
    virtual void func(){}
};

class X {
    int v;
    virtual void func1() {}
};

class Y : public virtual Z, public virtual X {
    int k;
    virtual void func() {}			//重写
    virtual void func1() {}			//重写
};
1&amp;gt;class Y	size(48):
1&amp;gt;	+---
1&amp;gt; 0	| {vbptr}							//虚基类表指针，只有一个
1&amp;gt; 8	| k
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base Z)
1&amp;gt;16	| {vfptr}							//Z的虚函数表指针
1&amp;gt;24	| v
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base X)
1&amp;gt;32	| {vfptr}							//X的虚函数表指针
1&amp;gt;40	| v
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;Y::$vbtable@:
1&amp;gt; 0	| 0
1&amp;gt; 1	| 16 (Yd(Y+0)Z)
1&amp;gt; 2	| 32 (Yd(Y+0)X)
1&amp;gt;Y::$vftable@Z@:
1&amp;gt;	| -16
1&amp;gt; 0	| &amp;amp;Y::func
1&amp;gt;Y::$vftable@X@:
1&amp;gt;	| -32
1&amp;gt; 0	| &amp;amp;Y::func1
1&amp;gt;Y::func this adjustor: 16
1&amp;gt;Y::func1 this adjustor: 32
1&amp;gt;vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1&amp;gt;               Z      16       0       4 0
1&amp;gt;               X      32       0       8 0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在虚继承中，&lt;strong&gt;无论是否是多重虚继承&lt;/strong&gt;，如果是&lt;strong&gt;不仅仅重写了对应虚继承的类的虚函数，而且增加了自己的虚函数，则会在上面一条的基础上再增加一个自己的虚函数表和虚函数表指针&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;注意这里是虚继承和非虚继承的区别。非虚继承会在第一个继承的虚函数表后面新增。虚继承则会创建新的虚函数表。&lt;/li&gt;
&lt;li&gt;注意这里Y是直接虚继承了Z和X。所以Z和X会被按照顺序排在Y的后面。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Z {
    int v;
    virtual void func(){}
};

class X {
    int v;
    virtual void func1() {}
};

class Y : public virtual Z, public virtual X {
    int k;
    virtual void func() {}			//重写
    virtual void func1() {}			//重写
    virtual void func2() {}			//!新增

};
1&amp;gt;class Y	size(56):
1&amp;gt;	+---
1&amp;gt; 0	| {vfptr}								//自己的虚函数表指针
1&amp;gt; 8	| {vbptr}								//虚基类表指针，只有一个
1&amp;gt;16	| k
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base Z)
1&amp;gt;24	| {vfptr}								//Z的虚函数表指针
1&amp;gt;32	| v
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base X)
1&amp;gt;40	| {vfptr}								//X的虚函数表指针
1&amp;gt;48	| v
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;Y::$vftable@Y@:
1&amp;gt;	| &amp;amp;Y_meta
1&amp;gt;	|  0
1&amp;gt; 0	| &amp;amp;Y::func2
1&amp;gt;Y::$vbtable@:
1&amp;gt; 0	| -8
1&amp;gt; 1	| 16 (Yd(Y+8)Z)		//此处的Y+8意思是这个虚基类表指针距离该类头部的偏移量。因为虚基类表指针在虚函数表指针后面，所以+8了
1&amp;gt; 2	| 32 (Yd(Y+8)X)
1&amp;gt;Y::$vftable@Z@:
1&amp;gt;	| -24
1&amp;gt; 0	| &amp;amp;Y::func
1&amp;gt;Y::$vftable@X@:
1&amp;gt;	| -40
1&amp;gt; 0	| &amp;amp;Y::func1
1&amp;gt;Y::func this adjustor: 24
1&amp;gt;Y::func1 this adjustor: 40
1&amp;gt;Y::func2 this adjustor: 0
1&amp;gt;vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1&amp;gt;               Z      24       8       4 0
1&amp;gt;               X      40       8       8 0

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在菱形虚继承中，&lt;strong&gt;继承了几个虚继承的类，就会有几个虚基类表&lt;/strong&gt;。因为此时中间类已经含有了虚基类表，此处是继承下来。而且，无论本类是否有新增虚函数，&lt;strong&gt;虚函数表的数量和普通多继承的算法一致，也就是继承了几个虚函数的类就有几个虚函数表。自己新增的虚函数会被写在第一个继承的虚函数表内。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;注意，这里类Y没有直接虚继承X和W 所以X和W依旧按照正常继承顺序排列在Y的前面。直到X和W直接虚继承了Z，Z排在最后尾。所以此时X和W的虚函数表和虚基类表和变量都是正常顺序。整体顺序是X, W, Y, Z。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Z {
    int Zzz;
    virtual void func(){}				//虚函数，Z有自己的虚函数表。
};

class X:public virtual Z {				//虚继承。X有自己的虚基类表
    int Xxx;
    virtual void func1() {}				//此处有重名函数。虚函数，X有自己的虚函数表。
};
class W :public virtual Z {				//虚继承。W有自己的虚基类表
    int Www;
    virtual void func1() {}				//此处有重名函数。虚函数，W有自己的虚函数表。
};
class Y : public X, public W {			//菱形继承
    int Yyy;
    virtual void func() {}				//重写
    virtual void func1() {}				//重写
    virtual void func2() {}				//新增！！

};

1&amp;gt;class Y	size(72):
1&amp;gt;	+---
1&amp;gt; 0	| +--- (base class X)
1&amp;gt; 0	| | {vfptr}							//X自己的虚函数表指针
1&amp;gt; 8	| | {vbptr}							//X自己的虚基类表指针
1&amp;gt;16	| | Xxx
1&amp;gt;  	| | &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	| +---
1&amp;gt;24	| +--- (base class W)
1&amp;gt;24	| | {vfptr}							//W自己的虚函数表指针
1&amp;gt;32	| | {vbptr}							//W自己的虚基类表指针
1&amp;gt;40	| | Www
1&amp;gt;  	| | &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	| +---
1&amp;gt;48	| Yyy
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;	+--- (virtual base Z)					//虚基类元素放在最后！！
1&amp;gt;56	| {vfptr}							//Z自己的虚函数表指针
1&amp;gt;64	| Zzz
1&amp;gt;  	| &amp;lt;alignment member&amp;gt; (size=4)
1&amp;gt;	+---
1&amp;gt;Y::$vftable@X@:
1&amp;gt;	| &amp;amp;Y_meta
1&amp;gt;	|  0
1&amp;gt; 0	| &amp;amp;Y::func1
1&amp;gt; 1	| &amp;amp;Y::func2								//Y先继承的X，所以新增的虚函数放入了X的虚函数表	
1&amp;gt;Y::$vftable@W@:
1&amp;gt;	| -24
1&amp;gt; 0	| &amp;amp;thunk: this-=24; goto Y::func1		//重名函数导致的thunk
1&amp;gt;Y::$vbtable@X@:
1&amp;gt; 0	| -8
1&amp;gt; 1	| 48 (Yd(X+8)Z)	//此处的X+8意思是这个虚基类表指针距离该类（X）头部的偏移量。因为虚基类表指针在虚函数表指针后面，所以+8了
1&amp;gt;Y::$vbtable@W@:
1&amp;gt; 0	| -8
1&amp;gt; 1	| 24 (Yd(W+8)Z)	//此处的W+8意思是这个虚基类表指针距离该类（W）头部的偏移量。因为虚基类表指针在虚函数表指针后面，所以+8了
1&amp;gt;Y::$vftable@Z@:
1&amp;gt;	| -56
1&amp;gt; 0	| &amp;amp;Y::func
1&amp;gt;Y::func this adjustor: 56
1&amp;gt;Y::func1 this adjustor: 0
1&amp;gt;Y::func2 this adjustor: 0
1&amp;gt;vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1&amp;gt;               Z      56       8       4 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;此处继承了两个虚继承的类，所以两个虚基类表。总计继承了三个有虚函数的类，所以有三个虚函数表。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;待补充 可以看文章：https://blog.csdn.net/xiaxzhou/article/details/76576516&lt;/p&gt;
&lt;p&gt;解决菱形继承问题。&lt;/p&gt;
&lt;h1&gt;哪些函数不可被声明为虚函数：&lt;/h1&gt;
&lt;h2&gt;普通函数（非成员函数）&lt;/h2&gt;
&lt;p&gt;非成员函数只能被重载(overload)，不能被继承(override)，而虚函数主要的作用是在继承中实现动态多态，非成员函数早在&lt;strong&gt;编译期间就已经绑定函数了&lt;/strong&gt;，无法实现动态多态，那声明成虚函数还有什么意义呢？&lt;/p&gt;
&lt;h2&gt;构造函数（注意析构函数不仅可以，而且必须把析构函数声明为虚函数）&lt;/h2&gt;
&lt;p&gt;要想调用虚函数必须要通过“虚函数表”来进行的，但虚函数表是要在对象实例化之后才能够进行调用。而在构造函数运行期间，还没有为虚函数表分配空间，自然就没法调用虚函数了。&lt;strong&gt;一句话概括：构造函数用来实例化对象，虚函数必须要在对象实例化后才可调用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;静态成员函数&lt;/h2&gt;
&lt;p&gt;静态成员函数对于每个类来说只有一份，所有的对象都共享这一份代码，它是属于类的而不是属于对象。&lt;strong&gt;虚函数必须根据对象类型才能知道调用哪一个虚函数&lt;/strong&gt;，故虚函数是一定要在对象的基础上才可以的，两者一个是与实例相关，一个是与类相关。而且因为只有一份，没有动态绑定的必要。&lt;/p&gt;
&lt;h2&gt;内联成员函数&lt;/h2&gt;
&lt;p&gt;内联函数是为了在代码中直接展开，减少函数调用花费的代价，虚函数是为了在继承后对象能够实施多态性。但是inline函数在编译时被展开，而虚函数在运行时才能动态地绑定函数。&lt;/p&gt;
&lt;h2&gt;友元函数&lt;/h2&gt;
&lt;p&gt;C++不支持友元函数的继承。对于没有继承特性的函数没有虚函数的说法。友元函数不属于类的成员函数，不能被继承。&lt;/p&gt;
&lt;h2&gt;更清晰的继承状态下的虚函数表例子：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};
class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};
class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};


int main() 
{
    B bObject;
    A *p = &amp;amp; bObject;
    p-&amp;gt;vfunc1();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20220724172506.png&quot; alt=&quot;QQ截图20220724172506&quot; /&gt;&lt;/p&gt;
&lt;p&gt;程序在执行&lt;code&gt;p-&amp;gt;vfunc1()&lt;/code&gt;时，会发现&lt;code&gt;p&lt;/code&gt;是个指针，且调用的函数是虚函数，接下来便会进行以下的步骤。&lt;/p&gt;
&lt;p&gt;首先，根据虚表指针&lt;code&gt;p-&amp;gt;_vfptr&lt;/code&gt;来访问对象&lt;code&gt;bObject&lt;/code&gt;对应的虚表。虽然指针&lt;code&gt;p&lt;/code&gt;是基类&lt;code&gt;A*&lt;/code&gt;类型，但是&lt;code&gt;_vfptr&lt;/code&gt;也是基类的一部分，所以可以通过&lt;code&gt;p-&amp;gt;_vfptr&lt;/code&gt;可以访问到对象对应的虚表。&lt;strong&gt;即便是父类指针指向子类对象，也是查找子类对象的虚函数表。因为子类调用父类构造函数完毕后，调用子类构造期间会将自己的&lt;code&gt;_vfptr&lt;/code&gt;指向子类自身的虚函数表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后，在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了，所以可以根据所调用的函数定位到虚表中的对应条目。对于&lt;code&gt;p-&amp;gt;vfunc1()&lt;/code&gt;的调用，B 的虚函数表的第一项即是&lt;code&gt;vfunc1&lt;/code&gt;对应的条目。&lt;/p&gt;
&lt;p&gt;最后，根据虚表中找到的函数指针，调用函数。可以看到，B 的虚函数表的第一项指向&lt;code&gt;B::vfunc1()&lt;/code&gt;，所以&lt;code&gt;p-&amp;gt;vfunc1()&lt;/code&gt;实质会调用&lt;code&gt;B::vfunc1()&lt;/code&gt;函数。&lt;/p&gt;
&lt;h2&gt;注意重载 重写 重定义的区别&lt;/h2&gt;
&lt;h3&gt;1. 重载（overload）&lt;/h3&gt;
&lt;p&gt;指函数名相同，但是它的&lt;strong&gt;参数表列个数或顺序，类型不同&lt;/strong&gt;。但是不能靠返回类型来判断。
特征是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;相同的范围（在同一个作用域中）&lt;/li&gt;
&lt;li&gt;函数名字相同&lt;/li&gt;
&lt;li&gt;参数不同&lt;/li&gt;
&lt;li&gt;返回值可以不同。但要注意仅通过返回值不能进行重载。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virtual&lt;/code&gt; 关键字可有可无&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;仅根据函数返回值类型不能进行重载&lt;/strong&gt;。如果两个函数仅仅返回值类型不同，则不可以进行重载。因为调用时不能指定类型信息，编译器不知道你要调用哪个函数。&lt;/p&gt;
&lt;h3&gt;2. 重写（也称为覆盖 override）&lt;/h3&gt;
&lt;p&gt;指派生类重新定义基类的虚函数&lt;/p&gt;
&lt;p&gt;特征是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不在同一个作用域（分别位于派生类与基类） ；&lt;/li&gt;
&lt;li&gt;函数名字相同（析构函数除外，&lt;strong&gt;但是析构函数会被理解为同名。所以会触发thunk&lt;/strong&gt;）；&lt;/li&gt;
&lt;li&gt;参数相同；&lt;/li&gt;
&lt;li&gt;基类函数必须有 &lt;code&gt;virtual&lt;/code&gt; 关键字，不能有&lt;code&gt;static&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;返回值相同（或是协变），否则报错&lt;/li&gt;
&lt;li&gt;重写函数的访问修饰符可以不同。尽管 virtual 是 private 的，派生类中重写改写为 public,protected 也是可以的&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. 重定义（也称为隐藏）&lt;/h3&gt;
&lt;p&gt;特征是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不在同一个作用域（分别位于派生类与基类） ；&lt;/li&gt;
&lt;li&gt;函数名字相同；&lt;/li&gt;
&lt;li&gt;返回值可以不同；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数不同时&lt;/strong&gt;，&lt;strong&gt;不论有无&lt;/strong&gt; &lt;code&gt;virtual&lt;/code&gt; 关键字，基类的函数将被隐藏（注意别与重载以及覆盖混淆）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数相同时&lt;/strong&gt;，&lt;strong&gt;基类函数没有&lt;/strong&gt;&lt;code&gt; virtual&lt;/code&gt;关键字。此时，基类的函数被隐藏（注意别与覆盖混淆） 。&lt;/li&gt;
&lt;li&gt;避免无意间重定义，可以使用&lt;code&gt;override&lt;/code&gt;关键字。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	&lt;strong&gt;特别注意：当使用基类指针指向子类对象，并且发生重定义，无论基类函数有还是没有&lt;code&gt;virtual&lt;/code&gt;，都只执行基类函数。没有virtual的时候不发生多态，自然只执行基类。有virtual但是参数不同的时候，尽管可能发生多态，但是函数签名不一致，基类指针指向子类对象，他只能访问子类的基类部分。所以说他也就是基类类型，自然只能访问到基类函数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐藏可以使用&lt;code&gt;using&lt;/code&gt;来引入父类的对应函数来解决。--effective C++ 条款34&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;因为默认情况下，在子类对象中查找对应的函数，因为是逐个作用域进行查找，如果子类实现了，则函数找到后立刻就不找了，不会查看父类作用域。&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;using&lt;/code&gt;引入后，这个父类函数会被放置在子类作用域里，因为都在一个作用域内，所以都能看得到。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/1896830/why-should-i-use-the-using-keyword-to-access-my-base-class-method&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;语法&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;重载&lt;/th&gt;
&lt;th&gt;重定义（&lt;strong&gt;隐藏&lt;/strong&gt;）&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;重写（覆盖）&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;函数作用域&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;不在同一个作用域（分别位于派生类与基类）&lt;/td&gt;
&lt;td&gt;不在同一个作用域（分别位于派生类与基类）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;函数名&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同(析构函数除外)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数列表&lt;/td&gt;
&lt;td&gt;不同&lt;/td&gt;
&lt;td&gt;无要求&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;返回值&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;无要求&lt;/td&gt;
&lt;td&gt;相同(协变除外)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否必须为虚函数&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;无要求&lt;/td&gt;
&lt;td&gt;两个函数必须是虚函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;重载是编译期的多态。根据正确的数据类型匹配正确的方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重写是运行期的多态。根据正确的对象匹配正确的方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;父类指针指向子类对象，仅可访问子类的父类部分。因为不知道子类的内存布局，所以没法访问子类的部分。也就是说，父类指针指向子类对象会发生隐式转换。所以说它还是个父类类型。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;隐藏也会发生在父类子类中有同名变量的时候。子类中会隐藏父类的变量，我们可以用作用域访问运算符去访问。&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    int val;

    test(int x):val(x){}
    virtual void print(){
        cout &amp;lt;&amp;lt;&quot;test&quot; &amp;lt;&amp;lt; endl;
    }
};
class test1 : public test{
    public: 
    int val = 10;               //子类父类同名变量 这个val是test1::val 所以值是10.
    int vala;
    test1(int x, int y):test(x), vala(y){};     //调用父类构造函数赋值，这里是给子类的父类变量赋值，也就是给test::val赋值
    void print(){
        cout &amp;lt;&amp;lt;&quot;test1&quot; &amp;lt;&amp;lt; endl;
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
    test t(4);
    cout &amp;lt;&amp;lt; t.val &amp;lt;&amp;lt; endl;      //此时输出4
    test1 t1(43,5);
    cout &amp;lt;&amp;lt; t1.val &amp;lt;&amp;lt; endl;     //此时输出10。因为t1是子类对象，访问的是子类的val，也就是test1::val
    t = t1;                     //这样赋值的时候只会赋值43。因为我们的构造函数是给test::val赋值而不是test1::val赋值。而且这样的拷贝赋值只会操作子类父类共有的部分，也就是test::val。
    //而test1::val是子类自己的。所以无法赋值。
    cout &amp;lt;&amp;lt; t.val &amp;lt;&amp;lt; endl;      //此时输出43。
    test* tptr = new test1(43,5);   //多态调用，输出43。因为父类指针指向子类对象也是父类类型。父类无法访问子类独有部分。所以这里访问的是test::val而不是test1::val
    cout &amp;lt;&amp;lt; tptr-&amp;gt;val &amp;lt;&amp;lt;endl;       //输出43。
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注意不能靠返回值类型来进行函数重载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;因为重载是编译期决定，函数要么是可以调用要么是不能调用，不存在有可能调用，所以有歧义。（静态多态）&lt;/li&gt;
&lt;li&gt;我们调用函数的时候没有指定函数返回值类型，也不能指定函数返回值类型，所以有歧义。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果硬要返回值重载，那么就要重新设计语言。设计成函数调用的时候需要指定返回值类型，而且函数签名也要包含返回值。&lt;/p&gt;
&lt;h2&gt;C语言没有函数重载。因为C语言的函数签名只有名称没有参数。C++可以重载是因为函数签名包括函数名字和参数类型，但是不包括返回值。&lt;/h2&gt;
&lt;h1&gt;只要是指针或者是引用，无论指向的对象在栈上还是堆上，都可以实现多态。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    virtual void pr(){
        cout &amp;lt;&amp;lt;&quot;a&quot; &amp;lt;&amp;lt; endl;
    }
};


class B: public A{
    public:
    virtual void pr(){
        cout &amp;lt;&amp;lt;&quot;B&quot; &amp;lt;&amp;lt; endl;
    }
};


int main(){
    B bb;
    A* ptra = &amp;amp;bb;
    A&amp;amp; refa = bb;
    ptra-&amp;gt;pr();
    refa.pr();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，对象&lt;code&gt;bb&lt;/code&gt;在栈上。父类指针指向子类对象的条件满足了，无论是堆或者栈都可以。可以多态。&lt;/p&gt;
&lt;p&gt;引用也可以。&lt;/p&gt;
&lt;h1&gt;协变 (Covariance )&lt;/h1&gt;
&lt;p&gt;协变的意思是 如果父类虚函数返回的是父类的指针或者引用，那么子类重写父类虚函数的时候，返回值可以不同，可以换成子类的指针或者引用。&lt;strong&gt;注意不可以是对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    int a = 5;
    int b = 10;
    virtual A* getitem(){	//注意返回必须是指针或者引用，对象不行。
        return this;
    }
};

class B:public A {
public:
    int a = 5;
    int b = 10;
    virtual B* getitem(){ //注意返回必须是指针或者引用，对象不行。
        return this;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt;不能直接协变，需要转换一下。参考&lt;a href=&quot;https://stackoverflow.com/questions/6924754/return-type-covariance-with-smart-pointers&quot;&gt;这篇文章&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;纯虚类&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;纯虚类必须包含至少一个纯虚函数。&lt;/li&gt;
&lt;li&gt;纯虚类不可被实例化。但是可以使用其指针或引用。
&lt;ul&gt;
&lt;li&gt;这个包含了声明对象，值传递和值返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;纯虚类可以有成员变量&lt;/li&gt;
&lt;li&gt;纯虚类可以有普通的成员函数&lt;/li&gt;
&lt;li&gt;纯虚类可可以有其他虚函数&lt;/li&gt;
&lt;li&gt;纯虚类可以有带有参数的构造函数&lt;/li&gt;
&lt;li&gt;可以在纯虚类的派生类的构造函数中显式调用纯虚类的带参数构造函数&lt;/li&gt;
&lt;li&gt;派生类不会自动继承纯虚函数的定义，&lt;strong&gt;只要子类没有全部实现父类的虚函数，那么这个类依旧是个抽象类&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Base {
public:
    int val; //可以有数据成员
    Base() {
        cout &amp;lt;&amp;lt; &quot;base default const&quot; &amp;lt;&amp;lt; endl;
    }
    Base(int a) { //可以有带参构造
        val = a;
        cout &amp;lt;&amp;lt; &quot;base param const&quot; &amp;lt;&amp;lt; endl;
    }
    virtual ~Base() { //非纯虚析构

    }
    virtual void func() = 0; //纯虚函数
    void anotherfunc() { //可以有其它函数
        cout &amp;lt;&amp;lt; &quot;base another func&quot; &amp;lt;&amp;lt; endl;
    }
};

void Base::func() { //纯虚函数必须类外实现
    cout &amp;lt;&amp;lt; &quot;Base pure func&quot; &amp;lt;&amp;lt; endl;
}

class Derive :public Base {
public:
    Derive() = default;
    Derive(int x) :Base(x) {}; //子类可以使用纯虚类的带参构造。和平常的子类调用父类有参遵循一个规则。
    virtual ~Derive() {

    }
    virtual void func() {
        cout &amp;lt;&amp;lt; &quot;derive pure func&quot; &amp;lt;&amp;lt; endl;
    }
};



int main(){
    Derive obj(10);
    obj.func(); //调用子类实现的纯虚函数。正常调用
    obj.Base::func(); //显式调用父类的纯虚函数。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;纯虚函数&lt;/h1&gt;
&lt;p&gt;纯虚函数 目前没有办法写出来的东西，而且也没办法写默认的就把它设计为纯虚函数&lt;/p&gt;
&lt;p&gt;比如一个类叫牛子。里面有个尿尿的行为。你没办法在父类定义尿尿具体要尿多少，也没办法给尿尿设计默认值。但是牛子必须能尿尿。所以你就要设计为纯虚函数这样让子类必须能尿尿而且根据子类自己来决定什么时候尿多少 如果设计不是纯虚函数，而设计了一个空函数作为默认值，那么子类有可能忘了写牛子能尿尿，这样就麻烦了。 纯虚函数的优点类似于关键字overwrite。强制子类进行某种操作。否则无法通过编译。 纯虚函数也促成了接口 抽象类的实现&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯虚函数只可以类内声明，类外定义。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纯虚析构函数必须有函数定义（默认实现）。也就是必须要有函数体。用来做一些基类的清理工作，防止基类出现内存泄漏（当然啥也不干也行，但是必须要有）。 而且。纯虚析构函数，必须要在类外定义。否则，派生类的析构函数由于编译器的扩展而显式的调用基类的析构函数时会找不到定义。同时编译器也无法为已经声明为纯虚的析构函数生成一个默认的实现。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纯虚函数通常没有定义体。但是也可以拥有。所以：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;基类的纯虚函数的默认实现必须由派生类显式的要求调用。这句话翻译过来就是&lt;strong&gt;如果子类想使用纯虚函数的默认实现，就需要加作用域访问运算符去显式调用&lt;/strong&gt;。举个例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; class test{
     public:
         virtual void getval() = 0; //纯虚函数类内声明
 };
 
 void test::getval(){ 				//类外定义默认实现
     cout &amp;lt;&amp;lt;&quot;pure func&quot; &amp;lt;&amp;lt; endl;
 }
 
 class test1: public test{
     public:
         virtual void getval(){			//子类必须要实现纯虚函数。
             test::getval();			//如果我就想用默认实现，就得加作用域访问运算符。
         }
 };
 
 
 int main(){
     test1 T1;
     T1.getval();
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;派生类不会自动继承这个纯虚函数的定义，如果子类&lt;code&gt; test1&lt;/code&gt;未定义 &lt;code&gt;getval()&lt;/code&gt;，那么 子类&lt;code&gt; test1&lt;/code&gt; 依然是一个抽象类型。这句话翻译过来就是，&lt;strong&gt;只要子类没有全部实现父类的虚函数，那么这个类依旧是个抽象类&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这种 纯虚 函数还提供实现的方案比较好的应用场景为：基类提供了一个默认的实现，但是不希望自动的继承给派生类使用，除非派生类明确的要求。&lt;/li&gt;
&lt;li&gt;纯虚函数&lt;strong&gt;理论上&lt;/strong&gt;不会占据虚函数表。但是一旦纯虚函数被子类实现，他就变成了正常的虚函数，还是会进入虚函数表。&lt;strong&gt;所以结论是纯虚函数也会占据一个虚函数表位置。可能依靠实现。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;more effective c++ 条款24&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-vptr.assets/QQ%E6%88%AA%E5%9B%BE20230228212906.png&quot; alt=&quot;QQ截图20230228212906&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;比较不错的文章&lt;/h1&gt;
&lt;p&gt;1.&lt;a href=&quot;https://sofiabelen.github.io/projects/visualizing-the-cpp-object-memory-layout-part-1-single-inheritance/&quot;&gt;可视化 C++ 对象内存布局（第一部分）：单继承&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>初始化</title><link>https://sterben-01.github.io/posts/%E5%88%9D%E5%A7%8B%E5%8C%96/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E5%88%9D%E5%A7%8B%E5%8C%96/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;聚合初始化&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;从 初始化器列表初始化聚合体。是&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/list_initialization&quot;&gt;列表初始化&lt;/a&gt;的一种形式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;(aggregate initialization)&lt;/code&gt;。聚合类型可以进行直接列表初始化。&lt;strong&gt;没有构造函数也可以初始化其中的参数&lt;/strong&gt;。但是聚合类型有如下限制&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/aggregate_initialization.html&quot;&gt;聚合&lt;/a&gt;（Aggregate）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;聚合&lt;/strong&gt;属于以下类型之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数组类型&lt;/strong&gt;（array types）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类类型&lt;/strong&gt;，并满足下列条件：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;没有用户声明的构造函数&lt;/strong&gt;（no user-declared constructors）
（至 C++11 之前）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有用户提供的、继承的或显式的构造函数&lt;/strong&gt;（no user-provided, inherited, or explicit constructors）
（自 C++11 起，至 C++20 之前）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有用户声明或继承的构造函数&lt;/strong&gt;（no user-declared or inherited constructors）
（自 C++20 起）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有私有或受保护的直接非静态数据成员&lt;/strong&gt;（no private or protected direct non-static data members）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有基类&lt;/strong&gt;（no base classes）
（至 C++17 之前）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有虚拟基类&lt;/strong&gt;（no virtual base classes）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有私有或受保护的直接基类&lt;/strong&gt;（no private or protected direct base classes）
（自 C++17 起）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有虚成员函数&lt;/strong&gt;（no virtual member functions）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有默认成员初始化器&lt;/strong&gt;（no default member initializers）
（自 C++11 起，至 C++14 之前）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;元素（Element）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;聚合的元素&lt;/strong&gt;为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;strong&gt;数组&lt;/strong&gt;：按下标递增顺序的数组元素。&lt;/li&gt;
&lt;li&gt;对于&lt;strong&gt;类&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;至 C++17 之前&lt;/strong&gt;：按声明顺序的&lt;strong&gt;非静态数据成员&lt;/strong&gt;（不包括匿名位域，anonymous bit-fields）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自 C++17 起&lt;/strong&gt;：先是按声明顺序的&lt;strong&gt;直接基类&lt;/strong&gt;，随后是按声明顺序的&lt;strong&gt;直接非静态数据成员&lt;/strong&gt;，且这些成员&lt;strong&gt;既不是匿名位域，也不是匿名联合（anonymous union）的成员&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    struct obj{
        int _objval;
    };
    int _val;
    obj inclass_obj;
    test(int x, int y): _val(x), inclass_obj._objval(y){} // 错误。这是构造函数初始化列表。用来初始化的。这里的inclass_obj._objval(y)是赋值操作。不允许。详细说就是，初始化列表只能初始化对象。这里是给一个对象的某个变量赋值。这是错误的。
    test(int x, int y): _val(x), inclass_obj(y){} 	// 错误 obj类没有有参构造函数。
    test(int x, int y): _val(x), inclass_obj(){} 	// 可以 obj类有默认无参构造函数。
    test(int x, int y): _val(x), inclass_obj{y}{} 	// 可以 这里的inclass_obj{y}叫做聚合初始化。注意后面的{}是test构造函数的{}。不要搞混
    
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对第一行错误代码，我们可以让&lt;code&gt;obj&lt;/code&gt;自带一个有参构造。然后使用有参构造对&lt;code&gt;obj&lt;/code&gt;进行初始化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这东西真正牛逼的在这呢。如果聚合体中间有嵌套，你可以不用使用花 括号分割&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct Aggregate {
    int arr[4];
    int j;
};
int main(){
    Aggregate aggr = {1, 2, 3, 4, 5};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;聚合初始化的例子 和 构造函数的重载决议&lt;/h2&gt;
&lt;p&gt;看一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A1{
    explicit A1(int x = 10) : x_(x) {}
    int x_;
};


struct B{
    B() = default;
    A1 a;
};


int main(){
    B b;
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样都正常，但是如果我们给&lt;code&gt;B b&lt;/code&gt;换成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B b = B{};
B b = {};
B b{B{}};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会提示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;source&amp;gt;: In function &apos;int main()&apos;:
&amp;lt;source&amp;gt;:19:13: warning: converting to &apos;A1&apos; from initializer list would use explicit constructor &apos;A1::A1(int)&apos;
   19 |     B b = B{};
      |             ^
&amp;lt;source&amp;gt;:7:14: note: &apos;A1::A1(int)&apos; declared here
    7 |     explicit A1(int x = 10) : x_(x) {}
      |              ^~
&amp;lt;source&amp;gt;:19:13: note: in C++11 and above a default constructor can be explicit
   19 |     B b = B{};
      |             ^
ASM generation compiler returned: 0
&amp;lt;source&amp;gt;: In function &apos;int main()&apos;:
&amp;lt;source&amp;gt;:19:13: warning: converting to &apos;A1&apos; from initializer list would use explicit constructor &apos;A1::A1(int)&apos;
   19 |     B b = B{};
      |             ^
&amp;lt;source&amp;gt;:7:14: note: &apos;A1::A1(int)&apos; declared here
    7 |     explicit A1(int x = 10) : x_(x) {}
      |              ^~
&amp;lt;source&amp;gt;:19:13: note: in C++11 and above a default constructor can be explicit
   19 |     B b = B{};
      |             ^
Execution build compiler returned: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为啥呢？我们先看一下&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/aggregate_initialization.html&quot;&gt;聚合初始化的细节&lt;/a&gt; (简略版)：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先确定聚合中&lt;strong&gt;被显式初始化的元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;被显示初始化的元素规则...[此处不展开]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;被隐式初始化的元素&lt;/strong&gt;规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;strong&gt;非 union 的聚合&lt;/strong&gt;，每个&lt;strong&gt;未被显式初始化&lt;/strong&gt;的元素按如下方式初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果该元素有&lt;strong&gt;默认成员初始化器&lt;/strong&gt;（default member initializer），则从该初始化器进行初始化。（自 C++11 起）&lt;/li&gt;
&lt;li&gt;否则，如果该元素&lt;strong&gt;不是引用&lt;/strong&gt;，则从一个&lt;strong&gt;空的初始化列表&lt;/strong&gt;进行拷贝初始化。（the element is &lt;a href=&quot;https://en.cppreference.com/w/cpp/language/copy_initialization.html&quot;&gt;copy-initialized&lt;/a&gt; from an empty initializer list.）&lt;/li&gt;
&lt;li&gt;否则（即该元素是引用且没有默认成员初始化器），程序是不良形式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果聚合是一个 &lt;code&gt;union&lt;/code&gt; 且初始化列表&lt;strong&gt;为空&lt;/strong&gt;，则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有任何变体成员具有默认成员初始化器，则该成员由其默认成员初始化器进行初始化。（自 C++11 起）&lt;/li&gt;
&lt;li&gt;否则，&lt;code&gt;union&lt;/code&gt; 的&lt;strong&gt;第一个成员&lt;/strong&gt;（如果有）从一个&lt;strong&gt;空初始化列表&lt;/strong&gt;进行拷贝初始化。（the element is &lt;a href=&quot;https://en.cppreference.com/w/cpp/language/copy_initialization.html&quot;&gt;copy-initialized&lt;/a&gt; from an empty initializer list.）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以我们的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B b = B{};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们没有显式初始化&lt;code&gt;A1&lt;/code&gt;，且&lt;code&gt;A1&lt;/code&gt;不是引用，所以&lt;code&gt;A1&lt;/code&gt;是从一个&lt;strong&gt;空初始化列表&lt;/strong&gt;进行&lt;strong&gt;拷贝&lt;/strong&gt;初始化（复制列表初始化）&lt;/p&gt;
&lt;p&gt;所以就是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A1 a = {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个地方意味着a是被值初始化。我们查看值初始化的定义，发现a会被默认初始化。这里会尝试找到最匹配的构造函数。但是我们在构造函数重载决议中找到了这一点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4&gt;通过构造函数初始化&lt;/h4&gt;
&lt;p&gt;当类类型的对象被&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/direct_initialization.html&quot;&gt;直接初始化&lt;/a&gt;或&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/default_initialization.html&quot;&gt;默认初始化（包括&lt;/a&gt;&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/list_initialization.html&quot;&gt;复制列表&lt;/a&gt;初始化上下文中的默认初始化）(C++11 起)时，候选函数均为被初始化类的构造函数。参数列表为初始化器的表达式列表。&lt;/p&gt;
&lt;p&gt;否则，候选函数均为被初始化类的&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/converting_constructor.html&quot;&gt;转换构造函数&lt;/a&gt;。参数列表为初始化器的表达式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于复制列表初始化上下文中的默认初始化，如果选择到了explicit标记的构造函数，则初始化格式不正确。&lt;/strong&gt;（自 C++11 起）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以我们发现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A1 a = {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个代码格式不正确，所以有问题。&lt;/p&gt;
&lt;p&gt;来自&lt;a href=&quot;https://stackoverflow.com/questions/72712318/converting-to-a-from-initializer-list-would-use-explicit-constructor-aaint&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;列表初始化（花/大括号初始化）&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;从 &lt;em&gt;花括号初始化器列表&lt;/em&gt; 列表初始化对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;带等号和不带等号一般不做区分。&lt;code&gt;int c{5};&lt;/code&gt;是直接列表初始化 ，&lt;code&gt;int d = {5};&lt;/code&gt;是拷贝列表初始化。 两种形式一般不做区分。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先第一点：c++不允许在一个类内使用圆括号初始化另一个类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    int a = 5; 	//拷贝初始化 OK
    int b(5); 	//直接初始化。ERROR。
    int c{5};	//默认类型使用的列表初始化。其实还是调用了对应的构造函数进行了直接初始化。也叫直接列表初始化
    int d = {5}; //和上面那种一般不做区分。一般也不用。但是这个叫做拷贝列表初始化。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class test{
    int test_val1;
    int test_val2;
    public:
    test(int x, int y):test_val1(x), test_val2(y){};
    struct obj{
        int _objval;
        int _faf;
        obj(){}
        obj(int x): _objval(x){}		//注意我们obj有自定义构造函数。所以不是聚合体。
    };
    obj inclass_obj1; 		//obj对象使用无参构造函数
    obj inclass_obj2(); 	//这是返回obj类型的函数
    obj inclass_obj3(5); 	//这里表面是使用了obj的有参构造，但是不允许这种行为。因为有二义性。编译器认为这是个返回obj类型的形参为5的函数。但是形参为5是啥玩意？所以一个类内不允许使用圆括号初始化另一个类。也就是不允许使用直接初始化
    //替代方案：
    obj inclass_obj4 = {test_val1}; // 列表初始化(其实还是调用了对应的构造函数) OK //* 注意这里使用了类的有参构造函数。没有使用拷贝构造  //!注意这不是聚合初始化。聚合初始化不能有用户定义的构造函数
    obj inclass_obj5{test_val2}; //列表初始化(其实还是调用了对应的构造函数) OK //* 注意这里使用了类的有参构造函数。没有使用拷贝构造 //!注意这不是聚合初始化。聚合初始化不能有用户定义的构造函数
    obj inclass_obj6 = 5; //该处的初始化方式是隐式调用obj(int)构造函数生成一个临时的匿名对象，再调用拷贝构造函数完成初始化。
    obj inclass_obj7 = obj(5); //显式调用有参构造函数生成一个临时的匿名对象，再调用拷贝构造函数完成初始化 OK

};
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;为什么使用列表初始化？&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;可以使用初始化列表接受任意长度。不想细说了，查查就可以。用得少。除了容器类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有场合都可用，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它能表达一组值，来初始化STL容器：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; std::vector&amp;lt;int&amp;gt; v{1, 3, 5};
 std::vector&amp;lt;int&amp;gt; v = {1, 3, 5}; //当然这也可以。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;它能用来给类的非static成员设定默认值（而&lt;code&gt;()&lt;/code&gt;就不行）上面提到了。：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Widget {
...
private:
    int x{0};   // 可以
    int y = 0;  // 可以
    int z(0);   // 不行
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;它和&lt;code&gt;()&lt;/code&gt;都能用于初始化一个uncopyable的对象（而&lt;code&gt;=&lt;/code&gt;就不行）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;int&amp;gt; ai1{0};   // 可以
std::atomic&amp;lt;int&amp;gt; ai2(0);   // 可以
std::atomic&amp;lt;int&amp;gt; ai3 = 0;  // 不行
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;包括替换&lt;code&gt;make_pair&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;unordered_map&amp;lt;int, int&amp;gt; my_map;
my_map.insert(make_pair&amp;lt;int, int&amp;gt;(5,10)); //使用make_pair
my_map.insert({10,20}); //使用uniform_initialization
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用列表初始化初始&lt;strong&gt;内置类型&lt;/strong&gt;的变量时，防止类型窄化，避免精度丢失的隐式类型转换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int b = 4.4;		//可以。隐式类型转换。
int c = {5.5};		//不可以，列表初始化防止隐式转换带来的窄化。
int d{6.6};			//不可以，列表初始化防止隐式转换带来的窄化。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;什么是类型窄化，列表初始化通过禁止下列转换，对隐式转化加以限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从浮点类型到整数类型的转换&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;long double&lt;/code&gt; 到 &lt;code&gt;double&lt;/code&gt;或 &lt;code&gt;float&lt;/code&gt; 的转换，以及从 &lt;code&gt;double&lt;/code&gt; 到 &lt;code&gt;float&lt;/code&gt; 的转换，除非源是常量表达式且不发生溢出&lt;/li&gt;
&lt;li&gt;从整数类型到浮点类型的转换，除非源是其值能完全存储于目标类型的常量表达式&lt;/li&gt;
&lt;li&gt;从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换，除非源是其值能完全存储于目标类型的常量表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;它不会被认为是声明。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++中规定“所有看起来像声明的语句都会被视为声明”，这导致&lt;code&gt;()&lt;/code&gt;在一些场景下会被视为函数声明，而&lt;code&gt;{}&lt;/code&gt;则不会&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Widget w1(10);      // 调用有参构造。
Widget w2();        // 声明了一个无参，名为w2返回Widget的函数
Widget w3{};        // 调用无参构造


template&amp;lt;typename T&amp;gt;
void foo(){
    T x1(); 	//这是函数声明。声明了一个无参，叫x，返回T的函数
    T x2 = T();	//初始化x2，但是看起来不像是赋值。
    T x3{};		//看起来非常好。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;为什么不使用列表初始化？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在类有&lt;code&gt;std::initializer_list&lt;/code&gt;参数的构造函数时，&lt;code&gt;{}&lt;/code&gt;会有麻烦：&lt;code&gt;{}&lt;/code&gt;总会被认为是&lt;code&gt;std::initializer_list&lt;/code&gt;，即使解析出错。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;auto&lt;/code&gt;会把列表初始化的型别推导为&lt;code&gt;std::initializer_list&lt;/code&gt;, 值是&lt;code&gt;{}&lt;/code&gt;内的内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参见下方&lt;code&gt;i11&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;注意区别&lt;code&gt;i12&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个有趣的地方：如果&lt;code&gt;{}&lt;/code&gt;中没有元素，那么被调用的是默认构造函数，而不是一个空的&lt;code&gt;std::initializer_list&lt;/code&gt;。如果你真的想传入一个空的&lt;code&gt;std::initializer_list&lt;/code&gt;，那么这样：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Widget w4({});
Widget w5{{}};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::vector&amp;lt;int&amp;gt;&lt;/code&gt;会有二义性。我们有&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;int&amp;gt; v1(10, 20);   //使用普通构造函数创建vector，里面有10个元素，初始值为20.
std::vector&amp;lt;int&amp;gt; v2{10, 20};   // 使用初始化列表构造函数，创建一个有两个元素的vector，元素分别为10和20
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;同样的情况存在于使用&lt;code&gt;make&lt;/code&gt;方法进行智能指针的创建之时。&lt;code&gt;make&lt;/code&gt;方法无法使用列表初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;看一点有意思的事情&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;int i1;				//未定义值。undefined value
int i2 = 42;		//拷贝初始化 使用42
int i3(42);			//直接初始化 使用42
int i4 = int();		//拷贝初始化 使用0（默认值）
int i5{42};			//直接列表初始化 使用42
int i6{};			//直接列表初始化 使用0（默认值）
int i7 = {42};		//拷贝列表初始化 使用42
int i8 = {};		//拷贝列表初始化 使用0 （默认值）

auto i9 = 42;  		//使用42初始化，推导为int
auto i10{42};		//使用42初始化，推导为int。[除旧编译器外]
auto i11 = {42};	//使用42初始化std::initializer_list&amp;lt;int&amp;gt;。推导为std::initializer_list&amp;lt;int&amp;gt;
auto i12 = int{42};	//使用42初始化的int来初始化i12, 推导为int

int i13();			//这是函数声明
int i14(7,9);		//这是编译时错误
int i15 = (7,9);	//使用9。这是使用了逗号运算符。
int i16 = int(7,9);	//这是编译时错误

auto i17(7,9);		//这是编译时错误
auto i18 = (7,9);	//使用9，这是使用了逗号运算符。
auto i19 = int(7,9);//这是编译时错误。


//针对不可拷贝和/或不可移动的类型，在C++17以前 如下是不允许的
auto a = std::atomic&amp;lt;int&amp;gt;{9}; //不可拷贝，C++14编译错误。使用了已删除的拷贝构造函数。但是C++17使用了复制省略技术，可以通过编译。

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;逗号运算符
&lt;ul&gt;
&lt;li&gt;整个逗号表达式的值为系列中最后一个表达式的值。&lt;/li&gt;
&lt;li&gt;从本质上讲，逗号的作用是将一系列运算按顺序执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;视频中建议不要使用&lt;code&gt;()&lt;/code&gt;进行初始化。尽可能使用花括号&lt;code&gt;{}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i10&lt;/code&gt;的原始语义是使用42初始化一个&lt;code&gt;std::initializer_list&amp;lt;int&amp;gt;&lt;/code&gt;。已在C++14中修复。&lt;/li&gt;
&lt;li&gt;注意&lt;code&gt;i11&lt;/code&gt;的等号改变了&lt;code&gt;auto&lt;/code&gt;推导的类型。初始化阶段使用等号可能会改变变量的类型。&lt;/li&gt;
&lt;li&gt;剩余auto解释参见杂记3.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;C++17 复制省略技术&lt;/h2&gt;
&lt;p&gt;参见杂记3&lt;/p&gt;
&lt;h1&gt;std::initializer_list&lt;/h1&gt;
&lt;p&gt;参见杂记3&lt;/p&gt;
&lt;h1&gt;初始化和赋值的区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当对象在创建时获得了一个特定的值，我们说这个对象被&lt;strong&gt;初始化（initialized）&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而赋值的含义是把对象的当前值擦除，而以一个新值来替代。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化不是赋值，初始化的含义是创建变量时赋予其一个初始值&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;初始化器&lt;/h1&gt;
&lt;p&gt;看下面的几种初始化之前先看这个&lt;/p&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/initialization&lt;/p&gt;
&lt;h1&gt;默认初始化 default initialization&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;T 对象 //当不带初始化器而声明具有自动、静态或线程局部存储期的变量时；
new T //当以不带初始化器的 new 表达式创建具有动态存储期的对象时
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如&lt;code&gt;[new] T object;&lt;/code&gt;这样的都叫做默认（缺省）初始化。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int a&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double b&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bool c&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当我们不使用初始化器的时候，就会自动被编译器调用这种方式初始化。但是它有个问题。在不是类类型的时候，也就是是基本类型的时候，这里的对象值是脏值，也就是不确定值。（有的地方称之为未初始化，但我不确定）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;T&lt;/code&gt; 是类类型，&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/overload_resolution.html&quot;&gt;那么考虑各构造函数并实施针对空实参列表的重载决议&lt;/a&gt;。调用所选的构造函数（即默认构造函数之一），以提供新对象的初始值;
&lt;ul&gt;
&lt;li&gt;说白了就是调用默认构造函数。&lt;/li&gt;
&lt;li&gt;但是这块的重载决议太复杂了，看下链接吧。关键词搜索constructor&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;T&lt;/code&gt; 是数组类型，那么该数组的每个元素都被默认初始化；&lt;/li&gt;
&lt;li&gt;否则，不做任何事：具有自动存储期的对象（及其子对象）被初始化为不确定值。
&lt;ul&gt;
&lt;li&gt;这句话翻译过来就是非类类型的参数都是不确定值。也就是脏数据。&lt;/li&gt;
&lt;li&gt;比如上面的&lt;code&gt;a&lt;/code&gt;,&lt;code&gt;b&lt;/code&gt;,&lt;code&gt;c&lt;/code&gt;都是不确定值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意一下，这里说的是自动储存期限。而且下面说了块作用域内。意思就是如果这个基本类型对象是个静态或全局变量，还是会被初始化为0&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;很好理解，因为存放的区域都不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这时候建议给基本类型或内置类型成员使用&lt;code&gt;{}&lt;/code&gt;进行值初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a; //全局
int b;
int c;
void func(){
    int d; //块作用域
    float e;
    double f;
    static int n; //块作用域但是静态
    static int t;
    static int o;
    cout &amp;lt;&amp;lt; d &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; e &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; n &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; o &amp;lt;&amp;lt; endl;
    
}

int main(){ 
    int x; //块作用域
    float y;
    double z;
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
    func();
    cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; y &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; z &amp;lt;&amp;lt; endl;
    return 0;
}
/*
0 全局 正确零初始化
0
0
848103824 块作用域，脏值
4.59149e-41
6.92062e-310
0 块作用域但是静态，正确零初始化
0
0
848104048 块作用域 脏值
4.59149e-41
0
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;调用时机：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当我们在块作用域内不使用任何初始值定义一个非静态变量时，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果类类型的局部静态变量（块作用域内）没有显式的初始值，它将执行值初始化。非类类型则进行零初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个类本身含有类类型成员且使用合成的默认构造函数时；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当类类型的成员没有在构造函数初始值列表中显式地初始化时；&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;值初始化 value initialization&lt;/h1&gt;
&lt;p&gt;那么如果在列表形式中，没有任何参数，也就是如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T ();
new T ();
class::class(...) : object() { ... };
T object {};
T {};
new T {};
class::class(...) : object{} { ... };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的初始化方法，我们称之为值初始化。&lt;/p&gt;
&lt;p&gt;按我的理解，实质应该叫做“广义缺省初始化”。因为值初始化一般是三种处理方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;code&gt;T&lt;/code&gt;是类类型，且没有默认构造函数，或拥有由用户提供的或被删除的默认构造函数，那么对象是默认初始化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接的默认初始化会导致自动储存期的基本类型成员不会被正确初始化。也就是脏值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;code&gt;T&lt;/code&gt;是类类型，且拥有默认构造函数，而且默认构造函数既不被用户提供，也没有被删除，那么就使用零初始化。然后如果它拥有非平凡的默认构造函数，那么默认初始化它。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后半句话的意思是，如果类T有默认构造。然后如果有一些数据成员是非静态类型，且这些类型中有的没有平凡的默认构造函数，则当前这个类T的默认构造不是平凡构造。所以说，如果该类T有一个数据成员，比如&lt;code&gt;string&lt;/code&gt;，则首先类T被零初始化，然后由于类T的默认构造不是平凡的默认构造，所以会再次默认初始化它。也就相当于调用其数据成员的默认构造。（比如string会调用其默认构造初始化为空字符串）。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;由于是先零初始化，再默认初始化。此处保证了自动存储期的基本类型成员可以被零初始化。因为针对自动存储期的基本类型成员，默认初始化不作任何事情。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认构造相当于空初始化器和空函数体。但是依旧会调用各个基类和各个非静态成员的默认构造。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;T&lt;/code&gt; 是数组类型，那么值初始化数组的每个元素；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;否则，零初始化对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上部分出自官方文档，下面部分来自知乎回答。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;T&lt;/code&gt;有用户定义的缺省构造函数，直接调用；&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;T&lt;/code&gt;有编译器生成的缺省构造函数，先0值初始化再调用；&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;T&lt;/code&gt;根本不是类，直接0值初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;https://www.zhihu.com/question/36735960/answer/68902926&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所有情况下，如果使用空花括号对 &lt;code&gt;{}&lt;/code&gt; 且 &lt;code&gt;T&lt;/code&gt; 是&lt;em&gt;聚合类型&lt;/em&gt;，那么进行聚合初始化而非值初始化。巨大坑点！！！&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;T&lt;/code&gt; 是没有默认构造函数但带有接受 &lt;code&gt;std::initializer_list&lt;/code&gt;的构造函数的类类型，那么进行列表初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;调用时机&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在数组初始化的过程中，如果提供的初始值数量少于数组的大小，剩下的元素会进行值初始化；&lt;/li&gt;
&lt;li&gt;当我们不使用初始值定义一个局部静态变量时；&lt;/li&gt;
&lt;li&gt;当我们通过书写形如&lt;code&gt;T()&lt;/code&gt;的表达式显示地请求值初始化时；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;加深对象构造和使用初始化器之间的关系理解&lt;/h2&gt;
&lt;p&gt;所以当我们遇到这样的表达式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T obj;
T obj(...);
int a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要知道，它并不是调用构造函数的意思。而是通过某种初始化方式得到一个&lt;code&gt;T&lt;/code&gt;类型对象。又恰好由于类型&lt;code&gt;T&lt;/code&gt;是一个带有自定义构造器的类类型，因此对类型&lt;code&gt;T&lt;/code&gt;对象的值初始化等于默认初始化。默认初始化一个类类型，会考虑各构造函数并找到最匹配的那一个，然后调用所选的构造函数，以提供新对象的初始值。所以我们说，这个值初始化过程会包含一次对&lt;code&gt;A&lt;/code&gt;的构造器的调用。并不是说这个表达式就是调用构造器。&lt;/p&gt;
&lt;h1&gt;零初始化 zero initialization&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;注意零初始化在语言中没有专用语法，因此下列语法不是零初始化语法。这些是可能会进行零初始化的其他初始化的例子。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static T 对象
T () ;
T t = {} ;
T {} ; (C++11 起)
CharT 数组 [ n ] = &quot; 短序列 &quot;;	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;尤其注意非局部静态变量如果不能被常量初始化，那么它会被零初始化。但是类成员变量由于需要类外定义并初始化，所以一开始是零初始化，然后在类外定义的时候根据具体语法规则来进行对应的初始化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E5%88%9D%E5%A7%8B%E5%8C%96.assets/QQ%E6%88%AA%E5%9B%BE20230405011410.png&quot; alt=&quot;QQ截图20230405011410&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E5%88%9D%E5%A7%8B%E5%8C%96.assets/QQ%E6%88%AA%E5%9B%BE20230405011403.png&quot; alt=&quot;QQ截图20230405011403&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在所有其他初始化前，对每个具有静态或线程局部 (C++11 起)&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/storage_duration&quot;&gt;存储期&lt;/a&gt;的，不进行&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/constant_initialization&quot;&gt;常量初始化&lt;/a&gt;的具名变量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将一个对象的初始值设为零。&lt;/p&gt;
&lt;p&gt;零初始化的效果是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;T&lt;/code&gt; 是标量类型，那么对象的初始值是将整数字面量 0显式转换到 &lt;code&gt;T&lt;/code&gt; 的值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;T&lt;/code&gt; 是数组类型，那么零初始化每个元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;T&lt;/code&gt; 是引用类型，那么不做任何事。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;T&lt;/code&gt; 是非联合体类类型，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化所有填充位为零位&lt;/li&gt;
&lt;li&gt;零初始化所有非静态数据成员&lt;/li&gt;
&lt;li&gt;零初始化所有非虚基类子对象&lt;/li&gt;
&lt;li&gt;如果对象不是基类子对象，那么也零初始化所有虚基类子对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;T&lt;/code&gt; 是联合体类型，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化所有填充位为零位&lt;/li&gt;
&lt;li&gt;零初始化对象的首个非静态具名数据成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;统一初始化 uniform initialization&lt;/h1&gt;
&lt;p&gt;个人理解统一初始化的背后其实就是值初始化。目的是让一切看起来更好。&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/danshiming/article/details/116273447&lt;/p&gt;
&lt;h1&gt;常量初始化&lt;/h1&gt;
&lt;p&gt;目的是设置静态变量的初值为编译时常量。&lt;/p&gt;
&lt;p&gt;如果静态或线程局部变量以常量被初始化，那么就会在其他所有初始化之前进行常量初始化以取代零初始化。&lt;/p&gt;
&lt;p&gt;变量或临时对象 &lt;code&gt;obj &lt;/code&gt;在满足以下所有条件时会以常量被初始化 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;要么它有初始化器 （&lt;code&gt;(表达式列表)&lt;/code&gt;,&lt;code&gt;{初始化器列表}&lt;/code&gt;,&lt;code&gt;=表达式&lt;/code&gt;），要么它的默认初始化会进行某些初始化，并且&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它的初始化完整表达式是常量表达式，或者在&lt;code&gt;obj&lt;/code&gt;是对象时，该完整表达式也可以为&lt;code&gt;obj&lt;/code&gt;和它的子对象调用constexpr构造函数，即使这些对象不是字面类类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;常量初始化的效果与其所对应的初始化的效果相同，但保证它在任何其他静态或线程局部对象的初始化前完成，并可能在编译时进行。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一般来说，常量初始化在编译时进行，并将预先计算的对象表示作为程序映像的一部分在 &lt;code&gt;.data&lt;/code&gt; 段存储。如果变量既为 &lt;code&gt;const&lt;/code&gt; 又被常量初始化，那么它会被存储于程序映像的只读段 &lt;code&gt;.rodata&lt;/code&gt; 段。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;非局部变量的静态初始化&lt;/h1&gt;
&lt;p&gt;针对静态变量而言。尤其要注意区分静态成员变量和静态局部变量的区别。单例的笔记中提到了。&lt;/p&gt;
&lt;p&gt;所有具有静态存储期的非局部变量的初始化会作为程序启动的一部分在main 函数的执行之前进行（除非被延迟）。所有具有线程局部存储期的非局部变量的初始化会作为线程启动的一部分进行，按顺序早于线程函数的执行开始。对于这两种变量，初始化发生于两个截然不同的阶段：&lt;strong&gt;静态初始化和动态初始化&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;动态初始化这里先不提。我们关注一下静态初始化：&lt;/p&gt;
&lt;p&gt;有两种静态初始化的形式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果可能，那么应用常量初始化。&lt;/li&gt;
&lt;li&gt;否则非局部静态及线程局域变量会被零初始化。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实践中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常量初始化通常在编译期进行。预先被计算的对象表示会作为程序映像的一部分存储下来。如果编译器没有这样做，那么它仍然必须保证该初始化发生早于任何动态初始化。&lt;/li&gt;
&lt;li&gt;零初始化的变量将被置于程序映像的 &lt;code&gt;.bss&lt;/code&gt; 段，它不占据磁盘空间，并在加载程序时由操作系统以零填充。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意，静态成员变量虽然属于具有静态存储期的非局部变量，但是在默认初始化中，格外提到了&lt;em&gt;当不带初始化器而声明具有自动、静态或线程局部存储期的变量时&lt;/em&gt;是默认初始化。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;个人理解&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;{}&lt;/code&gt;这花括号本身叫做花括号初始化器列表(Brace-init-list)&lt;/strong&gt; &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/initialization&quot;&gt;在这里&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;个人理解。当是我们&lt;strong&gt;使用花括号初始化器列表进行列表初始化&lt;/strong&gt;的时候，如果是聚合类型，看见&lt;code&gt;{}&lt;/code&gt;是聚合初始化(aggregate initialization)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不是聚合类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有&lt;code&gt;std::initializer_list&lt;/code&gt;构造函数，那么它在能匹配&lt;code&gt;std::initializer_list&lt;/code&gt;构造函数的时候就尽可能使用这个构造函数。&lt;/li&gt;
&lt;li&gt;如果没有&lt;code&gt;std::initializer_list&lt;/code&gt;构造函数，那么它叫做统一初始化（value initialization)。他会寻找其他匹配的构造函数参数来寻找合适的方式初始化对象。
&lt;ul&gt;
&lt;li&gt;如果是类类型，
&lt;ul&gt;
&lt;li&gt;如果构造函数是编译器合成的，则为零初始化（zero initialization）&lt;/li&gt;
&lt;li&gt;如果构造函数不是编译器合成的，则是值初始化 (value initialization)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果不是类类型，也就是内置类型，就是零初始化（zero initialization）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=7DTlWPgX6zs&quot;&gt;CppCon 2018: Nicolai Josuttis “The Nightmare of Initialization in C++”&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_39583450/article/details/109624599&lt;/p&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/list_initialization&lt;/p&gt;
&lt;h1&gt;非常诡异的例子&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;struct test{
    test() = delete;
    int val;
    char b;
    float c;
    double d;
};
int main(){
    test obj{};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码在C++11/14/17可以运行，但是20不可以。为什么。&lt;/p&gt;
&lt;p&gt;首先我们看到&lt;code&gt;test obj{}&lt;/code&gt;的时候可能会想到这是值初始化。但是看好了，&lt;code&gt;test&lt;/code&gt;在C++20前。c++11后是聚合体。所以这是聚合初始化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;聚合体的定义在C++11-C++20间有这样一条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;没有用户提供、继承或 explicit 的构造函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;首个声明被显式预置或显式弃置的时候，不是由用户提供的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以这里的&lt;code&gt;=delete&lt;/code&gt;不是用户提供的。所以符合要求&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;聚合体的定义在C++20后被修改为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;没有用户声明或继承的构造函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;=delete&lt;/code&gt;是用户声明的。所以不符合要求了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E5%88%9D%E5%A7%8B%E5%8C%96.assets/initialization.png&quot; alt=&quot;initialization&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图片来自&lt;a href=&quot;https://pica.zhimg.com/v2-71ee7afb16ed13a42c7e17f74a4bec53_r.jpg&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;延伸&lt;/h1&gt;
&lt;h2&gt;什么时候会生成未初始化的变量&lt;/h2&gt;
&lt;p&gt;来自PVS这篇文章的&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1193/&quot;&gt;Program execution: uninitialized variables 章节&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果我们不考虑特殊函数和原始内存分配器，且如果类型是&lt;code&gt;is_trivially_constructible&lt;/code&gt;的 (不知道这个定义的查看模板笔记) 情况下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T x;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T x[N];&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T* p = new T;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T* p = new T[N];&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;会产生未初始化的变量/数组（或指向未初始化的变量/数组的指针）。&lt;/p&gt;
</content:encoded></item><item><title>操作系统相关</title><link>https://sterben-01.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9B%B8%E5%85%B3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9B%B8%E5%85%B3/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;进程是进程是操作系统分配资源的最小单位。线程是程序执行的最小单位。&lt;/h1&gt;
&lt;h1&gt;进程和线程资源共享：&lt;/h1&gt;
&lt;h2&gt;线程共享的内容包括:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1.进程代码段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2.进程数据段（包括BSS段）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以全局变量和静态变量是共享的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.进程打开的文件描述符&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4.信号的处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5.进程的当前目录和&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;6.进程用户ID与进程组ID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;7.堆&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程独有的内容包括:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1.&lt;strong&gt;线程ID&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2**.寄存器组的值**&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.&lt;strong&gt;线程的栈&lt;/strong&gt; （注意，线程栈是理论上私有，由于，没有独立地址空间所以依旧可以互相访问。也就是子线程依旧可以访问主线程的栈或其他线程的栈）（我们所说的线程栈独有是因为新建线程的时候会从进程的&lt;code&gt;mmap&lt;/code&gt;区划一块给线程当栈，然后系统自动在这个部分当栈而已。）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4.错误返回码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5.线程的信号屏蔽码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在一个线程中访问另一个线程的局部变量是否合法？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果被访问的变量的声明周期长于访问方，则是合法。否则行为未定义。&lt;a href=&quot;https://stackoverflow.com/questions/64669754/accessing-a-local-variable-of-the-main-thread-from-another-thread&quot;&gt;参考&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;文件描述符&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件描述符表每个进程都有一个&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打开文件表和 i-node 表整个系统只有一个&lt;/strong&gt;，它们三者之间的关系如下图所示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220926010323.png&quot; alt=&quot;QQ截图20220926010323&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有了以上对文件描述符的认知，我们很容易理解以下情形：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个进程的不同文件描述符可以指向同一个文件；&lt;/li&gt;
&lt;li&gt;不同进程可以拥有相同的文件描述符；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不同进程的同一个文件描述符可以指向不同的文件（一般也是这样，除了 0、1、2 这三个特殊的文件）；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不同进程的不同文件描述符也可以指向同一个文件。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;进程退出&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;进程退出的时候会自动关闭自己打开的所有文件和网络连接&lt;/li&gt;
&lt;li&gt;进程退出的时候&lt;strong&gt;不会自动销毁共享内存&lt;/strong&gt;（注意不是mmap是shmget）。因为进程间通信使用的数据结构是内核级别的。创建 后由内核管理&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;孤儿进程 僵尸进程&lt;/h1&gt;
&lt;h2&gt;孤儿进程：&lt;/h2&gt;
&lt;p&gt;一个父进程退出，而它的一个或多个子进程还在运行，那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养，并由init进程对它们完成状态收集工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话总结：父进程比子进程走得早。子进程由系统接管。变成孤儿进程&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;僵尸进程：&lt;/h2&gt;
&lt;p&gt;一个进程使用fork创建子进程，如果子进程退出，而父进程并没有调用wait或waitpid获取子进程的状态信息（没有回收子进程资源），那么子进程的进程描述符（资源）仍然保存在系统中。这种进程称之为僵死进程。也就是子进程结束了父进程没结束，子进程部分PCB信息就会被悬挂。 当子进程走完了自己的生命周期后,它会执行exit()系统调用,内核释放该进程所有的资源,包括打开的文件,占用的内存等。&lt;strong&gt;但是仍然为其保留一定的信息(包括进程号the process ID,退出码exit code,退出状态the terminationstatus of the process,运行时间the amount of CPU time taken by the process等),这些数据会一直保留到系统将它传递给它的父进程为止,直到父进程通过wait / waitpid来取时才释放。&lt;/strong&gt; 也就是说,当一个进程死亡时,它并不是完全的消失了。进程终止,它不再运行,但是还有一些残留的数据等待父进程收回。当父进程 fork() 一个子进程后,它必须用 wait() (或者 waitpid())等待子进程退出。正是这个 wait() 动作来让子进程的残留数据消失。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一句话总结：如果子进程结束，父进程没有回收子进程的资源，则子进程部分PCB信息会被悬挂。如果不用wait或waitpid回收就会变成僵尸进程。因为没办法回收掉其中资源&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;守护进程&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;守护进程的父进程是init进程&lt;/strong&gt;，因为它真正的父进程在fork出子进程后就先于子进程exit退出了，所以它是一个由init继承的孤儿进程。&lt;strong&gt;守护进程是非交互式程序，没有控制终端，所以任何输出&lt;/strong&gt;，无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。&lt;/li&gt;
&lt;li&gt;守护进程的名称通常以d结尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建守护进程：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;fork()创建子进程，父进程exit()退出&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这是创建守护进程的第一步。由于守护进程是脱离控制终端的，完成这一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成，而用户在Shell终端里则可以执行其他命令，从而在形式上做到了与控制终端的脱离，在后台工作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于父进程先于子进程退出，子进程就变为孤儿进程，并由 init 进程作为其父进程收养。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;此步骤目的是脱离控制终端在后台工作。使其被init进程接管。但是此时并未完全脱离。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在子进程调用setsid()创建新会话&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在调用了 fork() 函数后，子进程全盘拷贝了父进程的会话期、进程组、控制终端等，虽然父进程退出了，但会话期、进程组、控制终端等并没有改变。这还不是真正意义上的独立开来，而 setsid() 函数能够使进程完全独立出来。&lt;/li&gt;
&lt;li&gt;setsid()创建一个新会话，调用进程担任新会话的首进程，其作用有：
&lt;ul&gt;
&lt;li&gt;使当前进程脱离原会话的控制&lt;/li&gt;
&lt;li&gt;使当前进程脱离原进程组的控制&lt;/li&gt;
&lt;li&gt;使当前进程脱离原控制终端的控制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这样，当前进程才能实现真正意义上完全独立出来，摆脱其他进程的控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;此步骤目的是彻底和父进程断开联系完全独立。因为需要创建新的会话以使守护进程脱离原会话，进程组，控制终端的控制。但是现在它依旧可以重新申请打开一个控制终端，因为它是会话组长。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再次 fork() 一个子进程，父进程exit退出&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们刚才提到了，在上一步骤结束的时候，进程已经成为无终端的会话组长，但它可以重新申请打开一个控制终端。但是我们不希望让他打开终端，怎么办？那就重复第一步。再次fork子进程，由于该子进程不是会话首进程，所以该进程将不能重新打开控制终端。然后退出父进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;此步骤目的是通过再次创建子进程后结束当前进程，使进程不再是会话首进程来禁止进程重新打开控制终端。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在子进程中调用chdir()让根目录“/”成为子进程的工作目录；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中，&lt;strong&gt;当前目录所在的文件系统（如“/mnt/usb”）是不能卸载的，这对以后的使用会造成诸多的麻烦&lt;/strong&gt;（比如系统由于某种原因要进入单用户模式）。因此，通常的做法是让&quot;/&quot;作为守护进程的当前工作目录，这样就可以避免上述的问题，当然，如有特殊需要，也可以把当前工作目录换成其他的路径，如/tmp。改变工作目录的常见函数是chdir。(避免原父进程当前目录带来的一些麻烦)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在子进程中调用umask()重设文件权限掩码为0；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件权限掩码是指屏蔽掉文件权限中的对应位。比如，有个文件权限掩码是050，它就屏蔽了文件组拥有者的可读与可执行权限（就是说可读可执行权限均变为7）。&lt;strong&gt;由于使用fork函数新建的子进程继承了父进程的文件权限掩码，这就给该子进程使用文件带来了诸多的麻烦&lt;/strong&gt;。因此把文件权限掩码重设为0即清除掩码（权限为777），这样可以大大增强该守护进程的灵活性。通常的使用方法为umask(0)。(相当于把权限开发)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在子进程中close()不需要的文件描述符&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同文件权限码一样，用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写，但它们一样消耗系统资源，而且可能导致所在的文件系统无法卸下。其实在上面的第二步之后，守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程，守护进程中用常规方法（如printf）输出的字符也不可能在终端上显示出来。所以，文件描述符为0、1和2 的3个文件（常说的输入、输出和报错）已经失去了存在的价值，也应被关闭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;此步骤主要目的是：关闭失去价值的如输入、输出、报错等对应的文件描述符&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;主线程和子线程退出关系：&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;主线程和子线程之间没有必然的退出次序关系。主线程退出，子线程可以继续执行，也可以整体退出。；子线程退出，主线程也可以继续执行。&lt;/strong&gt; 程序加载到内存中执行的时候，&lt;strong&gt;进程&lt;/strong&gt;就会生成一个&lt;strong&gt;主线程&lt;/strong&gt;。虽然主线程和子线程之间没有必然的退出次序关系，但是&lt;strong&gt;如果进程终止，那么进程下所有的线程都会终止&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;若想主线程在子线程前结束，并且进程不结束，就需要用到pthread_exit()函数&lt;/strong&gt;。按照POSIX标准定义，当主线程在子线程终止之前调用pthread_exit()时，子线程是不会退出的。 这里在main函数中调用pthread_exit()只会是主线程退出，而进程并未退出。通过调用pthread_exit函数，线程会显式终止。如果主线程调用pthread_exit，它会先终止，然后等待其他子线程终止后，进程才终止。&lt;/li&gt;
&lt;li&gt;如果主线程以return的方式退出，则子线程会退出。如果主线程使用的是pthread_exit() ，实际上是提前结束了 main 的主线程，也就无法执行后续的 exit() 函数了。所以，这种方法是可以达到主线程退出子线程继续运行的目的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;EXIT和return区别&lt;/h1&gt;
&lt;h2&gt;EXIT&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;是系统层级的系统调用，&lt;strong&gt;指的是让进程退出&lt;/strong&gt;。调用后会开始进行资源的清理和回收。&lt;/li&gt;
&lt;li&gt;exit是一个函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;return&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;return是一个关键字，用于退出这个函数。（结束函数的执行）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;pthread_exit&lt;/h2&gt;
&lt;p&gt;所以使用pthread_exit可以维持子线程不退出的原因是，虽然主进程也是主线程，但是我只让主进程自己的线程退出，也就是不回收进程资源，相当于进程没有结束，子线程依旧可以执行。&lt;/p&gt;
&lt;h1&gt;用户空间(0-3G):进程私有,内核空间(3G-4G):所有进程共享&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220809045820.png&quot; alt=&quot;QQ截图20220809045820&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意，此处的所有进程共享内核空间的意思是共享内核页表。也就是所有进程看到的内核虚拟地址空间是同一个。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内核对于所有的进程，不但物理内存只有一份，虚拟内存也是只有一份。也就是说
&lt;ul&gt;
&lt;li&gt;A进程&lt;strong&gt;用户态&lt;/strong&gt;访问0x100虚拟地址和B进程用户态访问0x100虚拟地址是不同的虚拟地址，也即A进程用户态在0x100虚拟地址里面放了一个数值123，B进程用户态的0x100虚拟地址看不到123，因为对应的是不同的物理地址。&lt;strong&gt;因为不是同一张页表&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A进程内核态访问的0xff虚拟地址，和B进程内核态访问的0xff虚拟地址，是同一个虚拟地址，也对应相同的物理地址。也即A进程内核态在0xff虚拟地址放一个数值123，B进程的内核态如果能够访问0xff虚拟地址的话，也能看到123。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们提到过，子进程fork父进程会复制页表。进程创建的时候会从init进程fork。所以自然会fork init进程的页表。也即实现了内核空间的共享。所以&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个进程在内核态 可以直接通过虚拟地址访问其他进程内核态的数据，因为他们是一个页表。&lt;/li&gt;
&lt;li&gt;一个进程在内核态 不可以直接通过虚拟地址访问其他进程的用户态的数据，因为他们不使用同一个页表。
&lt;ul&gt;
&lt;li&gt;这也就是进程的隔离性，也就是每一个进程都有自己的页表。但是内核空间的页表是共享的。所以一个空间在内核态使用内核页表可以访问其他进程的内核态数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于系统中只有一个内核实例在运行，因此所有进程都映射到单一内核地址空间。内核中维护全局数据结构和每个进程的一些对象信息，后者包括的信息使得内核可以访问任何进程的地址空间。通过地址转换机制进程可以直接访问当前进程的地址空间（通过MMU），而通过一些特殊的方法也可以访问到其它进程的地址空间。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个进程都有两个栈。一个用户栈，一个内核栈。内核栈运行在内核态。&lt;/li&gt;
&lt;li&gt;每一个进程都有自己独立的内核栈，尽管他们共享内核态页表。
&lt;ul&gt;
&lt;li&gt;在内核运行的过程中，如果碰到系统调用创建进程，会创建 &lt;code&gt;task_struct&lt;/code&gt; 这样的实例，内核的进程管理代码会将实例创建在 &lt;code&gt;3G&lt;/code&gt; 至 &lt;code&gt;3G+896M&lt;/code&gt; 的虚拟空间中（这一部分叫做直接映射区。就是这一块空间是连续的，和物理内存是非常简单的映射关系，其实就是虚拟内存地址减去 &lt;code&gt;3G&lt;/code&gt;，就得到物理内存的位置。）当然也会被放在物理内存里面的前 &lt;code&gt;896M&lt;/code&gt; 里面，相应的页表也会被创建。&lt;/li&gt;
&lt;li&gt;在内核运行的过程中，会涉及内核栈的分配，内核的进程管理的代码会将内核栈创建在 &lt;code&gt;3G&lt;/code&gt; 至 &lt;code&gt;3G+896M&lt;/code&gt; 的虚拟空间中，当然也就会被放在物理内存里面的前 &lt;code&gt;896M&lt;/code&gt; 里面，相应的页表项也会被创建。&lt;/li&gt;
&lt;li&gt;内核空间的不同进程的内核栈虚拟地址互不重叠。重叠了不就坏了么。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;为什么每一个进程都是用各自的内核栈呢。
&lt;ul&gt;
&lt;li&gt;假设某个进程通过系统调用运行在内核态（使用这个全局内核堆栈），此时如果被抢占，发生一次切换，另一个进程开始运行，如果这个当前进程又通过系统调用陷入内核，那么这个进程也将使用这个全局内核堆栈，这样的话就把以前那个进程的内核空间堆栈给破坏了。
而如果进程使用独立的内核栈，就避免了这种情况的发生&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每当我们创建新的线程的时候都会一同创建线程的内核栈&lt;/strong&gt;，实现上是通过 slab 分配器从&lt;code&gt; thread_info_cache&lt;/code&gt; 缓存池中分配出来，其大小为 &lt;code&gt;THREAD_SIZE&lt;/code&gt;，一般来说是一个页大小 4K。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://time.geekbang.org/column/article/95715&lt;/p&gt;
&lt;p&gt;https://fanlv.wiki/2021/07/25/linux-mem/#%E5%86%85%E6%A0%B8%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9B%B8%E5%85%B3.assets/12321605-b62889a2b3c2eb38.jpg&quot; alt=&quot;12321605-b62889a2b3c2eb38&quot; /&gt;&lt;/p&gt;
&lt;p&gt;蓝色区域的.bbs .data不太懂。&lt;/p&gt;
&lt;p&gt;https://www.jianshu.com/p/174c1da40c03&lt;/p&gt;
&lt;h1&gt;内核栈&lt;/h1&gt;
&lt;p&gt;看杂记的六大内存区&lt;/p&gt;
&lt;h1&gt;每一个进程都有一个自己的页表&lt;/h1&gt;
&lt;h1&gt;进程有独立地址空间，线程没有。因为线程属于进程，多个线程都属于一个进程，所以互相之间共享这个地址空间。也导致没有隔离性。&lt;/h1&gt;
&lt;h1&gt;中断中的缺页中断&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在请求分页的过程中，如果访问的页面不再内存中，会产生一次缺页中断，在外存中找到所缺的一页将其调入内存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;保护cpu现场&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分析中断原因&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转入缺页中断处理函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;恢复cpu现场，继续执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;缺页异常&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;会出现缺页异常的情况：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线性地址不在虚拟地址空间中&lt;/li&gt;
&lt;li&gt;线性地址在虚拟地址空间中，但没有访问权限&lt;/li&gt;
&lt;li&gt;接上一条，没有与物理地址建立映射关系&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;fork等系统调用时并没有映射物理页（没有权限），写数据-&amp;gt;缺页异常-&amp;gt;写时拷贝&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;映射关系建立了，但在交换分区中&lt;/li&gt;
&lt;li&gt;页面访问权限不足&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;上下文切换&lt;/h1&gt;
&lt;h2&gt;上下文分为三种：寄存器上下文，系统级上下文 和 用户级上下文&lt;/h2&gt;
&lt;h3&gt;寄存器上下文：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;通用寄存器&lt;/li&gt;
&lt;li&gt;程序寄存器(IP)&lt;/li&gt;
&lt;li&gt;处理器状态寄存器(EFLAGS)&lt;/li&gt;
&lt;li&gt;栈指针(ESP)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;系统级上下文&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;进程控制块task_struct&lt;/li&gt;
&lt;li&gt;内存管理信息(mm_struct、vm_area_struct、pgd、pte)&lt;/li&gt;
&lt;li&gt;内核栈&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用户级上下文&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;正文&lt;/li&gt;
&lt;li&gt;数据&lt;/li&gt;
&lt;li&gt;用户态堆栈&lt;/li&gt;
&lt;li&gt;共享存储区&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CPU的上下文切换- CPU上下文包含寄存器和程序计数器。也就是寄存器上下文&lt;/h2&gt;
&lt;p&gt;任务是交给 CPU 运行的，那么在每个任务运行前，CPU 需要知道任务从哪里加载，又从哪里开始运行。&lt;/p&gt;
&lt;p&gt;所以，操作系统需要事先帮 CPU 设置好 &lt;strong&gt;CPU 寄存器和程序计数器&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU 寄存器&lt;/strong&gt;是 CPU 内部一个容量小，但是速度极快的内存（缓存）。我举个例子，寄存器像是你的口袋，内存像你的书包，硬盘则是你家里的柜子，如果你的东西存放到口袋，那肯定是比你从书包或家里柜子取出来要快的多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序计数器&lt;/strong&gt;则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以说，CPU 寄存器和程序计数是 CPU 在运行任何任务前，所必须依赖的环境，这些环境就叫做 &lt;strong&gt;CPU 上下文&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;CPU 上下文切换就是先把前一个任务的 CPU 上下文（CPU 寄存器和程序计数器）保存起来，然后加载新任务的上下文到这些寄存器和程序计数器，最后再跳转到程序计数器所指的新位置，运行新任务。&lt;/p&gt;
&lt;p&gt;系统内核会存储保持下来的上下文信息，当此任务再次被分配给 CPU 运行时，CPU 会重新加载这些上下文，这样就能保证任务原来的状态不受影响，让任务看起来还是连续运行。&lt;strong&gt;（也叫做保存和恢复CPU现场）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话：任务需要切换的时候，（CPU切换到其他任务进行执行的时候），需要先保存旧任务的上下文，然后再加载新的上下文，上下文包括cpu寄存器和程序计数器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上面说到所谓的「任务」，主要包含进程、线程和中断。所以，可以根据任务的不同，把上下文切换分成：&lt;strong&gt;进程上下文切换、线程上下文切换和中断上下文切换&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;进程的上下文切换 - 进程上下文包括内核空间的资源（内核堆栈，寄存器等） + 用户空间资源（虚拟内存，堆，栈，全局变量等） 这一大堆就是PCB&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;进程是由内核管理和调度的，所以进程的切换只能发生在内核态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源，还包括了内核堆栈、寄存器等内核空间的资源。而且，由于切换了进程，也切换了页表。涉及到缓存的部分比如L1 L2 L3 和 TLB大概率会被全部击穿，导致需要进行IO&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通常，会把交换的信息保存在进程的 PCB，当要运行另外一个进程的时候，我们需要从这个进程的 PCB 取出上下文，然后恢复到 CPU 中，这使得这个进程可以继续执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话，我们把重要的信息保存在PCB里面，进程上下文就是PCB内容（如进程ID，进程堆栈，进程各个寄存器的状态）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以 进程的上下文切换需要寄存器上下文（CPU现场），系统级上下文（内核空间的资源） 和 用户级上下文 （用户空间资源）同时切换。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;发生进程上下文切换有哪些场景？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;为了保证所有进程可以得到公平调度，CPU 时间被划分为一段段的时间片，这些时间片再被轮流分配给各个进程。这样，当某个进程的时间片耗尽了，进程就从运行状态变为就绪状态，系统从就绪队列选择另外一个进程运行；&lt;/li&gt;
&lt;li&gt;进程在系统资源不足（比如内存不足）时，要等到资源满足后才可以运行，这个时候进程也会被挂起，并由系统调度其他进程运行；&lt;/li&gt;
&lt;li&gt;当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时，自然也会重新调度；&lt;/li&gt;
&lt;li&gt;当有优先级更高的进程运行时，为了保证高优先级进程的运行，当前进程会被挂起，由高优先级进程来运行；&lt;/li&gt;
&lt;li&gt;发生硬件中断时，CPU 上的进程会被中断挂起，转而执行内核中的中断服务程序；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程的上下文切换：&lt;/h2&gt;
&lt;p&gt;在前面我们知道了，线程与进程最大的区别在于：&lt;strong&gt;线程是调度的基本单位，而进程则是资源拥有的基本单位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以，所谓操作系统的任务调度，实际上的调度对象是线程，而进程只是给线程提供了虚拟内存、全局变量等资源。&lt;/p&gt;
&lt;p&gt;对于线程和进程，我们可以这么理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当进程只有一个线程时，可以认为进程就等于线程；&lt;/li&gt;
&lt;li&gt;当进程拥有多个线程时，这些线程会共享相同的虚拟内存（虚拟地址空间）和全局变量， &lt;strong&gt;堆&lt;/strong&gt;等资源，这些资源在上下文切换时是不需要修改的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;另外，线程也有自己的私有数据，比如栈和寄存器等，这些在上下文切换时是需要保存的。（这部分不共享，每一个线程独有）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;所以上下文切换这还得看线程是不是属于同一个进程：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;当两个线程&lt;strong&gt;不属于&lt;/strong&gt;同一个进程，则切换的过程就跟进程上下文切换一样；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当两个线程是属于同一个进程，因为虚拟内存是共享的，所以在切换时，虚拟内存这些资源就保持不动，只需要切换线程的私有数据、寄存器等不共享的数据。比如上面写到的不共享的，重点就是俩：寄存器，栈，线程ID&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，线程的上下文切换相比进程，开销要小很多&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以 同进程线程的上下文切换只需要切换寄存器上下文。因为系统级上下文（如进程控制块，内存管理信息）和 用户级上下文（如虚拟内存，堆栈等等是共享的）。注意，这里不切换用户级上下文是因为线程栈是从进程的mmap里面划出来的。因为处于同一地址空间所以用户级上下文没有切换，只是多分出来了一块而已。这也就是为什么我们线程也可以访问其他线程的栈，包括主线程的栈。因为处于同一个地址空间。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;系统调用&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;凡是与资源有关的操作、会直接影响到其他进程的操作,一定需要操作系统介入，即需要通过系统调用来实现&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用发生在用户态，对系统调用的处理发生在内核态。执行陷入指令会产生内中断，使处理器从用户态进入内核态&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;因为用户态和内核态不共用一个堆栈，所以用户态调用系统调用时，得先保存用户态的信息到寄存器，然后切入到内核态，将寄存器中的信息拷贝到内核栈开始执行，执行结束后，还原之前用户态的状态让用户态继续往下执行。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;软中断指令&lt;code&gt;int 0X80&lt;/code&gt; 这个int不是integer，interrupt。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用户态和核心态之间的切换是通过中断实现的。并且，中断是实现状态切换的唯一途径。&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;中断只可以被操作系统进行操作。我们想要操作中断必须使用系统调用&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当中断发生时，CPU立即进入核心态。&lt;/li&gt;
&lt;li&gt;当中断发生后，当前运行的进程暂停运行，并由操作系统内核对中断进行处理。&lt;/li&gt;
&lt;li&gt;对于不同的中断信号，会进行不同的处理。&lt;/li&gt;
&lt;li&gt;CPU执行完每一条指令后都会检查是否有中断信号。除执行了关中断指令外。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个过程是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保存 CPU 寄存器里原来用户态的指令位 （保存内核&lt;strong&gt;空间&lt;/strong&gt;资源）&lt;/li&gt;
&lt;li&gt;为了执行内核态代码，CPU 寄存器需要更新为内核态指令的新位置。&lt;/li&gt;
&lt;li&gt;跳转到内核态运行内核任务。&lt;/li&gt;
&lt;li&gt;当系统调用结束后，CPU 寄存器需要恢复原来保存的用户态（内核空间资源），然后再切换到用户空间，继续运行进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20230514201505.png&quot; alt=&quot;QQ截图20230514201505&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中断的核心是跳转。因为CPU内部的中断控制器接收到中断指令后，需要查找对应的中断处理程序，然后告知CPU具体使用的中断处理程序。也就是说，针对特定的中断号，CPU会有不同的处理方式。也就是说，IRQ这种可屏蔽中断信号只负责告诉CPU有事情来了，CPU可以选择不响应（屏蔽）也可以响应。而且具体的响应方式依靠IDT中断描述符表来决定。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;程序在执行过程中通常有用户态和内核态两种状态，CPU对处于内核态根据上下文环境进一步细分，因此有了下面三种状态：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内核态，运行于&lt;strong&gt;进程上下文&lt;/strong&gt;，内核代表&lt;strong&gt;进程&lt;/strong&gt;运行于内核空间。&lt;/li&gt;
&lt;li&gt;内核态，运行于&lt;strong&gt;中断上下文&lt;/strong&gt;，内核代表&lt;strong&gt;硬件&lt;/strong&gt;运行于内核空间。&lt;/li&gt;
&lt;li&gt;用户态，运行于用户空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，&lt;strong&gt;一次系统调用的过程，其实是发生了两次 CPU 上下文切换&lt;/strong&gt;。（用户态-内核态-用户态）&lt;/p&gt;
&lt;p&gt;不过，需要注意的是，&lt;strong&gt;系统调用过程中，并不会涉及到虚拟内存等进程用户态的资源，也不会切换进程&lt;/strong&gt;。这跟我们通常所说的进程上下文切换是不一样的：&lt;strong&gt;进程上下文切换，是指从一个进程切换到另一个进程运行；而系统调用过程中一直是同一个进程在运行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;系统调用过程通常称为特权模式切换，而不是上下文切换。系统调用属于同进程内的 CPU 上下文切换&lt;/strong&gt;。&lt;strong&gt;但实际上，系统调用过程中，CPU 的上下文切换还是无法避免的。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;系统调用比进程上下文切换少了一步 - 保存用户空间资源。因为系统调用是CPU上下文切换&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;系统调用是CPU上下文切换。注意区分进程上下文切换。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以我们也可以理解为，系统调用也仅仅是进行了寄存器上下文切换。因为只切换了寄存器内容。因为处于同进程中，所以用户级和系统级上下文无需切换，只需要保留。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;系统调用过程中可能发生进程切换。比如来自时钟中断 - 时间片用完&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;系统调用过程中可能发生中断。因为中断可以在任意时刻发生。中断不属于任何一个进程上下文。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;为什么维护进程开销大？而线程开销小？&lt;/h1&gt;
&lt;h2&gt;进程：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创建进程时，分配资源、建立 PCB；&lt;/li&gt;
&lt;li&gt;终止进程时，回收资源、撤销 PCB；&lt;/li&gt;
&lt;li&gt;进程切换时，保存当前进程的状态信息；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;线程的创建时间比进程快，因为进程在创建的过程中，还需要资源管理信息，比如内存管理信息、文件管理信息，而线程在创建的过程中，不会涉及这些资源管理信息，而是共享它们；&lt;strong&gt;一句话：线程不复制（建立）PCB&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;线程的终止时间比进程快，因为线程释放的资源相比进程少很多；&lt;strong&gt;一句话：线程不复制pcb，所以也不撤销PCB&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;同一个进程内的线程切换比进程切换快，因为线程具有相同的地址空间（虚拟内存共享），这意味着同一个进程的线程都具有同一个页表，那么在切换的时候不需要切换页表。而对于进程之间的切换，切换的时候要把页表给切换掉，而页表的切换过程开销是比较大的；&lt;strong&gt;一句话：共享虚拟地址空间，映射也不修改所以侧面也是共享物理地址空间，切换线程不需要切换页表&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;由于同一进程的各线程间共享内存和文件资源，那么在线程之间数据传递的时候，就不需要经过内核了，这就使得线程之间的数据交互效率更高了；&lt;strong&gt;一句话：线程没有隔离性，文件资源和内存都共享。因为线程是属于进程的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;子进程fork父进程，哪些数据复制哪些不复制？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;所有用户区数据都复制，比如堆，栈，bss, data, text, 内存映射区，环境变量命令行参数等。&lt;/li&gt;
&lt;li&gt;内核区只有一样不同（不复制）：PID。&lt;/li&gt;
&lt;li&gt;会复制页表（因为每一个进程都有自己的页表），&lt;strong&gt;但不复制物理页面。（如果有写时拷贝COW）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一句话：没有写时复制的话全都复制，只有PID不同。有写时复制的话用哪儿复制哪儿。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在没有写时复制（copy on write）的时候，fork的那一刻，就会为子进程分配新的虚拟内存并且把物理内存中的数据复制至新开辟的物理内存中，然后映射至虚拟内存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果有写时复制，则fork的时候为子进程开辟新的虚拟内存，&lt;strong&gt;但是物理内存依旧映射至父进程的物理内存&lt;/strong&gt;。所以可以理解为现在父子进程共享这一段物理内存，但是&lt;strong&gt;进程针对这段虚拟内存一定是只读的，因为只有设置为只读，这样进行写操作的时候才能触发缺页异常然后分配新的物理内存并复制里面的内容。在这之后，就可以把进程的内存设置为可读写了 。&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意上面是触发缺页异常不是缺页中断。缺页中断说法不准确，应该叫因为缺页异常触发的中断叫缺页中断，因为要和一般中断区分开。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直到：
&lt;ul&gt;
&lt;li&gt;父进程或者是子进程对相应的一段区域（代码段，数据段等等）修改，（理解为对共享区域进行写操作），这个时候内核才会为其对应的段复制一个对应的物理页面然后分配给这个进程使用。同时修改对应的页表。&lt;/li&gt;
&lt;li&gt;理论上因为写时复制，所以代码段不会被修改，如果有进程替换的话，则代码段会被修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;fork前打开的文件描述符共享，fork后的不共享&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为子进程也复制了父进程 的PCB，所以也将父进程中的文件描述符复制了，struct file是内核文件表，每个进程只要有它的地址，就可以找到，所以子进程便可以找到这个文件，对文件进行操作。&lt;strong&gt;所以fork前打开的文件描述符被复制后，引用计数器也会+1。子进程对文件操作也会影响父进程&lt;/strong&gt;，实际上是操作的文件中的偏移量，共享了文件偏移量。&lt;strong&gt;但是在fork之后打开的文件，那就是各自进程打开各自的了，这当然是不共享的了。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;线程崩溃会导致进程崩溃吗&lt;/h1&gt;
&lt;p&gt;线程崩溃会触发SIGSEGV信号。你可以让他选择不直接崩溃，捕捉信号然后继续运行。但是这个信号一般意味着很严重的错误，你也不希望有任何线程触碰到引起错误的这块。**但是线程没有隔离性，各个线程共享同一个地址空间你也无法保证说引起错误的这块一定不会被其他线程碰到。**所以一个接着一个的错误会陆续发生。所以进程崩溃是迟早的事。&lt;/p&gt;
&lt;h1&gt;一个进程中的线程在其他进程中可见吗？ -- 不可见&lt;/h1&gt;
&lt;p&gt;每一个进程都认为系统中只有自己，因为虚拟化。所以说他根本就不知道有其他进程，更不可见其他进程中的线程&lt;/p&gt;
&lt;h2&gt;所有的进程间通信都是间接的。都依赖系统调用&lt;/h2&gt;
&lt;h1&gt;为什么需要虚拟内存？ -- 查看笔记OS30&lt;/h1&gt;
&lt;p&gt;查看笔记OS30&lt;/p&gt;
&lt;h1&gt;Linux有几级页表？ -- 四级页表&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;因为页表一定要覆盖全部虚拟地址空间，不分级的页表就需要有 100 多万个页表项来映射，而二级分页则只需要 1024 个页表项&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于 64 位的系统，两级分页肯定不够了，就变成了四级目录，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局页目录项 PGD（&lt;em&gt;Page Global Directory&lt;/em&gt;）；&lt;/li&gt;
&lt;li&gt;上层页目录项 PUD（&lt;em&gt;Page Upper Directory&lt;/em&gt;）；&lt;/li&gt;
&lt;li&gt;中间页目录项 PMD（&lt;em&gt;Page Middle Directory&lt;/em&gt;）；&lt;/li&gt;
&lt;li&gt;页表项 PTE（&lt;em&gt;Page Table Entry&lt;/em&gt;）；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-27-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9B%B8%E5%85%B3.assets/QQ%E6%88%AA%E5%9B%BE20220926013258.png&quot; alt=&quot;QQ截图20220926013258&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;如何控制虚拟地址和物理地址的转换？ -- MMU&lt;/h1&gt;
&lt;h1&gt;Linux 系统的根目录下主要包括哪些文件夹，各自的作用是什么？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/boot&lt;/code&gt;  存放Linux内核、引导配置等启动文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/bin&lt;/code&gt;  存放最基本的用户命令，普通用户有权限执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev&lt;/code&gt;  存放硬盘、键盘、鼠标、光驱等各种设备文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc&lt;/code&gt;  存放各种配置文件、配置目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/home&lt;/code&gt; 存放普通用户的默认工作文件夹(即 宿主目录、家目录)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/root&lt;/code&gt;  Linux系统管理员(超级用户)root的宿主目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/sbin&lt;/code&gt;  存放最基本的管理命令，一般是管理员才有权限执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr&lt;/code&gt;  存放额外安装的应用程序、源码编译文件、文档等各种用户资料&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var&lt;/code&gt; 存放日志文件、用户邮箱目录、进程运行数据等变化的文档。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/tmp&lt;/code&gt; 存放系统运行过程中使用的一些临时文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/proc&lt;/code&gt; 存放正在运行的进程&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;什么时候陷入内核态&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;注意：陷入内核态和上下文切换无必然关联。上下文切换理解为切换任务。而陷入内核态和此事无必然联系。因为我可以在同一个任务内执行特权指令，但是并不需要切换任务。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;中断是陷入内核的唯一方式&lt;/h2&gt;
&lt;p&gt;中断分为内中断和外中断。&lt;/p&gt;
&lt;h3&gt;内中断（异常，例外） 中断信号来自CPU内部。和当前指令有关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;陷阱，陷入&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一般由陷入指令引发。是应用程序故意引发的。比如系统调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;故障&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;错误条件引发，可以被修复。比如缺页中断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;终止&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异常错误无法修复。比如整数除0 或 非法请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;外中断。中断信号来自CPU外部。和当前指令无关。&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;时钟中断&lt;/li&gt;
&lt;li&gt;IO中断请求&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Linux 查看进程指令&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;ps (process status) 列出当前系统运行的进程
&lt;ul&gt;
&lt;li&gt;aux
&lt;ul&gt;
&lt;li&gt;all 列出所有用户启动的进程&lt;/li&gt;
&lt;li&gt;user 使用该格式列出&lt;/li&gt;
&lt;li&gt;x 列出当前用户在所有中端下的进程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;-ef
&lt;ul&gt;
&lt;li&gt;打印所有进程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这俩区别就是格式不同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;top
&lt;ul&gt;
&lt;li&gt;交互式的，实时动态显示，默认情况下每3秒更新一次。 以全屏交互式的界面显示进程排名，及时跟踪包括CPU、内存等系统资源占用情况   按ctrl + c 终止&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;pstree -aup
&lt;ul&gt;
&lt;li&gt;以树状图的方式展现进程之间的派生关系，显示效果比较直观。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Linux 查看CPU使用率&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;top
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;%us：表示用户空间程序的cpu使用率（没有通过nice调度）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;%sy：表示系统空间的cpu使用率，主要是内核程序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;%ni：表示用户空间且通过nice调度过的程序的cpu使用率。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;%id：空闲cpu&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;%wa：cpu运行时在等待io的时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;%hi：cpu处理硬中断的数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;%si：cpu处理软中断的数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;%st：被虚拟机偷走的cpu&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;GDB&lt;/h1&gt;
&lt;p&gt;记得编译的时候要加&lt;code&gt;-g&lt;/code&gt;才能源码级调试。&lt;code&gt;-g&lt;/code&gt; 选项的作用是在可执行文件中加入源代码的信息，比如可执行文件中第几条机器指令对应源代码的第几行，但并不是把整个源文件嵌入到可执行文件中，所以在调试时必须保证 gdb 能找到源文件。&lt;/p&gt;
&lt;h1&gt;进程间通信和线程间通信&lt;/h1&gt;
&lt;p&gt;查看项目笔记&lt;/p&gt;
&lt;h1&gt;atomic和mutex之间的区别&lt;/h1&gt;
&lt;p&gt;主要是粒度不同。mutex可以让一大块区间被锁住，而atomic只能对单个变量进行互斥。&lt;/p&gt;
&lt;h1&gt;编译期间可以确定函数需要多少内存来存储栈帧&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;栈里面只有函数相关的东西，局部变量，入参，返回值，函数调用之类的，描述一个类的信息在编译期就被转变成具体的内存布局了，唯一的区别就是在函数里new的时候放在heap里，不走new的时候就在stack里&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;挂起 阻塞和睡眠&lt;/h1&gt;
&lt;h2&gt;阻塞：(被动)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;进/线程被动暂停执行&lt;/strong&gt;，&lt;strong&gt;阻塞的进程仍处于内存中&lt;/strong&gt;，OS把处理机分配给另一个就绪进程，而让被暂停的进程处于暂停状态。&lt;/p&gt;
&lt;p&gt;(自动)&lt;strong&gt;阻塞恢复&lt;/strong&gt;：需要在等待的资源得到满足（例如获得了锁）后，才会进入就绪状态，等待被调度而执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阻塞的进程调度机不调度，所以不占用CPU。也就是释放CPU资源&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;阻塞原因：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程：进程由于提出系统服务请求（如I/O操作），但因为某种原因未得到操作系统的立即响应，或者需要从其他合作进程获得的数据尚未到达等原因。&lt;/li&gt;
&lt;li&gt;线程：线程锁问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;挂起（主动）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;用户主动暂停执行进/线程&lt;/strong&gt;，&lt;strong&gt;挂起的进程被换出到外存(磁盘)中。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;挂起的进程不占用CPU，但是原理和阻塞不同。挂起不占用CPU的原因是主动放弃（让出）。调度机会分配时间片给挂起进程，但是挂起进程会主动放弃（让出）。所以在外部来看也是不占用CPU。而阻塞是调度机根本不给阻塞进程分配时间片。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;挂起恢复&lt;/strong&gt;：需要用户主动控制，挂起时线程不会释放对象锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;挂起原因：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时，希望暂停使自己的程序静止下来。亦即，使正在执行的进程暂停执行；若此时用户进程正处于就绪状态而未执行，则该进程暂不接受调度，以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。&lt;/li&gt;
&lt;li&gt;父进程的请求。有时父进程希望挂起自己的某个子进程，以便考察和修改子进程，或者协调各子进程间的活动。&lt;/li&gt;
&lt;li&gt;负荷调节的需要。当实时系统中的工作负荷较重，已可能影响到对实时任务的控制时，可由系统把一些不重要的进程挂起，以保证系统能正常运行。&lt;/li&gt;
&lt;li&gt;操作系统的需要。操作系统有时希望挂起某些进程，以便检查运行中的资源使用情况或进行记账。&lt;/li&gt;
&lt;li&gt;对换出的需要。为了缓和内存紧张的情况，将内存中处于阻塞状态的进程换至外存上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;睡眠（主动）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;用户主动暂停执行进/线程，睡眠进/线程任存于内存。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;睡眠恢复&lt;/strong&gt;：是自动完成的，睡眠时间到了则恢复到就绪态，睡眠时线程不会释放对象锁。&lt;/p&gt;
&lt;h1&gt;常用指令&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;strace -f&lt;/code&gt; 跟踪发出的系统调用和由初始进程创建的所有子进程&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;pmap&lt;/code&gt;查看&lt;code&gt;proc&lt;/code&gt;目录下的进程文件(&lt;code&gt;maps&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nm&lt;/code&gt;查看符号表&lt;/p&gt;
&lt;h1&gt;操作系统和进程都是状态机&lt;/h1&gt;
&lt;p&gt;我们要从状态机的角度理解操作系统和进程。因为是状态机，它会有状态的切换。会有初始状态和终止状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fork&lt;/code&gt;理解为一个两个分叉的叉子。也就是复制。它完整的复制出一个原先的状态机，所有的资源都会被复制。所以&lt;code&gt;fork&lt;/code&gt;是创建状态机。
&lt;ul&gt;
&lt;li&gt;所以linux的第一个进程是&lt;code&gt;init&lt;/code&gt;。然后我们会通过&lt;code&gt;fork&lt;/code&gt;这个&lt;code&gt;init&lt;/code&gt;创建所有后续进程。这也是进程树的由来。因为&lt;code&gt;fork&lt;/code&gt;一定会有父子关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execve&lt;/code&gt;函数族负责把状态机的状态重置（切换）。简单理解，操作系统这个状态机只有两个模式。一个模式是从父进程&lt;code&gt;fork&lt;/code&gt;出来的时候。也就是初始状态。另一个是执行其他任务的时候。&lt;code&gt;execve&lt;/code&gt;是唯一一个可以执行任务的系统调用，所以它是负责状态的切换（重置）
&lt;ul&gt;
&lt;li&gt;所以&lt;code&gt;execve&lt;/code&gt;会让你输入程序路径，传入参数和环境变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exit&lt;/code&gt;就是销毁状态机。但是要注意杂记4中提到的&lt;code&gt;exit&lt;/code&gt;和&lt;code&gt;_exit&lt;/code&gt;之间的区别。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;使用pmap查看&lt;code&gt;proc&lt;/code&gt;目录下的进程文件。&lt;/h1&gt;
&lt;p&gt;在下面我们会发现内存区域有的会被标识为&lt;code&gt;vvar&lt;/code&gt;和&lt;code&gt;vdso&lt;/code&gt;或&lt;code&gt;vsyscall&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vvar&lt;/strong&gt;（Virtual Variable）：&lt;code&gt;vvar&lt;/code&gt;是一个虚拟内存区域，用于存储与线程特定变量（Thread-specific Variables）相关的数据。这些变量对于每个线程而言都是唯一的，包括线程 ID、栈保护、时间信息等。&lt;code&gt;vvar&lt;/code&gt;通常位于用户态的内核映射区域，用于提供对这些变量的快速访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vdso&lt;/strong&gt;（Virtual Dynamic Shared Object）：&lt;code&gt;vdso&lt;/code&gt;是一种特殊的共享对象，它在用户空间和内核空间之间提供了一些函数和数据的映射。&lt;strong&gt;这些函数和数据可以直接在用户空间中访问，而无需进行系统调用。系统调用需要进入内核态，会有性能损失。&lt;/strong&gt;&lt;code&gt;vdso&lt;/code&gt;中包含一些常见的系统调用函数的快速实现，例如获取当前时间和系统调用指令的执行。&lt;strong&gt;所以&lt;code&gt;vdso&lt;/code&gt;理解为快速系统调用，也就是无需陷入内核的系统调用&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;举个例子就是&lt;code&gt;time(2)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;内核维护的秒级时间。所有进程映射同一个页面&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可以查看&lt;a href=&quot;https://zhuanlan.zhihu.com/p/436454953&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;比较有意思的事情是，获取&lt;code&gt;vvar&lt;/code&gt;部分一些特定信息的函数就在&lt;code&gt;vdso&lt;/code&gt;里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vsyscall&lt;/strong&gt;（Virtual System Call）：&lt;code&gt;vsyscall&lt;/code&gt;是一种特殊的映射区域，用于在用户空间中执行一些常见的系统调用。与传统的系统调用相比，&lt;code&gt;vsyscall&lt;/code&gt;提供了更快的调用机制，避免了用户空间和内核空间之间的上下文切换。但是，在现代的Linux系统中，&lt;code&gt;vsyscall&lt;/code&gt;已经被&lt;code&gt;vdso&lt;/code&gt;所取代。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;入侵其他进程地址空间&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;/proc/[PID]/mem&lt;/code&gt;文件：该文件允许对进程的内存进行直接读取和写入操作。通过读取&lt;code&gt;/proc/[PID]/mem&lt;/code&gt;文件，我们可以读取进程的内存内容，包括代码、数据和堆栈等。同时还可以将数据写入&lt;code&gt;/proc/[PID]/mem&lt;/code&gt;文件，以修改进程的内存。&lt;/p&gt;
&lt;h1&gt;操作系统的本质&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;操作系统的本质是API+对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API+对象 = kernel&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;但是用户直接操作kernel不太现实&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以我们提供了kernel的封装，这个应用程序就是shell&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令行是shell，桌面也是shell只不过是加了图形&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;shell 应该理解为是一种编程语言，是一种能把命令翻译成系统调用的编程语言。&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;重定向: &lt;code&gt;cmd &amp;gt; file &amp;lt; file 2&amp;gt; /dev/null&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;顺序结构: &lt;code&gt;cmd1; cmd2&lt;/code&gt;, &lt;code&gt;cmd1 &amp;amp;&amp;amp; cmd2&lt;/code&gt;, &lt;code&gt;cmd1 || cmd2&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管道: &lt;code&gt;cmd1 | cmd2&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;预处理: &lt;code&gt;$()&lt;/code&gt;, &lt;code&gt;&amp;lt;()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变量/环境变量、控制流……&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类比窗口管理器里的 “叉”、“最小化”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jobs&lt;/code&gt;, &lt;code&gt;fg&lt;/code&gt;, &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;wait&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jobs&lt;/code&gt;：该命令用于列出当前Shell会话中正在运行的作业（jobs）。作业可以是前台或后台运行的命令或程序。&lt;code&gt;jobs&lt;/code&gt; 命令通常与作业控制命令一起使用，例如 &lt;code&gt;fg&lt;/code&gt; 和 &lt;code&gt;bg&lt;/code&gt;，以管理作业。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fg&lt;/code&gt;：该命令用于将一个在后台运行的作业切换到前台运行。它将指定的作业ID或最近一个后台作业切换到前台，并将其作为当前正在运行的命令。&lt;code&gt;fg&lt;/code&gt; 命令常用的语法是 &lt;code&gt;fg [jobID]&lt;/code&gt;，其中 &lt;code&gt;jobID&lt;/code&gt; 是作业的标识符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bg&lt;/code&gt;：该命令用于将一个在后台暂停的作业切换到继续在后台运行。它将指定的作业ID或最近一个停止的后台作业切换到后台运行。&lt;code&gt;bg&lt;/code&gt; 命令常用的语法是 &lt;code&gt;bg [jobID]&lt;/code&gt;，其中 &lt;code&gt;jobID&lt;/code&gt; 是作业的标识符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait&lt;/code&gt;：该命令用于等待指定的作业完成。它会阻塞当前的Shell进程，直到指定的作业及其相关的子进程全部结束。&lt;code&gt;wait&lt;/code&gt; 命令常用的语法是 &lt;code&gt;wait [jobID]&lt;/code&gt;，其中 &lt;code&gt;jobID&lt;/code&gt; 是作业的标识符。如果未指定 &lt;code&gt;jobID&lt;/code&gt;，则 &lt;code&gt;wait&lt;/code&gt; 命令将等待所有当前活动的子进程完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>智能指针（一）：unique_ptr、shared_ptr 与 weak_ptr 详解</title><link>https://sterben-01.github.io/posts/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;智能指针&lt;/h1&gt;
&lt;p&gt;我们不爱裸指针的原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;裸指针的声明没办法告诉我们它指向的是单个对象还是数组。&lt;/li&gt;
&lt;li&gt;没办法知道用完这个裸指针后要不要销毁它指向的对象。&lt;/li&gt;
&lt;li&gt;没办法知道怎么销毁这个裸指针，是用&lt;code&gt;operator delete&lt;/code&gt;还是什么其它自定义的途径。&lt;/li&gt;
&lt;li&gt;参照原因1，没办法知道该用&lt;code&gt;delete&lt;/code&gt;还是&lt;code&gt;delete[]&lt;/code&gt;，如果用错了，结果未定义。&lt;/li&gt;
&lt;li&gt;很难保证调用路径上恰好销毁这个指针一次，可能内存泄露，也可能double free。&lt;/li&gt;
&lt;li&gt;通常没办法知道裸指针是否是空悬指针，即是否指向已销毁的对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;智能指针就是来解这些问题的，它们用起来像裸指针，但能避免以上的很多陷阱。C++11中有4种智能指针：&lt;code&gt;std::auto_ptr&lt;/code&gt;、&lt;code&gt;std::unique_ptr&lt;/code&gt;、&lt;code&gt;std::shared_ptr&lt;/code&gt;、&lt;code&gt;std::weak_ptr&lt;/code&gt;。其中&lt;code&gt;std::auto_ptr&lt;/code&gt;已经过时了，C++11中可以被&lt;code&gt;std::unique_ptr&lt;/code&gt;取代了。&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;unique_ptr&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;std::unique_ptr&lt;/code&gt;体现了&lt;strong&gt;显式所有权&lt;/strong&gt;的语义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;非空的&lt;code&gt;std::unique_ptr&lt;/code&gt;总是拥有它指向的对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动一个&lt;code&gt;std::unique_ptr&lt;/code&gt;会将源指针持有的所有权移交给目标指针；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不允许复制&lt;code&gt;std::unique_ptr&lt;/code&gt;；（无拷贝构造和拷贝赋值）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意可以通过直接初始化使用裸指针构造一个&lt;code&gt;unique_ptr&lt;/code&gt; (如下面的&lt;code&gt;ptr3&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只允许移动&lt;code&gt;std::unique_ptr&lt;/code&gt;（只有移动构造和移动赋值）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非空的&lt;code&gt;std::unique_ptr&lt;/code&gt;总是销毁它持有的资源，默认是通过&lt;code&gt;delete&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以使用 &lt;code&gt;std::unique_ptr&lt;/code&gt; 管理具备专属所有权的资源。&lt;code&gt;unique_ptr&lt;/code&gt;的作用基本上就是帮助你进行资源释放（析构）&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;创建指针&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;struct myobj{
    myobj(int x):val(x){}
    int val;
};

void deleter(myobj* ptr){
    cout &amp;lt;&amp;lt;&quot;deleter called&quot; &amp;lt;&amp;lt; endl;
    delete ptr;        //需要在这里删掉new出来的内容 如果不写的话会内存泄漏。因为智能指针不再执行自己的释放资源的函数。改为执行这个函数。但是这个函数里面如果没有释放资源就会内存泄漏。
    ptr = nullptr;
}


template&amp;lt;typename T&amp;gt;
class mydeleter{ 		//自定义删除器类。functor
    public:
        void operator()(T* ptr) const{
            cout &amp;lt;&amp;lt;&quot;deleter called&quot; &amp;lt;&amp;lt; endl;
            delete ptr;        
            ptr = nullptr;
        }
};

int main(){
    unique_ptr&amp;lt;myobj&amp;gt; ptr1;                                         //空指针
    unique_ptr&amp;lt;myobj&amp;gt; ptr2 = new myobj(5);                        	//错误。智能指针构造函数是explicit的。禁止隐式转换，所以不允许带有隐式转换的拷贝初始化。但是可以用直接初始化
    unique_ptr&amp;lt;myobj&amp;gt; ptr3(new myobj(5));                           //ok
    unique_ptr&amp;lt;myobj&amp;gt; ptr3_1 = move(ptr3); 						//使用move拷贝初始化
    unique_ptr&amp;lt;myobj&amp;gt; ptr3_2(move(ptr3));						//使用move直接初始化
    unique_ptr&amp;lt;myobj, void(*)(myobj*)&amp;gt; ptr4(new myobj(5), deleter); //自定义删除器。注意需要在自定义删除器内部进行资源释放，因为使用自定义删除器后，智能指针不会接管释放资源。注意这里传入的是函数指针，会使unique_ptr整体增大，变成两个指针。正确使用应该是下面那种定义类型。
    unique_ptr&amp;lt;myobj, mydeleter&amp;lt;myobj&amp;gt;&amp;gt; ptr4_1(new myobj(5));       //自定义删除器。注意，这里在尖括号内传入的是仿函数类型。这样会直接使用类型的可调用对象做为删除器。这样效率很高，少一个指针
    //所以写自定义删除器的意义是如果你想在某一个资源的析构函数或者是释放资源的同时执行其他动作，就需要写这个。因为，资源的释放阶段是全部交给智能指针接管的。
    unique_ptr&amp;lt;myobj&amp;gt; ptr5 = make_unique&amp;lt;myobj&amp;gt;(new myobj(5));    	//错误 使用make方法。make_unique是把传入参数完美转发至对象构造函数。只需要传递参数即可，无需再次new。
    unique_ptr&amp;lt;myobj&amp;gt; ptr5 = make_unique&amp;lt;myobj&amp;gt;(5);                 //使用make方法。make方法无法使用自定义删除器和大括号初始化（列表初始化）
    unique_ptr&amp;lt;myobj&amp;gt; ptr5_1(make_unique&amp;lt;myobj&amp;gt;(5)); 				//使用make方法。直接初始化
    unique_ptr&amp;lt;myobj&amp;gt; ptr5(ptr3);                                   //禁止拷贝。因为只能有一个unique_ptr指向一个资源。所有权语义。
    ptr5 = ptr3;													//禁止拷贝赋值。
    
    //------------------------------------------------------------------------
    unique_ptr&amp;lt;int&amp;gt;a_ptr(new int(20)); //使用直接初始化
    a_ptr = make_unique&amp;lt;int&amp;gt;(30); //依旧可以。这里调用的是operator=，参数是unique_ptr&amp;lt;int&amp;gt;&amp;amp;&amp;amp;。因为make_unique会按值返回一个unique_ptr对象，所以是右值。并且unique_ptr&amp;amp; operator=( unique_ptr&amp;amp;&amp;amp; r )会先把自己的资源释放掉，再接管新的资源，此处不会有内存泄漏。
}         
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;make&lt;/code&gt;方法无法使用大括号初始化（列表初始化）可以用&lt;code&gt;auto&lt;/code&gt;先把大括号接住（推导出一个&lt;code&gt;list initializer&lt;/code&gt;类型），然后把&lt;code&gt;auto&lt;/code&gt;声明的变量拿过来用。(使用列表初始化构造)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意，正确使用自定义删除器应该是定义一个类型并且这个类型需要是空基类而不是传入一个函数指针，这也是为什么&lt;code&gt;unique_ptr&lt;/code&gt;把删除器当作类型一部分的原因&lt;/strong&gt;。如果传入函数指针的话，智能指针大小将会是16。因为他会额外包含一个指针。这里最后提到。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关于自定义删除器的意思是：&lt;strong&gt;智能指针不再调用自己的函数来释放资源。改为调用你指定的函数。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;但是要注意，因为智能指针不再调用自己的函数释放资源，你必须在你自己定义的函数内释放对应的资源。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;unique_ptr做为自定义对象的成员时，如何构造&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        int val;
        string str;
        unique_ptr&amp;lt;int&amp;gt;u_ptr = nullptr;

        myobj(int x, const string&amp;amp; rhsstr, int num): val(x), str(rhsstr), u_ptr(make_unique&amp;lt;int&amp;gt;(num)){} //可以，使用make方法直接初始化
        myobj(int x, const string&amp;amp; rhsstr, int num): val(x), str(rhsstr), u_ptr(new int(num)){} //可以，直接使用构造函数初始化
        myobj(int x, const string&amp;amp; rhsstr, int num): val(x), str(rhsstr){} //可以
        myobj(int x, const string&amp;amp; rhsstr, int num): val(x), str(rhsstr){
            u_ptr = make_unique&amp;lt;int&amp;gt;(num); //可以。使用operator=搭配make_unique方法。看上面横线下方的例子。
        }

        myobj(int x, const string&amp;amp; rhsstr, int* num): val(x), str(rhsstr), u_ptr(num){} //可以。使用裸指针直接初始化
        myobj(int x, const string&amp;amp; rhsstr, unique_ptr&amp;lt;int&amp;gt; num): val(x), str(rhsstr), u_ptr(move(num)){} //可以，使用move
    
    
        myobj(int x, const string&amp;amp; rhsstr, unique_ptr&amp;lt;int&amp;gt; num): val(x), str(rhsstr), u_ptr(num){}  //不可以，禁止拷贝初始化
        myobj(int x, const string&amp;amp; rhsstr, int num): val(x), str(rhsstr), u_ptr(num){} //不行。unique_ptr没有这个构造函数
        myobj(int x, const string&amp;amp; rhsstr, int num): val(x), str(rhsstr){
            u_ptr = new int(5); //不可以。没有形参为裸指针的operator=函数。
        }
        
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;传入裸指针的时候注意double free问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用&lt;code&gt;get()&lt;/code&gt;来获取内部包含的普通指针&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;unique_ptr&amp;lt;myobj&amp;gt; ptr5 = make_unique&amp;lt;myobj&amp;gt;(5);   
ptr5.get();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;所有智能指针都可以通过解引用直接获取值。和普通指针一样。&lt;/h4&gt;
&lt;h2&gt;release()和reset()&lt;/h2&gt;
&lt;h3&gt;release()&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;u.release()&lt;/code&gt;是放弃对指针的控制权，返回指针，并将u置空。意思是释放后会返回裸指针，我们可以继续使用。（释放所有权，但是指向的对象不会被销毁）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;reset()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void reset( pointer ptr = pointer() ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给定指向 &lt;code&gt;*this&lt;/code&gt; 所管理对象的指针 &lt;code&gt;current_ptr&lt;/code&gt; ，进行下列行动，以此顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保存当前指针的副本 &lt;code&gt;old_ptr = current_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;以传入参数重写当前指针 &lt;code&gt;current_ptr = ptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若&lt;strong&gt;旧指针非空&lt;/strong&gt;，则删除先前管理的对象 &lt;code&gt;if(old_ptr != nullptr) get_deleter()(old_ptr)&lt;/code&gt; 。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;u.reset()&lt;/code&gt;销毁u指向的对象，并将u置为空。意思是释放后无法继续使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;u.reset(q)&lt;/code&gt;销毁u原来指向的对象，令u获取q指向对象的所有权。（q的所有权被接管后会成为空指针）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意这个要这么用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;unique_ptr&amp;lt;int&amp;gt; ptr1(new int (10));
unique_ptr&amp;lt;int&amp;gt; ptr2(new int (20));
ptr2.reset(ptr1.release()); //等同于 ptr2 = move(ptr1);
ptr2.reset(ptr1); //错误！！！不能这么用。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;强调一下上面&lt;code&gt;reset()&lt;/code&gt;的顺序。之所以是这个顺序是为了保证抛出异常的时候，新的对象需要被当前指针接管。这样就算再删除旧的对象的时候发生了异常，新的对象已经被当前指针接管了，不会产生悬空对象的问题。因为前两步都是指针之间的赋值，不会有什么异常的可能。但是删除销毁对象的时候则可能出现问题。&lt;/p&gt;
&lt;h3&gt;operator=(std::nullptr_t)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;u = nullptr&lt;/code&gt;和使用&lt;code&gt;reset&lt;/code&gt;等效
&lt;ul&gt;
&lt;li&gt;https://zh.cppreference.com/w/cpp/memory/unique_ptr/operator%3D&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;可能的实现&lt;/h2&gt;
&lt;p&gt;查看&lt;code&gt;uptr.cpp&lt;/code&gt;。简陋版本。&lt;/p&gt;
&lt;h2&gt;unique_ptr 做为函数入参如何传递比较好？&lt;/h2&gt;
&lt;p&gt;看情况。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一般情况强烈建议使用值传递。因为这显示了所有权转移。但是可能允许使用右值引用为形参。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base(std::unique_ptr&amp;lt;Base&amp;gt; &amp;amp;&amp;amp;n): next(std::move(n)) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候我可以合法传递临时对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base newBase(std::unique_ptr&amp;lt;Base&amp;gt;(new Base));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传递非临时对象依旧必须使用&lt;code&gt;move&lt;/code&gt;， &lt;strong&gt;但是会引发一个问题，让调用者困惑。&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base newBase(std::move(nextBase));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个问题在于。在这一行之后，我们应该期望nextBase已经为空。因为move在那。但是这并不能保证。需要看函数内的代码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么时候这一行会使得move过的指针&lt;strong&gt;不为空&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void funca(unique_ptr&amp;lt;int&amp;gt;&amp;amp;&amp;amp; ptr){ //注意我们的形参不是值传递，而是右值引用。所以我们没有拷贝，也没有赋值。什么都没做。引用传递不构造新对象对吧？右值引用也是引用呀。所以funca什么都没做。
    cout &amp;lt;&amp;lt;&quot;called &quot; &amp;lt;&amp;lt; endl;
}
int main(){
    unique_ptr&amp;lt;int&amp;gt; ptr1(new int (10));
    funca(move(ptr1)); //记住，这里move仅仅是static_cast成了一个右值引用的形式。这时候允许传入funca。具体是否要调用移动构造需要看funca是值传递还是引用传递。
    if(ptr1.get() != nullptr){
        cout &amp;lt;&amp;lt;&quot;not null&quot; &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt; *ptr1 &amp;lt;&amp;lt; endl;
    return 0;
}
/*
输出：
called 
not null
10
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码看到，我们传入函数后没有对指针产生任何操作。所以是引用传入，然后什么也没干。从头到尾也只有第一行的&lt;code&gt;ptr1&lt;/code&gt;和其在函数头的引用&lt;code&gt;ptr&lt;/code&gt;在管理同一份资源。由于入参是引用，也没有double free。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;记住move只是把参数变为右值。至于什么时候触发移动构造或移动赋值，则需要我们进行赋值或构造的动作。在这里没有任何赋值或构造的动作，自然不会触发移动。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么时候这一行会使得move过的指针&lt;strong&gt;为空&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class v{
    public:
        unique_ptr&amp;lt;int&amp;gt; ptr;
        v(unique_ptr&amp;lt;int&amp;gt;&amp;amp;&amp;amp; rhs): ptr(move(rhs)){} //注意这里，我们产生了构造动作。调用了移动构造。
};
void funca(unique_ptr&amp;lt;int&amp;gt;&amp;amp;&amp;amp; ptr){
    cout &amp;lt;&amp;lt;&quot;called &quot; &amp;lt;&amp;lt; endl;
}
int main(){
    unique_ptr&amp;lt;int&amp;gt; ptr1(new int (10));
    v obj(move(ptr1));
    if(ptr1.get() != nullptr){
        cout &amp;lt;&amp;lt;&quot;not null&quot; &amp;lt;&amp;lt; endl;
    }
    else{
        cout &amp;lt;&amp;lt; &quot;null&quot; &amp;lt;&amp;lt; endl;
    }

    return 0;
}
/*
输出
null
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的代码看到，我们的&lt;code&gt;v&lt;/code&gt;类构造函数再次使用了&lt;code&gt;move&lt;/code&gt;把入参移动赋值给了类内的&lt;code&gt;ptr&lt;/code&gt;。因为这里有了构造动作，所以调用了移动构造。所以这时候入参的&lt;code&gt;rhs&lt;/code&gt;会被移动到&lt;code&gt;ptr&lt;/code&gt;。所以构造对象后，外部的&lt;code&gt;ptr1&lt;/code&gt;为空。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;所以如果使用右值引用做为入参，则调用方不能仅通过函数签名确定传入的智能指针的所有权是否会被转移，依旧需要看函数内部执行了什么，会产生困惑。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果按照值传递，则传入前需要move接管所有权。此时外部的智能指针已经无效。函数返回后指向的资源会被释放。意味着这个指针（里面包裹的指针）一旦传入函数就不能在函数返回后继续使用。因为已经无效。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果按照引用传递，则表明需要在该函数内修改传入的智能指针。所以又想要在函数调用后继续使用该指针，又不想修改传入的智能指针，最好的办法是使用常量左值引用。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://stackoverflow.com/questions/8114276/how-do-i-pass-a-unique-ptr-argument-to-a-constructor-or-a-function&lt;/p&gt;
&lt;p&gt;https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r33-take-a-unique_ptrwidget-parameter-to-express-that-a-function-reseats-thewidget&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;shared_ptr&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt;内部有引用计数，被复制时，引用计数+1（因为多个指针指向了同一个对象），有&lt;code&gt;std::shared_ptr&lt;/code&gt;析构时，引用计数-1，当引用计数为0时，析构持有的对象。引用就是计算有多少个&lt;code&gt;std::shared_ptr&lt;/code&gt;指向了同一个对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意，计数器为0释放资源的时候是通过控制块内的指向对象的指针释放的 （通过管理指针）。而不是通过外层的指向对象的指针释放的。（而不是通过存储指针）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引用计数的存在有以下性能影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt;的大小是裸指针的两倍：一个指针指向持有的对象，一个指针指向控制块。&lt;/li&gt;
&lt;li&gt;引用计数使用的内存必须动态分配，原因是&lt;code&gt;std::shared_ptr&lt;/code&gt;的引用计数是非侵入式的，必须要独立在对象外面。用&lt;code&gt;std::make_shared&lt;/code&gt;能避免这次单独的内存分配。&lt;/li&gt;
&lt;li&gt;引用计数的加减必须是原子的，因此你必须假设读写引用计数是有成本的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意，不是所有&lt;code&gt;std::shared_ptr&lt;/code&gt;的构造都会增加引用计数，移动构造就不会。因此移动构造一个&lt;code&gt;std::shared_ptr&lt;/code&gt;要比复制一个更快。&lt;/p&gt;
&lt;p&gt;与&lt;code&gt;std::unique_ptr&lt;/code&gt;类似，&lt;code&gt;std::shared_ptr&lt;/code&gt;的默认销毁动作也是&lt;code&gt;delete&lt;/code&gt;，且也可以接受自定义的销毁器。但与&lt;code&gt;std::unique_ptr&lt;/code&gt;不同的是，&lt;code&gt;std::shared_ptr&lt;/code&gt;的销毁器类型不必作为它的模板参数之一。&lt;/p&gt;
&lt;p&gt;因此&lt;code&gt;std::shared_ptr&lt;/code&gt;要比&lt;code&gt;std::unique_ptr&lt;/code&gt;使用更灵活，比如不同销毁器的&lt;code&gt;std::shared_ptr&lt;/code&gt;可以放到同一个容器中，而&lt;code&gt;std::unique_ptr&lt;/code&gt;则不可以。&lt;/p&gt;
&lt;p&gt;另外，不同的销毁器不会改变&lt;code&gt;std::shared_ptr&lt;/code&gt;的大小。&lt;code&gt;std::shared_ptr&lt;/code&gt;&lt;strong&gt;内部需要为引用计数单独开辟一块内存&lt;/strong&gt;，那么这块内存中再放一个销毁器也没什么额外开销。实际上这块内存被称为”控制块”，它里面包含以下元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用计数&lt;/li&gt;
&lt;li&gt;弱引用计数 (由&lt;code&gt;weak_ptr&lt;/code&gt;控制)&lt;/li&gt;
&lt;li&gt;其它数据，包括：
&lt;ul&gt;
&lt;li&gt;自定义销毁器&lt;/li&gt;
&lt;li&gt;内存分配器&lt;/li&gt;
&lt;li&gt;**一个指向对象的指针。但是这个指针的类型是对象的原始类型。**这个指针可以叫管理指针。
&lt;ul&gt;
&lt;li&gt;比如&lt;code&gt;shared_ptr&amp;lt;father&amp;gt; ptr(new child)&lt;/code&gt;。这里控制块内指向对象的指针是&lt;code&gt;child&lt;/code&gt;类的。而控制块外部另一个指向对象的指针是&lt;code&gt;father&lt;/code&gt;类的。至于为什么，看下面的副作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;控制块的创建规则为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;std::make_shared&lt;/code&gt;总会创建一个控制块。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过一个独享所有权的指针（如&lt;code&gt;std::unique_ptr&lt;/code&gt;或&lt;code&gt;std::auto_ptr&lt;/code&gt;）创建出的&lt;code&gt;std::shared_ptr&lt;/code&gt;总会创建一个控制块。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如&lt;code&gt;std::unique_ptr&lt;/code&gt;转为&lt;code&gt;shared_ptr&lt;/code&gt;时会创建控制块，因为&lt;code&gt;unique_ptr&lt;/code&gt;本身不使用控制块，同时&lt;code&gt;unique_ptr&lt;/code&gt;置空）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过裸指针创建的&lt;code&gt;std::shared_ptr&lt;/code&gt;会创建控制块。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这意味从同一个裸指针出发来构造不止一个&lt;code&gt;std::shared_ptr&lt;/code&gt;时会创建多重的控制块，也意味着对象会被析构多次。如果想从一个己经拥有控制块的对象出发创建一个&lt;code&gt;std::shared_ptr&lt;/code&gt;，可以传递一个&lt;code&gt;shared_ptr&lt;/code&gt;或&lt;code&gt;weak_ptr&lt;/code&gt;而非裸指针作为构造函数的实参，这样则不会创建新的控制块。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;再次重申，&lt;code&gt;shared_ptr&lt;/code&gt;对象并不包含控制块本身，它包含的是指向控制块的指针。由此会引出&lt;code&gt;make&lt;/code&gt;方法的缺点。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/shared_ptr.png&quot; alt=&quot;shared_ptr&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/QQ%E6%88%AA%E5%9B%BE20220728115439.png&quot; alt=&quot;QQ截图20220728115439&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/1449936-20211201093930856-124882834-16591462032631.png&quot; alt=&quot;1449936-20211201093930856-124882834&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;从上图我们可以看到，一个&lt;code&gt;shared_ptr&lt;/code&gt;会有两个指针指向同一块堆区内存。一个是外面的那个指针叫做存储指针，一个是控制块里面的那个指针叫管理指针。两者类型并不一定相同。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 持有的指针是通过 &lt;code&gt;get()&lt;/code&gt;返回的（也就是外层的指针）；而控制块所持有的指针/对象则是最终引用计数归零时会被删除的那个。两者并不一定相等。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建指针&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;myobj&amp;gt; ptr1(new myobj(5));       //创建共享指针     
shared_ptr&amp;lt;myobj&amp;gt; ptr2(ptr1);               //通过共享指针直接初始化共享指针。两个指针指向同一个数据，所以不新建控制块。引用计数+1
shared_ptr&amp;lt;myobj&amp;gt; ptr3 = ptr1;              //可以通过赋值方法拷贝初始化共享指针。两个指针指向同一个数据，所以不新建控制块。引用计数+1
cout &amp;lt;&amp;lt; ptr1.use_count() &amp;lt;&amp;lt; endl;           //使用use_count查看引用计数。输出3
cout &amp;lt;&amp;lt; ptr2.use_count() &amp;lt;&amp;lt; endl;           //输出3
cout &amp;lt;&amp;lt; ptr3.use_count() &amp;lt;&amp;lt; endl;           //输出3

shared_ptr&amp;lt;myobj&amp;gt; ptr4 = make_shared&amp;lt;myobj&amp;gt;(5);     //使用make方法创建
shared_ptr&amp;lt;myobj&amp;gt; ptr4_1(make_shared&amp;lt;myobj&amp;gt;(5));    //使用make方法直接初始化
ptr4 = ptr3;                                        //可以拷贝赋值。此时ptr4的控制块引用计数为0。因为被3覆盖了。所以ptr4指向的原始资源析构。ptr4此时和ptr3指向同一资源
cout &amp;lt;&amp;lt; ptr4.use_count()&amp;lt;&amp;lt;endl;                     //共享控制块，此时为4。

shared_ptr&amp;lt;myobj&amp;gt; ptr5 = new myobj(4);            	//不可以。禁止隐式转换。智能指针构造函数是explicit的。所以不允许使用带有隐式转换的拷贝初始化。但可以使用直接初始化
shared_ptr&amp;lt;myobj&amp;gt; ptr5_1(new myobj(4));    			//可以 裸指针直接初始化
shared_ptr&amp;lt;myobj&amp;gt; ptr6(new myobj(4), deleter);      //自定义删除器。无需指定类型名。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用&lt;code&gt;get()&lt;/code&gt;来获取内部包含的普通指针&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;这个获取的指针指的是存储指针而非管理指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;myobj&amp;gt; ptr5 = make_shared&amp;lt;myobj&amp;gt;(5);   
ptr5.get();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;reset()&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当函数没有实参时，该函数会使当前 &lt;code&gt;shared_ptr&lt;/code&gt; 所指堆内存的引用计数减 1，同时将当前对象重置为一个空指针。当然，如果指向对象的引用计数为0，则释放资源。&lt;strong&gt;注意这个重置为空指针的行为包括剥离与原控制块的访问权限！！因为它已经是空指针，没有理由继续和源对象的控制块有关联。所以就算此时指向的对象计数仍不为0，但是由于此时是空指针，访问空指针的控制块自然会返回引用计数为0。因为空的shared_ptr对象没有控制块&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意，释放资源是通过控制块内的指向对象的指针释放的 （通过管理指针）。而不是通过外层的指向对象的指针释放的。（而不是通过存储指针）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;但是reset会置空整个指针。包括控制块。所以意思是存储指针和管理指针都会被剥离（满足条件时释放资源）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过新建临时对象后调用&lt;code&gt;swap()&lt;/code&gt;实现。&lt;code&gt;shared_ptr().swap(*this);&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt;的默认构造函数是&lt;code&gt;default&lt;/code&gt;的。也就是什么都不做。不给控制块分配空间也不持有任何数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当为函数传递一个新申请的堆内存时，则调用该函数的&lt;code&gt;shared_ptr&lt;/code&gt;对象会获得该存储空间的所有权，并且引用计数的初始值为 1。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;int&amp;gt; p1 = make_shared&amp;lt;int&amp;gt;(5);		//创建指针
p1.reset();										//它将引用计数减少1，同时将当前对象重置为一个空指针。如果引用计数变为0，则释放内存并且删除指针。
p1.reset(new int(34));							//它将原对象引用计数减少1后指向新空间。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：置空包括对控制块置空&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;myobj&amp;gt; shared = make_shared&amp;lt;myobj&amp;gt;(5);
shared_ptr&amp;lt;myobj&amp;gt; shared1(shared);
cout &amp;lt;&amp;lt; shared1.use_count()&amp;lt;&amp;lt;endl; //输出2
cout &amp;lt;&amp;lt; shared.use_count()&amp;lt;&amp;lt;endl;	//输出2
shared1.reset();//把shared1指向对象的引用计数器-1，并且置空shared1指针。包括指向对象的指针和指向控制块的指针
cout &amp;lt;&amp;lt; shared1.use_count()&amp;lt;&amp;lt;endl;	//置空shared1输出0
cout &amp;lt;&amp;lt; shared.use_count()&amp;lt;&amp;lt;endl; 	//输出1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到，把&lt;code&gt;shared1&lt;/code&gt;指向对象的引用计数器-1，并且置空&lt;code&gt;shared1&lt;/code&gt;指针。包括指向对象的指针和指向控制块的指针。所以此时我们打印&lt;code&gt;shared1&lt;/code&gt;的引用计数是0，如果打印&lt;code&gt;shared&lt;/code&gt;的引用计数则是正常的1.&lt;/p&gt;
&lt;h4&gt;unique()&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;判断是否还有其他的共享指针指向当前指针指向的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;h3&gt;通过一个裸指针创建两个&lt;code&gt;std::shared_ptr&lt;/code&gt;，会创建两个控制块，进而导致这个裸指针会被析构两次！&lt;/h3&gt;
&lt;p&gt;所以&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不要直接用裸指针构造&lt;code&gt;std::shared_ptr&lt;/code&gt;，尽量用&lt;code&gt;std::make_shared&lt;/code&gt;直接创建指针。当然在需要自定义的销毁器时不能用&lt;code&gt;std::make_shared&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非要用&lt;code&gt;new&lt;/code&gt;构造&lt;code&gt;std::shared_ptr&lt;/code&gt;的话，尽量直接new，不要传入已有的裸指针变量。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj* ptr1 = new myobj(1); 
shared_ptr&amp;lt;myobj&amp;gt; ptr2(ptr1);	//ptr2有控制块
shared_ptr&amp;lt;myobj&amp;gt; ptr3(ptr1);	//ptr3也有控制块！这样会创建第二个。

//所以尽量直接这样new
shared_ptr&amp;lt;myobj&amp;gt; ptr4(new myobj(1));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;有一种场景下，我们可能无意间创建了对应同一指针的两个控制块。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;std::shared_ptr&amp;lt;Widget&amp;gt;&amp;gt; processedWidgets;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;processedWidgets&lt;/code&gt;表示所有处理过的&lt;code&gt;Widget&lt;/code&gt;。进一步假设&lt;code&gt;Widget&lt;/code&gt;有一个成员函数&lt;code&gt;process&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Widget {
public:
    ...
    void process() {
        ...
        processedWidgets.emplace_back(this); // this is wrong!
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果被调用&lt;code&gt;process&lt;/code&gt;的&lt;code&gt;Widget&lt;/code&gt;对象本身就被&lt;code&gt;std::shared_ptr&lt;/code&gt;所管理，上面那行代码会导致它又创建了一个新的控制块。这种情况下我们应该令&lt;code&gt;Widget&lt;/code&gt;继承自&lt;code&gt;std::enable_shared_from_this&lt;/code&gt;，它允许创建一个指向自身控制块的&lt;code&gt;std::shared_ptr&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Widget: public std::enable_shared_from_this&amp;lt;Widget&amp;gt; {
public:
    ...
    void process() {
        ...
        processedWidgets.emplace_back(shared_from_this());
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过栈对象创建&lt;code&gt;shared_ptr&lt;/code&gt;的时候一定要注意传入自定义删除器（接管资源管理）。因为共享指针在引用计数清零时会去&lt;code&gt;delete&lt;/code&gt;指向的对象。但此时是个指向栈对象的指针。栈对象不能被&lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
    int val;
    myobj(int x):val(x){}
};

void deleter(myobj* ptr){
    cout &amp;lt;&amp;lt;&quot;deleter called&quot; &amp;lt;&amp;lt; endl;
    //这里不能delete了 因为是栈对象。
    ptr = nullptr;
}
int main(){
    myobj s = 100;
    shared_ptr&amp;lt;myobj&amp;gt; ptr1(&amp;amp;s,deleter); //接管资源管理
    cout &amp;lt;&amp;lt; (*ptr1).val &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要将&lt;code&gt;this&lt;/code&gt;指针返回给&lt;code&gt;shared_ptr&lt;/code&gt;&lt;/strong&gt;。当希望将&lt;code&gt;this&lt;/code&gt;指针托管给&lt;code&gt;shared_ptr&lt;/code&gt;时，类需要继承自&lt;code&gt;std::enable_shared_from_this&lt;/code&gt;，并且从&lt;code&gt;shared_from_this()&lt;/code&gt;中获得&lt;code&gt;shared_ptr&lt;/code&gt;指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;并发安全&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;多线程对同一个共享指针“写”是不安全的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当我们在多线程回调中修改shared_ptr指向的时候。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void fn(shared_ptr&amp;lt;A&amp;gt;&amp;amp; sp) {
    ...
    if (..) {
        sp = other_sp;
    } else if (...) {
        sp = other_sp2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;shared_ptr&lt;/code&gt;内数据指针要修改指向，&lt;code&gt;sp&lt;/code&gt;原先指向对象的引用计数的值要减去1，&lt;code&gt;other_sp&lt;/code&gt;指向的引用计数值要加1。&lt;strong&gt;然而这几步操作加起来并不是一个原子操作&lt;/strong&gt;，如果多线程都在修改&lt;code&gt;sp&lt;/code&gt;的指向的时候，那么有可能会出问题。比如在导致计数在操作减一的时候，其内部的指向，已经被其他线程修改过了。引用计数的异常会导致某个管理的对象被提前析构，后续在使用到该数据的时候触发core dump。&lt;/p&gt;
&lt;p&gt;当然如果你没有修改指向的时候，是没有问题的。&lt;/p&gt;
&lt;p&gt;https://juejin.cn/post/7038581008945872927&lt;/p&gt;
&lt;h2&gt;shared_ptr做为函数入参是用值传递还是引用传递？ -- 最好使用值传递。但是要具体情况具体分析。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先，对&lt;code&gt;shared_ptr&lt;/code&gt;进行拷贝并不耗费特别多的资源。&lt;/li&gt;
&lt;li&gt;其次，使用拷贝正是保证了在&lt;code&gt;shared_ptr&lt;/code&gt;对象的作用域中，它一定能指向一个有效的内存对象这一假设。&lt;/li&gt;
&lt;li&gt;假设：**如果我们使用引用传递了这个智能指针，我们在作用域内正在使用这个智能指针的过程中，由于引用计数没有增加，那么可能导致在多线程中，外部的这个资源已经被释放了。**因为可能外部的剩下的指向这个资源的智能指针已经超出了生命周期。&lt;/li&gt;
&lt;li&gt;但是，如果可以确定不会有释放问题，则可以用引用传递稍微的减少开销。但是意义小。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;所以，传递shared_ptr参数在一般情况下还是用传值更好！但是依旧要具体情况具体分析。&lt;/h3&gt;
&lt;h2&gt;函数返回值为shared_ptr解析&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;int&amp;gt; func1(){
    return make_shared&amp;lt;int&amp;gt;(10);
}


int main(){

    shared_ptr&amp;lt;int&amp;gt; ptr = func1();
    cout &amp;lt;&amp;lt; ptr.use_count() &amp;lt;&amp;lt; endl; //返回1
    return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么引用计数是1？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为求值运算符从右向左计算，所以首先调用函数，函数返回一个临时的智能指针。所以首先我们&lt;code&gt;func1&lt;/code&gt;函数内&lt;code&gt;make_shared&lt;/code&gt;生成（构造，调用构造）一个对象，此时引用计数为1。&lt;/li&gt;
&lt;li&gt;然后&lt;code&gt;return&lt;/code&gt;产生（拷贝初始化，调用拷贝构造）了一个临时对象，这时候引用计数为2&lt;/li&gt;
&lt;li&gt;然后离开&lt;code&gt;func1&lt;/code&gt;，对象被析构（调用析构），引用计数-1，此时为1。&lt;/li&gt;
&lt;li&gt;然后回到&lt;code&gt;main&lt;/code&gt;，拷贝构造（拷贝初始化，调用拷贝构造）给&lt;code&gt;ptr1&lt;/code&gt;。引用计数+1，此时为2。同时，因为函数的返回值是临时对象，所以函数返回后会被销毁（调用析构）。引用计数-1，现在依旧是1。+1和-1其实是几乎同步发生的。因为是拷贝到栈帧后直接释放临时对象栈帧。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;可能的实现&lt;/h2&gt;
&lt;p&gt;查看&lt;code&gt;sptr.cpp&lt;/code&gt;。简陋版本。&lt;/p&gt;
&lt;h2&gt;enable_shared_from_this&lt;/h2&gt;
&lt;p&gt;查看模板的21.2&lt;/p&gt;
&lt;p&gt;注意使用问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class st:public enable_shared_from_this&amp;lt;st&amp;gt;{
    public:
    int val = 100;
    shared_ptr&amp;lt;st&amp;gt; getptr(){
        return shared_from_this();
    }
};


int main(){
	st obj;//错误
    shared_ptr&amp;lt;st&amp;gt; obj(new st);//正确
    shared_ptr&amp;lt;st&amp;gt; t = obj-&amp;gt;getptr(); //别搞混了。访问储存的对象的成员函数用-&amp;gt;
    cout &amp;lt;&amp;lt; t-&amp;gt;val &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; obj.use_count()&amp;lt;&amp;lt;endl;//访问shared_ptr本身的成员函数用.
    cout &amp;lt;&amp;lt; t.use_count()&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_from_this&lt;/code&gt;只能用在已经被托管在&lt;code&gt;shared_ptr&lt;/code&gt;的对象上。具体原理在分析部分有。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能在构造自身的时候或构造函数中使用&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;核心原因：我们是继承自&lt;code&gt;enable_shared_from_this&lt;/code&gt;。它先初始化，就算它初始化的时候储存的&lt;code&gt;weak_ptr&lt;/code&gt;是延迟实例化，但是在我们自己的类被完全构造前，我们是不能构建&lt;code&gt;shared_ptr&lt;/code&gt;指向自己的。因为构造函数未完成就是对象构建未完成。我们也不能使用&lt;code&gt;shared_ptr&lt;/code&gt;储存一个不完整对象类型本身。因为此时&lt;code&gt;this&lt;/code&gt;是不完整的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;只容许在先前共享的对象，即 &lt;a href=&quot;http://zh.cppreference.com/w/cpp/memory/shared_ptr&quot;&gt;std::shared_ptr&lt;/a&gt; 所管理的对象上调用 &lt;code&gt;shared_from_this&lt;/code&gt; 。（特别是不能在构造 *this 期间 &lt;code&gt;shared_from_this&lt;/code&gt; 。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/HglJp8cOOKY?si=Or59cZHBc4ukaD8R&amp;amp;t=594&quot;&gt;视频资料:Traps with Smart Pointers&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;http://hahaya.github.io/use-enable-shared-from-this&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/gong_xucheng/article/details/26839069&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/fortunely/p/16370208.html&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/31924396/why-shared-from-this-cant-be-used-in-constructor-from-technical-standpoint&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;weak_ptr&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;有时候我们需要一种类似&lt;code&gt;std::shared_ptr&lt;/code&gt;，但又不参与这个共享对象的所有权的智能指针。这样它就需要能知道共享对象是否已经销毁了。这就是&lt;code&gt;std::weak_ptr&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::weak_ptr&lt;/code&gt;不是单独存在的，它不能解引用，也不能检测是否为空，它就是配合&lt;code&gt;std::shared_ptr&lt;/code&gt;使用的&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;std::weak_ptr&lt;/code&gt;没有解引用和访问成员的功能。也就是不可以操作数据。而且不提供&lt;code&gt;get()&lt;/code&gt;函数来获得裸指针。它只获得资源的观测权，不共享资源，它的构造不会引起指针强引用计数的增加。同样，在&lt;code&gt;weak_ptr&lt;/code&gt;析构时也不会导致强引用计数的减少&lt;/p&gt;
&lt;p&gt;一般来说，&lt;code&gt;weak_ptr&lt;/code&gt;需要通过&lt;code&gt;shared_ptr&lt;/code&gt;来创建。这样这两个指针会指向同一个数据。但是&lt;code&gt;weak_ptr&lt;/code&gt;不会涉及到&lt;code&gt;shared_ptr&lt;/code&gt;的强引用计数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用&lt;code&gt;shared_ptr&lt;/code&gt;创建了一个&lt;code&gt;weak_ptr&lt;/code&gt;后，计数器内的弱引用计数器会+1， &lt;code&gt;weak_ptr&lt;/code&gt;析构后，弱引用计数器会-1&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;创建指针&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;weak_ptr&amp;lt;myobj&amp;gt; ptr2(ptr1);                 //可以 直接初始化
weak_ptr&amp;lt;myobj&amp;gt; ptr3 = ptr1;                //可以 隐式隐式转换后使用拷贝初始化。
weak_ptr&amp;lt;myobj&amp;gt; ptr4(ptr1);                 //可以 弱指针初始化弱指针
ptr4 = ptr2;                                //可以 拷贝赋值
weak_ptr&amp;lt;myobj&amp;gt; ptr5(new myobj(7));         //不可以使用裸指针初始化弱指针。   

unique_ptr&amp;lt;myobj&amp;gt; ptr7(new myobj(5));
weak_ptr&amp;lt;myobj&amp;gt; test1(ptr7);                //不可以使用unique指针初始化弱指针
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可以使显式使用构造函数通过&lt;code&gt;weak_ptr&lt;/code&gt;对象构建&lt;code&gt;shared_ptr&lt;/code&gt;但是非常不推荐&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;int&amp;gt; sptr(wkptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;reset(), lock(), use_count(), expired()&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;use_count()&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;use_count()&lt;/code&gt;返回对应弱指针绑定的共享指针的&lt;strong&gt;强引用计数&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;myobj&amp;gt; ptr1(new myobj(5));       //创建共享指针    
shared_ptr&amp;lt;myobj&amp;gt; ptr2(ptr1);   			//引用计数+1
weak_ptr&amp;lt;myobj&amp;gt; ptr3(ptr1);                 //创建弱指针
cout &amp;lt;&amp;lt; ptr2.use_count() &amp;lt;&amp;lt; endl;			//输出2
cout &amp;lt;&amp;lt; ptr3.use_count() &amp;lt;&amp;lt; endl;			//输出2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;既然&lt;code&gt;weak_ptr&lt;/code&gt;并不改变其所共享的&lt;code&gt;shared_ptr&lt;/code&gt;实例的引用计数，那就可能存在&lt;code&gt;weak_ptr&lt;/code&gt;指向的对象被释放掉这种情况。这时，就不能使用&lt;code&gt;weak_ptr&lt;/code&gt;直接访问对象。那么如何判断&lt;code&gt;weak_ptr&lt;/code&gt;指向对象是否存在呢？使用&lt;code&gt;lock()&lt;/code&gt; 或 &lt;code&gt;expired()&lt;/code&gt;来进行判断。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;lock()&lt;/code&gt; -- &lt;strong&gt;原子地&lt;/strong&gt;检测对象是否有效并返回一个共享指针。&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;它能&lt;strong&gt;原子地&lt;/strong&gt;检测对象是否有效。如果对象存在，&lt;code&gt;lock()&lt;/code&gt;函数返回一个指向共享对象的&lt;code&gt;shared_ptr&lt;/code&gt;(引用计数会增1)，否则返回一个空&lt;code&gt;shared_ptr&lt;/code&gt;。所以&lt;code&gt;lock()&lt;/code&gt;并不是简单的判断。它是通过查看控制块的强引用计数判断的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;code&gt;expired()&lt;/code&gt; -- 检测所指向的对象是否被销毁&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;expired()&lt;/code&gt;函数用来判断所指对象是否已经被销毁。如果&lt;code&gt;use_count()&lt;/code&gt;是0，则为&lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;reset()&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;将对应的弱指针置空。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;使用&lt;code&gt;lock()&lt;/code&gt;和显式使用构造函数通过&lt;code&gt;weak_ptr&lt;/code&gt;对象构建&lt;code&gt;shared_ptr&lt;/code&gt;的区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;lock()&lt;/code&gt;的时候，如果其资源已被释放（强引用计数已为0），则会构造一个空的&lt;code&gt;shared_ptr&lt;/code&gt;并返回。&lt;/li&gt;
&lt;li&gt;如果显式使用构造函数，如果其资源已被释放（强引用计数已为0），则此构造函数会抛出异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;几种指针的大小&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unique_ptr&lt;/code&gt;的大小是一个指针或两个指针
&lt;ul&gt;
&lt;li&gt;一个指针的时候是不使用自定义删除器 或 使用了仿函数作为自定义删除器&lt;/li&gt;
&lt;li&gt;两个指针大小的时候是使用了函数指针做为自定义删除器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt;的大小是两个指针。第一个指针指向的是对象，第二个指针指向的是控制块&lt;/li&gt;
&lt;li&gt;&lt;code&gt;weak_ptr&lt;/code&gt;的大小是两个指针。**推测原因：**第一个指针指向的是对象，第二个指针指向的是控制块。因为&lt;code&gt;weak_ptr&lt;/code&gt;和&lt;code&gt;shared_ptr&lt;/code&gt;，继承自同一个基类 &lt;code&gt;_Ptr_base&lt;/code&gt;。这个基类有两个变量。一个是对象类型的指针&lt;code&gt;element_type* _Ptr&lt;/code&gt;, 一个是指向控制块的指针&lt;code&gt;_Ref_count_base* _Rep&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;code&gt;weak_ptr&lt;/code&gt;和&lt;code&gt;shared_ptr&lt;/code&gt;共享控制块&lt;/h1&gt;
&lt;p&gt;直到释放为止（手动或超出作用域），无论对应的共享指针和其指向的资源是否存在，弱指针都一直存在。只要弱指针还在，共享指针的控制块的弱引用计数就会在。控制块就不会被释放。所以这会产生下面的一个make方法的缺点。&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;unique_ptr&lt;/code&gt;和&lt;code&gt;shared_ptr&lt;/code&gt;之间的转换&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 不可以转换为 &lt;code&gt;unique_ptr&lt;/code&gt;
&lt;ol&gt;
&lt;li&gt;原因非常简单。转换的时候不能保证&lt;code&gt;shared_ptr&lt;/code&gt;的对象现在只被一个指针拥有。而&lt;code&gt;unique_ptr&lt;/code&gt;必须保证独占所有权。这种由宽到窄的所有权语义转换是说不过去的。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unique_ptr&lt;/code&gt;可以转换为 &lt;code&gt;shared_ptr&lt;/code&gt;。 使用&lt;code&gt;move&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    unique_ptr&amp;lt;int&amp;gt; myUnique = make_unique&amp;lt;int&amp;gt;(10);
    //shared_ptr&amp;lt;int&amp;gt; myShared = move(myUnique); 也可以
    shared_ptr&amp;lt;int&amp;gt; myShared(move(myUnique));
    if(myUnique.get() == nullptr){
        cout &amp;lt;&amp;lt;&quot;unique释放&quot; &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt; myShared.use_count() &amp;lt;&amp;lt; endl;
    return 0;

}
/*输出 
unique释放 
1
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;unique_ptr&lt;/code&gt;&lt;strong&gt;也可以隐式转换为&lt;/strong&gt; &lt;code&gt;shared_ptr&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;int&amp;gt; sptr = make_unique&amp;lt;int&amp;gt;(5);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;code&gt;shared_ptr&lt;/code&gt;指针的循环引用问题&lt;/h1&gt;
&lt;p&gt;如下面代码所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class B;

class A{
    public:
        shared_ptr&amp;lt;B&amp;gt; bptr;
        ~A(){
            cout &amp;lt;&amp;lt; &quot;A destory&quot; &amp;lt;&amp;lt; endl;
            cout &amp;lt;&amp;lt; bptr.use_count() &amp;lt;&amp;lt; endl;
        }
};

class B{
    public:
        //weak_ptr&amp;lt;A&amp;gt; aptr;
    	shared_ptr&amp;lt;A&amp;gt; aptr;
        ~B(){
            cout &amp;lt;&amp;lt; &quot;B destory&quot; &amp;lt;&amp;lt; endl;
            cout &amp;lt;&amp;lt; aptr.use_count() &amp;lt;&amp;lt; endl;
        }
};



int main(){
    {
        shared_ptr&amp;lt;A&amp;gt; classA(new A());
        shared_ptr&amp;lt;B&amp;gt; classB(new B());
        cout &amp;lt;&amp;lt; classA.use_count() &amp;lt;&amp;lt; endl;         //1
        cout &amp;lt;&amp;lt; classB.use_count() &amp;lt;&amp;lt; endl;         //1
        classA-&amp;gt;bptr = classB;
        cout &amp;lt;&amp;lt; classA.use_count() &amp;lt;&amp;lt; endl;         //1
        cout &amp;lt;&amp;lt; classB.use_count() &amp;lt;&amp;lt; endl;         //2
        classB-&amp;gt;aptr = classA;
        cout &amp;lt;&amp;lt; classA.use_count() &amp;lt;&amp;lt; endl;         //2	//换成弱指针之后这里为1。因为弱指针不计数
        cout &amp;lt;&amp;lt; classB.use_count() &amp;lt;&amp;lt; endl;         //2
        //所有的析构都不会执行。
       	//换成弱指针，所有析构正常执行。
    }
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们在两个类中互相包含一个指向对方类的&lt;code&gt;shared_ptr&lt;/code&gt;的时候，会发生互相引用。这导致了每一个对象的引用计数都会永远至少保持为1。可以理解为两个指针互相都想让对方放开，但是两个人都必须等对方放开了自己才能放开对方。这样就发生了内存泄漏。&lt;/p&gt;
&lt;p&gt;解决方案就是把其中一个类的共享指针换成弱指针。因为弱指针的引用不会增加计数。所以那个类的引用计数会到0从而正确释放。&lt;/p&gt;
&lt;p&gt;如上面代码，把B的共享指针换成弱指针即可。&lt;/p&gt;
&lt;h1&gt;More Effective C++ 条款21&lt;/h1&gt;
&lt;p&gt;先做一下介绍，&lt;code&gt;std::make_shared&lt;/code&gt;是在C++11中增加的，但&lt;code&gt;std::make_unique&lt;/code&gt;却是在C++14中增加的。如果你想在C++11中就用上&lt;code&gt;std::make_unique&lt;/code&gt;，自己写一个简单版的也不难：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, typename... Ts&amp;gt;
std::unique_ptr&amp;lt;T&amp;gt; make_unique(Ts&amp;amp;&amp;amp;... params) {
    return std::unique_ptr&amp;lt;T&amp;gt;(new T(std::forward&amp;lt;Ts&amp;gt;(params)...));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个版本不支持数组，不支持自定义的销毁器，但这些都不重要，它足够用了。但要记住的是，不要把它放到&lt;code&gt;namespace std&lt;/code&gt;下面。&lt;/p&gt;
&lt;p&gt;这两个make函数的功能就不解释了，和它们类似的还有一个&lt;code&gt;std::allocate_shared&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;make方法的优点&lt;/h2&gt;
&lt;h4&gt;make方法是把传入参数完美转发至对象构造函数。只需要传递参数即可，无需再次new。&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;第一个好处：不需要重复写一遍类型。所有程序员都知道：不要重复代码。代码越少，bug越少。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;auto upw1(std::make_unique&amp;lt;Widget&amp;gt;());
std::unique_ptr&amp;lt;Widget&amp;gt; upw2(new Widget);

auto spw1(std::make_shared&amp;lt;Widget&amp;gt;());
std::shared_ptr&amp;lt;Widget&amp;gt; spw2(new Widget);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第二个好处：异常安全性。想象我们有两个函数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void processWidget(std::shared_ptr&amp;lt;Widget&amp;gt; spw, int priority);
int computePriority();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用代码很可能长成这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;processWidget(std::shared_ptr&amp;lt;Widget&amp;gt;(new Widget), computePriority()); // potential resource leak!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这行代码有内存泄漏的风险，为什么？根据C++标准，在&lt;code&gt;processWidget&lt;/code&gt;的参数求值过程中，我们只能确定下面几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new Widget&lt;/code&gt;一定会执行，即一定会有一个&lt;code&gt;Widget&lt;/code&gt;对象在堆上被创建。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt;&lt;/code&gt;的构造函数一定会执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;computePriority&lt;/code&gt;一定会执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;new Widget&lt;/code&gt;的结果是&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt;&lt;/code&gt;构造函数的参数，因此前者一定早于后者执行。除此之外，编译器不保证其它操作的顺序，即有可能执行顺序为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;new Widget&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行&lt;code&gt;computePriority&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;构造&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果第2步抛异常，第1步创建的对象还没有被&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt;&lt;/code&gt;管理，就会发生内存泄漏。&lt;/p&gt;
&lt;p&gt;如果这里我们用&lt;code&gt;std::make_shared&lt;/code&gt;，就能保证&lt;code&gt;new Widget&lt;/code&gt;和&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt;&lt;/code&gt;是一起完成的，中间不会有其它操作插进来，即不会有不受智能指针保护的裸指针出现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;processWidget(std::make_shared&amp;lt;Widget&amp;gt;(), computePriority()); // no potential resource leak
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第三个好处：更高效。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std:shared_ptr&amp;lt;Widget&amp;gt; spw(new Widget);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码中，我们以为只有一次内存分配，实际发生了两次，第二次是在分配&lt;code&gt;std::shared_ptr&lt;/code&gt;控制块。&lt;strong&gt;我们前文提到过，共享指针的控制块是单独建立在堆上的。所以如果使用直接&lt;code&gt;new&lt;/code&gt;的方式，在分配完我们想要分配的变量后，系统会再次分配一块空间给控制块。所以分配了两次内存。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是如果用&lt;code&gt;std::make_shared&lt;/code&gt;，它会把&lt;code&gt;Widget&lt;/code&gt;对象和控制块合并为一次内存分配。&lt;strong&gt;但是这也会成为缺点&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;make方法的缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个缺点：无法传入自定义删除器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp, typename... _Args&amp;gt;
    inline typename _MakeUniq&amp;lt;_Tp&amp;gt;::__single_object
    make_unique(_Args&amp;amp;&amp;amp;... __args){ 
        return unique_ptr&amp;lt;_Tp&amp;gt;(new _Tp(std::forward&amp;lt;_Args&amp;gt;(__args)...)); 
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;这里返回的是默认删除器类型的&lt;code&gt;unique_ptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;来自&lt;a href=&quot;https://www.misteo.top/2020/11/30/std-unique-ptr%E4%BD%BF%E7%94%A8incomplete-type%E7%9A%84%E6%8A%A5%E9%94%99%E5%88%86%E6%9E%90%E5%92%8C%E8%A7%A3%E5%86%B3/&quot;&gt;这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二个缺点：无法使用列表初始化。必须用&lt;code&gt;auto&lt;/code&gt;先推导出来一个&lt;code&gt;initializer_list&lt;/code&gt;然后把初始化列表传入。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三个缺点：对象和控制块分配在一块内存上，减少了内存分配的次数，但也导致对象和控制块占用的内存也要一次回收掉&lt;/strong&gt;。即，如果还有&lt;code&gt;std::weak_ptr&lt;/code&gt;存在，控制块就要在（因为控制块中有一块弱引用，储存着弱指针的引用），对象占用的内存也没办法回收。如果对象比较大，且&lt;code&gt;std::weak_ptr&lt;/code&gt;在对象析构后还可能长期存在，那么这种开销是不可忽视的。也就是假如&lt;code&gt;shared_ptr&lt;/code&gt;本身被析构了（因为自己和控制块是分开的），&lt;code&gt;weak_ptr&lt;/code&gt;可能过了很久才被析构。这样这一大块内存就都要存在，因为用&lt;code&gt;make&lt;/code&gt;方法分配的是一整块&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们因为前面这三个缺点而不能使用&lt;code&gt;std::make_shared&lt;/code&gt;，那么我们要保证，智能指针的构造一定要单独一个语句。回到之前&lt;code&gt;processWidget&lt;/code&gt;的例子中，假设我们有个自定义的销毁器&lt;code&gt;void cusDel(Widget* ptr);&lt;/code&gt;，因此不能使用&lt;code&gt;std::make_shared&lt;/code&gt;，那么我们要这么写来保证异常安全性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt; spw(new Widget, cusDel);
processWidget(spw, computePriority());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这么写还不够高效，这里我们明确知道&lt;code&gt;spw&lt;/code&gt;就是给&lt;code&gt;processWidget&lt;/code&gt;用的，那么可以使用&lt;code&gt;std::move&lt;/code&gt;，将其转为右值，来避免对引用计数的修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt; spw(new Widget, cusDel);
processWidget(std::move(spw), computePriority());
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;不适于使用 &lt;code&gt;make&lt;/code&gt; 系列函数的场景包括
&lt;ul&gt;
&lt;li&gt;需要定制删除器，&lt;/li&gt;
&lt;li&gt;期望直接传递大括号初始化物&lt;/li&gt;
&lt;li&gt;需要自定义内存管理（自定义删除器）&lt;/li&gt;
&lt;li&gt;内存紧张的系统&lt;/li&gt;
&lt;li&gt;非常大的对象&lt;/li&gt;
&lt;li&gt;存在比指涉到相同对象的 &lt;code&gt;std: :shared_ptr&lt;/code&gt; 生存期史久的 &lt;code&gt;std: :weak_ptr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;稍微具体说一下自定义删除器为什么用类的时候不增加大小，用函数指针的时候会增加大小&lt;/h1&gt;
&lt;p&gt;先看一下&lt;code&gt;unique_ptr&lt;/code&gt;的析构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_Compressed_pair&amp;lt;_Dx, pointer&amp;gt; _Mypair; 		//mypair定义。_Dx是自定义删除器类型
~unique_ptr() noexcept {
    if (_Mypair._Myval2) {
        _Mypair._Get_first()(_Mypair._Myval2); // call deleter
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用&lt;code&gt;_Get_first&lt;/code&gt;把&lt;code&gt;_Mypair&lt;/code&gt;的第一个数据也就是自定义删除器提取出来，然后用&lt;code&gt;（）&lt;/code&gt;创建临时对象，然后把数据塞入这个临时对象的&lt;code&gt;operator()&lt;/code&gt;里面完成资源释放。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是这个_Compressed_pair&lt;/strong&gt;有两个特化版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// store a pair of values, deriving from empty first
template &amp;lt;class _Ty1, class _Ty2, bool = is_empty_v&amp;lt;_Ty1&amp;gt; &amp;amp;&amp;amp; 
                                         !is_final_v&amp;lt;_Ty1&amp;gt;&amp;gt;
class _Compressed_pair final : private _Ty1 {
public:
    _Ty2 _Myval2;
    
    // ... the rest of impl
}

// store a pair of values, not deriving from first
template &amp;lt;class _Ty1, class _Ty2&amp;gt;
class _Compressed_pair&amp;lt;_Ty1, _Ty2, false&amp;gt; final {
public:
    _Ty1 _Myval1;
    _Ty2 _Myval2;
    
    // ... the rest of impl
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看第二个版本，这个很直观，基本就是普通的 &lt;code&gt;std::pair&lt;/code&gt; 的定义。&lt;/p&gt;
&lt;p&gt;而当 &lt;code&gt;_Ty1&lt;/code&gt; (自定义删除器)是一个空类时，则会特化为第一个版本。这里 &lt;code&gt;_Ty2&lt;/code&gt; 依然作为一个普通的成员，但 &lt;code&gt;_Ty1&lt;/code&gt; 却通过继承的方式内嵌到 &lt;code&gt;_Compressed_pair&lt;/code&gt; 中。乍一看这好像有点不伦不类，毕竟从概念上来说 &lt;code&gt;_Compressed_pair&lt;/code&gt; 和 &lt;code&gt;_Ty1&lt;/code&gt; 似乎不应该是继承的关系。但注意这里用的是 private 继承，相较于 public 继承表达的 &lt;code&gt;is-a&lt;/code&gt; 关系，private 继承隐含的意思其实是 &lt;code&gt;is-implemented-in-terms-of&lt;/code&gt;，即「由…实现出」。这就说得通了，&lt;code&gt;_Ty1&lt;/code&gt; 是组成 &lt;code&gt;_Compressed_pair&lt;/code&gt; 的一部分，反过来 &lt;code&gt;_Compressed_pair&lt;/code&gt; 是由 &lt;code&gt;_Ty1&lt;/code&gt; 实现的。这也是为什么很多情况下，组合和 private 继承这两种设计可以互换的原因，详细内容可以参阅 《Effective C++》Item 38。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;空类继承不会增加体积。因为有空基类优化。所以传入一个仿函数（无成员变量，所以是空类）的时候会匹配到第一个模板，使用空基类优化使得不需要额外空间。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以如果自定义删除器传入一个函数指针，因为不是空类而是一个函数指针，会被匹配到第二个模板。所以会把函数指针在&lt;code&gt;_Ty1 _Myval1;&lt;/code&gt;这里存下来。就多了一个指针。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么删除器是unique_ptr的模板参数之一(是类型的一部分), 而shared_ptr不是？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T, class D = default_delete&amp;lt;T&amp;gt;&amp;gt;
class unique_ptr {
public:
    ...
    unique_ptr (pointer p,
        typename conditional&amp;lt;is_reference&amp;lt;D&amp;gt;::value,D,const D&amp;amp;&amp;gt; del) noexcept;
    ...
};

template &amp;lt;class T&amp;gt; 
class shared_ptr {
public:
    ...
    template &amp;lt;class U, class D&amp;gt; 
    shared_ptr (U* p, D del); //删除器只是构造函数参数的一部分，而不是shared_ptr类型的一部分
    ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，为了尽可能让&lt;code&gt;unique_ptr&lt;/code&gt;的性能贴近裸指针，比如在删除器为非函数指针的时候，可以触发空基类优化，使得无需额外空间。其次，主要的一点是&lt;code&gt;unique_ptr&lt;/code&gt;的删除器是编译时绑定的，所以我们必须显式指定其类型。而&lt;code&gt;shared_ptr&lt;/code&gt;的删除器是运行期绑定的，所以不需要。正因为共享指针的删除器是运行时绑定，则必须要有一个额外的指针（控制块）指向（保存）删除器来让共享指针可以访问。因为删除器的类型是运行时绑定所以直到运行的时候才能知道。所以调用删除器的时候会有一次额外的寻址操作。&lt;/p&gt;
&lt;p&gt;另外，正因为如此，&lt;code&gt;unique_ptr&lt;/code&gt;的删除器，在使用&lt;code&gt;reset&lt;/code&gt;更换的时候只可以更换同类型的删除器。而&lt;code&gt;shared_ptr&lt;/code&gt;的删除器可以在&lt;code&gt;reset&lt;/code&gt;的时候随意更换。因为类成员类型在运行期间是不可以改变的。而删除器的类型不是共享指针的一部分。&lt;/p&gt;
&lt;p&gt;（此部分参照了c++ primer P.599)&lt;/p&gt;
&lt;p&gt;https://fuzhe1989.github.io/2017/05/19/cpp-different-role-of-deleter-in-unique-ptr-and-shared-ptr/&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/24365250/why-does-stdunique-ptr-need-to-be-specialized-for-dynamic-arrays&lt;/p&gt;
&lt;h1&gt;lambda和智能指针之间的联系 （小米面经）&lt;/h1&gt;
&lt;h2&gt;用lambda捕获uniquePtr应该怎么做 -- 必须按照引用捕获&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;lambda我们知道，如果按值捕获，他会对捕获的对象进行一个拷贝。因为可以把lambda对象理解为一个匿名类，那么捕获的变量就是这个匿名类的成员变量。这个匿名类重载了&lt;code&gt;operator()&lt;/code&gt;并且设置其为&lt;code&gt;const&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是，unique_ptr是所有权语义。它不允许被拷贝。所以必须按照引用捕获。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myobj{
    myobj(int x):val(x){}
    int val;
};


int main(){
    
    unique_ptr&amp;lt;myobj&amp;gt; unique = make_unique&amp;lt;myobj&amp;gt;(5);
    auto lambda = [=](int a){ unique-&amp;gt;val = unique-&amp;gt;val + a;}; //报错，因为unique_ptr不允许拷贝。
    auto lambda = [&amp;amp;](int a){ unique-&amp;gt;val = unique-&amp;gt;val + a;};
    lambda(10);
    cout &amp;lt;&amp;lt; unique-&amp;gt;val&amp;lt;&amp;lt; endl; //输出15

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样做也不好，会有语义问题。参考：https://zhuanlan.zhihu.com/p/101938827 和 下面的问题&lt;/p&gt;
&lt;h2&gt;lambda捕获uniquePtr后如何避免调用lambda时uniquePtr的指针已失效 -- 使用c++14的移动捕获（通用捕获）[使用带有初始化器的捕获符捕获仅可移动的类型。]&lt;/h2&gt;
&lt;p&gt;也是为了解决上面的语义问题，我们应该把unique_ptr移入到lambda， 如果想的话也可以再移回原指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myobj{
    myobj(int x):val(x){}
    int val;
};


int main(){
    
    unique_ptr&amp;lt;myobj&amp;gt; unique = make_unique&amp;lt;myobj&amp;gt;(5);
    auto lambda = [&amp;amp;, temp_unique = move(unique)](int a) mutable{  //注释1
        temp_unique-&amp;gt;val = temp_unique-&amp;gt;val + a;
        unique = move(temp_unique); //注释2
        };
    lambda(10);
    cout &amp;lt;&amp;lt; unique-&amp;gt;val&amp;lt;&amp;lt; endl;
    return 0;
}
//上面这个例子并不太好。因为我们做的事情是：引用捕获unique，然后把unique移动到一个新建的lambda内局部变量temp上。
//然后对temp进行处理，然后再移动到unique上。这也是为什么必须有一个&amp;amp;来引用捕获unique。这完全脱裤子放屁
//所以正常这种情况下的使用方式应该是移动进lambda后，外部不会再使用该智能指针
int main(){
    
    unique_ptr&amp;lt;myobj&amp;gt; unique = make_unique&amp;lt;myobj&amp;gt;(5);
    auto lambda = [temp_unique = move(unique)](int a) mutable{  //移动进来
        temp_unique-&amp;gt;val = temp_unique-&amp;gt;val + a;
        cout &amp;lt;&amp;lt; temp_unique-&amp;gt;val &amp;lt;&amp;lt; endl; //对数据处理。
        };
    lambda(10);
    //外部并不会再次使用unique指针。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注释1
&lt;ul&gt;
&lt;li&gt;这里有一个看起来很奇怪的东西。&lt;code&gt;temp_unique = move(unique)&lt;/code&gt;。这个东西叫做带有初始化器的捕获符（列表）[来自校友赖斯理大佬的博客https://lesleylai.info/zh/c++-lambda/#fn-3]&lt;/li&gt;
&lt;li&gt;等号左侧是表达式中数据成员的名称，这个是由你自由决定的。等号右侧是初始化表达式，用于初始化数据成员。&lt;strong&gt;等号左右两侧的作用域是不同的，左侧的作用域在lambda表达式中，而右侧的作用域在表达式定义的区域&lt;/strong&gt;。在上面的例子中，等号左侧的&lt;code&gt;temp_unique &lt;/code&gt;指的是lambda中的数据成员，而右侧则是在lambda表达式上方定义的&lt;code&gt;unique&lt;/code&gt;。因此&lt;code&gt;temp_unique = move(unique)&lt;/code&gt;意味着在lambda中创建一个数据成员&lt;code&gt;temp_unique&lt;/code&gt;，并且用&lt;code&gt;std::move&lt;/code&gt;一个本地变量的&lt;code&gt;unique&lt;/code&gt;以初始化这个数据成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注释2
&lt;ul&gt;
&lt;li&gt;这里不可以写成&lt;code&gt;unique(move(temp_unique));&lt;/code&gt;因为这个叫函数调用。调用了一个叫&lt;code&gt;unique&lt;/code&gt;的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;用lambda捕获sharedPtr要怎么做 -- 值捕获，避免调用时sharedPtr对象已销毁&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;创建shared_ptr的引用不会改变其强引用计数器或弱引用计数器。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;myobj&amp;gt; shared = make_shared&amp;lt;myobj&amp;gt;(5);
shared_ptr&amp;lt;myobj&amp;gt; shared2 = shared;
cout &amp;lt;&amp;lt; shared2.use_count() &amp;lt;&amp;lt; endl; //输出2
shared_ptr&amp;lt;myobj&amp;gt;&amp;amp; sharedref = shared; //创建引用
cout &amp;lt;&amp;lt; shared2.use_count() &amp;lt;&amp;lt; endl; //依旧输出2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以假如我们按照引用捕获了（再次强调，引用捕获相当于在匿名类内部创建了一个入参的引用类型的类成员变量），我们也知道了创建shared_ptr的引用不会改变其强引用计数器或弱引用计数器。所以会导致计数器并不会增加。这样，有可能外部该&lt;code&gt;shared_ptr&lt;/code&gt;的计数器已经清零并且析构了，我们内部才刚要进行调用。&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    shared_ptr&amp;lt;myobj&amp;gt; shared = make_shared&amp;lt;myobj&amp;gt;(5);
    auto lambda = [&amp;amp;](int b){ //错误地按照引用捕获
        cout &amp;lt;&amp;lt; shared-&amp;gt;val + b &amp;lt;&amp;lt; endl;
        
    };
    shared.reset(); //计数器-1，如果为0就释放。此时已经释放了
    lambda(10); //崩溃
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    shared_ptr&amp;lt;myobj&amp;gt; shared = make_shared&amp;lt;myobj&amp;gt;(5);
    auto lambda = [=](int b){
        cout &amp;lt;&amp;lt; shared-&amp;gt;val + b &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt;&quot;in lambda:&quot; &amp;lt;&amp;lt;shared.use_count() &amp;lt;&amp;lt; endl; //输出1
    };
    cout &amp;lt;&amp;lt;&quot;out lambda:&quot; &amp;lt;&amp;lt;shared.use_count() &amp;lt;&amp;lt; endl; // 输出2
    shared.reset(); //reset的是全局作用域的shared。
    cout &amp;lt;&amp;lt;&quot;out lambda:&quot; &amp;lt;&amp;lt;shared.use_count() &amp;lt;&amp;lt; endl; //输出0 不要惊讶
    lambda(10);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要惊讶为什么输出0后lambda内部输出1。我们解释一下&lt;/p&gt;
&lt;p&gt;首先我们按值捕获一个&lt;code&gt;shared_ptr&lt;/code&gt;，相当于两个指针共享一份数据和一个数据块。&lt;code&gt;reset&lt;/code&gt;全局作用域的&lt;code&gt;shared&lt;/code&gt;后，我们提到过，这个&lt;code&gt;reset&lt;/code&gt;不仅会给引用计数-1，而且会把自己置空，也就是自己的指向控制器的指针置空，自然就是0。详细的看一下上面&lt;code&gt;reset&lt;/code&gt;使用部分。&lt;/p&gt;
&lt;p&gt;但是我们&lt;code&gt;lambda&lt;/code&gt;作用域内的&lt;code&gt;shared&lt;/code&gt;依旧存在，所以它的引用计数器是正常的1。&lt;/p&gt;
&lt;h2&gt;捕获sharedPtr时如果不想延长对象生命周期怎么做（先赋值给weakPtr,再捕获）&lt;/h2&gt;
&lt;p&gt;这问题好没劲。&lt;/p&gt;
&lt;p&gt;就是假设有一个情况，我们这个lambda挺无关紧要的，我不希望在外面我想释放&lt;code&gt;shared_ptr&lt;/code&gt;的时候还要等lambda里面的&lt;code&gt;shared_ptr&lt;/code&gt;释放后再释放。我们就可以新建一个&lt;code&gt;weak_ptr&lt;/code&gt;, 然后把这个&lt;code&gt;weak_ptr&lt;/code&gt;传入lambda。j记得每次使用的时候需要先用&lt;code&gt;lock&lt;/code&gt;或者&lt;code&gt;expired&lt;/code&gt;判断一下是否有效。&lt;/p&gt;
&lt;h1&gt;实现一个简单的make_unique&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt; //变长参数模板
std::unique_ptr&amp;lt;T&amp;gt; my_make_unique(Args&amp;amp;&amp;amp;... args){ //函数会返回unique_ptr
    return std::unique_ptr&amp;lt;T&amp;gt; (new T(std::forward&amp;lt;Args&amp;gt;(args)...)); //使用完美转发至对象的构造函数。构造智能指针。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;实现一个简单的make_shared&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt; //变长参数模板
std::shared_ptr&amp;lt;T&amp;gt; my_make_shared(Args&amp;amp;&amp;amp;... args){ //函数会返回unique_ptr
    return std::shared_ptr&amp;lt;T&amp;gt; (new T(std::forward&amp;lt;Args&amp;gt;(args)...)); //使用完美转发至对象的构造函数。构造智能指针。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;shared_ptr不能直接协变&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt;不能直接协变，需要转换一下。参考&lt;a href=&quot;https://stackoverflow.com/questions/6924754/return-type-covariance-with-smart-pointers&quot;&gt;这篇文章&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;个人理解是因为他找不到&lt;code&gt;shared_ptr&amp;lt;Base&amp;gt;&lt;/code&gt;和&lt;code&gt;shared_ptr&amp;lt;Derive&amp;gt;&lt;/code&gt;之间的直接联系&lt;/p&gt;
&lt;h1&gt;通过构造函数创建&lt;code&gt;shared_ptr&lt;/code&gt;对象一个非常牛逼的副作用 ：&lt;code&gt;shared_ptr&lt;/code&gt; 可以正确地通过基类指针析构整个对象，即使基类没有定义基类析构函数为虚析构。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;struct Base {
    // no virtual here
    ~Base()
    {
        printf(&quot;~Base\n&quot;);
    }
};

struct Derived : Base {
    ~Derived()
    {
    	printf(&quot;~Derived\n&quot;);
    }
};


int main(){
    std::shared_ptr&amp;lt;Base&amp;gt; sp(new Derived()); //输出 ~Base 和 ~Derived
    std::shared_ptr&amp;lt;Base&amp;gt; sp2 = make_shared&amp;lt;Derived&amp;gt;(); //输出 ~Base 和 ~Derived
    Base* ptr = new Derived; //只输出~Base
    delete ptr;
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个“特性”目前是 &lt;code&gt;shared_ptr&lt;/code&gt; 独有的，&lt;code&gt;make_shared&lt;/code&gt;也可以。我们可以通过研究代码来理解为什么可以这样做。&lt;/p&gt;
&lt;p&gt;我们提到过，控制块内有一个储存数据实际类型的指针。从一开始 &lt;code&gt;shared_ptr&lt;/code&gt; 的构造函数到这里的 &lt;code&gt;_Ref_count&lt;/code&gt;，所有相关函数都是 template，类型逐层传递保证 &lt;code&gt;_Ref_count::_Ptr&lt;/code&gt; 是 heap 对象的&lt;strong&gt;实际类型&lt;/strong&gt;，这意味着这个 &lt;code&gt;shared_ptr&lt;/code&gt; 实现了在内部保存了管理对象的实际类型，并且 &lt;code&gt;_Ref_count::_Destroy()&lt;/code&gt; 是直接对实际类型进行 &lt;code&gt;delete&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以，哪怕基类的析构函数不是 virtual，&lt;code&gt;sp&lt;/code&gt; 一样能够正确析构。&lt;/p&gt;
&lt;p&gt;假设我们现在有个例子是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;father&amp;gt; ptr(new child);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20221121054220.png&quot; alt=&quot;微信图片_20221121054220&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从上图我们看到了，模板类的类型也就是&lt;code&gt;shared_ptr&amp;lt;&amp;gt;&lt;/code&gt;里面写的是&lt;code&gt;_Ty&lt;/code&gt;，而放到构造函数里面的也就是&lt;code&gt;new&lt;/code&gt;的对象的类型是&lt;code&gt;_Ux&lt;/code&gt;。&lt;code&gt;_UX&lt;/code&gt;的类型会被自动推导不必显式指定。因为是函数模板。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new child&lt;/code&gt;返回的指针类型是&lt;code&gt;child&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20221121054225.png&quot; alt=&quot;微信图片_20221121054225&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上图的析构是调用&lt;code&gt;_Decref&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/QQ%E6%88%AA%E5%9B%BE20221121054320.png&quot; alt=&quot;QQ截图20221121054320&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_Decref&lt;/code&gt;会在资源真正需要被销毁的时候调用&lt;code&gt;_Destroy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20221121054230.png&quot; alt=&quot;微信图片_20221121054230&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里的传入&lt;code&gt;_Ref_count&lt;/code&gt;的&lt;code&gt;_Ty&lt;/code&gt;类型就是刚才传入的&lt;code&gt;_Ux&lt;/code&gt;。也就是实际数据类型&lt;code&gt;child&lt;/code&gt;。调用&lt;code&gt;_Destroy&lt;/code&gt;的时候直接&lt;code&gt;delete&lt;/code&gt;的是&lt;code&gt;child&lt;/code&gt;。所以可以正确释放。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_Ref_count_base&lt;/code&gt;就是控制块基类。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://kingsamchen.github.io/2018/03/16/demystify-shared-ptr-and-weak-ptr-in-msvc-stl/&quot;&gt;大佬的&lt;code&gt;shared_ptr&lt;/code&gt;解析&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;https://kingsamchen.github.io/2018/03/16/demystify-shared-ptr-and-weak-ptr-in-msvc-stl/&lt;/p&gt;
&lt;h1&gt;shared_ptr的别名构造函数 （Alias Constructor ）&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;这个别名构造再标准库里的enable_shared_from_this中使用了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过上面的部分，我们理解了在&lt;code&gt;shared_ptr&lt;/code&gt;中，指向对象的指针和控制块内指向对象的指针是完全独立的。后面的特征通过了&lt;code&gt;Type erasure&lt;/code&gt; 而逃过了编译器的类型检查。内部控制块的模板参数（控制块内指向对象的指针的类型）和外部智能指针的模板参数（单独指向对象的那个指针的类型）毫无干系, 因此控制块指针和存储指针本质上是完全独立的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class Y &amp;gt;
shared_ptr( const shared_ptr&amp;lt;Y&amp;gt;&amp;amp; r, element_type* ptr ) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;别名使用构造函数&lt;/em&gt;：构造 &lt;code&gt;shared_ptr&lt;/code&gt; ，与 &lt;code&gt;r&lt;/code&gt; 的初始值共享所有权信息，但保有无关且不管理的指针 &lt;code&gt;ptr&lt;/code&gt; 。若此 &lt;code&gt;shared_ptr&lt;/code&gt; 是离开作用域的组中的最后者，则它将调用最初 &lt;code&gt;r&lt;/code&gt; 所管理对象的析构函数。&lt;strong&gt;然而，在此 &lt;code&gt;shared_ptr&lt;/code&gt; 上调用 &lt;code&gt;get()&lt;/code&gt; 将始终返回 &lt;code&gt;ptr&lt;/code&gt; 的副本&lt;/strong&gt;。程序员负责确保只要此 &lt;code&gt;shared_ptr&lt;/code&gt; 存在，此 &lt;code&gt;ptr&lt;/code&gt; 就保持合法，&lt;strong&gt;例如在典型使用情况中，其中 &lt;code&gt;ptr&lt;/code&gt; 是 &lt;code&gt;r&lt;/code&gt; 所管理对象的成员&lt;/strong&gt;，或是 &lt;code&gt;r.get()&lt;/code&gt; 的别名（例如向下转型）。&lt;a href=&quot;https://zh.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr&quot;&gt;来自cppreference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;和拷贝构造函数一样。除了存储指针不同 &lt;a href=&quot;https://cplusplus.com/reference/memory/shared_ptr/shared_ptr/&quot;&gt;来自cplusplus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;注意，使用的时候算作对指针&lt;code&gt;r&lt;/code&gt;的资源的一次额外使用。也就是强引用计数+1。 &lt;a href=&quot;https://cplusplus.com/reference/memory/shared_ptr/shared_ptr/&quot;&gt;来自cplusplus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;这句话翻译过来就是，这个指针的控制块使用的是&lt;code&gt;r&lt;/code&gt;的(共享控制块) &lt;em&gt;[与 &lt;code&gt;r&lt;/code&gt; 的初始值共享所有权信息]&lt;/em&gt;。但是实际指向的数据是&lt;code&gt;ptr&lt;/code&gt;的 &lt;em&gt;[但保有无关且不管理的指针 &lt;code&gt;ptr&lt;/code&gt;]&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;控制块使用的是&lt;code&gt;r&lt;/code&gt;的 ，这句话代表了管理指针是指向&lt;code&gt;r&lt;/code&gt;的。&lt;/li&gt;
&lt;li&gt;实际指向的数据是&lt;code&gt;ptr&lt;/code&gt;的代表存储指针是指向&lt;code&gt;ptr&lt;/code&gt;的。&lt;/li&gt;
&lt;li&gt;白话就是，只要&lt;code&gt;r&lt;/code&gt;的计数器为0，就该释放&lt;code&gt;ptr&lt;/code&gt;的资源了，我不管有没有人用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj {
public:
	myobj(int n):val(n){}
	~myobj() {
		cout &amp;lt;&amp;lt; &quot;myobj析构 &quot;&amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
	}
	int val;

};
int main() {
	shared_ptr&amp;lt;myobj&amp;gt; sp1{ new myobj(5) };//sp1是共享对象（也就是new出来的Person）的唯一拥有者。此时控制指针和储存指针都指向myobj(5)
	shared_ptr&amp;lt;myobj&amp;gt; sp2{ sp1,new myobj(6) };//现在sp1,sp2都是共享对象的拥有者。但是注意，此时sp2的控制块和sp1共享。也就是此时sp1的引用计数和sp2的引用计数均为2。但是sp2的存储指针被替换为指向myobj(6)
    cout &amp;lt;&amp;lt; sp1.get()-&amp;gt;val &amp;lt;&amp;lt; endl; //get返回存储指针，此处输出5
    cout &amp;lt;&amp;lt; sp2.get()-&amp;gt;val &amp;lt;&amp;lt; endl; //get返回存储指针，此处输出6
    cout &amp;lt;&amp;lt; sp1.use_count() &amp;lt;&amp;lt;&quot;, &quot;&amp;lt;&amp;lt; sp2.use_count()&amp;lt;&amp;lt; endl; //2，2 此时sp1的引用计数和sp2的引用计数均为2。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面的注释已经很详细了，但是还有一点，这里大家发现了有内存泄漏。
&lt;ul&gt;
&lt;li&gt;因为&lt;code&gt;sp2&lt;/code&gt;和&lt;code&gt;sp1&lt;/code&gt;共享控制块，所以他们其实只对&lt;code&gt;myobj(5)&lt;/code&gt;这个资源负责。而sp2的存储指针被替换为&lt;code&gt;myobj(6)&lt;/code&gt;，但是并没有指针管理。所以这块内存是泄漏的。&lt;/li&gt;
&lt;li&gt;没有指针管理&lt;code&gt;myobj(6)&lt;/code&gt;是因为我们说过，&lt;code&gt;shared_ptr&lt;/code&gt;的资源释放，释放的是管理指针指向的对象而非存储指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/QQ%E6%88%AA%E5%9B%BE20221121094127.png&quot; alt=&quot;QQ截图20221121094127&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第二部分：&lt;/p&gt;
&lt;p&gt;我们继续上面的代码往下写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sp1.reset(); //此时，sp1置空。和资源myobj(5)，控制块都毫无关联。所以sp1存储指针为空，控制块依旧为空。因为已经和之前的控制块没有关系。
cout &amp;lt;&amp;lt; sp1.use_count() &amp;lt;&amp;lt;&quot;, &quot;&amp;lt;&amp;lt; sp2.use_count()&amp;lt;&amp;lt; endl; //0, 1
if(sp1.get() == nullptr){ //输出true。因为确实是空指针了。
    cout &amp;lt;&amp;lt;&quot;true&quot; &amp;lt;&amp;lt; endl;
}
cout &amp;lt;&amp;lt; sp2.get()-&amp;gt;val &amp;lt;&amp;lt; endl; //这时候sp2依旧掌握myobj(6)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注释已经详细说明了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三部分：&lt;/p&gt;
&lt;p&gt;我们继续写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shared_ptr&amp;lt;myobj&amp;gt; sp3(sp1,new myobj(7)); // 注意之前sp1的控制块为空。此时sp3和sp1共享控制块。也就是都没有控制块。自然没有引用计数。
cout &amp;lt;&amp;lt; sp3.use_count() &amp;lt;&amp;lt;  endl; //0
cout &amp;lt;&amp;lt; sp1.use_count() &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; sp2.use_count() &amp;lt;&amp;lt; endl;//0，1
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意之前&lt;code&gt;sp1&lt;/code&gt;的控制块为空。此时&lt;code&gt;sp3&lt;/code&gt;和&lt;code&gt;sp1&lt;/code&gt;共享控制块。也就是都没有控制块。自然没有引用计数。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reset&lt;/code&gt;的实现是临时对象+&lt;code&gt;swap&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所以此时内存泄漏两个位置。一个是&lt;code&gt;myobj(6)&lt;/code&gt; 一个是&lt;code&gt;myobj(7)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-28-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.assets/QQ%E6%88%AA%E5%9B%BE20221121102638.png&quot; alt=&quot;QQ截图20221121102638&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;一般什么时候用？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;不同类型对象共享生存期。&lt;/li&gt;
&lt;li&gt;直接持有某个对象的成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举一个直接持有某个对象的成员的例子。&lt;strong&gt;对象的成员和对象本身,显然的确应该共享生存期&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class innerobj{
    public:
        int inner_val;
        innerobj(int x):inner_val(x){}
};

class myobj {
public:
	myobj(int n):obj(n){}
	~myobj() {
		cout &amp;lt;&amp;lt; &quot;myobj的析构 &quot;&amp;lt;&amp;lt; obj.inner_val &amp;lt;&amp;lt; endl;
	}
	innerobj obj;

};
int main() {
    shared_ptr&amp;lt;myobj&amp;gt; ptr{ new myobj(5) };//ptr是共享对象（也就是new出来的myobj）的唯一拥有者
    shared_ptr&amp;lt;innerobj&amp;gt; sub_ptr(ptr, &amp;amp;(ptr-&amp;gt;obj)); //此时sub_ptr的控制块和ptr共享，但是subptr的存储指针直接指向myobj的innerobj成员变量。
    //记得要取地址。第二个参数要传入指针。

    cout &amp;lt;&amp;lt; ptr.use_count() &amp;lt;&amp;lt; endl; //2 引用计数被+1
    cout &amp;lt;&amp;lt; sub_ptr.use_count() &amp;lt;&amp;lt; endl; //2 引用计数被+1

    ptr.reset(); //reset, 此时ptr为空。
    cout &amp;lt;&amp;lt; ptr.use_count() &amp;lt;&amp;lt; endl; //0 无控制块
    cout &amp;lt;&amp;lt; sub_ptr.use_count() &amp;lt;&amp;lt; endl; //1 引用计数被-1

    cout &amp;lt;&amp;lt; sub_ptr-&amp;gt;inner_val &amp;lt;&amp;lt; endl; //正常输出5 。


    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意这里不会有内存泄漏。因为在&lt;code&gt;reset&lt;/code&gt;后，尽管实际持有的存储指针是子对象，但是由于共享控制块，控制块实际的管理指针依旧是&lt;code&gt;myobj&lt;/code&gt;这个大类类型的。所以析构的时候调用的是&lt;code&gt;myobj&lt;/code&gt;的析构。由于&lt;code&gt;myobj&lt;/code&gt;和&lt;code&gt;innerobj&lt;/code&gt;是包含关系，&lt;code&gt;innerobj&lt;/code&gt;自然也会被正确析构。因为&lt;code&gt;ptr-&amp;gt;obj&lt;/code&gt;不会被单独管理，所以也不存在double free 错误&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;有时候，这个东西被叫做幻影指针（Phantom)&lt;/strong&gt; ，依旧&lt;a href=&quot;https://devblogs.microsoft.com/oldnewthing/20230818-00/?p=108619&quot;&gt;来自 Raymond Chen&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/mingwu96/article/details/123327325&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/47744606&lt;/p&gt;
&lt;p&gt;https://stackoverflow.com/questions/27109379/what-is-shared-ptrs-aliasing-constructor-for&lt;/p&gt;
</content:encoded></item><item><title>C++杂记 - 2（一）：delete this、运算符重载、类型转换与 inline</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B02/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B02/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++杂记 - 2&lt;/h1&gt;
&lt;h1&gt;关于&lt;code&gt;delete this&lt;/code&gt;&lt;/h1&gt;
&lt;h2&gt;能否在类的析构函数中调用&lt;code&gt;delete this&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;不能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实验告诉我们，会导致堆栈溢出。原因很简单，&lt;strong&gt;&lt;code&gt;delete&lt;/code&gt;的本质是“为将被释放的内存调用一个或多个析构函数，然后，释放内存&lt;/strong&gt;显然，&lt;code&gt;delete this&lt;/code&gt;会去调用本对象的析构函数，而析构函数中又调用&lt;code&gt;delete this&lt;/code&gt;，形成无限递归，造成堆栈溢出，系统崩溃。&lt;/p&gt;
&lt;h2&gt;能否在类的其它函数中调用&lt;code&gt;delete this&lt;/code&gt;？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;可以，但不可以涉及到任何和this指针（成员变量，成员函数，虚函数）相关的东西。一旦涉及到this指针，如操作数据成员，调用虚函数等，就会出现不可预期的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，让我们明确几点概念&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在类对象的内存空间中，只有数据成员和虚表指针，并不包含代码内容。类的成员函数单独放在代码段中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在调用成员函数时，隐含传递一个&lt;code&gt;this&lt;/code&gt;指针，让成员函数知道当前是哪个对象在调用它。当 调用&lt;code&gt;delete this&lt;/code&gt;时，类对象的内存空间被释放。在&lt;code&gt;delete this&lt;/code&gt;之后进行的其他任何函数调用，只要不涉及到&lt;code&gt;this&lt;/code&gt;指针的内容，都能够正常运行。一旦涉及到&lt;code&gt;this&lt;/code&gt;指针，如操作数据成员，调用虚函数等，就会出现不可预期的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;delete this&lt;/code&gt;释放了类对象的内存空间，但是内存空间却并不是马上被回收到系统中，导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的。这时访问数据成员可能是随机数，访问虚表发生指针无效的概率很高，系统崩溃。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么时候可以？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt;指向的实例必须是&lt;code&gt;new&lt;/code&gt;出来的，不能是&lt;code&gt;new[] &lt;/code&gt;/&lt;code&gt;placement new&lt;/code&gt;出来的，也不能是栈上的，也不是全局变量。只能是简单的&lt;code&gt;new&lt;/code&gt;出来的。&lt;/p&gt;
&lt;p&gt;调用了&lt;code&gt;delete this&lt;/code&gt; 的成员函数（下面称为当事函数）&lt;strong&gt;返回&lt;/strong&gt;后，这个实例不可以调用其他任何成员函数（这好理解，涉及到&lt;code&gt;this&lt;/code&gt;指针，因为实例不存在了）。这个成员函数应当成为这个实例访问的最后一个函数。&lt;/p&gt;
&lt;p&gt;调用了&lt;code&gt;delete this&lt;/code&gt; 的成员函数后（从&lt;code&gt;delete this&lt;/code&gt;这一行之后开始），这个实例不可以调用任何成员函数和成员变量（这好理解，理由同2）&lt;/p&gt;
&lt;p&gt;调用了&lt;code&gt;delete this&lt;/code&gt;之后，不得以任何形式提及&lt;code&gt;this&lt;/code&gt;，包括比较、打印、类型转换等（因为this已经不存在了）。&lt;/p&gt;
&lt;h2&gt;换言之，为什么一个指向类成员的指针被delete并置空后依旧可以调用不访问类成员数据的函数？或者是为什么一个类类型的空指针可以访问类内非虚且不访问成员变量的函数？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
        void print(){
            cout &amp;lt;&amp;lt;&quot;myclass&quot; &amp;lt;&amp;lt; endl;
        }

        int vala = 10;//类成员没有被print函数访问。

};
int main(){
    myclass* ptr = new myclass();
    ptr-&amp;gt;print(); //输出myclass
    delete ptr;
    ptr = nullptr;
    ptr-&amp;gt;print(); //输出myclass
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到&lt;code&gt;delete&lt;/code&gt;后依旧可以调用没有访问类成员的函数。&lt;strong&gt;另一个前提是该函数不是虚函数。因为虚函数的调用需要依靠对象的虚函数指针找虚函数表。调用&lt;code&gt;delete&lt;/code&gt;资源释放后置空导致和&lt;code&gt;this&lt;/code&gt;有关联了。所以报错。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**非虚的类成员函数在编译的时候就已经解决了访问问题。也就是编译成带作用域的全局函数。并添加&lt;code&gt;this&lt;/code&gt;形参。**也就是访问非虚而且不访问类成员(变量)的函数是不涉及到&lt;code&gt;this&lt;/code&gt;指针的。虽然指针必须要被传入，但是传入参数并不关心当前指针是否有效。只要没有解引用&lt;code&gt;this&lt;/code&gt;指针即可。&lt;/p&gt;
&lt;p&gt;我们再思考一下。如果上面的例子换成C写是什么样子的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myclass_inC{
    int vala = 10;
};

void print(myclass_inC* self){
    cout &amp;lt;&amp;lt;&quot;myclass&quot; &amp;lt;&amp;lt; endl;
}


int main(){
    struct myclass_inC *myclass_c = NULL;
    print(myclass_c); //输出my_class
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在深度探索C++对象模型的笔记4.1我们提到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为编译器会把每所有的非虚函数（此处不确定是否正确。有的笔记里写的是普通函数，普通成员函数和静态成员函数。书里此处没有提及静态成员函数。）进行处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先是把对象调用的方式改为值调用。也就是在形参的第一个位置放一个this指针（指向对象的指针）做为入参&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其次是把函数内对于非静态数据成员的直接操作变成用this指针的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后是名称重新编译成独特名称（这一步的目的是实现重载。注意，C++重载是依靠名称和参数，C只有名称）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后是实施NRV优化（如果有）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;类成员函数在处理后可以理解为变成了带作用域的全局函数。虚函数也只不过是多了查表的步骤。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们上面用C写的例子和处理后的非虚类成员函数差不多。此时我们可以看到。尽管我们的类指针是空的。但是它依旧符合函数入参类型。而且我们在函数内并没有通过这个指针访问任何数据成员。所以并不会报错。单纯传递空指针是可以的。所以是可以调用这样的成员函数的。&lt;/p&gt;
&lt;h1&gt;什么时候需要虚析构函数&lt;/h1&gt;
&lt;p&gt;是多态基类的时候。多态一定有虚函数。（effective C++ 第七条)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多态实现：通过指针+向上转换(子类转父类)(父类指针指向子类对象） + 虚函数 = 动态绑定 、虚机制&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么如果析构函数不是虚函数的时候，仅仅执行父类析构函数？&lt;/h2&gt;
&lt;p&gt;很多人可能会有疑问，问我们知道构造顺序是先父类构造再子类构造，析构顺序是先子类析构再父类析构（仅多态情况）。那么为什么我父类析构不是虚析构的时候仅执行父类析构？难道不应该是仅执行子类析构吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为忘了一点，多态调用必须是虚函数才可以，仅通过父类指针指向子类对象是不会触发虚机制（动态绑定（多态））的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;虽然父类指针可以指向子类，但是其访问范围还是仅仅局限于父类本身有的数据，那些子类的数据，父类指针是无法访问的。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;类继承是静态绑定，包含虚函数的情况才是动态绑定&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    A(){}

};

class B: public A{
    public:
    B(){}
    int bval = 3;
};

class C :public B{
    public:
    C(){}
    int cval = 4;
};

int main(){
    A* a = new C;
    cout &amp;lt;&amp;lt; a-&amp;gt;bval &amp;lt;&amp;lt; endl; //错误。类A没有成员bval
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当定义一个指向子类实例的父类指针的时候，内存中实例化了子类，由于子类继承了父类,因此内存中的子类里包含父类的所有成员。但由于&lt;strong&gt;申明的是父类指针，因此该指针不能够访问子类的成员，而只能访问父类的成员。然而在父类里可以声明纯虚函数和定义虚函数，使用父类指针访问虚函数或纯虚函数的时候，访问到的是子类里重写的函数。当然，对于虚函数，如果子类里没有对其重写的话，仍然访问到父类里定义的虚函数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;父类指针指向子类对象，仅可访问子类的父类部分。因为不知道子类的内存布局，所以没法访问子类的部分。也就是说，父类指针指向子类对象会发生隐式转换。所以说它还是个父类类型。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;多态的时候由于有RTTI所以可以在运行时获知子类类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们提到过，指向虚表的指针&lt;code&gt;_vfptr&lt;/code&gt;是子类的父类部分。也就是父类指针可以访问的到子类的指向子类自己的虚表的指针&lt;code&gt;_vfptr&lt;/code&gt;。父类访问这个指针后即可访问子类的虚表。（反复强调虚表一个类只有一个）。访问到这个虚表后，就会查表。即访问对应的虚函数。如果子类有重写，那么这个对应的虚表中的函数会在编译期就从父类的覆盖为子类的。这样就可以执行子类的函数了。如果子类没写，那个虚表里面的函数就还是父类的虚函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，如果没有虚析构，则虚表里没有这一条。那么编译的时候发现不是虚函数，就会静态的将父类的析构函数编译过来。如果有了虚析构，则虚表里会有这一条。于是就会执行正常多态析构动作，先调用子类析构，再调用父类析构。也就是根据指向的对象找到对象的虚表指针，找到对象的虚表再调用对应的虚函数。实现多态。&lt;/p&gt;
&lt;h1&gt;运算符的重载，参数的数量怎么确定？在类里面和不在类里面？不在类里面声明怎么访问类里面的属性（如果是私有的呢？）&lt;/h1&gt;
&lt;h2&gt;运算符重载可以继续被重载。符合函数重载规则即可。&lt;/h2&gt;
&lt;h2&gt;类模板的运算符重载 在 模板笔记内&lt;/h2&gt;
&lt;h2&gt;运算符重载&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class multiple{
    public:
    int vala;
    int valb;
    multiple(){}
    multiple(int x, int y):vala(x), valb(y){}
    const multiple operator*(const multiple&amp;amp; rhs) const{
        multiple temp(this-&amp;gt;vala * rhs.vala, this-&amp;gt;valb*rhs.valb);
        return temp;
    }
    注意 乘法我们说是有两个参数即a*b 但是这里现在是成员函数，第一个参数默认是调用对象 即this 所以形参只有一个rhs。
    但是我们可以写为非成员函数，这样形参就会有rhs和lhs。
    非成员函数在进行隐式类型转换的时候可以实现交换律。参考effectiveC++ 条款24
    void getval(){
        cout &amp;lt;&amp;lt; this-&amp;gt;vala&amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; this-&amp;gt;valb &amp;lt;&amp;lt; endl;
    }

};

const multiple operator*(const multiple&amp;amp; lhs, const multiple&amp;amp; rhs){
    multiple temp(lhs.vala * rhs.vala, lhs.valb*rhs.valb);
    return temp;
}

//类外可以这么写。注意const不可修饰非成员函数！！
int main()
{
    multiple t1(5,5);
    multiple t2(6,4);
    multiple t3 = t1 * t2;
    t3.getval();
这里t3 = t1 * t2本质上是t3 = t1.operator*(t2)
t1是this，t2是rhs，t1.operator*(t2)对应函数返回值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 这里重载第三个const的目的是让这个函数成为const函数。目的是可以被const对象调用 这里的例子就是我们避免意外赋值，使重载返回值也是const 因为如果不设置const的话 像是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(t1 * t2) = t3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种就不会报错。因为t1*t2被当成了一个multiple类型的变量。可以给它赋值。因为它返回一个multiple对象。但是这违背了我们的意愿。所以我们加const来模拟常量确保t1*t2不会被赋值 如果我们这个重载函数不设置const属性（也就是第三个const）的话，我们这样就无法链式调用了。因为我们现在的t1*t2返回的是一个const multiple对象。const对象无法调用非const函数。所以我们的函数必须要const&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当运算符重载为&lt;strong&gt;类的成员函数&lt;/strong&gt;时，函数的参数个数比原来的操作数要少一个（后置单目运算符除外），这是因为成员函数用this指针&lt;strong&gt;隐式地访问了类的一个对象&lt;/strong&gt;，它充当了运算符函数最左边的操作数。因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;双目运算符重载为类的成员函数时，函数&lt;strong&gt;只显式说明一个参数&lt;/strong&gt;，该形参是运算符的右操作数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前置单目运算符重载为类的成员函数时，&lt;strong&gt;不需要显式说明参数&lt;/strong&gt;，&lt;strong&gt;即函数没有形参&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后置单目运算符重载为类的成员函数时，函数要带有一个整型形参。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用成员函数运算符的格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;对象名&amp;gt;.operator &amp;lt;运算符&amp;gt;(&amp;lt;参数&amp;gt;)
它等价于
&amp;lt;对象名&amp;gt;&amp;lt;运算符&amp;gt;&amp;lt;参数&amp;gt;                
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当运算符重载为&lt;strong&gt;类的友元函数&lt;/strong&gt;时，由于没有隐含的this指针，因此操作数的个数没有变化，所有的操作数都必须通过函数的&lt;strong&gt;形参&lt;/strong&gt;进行传递，函数的参数与操作数自左至右一一对应。调用友元函数运算符的格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;operator&amp;lt;运算符&amp;gt;(&amp;lt;参数1&amp;gt;,&amp;lt;参数2&amp;gt;)
它等价于
&amp;lt;参数1&amp;gt;&amp;lt;运算符&amp;gt;&amp;lt;参数2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;友元函数不属于任何类，但是可以当做类成员函数使用，即访问私有部分。&lt;strong&gt;友元函数必须在类内声明。但是可以在类内或类外定义&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class multiple{
    public:
    multiple(){}
    multiple(int x, int y):vala(x), valb(y){}
    friend const multiple operator*(const multiple&amp;amp; lhs, const multiple&amp;amp; rhs); //类内声明
    void getval(){
        cout &amp;lt;&amp;lt; this-&amp;gt;vala&amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; this-&amp;gt;valb &amp;lt;&amp;lt; endl;
    }
    private:
    int vala;
    int valb;

};
const multiple operator*(const multiple&amp;amp; lhs, const multiple&amp;amp; rhs){ //类内或类外定义均可。
    multiple temp(lhs.vala * rhs.vala, lhs.valb*rhs.valb);
    return temp;
}
int main()
{
    multiple t1(5,5);
    multiple t2(6,4);
    multiple t3 = t1 * t2;
    t3.getval();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在类外，因为不是成员函数所以没有this指针，所以必须要有全部参数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运算符重载写为友元的核心目的是解决两个不同的类的数据访问问题。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;比如&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;B&lt;/code&gt;想要做&lt;code&gt;A+B&lt;/code&gt;的时候。因为&lt;code&gt;A&lt;/code&gt;不能访问&lt;code&gt;B&lt;/code&gt;的私有成员。所以可以把这个&lt;code&gt;A.operator+(const B&amp;amp;)&lt;/code&gt;写进&lt;code&gt;B&lt;/code&gt;并且声明为&lt;code&gt;friend&lt;/code&gt;。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;注意，类外的运算符重载（此处指的是类的运算符而不是函数调用运算符）的时候，不能单独写一个类。要么在原来的类里面，要么是全局函数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
    int val1;
    int val2;
    myclass(){}
    myclass(int x, int y):val1(x), val2(y){}
    bool operator()(const myclass&amp;amp; a, const myclass&amp;amp; b){
        cout &amp;lt;&amp;lt;&quot;comp&quot; &amp;lt;&amp;lt;endl;
        return a.val1 &amp;lt; b.val1;
    }
    
};

class mycomp{						//这么写是错的。这里的运算符重载被当做了mycomp的运算符重载
    public:
    bool operator&amp;lt;(const myclass&amp;amp; a, const myclass&amp;amp; b){
        cout &amp;lt;&amp;lt;&quot;comp11&quot; &amp;lt;&amp;lt;endl;
        return a.val1 &amp;lt; b.val1;
    }
};
bool operator&amp;lt;(const myclass&amp;amp; a, const myclass&amp;amp; b){
        cout &amp;lt;&amp;lt;&quot;comp11&quot; &amp;lt;&amp;lt;endl;
        return a.val1 &amp;lt; b.val1;
    }


int main()
{
    set&amp;lt;myclass&amp;gt; myset;					//如果重载的是小于号，则写在类内类外都不需要在模板处指定排序类型。
    set&amp;lt;myclass, myclass&amp;gt; myset1;		//如果重载的是括号也就是调用运算符，则无论写在类内类外都需要在模板处指定排序类型。
    myset.insert(myclass(5,10));
    myset.insert(myclass(1,10));
    myset.insert(myclass(10,10));
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;如果是指针容器呢？&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class myclass{
    public:
    int val1;
    int val2;
    myclass(){}
    myclass(int x, int y):val1(x), val2(y){}
    bool operator()(const myclass&amp;amp; a, const myclass&amp;amp; b) const {
        cout &amp;lt;&amp;lt;&quot;comp&quot; &amp;lt;&amp;lt;endl;
        return a.val1 &amp;lt; b.val1;
    }
    bool operator()(const myclass* a, const myclass* b){
        cout &amp;lt;&amp;lt;&quot;comp112&quot; &amp;lt;&amp;lt;endl;
        return a-&amp;gt;val1 &amp;lt; b-&amp;gt;val1;
    }
    
};


class mycomp{
    public:
    bool operator()(const myclass* a, const myclass* b){		//写在类外
        cout &amp;lt;&amp;lt;&quot;comp11&quot; &amp;lt;&amp;lt;endl;
        return a-&amp;gt;val1 &amp;lt; b-&amp;gt;val1;
    }
};
int main()
{
    set&amp;lt;myclass*, myclass&amp;gt; myset;				//指针容器，必须使用重载函数调用运算符的方式，并且指定其类型。可以写在类外
    set&amp;lt;myclass*, mycomp&amp;gt; myset;				//也可以
    myset.insert(new myclass(5,10));
    myset.insert(new myclass(1,10));
    myset.insert(new myclass(10,10));
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;指针容器，必须使用重载函数调用运算符的方式，并且在容器模板处指定其（排序函数所在的）类型。可以写在类外。因为重载普通的运算符 如 &lt;code&gt;&amp;lt;&lt;/code&gt;，要求传入的形参必须是类类型。但是指针不是类类型。所以无法写在类外。因为对象指针和对象不是一个东西。对象指针依旧是指针类型。所以他会去指针的类里面找重载，写在类内的重载无法被触发。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;成员访问运算符 -&amp;gt; 重载&lt;/h2&gt;
&lt;p&gt;类成员访问运算符（&lt;code&gt; -&amp;gt;&lt;/code&gt; ）可以被重载，它被定义用于为一个类赋予&quot;指针&quot;行为。&lt;strong&gt;运算符 &lt;code&gt;-&amp;gt; &lt;/code&gt;必须是一个成员函数。如果使用了 &lt;code&gt;-&amp;gt; &lt;/code&gt;运算符，返回类型必须是指针或者是类的对象。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class obj{
    public:
    int val;
    obj(int x):val(x){};

    void objfunc(){
        cout &amp;lt;&amp;lt;&quot;obj&quot; &amp;lt;&amp;lt; endl;
    }
    void getval(){
        cout &amp;lt;&amp;lt; val &amp;lt;&amp;lt; endl;
    }
};
class objhelper{
    public:
    objhelper(){
        objptr = new obj(10); //构造函数初始化另一个类的对象
    }
    obj* operator-&amp;gt;(){ //注意，返回的是obj类的指针。
        return objptr;
    }

    ~objhelper(){
        delete objptr; //析构函数释放对象。
    }
    obj* objptr; //类内持有一个另一个类的指针。

    
};
int main(){

    objhelper helper;
    helper-&amp;gt;objfunc(); //输出obj
    helper-&amp;gt;getval();  //输出10

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面代码我们可以看到，重载类成员访问运算符多半是起到一个帮助作用，帮助我们访问另一个类。&lt;/p&gt;
&lt;p&gt;重载成员访问运算符根据调用者的类型不同，有两条作用规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果调用者是指针，则按照内置的箭头运算符去处理。表达式等价于&lt;code&gt;(*调用者).member&lt;/code&gt;。首先解引用该指针，然后从所得的对象中获取指定的成员。如果调用者所指的类没有名为member的成员，则编译器报错。&lt;/li&gt;
&lt;li&gt;如果调用者是一个定义了&lt;code&gt;operator-&amp;gt;() &lt;/code&gt;的类对象，则&lt;code&gt;调用者-&amp;gt;member&lt;/code&gt;等价于&lt;code&gt;调用者.operator-&amp;gt;() -&amp;gt;member&lt;/code&gt;。其中，如果&lt;code&gt;operator-&amp;gt;()&lt;/code&gt;的返回结果是一个指针，则转第1步；如果返回结果仍然是一个对象，且该对象本身也重载了&lt;code&gt;operator-&amp;gt;()&lt;/code&gt;，则重复调用第2步，否则编译器报错。最终，过程要么结束在第一步，要么无限递归，要么报错。
https://blog.csdn.net/friendbkf/article/details/45949661&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-%E6%9D%82%E8%AE%B02.assets/QQ%E6%88%AA%E5%9B%BE20230311010451.png&quot; alt=&quot;QQ截图20230311010451&quot; /&gt;&lt;/p&gt;
&lt;p&gt;——来自Modern C++ Design 7.3 P.160&lt;/p&gt;
&lt;h2&gt;解引用运算符&lt;code&gt;*&lt;/code&gt;重载&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt;是一个一元操作符，作用于指针，获取指针所指单元的内容。当某个类中对&lt;code&gt;*&lt;/code&gt;操作符重载时，是将该类对象当做一个指针看待，用&lt;code&gt;*&lt;/code&gt;操作符提取指针所指向的内容。&lt;strong&gt;它一般有两个版本。一个是普通成员函数返回某个类中的指针的解引用的引用，也就是指针指向的对象的引用。另一个版本是常量成员函数返回&lt;code&gt;const&lt;/code&gt;引用。（再次强调返回值不是函数签名。但是修饰成员函数的const是函数签名）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么要返回引用？如果不返回引用则无法给解引用的对象赋值。比如&lt;code&gt;*ptr = 200&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;不返回引用的话，解引用返回的是一个临时对象，是右值。无法给右值赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//在上面的代码中添加：
obj&amp;amp; operator*(){ //返回的是引用！！
    return *objptr; //返回类内指针的解引用
}

const obj&amp;amp; operator*() const{
    return *objptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;取址运算符&lt;code&gt;&amp;amp;&lt;/code&gt;重载&lt;/h2&gt;
&lt;p&gt;罕有使用。参考模板笔记的&lt;code&gt;std::address_of&lt;/code&gt;部分和more effective c++的条款30。&lt;/p&gt;
&lt;h2&gt;解答：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在类内，如果是双目运算符就少一个。因为左手部分是this。隐式传递。如果是类外，就正常两个。&lt;/li&gt;
&lt;li&gt;不在类里面声明，可以正常访问类公有部分。如果需要访问私有部分就需要使用友元函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;关于为什么赋值运算符必须有返回值并且一般都返回引用&lt;/h2&gt;
&lt;p&gt;比如在移动赋值或者拷贝赋值中返回的都是引用。&lt;/p&gt;
&lt;p&gt;原因主要是性能和为了链式调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链式调用：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;a = b = c;
//就是先执行b=c然后执行a = b。等价于
a.operator=(b.operator=(c))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时若不返回左值的引用，将会生成临时对象，&lt;/p&gt;
&lt;h2&gt;因为不能修改标准库，所以重载流插入&amp;lt;&amp;lt;和流提取&amp;gt;&amp;gt;的时候如果在类内则必须用友元的方式。要么就是全局&lt;/h2&gt;
&lt;h2&gt;为什么有时候运算符重载要在类外也写一份？&lt;/h2&gt;
&lt;p&gt;我们来看下面这个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myInt{
    public:
    int val1;
    myInt(){}
    myInt(int x):val1(x){} //这里没有explicit
    
    const myInt operator+(const myInt&amp;amp; rhs){
        return myInt(this-&amp;gt;val1 + rhs.val1);
    }
    
};

const myInt operator+(const myInt&amp;amp; lhs, const myInt&amp;amp; rhs){ //这里类外要有一份
    return myInt(lhs.val1 + rhs.val1);
}


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

    myInt c = a + b;

    myInt d = a + 10; //这个等于 myInt d = a + myInt(10); 因为有转换构造所以可以。
    
    myInt e = 10 + a; //这等于 myInt e = myInt(10) + a; 如果不写类外，这个不行。
    cout &amp;lt;&amp;lt; c.val1 &amp;lt;&amp;lt; endl; 
    cout &amp;lt;&amp;lt; d.val1 &amp;lt;&amp;lt; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们这里有隐式类型转换。因为有单参的转换构造函数。所以如果构造函数用explicit修饰了就不行了，就要显式调用构造函数生成临时对象了。&lt;/li&gt;
&lt;li&gt;其次，如果不写类外实现，如&lt;code&gt;10+a&lt;/code&gt;这样的就不能调用。因为&lt;code&gt;a+10&lt;/code&gt;是&lt;code&gt;a.operator+(10)&lt;/code&gt;，但是没有&lt;code&gt;10.operator+(a)&lt;/code&gt;。所以这时候就需要有一个全局的接受两个参数的&lt;code&gt;operator+&lt;/code&gt;。这两个可以并存。所以现在是&lt;code&gt;operator+(10, a)&lt;/code&gt;。是可以的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所以一般来说，当前面的调用者是基本类型的时候，就需要写一个全局的操作符重载。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-%E6%9D%82%E8%AE%B02.assets/QQ%E6%88%AA%E5%9B%BE20230106181815.png&quot; alt=&quot;QQ截图20230106181815&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;千万不要重载 &lt;code&gt;||&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 和 &lt;code&gt;,&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;因为他们的表现方式和内建版本不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;布尔逻辑运算符的重载版本无法实现短路求值，并且不会令左侧操作数的求值按照顺序早于右侧操作数。&lt;/li&gt;
&lt;li&gt;逗号运算符的重载版本不会令左侧操作数的求值按照顺序早于右侧操作数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于逗号运算符这里有一个非常特殊的点。在文档中提到了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为此运算符可能被重载，所以泛型库都用 &lt;code&gt;a,void(),b&lt;/code&gt; 这种表达式取代&lt;code&gt; a,b&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在stackoverflow也有同样的&lt;a href=&quot;https://stackoverflow.com/questions/69314599/casting-to-void-to-avoid-use-of-overloaded-user-defined-comma-operator&quot;&gt;疑惑&lt;/a&gt;，为什么要这么做？&lt;/p&gt;
&lt;p&gt;我们拆开看。在&lt;code&gt;a&lt;/code&gt;有自己的逗号运算符的时候，&lt;code&gt;a,void()&lt;/code&gt;会变成&lt;code&gt;a.operator,(void())&lt;/code&gt;。但是因为&lt;code&gt;void()&lt;/code&gt;表达式会返回一个&lt;code&gt;void&lt;/code&gt;类型, 但是&lt;code&gt;void&lt;/code&gt;不是有效类型(不完整类型)，所以&lt;code&gt;a&lt;/code&gt;无论是任何类型都不可能有一个自己的逗号运算符重载的操作数类型是&lt;code&gt;void&lt;/code&gt;。同时重载&lt;code&gt;operator,&lt;/code&gt;必须提供一个参数(右操作数)。 所以这里一定会用到内置的版本。在运算完后，结果会变成&lt;code&gt;void&lt;/code&gt;，因为第一个表达式的运算结果在求值后会被丢弃，只会保留第二个的结果。现在是&lt;code&gt;void,b&lt;/code&gt;了。左侧&lt;code&gt;void&lt;/code&gt;必然只会用内置类型的逗号表达式。所以这样做是有效的。&lt;/p&gt;
&lt;p&gt;剩下的请参照杂记4的序列点部分和more effective c++ 条款7&lt;/p&gt;
&lt;h2&gt;运算符重载的语义，个人感悟&lt;/h2&gt;
&lt;p&gt;运算符重载就是函数调用，一定要思考语义。&lt;/p&gt;
&lt;p&gt;首先关于函数内部要做什么，要不要修改左侧操作数？&lt;/p&gt;
&lt;p&gt;用加法举例子，如果你想要达到执行&lt;code&gt;a+500&lt;/code&gt;能直接把&lt;code&gt;a&lt;/code&gt;的数也改变，那么可以直接在加法重载内改变&lt;code&gt;a&lt;/code&gt;的值。如果你不允许链式调用，则没必要有返回值。这完全合法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;包括拷贝赋值运算符也不是强制返回&lt;code&gt;T&amp;amp;&lt;/code&gt;的。如果你不想链式调用，完全可以写成这样&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myclass{
    int val = 0;
    myclass(int x):val(x){};
    myclass operator+(const myclass&amp;amp; rhs){
            return myclass(val + rhs.val);
    }
    void operator=(const myclass&amp;amp; rhs){
        val = rhs.val;
        cout &amp;lt;&amp;lt;&quot;copy assignment&quot; &amp;lt;&amp;lt; endl;
    }
};
int main(){
    myclass obj(200);
    myclass obj2(300);
    cout &amp;lt;&amp;lt; obj2.val &amp;lt;&amp;lt; endl;
    obj2 = obj;
    cout &amp;lt;&amp;lt; obj2.val &amp;lt;&amp;lt; endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a.operator=(b)&lt;/code&gt;压根不是返回值作用于&lt;code&gt;a&lt;/code&gt;。是先修改了&lt;code&gt;a&lt;/code&gt;然后同时返回自己而已。目的就是链式调用。所以&lt;code&gt;a = b&lt;/code&gt;这个表达式，返回值压根就没用上。&lt;/p&gt;
&lt;p&gt;所以核心就是要思考某个特定的运算符重载是否符合直观性的语义。也就是一般性的使用方式。&lt;/p&gt;
&lt;p&gt;可以参考这个：https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading&lt;/p&gt;
&lt;h1&gt;关于mutable&lt;/h1&gt;
&lt;p&gt;很多人多&lt;code&gt;mutable&lt;/code&gt;有错误认知。不仅仅是因为用得少，而且是有模糊概念。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mutable&lt;/code&gt;是修饰成员变量所用。他的作用是让这个成员变量在任何时候都可以被修改，无论是:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;做为参数传入形参带有&lt;code&gt;const&lt;/code&gt;的函数&lt;/li&gt;
&lt;li&gt;做为参数传入被&lt;code&gt;const&lt;/code&gt;修饰的(成员)函数&lt;/li&gt;
&lt;li&gt;在被&lt;code&gt;const&lt;/code&gt;修饰的(成员)函数中&lt;/li&gt;
&lt;li&gt;被&lt;code&gt;const&lt;/code&gt;修饰的变量中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先举反例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mutable int a; //这是啥？
const mutable int a; //这又是啥？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面，正式开始举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class multiple{
    public:
    multiple(){}
    multiple(int x, int y):vala(x), valb(y){}
    
    
    mutable int vala;
    int valb;

};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们拿到了类的定义。我们开始举例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mutable const int vala; //不可以 这啥玩意
const mutable int vala; //不可以 这啥玩意
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为了简便起见，我们只举成员函数的例子&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class multiple{
    public:
    multiple(){}
    multiple(int x, int y):vala(x), valb(y){}
    //------------------------------------- 被const修饰的成员函数
    void test1(multiple&amp;amp; x) const{
        vala = x.vala;
        x.vala = vala;
    }
    //-------------------------------------被const修饰的函数形参
    void test2(const multiple&amp;amp; x){
        vala = x.vala;
        x.vala = vala;
    }
    //-------------------------------------被const修饰的成员函数 和 被const修饰的函数形参
    void test3(const multiple&amp;amp; x) const{
        vala = x.vala;
        x.vala = vala;
    }
    //-------------------------------------
    mutable int vala;
    int valb;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;以上三个函数代表了三个例子。我们发现被无论是在被&lt;code&gt;const&lt;/code&gt;修饰的成员函数中 还是 被当成参数传入被&lt;code&gt;const&lt;/code&gt;修饰的函数形参中，我们都可以修改&lt;code&gt;mutable&lt;/code&gt;修饰的变量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;还有第四个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const multiple t1(3,3);
t1.valb = 3; //不可以
t1.vala = 3; //可以 因为被mutable修饰
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;四种cast显式（强制）类型转换表达式（运算符/关键字/特殊运算符），都不改变对象本身。&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**四种cast强制类型转换是表达式（特殊运算符）。**他只是长得像类模板但是不是。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这类运算符还包括如&lt;code&gt;new&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;sizeof&lt;/code&gt;, &lt;code&gt;typeid&lt;/code&gt;,&lt;code&gt;noexcept&lt;/code&gt;等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;四种cast强制类型转换对转换对象没有任何影响。他只是生成一个你想转换的类型的临时变量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无论何种类型转换都是创建对应类型的临时&quot;对象&quot;。&lt;/li&gt;
&lt;li&gt;此处可能不严谨。如果转换的类型是引用，则不会创建“对象”因为引用不是对象。我们可以说cast总是会创建一个临时的“东西”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;强制类型转换并不改变原对象类型，只是通过原对象生成新的对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C风格类型转换是运算符。优先级是第三级。C++风格类型转换也是运算符。优先级是第二级。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;通过如构造函数的方式进行显式类型转换，我们称之为函数风格转换functional-style cast。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如&lt;code&gt;新类型 (表达式)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C风格的不要搞混。C风格的是&lt;code&gt;(新类型) 表达式&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C风格和函数风格没什么差别 ---- effective C++ 条款27&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数指针有类型，所以可以进行类型转换&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;记住了，所有类型转换表达式后面的圆括号里面塞的也是表达式！！！&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p = new int(20);
void* pp = static_cast&amp;lt;void*&amp;gt;(p); //p是表达式。
void* pp = static_cast&amp;lt;void*&amp;gt;(int*);//这是啥玩意？int*是类型，不是表达式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float ss = 10.12345;
cout &amp;lt;&amp;lt; ss &amp;lt;&amp;lt; endl; //10.12345
cout &amp;lt;&amp;lt; static_cast&amp;lt;int&amp;gt;(ss) &amp;lt;&amp;lt; endl; //10
cout &amp;lt;&amp;lt; ss &amp;lt;&amp;lt;endl; //10.12345
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由此可见，强制类型转换会生成一个转换类型的临时变量。并不改变原来的对象。&lt;/p&gt;
&lt;p&gt;https://zh.cppreference.com/w/cpp/language/explicit_cast&lt;/p&gt;
&lt;h2&gt;强制类型转换可以进行引用类型的转换&lt;/h2&gt;
&lt;p&gt;其实就是通过原对象生成一个原对象的引用。这个操作合法。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float ss = 10.12345;
float&amp;amp; dd = static_cast&amp;lt;float&amp;amp;&amp;gt;(ss); //生成一个ss的引用。也就是通过ss生成一个新的float&amp;amp;类型的对象。
cout &amp;lt;&amp;lt; dd &amp;lt;&amp;lt;endl; //10.1235
dd = 9.324;
cout &amp;lt;&amp;lt; dd &amp;lt;&amp;lt; endl; //9.324
cout &amp;lt;&amp;lt; ss &amp;lt;&amp;lt; endl; //9.324
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;reinterpret_cast简单介绍&lt;/h1&gt;
&lt;p&gt;需要记住的有两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reinterpret_cast&lt;/code&gt; 表达式不会编译成任何 CPU 指令（除非在整数和指针间转换，或在指针表示依赖它的类型的不明架构上）。它纯粹是一个编译时指令，指示编译器将 &lt;em&gt;表达式&lt;/em&gt; 视为如同具有 &lt;em&gt;新类型&lt;/em&gt; 类型一样处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reinterpret_cast&lt;/code&gt;不能去除CV。去除CV需要使用&lt;code&gt;const_cast&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;关于&lt;code&gt;static_cast&lt;/code&gt; 和 &lt;code&gt;const_cast&lt;/code&gt;之间的区别&lt;/h1&gt;
&lt;h2&gt;&lt;code&gt;static_cast&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static_cast &amp;lt;type-id&amp;gt; (expression)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将expression转换为type-id类型，主要用于非多态类型之间的转换，不提供运行时的检查来确保转换的安全性。主要在以下几种场合中使用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用于类层次结构中，父类和子类之间指针和引用的转换；
&lt;ul&gt;
&lt;li&gt;当进行上行转换，也就是把子类的指针或引用转换成父类表示，这种转换是安全的；&lt;/li&gt;
&lt;li&gt;当进行下行转换，也就是把父类的指针或引用转换成子类表示，这种转换是不安全的，也需要程序员来保证；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;用于基本数据类型之间的转换，如把&lt;code&gt;int&lt;/code&gt;转换成&lt;code&gt;char&lt;/code&gt;，把&lt;code&gt;int&lt;/code&gt;转换成&lt;code&gt;enum&lt;/code&gt;等等，这种转换的安全性需要程序员来保证；
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;static_cast可以转换对象。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;转换对象的前提是：
&lt;ul&gt;
&lt;li&gt;基础类型之间：提到过比如&lt;code&gt;int&lt;/code&gt;换成&lt;code&gt;char&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;**有继承关系之间的对象。且由于是对象，只能进行上行转换。也就是子类对象转为父类对象。父类对象转为子类对象不可能的。**因为只能往小切，不能往大扩。多出来的内存空间不知道用来作什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可被转换（如拥有转换(构造)函数）的对象&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;把void指针转换成目标类型的指针，是及其不安全的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;将一个左值转换为右值引用&lt;/strong&gt;，这是允许的。（std::move的实现)。对于操作右值引用的代码来说，将一个右值引用绑定到一个左值的特性&lt;strong&gt;允许它们截断左值&lt;/strong&gt;。有时候这种截断是安全的&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;static_cast不能移除掉表达式的const、volatile和__unaligned属性。但是可以添加。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;dynamic_cast&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dynamic_cast &amp;lt;type-id&amp;gt; (expression)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将expression转换为type-id类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;dynamic_cast转换仅适用于指针或引用。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;*&lt;em&gt;type-id必须是类的指针、类的引用或者是void &lt;em&gt;；如果type-id是指针类型，那么expression也必须是一个指针；如果type-id是一个引用，那么expression也必须是一个引用。&lt;/em&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;必须满足多态条件（有虚函数，因为依赖RTTI）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不能移除CV限定。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;dynamic_cast主要用于类层次间的上行转换和下行转换，还可以用于类之间的交叉转换。在类层次间进行上行转换时，dynamic_cast和static_cast的效果是一样的；**在进行下行转换时，dynamic_cast具有类型检查的功能，比static_cast更安全。在多态类型之间的转换主要使用dynamic_cast，因为类型提供了运行时信息。**就像我在虚表那一章节说的。虚表头还有一块东西就是RTTI，表明了当前类的类型信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般来说，dynamic_cast 用于下行转换的时候，只能用于有虚函数表的类。因为dynamic_cast依赖于RTTI的type_info，然而这个信息储存在虚函数表的头部。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个&lt;code&gt;dynamic_cast&lt;/code&gt;是怎么用的呢？我们父类指针指向子类对象的时候，限制了指针仅可以读取子类对象的父类部分。我们进行&lt;code&gt;dynamic_cast&lt;/code&gt;的时候，会使用RTTI信息。这个信息是储存在虚函数表的头部。也就是编译器找到了虚函数表指针（子类对象的虚函数表指针一定指向自己的虚函数表，所以RTTI信息类型也是子类），然后问RTTI：”这人想要让我转换成子类，也就是多读点儿子类的东西，你帮我看看我指向的对象到底是不是子类，多读的那部分被正确初始化了没有？合法不合法？“。RTTI看了一眼回答：”哦，你指向的部分实际类型确实是一个子类的类型，因为他&lt;code&gt;new&lt;/code&gt;的时候&lt;code&gt;new&lt;/code&gt;的是一个子类对象。我检查了，都初始化了，也合法，那你就多读点儿吧“。这就是&lt;code&gt;dynamic_cast&lt;/code&gt;和RTTI的联系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有一个实际问题，当我们需要判断该指针实际指向的类型的时候，假如我们加了一堆ifelse然后用dynamic cast判断，这非常慢，尤其是继承非常复杂的时候，效率非常低。需要注意。所以很多引擎的RTTI是自己的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;dynamic_cast&lt;/code&gt; 在传入引用和指针的时候不一样。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用指针的时候，如果可以转换则传回真正的地址。&lt;/li&gt;
&lt;li&gt;如果不可以则会返回0（&lt;code&gt;nullptr&lt;/code&gt;）表示转换失败&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是使用引用的时候却不可以这样。首先，引用不能像指针那样设置为0。设置为0代表着有一个临时对象产生出来，然后这个临时对象初始值为0，然后这个引用成为了这个临时对象的别名。（右值相关，对吧。）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以使用引用的时候，如果可以转换则会把对象引用至正确的子类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但是如果失败，由于引用不可以传0，则会抛出一个&lt;code&gt;bad_cast_exception&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;dynamic_cast 的向上转换是编译时还是运行时&lt;/h3&gt;
&lt;p&gt;向上转换是编译时。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;但是在某些stack overflow 的回答中侧面展示了另一种可能。也就是这依赖于实现
&lt;ul&gt;
&lt;li&gt;https://stackoverflow.com/questions/7687041/dynamic-cast-with-rtti-disabled&lt;/li&gt;
&lt;li&gt;RTTI是可以关闭的，但是关闭之后dynamic_cast会对一些转换标记为非法。这些标记通常会被用于那些向下转换。&lt;/li&gt;
&lt;li&gt;也就是说使用dynamic_cast向上转换的时候，转换关系在编译时是已知的。那么这个时候dynamic_cast不会依赖于RTTI，也就是会被提前到编译时。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;include社群的回答是这样：
&lt;ul&gt;
&lt;li&gt;在使用dynamic_cast进行非向下转换（也就是向上转换和同类型转换的时候），不涉及RTTI，也就是不涉及运行时。编译时就可以做到。&lt;/li&gt;
&lt;li&gt;进行向下转换的时候，必然RTTI也就是必然运行时。&lt;/li&gt;
&lt;li&gt;因为upcast是自动的，不需要任何的显式cast。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;来自&lt;a href=&quot;https://mp.weixin.qq.com/s/X2U5EwCi8sBh4nWNtn_ivg&quot;&gt;CPP开发者&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dynamic_cast&lt;/code&gt;最终会调用&lt;code&gt;libstdc++&lt;/code&gt;中的&lt;code&gt;__dynamic_cast&lt;/code&gt;函数。&lt;/li&gt;
&lt;li&gt;如果是向上转换，编译时即可完成。&lt;/li&gt;
&lt;li&gt;如果是向下转换，需要运行时才可以。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关于更多&lt;code&gt;dynamic_cast&lt;/code&gt;，虚继承，多重虚继承和&lt;code&gt;dynamic_cast&lt;/code&gt;的设计，看&lt;a href=&quot;https://youtu.be/QzJL-8WbpuU&quot;&gt;这里&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;关于&lt;code&gt;const_cast&lt;/code&gt;&lt;/h1&gt;
&lt;h2&gt;给一般读者：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;常量指针（指向常量的指针）被转化成非常量的指针（或逆向），并且仍然指向原来的对象；&lt;/li&gt;
&lt;li&gt;常量引用被转换成非常量的引用（或逆向），并且仍然指向原来的对象；&lt;/li&gt;
&lt;li&gt;不改变原指针/引用的常量状态。&lt;/li&gt;
&lt;li&gt;不可进行类型更改，只能进行CV操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们来看代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using namespace std;
class multiple{
    public:
    multiple():valc(0){}
    multiple(int x, int y, int z):vala(x), valb(y), valc(z){}
    int vala;
    int valb;
    const int valc;

};

int main()
{
    multiple p(1,3,5);
    const multiple* const_ptr = &amp;amp;p; 					//指向常量的指针
    //---------------------------------------------------------------------
    const_ptr = const_cast&amp;lt;multiple*&amp;gt; (const_ptr);		//进行const cast
    const_ptr-&amp;gt;vala = 100;								//不可以, 必须要赋值给新的指针。
    const_cast&amp;lt;multiple*&amp;gt; (const_ptr)-&amp;gt;vala = 100; 		//可以

    //---------------------------------------------------------------------
    multiple* ptr = const_cast&amp;lt;multiple*&amp;gt; (const_ptr);	//赋值给新指针
    ptr-&amp;gt;vala = 10;										//可以
    ptr-&amp;gt;valc = 100;									//不可以。因为valc是常量。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;具体使用场景&lt;/h3&gt;
&lt;h3&gt;1. &lt;strong&gt;一个函数的形参不是const指针/引用，并且编程者事先已经知道在这个函数中不会对参数进行修改，但需要传递的实参却是已经定义好的const对象。为了成功调用这个函数，就需要利用到const_cast在实参传递前对其进行处理，从而使函数能够成功接收这个实参&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Function(int &amp;amp;val){
    cout&amp;lt;&amp;lt;val&amp;lt;&amp;lt;endl;
}
int main(){
    const int value=21;
    Function(value);					//不可以
    Function(const_cast&amp;lt;int&amp;amp;&amp;gt;(value));	//转换为常量引用，可以
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. &lt;strong&gt;如果我们定义了一个非const的变量，却使用了一个指向const对象的指针来指向它，而在程序的某处希望改变这个变量时发现只有指针可用，此时就可以const_cast进行处理&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int value=26;
    const int* const_ptr=&amp;amp;value;
    *const_ptr=3;							//不可以
    int* ptr=const_cast&amp;lt;int*&amp;gt;(const_ptr);	//转换为非常量指针。可以
    *ptr=3;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用const_cast去除const限定的目的绝对不是为了修改它的内容，只是出于无奈&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;给能思考的读者。因为这里我还没有完全弄明白，可能存在错误&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;原因是全局&lt;code&gt;const&lt;/code&gt;在静态区。不能间接修改。因为这个内存段所属的页面的权限是只读的，硬写一定会段错误。而局部&lt;code&gt;const&lt;/code&gt;在栈上。这个内存段所属的页面是可读也可写的，所以可以间接修改。但是修改任何标记为&lt;code&gt;const&lt;/code&gt;的变量都是未定义行为。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们直接看代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class multiple{
    public:
    multiple():valc(0){}
    multiple(int x, int y, int z):vala(x), valb(y), valc(z){}
    int vala;
    int valb;
    const int valc;

};
int main()
{
    const multiple np(2,4,6);					//常量对象
    multiple ucnp = const_cast&amp;lt;multiple&amp;amp;&amp;gt;(np);	//这里下面会解释
    ucnp.vala = 10;
    //---------------------------------------------------------
    cout &amp;lt;&amp;lt; ucnp.vala&amp;lt;&amp;lt; endl;						//输出10
    cout &amp;lt;&amp;lt; ucnp.valb&amp;lt;&amp;lt; endl;						//输出4	
    cout &amp;lt;&amp;lt; ucnp.valc&amp;lt;&amp;lt; endl;						//输出6
    //---------------------------------------------------------
    cout &amp;lt;&amp;lt; np.vala&amp;lt;&amp;lt; endl;						//输出2
    cout &amp;lt;&amp;lt; np.valb&amp;lt;&amp;lt; endl;						//输出4	
    cout &amp;lt;&amp;lt; np.valc&amp;lt;&amp;lt; endl;						//输出6
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这里面我们进行&lt;code&gt;const_cast&lt;/code&gt;的时候返回值并没有使用引用来接受。这是可以的。这里会产生一个临时变量做为中间值传给了结果值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有没有注意到问题，我们的&lt;code&gt;np&lt;/code&gt;不是引用，是个对象。可能无法直观感受，我们接着看&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这句话不知道对不对。来自：https://www.cnblogs.com/dracohan/p/3417842.html&lt;/p&gt;
&lt;p&gt;下面有更狠的。&lt;/p&gt;
&lt;p&gt;我们把&lt;code&gt;ucnp&lt;/code&gt;换成引用。看看结果如何&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
    const multiple np(2,4,6);					//常量对象
    multiple&amp;amp; ucnp = const_cast&amp;lt;multiple&amp;amp;&amp;gt;(np);	//注意这里
    ucnp.vala = 10;
    //---------------------------------------------------------
    cout &amp;lt;&amp;lt; ucnp.vala&amp;lt;&amp;lt; endl;						//输出10
    cout &amp;lt;&amp;lt; ucnp.valb&amp;lt;&amp;lt; endl;						//输出4	
    cout &amp;lt;&amp;lt; ucnp.valc&amp;lt;&amp;lt; endl;						//输出6
    //---------------------------------------------------------
    cout &amp;lt;&amp;lt; np.vala&amp;lt;&amp;lt; endl;						//输出10
    cout &amp;lt;&amp;lt; np.valb&amp;lt;&amp;lt; endl;						//输出4	
    cout &amp;lt;&amp;lt; np.valc&amp;lt;&amp;lt; endl;						//输出6
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;好像没什么问题啊？确实是这样的嘛。常量引用转换成非常量引用，有啥问题？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你再仔细看一眼。&lt;code&gt;np&lt;/code&gt;是什么？是对象啊。这是个常量对象啊，我们修改了常量对象啊！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们可以来一个更明显的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main{
    
    multiple p(1,3,5);
    cout &amp;lt;&amp;lt; p.valc &amp;lt;&amp;lt; endl;					//输出5
    int&amp;amp; wtf = const_cast&amp;lt;int&amp;amp;&amp;gt;(p.valc);	//修改常量。
    wtf = 1000;
    cout &amp;lt;&amp;lt; p.valc &amp;lt;&amp;lt; endl;					//输出1000
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们的&lt;code&gt;valc&lt;/code&gt;是一个&lt;code&gt;const int&lt;/code&gt;。但是这里我们修改了一个对象里的常量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以上操作，编译器没有任何警告。但是这应该是一个未定义行为。需要避免修改任何已经声明为常量的对象。&lt;code&gt;const_cast&lt;/code&gt;只能正确转换顶层（引用，指针）的常量性质，但是无法正确转换底层（引用的本体，指针指向的对象）的常量性质。强行转换会导致未定义行为。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;const_cast&lt;/code&gt;的一些错误使用可能不会让编译器报错。但是它不会做你想要让它做的，&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;const_cast的正确使用方式&lt;/h2&gt;
&lt;p&gt;假设有如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class my_array{
    public:
    	const char&amp;amp; operator[](size_t offset) const{
            return buffer[offset]
        }
    	char&amp;amp; operator[](size_t offset){
            return const_cast&amp;lt;char&amp;amp;&amp;gt;(const_cast&amp;lt;const my_array&amp;amp;&amp;gt;(*this)[offset])
        }
    private:
    	char buffer[10];
};
int main(){
    const my_array c_arr;
    my_array arr;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这种情况下，与其写两个函数，不如让非&lt;code&gt;const&lt;/code&gt;的调用&lt;code&gt;const&lt;/code&gt;。具体过程如下。&lt;/li&gt;
&lt;li&gt;如果是&lt;code&gt;const&lt;/code&gt;对象调用，则正确匹配至&lt;code&gt;const&lt;/code&gt;成员函数。&lt;/li&gt;
&lt;li&gt;如果是非&lt;code&gt;const&lt;/code&gt;对象调用，则正确匹配至非&lt;code&gt;const&lt;/code&gt;成员函数。在非&lt;code&gt;const&lt;/code&gt;成员函数内，我们先把自己cast成&lt;code&gt;const&lt;/code&gt;引用，必须是引用否则就新建对象了。然后这时候调用&lt;code&gt;operator[]&lt;/code&gt;会匹配到&lt;code&gt;const&lt;/code&gt;的成员函数。这时候返回的是&lt;code&gt;const char&amp;amp;&lt;/code&gt;。然后再把这个带&lt;code&gt;const&lt;/code&gt;的cast成&lt;code&gt;char&amp;amp;&lt;/code&gt;。这样做是安全的，因为传入的数组本来就不是&lt;code&gt;const&lt;/code&gt;的，可以修改。我们只是把自己cast成了&lt;code&gt;const&lt;/code&gt;引用。但是本身还是非&lt;code&gt;const&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;C风格cast的过程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-24-%E6%9D%82%E8%AE%B02.assets/QQ%E6%88%AA%E5%9B%BE20230203142712-1675456103483-2.png&quot; alt=&quot;QQ截图20230203142712&quot; /&gt;&lt;/p&gt;
&lt;p&gt;来自&lt;a href=&quot;https://youtu.be/2h2hdRqRIRk&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;explicit 和 隐式类型转换 和 转换构造函数&lt;/h1&gt;
&lt;p&gt;叽叽歪歪一大堆没意思。先看一个关键知识点&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当一个构造函数只有一个参数，而且该参数又不是本类的&lt;code&gt;const&lt;/code&gt;引用时（这里存在不同说法。一说是只要不是本类类型即可），这种构造函数称为转换构造函数，即转换构造函数为单参构造函数的一种，又是类型转换函数的一种&lt;/strong&gt;。这里的只有一个参数可以拆分为，&lt;strong&gt;只有一个形参&lt;/strong&gt;，或，&lt;strong&gt;形参中只有一个没有默认值。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意了。这里提到的是转换构造函数。下面提到的&lt;code&gt;operator&lt;/code&gt;开头的叫用户定义转换函数。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;c++11后单参的限制被放宽了。只要不被explicit修饰的构造函数都可以叫做转换构造函数。（也就是多个参数也可以）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐式声明的及用户定义的非显式复制构造函数与移动构造函数也是转换构造函数。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;explicit只能限制住拷贝初始化，并不能限制直接初始化（杂记1拷贝初始化那段里面有原因）。我们下面提到了四种隐式转换的条件。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;啥意思？&lt;/p&gt;
&lt;h2&gt;只有一个形参&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class A
{
public:
    A(int i):a(i){} 		//单参构造函数
    int getValue()
    {
        return a;
    };
private:
    int a;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    A a = 10;			//case1
    A a(10);			//case2 注意这是直接初始化，不会受到explicit限制。
    A a;				//case3
    a = 10;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;case1中，我们没有临时对象。而是直接把10作为参数传递给类型转换构造函数。使用了拷贝初始化（先调用默认构造，再调用拷贝构造）这虽然使用了&quot;=&quot;，但是实际上我们在创建新对象。创建新对象的时候一律是拷贝构造而不是拷贝赋值。因为对象还不存在&lt;/p&gt;
&lt;p&gt;case2中，我们没有临时对象，使用直接初始化调用默认构造。&lt;/p&gt;
&lt;p&gt;case3中，我们有临时对象。编译器执行 &lt;code&gt;a = 10;&lt;/code&gt;的时候相当于先使用 &lt;code&gt;A(10);&lt;/code&gt;在栈中创建了一个临时对象。然后再调用对象&lt;code&gt;a&lt;/code&gt;的拷贝赋值 &lt;code&gt;a(10)&lt;/code&gt; 给&lt;code&gt;a&lt;/code&gt;初始化（因为对象a已经存在）。然后临时对象销毁。这就是编译器做的隐式转换工作。&lt;/p&gt;
&lt;p&gt;也就是说，如果我们使用了&lt;code&gt;explicit&lt;/code&gt;，这里将会变成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A a;
a = A(10); 	//显式调用构造函数 禁止隐式转换
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多个形参但是只有一个没有默认值&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class A
{
public:
    A(int i, int j = 5):a(i),b(j){} 		//单参构造函数
    int getValue()
    {
        return a;
    };
private:
    int a;
    int b;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理和上面一样。但是有一个知识点：函数默认值假如第N个参数有，那么N后面的参数必须全都有。&lt;/p&gt;
&lt;h2&gt;发生隐式转换的条件&lt;/h2&gt;
&lt;p&gt;注意！四种情况会发生隐式转换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;混合类型的算数运算表达式。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a = 3;
double b = 4.5;
a + b; // a将会被自动转换为double类型，转换的结果和b进行加法操作
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;不同类型的赋值操作。（这里包含拷贝初始化）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a = true ; ( bool 类型被转换为 int 类型)
int * ptr = null;（null被转换为 int *类型）
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;函数参数传值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func(double a);
func(1); // 1被隐式的转换为double类型1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;函数返回值。（此处例子）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;double add( int a, int b)
{
    return a + b;
} //运算的结果会被隐式的转换为double类型返回

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对第四种我们举个例子。看代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class tmp{
    public:
    string _text;
    tmp(string c): _text(c) {}			//单参构造，可以看做转换构造

};
class test{
    public:
    string _text;
    test(string a): _text(a){}		//单参构造，可以看做转换构造
    test(tmp s): _text(s._text){}  //单参构造，可以看做转换构造。此处入参是tmp类型，return一个test类型，可以看成tmp可以转为test
};
class twonumber{
    public:
    test ret(){
        test(tmp(&quot;cde&quot;));
        return(tmp(&quot;abc&quot;)); //此处返回的对象是一个tmp，但是函数头的返回类型却是test。可以运行。这里使用了构造函数的隐式转换。使用了临时对象
        return(test(tmp(&quot;abc&quot;))); //如果设置了explicit，则需要这样进行显式转换。
    }
    //!由于return语句内是隐式转换，所以对应的构造函数如果有explicit关键字则无法进行隐式转换。即便这里看起来像拷贝初始化。

};
int main(){
    twonumber ttt;
    test rrr = ttt.ret();
    cout &amp;lt;&amp;lt; rrr._text &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;隐式类型转换都会创建临时对象&lt;/h3&gt;
&lt;p&gt;我们在上面看到了，为什么下面的类型转换是OK的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test ret(){
        test(tmp(&quot;cde&quot;));
        return(tmp(&quot;abc&quot;)); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为虽然我们要返回&lt;code&gt;test&lt;/code&gt;对象，而且我们实际把一个&lt;code&gt;tmp&lt;/code&gt;对象塞进去了，但是&lt;code&gt;test&lt;/code&gt;类有一个使用&lt;code&gt;tmp&lt;/code&gt;对象为形参的构造函数。编译器此时会调用&lt;code&gt;test&lt;/code&gt;类的形参为&lt;code&gt;tmp&lt;/code&gt;的构造函数，生成一个临时对象。然后再把这个&lt;code&gt;test&lt;/code&gt;类型的临时对象return出去（赋值回去）。&lt;/p&gt;
&lt;p&gt;这里比较特殊是做为函数返回值了。可能有NRV优化。但是正常的隐式转换就是这个样子，调用对应类型和入参的构造函数构造临时对象，再赋值。&lt;/p&gt;
&lt;p&gt;显式类型转换无非就是手动调用了构造函数。&lt;/p&gt;
&lt;h3&gt;为什么引用传参不会发生隐式类型转换从而产生临时对象？&lt;/h3&gt;
&lt;p&gt;假设我们有一个函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void changesomething(string&amp;amp; s){
    //改变一些东西
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果此时有隐式类型转换，则一定会有临时对象。那么我们修改的其实是那个临时对象。传入的那个字符串并没有被修改。所以这会引起错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以c++禁止为非常量引用产生临时对象，而常量引用没所谓。以为常量引用保证了对象不会被修改。注意这个和拷贝没啥关系&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;说到底，我们啥时候想禁用呢？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;当类中同时存在“non-explicit-one-argument 构造函数和类型转换函数的时候。有可能产生二义性。所以需要对“non-explicit-one-argument构造函数前加explicit防止编译器隐式自动转换类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;explicit可以理解为，在拷贝初始化的时候，尝试去对等号右侧对象进行一个隐式转换以满足等号左侧的类型要求。但是explicit会让隐式转换序列中，忽略掉explicit标注的类型转换函数（转换构造函数或用户定义的类型转换函数）。所以他会找不到对应的转换方式所以约等于禁止隐式转换了。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;也就是说，如果加了explicit，使用拷贝初始化的时候我们就必须用显式类型转换。比如C风格cast或c++风格的cast或显式调用构造函数进行直接初始化。
&lt;ul&gt;
&lt;li&gt;显式地调用构造函数进行直接初始化实际上是显式类型转换的一种。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;再次重申，explicit只限制拷贝初始化，不限制直接初始化。而且对象类型为本类类型的时候不限制。因为如果是本类类型不涉及任何类型转换。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A a(5);		//可以。直接初始化
A a = 5;	//不可以，拷贝初始化。
A b(a);		//可以。对象类型为本类，不属于类型转换。不发生隐式转换。
A b = a;	//可以。对象类型为本类，不属于类型转换。不发生隐式转换。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可以进行多种构造函数的搭配自由使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class obj{
    public:
        obj(){};
        obj(int a):val1(a){};
        explicit obj(int a, int b):val1(a), val2(b){};
    int val1;
    int val2;
};

int main(){
    obj t1; //可以，有默认构造函数
    obj t2(10,20); //可以，explicit只能限制拷贝初始化。这里是直接初始化。
    obj t3 = (10,20);//不可以，禁止隐式类型转换。
    obj t4 = obj(10,20);//可以，显式类型转换。
    obj t5 = (20); //可以。因为这个构造函数没有被explicit修饰
    
    vector&amp;lt;obj&amp;gt; myvec{obj(10,20), (20)}; //可以这样搭配使用。

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;所以explicit卡在哪儿了？卡在隐式类型转换所需的构造等号右侧的临时对象了。&lt;/h2&gt;
&lt;p&gt;我们上面提到了如果其他对象的类型不是目标类型或从目标类型派生，那就需要找能转换的转换序列（包括转换构造函数和用户定义转换函数）。&lt;strong&gt;说人话就是我们要使用等号右面的这个东西，构建出一个等号左边的类型的临时对象。但是在这个临时对象的构造中&lt;/strong&gt;（转换序列的查找中），&lt;strong&gt;忽略掉所有带&lt;code&gt;explicit&lt;/code&gt;声明符的可用函数。这时候自然就防止了隐式类型转换。因为一般情况下，我们期望的是提供一系列参数，然后进行对象构造。按理说直接初始化提供的参数和拷贝初始化提供的参数是相同的。因为在拷贝初始化临时对象后，会调用拷贝构造或移动构造来对左侧的对象进行初始化。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是，它有时候想的和你不太一样。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explicit myobj(int x):val(new int(x)){ //explicit
    cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
}
myobj(int x, int y = 20):val(new int(x)){ //带默认值
    cout &amp;lt;&amp;lt;&quot;const with default value&quot; &amp;lt;&amp;lt; endl;
}
myobj b = 10; //可以

/*
const with default value
mv
dest
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这个时候为啥可以呢？因为尽管通过&lt;code&gt;10&lt;/code&gt;来构造&lt;code&gt;myobj&lt;/code&gt;临时对象的时候，第一个匹配的构造函数是&lt;code&gt;explicit&lt;/code&gt;不能用，但是第二个可以用。所以此时依旧可以拷贝初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;对于类来说，直接在类型后面加上括号，就是调用构造函数&lt;/h1&gt;
&lt;p&gt;类型后面+名字再+括号只不过是给了个名字而已。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class p{
    public:
    int a;
    int b;
    p(){}
    p(int x, int y):a(x),b(y){}

};
int main(){
    p t1;
    t1.a(5); //这是啥东西？构造函数怎么能构造出来对象的变量而不是对象本身呢？怎么可以通过构造函数给变量赋值？
    t1.a = 5;//对了
    
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;类型转换函数 （用户定义转换函数）&lt;/h1&gt;
&lt;p&gt;我们提到过：&lt;strong&gt;当一个构造函数只有一个参数，而且该参数又不是本类的&lt;code&gt;const&lt;/code&gt;引用时（这里存在不同说法。一说是只要不是本类类型即可），这种构造函数称为转换构造函数，即转换构造函数为单参构造函数的一种，又是类型转换函数的一种&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所以类型转函数还有一个单独的函数。也就是&lt;code&gt;operator type（）&lt;/code&gt;。他的作用是把一个类类型转换成另一个类类型。也叫类型转换函数或用户定义转换函数。注意和转换构造函数区别开。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它的目的是让我们的这个类对象可以转换成满足目标类对象的构造函数的样子。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意，用户定义转换函数依旧可以被&lt;code&gt;explicit&lt;/code&gt;修饰。具体作用和构造函数一致。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果用户定义转换函数被&lt;code&gt;explicit&lt;/code&gt;修饰，则该用户定义转换函数不是用户定义转换，不被隐式类型转换考虑&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;explicit&lt;/code&gt;修饰用户定义转换函数后，就需要使用如&lt;code&gt;static_cast&lt;/code&gt;的形式显式触发类型转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一般来说，转换构造函数的目的是把其他类转换为本类。用户定义转换函数的目的是把本类转换为其他类。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基础语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class 源类{
    源类(目标类){
        //这种转换构造函数的目的是把目标类转为源类。也就是把其他类转为本类
    }
    operator 目标类(){
        //这种用户定义转换函数的目的是把源类转为目标类。也就是把本类转为其他类。
        目标类 obj;
        //根据需要进行从源类型到目标类型的转换
        
        return obj;
    }
}
class Type{
    
    operator OtherType(){
        
        OtherType obj;
        //....
        
        return obj;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;此处，我们的目的是把Type转换为OtherType&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以可以有这样的类型转换调用（隐式）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    OtherType obj = Type();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来看进阶例子。此处是把other转换为test&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:

    int val_1;
    int val_2;
    test(int x = 4, int y = 5):val_1(x), val_2(y){}
};

class other{
    public:
    int val1;
    int val2;
    other(int x = 10, int y = 100):val1(x), val2(y){}
    operator test(){   //用来把other转换为test。
        test obj;
        obj.val_1 = val1;
        obj.val_2 = val2;
        return obj;
    }
};


int main(){
    other O;
    test T;
    cout &amp;lt;&amp;lt; T.val_1 &amp;lt;&amp;lt; endl;
    T = O;              //other转化为test
    T = (test)O;        //显式
    
    test obj1(other()); //不行，语法不对，会被当成函数。必须要用花括号
    test obj1(other{}); // OK 因为有类型转换。
    
    cout &amp;lt;&amp;lt; T.val_1 &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**类型转换函数会为我们提供隐式类型转换。所以会产生临时对象。**其实代码中就体现出来了。因为我们单独创建了一个对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意这里我们&lt;code&gt;test&lt;/code&gt;类没有写自己的拷贝构造。所以编译器默认生成了。当我们执行&lt;code&gt;test obj1(other{});&lt;/code&gt;的时候，编译器先会想办法把&lt;code&gt;other&lt;/code&gt;弄成满足&lt;code&gt;test&lt;/code&gt;构造函数的要求的类型。这里我们&lt;code&gt;test&lt;/code&gt;没有对应的构造函数，所以会开始查看&lt;code&gt;other&lt;/code&gt;的转换函数。转换函数内部先生成&lt;code&gt;test&lt;/code&gt;类对象（构造），然后拷贝出来（拷贝构造）然后再使用拷贝出来的&lt;code&gt;test&lt;/code&gt;临时对象去调用拷贝构造去构造&lt;code&gt;obj1&lt;/code&gt;（第二次拷贝构造）。[没有编译器优化的时候]&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户定义转换函数内并不一定需要返回一个目标类对象。也可以是返回一个可以构造目标类对象的参数。（满足目标类的构造函数）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class middle{
    public:
    int val_a;
    int val_b;
    middle(int a, int b):val_a(a), val_b(b){}
};

class test{
    public:

    int val_1;
    int val_2;
    test(int x = 4, int y = 5):val_1(x), val_2(y){}
    test(const middle&amp;amp; a):val_1(a.val_a), val_2(a.val_b){}
};

class other{
    public:
    int val1;
    int val2;
    other(int x = 10, int y = 100):val1(x), val2(y){}
    // operator test(){   //不能有重载，会有二义性。
    //     test obj;
    //     obj.val_1 = val1;
    //     obj.val_2 = val2;
    //     return obj;
    // }
    operator test(){   //用来把other转换为test。
        middle obj(val1, val2); //因为test类可以接受一个middle对象来进行构造。所以这样也可以。
        return obj;
    }
};

int main(){
    test t_obj = other(1,2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;test&lt;/code&gt;类可以接受一个&lt;code&gt;middle&lt;/code&gt;对象来进行构造。所以这样也可以。&lt;/p&gt;
&lt;p&gt;当然了最好的写法是和返回语句写到一起。这里是为了看起来清晰。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;因为核心是让我们源类对象转换成满足目标类对象的构造函数的样子。所以甚至这样都是可以的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:

    int val_1;
    int val_2;
    test(int x = 4, int y = 5):val_1(x), val_2(y){}
};

class other{
    public:
    int val1;
    int val2;
    other(int x = 10, int y = 100):val1(x), val2(y){}
    operator test(){   //用来把other转换为test。
        return val1;
    }
};

int main(){
    test t_obj = other();
    cout &amp;lt;&amp;lt; t_obj.val_1 &amp;lt;&amp;lt; t_obj.val_2 &amp;lt;&amp;lt; endl; //10 5

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;test&lt;/code&gt;类对象的构造函数都含有默认值。所以我甚至在转换函数内只返回了第一个参数的值。这个值随后会被当做第一个参数放入&lt;code&gt;test&lt;/code&gt;的构造函数。所以最后输出的结果会是&lt;code&gt;10，5&lt;/code&gt;。这个&lt;code&gt;10&lt;/code&gt;来自&lt;code&gt;other&lt;/code&gt;对象构造函数的默认值。这个&lt;code&gt;5&lt;/code&gt;来自&lt;code&gt;test&lt;/code&gt;对象构造函数的默认值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户自定义的转换函数还可以是虚函数，但是只有从基类的引用或指针进行派发的时候才会调用子类实现的转换函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct D;
struct B {
    virtual operator D() = 0;
};
struct D : B
{
    operator D() override { 
        cout &amp;lt;&amp;lt; &quot;D&quot; &amp;lt;&amp;lt; endl;
        return D(); 
    }
};
 
int main()
{
    D obj;
    D obj2 = obj; // 不调用 D::operator D()
    B* br = new D;
    D obj3 = *br; // 通过多态调用 D::operator D() 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户定义转换函数不能是类的静态成员函数。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;类型转换函数和void&lt;/h2&gt;
&lt;p&gt;我们看下面代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

struct S {
  operator void() {
    std::cout &amp;lt;&amp;lt; &quot;F&quot;;
  }
};

int main() {
  S s;
  (void)s; // 无输出
  static_cast&amp;lt;void&amp;gt;(s); // 无输出
  s.operator void(); //输出 F
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么前两个无输出？因为标准说了:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可以定义转换到（可有 cv 限定的）自身类（或它的引用），转换到它自身类的基类（或它的引用），和转换到类型 void 的转换函数，&lt;strong&gt;但它无法作为转换序列的一部分执行&lt;/strong&gt;，除非在某些情况下通过&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/virtual&quot;&gt;虚&lt;/a&gt;派发来执行：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，用户定义转换函数永远不会被用于转换一个类型到它的自身类，自身类的基类，和void。如果想这么用，必须手动调用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://timsong-cpp.github.io/cppwp/std23/class.conv.fct#4&quot;&gt;[class.conv.fct]4&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A conversion function is never used to convert a (possibly cv-qualified) object to the (possibly cv-qualified) same object type (or a reference to it), to a (possibly cv-qualified) base class of that type (or a reference to it), or to &lt;em&gt;cv&lt;/em&gt; &lt;code&gt;void&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;来自&lt;a href=&quot;https://cppquiz.org/quiz/question/318&quot;&gt;CppQuiz&lt;/a&gt; 和 这篇&lt;a href=&quot;https://stackoverflow.com/questions/4031228/why-is-operator-void-not-invoked-with-cast-syntax&quot;&gt;StackOverflow&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;显式类型转换和隐式类型转换的定义&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/explicit_cast&quot;&gt;显式类型转换&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/implicit_conversion&quot;&gt;隐式类型转换&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;##　显式类型转换定义&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果对于某个虚设变量 &lt;code&gt;temp&lt;/code&gt; 存在良构声明 &lt;code&gt;目标类型 temp(表达式 )&lt;/code&gt;;，那么&lt;em&gt;表达式&lt;/em&gt; ﻿可以显式转换到&lt;em&gt;目标类型&lt;/em&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;隐式类型转换定义&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;当且仅当 &lt;code&gt;T2&lt;/code&gt; 能从表达式 &lt;code&gt;e&lt;/code&gt; &lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/copy_initialization&quot;&gt;复制初始化&lt;/a&gt;，即对于虚设的临时对象 &lt;code&gt;t&lt;/code&gt;，声明 &lt;code&gt;T2 t = e&lt;/code&gt;; 良构（能编译）时，称表达式 &lt;code&gt;e&lt;/code&gt; 可隐式转换到 &lt;code&gt;T2&lt;/code&gt;。注意这与&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/direct_initialization&quot;&gt;直接初始化&lt;/a&gt;（&lt;code&gt;T2 t(e)&lt;/code&gt;）不同，其中还会额外考虑显式构造函数和转换函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;隐式转换序列&lt;/h1&gt;
&lt;h2&gt;转换构造函数&lt;/h2&gt;
&lt;p&gt;上文explicit提到过&lt;/p&gt;
&lt;h2&gt;类型转换函数&lt;/h2&gt;
&lt;p&gt;就在上面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于需要进行隐式转换的上下文，编译器会生成一个隐式转换序列：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;零个或一个由标准转换规则组成的标准转换序列，叫做初始标准转换序列&lt;/li&gt;
&lt;li&gt;零个或一个由用户自定义的转换规则构成的用户定义转换序列&lt;/li&gt;
&lt;li&gt;零个或一个由标准转换规则组成的标准转换序列，叫做第二标准转换序列&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;对于隐式转换发生在构造函数的参数上时，第二标准转换序列不存在。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;初始标准转换序列：&lt;/h2&gt;
&lt;p&gt;初始标准转换序列很好理解，在&lt;strong&gt;调用用户自定义转换前&lt;/strong&gt;先把值的类型处理好，比如加上cv限定符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {};
struct B {
    operator A() const;
};
 
const B b;
const A &amp;amp;a = b;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始标准转换序列会把值先转换成适当的形式以供用户转换序列使用，在这里&lt;code&gt;operator A() const&lt;/code&gt;希望传进来的this是&lt;code&gt;const B*&lt;/code&gt;(this)类型的，而对b直接取地址只能得到&lt;code&gt;B*&lt;/code&gt;，正好标准转换规则里有添加底层const的规则，所以适用。&lt;/p&gt;
&lt;p&gt;如果值的类型正好，不需要任何预处理，那么初始标准转换序列不会做任何多余的操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果第一步还不能转换出合适的类型，那么就会进入用户定义转换序列。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;用户定义转换序列&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用户定义的转换函数在隐式转换的第二阶段被调用，第二阶段由&lt;strong&gt;零个或一个转换构造函数&lt;/strong&gt;或&lt;strong&gt;零个或一个用户定义转换函数&lt;/strong&gt;构成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果转换函数和转换构造函数都能用于进行某个用户定义转换&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果类型是直接初始化（参考杂记1），那么只会调用转换构造函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;**如果是复制初始化或者引用绑定，那么转换构造函数和用户定义转换函数会根据重载决议确定使用谁(忽略掉explicit标注的转换函数）。**另外如果转换函数不是&lt;code&gt;const&lt;/code&gt;限定的，那么在两者都是可行函数时优先选择转换函数，比如&lt;code&gt;operator A();&lt;/code&gt;这样的，否则会报错有歧义（GCC 10.2上测试显示有歧义的时候会选择转换构造函数，clang++11.0和标准描述一致）。这也是我们复习了几种初始化有什么区别的原因，因为类的构造形式不同结果也可能会不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;选择好一个规则后就可以进入下一步了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果是在构造函数的参数上，那么隐式转换到此就结束了。除此之外我们需要进行第三步。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;第二标准转换序列&lt;/h2&gt;
&lt;p&gt;这是针对用户转换序列处理后的值的类型做一些善后工作。之所以不允许在构造函数的参数上执行这一步是因为防止过度转换后和用户转换规则产生循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A
{
    operator int() const;
};
 
A a;
bool b = a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里a只能转换成int，而为了偷懒我们直接把a隐式转换成bool，问题来了，初始标准转换序列把&lt;code&gt;A*&lt;/code&gt;转换成了&lt;code&gt;const A*&lt;/code&gt;（作为this，类方法的隐式参数），用户转换序列把&lt;code&gt;const A*&lt;/code&gt;转换为了int，int和bool是完全不同的类型，怎么办呢？&lt;/p&gt;
&lt;p&gt;这就用上第二标准转换序列了，这里是数值转换，int转成bool。&lt;/p&gt;
&lt;p&gt;不过上面只是个例子，请不要这么写，因为在实际代码中会出现问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
struct SmartPointer {
    //...
    T *ptr = nullptr;
    operator bool() {
        return ptr != nullptr;
    }
 
    T&amp;amp; operator*() {
        return *ptr;
    }
};
 
auto ptr = get_smart_pointer();
if (ptr) {
    // ptr 是int*的包装，现在我们想取得ptr指向的值
    int value = ptr;
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码不会有任何编译错误，然而它将引发严重的运行时错误。&lt;/p&gt;
&lt;p&gt;为什么呢？因为如注释所说我们想取得指针指向的值，&lt;strong&gt;然而我们忘记解引用了&lt;/strong&gt;！实际上因为要转换成int，隐式转换序列里是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始标准转换序列 -----&amp;gt; 当前类型已经满足调用用户转换序列的要求了，什么都不做&lt;/li&gt;
&lt;li&gt;用户定义转换序列 -----&amp;gt; 和int最接近的有转换关系的类型只有bool了，调用这个&lt;/li&gt;
&lt;li&gt;第二标准转换序列 -----&amp;gt; 得到了bool，目标的int，正好有规则可用，进行转换&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此你的value只会有两种值，0和1。这就是隐式转换带来的第一个大坑，而上面代码反应出的问题叫做“安全bool（safe bool）”问题。&lt;/p&gt;
&lt;p&gt;我们可以用&lt;code&gt;explicit&lt;/code&gt;把它踢出转换序列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
struct SmartPointer {
    //...
    T *ptr = nullptr;
    explicit operator bool() {
        return ptr != nullptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://www.cnblogs.com/apocelipes/p/14415033.html#%E4%BB%80%E4%B9%88%E6%98%AF%E9%9A%90%E5%BC%8F%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意引用和隐式类型转换的坑。查看下面的引用和指针的区别。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;临时对象：&lt;/h1&gt;
&lt;p&gt;一般来说临时对象有三种情况产生：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;以值的方式（包括常量引用）给函数传参&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐式类型转换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数返回一个对象时&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;析构函数和构造函数中的异常&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;构造函数中可以抛出异常，但是抛出的异常会导致析构函数无法被调用。因为被视为对象没有成功构造。会存在内存泄漏风险&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在析构函数中是可以但是是极度极度不推荐抛出异常的&lt;/strong&gt;。原因在《More Effective C++》中提到两个：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（1）如果析构函数抛出异常，则异常点之后的程序不会执行，如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源，则这些动作不会执行，会造成诸如资源泄漏的问题。&lt;/p&gt;
&lt;p&gt;（2）通常异常发生时，c++的异常处理机制在异常的传播过程中会进行栈展开（stack-unwinding），因发生异常而逐步退出复合语句和函数定义的过程，被称为栈展开。在栈展开的过程中就会调用已经在栈构造好的对象的析构函数来释放资源，此时若其他析构函数本身也抛出异常，则前一个异常尚未处理，又有新的异常，会造成程序崩溃。&lt;/p&gt;
&lt;h1&gt;inline与宏区别?&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;inline：
&lt;ul&gt;
&lt;li&gt;编译期进行展开。&lt;/li&gt;
&lt;li&gt;是函数&lt;/li&gt;
&lt;li&gt;嵌入到目标代码&lt;/li&gt;
&lt;li&gt;可以进行安全检查，语句正确等编译功能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;宏：
&lt;ul&gt;
&lt;li&gt;预处理期进行替换。&lt;/li&gt;
&lt;li&gt;不是函数&lt;/li&gt;
&lt;li&gt;只是文本替换&lt;/li&gt;
&lt;li&gt;不能进行安全检查等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;inline函数与普通函数区别？&lt;/h1&gt;
&lt;p&gt;当普通函数在被调用时，系统首先跳跃到该函数的入口地址，执行函数体，执行完成后，再返回到函数调用的地方，函数始终只有一份。&lt;/p&gt;
&lt;p&gt;而内联函数则不需要进行一个寻址的过程，当执行到内联函数时，此函数展开（很类似宏的使用），如果在 N 处调用了此内联函数，则此函数就会有 N 个代码段的拷贝。空间换时间。&lt;/p&gt;
</content:encoded></item><item><title>算法相关</title><link>https://sterben-01.github.io/posts/%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86/</guid><pubDate>Wed, 20 Jul 2022 06:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;时间复杂度&lt;/h1&gt;
&lt;p&gt;时间复杂度主要就是看基本操作是啥，然后基本操作要进行几次&lt;/p&gt;
&lt;h2&gt;O1&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这代码操作都是执行一次。&lt;/p&gt;
&lt;h2&gt;On&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;for(i=1; i&amp;lt;=n; ++i)
{
   j = i;
   j++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们能看到，for循环的代码会执行n次。所以消耗的时间是随着n变化而变化。所以是On&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二叉树的遍历就是On，因为每个节点访问一次&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;O logN&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int i = 1;
while(i&amp;lt;n)
{
    i = i * 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到，while循环的i虽然随着n变化而变化，但是每次i是指数增长。所以复杂度也就是对数。&lt;/p&gt;
&lt;h2&gt;O NlogN&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;for(m=1; m&amp;lt;n; m++)
{
    i = 1;
    while(i&amp;lt;n)
    {
        i = i * 2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到，外层for循环时间随着n改变，所以是On。内层i每次是指数增长，所以是对数复杂度。所以是NlogN&lt;/p&gt;
&lt;h2&gt;O n^2&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;for(x=1; i&amp;lt;=n; x++)
{
   for(i=1; i&amp;lt;=n; i++)
    {
       j = i;
       j++;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;俩循环嵌套&lt;/p&gt;
&lt;h1&gt;空间复杂度&lt;/h1&gt;
&lt;p&gt;就是看开辟空间了没有。&lt;/p&gt;
&lt;p&gt;比如二叉树遍历，如果要记录节点，所以开辟额外空间的空间复杂度是N。递归调用栈深度是树的高度。树的高度是logn因为每一层节点翻倍。&lt;/p&gt;
&lt;p&gt;所以如果包含额外空间，则递归调用遍历二叉树节点并储存的话是O N+logN，（N是保存节点，logN是调用栈深度, 保存节点的数量和栈深度没有关联，所以不是nlogn, 而是n+logn）&lt;/p&gt;
&lt;p&gt;如果不看这个储存的节点的空间，就是单纯的O logN&lt;/p&gt;
&lt;h1&gt;堆排序&lt;/h1&gt;
&lt;p&gt;就是排序一个堆。&lt;/p&gt;
&lt;p&gt;最大堆和最小堆只保证父节点比子节点大或者小，他不保证子节点的绝对顺序。&lt;/p&gt;
&lt;p&gt;把数据组织成一个最大堆或者最小堆。然后整个堆都是待排序状态。&lt;/p&gt;
&lt;p&gt;这时候把堆顶的数（一定是最大的或者是最小的）和最后一个节点的数交换(或者是直接拿出来)。&lt;/p&gt;
&lt;p&gt;交换后整个堆的顺序会被破坏，所以需要重新调整以符合最大堆/最小堆定义。然后重复交换-》拿出-》排序的过程。&lt;/p&gt;
&lt;h1&gt;STL的sort&lt;/h1&gt;
&lt;p&gt;复合排序&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在数据量很大时采用正常的快速排序，此时效率为O(logN)。&lt;/li&gt;
&lt;li&gt;一旦分段后的数据量小于某个阈值，就改用插入排序，因为此时这个分段是基本有序的，这时效率可达O(N)。&lt;/li&gt;
&lt;li&gt;在递归过程中，如果递归层次过深，分割行为有恶化倾向时，它能够自动侦测出来，使用堆排序来处理，在此情况下，使其效率维持在堆排序的O(N logN)，但这又比一开始使用堆排序好。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://feihu.me/blog/2014/sgi-std-sort/#stdsort%E9%80%82%E5%90%88%E5%93%AA%E4%BA%9B%E5%AE%B9%E5%99%A8&lt;/p&gt;
&lt;h1&gt;排序算法稳定性&lt;/h1&gt;
&lt;h2&gt;稳定的算法：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;插入排序&lt;/li&gt;
&lt;li&gt;冒泡排序&lt;/li&gt;
&lt;li&gt;归并排序&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;快速排序&lt;/h2&gt;
&lt;p&gt;找到一个pivot，比如数组中间的值。从左往右找，在左边找到第一个比中间的值大的，在右边找到第一个比中间值小的，他俩交换。直到左侧的都比中间值小，右侧的都比中间值大。然后进行递归分别处理左侧的和右侧的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意我们写法的递归操作，是&lt;code&gt;start,right&lt;/code&gt; 和 &lt;code&gt;left, end&lt;/code&gt;。因为我们一定会走到两个下标相等的部分。也就是left = right。也就是刚好处于pivot的位置。然后我们会把left+1， right - 1。这样一来，right就跑到了左边，left就跑到了右边。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-11-%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86.assets/unknown%20(1).png&quot; alt=&quot;unknown (1)&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;归并排序&lt;/h2&gt;
&lt;p&gt;归并排序的过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;必须创建一个额外空间的数组。这也是归并稳定的原因&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;递归把数组切成两个的大小&lt;/li&gt;
&lt;li&gt;然后对这个大小为2的数组进行排序&lt;/li&gt;
&lt;li&gt;这个数组排序完成后，对下一个大小为2的数组进行排序&lt;/li&gt;
&lt;li&gt;然后对这两个大小为2的数组进行排序&lt;/li&gt;
&lt;li&gt;.....&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体的排序过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们有我们的额外数组。&lt;/li&gt;
&lt;li&gt;在原数组中选择中间值。&lt;/li&gt;
&lt;li&gt;使用双指针法，两个指针分别指向第一个元素和中间值后的第一个元素。&lt;/li&gt;
&lt;li&gt;如果第一个元素比中间值后的第一个元素大，那么把这个元素放到额外数组。反之亦然。&lt;/li&gt;
&lt;li&gt;如果原数组当中的左半部分和右半部分（中间值后面的部分）有未遍历完的，则原样复制到额外数组。&lt;/li&gt;
&lt;li&gt;把额外数组的值赋值回原数组。
&lt;ul&gt;
&lt;li&gt;此处必须格外注意。把额外数组赋值回原数组，起始位置和结束为止是函数的入参。不是从0开始。因为接下来排序整个原数组的后半部分的时候，前半部分已经是相对有序的了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;self.merge_sort(a, 0, 7, temparr)                           |1

        self.merge_sort(a, 0, 3, temparr)  # left               |2
        
            self.merge_sort(a, 0, 1, temparr)  # left               |3
                self.merge_sort(a, 0, 0, temparr)  # left               |4
                self.merge_sort(a, 1, 1, temparr)  # right              |4
                self.sort(a, 0, 1, temparr)  # sort                     |4
                
            self.merge_sort(a, 2, 3, temparr)  # right              |3
                self.merge_sort(a, 2, 2, temparr)  # left               |4
                self.merge_sort(a, 3, 3, temparr)  # right              |4
                self.sort(a, 2, 3, temparr)  # sort                     |4
                
            self.sort(a, 0, 3, temparr)  # sort                     |3
            
        self.merge_sort(a, 4, 7, temparr)  # right              |2
            self.merge_sort(a, 4, 5, temparr)  # left               |3
                self.merge_sort(a, 4, 4, temparr)  # left               |4
                self.merge_sort(a, 5, 5, temparr)  # right              |4
                self.sort(a, 4, 5, temparr)  # sort                     |4
                
            self.merge_sort(a, 6, 7, temparr)  # right              |3
                self.merge_sort(a, 6, 6, temparr)  # left               |4
                self.merge_sort(a, 7, 7, temparr)  # right              |4
                self.sort(a, 6, 7, temparr)  # sort                     |4
                
            self.sort(a, 4, 7, temparr)  # sort                     |3
            
        self.sort(a, 0, 7, temparr)  # sort                     |2

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-11-%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86.assets/unknown.png&quot; alt=&quot;unknown&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;二分查找&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;s&gt;&lt;code&gt;left=mid&lt;/code&gt;和&lt;code&gt;right=mid&lt;/code&gt;不能一起出现在代码里，会死循环&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;&lt;code&gt;left=mid+1&lt;/code&gt;和&lt;code&gt;right=mid-1&lt;/code&gt;不能一起出现在代码里，会提前跳出&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;s&gt;所以&lt;code&gt;left&lt;/code&gt;和&lt;code&gt;right&lt;/code&gt;的更新就是一个带1一个不带1。取决于&lt;code&gt;mid&lt;/code&gt;怎么取整。&lt;/s&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;s&gt;如果&lt;code&gt;mid&lt;/code&gt;靠&lt;code&gt;right&lt;/code&gt;近，则&lt;code&gt;right&lt;/code&gt;带1。&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;如果&lt;code&gt;mid&lt;/code&gt;靠&lt;code&gt;left&lt;/code&gt;近，则&lt;code&gt;left&lt;/code&gt;带1。&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;靠哪边近的意思是，如果left是4，right是5。如果mid是4那就是left近。&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;回溯&lt;/h1&gt;
&lt;p&gt;针对组合问题，没有重复元素，不可取重复的用startindex。 有重复元素且不可取重复的用startindex+used 针对排列问题，不需要startindex。比如全排列。12和21是两种排列。所以不需要startindex。used数组是用来标记同一树枝用过的数字的。 举例：123这个集合，先取了2，因为没有startindex，所以我们还是从1开始取。但是2已经被拿走了，所以2跳过。所以排列问题的used数组去重是判断是否为true。 但是如果排列问题有重复元素，那就是used数组既要判断false也要判断true。也就是树枝和树层都要去重 &amp;amp;树枝去重看used[i] = true 的原因是，我们没有startindex。所以每次都是从开头开始。如果没有树枝去重的话， 112这个组合的第一个1每次都会被选出来。就会出现111这种问题。 所以使用了used[i]可以在一定程度上替代startindex。但是我们不用startindex的原因是，我们从2开始可以选回之前的这个1。因为我们used树枝的限制仅限于单个for循环也就是单个路径。不限制整个路径。 &amp;amp;used树层去重是限制的起始点的值重复。used树枝去重限制的是路径上的选择重复。  for循环控制横向遍历。递归控制纵向遍历。所以我们在for里面操作startindex的时候是改的i而不是startindex。&lt;/p&gt;
&lt;h1&gt;DP&lt;/h1&gt;
&lt;p&gt;如果我们是比较类，也就是看放和不放哪个大/小... 的时候，我们会在后面加&lt;code&gt;nums[i] dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]] + nums[i]) &lt;/code&gt; 动态规划的核心就是每个物品可以拿也可以不拿。 这里 &lt;code&gt;dp[i-1][j] &lt;/code&gt;的意思是 不拿当前i物品的情况。  这里 &lt;code&gt;dp[i-1][j-nums[i]] + nums[i]&lt;/code&gt;的意思是 我要是拿了物品&lt;code&gt;i&lt;/code&gt;我就一定得拿。 所以我先把&lt;code&gt;i&lt;/code&gt;放进去，把价值加上去这是&lt;code&gt;+nums[i]&lt;/code&gt;。 &lt;code&gt;i&lt;/code&gt;已经拿了 所以得从&lt;code&gt;i-1&lt;/code&gt;看 所以是&lt;code&gt;dp[i-1] &lt;/code&gt;然后算放了&lt;code&gt;i&lt;/code&gt;之后我背包还剩下多大，我就找到重量为&lt;code&gt;j-nums[i]&lt;/code&gt;时候最大的价值是多少  然后二者取最大值 这个&lt;code&gt;nums[i] &lt;/code&gt;如果是求值那就是这个东西的价值那就是&lt;code&gt;value[i]&lt;/code&gt;或者就是&lt;code&gt;nums[i]&lt;/code&gt;本身。如果是求个数 那就是&lt;code&gt;+1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果是累加类 也就是求组合类 求有多少种组合或者方法 比如 518 和 494 那就不加&lt;code&gt;nums[i] &lt;/code&gt;二维不要用&lt;code&gt;+=&lt;/code&gt; 还是按照&lt;code&gt;+&lt;/code&gt;的老方法 例子：&lt;code&gt;dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]&lt;/code&gt;这里&lt;code&gt;dp[i-1][j]&lt;/code&gt;是我如果不拿的情况&lt;code&gt;dp[i-1][j-nums[i]]&lt;/code&gt;的意思是&lt;code&gt; i&lt;/code&gt;已经拿了 所以得从&lt;code&gt;i-1&lt;/code&gt;看 所以是&lt;code&gt;dp[i-1] &lt;/code&gt;然后我因为是求方法数量 不需要具体&lt;code&gt;nums[i]&lt;/code&gt;的数值。所以没有&lt;code&gt;+nums[i] &lt;/code&gt;但是我依旧还是看当我一定要装&lt;code&gt;i&lt;/code&gt;的时候，装了&lt;code&gt;i&lt;/code&gt;之后背包还剩多大。我要去看重量为&lt;code&gt;j-nums[i]&lt;/code&gt;的时候的最大值是多少。然后相加就可以&lt;/p&gt;
&lt;h1&gt;防止加法溢出&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;int mid = (left + right) / 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;替换为&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int mid = left + (right - left) / 2&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;二叉树&lt;/h1&gt;
&lt;h2&gt;二叉搜索树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;二叉搜索树的中序遍历是有序的&lt;/li&gt;
&lt;li&gt;二叉搜索树的左子树上所有结点的值均小于它的根结点的值以及右子树上所有结点的值均大于它的根结点的值&lt;/li&gt;
&lt;li&gt;在最好的情况下，二叉搜索树的查找效率比较高，是 &lt;code&gt;O(logn)&lt;/code&gt;，其访问性能近似于二分法；&lt;/li&gt;
&lt;li&gt;但最差时候会是 &lt;code&gt;O(n)&lt;/code&gt;，比如插入的元素是有序的，生成的二叉搜索树就退化为一个链表，树的一条腿特变长，这种情况下，需要遍历全部元素才行&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;完全二叉树&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;看到完全二叉树可以想成一个堆。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果给的输入是二叉树的层序遍历，则 对于任意节点,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若其下标为&lt;code&gt;i&lt;/code&gt; 则父节点下标&lt;code&gt;k&lt;/code&gt;为&lt;code&gt; i/2 - (i%2 == 0 ? 1 : 0);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若当前为&lt;code&gt;i&lt;/code&gt;, 两个子节点下标为&lt;code&gt; i*2 + 1&lt;/code&gt; 和&lt;code&gt;i*2+2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;满二叉树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;节点全满的就是满二叉树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-11-%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86.assets/QQ%E6%88%AA%E5%9B%BE20221026185828.png&quot; alt=&quot;QQ截图20221026185828&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;完全二叉树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;完全二叉树由满二叉树转化而来，也就是将满二叉树从最后一个节点开始删除，一个一个从后往前删除，剩下的就是完全二叉树。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-08-11-%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86.assets/QQ%E6%88%AA%E5%9B%BE20221026185832.png&quot; alt=&quot;QQ截图20221026185832&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;前缀和&lt;/h1&gt;
&lt;p&gt;如果想知道&lt;code&gt;nums[i] ~ nums[j] &lt;/code&gt;的和，我们只需要计算&lt;code&gt;presum[j+1] - presum[i] &lt;/code&gt;即可&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums = 	   [1,2,3,4, 5, 6, 7]
presum = [0,1,3,6,10,15,21,28]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如我们想知道&lt;code&gt;nums[2] ~ nums[5]&lt;/code&gt;的和，也就是&lt;code&gt;3+4+5+6 = 19&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们可以通过计算&lt;code&gt;presum[5+1] - presum[2] = 21 - 3 = 19&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;加1是因为前缀和比原数组在开头多一个0。&lt;/p&gt;
&lt;h2&gt;注意区分开差分数组和前缀和的区别。&lt;/h2&gt;
&lt;p&gt;我们假设新的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums =		  [1,2,4,6, 7, 20,10,3];
diff = 	     [1,1,2,2, 1, 13,-10,-7];
presum = 	[0,1,3,7,13,20,40,50,53];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前缀和怎么算的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;accumu = accumu + nums[i-1];
presum[i] = accumu; 或
presum[i] = presum[i - 1] + nums[i - 1] 因为有前导0
0+1=1
1+2=3
3+4=7
7+6=13
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;差分数组怎么算的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums[i] - nums[i - 1] = diff[i]
1-0 = 1（虚拟0）
2-1 = 1
4-2 = 2
6-4 = 2
7-6 = 1
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;字典树&lt;/h1&gt;
&lt;p&gt;字典树的节点有两个东西。一个是is_word用来判断到这个节点位置是否可以组成一个单词&lt;/p&gt;
&lt;p&gt;第二个是储存下一层节点的指针的容器。二叉树只有左右两个，而字典树最坏情况每一个节点都会有26个。&lt;/p&gt;
&lt;h1&gt;针对区间和类问题选择合适方法&lt;/h1&gt;
&lt;p&gt;针对不同的题目，我们有不同的方案可以选择（假设我们有一个数组）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组不变，求区间和：「前缀和」、「树状数组」、「线段树」&lt;/li&gt;
&lt;li&gt;多次修改某个数（单点），求区间和：「树状数组」、「线段树」&lt;/li&gt;
&lt;li&gt;多次修改某个区间，输出最终结果：「差分」&lt;/li&gt;
&lt;li&gt;多次修改某个区间，求区间和：「线段树」、「树状数组」（看修改区间范围大小）&lt;/li&gt;
&lt;li&gt;多次将某个区间变成同一个数，求区间和：「线段树」、「树状数组」（看修改区间范围大小）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;简单求区间和，用「前缀和」&lt;/li&gt;
&lt;li&gt;多次将某个区间变成同一个数，用「线段树」&lt;/li&gt;
&lt;li&gt;其他情况，用「树状数组」&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;https://leetcode.cn/problems/range-sum-query-mutable/solution/guan-yu-ge-lei-qu-jian-he-wen-ti-ru-he-x-41hv/&lt;/p&gt;
</content:encoded></item><item><title>C++ 特殊成员函数相关</title><link>https://sterben-01.github.io/posts/%E7%89%B9%E6%AE%8A%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E7%89%B9%E6%AE%8A%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3/</guid><pubDate>Mon, 18 Jul 2022 20:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;拷贝构造&lt;/h1&gt;
&lt;h3&gt;调用的是拷贝构造函数还是赋值运算符，主要是看是否有新的对象实例产生。&lt;/h3&gt;
&lt;p&gt;如果产生了新的对象实例，那调用的就是拷贝构造函数；如果是对已有的对象赋值，调用的是拷贝赋值。&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; P a = P(10); //这是拷贝构造。因为a还不存在。
//------------------------------------------------------
 P a;
 a =  P(10); //这是拷贝赋值。因为a已经存在了
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;拷贝构造函数必须以引用的方式传递参数&lt;/strong&gt;。这是因为，在值传递的方式传递给一个函数的时候，会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式，就会无限循环的调用下去，直到函数的栈溢出。&lt;/h3&gt;
&lt;p&gt;拷贝构造有严格的函数签名限制：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;类 &lt;code&gt;T&lt;/code&gt; 的复制构造函数是首个形参是 T&amp;amp;、const T&amp;amp;、volatile T&amp;amp; 或 const volatile T&amp;amp;，而且要么没有其他形参，要么剩余形参均有默认值的非模板构造函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;很多种情况都会调用拷贝构造。一般来说有如下几种形式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;对象作为函数的参数，以值传递的方式传给函数。&lt;/li&gt;
&lt;li&gt;对象作为函数的返回值，以值的方式从函数返回&lt;/li&gt;
&lt;li&gt;使用一个对象给另一个对象&lt;strong&gt;初始化&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;P p1(p2); //使用一个对象给另一个对象初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;P p1 = p2; //使用一个对象给另一个对象初始化
P p1 = P(10);//使用一个临时对象给另一个对象初始化
P* p1 = new P(10); //指针也不例外 这种声明使用10初始化一个匿名对象，并将新对象的地址赋给p1指针。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这里虽然使用了&lt;code&gt;=&lt;/code&gt;，但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象，所以调用的也是拷贝构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里的第一行和第二行可能会使用复制构造函数直接创建p1，也可能使用复制构造函数生成一个临时对象，然后将临时对象的内容赋给p1，&lt;strong&gt;这取决于具体的实现&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;举例：&lt;/h2&gt;
&lt;p&gt;示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person
{
public:
	Person(){}
	Person(const Person&amp;amp; p)
	{
		cout &amp;lt;&amp;lt; &quot;Copy Constructor&quot; &amp;lt;&amp;lt; endl;
	}

	Person&amp;amp; operator=(const Person&amp;amp; p)
	{
		cout &amp;lt;&amp;lt; &quot;Assign&quot; &amp;lt;&amp;lt; endl;
		return *this;
	}

private:
	int age;
	string name;
};

void f(Person p)
{
	return;
}

Person f1()
{
	Person p;
	return p;
}

int main()
{
	Person p;
	Person p1 = p;    // 1
	Person p2;
	p2 = p;           // 2
	f(p2);            // 3

	p2 = f1();        // 4

	Person p3 = f1(); // 5

	getchar();
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Copy Constructor&quot; 	//1
    
&quot;Assign&quot;			//2
    
&quot;Copy Constructor&quot;	//3
    
&quot;Copy Constructor&quot; 	//4
&quot;Assign&quot;
    
&quot;Copy Constructor&quot; 	//5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这是虽然使用了&quot;=&quot;，但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象，所以调用的是拷贝构造函数。&lt;/li&gt;
&lt;li&gt;首先声明一个对象p2，然后使用赋值运算符&quot;=&quot;，将p的值复制给p2，显然是调用赋值运算符，为一个已经存在的对象赋值 。&lt;/li&gt;
&lt;li&gt;以值传递的方式将对象p2传入函数f内，调用拷贝构造函数构建一个函数f可用的实参。&lt;/li&gt;
&lt;li&gt;这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象，在返回时会调用拷贝构造函数创建一个临时对象&lt;code&gt;tmp&lt;/code&gt;作为返回值；返回后调用赋值运算符将临时对象&lt;code&gt;tmp&lt;/code&gt;赋值给p2.&lt;/li&gt;
&lt;li&gt;按照4的解释，应该是首先调用拷贝构造函数创建临时对象；然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3，也就是会调用两次拷贝构造函数。不过，编译器也没有那么傻，应该是直接调用拷贝构造函数使用返回值创建了对象p3。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;关于拷贝赋值和拷贝构造在继承中的注意事项&lt;/h2&gt;
&lt;p&gt;继承中，子类的拷贝赋值和拷贝构造不会将父类的成员变量复制。为了防止遗漏，我们一般在子类直接调用父类的拷贝赋值或者拷贝构造。但是要注意语法和一些小细节&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    public:
        A(){}
        A(int a) :m_a(a) {}
        virtual~A(){}
        A&amp;amp; operator=(const A&amp;amp; rhs) {
            m_a = rhs.m_a;
            return *this;
        }
	int m_a;
};

class B : public A{
    public:
        B(){}
        B(int x):m_b(x){}
        B(int no, int x):A(no),m_b(x){} //这里用到了子类调用父类构造
        ~B(){} 
        
        B&amp;amp; operator=(const B&amp;amp; rhs) {
            this-&amp;gt;A::operator=(rhs); //这里是函数调用。
            /*
            我们函数调用可以不接受返回值。operator=的返回值的目的是返回一个自己用来进行下一步操作。是为了满足链式调用。当然也可以啥也不返回。
            所以隔壁的拷贝构造就没返回值。
            但是我们子类调用父类的拷贝赋值仅仅是把子类的父类部分调用父类的拷贝赋值来进行一个赋值。
            这里的例子就是子类的父类部分m_a使用父类的拷贝赋值给copy一遍。
            然后我们直接调用完了父类的拷贝赋值后，再自己赋值子类部分。
            时刻谨记 b1 = b2 就是b1.operator=(b2);
            */
            m_b = rhs.m_b;
            return *this;

        }
        int m_b;
};

int main(){
    B b(5,8);
    cout &amp;lt;&amp;lt; b.m_a &amp;lt;&amp;lt; endl;
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们这里没有使用带指针对象的拷贝构造和拷贝赋值做为例子。&lt;/p&gt;
&lt;p&gt;但是我们想要深拷贝的时候，拷贝赋值要注意先检测自我赋值。&lt;/p&gt;
&lt;p&gt;其次要先删除掉调用方自己的对应内容，然后新开辟内存，然后复制数据。&lt;/p&gt;
&lt;h2&gt;移动赋值和移动构造记得移动后需要把原来数据置空。尤其是带有指针类型的&lt;/h2&gt;
&lt;p&gt;但是不能删除。因为是转移所有权。也就是浅拷贝。删除了东西就没了。如果不置空的话会有两个问题。第一是可能会被误用。其次是如果是把一个临时对象移动构造或者是移动赋值给一个对象，临时对象的那一行过后临时对象会被析构。也就是指针指向的数据会被释放。所以你对象拿到的临时对象的对应指针数据也会被清除。&lt;/p&gt;
&lt;p&gt;所以在有指针的时候需要格外注意。而且对应的析构函数也要进行判空，不然会多次删除。&lt;/p&gt;
&lt;p&gt;移动构造在使用vector的情况下，一般是vector扩容的时候重新分配内存的时候使用，如果自定义类型有移动构造就会用移动构造。没有就是拷贝构造。&lt;/p&gt;
&lt;h1&gt;拷贝构造 拷贝赋值 移动构造 移动赋值的例子。此处拷贝为深拷贝。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj(int x):val(new int(x)){}
        int&amp;amp; getval(){
            return *val;
        }

        myobj(const myobj&amp;amp; obj){ //拷贝构造 深拷贝
            val = new int(*obj.val); //以传入参数的值新开辟数据空间
        }
        myobj&amp;amp; operator=(const myobj &amp;amp; rhs){ //拷贝赋值 深拷贝
            if(this == &amp;amp;rhs){ //检测自我赋值
                return *this;
            }
            delete this-&amp;gt;val; //释放自身此时的资源。
            val = new int(*rhs.val); //以传入参数的值新开辟数据空间
            return *this;
        }

        myobj(myobj&amp;amp;&amp;amp; obj){ //移动构造
            cout &amp;lt;&amp;lt;&quot;mv&quot; &amp;lt;&amp;lt; endl;
            val = obj.val;     //浅拷贝指针
            obj.val = nullptr; //因为是所有权转移，传入对象的数据指针必须被置空
        }
        myobj&amp;amp; operator=(myobj&amp;amp;&amp;amp; rhs){ //移动赋值
            cout &amp;lt;&amp;lt;&quot;mv=&quot; &amp;lt;&amp;lt; endl;
            if(this == &amp;amp;rhs){ //检测自我赋值
                return *this;
            }
            if(this-&amp;gt;val != nullptr){ //先释放自身此时资源
                delete val;
            }
            val = rhs.val; //浅拷贝指针
            rhs.val = nullptr; //因为是所有权转移，传入对象的数据指针必须被置空
            return *this;

        }
        ~myobj(){
            delete val;
            val = nullptr;
        }


        int* val;
        
};

int main(){
    myobj a(5);
    myobj b(10);
    cout &amp;lt;&amp;lt; a.getval() &amp;lt;&amp;lt; endl;
    myobj c(a);
    cout &amp;lt;&amp;lt; c.getval() &amp;lt;&amp;lt; endl;
    myobj d = c;
    cout &amp;lt;&amp;lt; d.getval() &amp;lt;&amp;lt; endl;
    d = b;
    cout &amp;lt;&amp;lt; d.getval() &amp;lt;&amp;lt; endl;
    myobj e = move(a);
    cout &amp;lt;&amp;lt; e.getval() &amp;lt;&amp;lt; endl;
    d = move(b);
    cout &amp;lt;&amp;lt; d.getval() &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;移动构造函数的设计&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;参数：
&lt;ul&gt;
&lt;li&gt;参数为&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;类型，因为是移动操作&lt;/li&gt;
&lt;li&gt;参数不可设置为&lt;code&gt;const&lt;/code&gt;，因为需要改变入参&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在移动构造函数后添加&lt;code&gt;noexcept&lt;/code&gt;关键字，确保移动构造函数不会抛出异常。尤其是针对可能会有&lt;code&gt;vector&lt;/code&gt;容器储存该类对象时。&lt;/li&gt;
&lt;li&gt;内容：
&lt;ul&gt;
&lt;li&gt;在参数初始化列表中将参数的资源移动给自己（先执行）。
&lt;ul&gt;
&lt;li&gt;把入参的资源移动给自己。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;然后在函数体内处理入参所拥有的资源：
&lt;ul&gt;
&lt;li&gt;一般来说，对象应该置为0或默认值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指针必须置空避免不小心释放不应释放的资源。因为被移动过的对象在生命周期结束的时候依旧会调用析构。如果不给原对象指针置空，则新对象接管资源后资源也会被无意置空！&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;这样之后就达到了资源移动的目的（后执行）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注意要点：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动构造函数不分配任何内存，只是简单的资源移动而已&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动构造会构造对象。所以如果使用一个对象通过移动构造来构造一个新对象。我们依旧会有两个对象。只不过只有一个对象拥有有效值而已。最后析构函数还是会被执行两次。因为虽然内容（资源）移动走了，但是壳子（对象本身）还在。所以析构函数依旧会被调用。&lt;/strong&gt; （杂记中搜索关键词“壳子”）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;移动赋值的设计&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数为&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;类型，因为是移动操作&lt;/li&gt;
&lt;li&gt;参数不可设置为&lt;code&gt;const&lt;/code&gt;，因为需要改变&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在函数后添加&lt;code&gt;noexcept&lt;/code&gt;关键字，确保移动赋值运算符函数不会抛出异常。原因同上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与拷贝赋值运算符一样，函数返回自身引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在函数执行前，应该检测自我赋值的情况&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;先释放自身资源，再拷贝参数&lt;code&gt;rhs&lt;/code&gt;的资源，最后再将&lt;code&gt;rhs&lt;/code&gt;置为空。原因同上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;赋值运算符的重载调用的时候一定要把&lt;code&gt;this&lt;/code&gt;先安全的析构掉（释放自身资源），绝不是构造。另外一个十分重要的点在于虚表指针的初始化时机，C++没规定虚表指针的初始化是什么时候，但是绝对不可能发生在赋值时。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://blog.csdn.net/qq_41453285/article/details/104419356&lt;/p&gt;
&lt;p&gt;通常来讲，我们可以给成员函数设计一个移动版本一个拷贝版本。也就是一个入参类型为&lt;code&gt;const&amp;amp;&lt;/code&gt;，另一个为&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;。&lt;code&gt;push_back&lt;/code&gt;就是这样设计的&lt;/p&gt;
&lt;h1&gt;注意编译器并不是在任何场合都会生成默认移动构造函数或默认移动赋值&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;针对移动构造和移动赋值，有更严格的要求需要满足，编译期才会为我们生成默认移动构造或默认移动赋值：
&lt;ul&gt;
&lt;li&gt;编译器只会针对满足如下情况的类生成移动构造和移动赋值
&lt;ul&gt;
&lt;li&gt;没有用户定义的移动构造/移动赋值&lt;/li&gt;
&lt;li&gt;没有用户声明的拷贝构造/拷贝赋值&lt;/li&gt;
&lt;li&gt;没有用户声明的析构函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所以会有0/3/5法则。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;特殊成员函数默认生成的条件&lt;/h1&gt;
&lt;p&gt;同时翻看 effective modern C++ 条款17&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;六种特殊的成员函数可以声明为类似 &lt;code&gt;X() = default&lt;/code&gt; 让编译器自动生成， 或者声明为 &lt;code&gt;X() = delete&lt;/code&gt; 阻止编译器自动生成相应的函数定义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;X::X()&lt;/code&gt; 默认构造函数 如果没有声明（包括 &lt;code&gt;=delete&lt;/code&gt;）,编译器将会自动生成定义。但是如果用户声明了其他形式的构造函数如&lt;code&gt;X(int x)&lt;/code&gt; , 编译器将不会自动生成，除非用户手动定义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝构造&lt;code&gt;X(const X&amp;amp;)&lt;/code&gt; 和 拷贝赋值&lt;code&gt;X&amp;amp; operator=(const X&amp;amp;)&lt;/code&gt; 如果用户没有提供，&lt;strong&gt;且没有提供移动构造或移动赋值&lt;/strong&gt;，在需要的时候编译器会自动生成，内容为对成员的值拷贝，如果是指针成员将是“浅复制”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意还有一些其他限制。这个建议去网站看文档。比较细碎。比如拥有不可拷贝的成员时。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意拷贝构造和拷贝赋值不会抑制互相的生成，但是其生成会被弃用deprecated。同时，用户定义的析构函数也会导致其弃用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;当 &lt;code&gt;T&lt;/code&gt; 拥有用户定义的析构函数或用户定义的复制构造函数时，隐式定义的复制赋值运算符的生成被弃用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;当 &lt;code&gt;T&lt;/code&gt; 拥有用户定义的析构函数或用户定义的复制赋值运算符时，隐式定义的复制构造函数的生成会被弃用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;再次重申，弃用不是&lt;code&gt;=delete&lt;/code&gt;。而是deprecated。g++不会提示报错，但是clang会。可能委员会认为如果被设置为&lt;code&gt;=delete&lt;/code&gt;会对大量代码导致遗留问题。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个类都有自己的拷贝构造或拷贝赋值。只不过他们要么可以用要么被=delete了。&lt;a href=&quot;https://youtu.be/9BM5LAvNtus&quot;&gt;来自&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对除构造函数之外的特殊成员函数的声明（哪怕是&lt;code&gt;=default&lt;/code&gt;或&lt;code&gt;delete&lt;/code&gt;），将会阻止编译器生成默认的移动构造和移动赋值。这将会导致该类无法使用移动语义&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这里是压根不生成（声明）移动构造和移动赋值。压根没有，而不是&lt;code&gt;=delete&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;针对移动构造和移动赋值，有更严格的要求需要满足，编译器才会为我们生成默认移动构造或默认移动赋值：
&lt;ul&gt;
&lt;li&gt;没有用户定义的移动构造/移动赋值&lt;/li&gt;
&lt;li&gt;没有用户声明的拷贝构造/拷贝赋值&lt;/li&gt;
&lt;li&gt;没有用户声明的析构函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注意和拷贝操作不同。移动操作的两种函数不是互相独立的。也就是移动构造和移动赋值会互相抑制编译器的提供。--- effective modern C++ 条款17。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果只声明了移动构造或移动赋值，则编译器隐式生成的默认拷贝构造和默认拷贝赋值将会声明为&lt;code&gt;delete&lt;/code&gt;, 也就是该类是仅移动的。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果一个本来会隐式声明且定义为弃置的拷贝构造被用户显式预置(=default)，那么这个复制构造函数被定义为弃置。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说如果某类有一个不可拷贝的成员，你还给这个类的拷贝构造用了&lt;code&gt;=default&lt;/code&gt;，那么没有用，还是会被编译器换成&lt;code&gt;=delete&lt;/code&gt;。当然了，如果这时候你没有对这个类成员进行会调用拷贝构造的操作，GCC不会报错，但是clang会有warning。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;0/3/5 法则&lt;/h1&gt;
&lt;h2&gt;3之法则&lt;/h2&gt;
&lt;p&gt;如果某个类需要用户定义的&lt;strong&gt;析构函数、用户定义的拷贝构造函数或用户定义的拷贝赋值运算符&lt;/strong&gt;，那么它几乎肯定需要全部三者。&lt;/p&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常，若一个类需要析构函数，则代表其合成的析构函数不足以释放类所拥有的资源，其中最典型的就是指针成员（析构时需要手动去释放指针指向的内存）。&lt;/li&gt;
&lt;li&gt;所以，若存在自定义（且正确）的析构函数，但使用合成的拷贝构造函数，那么拷贝过去的也只是指针，此时两个对象的指针变量同时指向同一块内存，指向同一块内存的后果很有可能是在两个对象中的析构函数中先后被释放两次。所以需要额外的拷贝控制函数去控制相应资源的拷贝。&lt;/li&gt;
&lt;li&gt;所以这类例子的共同点就是：一个对象拥有额外的资源（指针指向的内存），但另一个对象使用合成的拷贝构造函数也同时拥有这块资源。当一方对象被销毁后，析构函数释放了资源，这时另一个对象便失去了这块资源（但程序员还不知道）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5之法则&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;因为用户定义的析构函数、拷贝构造函数或拷贝赋值运算符的存在会阻止移动构造函数和移动赋值运算符的隐式定义，所以任何想要移动语义的类必须声明全部五个特殊成员函数。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;0之法则&lt;/h2&gt;
&lt;p&gt;如果不需要手动定义， 就不要定义，一切让它默认。&lt;/p&gt;
&lt;h2&gt;整理&lt;/h2&gt;
&lt;p&gt;图片&lt;a href=&quot;https://www.sandordargo.com/blog/2024/11/27/non-movable-classes&quot;&gt;来自这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;注意移动操作的含义&lt;/h1&gt;
&lt;p&gt;**移动是一种特殊的拷贝操作。也就是移动是比拷贝更&quot;先进“的操作。**因为根据函数重载决议规则，如果我们没有提供移动构造或移动赋值，当我们使用右值进行“移动”操作时，则会匹配到拷贝构造或拷贝赋值。&lt;strong&gt;所以如果想显式表达某个类不能移动，则应该声明拷贝操作为&lt;code&gt;=delete&lt;/code&gt;而不是放在那里不管。如果不写，我们的“移动”操作会变成拷贝操作。如果写了，则会报错。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;构造函数中使用move 或 forward移动参数的陷阱&lt;/h1&gt;
&lt;h2&gt;不要对入参为常量左值引用的对象使用move。它会被拷贝而不是移动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
        myobj(int x):val(new int(x)){ //构造
            cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
        }
        myobj(const myobj&amp;amp; obj){ //拷贝构造
            cout &amp;lt;&amp;lt;&quot;copy const&quot; &amp;lt;&amp;lt; endl;
            val = new int(*obj.val);
        }
        myobj&amp;amp; operator=(const myobj &amp;amp; rhs){ //拷贝赋值
            cout &amp;lt;&amp;lt;&quot;copy= const&quot; &amp;lt;&amp;lt; endl;
            if(this == &amp;amp;rhs){
                return *this;
            }
            delete this-&amp;gt;val;
            val = new int(*rhs.val);
            return *this;
        }

        myobj(myobj&amp;amp;&amp;amp; obj){ //移动构造
            cout &amp;lt;&amp;lt;&quot;mv&quot; &amp;lt;&amp;lt; endl;
            val = obj.val;
            obj.val = new int(3939);
        }
        myobj&amp;amp; operator=(myobj&amp;amp;&amp;amp; rhs){ //移动赋值
            cout &amp;lt;&amp;lt;&quot;mv=&quot; &amp;lt;&amp;lt; endl;
            if(this == &amp;amp;rhs){
                return *this;
            }
            if(this-&amp;gt;val != nullptr){
                delete val;
            }
            val = rhs.val;
            rhs.val = nullptr; 
            return *this;

        }
        ~myobj(){
            cout &amp;lt;&amp;lt;&quot;dest&quot; &amp;lt;&amp;lt; endl;
            delete val;
            val = nullptr;
        }

        int* val;
        
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面是一个写了所有特殊成员函数的类。我们继续看&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个版本。我们值传递。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    testobj(myobj obj):inner(move(obj)){} //版本1 值传递
};



int main(){
    myobj temp(20);
    testobj out(temp);
    cout &amp;lt;&amp;lt; *temp.val &amp;lt;&amp;lt; endl; //输出什么？
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const //构造temp对象
copy const //值传递导致的拷贝至构造函数中。
mv //构造函数中使用move把拷贝的对象转换为右值然后移动构造的inner
dest//拷贝进来的参数析构
20
dest //testobj里的myobj析构
dest//外部myobj析构
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这个20会正常输出。外部的temp对象是拷贝传值。被move变成右值的是值传递导致的拷贝的那个对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;值传递有另一种情况&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    testobj(myobj obj):inner(move(obj)){} //版本1 值传递
};



int main(){
    myobj temp(20);
    testobj out(move(temp)); //这里我们也用move
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const //构造temp对象
mv //右值触发移动构造传入函数参数
mv //右值触发移动构造从函数入参构建inner
dest
dest
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第二个版本。左值引用传递。
&lt;ul&gt;
&lt;li&gt;这个版本有个毛病。输入右值不行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    testobj(myobj&amp;amp; obj):inner(move(obj)){} //左值引用
};



int main(){
    myobj temp(20);
    testobj out(temp);
    //testobj out(myobj(20)); 不行。
    cout &amp;lt;&amp;lt; *temp.val &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const //temp对象构建
mv //引用传递不发生拷贝，这里的move变成右值的是temp对象本身。所以触发移动构造testobj内的inner
3939 //外部对象已经被移动。输出3939
dest
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们发现由于是左值引用传递，所以temp对象发生了移动。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第三个版本。常量左值引用传递。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    testobj(const myobj&amp;amp; obj):inner(move(obj)){} //常量左值引用
};



int main(){
    myobj temp(20);
    testobj out(temp);
    cout &amp;lt;&amp;lt; *temp.val &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const //temp对象构建
copy const //引用传递不发生拷贝。这里的拷贝是从构造函数入参中拷贝至myobj的inner对象。
20//外部不移动
dest
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么会拷贝？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常量左值引用既然是常量就不可能对该参数修改。这是简单的理解。&lt;/li&gt;
&lt;li&gt;深层次的理解是这样的。我们&lt;code&gt;move&lt;/code&gt;会把一个左值变成右值。我们&lt;code&gt;obj&lt;/code&gt;传进来的时候是&lt;code&gt;const &amp;amp;&lt;/code&gt;, 如果&lt;code&gt;move&lt;/code&gt;就会变成&lt;code&gt;const &amp;amp;&amp;amp;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这时候有一个问题。我们没有&lt;code&gt;const&amp;amp;&amp;amp;&lt;/code&gt;开头的函数。我们也知道&lt;code&gt;const&amp;amp;&lt;/code&gt;可以接受一切参数包括&lt;code&gt;const&amp;amp;&amp;amp;&lt;/code&gt;。（杂记）&lt;strong&gt;所以这时候会去匹配&lt;code&gt;const&amp;amp;&lt;/code&gt;的拷贝构造。所以最后会发生拷贝。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&amp;amp;&amp;amp;&lt;/code&gt;函数毫无意义。首先移动语义在有意义的时候一定要保证把被移动对象和资源进行分离。加了&lt;code&gt;const&lt;/code&gt;无法对参数进行修改。其次，入参为右值的时候我们有&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;接受。常量右值的时候会被&lt;code&gt;const&amp;amp;&lt;/code&gt;接受。所以&lt;code&gt;const&amp;amp;&amp;amp;&lt;/code&gt;没有意义。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.nextptr.com/tutorial/ta1211389378/beware-of-using-stdmove-on-a-const-lvalue&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第四个版本 右值引用传递。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个版本也有毛病。左值不行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    testobj(myobj&amp;amp;&amp;amp; obj):inner(move(obj)){} //右值引用
};



int main(){
    myobj temp(20);
    testobj out(move(temp)); //move换成右值
    cout &amp;lt;&amp;lt; *temp.val &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const //构造temp
mv //引用传递不拷贝。这个move是把temp对象转换为右值后触发拷贝构造
3939//temp已被移动。
dest
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们发现结果和左值引用是一致的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那么如果我们直接传入临时对象会发生什么呢？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;testobj out(myobj(10));


const //构造临时对象。临时对象必须得有啊不然传的是啥？
mv //move把临时对象换成右值，触发移动构造。
dest
dest

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;构造函数中使用forward的正确方式&lt;/h2&gt;
&lt;p&gt;对于完美转发我们已经在杂记中介绍了。但是我们当时只展示了函数的完美转发。&lt;/p&gt;
&lt;p&gt;我们必须要记住完美转发的核心要素之一，重中之重也就是完美转发依靠于万能引用。因为只有万能引用才能触发引用坍缩。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是格外注意万能引用的陷阱之一：如果不涉及型别推导，那么就算是&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;也不是万能引用。（格外注意！！！！）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class testobj{
    public:
    T inner;
    testobj(T&amp;amp;&amp;amp; obj) : inner(forward&amp;lt;T&amp;gt;(obj)){} // cannot bind rvalue reference of type ‘myobj&amp;amp;&amp;amp;’ to lvalue of type ‘myobj’
};
int main(){
    myobj var(10);
    testobj&amp;lt;myobj&amp;gt; a(var); //cannot bind rvalue reference of type ‘myobj&amp;amp;&amp;amp;’ to lvalue of type ‘myobj’
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的就是&lt;strong&gt;错误例子&lt;/strong&gt;。在这里，&lt;code&gt;T&lt;/code&gt;是类模板参数。因为我们使用的时候已经显式制定了&lt;code&gt;T&lt;/code&gt;为&lt;code&gt;myobj&lt;/code&gt;。这就代表了&lt;code&gt;T&lt;/code&gt;不涉及型别推导。所以&lt;code&gt;T&lt;/code&gt;在这里不是万能引用，而是右值引用。不涉及万能引用的时候自然不涉及引用坍缩，也就不涉及完美转发。&lt;/p&gt;
&lt;p&gt;什么是正确例子呢？我们知道必须要让含有完美转发的函数涉及到万能引用也就是型别推导。所以就需要成员函数模板。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    template &amp;lt;typename Arg&amp;gt;
    testobj(Arg&amp;amp;&amp;amp; obj):inner(forward&amp;lt;Arg&amp;gt;(obj)){}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候函数的入参的模板参数会涉及到型别推导。至于该类是否是类模板，则不重要。只是看是否有需求罢了。&lt;/p&gt;
&lt;h2&gt;构造函数中 想要触发蕴含的某个其他对象的移动构造，则必须使用move或forward&lt;/h2&gt;
&lt;p&gt;我们延续上面的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    //testobj(myobj&amp;amp;&amp;amp; obj):inner(move(obj)){}
    testobj(myobj&amp;amp;&amp;amp; obj):inner((obj)){} //我们这里不使用move
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现，我们这里使用了&lt;code&gt;move&lt;/code&gt;。如果不用&lt;code&gt;move&lt;/code&gt;会发生什么呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;testobj our(myobj(10));
const //构造临时对象
copy const //哇哦，发生了拷贝。
dest
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么会拷贝？我们不是传入了临时对象吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们一定知道：具名的右值引用是左值。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func(myobj&amp;amp;&amp;amp; rhs){
    //这里rhs是具名右值引用。是左值。所以如果这时候有函数接受左值或右值引用形式的rhs，则会匹配至左值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这也是为什么存在完美转发。这就是另一个要点！在构造函数中，如果想要显式对某一个对象进行移动，仍需要使用&lt;code&gt;move&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果我们这里不使用&lt;code&gt;move&lt;/code&gt;，则此时右值引用接住的临时对象会变为具名对象，会变成左值。传入后就会匹配拷贝构造。&lt;/p&gt;
&lt;p&gt;为了正确的传递其右值特性，或强制转换为右值，就需要使用&lt;code&gt;forward&lt;/code&gt;或&lt;code&gt;move&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以我们可以这样使用&lt;code&gt;move&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    testobj(myobj&amp;amp;&amp;amp; obj):inner(move(obj)){}//使用move
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或使用&lt;code&gt;forward&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class testobj{
    public:
    myobj inner;
    template &amp;lt;typename Arg&amp;gt;
    testobj(Arg&amp;amp;&amp;amp; obj):inner(forward&amp;lt;Arg&amp;gt;(obj)){} //使用forward
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;同样的，想要从头移到尾，则外部调用和内部构造函数中都要使用&lt;code&gt;move&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class ano{
    public:
        myobj val;
        ano(myobj ano):val(move(ano)){}; // 注意此时入参是值传递而不是右值引用。
};
int main(){
    myobj a(20);
    ano pro(move(a));
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用过程中，我们会触发两次移动构造。第一次是传入函数中。第二次是使用入参初始化类成员的时候。只要有一个地方没有&lt;code&gt;move&lt;/code&gt;就会触发拷贝构造&lt;/p&gt;
&lt;p&gt;更深入的可以看看&lt;a href=&quot;https://www.youtube.com/watch?v=PNRju6_yn3o&amp;amp;t=182s&quot;&gt;视频&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;具体在构造函数中，我们应该传值，传递两个版本还是完美转发呢？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;方案1：写出两个版本。&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;和&lt;code&gt;const &amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testobj2{
    public:
    myobj inner;
    testobj2(const myobj&amp;amp; obj):inner(obj){ //针对左值。记住，针对入参为常量左值引用，不要move
        cout &amp;lt;&amp;lt;&quot;called testobj2&quot; &amp;lt;&amp;lt; endl;
    }
    testobj2(myobj&amp;amp;&amp;amp; obj):inner(move(obj)){
        cout &amp;lt;&amp;lt;&quot;called&amp;amp;&amp;amp; testobj2&quot; &amp;lt;&amp;lt; endl;
    }
};

void test2L(){ //左值入参
    cout &amp;lt;&amp;lt; &quot;start test2L&quot; &amp;lt;&amp;lt; endl;
    myobj obj1(20);
    testobj2 v1(obj1);
    cout &amp;lt;&amp;lt; &quot;end test2L&quot; &amp;lt;&amp;lt; endl;
}
void test2R(){ //右值入参
    cout &amp;lt;&amp;lt; &quot;start test2R&quot; &amp;lt;&amp;lt; endl;
    myobj obj1(20);
    testobj2 v1(move(obj1));
    cout &amp;lt;&amp;lt; &quot;end test2R&quot; &amp;lt;&amp;lt; endl;
}
/*
----------
start test2L
const 构建对象
copy const 引用传递不拷贝。此处是拷贝构造至类内的inner对象
called testobj2
end test2L
dest
dest
----------
start test2R
const 构建对象
mv 引用传递不拷贝。又因为是右值所以触发了移动构造至类内的inner对象
called&amp;amp;&amp;amp; testobj2
end test2R
dest
dest
----------
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方案2：使用完美转发&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testobj1{
    public:
    myobj inner;
    template &amp;lt;typename Arg&amp;gt;
    testobj1(Arg&amp;amp;&amp;amp; obj):inner(forward&amp;lt;Arg&amp;gt;(obj)){ //完美转发应该怎么写，上面提到了。
        cout &amp;lt;&amp;lt;&quot;called testobj1&quot; &amp;lt;&amp;lt; endl;
    }
};

void test1L(){ //左值入参
    cout &amp;lt;&amp;lt; &quot;start test1L&quot; &amp;lt;&amp;lt; endl;
    myobj obj1(20);
    testobj1 v1(obj1);
    cout &amp;lt;&amp;lt; &quot;end test1L&quot; &amp;lt;&amp;lt; endl;
}
void test1R(){ //右值入参
    cout &amp;lt;&amp;lt; &quot;start test1R&quot; &amp;lt;&amp;lt; endl;
    myobj obj1(20);
    testobj1 v1(move(obj1));
    cout &amp;lt;&amp;lt; &quot;end test1R&quot; &amp;lt;&amp;lt; endl;
}
/*
start test1L
const 构建对象
copy const 完美转发发现是左值，调用拷贝构造
called testobj1
end test1L
dest
dest
----------
start test1R
const 构建对象
mv 完美转发发现是右值，调用移动构造
called testobj1
end test1R
dest
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方案3 值传递&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class testobj3{
    public:
    myobj inner;
    testobj3(myobj obj):inner(move(obj)){
        cout &amp;lt;&amp;lt;&quot;called testobj3&quot; &amp;lt;&amp;lt; endl;
    }
};

void test3L(){ //左值入参
    cout &amp;lt;&amp;lt; &quot;start test3L&quot; &amp;lt;&amp;lt; endl;
    myobj obj1(20);
    testobj3 v1(obj1);
    cout &amp;lt;&amp;lt; &quot;end test3L&quot; &amp;lt;&amp;lt; endl;
}
void test3R(){ //右值入参
    cout &amp;lt;&amp;lt; &quot;start test3R&quot; &amp;lt;&amp;lt; endl;
    myobj obj1(20);
    testobj3 v1(move(obj1));
    cout &amp;lt;&amp;lt; &quot;end test3R&quot; &amp;lt;&amp;lt; endl;
}

/*
----------
start test3L
const 构建对象
copy const 值传递发生一次多余拷贝，拷贝至函数入参。此时是临时对象
mv 调用移动构造
called testobj3
dest
end test3L
dest
dest
----------
start test3R
const 构建对象
mv 右值传递发生一次多余移动。移动至函数入参。此时是临时对象
mv 调用移动构造
called testobj3
dest
end test3R
dest
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;额外注意为何值传递传递右值的时候是多了一次移动而不是多了一次拷贝？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为传递至函数参数，相当于进行了一次以入参为值的直接初始化。也就是临时对象是直接初始化出来的。在杂记中我们提到了直接初始化会考虑全部的构造函数寻找最佳匹配，所以如果满足移动构造就会使用移动构造&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后我们发现了，写两种版本和完美转发效果相同。但是使用值传递会在左值的时候多一次拷贝，右值的时候多一次移动。尤其是拷贝或移动开销较大的时候应该避免。&lt;strong&gt;所以既可以写两种版本，也可以使用完美转发。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;拷贝/移动构造/赋值只要写了，就需要写出完整的&lt;/h1&gt;
&lt;p&gt;无论成员变量是什么类型的，堆的还是栈的，都需要写出来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
    int val;
    myobj() = default;
    myobj(int x):val(x){};
    myobj(const myobj&amp;amp; rhs){
        cout &amp;lt;&amp;lt; &quot;copy const&quot; &amp;lt;&amp;lt; endl;
        val = rhs.val;
    }
};
int main(){
    myobj a(20);
    myobj b(a);
    cout &amp;lt;&amp;lt; a.val &amp;lt;&amp;lt; endl; //20
    cout &amp;lt;&amp;lt; b.val &amp;lt;&amp;lt; endl; //20
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面是我们提供的，一切正常。&lt;/p&gt;
&lt;p&gt;要么就是不写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
    int val;
    myobj() = default;
    myobj(int x):val(x){};
    myobj(const myobj&amp;amp; rhs) = default;
};
int main(){
    myobj a(20);
    myobj b(a);
    cout &amp;lt;&amp;lt; a.val &amp;lt;&amp;lt; endl; //20
    cout &amp;lt;&amp;lt; b.val &amp;lt;&amp;lt; endl; //20
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;但是绝对不可以这样：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class myobj{
    public:
    int val;
    myobj() = default;
    myobj(int x):val(x){};
    myobj(const myobj&amp;amp; rhs){
        cout &amp;lt;&amp;lt;&quot;copy const&quot; &amp;lt;&amp;lt; endl;
    }
};
int main(){
    myobj a(20);
    myobj b(a);
    cout &amp;lt;&amp;lt; a.val &amp;lt;&amp;lt; endl;//20
    cout &amp;lt;&amp;lt; b.val &amp;lt;&amp;lt; endl;//乱七八糟
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们知道编译器默认生成的是浅拷贝。但是那是因为你没写才生成默认拷贝构造。你要是写了编译器就不会生成。那如果你不写完整那就是连浅拷贝都没有。这里我们的拷贝构造就是连浅拷贝都没有。所以&lt;code&gt;val&lt;/code&gt;是垃圾值。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;类静态成员变量不需要写入拷贝构造或拷贝赋值和析构函数&lt;/h1&gt;
&lt;p&gt;本来静态成员变量就是类的所有对象共有的。也不能在类内初始化。所以写入拷贝构造或拷贝赋值是无意义的。&lt;/p&gt;
&lt;p&gt;同样，静态成员变量生存周期是至程序结束。所以析构函数并不关心他们。&lt;/p&gt;
&lt;h1&gt;重新梳理赋值运算符的语义&lt;/h1&gt;
&lt;p&gt;赋值运算符&lt;code&gt;operator=&lt;/code&gt;算作运算符重载里。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这个&lt;code&gt;operator=&lt;/code&gt;的形参恰好接受一个 &lt;code&gt;T&lt;/code&gt;、&lt;code&gt;T&amp;amp;&lt;/code&gt;、&lt;code&gt;const T&amp;amp;&lt;/code&gt;、&lt;code&gt;volatile T&amp;amp;&lt;/code&gt; 或 &lt;code&gt;const volatile T&amp;amp; &lt;/code&gt;类型的形参，且它不是模板函数也不是静态成员函数，那么它就是拷贝赋值运算符&lt;/li&gt;
&lt;li&gt;如果这个&lt;code&gt;operator=&lt;/code&gt;的形参恰好接受一个 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;、&lt;code&gt;const T&amp;amp;&amp;amp;&lt;/code&gt;、&lt;code&gt;volatile T&amp;amp;&amp;amp;&lt;/code&gt; 或&lt;code&gt;const volatile T&amp;amp;&amp;amp;&lt;/code&gt;类型的形参，且它不是模板函数也不是静态成员函数，那么它就是移动赋值运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以一个类可以有多个&lt;code&gt;operator=&lt;/code&gt;重载。他们都是赋值运算符。但是如果不符合上面的要求，则不能称之为拷贝赋值或移动赋值。&lt;/p&gt;
&lt;p&gt;所以operator=的返回值压根没有明确要求。包括拷贝赋值和移动赋值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拷贝赋值运算符也不是强制返回&lt;code&gt;T&amp;amp;&lt;/code&gt;的。如果你不想链式调用，完全可以写成这样&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct myclass{
    int val = 0;
    myclass(int x):val(x){};
    myclass operator+(const myclass&amp;amp; rhs){
            return myclass(val + rhs.val);
    }
    void operator=(const myclass&amp;amp; rhs){
        val = rhs.val;
        cout &amp;lt;&amp;lt;&quot;copy assignment&quot; &amp;lt;&amp;lt; endl;
    }
};
int main(){
    myclass obj(200);
    myclass obj2(300);
    cout &amp;lt;&amp;lt; obj2.val &amp;lt;&amp;lt; endl;
    obj2 = obj;
    cout &amp;lt;&amp;lt; obj2.val &amp;lt;&amp;lt; endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a.operator=(b)&lt;/code&gt;压根不是返回值作用于&lt;code&gt;a&lt;/code&gt;。是先修改了&lt;code&gt;a&lt;/code&gt;然后同时返回自己而已。目的就是链式调用。所以&lt;code&gt;a = b&lt;/code&gt;这个表达式，返回值压根就没用上。&lt;/p&gt;
&lt;h1&gt;非常好的资料&lt;/h1&gt;
&lt;p&gt;https://youtu.be/9BM5LAvNtus&lt;/p&gt;
&lt;h1&gt;各种成员函数是否应该是explicit的&lt;/h1&gt;
&lt;p&gt;https://quuxplusone.github.io/blog/2024/06/25/most-stl-ctors-arent-explicit-but-yours-still-should-be/&lt;/p&gt;
&lt;h1&gt;移动赋值和 use-after-move&lt;/h1&gt;
&lt;p&gt;本文参考资料来自&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/1156/&quot;&gt;这里的&lt;em&gt;Errors in object lifetime: use-after-move&lt;/em&gt;章节&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;每一个vector都有我们可选的分配器对象。这点我们在PMR介绍过。&lt;/p&gt;
&lt;p&gt;标准库要求分配器类型定义&lt;code&gt;propagate_on_container_move_assignment&lt;/code&gt;属性，该属性会影响移动分配的行为。如果我们写&lt;code&gt;A = std::move(B)&lt;/code&gt;，我们有三个选项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;propagate_on_container_move_assignment{} == true&lt;/code&gt; 是的，这不是常量，而是一个结构，如false_type / true_type）。&lt;code&gt;A&lt;/code&gt;被释放，分配器被移动（再次使用移动分配，因此我们需要在这里注意一些保证），然后从&lt;code&gt;B&lt;/code&gt;中获取内容，最后将&lt;code&gt;B&lt;/code&gt;置空&lt;/li&gt;
&lt;li&gt;&lt;code&gt;propagate_on_container_move_assignment{} == false&lt;/code&gt;并且&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;B&lt;/code&gt;中的分配器相同 (&lt;code&gt;A.get_allocator() == B.get_allocator()&lt;/code&gt;)。&lt;code&gt;A&lt;/code&gt;被释放，分配器保持原位。 内容从&lt;code&gt;A&lt;/code&gt;转移到&lt;code&gt;B&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;propagate_on_container_move_assignment{} == false&lt;/code&gt;并且&lt;code&gt;A.get_allocator() != B.get_allocator()&lt;/code&gt;。这是最有趣的部分开始的地方：&lt;code&gt;A&lt;/code&gt;不能拿走分配器或数据。唯一的选择是分别移动每个元素。但是，清空和释放&lt;code&gt;B&lt;/code&gt;不是必要的。我们需要做的就是移动元素。在这种情况下，我们还可以得到一个由移出的元素组成的完整vector。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;在第三种情况下，vector的 libc++ 实现中，不会对其调用&lt;code&gt;clear()&lt;/code&gt;，但是libstdc++会。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;libc++:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-19-%E7%89%B9%E6%AE%8A%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241022005312.png&quot; alt=&quot;微信图片_20241022005312&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-19-%E7%89%B9%E6%AE%8A%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241022005316.png&quot; alt=&quot;微信图片_20241022005316&quot; /&gt;&lt;/p&gt;
&lt;p&gt;libstdc++:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-19-%E7%89%B9%E6%AE%8A%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241022005322.png&quot; alt=&quot;微信图片_20241022005322&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-19-%E7%89%B9%E6%AE%8A%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241022005319.png&quot; alt=&quot;微信图片_20241022005319&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们从图中可以看到libc++和libstdc++的源码实现差别。但是注意，就算不对B调用clear，也不代表B的元素是有效的。经过移动后，B的元素仅仅是可访问的。所以说它没有破坏语义也没有颠覆我们先前的认知。我们用个人的例子试一下，会得到如下结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  {
    VectorString v = {
      1,2,3,4
    };
    VectorString vv;
    cout &amp;lt;&amp;lt;&quot;---&quot;&amp;lt;&amp;lt;endl;
    vv = std::move(v);
    std::cout &amp;lt;&amp;lt; v.size() &amp;lt;&amp;lt; &quot;\n&quot;;
  }
/*
libstdc++
---
myobj move constructor
myobj move constructor
myobj move constructor
myobj move constructor
myobj destructor
myobj destructor
myobj destructor
myobj destructor
0
myobj destructor
myobj destructor
myobj destructor
myobj destructor

libc++
---
myobj move constructor
myobj move constructor
myobj move constructor
myobj move constructor
4
myobj destructor
myobj destructor
myobj destructor
myobj destructor
myobj destructor
myobj destructor
myobj destructor
myobj destructor
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出明显差别。&lt;/p&gt;
&lt;p&gt;所以我们针对移动对象的状态保证有四个级别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;仅析构。被移动的对象被销毁并且不再使用。如果决定向其对象添加移动语义，则应提供这项基本保证，以便整个析构函数自动调用机制不会出现任何超出人预期外的问题&lt;/li&gt;
&lt;li&gt;析构和赋值。这种情况下指的某个对象被移动后，可以通过为对象分配新值来重用它（然后正常使用它）。可以移动但不能重新分配的对象非常罕见。因此，通常这种保证与前一种保证相结合。&lt;/li&gt;
&lt;li&gt;有效但是未指明效果。我们可以使用已经被移动的对象，比如调用不需要先决条件的成员函数。但是里面有什么？天知道&lt;/li&gt;
&lt;li&gt;有效且定义明确。这是我们想要的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;copy and swap&lt;/h1&gt;
&lt;p&gt;https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom&lt;/p&gt;
&lt;p&gt;move and swap基本是一个意思&lt;/p&gt;
&lt;h1&gt;延伸&lt;/h1&gt;
&lt;h2&gt;如何确保一个类不可被拷贝或移动&lt;/h2&gt;
&lt;p&gt;https://www.sandordargo.com/blog/2024/11/27/non-movable-classes&lt;/p&gt;
</content:encoded></item><item><title>C++ 杂记</title><link>https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B0/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/%E6%9D%82%E8%AE%B0/</guid><pubDate>Fri, 15 Jul 2022 20:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++ 杂记&lt;/h1&gt;
&lt;h1&gt;拷贝初始化，拷贝构造 直接初始化。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt; 1 #include &amp;lt;iostream&amp;gt;  
 2 #include &amp;lt;cstring&amp;gt;  
 3 using namespace std;
 4 class ClassTest
 5 {
 6 public:
 7     ClassTest()
 8     {
 9         c[0] = &apos;\0&apos;;
10         cout &amp;lt;&amp;lt; &quot;ClassTest()&quot; &amp;lt;&amp;lt; endl;
11     }
12     ClassTest&amp;amp; operator=(const ClassTest &amp;amp;ct)
13     {
14         strcpy(c, ct.c);
15         cout &amp;lt;&amp;lt; &quot;ClassTest&amp;amp; operator=(const ClassTest &amp;amp;ct)&quot; &amp;lt;&amp;lt; endl;
16         return *this;
17     }
18     ClassTest(ClassTest&amp;amp;&amp;amp; ct)
19     {
20         cout &amp;lt;&amp;lt; &quot;ClassTest(ClassTest&amp;amp;&amp;amp; ct)&quot; &amp;lt;&amp;lt; endl;
21     }
22     ClassTest &amp;amp; operator=(ClassTest&amp;amp;&amp;amp; ct)
23     {
24         strcpy(c, ct.c);
25         cout &amp;lt;&amp;lt; &quot;ClassTest &amp;amp; operator=(ClassTest&amp;amp;&amp;amp; ct)&quot; &amp;lt;&amp;lt; endl;
26         return *this;
27     }
28     ClassTest(const char *pc)
29     {
30         strcpy(c, pc);
31         cout &amp;lt;&amp;lt; &quot;ClassTest (const char *pc)&quot; &amp;lt;&amp;lt; endl;
32     }
33     //private:  
34     ClassTest(const ClassTest&amp;amp; ct)
35     {
36         strcpy(c, ct.c);
37         cout &amp;lt;&amp;lt; &quot;ClassTest(const ClassTest&amp;amp; ct)&quot; &amp;lt;&amp;lt; endl;
38     }
39     virtual  int ff()
40     {
41         return 1;
42     }
43 private:
44     char c[256];
45 };
46 ClassTest f1()
47 {
48     ClassTest c;
49     return c;
50 }
51 void f2(ClassTest ct)
52 {
53     ;
54 }
55 int main()
56 {
57     ClassTest ct1(&quot;ab&quot;);//直接初始化  
58     ClassTest ct2 = &quot;ab&quot;;//复制初始化  
59     ClassTest ct3 = ct1;//复制初始化  
60     ClassTest ct4(ct1);//直接初始化  
61     ClassTest ct5 = ClassTest(&quot;ab&quot;);//复制初始化  
62     ClassTest ct6 = f1(); 
63     f1();
64     f2(ct1);
65     return 0;
66 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（1）什么是拷贝初始化（也称为复制初始化）：将一个已有的对象拷贝到正在创建的对象，&lt;strong&gt;如果需要的话还需要进行类型转换&lt;/strong&gt;。拷贝初始化发生在下列情况：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;拷贝初始化的含义是从另一个对象初始化对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;使用赋值运算符定义变量&lt;/li&gt;
&lt;li&gt;将对象作为实参传递给一个非引用类型的形参（值传递）&lt;/li&gt;
&lt;li&gt;将一个返回类型为非引用类型的函数返回一个对象（返回值）&lt;/li&gt;
&lt;li&gt;用花括号列表初始化一个数组中的元素或一个聚合类中的成员&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（2）&lt;strong&gt;什么是直接初始化：在对象初始化时，通过括号给对象提供一定的参数，并且要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数&lt;/strong&gt;。 意思就是找到最匹配的一个构造函数来进行调用。不一定调用哪一个。&lt;/p&gt;
&lt;p&gt;（3）在底层实现中，可以看出编译器的思想是能不用临时对象就不用临时对象。因此对于下面这些拷贝初始化，都不会生成临时对象再进行拷贝或移动到目标对象，而是直接通过函数匹配调用相应的构造函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 ClassTest ct2 =&quot;ab&quot;; //相当于ClassTest ct2(&quot;ab&quot;);
2 ClassTest ct5 = ClassTest(&quot;ab&quot;); //相当于ClassTest ct5(&quot;ab&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（4）下面的语句，visual studio才会生成一个无名的临时对象（位于main函数的栈中），注意：f1的返回值类型是非引用的，f2的形参类型是非引用的。&lt;/p&gt;
&lt;p&gt;通常如下三种情况会生成临时对象：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以值的方式给函数传参；（调用拷贝构造）&lt;/li&gt;
&lt;li&gt;类型转换；&lt;/li&gt;
&lt;li&gt;函数需要返回一个对象时；&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;1 f1(); //临时对象用于存储f1的返回值
2 f2(ct1); //临时对象用于拷贝实参，并传入函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而下面则是直接传入赋值表达式左边对象地址，然后再对该对象进行移动拷贝，注意f1返回值类型是非引用的，如果是引用的，则会调用拷贝构造函数。&lt;/p&gt;
&lt;p&gt;上面这句话的意思是 把&lt;code&gt;f1()&lt;/code&gt;作为参数放进&lt;code&gt;ct6&lt;/code&gt;调用的&lt;code&gt;operator=&lt;/code&gt;重载函数里。然后这个函数里面把&lt;code&gt;f1()&lt;/code&gt;函数的返回的对象&lt;strong&gt;移动&lt;/strong&gt;到&lt;code&gt;ct6&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;本质上应该是复制。但是因为这里他写了移动构造所以采用了移动。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 ClassTest ct6 = f1();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（5）直接初始化和拷贝初始化效率基本一样，因为在底层的实现基本一样，所以将拷贝初始化改为直接初始化效率提高不大。&lt;/p&gt;
&lt;p&gt;（6）拷贝初始化什么时候使用了移动构造函数：&lt;strong&gt;当你定义了移动构造函数，下列情况将调用移动构造函数&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将一个返回类型为非引用类型的函数返回一个[临时]对象 [并赋值给一个对象]（如上面的&lt;code&gt;ClassTest ct6 = f1();&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（7）我们需要先知道什么时候调用拷贝构造函数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当用类一个对象去初始化另一个对象时。&lt;/li&gt;
&lt;li&gt;如果函数形参是类对象。&lt;/li&gt;
&lt;li&gt;如果函数返回值是类对象，函数执行完成返回调用时。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（8）拷贝初始化什么时候使用拷贝构造函数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;赋值表达式右边是一个对象&lt;/li&gt;
&lt;li&gt;直接初始化时，括号内的参数是一个对象&lt;/li&gt;
&lt;li&gt;用花括号列表初始化一个数组中的元素或一个聚合类中的成员&lt;/li&gt;
&lt;li&gt;将一个返回类型为引用类型的函数返回一个对象&lt;/li&gt;
&lt;li&gt;形参为非引用类型的函数，其中是将实参拷贝到临时对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总之，拷贝构造函数有严格的参数限制。只能是本类的对象初始化本类的对象。如果用其他类对象初始化本类对象，那么形参必须是其他类对象。那么这个东西叫做构造函数。而不是拷贝构造函数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;类 &lt;code&gt;T&lt;/code&gt; 的复制构造函数是首个形参是 T&amp;amp;、const T&amp;amp;、volatile T&amp;amp; 或 const volatile T&amp;amp;，而且要么没有其他形参，要么剩余形参均有默认值的非模板构造函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（9）什么时候使用到拷贝赋值运算符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;赋值表达式右边是一个左值对象（如果需要，可以调用构造函数类型转换，生成一个临时对象）&lt;/li&gt;
&lt;li&gt;当赋值表达式右边是一个右值对象，且没有定义移动赋值运算符函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（10）什么时候使用移动赋值运算符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当赋值表达式右边是一个右值对象，且定义了移动赋值运算符函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（11）即使编译器略过了拷贝/移动构造函数，但是在这个程序点上，拷贝/移动构造函数必须存在且是可访问的（例如：不能是private），如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassTest ct2 = &quot;ab&quot;;//复制初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器会将其等同下面的语句，调用的是ClassTest的ClassTest(const char *pc)构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassTest ct2(&quot;ab&quot;);//直接初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是ClassTest的拷贝或移动构造函数需要定义至少其中一个，否则会报错&lt;/p&gt;
&lt;p&gt;拷贝初始化看起来像是给变量赋值，实际上是执行了初始化操作，与先定义再赋值本质不同。 对于内置类型变量（如 int，double，bool 等），直接初始化与拷贝初始化差别可以忽略不计。&lt;/p&gt;
&lt;p&gt;对于类类型的变量（如string或其他自定义类型）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;直接初始化调用类的构造函数&lt;/strong&gt;（&lt;strong&gt;调用参数类型最佳匹配的那个, 包括拷贝和移动构造。&lt;/strong&gt;）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;T s(...);&lt;/code&gt;或者&lt;code&gt;T s{...};&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;从明确的构造函数实参的集合初始化对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接初始化会选择最匹配的一个构造函数。并且考虑全部的构造函数，和所有用户定义转换函数。不会忽略explicit修饰的构造函数和explicit 修饰的用户定义转换函数&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接初始化看上去就是显式地调用类型的构造函数进行初始化。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式地调用构造函数进行直接初始化实际上是显式类型转换的一种。我们称之为functional cast。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;直接初始化会期待一个从初始化器到 &lt;em&gt;目标类型&lt;/em&gt; 的构造函数实参的隐式转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;拷贝（复制）初始化也会选择最匹配的一个构造函数（重申包括拷贝和移动构造），但是忽略任何explicit修饰的构造函数和explicit 修饰的用户定义转换函数。&lt;/strong&gt; (也就是看到等号就是拷贝初始化)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;T s = ...;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;从另一个对象初始化对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果 &lt;em&gt;目标类型&lt;/em&gt; 是类类型&lt;/strong&gt;，&lt;strong&gt;且 &lt;em&gt;其他对象&lt;/em&gt; 的类型&lt;/strong&gt;的无 cv 限定版本&lt;strong&gt;是 &lt;em&gt;目标类型&lt;/em&gt; 或是从 &lt;em&gt;目标类型&lt;/em&gt; 派生的类&lt;/strong&gt;，&lt;strong&gt;那么检测 &lt;em&gt;目标类型&lt;/em&gt; 的非显式构造函数，并由重载决议选择最佳匹配。然后调用构造函数以初始化该对象&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果 &lt;em&gt;目标类型&lt;/em&gt; 是类类型&lt;/strong&gt;，&lt;strong&gt;且 &lt;em&gt;其他对象&lt;/em&gt; 的类型&lt;/strong&gt;的无 cv 限定版本&lt;strong&gt;不是 &lt;em&gt;目标类型&lt;/em&gt; 或从 &lt;em&gt;目标类型&lt;/em&gt; 派生&lt;/strong&gt;，或如果 &lt;em&gt;目标类型&lt;/em&gt; 是非类类型，但 &lt;em&gt;其他对象&lt;/em&gt; 的类型是类类型，&lt;strong&gt;那么检测能从 &lt;em&gt;其他对象&lt;/em&gt; 的类型转换到 &lt;em&gt;目标类型&lt;/em&gt;&lt;/strong&gt; （或当 &lt;em&gt;目标类型&lt;/em&gt; 是类类型且存在转换函数时，转换到从 &lt;em&gt;目标类型&lt;/em&gt; 派生的类型）&lt;strong&gt;的用户定义转换序列，并通过重载决议选择最佳者&lt;/strong&gt;。&lt;strong&gt;该转换的结果&lt;/strong&gt;（如果使用&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/converting_constructor&quot;&gt;转换构造函数&lt;/a&gt;，那么就是无 cv 限定的 &lt;em&gt;目标类型&lt;/em&gt; 的右值临时量 (C++11 前)纯右值临时量 (C++11 起)(C++17 前)纯右值表达式 (C++17 起)）&lt;strong&gt;会被用于直接初始化该对象&lt;/strong&gt;。最后一步通常会被&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/copy_elision&quot;&gt;优化掉&lt;/a&gt;，而直接将转换结果构造于分配给目标对象的内存之中，但即便不会使用，也要求适合的构造函数（移动或复制）是可以访问的。 (C++17 前)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果 &lt;em&gt;其他对象&lt;/em&gt; 是右值表达式，那么重载决议会选择&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/move_constructor&quot;&gt;移动构造函数&lt;/a&gt;并在复制初始化期间调用它。没有“移动初始化”的概念。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;**所以他最终会尝试去对等号右侧的值进行一个隐式类型转换（创建临时对象）以符合要求。拷贝（复制）初始化要求编译器将右侧运算对象对象拷贝到正在创建的对象中 **。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;所以最后一定会调用拷贝或移动构造函数来使用等号右侧的对象来初始化左侧对象。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;等号右侧的对象在构造中会涉及到类型转换，这时候就是调用对应的构造函数或类型转换函数了，但是最后等号右侧的对象类型一定和等号左侧的对象类型相同。因为只有这样才可以调用拷贝构造或移动构造。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拷贝（复制）初始化会忽略掉任何explicit 修饰的构造函数。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复制初始化中的隐式转换必须从初始化器直接生成 &lt;em&gt;目标类型&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特别的，当对类类型变量进行初始化时，如果类的构造函数采用了 explicit 修饰而且需要隐式类型转换时，则只能通过直接初始化而不能通过拷贝初始化进行操作。因为explicit禁止了对等号右侧的那个临时对象使用隐式类型转换。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果自己实验发现没调用拷贝构造，把编译器优化关了就好了。&lt;/li&gt;
&lt;li&gt;注意C++17后有拷贝省略。看杂记3&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数的值传递，和函数的值返回都是拷贝初始化&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;所以在函数传值的时候，防止隐式类型转换可以通过给转换（构造函数）加&lt;code&gt;explicit&lt;/code&gt;来解决。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以一般来说，复制初始化可能会发生隐式或显式（这时候是手动，不受explicit限制）类型转换（生成临时对象），然后通过移动或拷贝的方式初始化左侧的对象。注意，这个移动或拷贝意思是会调用移动或拷贝构造函数。因为此时是拷贝初始化的语义：通过对象来初始化对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意是可能发生类型转换，什么时候不发生呢？就是当等号右侧和左侧的对象类型相同的时候。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而直接初始化，语义比较明确，直接通过传入的参数来寻找对应的构造函数，一步到位。符合直接初始化的语义：从明确的构造函数实参的集合初始化对象。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;所以explicit卡在哪儿了？卡在隐式类型转换所需的构造等号右侧的临时对象了。&lt;/h2&gt;
&lt;p&gt;我们上面提到了如果其他对象的类型不是目标类型或从目标类型派生，那就需要找能转换的转换序列（包括转换构造函数和用户定义转换函数）。&lt;strong&gt;说人话就是我们要使用等号右面的这个东西，构建出一个等号左边的类型的临时对象。但是在这个临时对象的构造中&lt;/strong&gt;（转换序列的查找中），&lt;strong&gt;忽略掉所有带&lt;code&gt;explicit&lt;/code&gt;声明符的可用函数。这时候自然就防止了隐式类型转换。因为一般情况下，我们期望的是提供一系列参数，然后进行对象构造。按理说直接初始化提供的参数和拷贝初始化提供的参数是相同的。因为在拷贝初始化临时对象后，会调用拷贝构造或移动构造来对左侧的对象进行初始化。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是，它有时候想的和你不太一样。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explicit myobj(int x):val(new int(x)){ //explicit
    cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
}
myobj(int x, int y = 20):val(new int(x)){ //带默认值
    cout &amp;lt;&amp;lt;&quot;const with default value&quot; &amp;lt;&amp;lt; endl;
}
myobj b = 10; //可以

/*
const with default value
mv
dest
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这个时候为啥可以呢？因为尽管通过&lt;code&gt;10&lt;/code&gt;来构造&lt;code&gt;myobj&lt;/code&gt;临时对象的时候，第一个匹配的构造函数是&lt;code&gt;explicit&lt;/code&gt;不能用，但是第二个可以用。所以此时依旧可以拷贝初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原文地址：&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/cposture/p/4925736.html&lt;/p&gt;
&lt;p&gt;https://nettee.github.io/posts/2018/What-happens-when-return-an-object-by-value-in-Cpp/&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/apocelipes/p/14415033.html&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/apocelipes/p/14415033.html#%E4%BB%80%E4%B9%88%E6%98%AF%E9%9A%90%E5%BC%8F%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2&lt;/p&gt;
&lt;p&gt;自己的提问 https://stackoverflow.com/questions/75480280/in-c-under-the-case-t-object-other-it-is-direct-initialization-or-copy#75480337&lt;/p&gt;
&lt;h1&gt;直接初始化，拷贝初始化的语义差别。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class otherc{
    public:
    otherc() = default;
    otherc(int x):val(x){
        cout &amp;lt;&amp;lt;&quot;otherc&quot; &amp;lt;&amp;lt; endl;
    };
    int val = 20;
};


class myobj{
    public:
        myobj() = default;
        myobj(const otherc&amp;amp;){
            cout &amp;lt;&amp;lt;&quot;convert const&quot; &amp;lt;&amp;lt; endl;
            val = new int(10);
        }
        myobj(int x):val(new int(x)){ //构造
            cout &amp;lt;&amp;lt;&quot;const&quot; &amp;lt;&amp;lt; endl;
        }
        // int&amp;amp; getval(){
        //     return *val;
        // }

        myobj(const myobj&amp;amp; obj){ //拷贝构造
            cout &amp;lt;&amp;lt;&quot;copy const&quot; &amp;lt;&amp;lt; endl;
            val = new int(*obj.val);
        }
        myobj&amp;amp; operator=(const myobj &amp;amp; rhs){ //拷贝赋值
            cout &amp;lt;&amp;lt;&quot;copy= const&quot; &amp;lt;&amp;lt; endl;
            if(this == &amp;amp;rhs){
                return *this;
            }
            delete this-&amp;gt;val;
            val = new int(*rhs.val);
            return *this;
        }

        myobj(myobj&amp;amp;&amp;amp; obj){ //移动构造
            cout &amp;lt;&amp;lt;&quot;mv&quot; &amp;lt;&amp;lt; endl;
            val = obj.val;
            obj.val = new int(3939);
        }

        myobj&amp;amp; operator=(myobj&amp;amp;&amp;amp; rhs){ //移动赋值
            cout &amp;lt;&amp;lt;&quot;mv=&quot; &amp;lt;&amp;lt; endl;
            if(this == &amp;amp;rhs){
                return *this;
            }
            if(this-&amp;gt;val != nullptr){
                delete val;
            }
            val = rhs.val;
            rhs.val = nullptr; 
            return *this;

        }
        ~myobj(){
            cout &amp;lt;&amp;lt;&quot;dest&quot; &amp;lt;&amp;lt; endl;
            delete val;
            val = nullptr;
        }


        int* val;
        
};

int main(){

    otherc c(10);
    myobj other(c);
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下，输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;otherc
convert const
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为是构建了对象&lt;code&gt;c&lt;/code&gt;，然后&lt;code&gt;other&lt;/code&gt;直接初始化，找到了最匹配的一个构造函数（接受&lt;code&gt;otherc&lt;/code&gt;类型的那一个）。直接进行了构造函数调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是，如果我们使用拷贝初始化&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj other = c;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则会输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;otherc
convert const
mv
dest
dest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为这时候相当于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj temp(c);
myobj other(move(temp));
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果 &lt;em&gt;目标类型&lt;/em&gt; 是类类型&lt;/strong&gt;，&lt;strong&gt;且 &lt;em&gt;其他对象&lt;/em&gt; 的类型&lt;/strong&gt;的无 cv 限定版本&lt;strong&gt;不是 &lt;em&gt;目标类型&lt;/em&gt;&lt;/strong&gt; 或从 &lt;em&gt;目标类型&lt;/em&gt; 派生，或如果 &lt;em&gt;目标类型&lt;/em&gt; 是非类类型，但 &lt;em&gt;其他对象&lt;/em&gt; 的类型是类类型，&lt;strong&gt;那么检测能从 &lt;em&gt;其他对象&lt;/em&gt; 的类型转换到 &lt;em&gt;目标类型&lt;/em&gt;&lt;/strong&gt; （或当 &lt;em&gt;目标类型&lt;/em&gt; 是类类型且存在转换函数时，转换到从 &lt;em&gt;目标类型&lt;/em&gt; 派生的类型）&lt;strong&gt;的用户定义转换序列，并通过重载决议选择最佳者&lt;/strong&gt;。&lt;strong&gt;该转换的结果&lt;/strong&gt;（如果使用&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/converting_constructor&quot;&gt;转换构造函数&lt;/a&gt;，那么就是无 cv 限定的 &lt;em&gt;目标类型&lt;/em&gt; 的右值临时量 (C++11 前)纯右值临时量 (C++11 起)(C++17 前)纯右值表达式 (C++17 起)）&lt;strong&gt;会被用于直接初始化该对象&lt;/strong&gt;。最后一步通常会被&lt;a href=&quot;https://zh.cppreference.com/w/cpp/language/copy_elision&quot;&gt;优化掉&lt;/a&gt;，而直接将转换结果构造于分配给目标对象的内存之中，但即便不会使用，也要求适合的构造函数（移动或复制）是可以访问的。 (C++17 前)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以，我们先有一个转换的结果，然后用结果直接初始化。&lt;/p&gt;
&lt;p&gt;转换的结果就是&lt;code&gt;myobj temp(c)&lt;/code&gt;。直接初始化就是&lt;code&gt;myobj other(move(temp))&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再来看一个简单的例子：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myobj a(10);
/*
const
dest
*/
myobj b = 10;
/*
const
mv
dest
dest
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;发现了吗？再次重申：拷贝初始化的核心语义是通过&lt;strong&gt;对象&lt;/strong&gt;来初始化对象，并且采用”拷贝“的方式。因为我们等号右侧会生成临时对象，然后通过拷贝或移动的方式初始化我们的真正对象。&lt;/li&gt;
&lt;li&gt;例子当中，第一个&lt;code&gt;const&lt;/code&gt;是等号右侧的&lt;code&gt;10&lt;/code&gt;发生了隐式类型转换，生成了&lt;code&gt;myobj&lt;/code&gt;的临时对象，然后调用移动构造函数移动到&lt;code&gt;b&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;右值引用 移动语义 完美转发 万能引用&lt;/h1&gt;
&lt;h2&gt;右值引用&lt;/h2&gt;
&lt;h3&gt;什么是左值？&lt;/h3&gt;
&lt;p&gt;描述： 能够用&lt;code&gt;&amp;amp;&lt;/code&gt;取地址的表达式是左值表达式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个区分左值和右值的便捷方法：看能不能对表达式取地址，若能，则为左值，若不能则为右值。所有的具名变量都是左值，而右值是不具名的。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数名和变量名&lt;/li&gt;
&lt;li&gt;返回左值引用的函数调用&lt;/li&gt;
&lt;li&gt;前置自增/自减运算符连接的表达式&lt;code&gt;++i&lt;/code&gt;/&lt;code&gt;-–i&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;由赋值运算符或复合赋值运算符连接的表达式 &lt;code&gt;a=b&lt;/code&gt;、&lt;code&gt;a+=b&lt;/code&gt;、&lt;code&gt;a%=b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;解引用表达式&lt;code&gt;*p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;字符串字面值&lt;code&gt;”abc”&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具名的右值引用本身是左值&lt;/strong&gt; &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;什么是纯右值？（prvalue）&lt;/h3&gt;
&lt;p&gt;描述： 满足下列条件之一的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;本身就是赤裸裸的、纯粹的字面值，如3、false；&lt;/li&gt;
&lt;li&gt;求值结果相当于字面值 或 是一个不具名的临时对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;除字符串字面值以外的字面值。比如&lt;code&gt;42&lt;/code&gt;，&lt;code&gt;true&lt;/code&gt;,&lt;code&gt;nullptr&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;临时对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回非引用类型的函数调用(表达式)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;翻译过来叫做按值返回的函数调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后置自增/自减运算符连接的表达式&lt;code&gt;i++&lt;/code&gt;/&lt;code&gt;i-–&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;算术表达式（&lt;code&gt;a+b&lt;/code&gt;、&lt;code&gt;a&amp;amp;b&lt;/code&gt;、&lt;code&gt;a&amp;lt;=b&lt;/code&gt;、&lt;code&gt;a&amp;lt;b&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取地址表达式&lt;code&gt;&amp;amp;a&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt;指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个转换至非引用类型的类型转换表达式。如&lt;code&gt;static_cast(x)&lt;/code&gt;, &lt;code&gt;std::string{}&lt;/code&gt;, &lt;code&gt;(int)42&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;lambda表达式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;什么是将亡值？（xvalue）&lt;/h3&gt;
&lt;p&gt;描述：在C++11之前的右值和C++11中的纯右值是等价的。C++11中的将亡值是随着右值引用的引入而新引入的。换言之，“将亡值”概念的产生，是由右值引用的产生而引起的，将亡值与右值引用息息相关。所谓的将亡值表达式，就是下列表达式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;返回右值引用的函数的调用表达式，例如&lt;code&gt;std::move(x)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;转换为右值引用的转换函数的调用表达式，例如 &lt;code&gt;static_cast&amp;lt;char&amp;amp;&amp;amp;&amp;gt;(x)；&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在C++11中，用左值去初始化一个对象或为一个已有对象赋值时，会调用拷贝构造函数或拷贝赋值运算符来拷贝资源，而当用一个右值（包括纯右值和将亡值）来初始化或赋值时，会调用移动构造函数或移动赋值运算符来移动资源，从而避免拷贝，提高效率。当该右值完成初始化或赋值的任务时，它的资源已经移动给了被初始化者或被赋值者，同时该右值也将会马上被销毁（析构）。也就是说，当一个右值准备完成初始化或赋值任务时，它已经“将亡”了。这种右值常用来完成移动构造或移动赋值的特殊任务，扮演着“将亡”的角色，所以C++11给这类右值起了一个新的名字——将亡值。
&lt;strong&gt;&lt;code&gt;declval&lt;/code&gt;也算。因为&lt;code&gt;is_same&amp;lt;int&amp;amp;&amp;amp;, decltype(declval&amp;lt;int&amp;gt;())&amp;gt;::value&lt;/code&gt;是true。因为本身它就是&lt;code&gt;add_rvalue_reference&lt;/code&gt;，也就是&lt;code&gt;static_cast&amp;lt;T&amp;amp;&amp;amp;&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;详细情况：&lt;/h3&gt;
&lt;h4&gt;情况1： ++i是左值，而i++是右值&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;++i&lt;/code&gt;对i加1后再赋给&lt;code&gt;i&lt;/code&gt;，最终的返回值就是&lt;code&gt;i&lt;/code&gt;，所以，&lt;code&gt;++i&lt;/code&gt;的结果是具名的，名字就是&lt;code&gt;i&lt;/code&gt;；
而对于&lt;code&gt;i++&lt;/code&gt;而言，是先对&lt;code&gt;i&lt;/code&gt;进行一次拷贝，将得到的副本作为返回结果，然后再对&lt;code&gt;i&lt;/code&gt;加1，由于&lt;code&gt;i++&lt;/code&gt;的结果是对&lt;code&gt;i&lt;/code&gt;加1前&lt;code&gt;i&lt;/code&gt;的一份拷贝，所以它是不具名的。假设自增前&lt;code&gt;i&lt;/code&gt;的值是6，那么，&lt;code&gt;++i&lt;/code&gt;得到的结果是7，这个7有个名字，就是&lt;code&gt;i&lt;/code&gt;；而&lt;code&gt;i++&lt;/code&gt;得到的结果是6，这个6是&lt;code&gt;i&lt;/code&gt;加1前的一个副本，它没有名字，&lt;code&gt;i&lt;/code&gt;不是它的名字，&lt;code&gt;i&lt;/code&gt;的值此时也是7。
可见，&lt;code&gt;++i&lt;/code&gt;和&lt;code&gt;i++&lt;/code&gt;都达到了使i加1的目的，但两个表达式的结果不同。&lt;/p&gt;
&lt;h4&gt;情况2： 解引用表达式&lt;code&gt;*p&lt;/code&gt;是左值，取地址表达式&lt;code&gt;&amp;amp;a&lt;/code&gt;是纯右值&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;(p)&lt;/code&gt;一定是正确的，因为&lt;code&gt;p&lt;/code&gt;得到的是&lt;code&gt;p&lt;/code&gt;指向的实体，&lt;code&gt;&amp;amp;(p)&lt;/code&gt;得到的就是这一实体的地址，正是&lt;code&gt;p&lt;/code&gt;的值。由于&lt;code&gt;&amp;amp;(p)&lt;/code&gt;的正确，所以&lt;code&gt;*p&lt;/code&gt;是左值。
而对&lt;code&gt;&amp;amp;a&lt;/code&gt;而言，得到的是&lt;code&gt;a&lt;/code&gt;的地址，相当于&lt;code&gt;unsigned int&lt;/code&gt;型的字面值，所以是纯右值。&lt;/p&gt;
&lt;h4&gt;情况3： &lt;code&gt;a+b&lt;/code&gt;、&lt;code&gt;a&amp;amp;&amp;amp;b&lt;/code&gt;、&lt;code&gt;a==b&lt;/code&gt;都是纯右值&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;a+b&lt;/code&gt;得到的是不具名的临时对象，而&lt;code&gt;a&amp;amp;&amp;amp;b&lt;/code&gt;和&lt;code&gt;a==b&lt;/code&gt;的结果非&lt;code&gt;true&lt;/code&gt;即&lt;code&gt;false&lt;/code&gt;，相当于字面值。&lt;/p&gt;
&lt;h4&gt;情况4： 字符串字面值是左值，而非字符串的字面量是纯右值&lt;/h4&gt;
&lt;p&gt;不是所有的字面值都是纯右值，字符串字面值是唯一例外。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意一下 虽然字符串字面值是左值，但是依旧不能用左值引用。因为字符串是常量 所以要么加&lt;code&gt;const&lt;/code&gt;要么用右值引用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string &amp;amp;s = &quot;abc&quot;; // 错误。 虽然字符串字面值是左值，但是依旧不能用普通左值引用。因为字符串是常量 所以要么加const变成常量左值引用，要么用右值引用。
const string &amp;amp;s = &quot;abc&quot;; //正确 常量引用
string &amp;amp;&amp;amp;s = &quot;abc&quot;; // 右值引用这里是为了接管所有权。所以相当于把“abc&quot;接管了。因为string类有自己的移动构造
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;情况5： 具名的右值引用是左值，不具名的右值引用是右值。&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;void B(int&amp;amp;&amp;amp; ref_r) {
    ref_r = 1;
}
//这里的ref_r是左值
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;为啥右值不能绑定给左值引用，也就是为啥右值不能取地址？为什么右值可以绑定给常量左值引用？&lt;/h2&gt;
&lt;p&gt;所有规则都是人订的。其实就仨字：没意义。&lt;/p&gt;
&lt;p&gt;我们有一个5。他在这了。你修改它有意义吗？没意义。取地址有意义吗？没意义。硬取可以取，万物皆有地址。但是没意义。&lt;/p&gt;
&lt;p&gt;我们有一个临时变量。注意临时变量你看不见。你看不见所以你也动不了他，那给他取个地址有意义吗？没意义。修改有意义吗？没意义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**为什么左值引用没法指向右值呢？因为一般来说右值都是临时变量，而且右值是没有地址的。我们也知道引用是别名。所以左值引用无法修改右值，也无法指向右值。这也解释了为什么const 常量左值引用可以指向右值。因为常量左值引用保证用户无法修改指向的值。**所以我们能看到vector的push_back使用了常量左值引用为形参。不然你没办法执行&lt;code&gt;v.push_back(5);&lt;/code&gt;这样的语句&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.cnblogs.com/philip-tell-truth/p/6370019.html&lt;/p&gt;
&lt;p&gt;https://youtu.be/wkWtRDrjEH4?t=359&lt;/p&gt;
&lt;h3&gt;从语义上分析：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为如果我们按照左值引用传递，语义上，函数会修改这个参数，而且调用方也希望看到这个修改。也就是副作用是可见的。但是如果我们传入一个右值，函数修改参数了，但是调用方无法看到这个修改，因为是临时对象。所以语义说不通。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以，为什么常量左值引用可以接一切？因为常量左值引用保证了我们不会修改这个参数。可以维持住一个不修改的语义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为什么右值引用传递不能传入左值引用？也是语义不通。右值引用传递，语义上是表明，这个值会被修改且调用方不再会使用这个对象存储的值。所以说如果传入左值引用，无法保证调用方不再使用的语义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;针对&lt;code&gt;move&lt;/code&gt;可以是因为程序员接管了这个保证语义正确的过程。也就是编译器允许程序员对语义进行把控。禁止传入本质上是一种提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左值引用语义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数会修改数据。且调用方会观测任何的修改行为（副作用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常量左值引用语义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数不会修改数据。在这个行为下，针对这个数据没有副作用。所以调用方是否观测不重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右值引用语义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数会修改数据。且调用方不会观测任何的修改行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;万能引用 （原来叫universal reference，现在叫forwarding reference）&lt;/h2&gt;
&lt;p&gt;假设我们现在有一个需求。即加法函数。&lt;/p&gt;
&lt;p&gt;我们可能会写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int add(const int&amp;amp; x, const int&amp;amp; y){
    return x + y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们可以这样使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int a = 10;
    int b = 20;
    add(a, b); //return 30
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是加法不可以直接接受数字 如 &lt;code&gt;add(10, 20)&lt;/code&gt;似乎不是很直观。但是因为10 和 20都是右值。当然了，我们有了右值的概念，现在它难不倒我们。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int add(const int&amp;amp;&amp;amp; x, const int&amp;amp;&amp;amp; y){
    return x + y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们可以这样用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    add(10, 20);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是现在它无法接受左值了。如 &lt;code&gt;add(a, b);&lt;/code&gt;。 尽管我们可以写出重载，但是有点麻烦。这样我们引出了万能引用,可以让这个函数不仅可以接受左值也可以接受右值。万能引用和右值引用的一个主要区别在于看模板参数和是不是auto声明 如果一个变量或者参数被声明为T&amp;amp;&amp;amp;，其中T是被推导的类型，那这个变量或者参数就是一个universal reference。也就是说，&lt;strong&gt;只有发生类型推导的时候，T&amp;amp;&amp;amp;才表示万能引用&lt;/strong&gt;；否则，表示右值引用。&lt;/p&gt;
&lt;p&gt;现在我们可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
int add(T&amp;amp;&amp;amp; x, T&amp;amp;&amp;amp; y){
    return x + y;
}
//这样如下代码可以通过编译
int main(){
    int a = 5;
    int b = 10;
    add(a, b);
    add(5, 10);
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为此时发生了类型推导。所以是万能引用。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;但是注意，因为底层用到了&lt;code&gt;bind&lt;/code&gt;所以如果这样写&lt;/s&gt; 不要这么玩。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
int add(const T&amp;amp;&amp;amp; x, const T&amp;amp;&amp;amp; y){
    return x + y;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则需要显式使用&lt;code&gt;std::cref&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    int a = 5;
    int b = 10;
    add(a, b); //失败
    add(5, 10); //可以
    add(std::cref(a), std::cref(b)); //可以
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;下面我们来一点小贴士&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 1;
int &amp;amp;b = a; // 正确 左值引用
int &amp;amp;b = 1; // 错误 左值引用了右值
int &amp;amp;&amp;amp;b = 1; // 正确 正确的右值引用
int &amp;amp;&amp;amp;b = a; //错误 右值引用只能引用右值
int &amp;amp;&amp;amp;b = move(a); //正确 move（移动语义）可以将左值转换为右值然后进行右值引用。
int &amp;amp;&amp;amp;b = (int&amp;amp;&amp;amp;)a; // 正确 强制将左值a转换为右值(临时变量)形式并引用。尽量不要用这种方式，能用move就用move



int &amp;amp;&amp;amp;a = 1; // 这叫右值引用
void test(int &amp;amp;&amp;amp; a){} //这也叫右值引用

auto &amp;amp;&amp;amp;b = 1; // 这不叫右值引用，这叫万能引用。因为是auto关键词修饰。所有的auto&amp;amp;&amp;amp;都是万能引用，因为总是有参数的推导过程

template&amp;lt;typename t&amp;gt;
void func(vector&amp;lt;t&amp;gt;&amp;amp;&amp;amp; param){}// 这不是万能引用， 是右值引用。虽然然有推导过程，但是这里模板只推导t的类型。但实际上param的类型一直都是vector&amp;lt;t&amp;gt;&amp;amp;&amp;amp;

template&amp;lt;typename t&amp;gt;
void func(t&amp;amp;&amp;amp; param){} // 这不是右值引用，这是万能引用。因为整个param的类型都需要被推导。

template&amp;lt;class T&amp;gt;
struct A
{
    template&amp;lt;class U&amp;gt;
    A(T&amp;amp;&amp;amp; x, U&amp;amp;&amp;amp; y, int* p); // x 不是转发引用：T 不是构造函数的类型模板形参。也就是T不涉及型别推导。
                             // 但 y 是转发引用。因为U是构造函数的类型模板形参。涉及到型别推导。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;小技巧&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;code&gt;T&lt;/code&gt;和&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;中间有&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;间隔，那么就&lt;strong&gt;不是&lt;/strong&gt;万能引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不涉及型别推导，那么就算是&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;也&lt;strong&gt;不是&lt;/strong&gt;万能引用。（&lt;strong&gt;格外注意！！！！&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当参数加上&lt;code&gt;const&lt;/code&gt;的时候一定&lt;strong&gt;不是&lt;/strong&gt;万能引用，是右值引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;所有的&lt;code&gt;auto&amp;amp;&amp;amp;&lt;/code&gt;都是万能引用&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;除了从花括号包围的初始化器列表推导时。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具名的右值引用本身是个左值。所以它有地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若采用右值来初始化万能引用，就会得到一个右值引用，如果采用左值来初始化，则会得到一个左值引用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;万能引用只可在函数模板中被推导。类模板无法被推导，因为类模板的类型在实例化的时候已经被确定。所以如果想要在模板类中针对某一函数使用万能引用，则该函数也要为模板函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;显式禁用某个函数接受右值引用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt; 
void f(const T&amp;amp;&amp;amp;) = delete;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;const T&amp;amp;&amp;amp;&lt;/code&gt;允许你重载一个函数模板，它只接受右值引用。如果&lt;code&gt;const T&amp;amp;&amp;amp;&lt;/code&gt;也被当做&lt;code&gt;universal reference&lt;/code&gt;，那么将没有办法让函数只接受右值引用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-06-%E6%9D%82%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20221128143238.png&quot; alt=&quot;QQ截图20221128143238&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;引用折叠（Universal Collapse）&lt;/h2&gt;
&lt;p&gt;万能引用说完了，接着来聊引用折叠（Universal Collapse），因为完美转发（Perfect Forwarding）的概念涉及引用折叠。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只有万能引用（auto, decltype, typedef）才有引用折叠&lt;/strong&gt; 为什么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void refd(T&amp;amp; obj){
    cout &amp;lt;&amp;lt; obj &amp;lt;&amp;lt; endl;
}
refd(T);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们这个函数，只可以接受左值。而且，这个时候无论函数实参是否是引用，&lt;code&gt;T&lt;/code&gt;都会被推导为&lt;code&gt;T&lt;/code&gt;，因为引用属性会被去掉。看一下下面函数模板参数推导的解释即可。&lt;/p&gt;
&lt;p&gt;然而&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void refd(T&amp;amp;&amp;amp; obj){
    cout &amp;lt;&amp;lt; obj &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数的形参是个万能引用。在函数模板参数推导的解释中，只有这样，&lt;code&gt;T&lt;/code&gt;才有可能被推导（折叠）为左值引用或是普通的数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在实参传递给函数模板的时候，推导出来的模板形参会把实参是左值还是右值的信息&quot;编码&quot;到结果里面：如果传递的实参是个左值， &lt;code&gt;T&lt;/code&gt;的推导结果就是个左值引用型别；如果传递的实参是个右值， &lt;code&gt;T&lt;/code&gt;的推导结果就是个非引用型别（&lt;strong&gt;注意这里的非对称性：左值的编码结果为左值引用型别，但右值的编码结果却是非引用型别&lt;/strong&gt;。后续&lt;code&gt;forward&lt;/code&gt;或&lt;code&gt;move&lt;/code&gt;会使用这个特点）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void func(T&amp;amp;&amp;amp; param);

Widget WidgetFactory() //返回右值
Widget w;

func(w);               		//T的推导结果是左值引用类型，T的结果推导为Widget&amp;amp;
func(WidgetFactory());   	//T的推导结果是非引用类型（注意这个时候不是右值），T的结果推导为Widget.因为WidgetFactory()函数返回的是一个右值，然后被推导为非引用类型。这个时候使用完美转发，会被forward推导为&amp;amp;&amp;amp;。就变成右值了。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++中，“引用的引用”是违法的，但是上面T的推导结果是&lt;code&gt;Widget&amp;amp;&lt;/code&gt;时，就会出现 &lt;code&gt;void func(Widget&amp;amp; &amp;amp;&amp;amp; param);&lt;/code&gt;左值引用+右值引用&lt;/p&gt;
&lt;p&gt;C++有单独的规则来把类型推断中出现的引用的引用转换为单个引用，称为“引用折叠”。折叠规则为：&lt;/p&gt;
&lt;p&gt;一个模板函数，根据定义的形参和传入的实参的类型，我们可以有下面四中组合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左值-左值&lt;code&gt; T&amp;amp;&lt;/code&gt; &lt;code&gt;&amp;amp;&lt;/code&gt; # 函数定义的&lt;strong&gt;形参类型&lt;/strong&gt;是左值引用，&lt;strong&gt;传入的实参&lt;/strong&gt;是左值引用&lt;/li&gt;
&lt;li&gt;左值-右值 &lt;code&gt;T&amp;amp;&lt;/code&gt; &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; # 函数定义的形参类型是左值引用，传入的实参是右值引用&lt;/li&gt;
&lt;li&gt;右值-左值 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt; &lt;code&gt;&amp;amp;&lt;/code&gt; # 函数定义的形参类型是右值引用，传入的实参是左值引用&lt;/li&gt;
&lt;li&gt;右值-右值 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt; &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; # 函数定义的形参类型是右值引用，传入的实参是右值引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是C++中不允许对引用再进行引用，对于上述情况的处理有如下的规则：&lt;/p&gt;
&lt;p&gt;所有的折叠引用最终都代表一个引用，要么是左值引用，要么是右值引用。规则是：&lt;strong&gt;如果任一引用为左值引用，则结果为左值引用。否则（即两个都是右值引用），结果为右值引用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;即就是前面三种情况代表的都是左值引用，而第四种代表的右值引用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用折叠是为了让移动语义&lt;code&gt;move&lt;/code&gt;和完美转发&lt;code&gt;forward&lt;/code&gt;正常运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;值类别（Value category）和 数据类型（data type)&lt;/h2&gt;
&lt;p&gt;值类别只有五种：泛左值（glvalue)，纯右值(prvalue)，将亡值(xvalue)，左值(lvalue)，右值(rvalue)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int&amp;amp;&amp;amp; foo = 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;foo&lt;/code&gt;的data type数据类型是右值引用（5）。但是&lt;code&gt;foo&lt;/code&gt;的Value category值类别是左值（因为有一个名字）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以我们说。具名右值引用本身是左值。因为它的数据类型是右值引用，但是值类别是左值。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;小练习：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Widget* foo;
void someMethod(const Widget&amp;amp;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;哪一种值类别value category可以被传入someMethod()？
&lt;ul&gt;
&lt;li&gt;任意一种。因为常量左值引用接一切。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;哪一种数据类型data type可以被传入？
&lt;ul&gt;
&lt;li&gt;Widget。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;完美转发&lt;/h2&gt;
&lt;p&gt;为什么会有完美转发？我们知道&lt;strong&gt;具名的右值引用是左值&lt;/strong&gt;。那我们一个&lt;strong&gt;函数接受右值引用然后将其放入另一个函数的时候这个具名的右值引用会变成左值&lt;/strong&gt;，所以放入另一个函数的时候会变成左值。&lt;/p&gt;
&lt;h4&gt;完美转发的核心意义在于，我们要维持传入的参数的特性。我们传入的是左值，就维持左值特性。传入右值就维持右值特性。所以我们才使用forward。因为forward只有在传入的实参是右值的时候才会将其转换为右值。否则都当做左值看待。&lt;/h4&gt;
&lt;h4&gt;尽管move和forward的第一层作用都是维持右值的右值性，但是forward是move的超集。因为forward可以搭配万能引用来进行坍缩（折叠）来维持左值的左值性。所以可以理解为完美转发 = 万能引用 + （引用折叠）+ forward&lt;/h4&gt;
&lt;p&gt;一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using std::cout;
using std::endl;
template&amp;lt;typename T&amp;gt;
void func(T&amp;amp; param) {
    cout &amp;lt;&amp;lt; &quot;传入的是左值&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T&amp;gt;
void func(T&amp;amp;&amp;amp; param) {
    cout &amp;lt;&amp;lt; &quot;传入的是右值&quot; &amp;lt;&amp;lt; endl;
}
template&amp;lt;typename T&amp;gt;
void warp(T&amp;amp;&amp;amp; param) {
    func(param);
}
int main() {
    int num = 2019;
    warp(num);
    warp(2019);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个程序会有如下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;传入的是左值&quot;
&quot;传入的是左值&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们传入的&lt;code&gt;2019&lt;/code&gt;在进入&lt;code&gt;warp&lt;/code&gt;函数的时候，这个右值有了名字。它叫做&lt;code&gt;param&lt;/code&gt;了。也就是它是一个具名右值引用。我们说过了具名右值引用是左值。所以会调用传入左值的&lt;code&gt;func&lt;/code&gt;函数。&lt;/p&gt;
&lt;p&gt;那我们如果&lt;strong&gt;依旧希望这个值是右值&lt;/strong&gt;应该怎么办？ 可以使用完美转发。&lt;code&gt;std::forward&amp;lt;T&amp;gt;(u)&lt;/code&gt;有两个参数：&lt;code&gt;T&lt;/code&gt;与 &lt;code&gt;u&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当函数入参&lt;code&gt;(param)&lt;/code&gt;为左值或左值引用类型时，&lt;code&gt;u&lt;/code&gt;将被转换为&lt;code&gt;T&lt;/code&gt;类型（左值引用)。(注意这里是转换不是保持。因为u(param)在传进来的时候形参是T&amp;amp;&amp;amp;。带了&amp;amp;&amp;amp;会触发推导。无论函数实参入参的时候是&lt;code&gt;int&lt;/code&gt;还是&lt;code&gt;int&amp;amp;&lt;/code&gt;统一会被推导为&lt;code&gt;int&amp;amp;&lt;/code&gt;。下面表格有)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上面我们说过，使用万能引用进行函数参数推导的时候，左值或左值引用类型的入参会被推断为左值引用类型。所以这里相当于维持或转换为左值引用的性质。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;否则&lt;code&gt;u&lt;/code&gt;将被转换为&lt;code&gt;T&lt;/code&gt;类型右值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上面我们说过，使用万能引用进行函数参数推导的时候，右值类型的入参会被推断为其本身的数据类型（不带引用）。这里就是维持右值特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;仅当传入的实参被绑定到右值时，&lt;code&gt; std::forward&lt;/code&gt;才针对该实参实施向右值型别的强制型别转换&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void warp(T&amp;amp;&amp;amp; param) {
    func(std::forward&amp;lt;T&amp;gt;(param));
}
int main(){
    int num = 2019;
    warp(2019); //这时候输出右值。
    warp(num); //这时候输出左值。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可能看起来有点奇怪。我们需要介绍一个很多人不会很仔细去看的小知识点 即 函数模板参数推导&lt;/p&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Tp&amp;gt; 
constexpr _Tp&amp;amp;&amp;amp;
forward(typename std::remove_reference&amp;lt;_Tp&amp;gt;::type&amp;amp; __t) noexcept //_Tp为左值引用的时候
{ return static_cast&amp;lt;_Tp&amp;amp;&amp;amp;&amp;gt;(__t); }

template&amp;lt;typename _Tp&amp;gt;
constexpr _Tp&amp;amp;&amp;amp;
forward(typename std::remove_reference&amp;lt;_Tp&amp;gt;::type&amp;amp;&amp;amp; __t) noexcept //_Tp为右值引用的时候。当且形如forward&amp;lt;int&amp;amp;&amp;amp;&amp;gt;(5)的时候或如同官方文档中的例子时才会匹配至这个版本
{return static_cast&amp;lt;_Tp&amp;amp;&amp;amp;&amp;gt;(__t);}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数模板参数推导&lt;/h2&gt;
&lt;p&gt;举例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void f(P param){ //此处P为故意。P可以代表 T/T&amp;amp;/T&amp;amp;&amp;amp;/const T/const T&amp;amp;/const T&amp;amp;&amp;amp;
    //这个param就是A。有的表格会把P换成A的部分。
    //...
}

int main(){
    f(A);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 P 和 T 有关。 有下列的推导规则:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; 的引用属性被忽略.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt; 是非引用时, &lt;code&gt;A&lt;/code&gt; 的 cv 限定符被忽略.&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;P&lt;/code&gt; 是无 cv 限定符的转发引用 (即 &lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;), 且&lt;code&gt; A&lt;/code&gt; 是左值时,&lt;code&gt;T&lt;/code&gt;被推导为左值引用.&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt; A&lt;/code&gt; 是数组或函数,&lt;code&gt; P&lt;/code&gt; 是值时, 数组和函数退化为指针. &lt;code&gt;P&lt;/code&gt; 是引用时, 不退化为指针.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里 cv 限定符指的是 const 和 volatile.&lt;/p&gt;
&lt;p&gt;下面我们看上面的规则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;warp(num);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里num是左值 即 &lt;code&gt;int&lt;/code&gt;类型, 函数头为&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;。参照下面的表格，&lt;code&gt;T&lt;/code&gt;被推导为&lt;code&gt;int&amp;amp;&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T = int&amp;amp;
void warp(int &amp;amp; &amp;amp;&amp;amp; param){
    func(std::forward&amp;lt;T&amp;gt;(param));
}
//折叠后：
T = int&amp;amp;
void warp(int&amp;amp; param){
    func(std::forward&amp;lt;int&amp;amp;&amp;gt;(param));
}
//套用forward
constexpr int&amp;amp; &amp;amp;&amp;amp; //折叠
    forward(typename std::remove_reference&amp;lt;int &amp;amp;&amp;gt;::type&amp;amp; __t) noexcept //remove_reference的作用与名字一致，就是去掉&amp;lt;&amp;gt;内类型的引用。这里就是int&amp;amp;变成int
{ return static_cast&amp;lt;int &amp;amp; &amp;amp;&amp;amp;&amp;gt;(__t); } //折叠
//折叠后：
constexpr int&amp;amp; //折叠
    forward(int&amp;amp; __t) noexcept
{ return static_cast&amp;lt;int&amp;amp;&amp;gt;(__t); } //折叠

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以最终会把参数&lt;code&gt;param&lt;/code&gt;强制转换为&lt;code&gt;int&amp;amp;&lt;/code&gt;一个左值引用。是左值，所以调用左值版本的&lt;code&gt;func&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;warp(2019);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里参数是右值。函数头为&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;。参照下面表格，&lt;code&gt;T&lt;/code&gt;被推导为&lt;code&gt;int&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T = int
void warp(int&amp;amp;&amp;amp; param){
    func(std::forward&amp;lt;T&amp;gt;(param));
}
//不发生折叠。处理后：
T = int
void warp(int&amp;amp;&amp;amp; param){
    func(std::forward&amp;lt;int&amp;gt;(param));
}
//套用forward
constexpr int &amp;amp;&amp;amp; 
    forward(typename std::remove_reference&amp;lt;int&amp;gt;::type&amp;amp; __t) noexcept //注意这里依旧匹配到的是&amp;amp;左值版本而不是&amp;amp;&amp;amp;右值版本
{ return static_cast&amp;lt;int &amp;amp;&amp;amp;&amp;gt;(__t); }
//处理后
constexpr int &amp;amp;&amp;amp;
    forward(int &amp;amp; __t) noexcept
{ return static_cast&amp;lt;int &amp;amp;&amp;amp;&amp;gt;(__t); } 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以最终会把参数&lt;code&gt;param&lt;/code&gt;强制转换为&lt;code&gt;int&amp;amp;&amp;amp;&lt;/code&gt;。是一个右值。所以调用右值版本的&lt;code&gt;func&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么这里的&lt;code&gt;forward&lt;/code&gt;依旧匹配的是左值版本而不是右值版本？我们说了，一旦右值参数被右值引用接住了，那么这时候这个参数就是左值了。所以说这时候&lt;code&gt;param&lt;/code&gt;本身是左值。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;那么什么时候可以匹配到右值引用呢？就是当括号内塞的本身是个右值的时候。比如&lt;code&gt;std::forward&amp;lt;int&amp;amp;&amp;amp;&amp;gt;(5)&lt;/code&gt;这个时候匹配的就是右值引用的&lt;code&gt;forward&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://blog.csdn.net/qq_33850438/article/details/107193177
https://lamforest.github.io/2021/04/29/cpp/wan-mei-zhuan-fa-yin-yong-zhe-die-wan-neng-yin-yong-std-forward/&lt;/p&gt;
&lt;h3&gt;更加详细的例子：&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;如果函数模板参数以万能引用作为一个模板参数，当参数传入左值的时候，模板会自动将其类型认定（推导）为左值引用；当传入右值的时候，会当做（推导）成普通数据使用。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void foo(T&amp;amp;&amp;amp; t){
    //...
}
//随后传入一个右值，T的类型将被推导为：

foo(42);  // foo&amp;lt;int&amp;gt;(42)
foo(3.14159);  // foo&amp;lt;double&amp;gt;(3.14159)
foo(std::string());  // foo&amp;lt;std::string&amp;gt;(std::string())
//不过，向foo传入左值的时候，T会被推导为一个左值引用：
int i = 42;
foo(i);  // foo&amp;lt;int&amp;amp;&amp;gt;(i)
//向foo传入右值的时候，T会被推导为一个普通数据：
foo(2019); //foo&amp;lt;int&amp;gt;(2019);
//因为函数参数声明为T&amp;amp;&amp;amp;，所以就是引用的引用，可以视为是原始的引用类型。那么foo&amp;lt;int&amp;amp;&amp;gt;()就相当于：
foo&amp;lt;int&amp;amp;&amp;gt;(); // void foo&amp;lt;int&amp;amp;&amp;gt;(int&amp;amp; t);

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这就允许一个函数模板可以即接受左值，又可以接受右值参数；这种方式已经被&lt;code&gt;std::thread&lt;/code&gt;的构造函数所使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;s&gt;&lt;strong&gt;若采用右值来初始化万能引用，就会得到一个右值引用，如果采用左值来初始化，则会得到一个左值引用。&lt;/strong&gt;&lt;/s&gt;&lt;/p&gt;
&lt;h3&gt;一个其他情况&lt;/h3&gt;
&lt;p&gt;在我们不使用万能引用, 只使用&lt;code&gt;std::forward&lt;/code&gt;的时候, 传入左值会被转换为右值引用. 原因看源码即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
void warp(T&amp;amp;&amp;amp; param) {
    cout &amp;lt;&amp;lt; __PRETTY_FUNCTION__ &amp;lt;&amp;lt; endl;
    func(std::forward&amp;lt;T&amp;gt;(param));
    cout &amp;lt;&amp;lt;std::is_same&amp;lt;decltype(std::forward&amp;lt;T&amp;gt;(param)), int&amp;amp;&amp;amp;&amp;gt;::value &amp;lt;&amp;lt; endl; // true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种情况下 转发后的类型是右值引用.&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;std::forward&lt;/code&gt;应该总是搭配万能引用.&lt;/p&gt;
&lt;h2&gt;移动语义&lt;/h2&gt;
&lt;p&gt;我们知道使用对象来对另一个对象进行初始化的时候会调用拷贝构造。但是如果我们不想使用拷贝构造呢？我们可以定义一个移动构造，形参为右值引用。换句话说，右值引用允许我们在使用临时对象时避免不必要的拷贝。只有右值引用才能调用移动构造函数和移动赋值函数。但是如果我们使用的是一个左值而且不想使用拷贝构造应该怎么办呢？ &lt;strong&gt;我们可以用&lt;code&gt;std::move&lt;/code&gt;移动语义来把一个左值转换为右值来触发移动构造。&lt;code&gt; std::move&lt;/code&gt;是一个非常有迷惑性的函数，不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量，但事实上&lt;code&gt;std::move&lt;/code&gt;移动不了什么，唯一的功能是把左值强制转化为右值，让右值引用可以指向左值。其实现等同于一个类型转换：&lt;code&gt;static_cast&amp;lt;T&amp;amp;&amp;amp;&amp;gt;(lvalue)&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;记住，在没有自定义的移动构造函数的时候，move函数仅仅是把左值转换为右值。当有移动构造函数的时候，调用过move（转换为右值）的参数就会触发移动构造，来实现移动语义（所有权转移）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当容器&lt;code&gt;vector&amp;lt;T&amp;gt;&lt;/code&gt;的类型&lt;code&gt;T&lt;/code&gt;有自定义的移动构造的时候，使用move将其元素转换为右值后才会触发移动构造。并且移动构造函数应声明为noexcept方可被vector使用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;移动构造函数中的异常问题： 对于移动构造函数来说，抛出异常有时是件危险的事情，因为可能移动语义还没完成，一个异常却被抛出来了，这就会导致一些指针成为悬挂指针。因此应尽量不要在移动构造函数中抛出异常，通过为其添加一个noexcept关键字，可以保证移动构造函数中抛出来的异常会直接调用terminate终止运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们举一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{
    public:
    int val = 0;
    A(){}
    A(int x):val(x){}
    A(A&amp;amp;&amp;amp; rhs){					//移动构造
        this-&amp;gt;val = rhs.val;
        cout &amp;lt;&amp;lt;&quot;move&quot; &amp;lt;&amp;lt; endl;
        rhs.val = 0;
    }
    A&amp;amp; operator=(A&amp;amp;&amp;amp; rhs){ 		//移动赋值
        if(this != &amp;amp;rhs){
            this-&amp;gt;val = rhs.val;
            cout &amp;lt;&amp;lt;&quot;move equal&quot; &amp;lt;&amp;lt; endl;
            rhs.val = 0; 
        }
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们有自定义的移动构造的时候，我们可以试试看&lt;code&gt;move&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){  
    A* a = new A(5);
    A* b = new A(move(*a));	//输出move 因为调用的是移动构造
    cout &amp;lt;&amp;lt; a-&amp;gt;val &amp;lt;&amp;lt; endl;	//输出0
    cout &amp;lt;&amp;lt; b-&amp;gt;val &amp;lt;&amp;lt; endl;	//输出5
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请记住。当使用&lt;code&gt;move&lt;/code&gt;对指针操作时，相当具有迷惑性。我们不应移动指针，应移动指针指向的对象。&lt;/p&gt;
&lt;p&gt;错误示范：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    A* a = new A(5);
    A* b = move(a);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;move&lt;/code&gt;什么也没干。仅仅是把指针&lt;code&gt;a&lt;/code&gt;本身转换成右值然后拷贝给指针&lt;code&gt;b&lt;/code&gt;。还记得我们说过指针给指针赋值，结果是让两个指针指向同一个地址吗？所以这段代码执行后，我们会得到一个浅拷贝的指针。 即 指针&lt;code&gt;a&lt;/code&gt;和指针&lt;code&gt;b&lt;/code&gt;都指向一个地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){  
    A* a = new A(5);
    A* b = new A(0);
    *b = move(*a); 			//输出 move equal 因为是移动赋值
    cout &amp;lt;&amp;lt; a-&amp;gt;val &amp;lt;&amp;lt; endl;	//输出0
    cout &amp;lt;&amp;lt; b-&amp;gt;val &amp;lt;&amp;lt; endl;	//输出5
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;给已有对象操作时调用赋值，生成新对象时调用构造。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;std::move 的底层实现&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;我们前面提到过一个引用折叠的知识点。可以回去查看。&lt;/li&gt;
&lt;li&gt;我们也提到过&lt;code&gt;move&lt;/code&gt;仅仅是把一个值转换为右值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;move&lt;/code&gt;不会改变参数的常量性。也就是保持CV属性&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;这会导致在对 以常量左值引用 传入的参数 使用move的时候，移动无效转而进行拷贝的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看拷贝构造。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;https://www.nextptr.com/tutorial/ta1211389378/beware-of-using-stdmove-on-a-const-lvalue&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看一下原型定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
typename remove_reference&amp;lt;T&amp;gt;::type&amp;amp;&amp;amp; move(T&amp;amp;&amp;amp; t)
{
	return static_cast&amp;lt;typename remove_reference&amp;lt;T&amp;gt;::type&amp;amp;&amp;amp;&amp;gt;(t);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，函数参数&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;是一个万能引用，通过引用折叠，此参数可以与任何类型的实参匹配（可以传递左值或右值，这是&lt;code&gt;std::move&lt;/code&gt;主要使用的两种场景)。关于引用折叠如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种情况：折叠成左值的时候：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;string s(&quot;hello&quot;);
此时s的型别为左值。然后通过引用折叠的规则，左值会被推导为左值引用的类型。也就是说此时的T由于是个左值，被推导为了T&amp;amp;
std::move(s) =&amp;gt; std::move(string&amp;amp; &amp;amp;&amp;amp;) [被推导为T&amp;amp;也就是string &amp;amp;] =&amp;gt; 触发折叠后 std::move(string&amp;amp; )
此时：T的类型为string&amp;amp;
typename remove_reference&amp;lt;T&amp;gt;::type为string 
整个std::move被实例化如下
string&amp;amp;&amp;amp; move(string&amp;amp; t) //t为左值，移动后不能在使用t。[这里是string&amp;amp;的原因就是 T = string&amp;amp;, 那么 T&amp;amp;&amp;amp; = string&amp;amp; &amp;amp;&amp;amp;, 触发折叠 string&amp;amp;]
{
    //通过static_cast将string&amp;amp;强制转换为string&amp;amp;&amp;amp;
    return static_cast&amp;lt;string&amp;amp;&amp;amp;&amp;gt;(t); 
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第二种情况：折叠成右值的时候：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;此时s的型别为右值。然后通过引用折叠的规则，右值会被推导为右值类型。也就是说此时的T由于是个右值，被推导为了T
std::move(string(&quot;hello&quot;)) =&amp;gt; std::move(string&amp;amp;&amp;amp;) [被推导为T也就是string] =&amp;gt; 触发折叠后 std::move(string&amp;amp;&amp;amp;)
//此时：T的类型为string 
//     remove_reference&amp;lt;T&amp;gt;::type为string 
//整个std::move被实例如下
string&amp;amp;&amp;amp; move(string&amp;amp;&amp;amp; t) //t为右值
{
    return static_cast&amp;lt;string&amp;amp;&amp;amp;&amp;gt;(t);  //返回一个右值引用
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，通过万能引用利用引用折叠原理：
&lt;ul&gt;
&lt;li&gt;右值经过&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;传递类型保持不变还是右值&lt;/li&gt;
&lt;li&gt;左值经过&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;折叠为普通的左值引用&lt;code&gt;T&amp;amp;&lt;/code&gt;，以保证模板可以传递任意实参，且保持类型不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;然后我们通过&lt;code&gt;static_cast&amp;lt;&amp;gt;T&lt;/code&gt;进行强制类型转换返回T&amp;amp;&amp;amp;右值引用，而&lt;code&gt;static_cast&amp;lt;T&amp;gt;&lt;/code&gt;之所以能使用类型转换，是通过&lt;code&gt;remove_refrence&amp;lt;T&amp;gt;::type&lt;/code&gt;模板移除&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;，&lt;code&gt;T&amp;amp;&lt;/code&gt;的引用，获取具体类型&lt;code&gt;T&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以说，&lt;code&gt;move&lt;/code&gt;的效果和&lt;code&gt;static_cast&amp;lt;T&amp;amp;&amp;amp;&amp;gt;()&lt;/code&gt;是一致的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;整理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在《Effective Modern C++》中建议（条款25）：&lt;strong&gt;对于右值引用的最后一次使用实施&lt;code&gt;std::move&lt;/code&gt;，对于万能引用的最后一次使用实施&lt;code&gt;std::forward&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为右值引用总是可以无条件转换为右值，因此用&lt;code&gt;std::move&lt;/code&gt;。所以函数形参为右值引用的时候，如果想使用参数的右值特性，可以直接使用&lt;code&gt;move&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;但万能引用不一定是右值，因此要用&lt;code&gt;std::forward&lt;/code&gt;做有条件的右值转换。如果无意间在万能引用接受的值实际是个左值引用的时候使用了&lt;code&gt;move&lt;/code&gt;可能会出现严重问题。&lt;/li&gt;
&lt;li&gt;局部对象在如下情况会触发RVO。此时使用&lt;code&gt;move&lt;/code&gt;或&lt;code&gt;forward&lt;/code&gt;会带来负优化。
&lt;ul&gt;
&lt;li&gt;局部对象类型和函数的返回值类型相同&lt;/li&gt;
&lt;li&gt;返回的就是局部对象本身&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在《Effective Modern C++》中提到（条款23）&lt;code&gt; std::move()&lt;/code&gt;不&lt;code&gt;move&lt;/code&gt;任何东西, &lt;code&gt;std::forward()&lt;/code&gt;也不转发任何东西。**这两者在运行期都无所作为。它们不会生成任何可执行代码，连一个字节都不会生成。他们都仅仅做了类型转换而已。**真正的移动操作是在移动构造函数或者移动赋值操作符中发生的。&lt;code&gt;std::move&lt;/code&gt;无条件的将它的参数转换成一个右值，而&lt;code&gt;std::forward&lt;/code&gt;当特定的条件满足时，才会执行它的转换。反复强调&lt;code&gt;std::forward&lt;/code&gt;仅当参数被右值绑定时，才会把参数转换为右值。也就是&lt;code&gt;std::forward&lt;/code&gt;可以理解为 &lt;code&gt;conditional move&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用move来转换为右值，使用forward来进行有条件的型别转换。在转换为右值后，相当于告诉编译器，我是右值，我可以被选择移动而不是拷贝。仅此而已。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一个类没有提供移动构造或移动赋值，使用&lt;code&gt;move&lt;/code&gt;转换为右值后，再进行构造新对象或赋值给对象的时候会匹配到拷贝构造或拷贝赋值。也就是能移动则移动，不能移动就拷贝。--- 笔记拷贝构造中提到了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;std::move()&lt;/code&gt;可以应用于左值(普通的变量int这些使用move与不使用move效果一样)，但这么做要谨慎。因为一旦“移动”了左值，就表示当前的值不再需要了，如果后续使用了该值，产生的行为是未定义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意，一个已经被移动过的对象被移动后仍然需要调用析构函数（或其析构函数仍然会被调用）。移动并不代表对象消亡。只是代表对象内含的数据的所有权转移。也就是说，移动的是对象蕴含的资源，而不是对象的状态。也就是说，移动后的对象虽然资源被窃取，但是其依旧是合法状态。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理解为目前是两个东西。只不过我们把第一个东西里面的数据转移给了第二个东西。但是现在依旧是俩东西。所以整个程序结束，还是有两次析构。这两个东西都要被析构。等于瓤只有一个，壳子有俩。只要销毁壳子，无论有没有瓤，都得析构。&lt;/li&gt;
&lt;li&gt;所以说，一个对象被移动了，必须也能被析构。&lt;/li&gt;
&lt;li&gt;https://stackoverflow.com/questions/20589025/why-is-the-destructor-call-after-the-stdmove-necessary&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myobj a(5);
//myobj b(move(a)); //这一种和下面的一种一样。都是调用移动构造。
myobj c = move(a);
/*
constructor
move constructor
destructor
destructor
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://blog.csdn.net/qq_41453285/article/details/104447573&lt;/p&gt;
&lt;h3&gt;右值引用解决了什么问题？面试回答&lt;/h3&gt;
&lt;p&gt;右值引用分两个部分，移动语义和完美转发。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;移动语义解决了所有权转移的问题，比如智能指针。而且可以配合移动构造函数避免使用临时对象的时候造成的拷贝。而且配合容器使用可以提升性能。&lt;strong&gt;可以理解为浅拷贝。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浅拷贝：我不需要两个指针指向同一个对象。那么可以把原来的指针指向的数据赋值给新的指针。这样数据依旧只有一份。&lt;/li&gt;
&lt;li&gt;深拷贝：我需要两个指针指向两个数据相同的对象。那么需要在拷贝构造中额外开辟一块内存，进行新的数据的初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完美转发就是保持传入值的特性，左值维持左值，右值维持右值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;函数类型推导表格&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void f(P param){
    //...
}
int main(){
    f(A);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;P&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;T&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int*&lt;/td&gt;
&lt;td&gt;int*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;int * const&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (*)(int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (*)(int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int *&lt;/td&gt;
&lt;td&gt;int *&amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int *&lt;/td&gt;
&lt;td&gt;const int *&amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;td&gt;const int &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const int * const&lt;/td&gt;
&lt;td&gt;const int * const &amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;char [2]&lt;/td&gt;
&lt;td&gt;char (&amp;amp;)[2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;const char [12]&lt;/td&gt;
&lt;td&gt;const char (&amp;amp;)[12]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;void (int)&lt;/td&gt;
&lt;td&gt;void (&amp;amp;)(int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const T&amp;amp;&lt;/td&gt;
&lt;td&gt;int &amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;https://www.cnblogs.com/5iedu/p/11183878.html&lt;/p&gt;
&lt;h1&gt;一些关于static 作用域的小问题&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态全局变量只能在当前的文件中访问，其它文件不可访问，即使是extern也不行。也就是其作用域是定义的文件内。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态全局变量有以下特点：
&lt;ul&gt;
&lt;li&gt;该变量在全局数据区分配内存；&lt;/li&gt;
&lt;li&gt;未经初始化的静态全局变量会被程序自动初始化为0（自动变量的值是随机的，除非它被显式初始化）&lt;/li&gt;
&lt;li&gt;其它文件中可以定义相同名字的变量，不会发生冲突；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态局部变量有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;该变量在依然全局数据区分配内存；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态局部变量在程序执行到该对象的声明处时被首次初始化，即以后的函数调用不再进行初始化；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态局部变量一般在声明处初始化，如果没有显式初始化，会被程序自动初始化为0；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它始终驻留在全局数据区，直到程序运行结束。但其作用域为局部作用域，当定义它的函数或语句块结束时，其作用域随之结束；&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同，它只能在声明它的文件当中可见，不能被其它文件使用。&lt;/li&gt;
&lt;li&gt;定义静态函数的好处：
&lt;ul&gt;
&lt;li&gt;静态函数不能被其它文件所用；&lt;/li&gt;
&lt;li&gt;其它文件中可以定义相同名字的函数，不会发生冲突；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态数据成员特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;理解为带有作用域的全局变量，但是：同全局变量相比，使用静态数据成员有两个优势：&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;静态数据成员没有进入程序的全局名字空间，因此不存在与程序中其它全局名字冲突的可能性；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以实现信息隐藏。静态数据成员可以是private成员，而全局变量不能；&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;对于非静态数据成员，每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个，静态数据成员在程序中也只有一份拷贝，由该类型的所有对象共享访问。也就是说，静态数据成员是该类的所有对象所共有的。对该类的多个对象来说，静态数据成员只分配一次内存，供所有对象共用。所以，静态数据成员的值对每个对象都是一样的，它的值可以更新；&lt;/li&gt;
&lt;li&gt;静态数据成员存储在全局数据区。静态数据成员定义时要分配空间，所以不能在类声明中定义。因为声明只是描述了如何分配内存, 但实际上并不真正分配内存。&lt;/li&gt;
&lt;li&gt;静态数据成员和普通数据成员一样遵从public,protected,private访问规则；&lt;/li&gt;
&lt;li&gt;因为静态数据成员在全局数据区分配内存，属于本类的所有对象共享，所以，它不属于特定的类对象，在没有产生类对象时其作用域就可见，即在没有产生类的实例时，我们就可以操作它；&lt;/li&gt;
&lt;li&gt;静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为：
&lt;code&gt;＜数据类型＞＜类名＞::＜静态数据成员名＞=＜值＞&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;类的静态数据成员有两种访问形式：
&lt;code&gt;＜类对象名＞.＜静态数据成员名＞&lt;/code&gt; 或 &lt;code&gt;＜类类型名＞::＜静态数据成员名＞&lt;/code&gt;
如果静态数据成员的访问权限允许的话（即public的成员），可在程序中，按上述格式来引用静态数据成员 ；&lt;/li&gt;
&lt;li&gt;静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类，每个实例的利息都是相同的。所以，应该把利息设为存款类的静态数据成员。这有两个好处，第一，不管定义多少个存款类对象，利息数据成员都共享分配在全局数据区的内存，所以节省存储空间。第二，一旦利息需要改变时，只要改变一次，则所有存款类对象的利息全改变过来了；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态函数成员特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;与静态数据成员一样，我们也可以创建一个静态成员函数，它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样，都是类的内部实现，属于类定义的一部分。普通的成员函数一般都隐含了一个this指针，this指针指向类的对象本身，因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下，this是缺省的。但是与普通函数相比，静态成员函数由于不是与任何的对象相联系，因此它不具有this指针。从这个意义上讲，它无法访问属于类对象的非静态数据成员，也无法访问非静态成员函数，它只能调用其余的静态成员函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;出现在类体外的函数定义不能指定关键字static；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态成员之间可以相互访问，包括静态成员函数访问静态数据成员和访问静态成员函数；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;非静态成员函数可以任意地访问静态成员函数和静态数据成员；（因为this指针可以省略不放进静态函数的入参）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态成员函数不能访问非静态成员函数和非静态数据成员；（缺一个this指针，肯定不行）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不可以同时用const和static修饰成员函数。（没有this指针）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于没有this指针的额外开销，因此静态成员函数与类的全局函数相比速度上会有少许的增长；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用静态成员函数，可以用成员访问操作符(.)和(-&amp;gt;)为一个类的对象或指向类对象的指针调用静态成员函数，也可以直接使用如下格式：
&lt;code&gt;＜类名＞::＜静态成员函数名＞（＜参数表＞）&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类的静态函数为什么不能调用非静态成员？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类的非静态函数在编译后，第一个隐藏的参数是一个this指针。但是静态函数没有这个隐藏参数。如果调用了非静态成员，参数无法匹配。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类的非静态函数调用的时候大概长这样：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  myDate.setMonth( 3 ); //非静态函数正常调用
  setMonth(&amp;amp;myDate, 3); //非静态函数假设调用（显式标出this指针）
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;this指针的特性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;this指针的类型：&lt;code&gt;类类型 *const&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;this指针并不是对象的一部分，不会影响对象的大小；&lt;/li&gt;
&lt;li&gt;this指针是非静态成员函数的第一个隐含指针形参，是编译器自己处理的，我们不能在成员函数的形参中添加this指针的参数定义，也不能在调用时显示&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为什么类的静态成员变量必须在类外部初始化？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为静态成员变量属于整个类，而不属于某个对象，如果在类内初始化，会导致每个对象都包含该静态成员，这是矛盾的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类的静态成员变量可以理解为加了作用域的全局变量。自然需要类外初始化&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**任何类内的内存分配函数必须要声明为静态函数 static **&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态成员函数即使在类对象不存在的情况下也能被调用，静态函数只要使用类名加范围解析运算符 :: 就可以访问。new对象时，对象还没有，设置成类非静态成员函数是没意思的，隐藏的this指针无从下手，所以要声明为static函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;static成员变量类外实例化看起来可能非常诡异&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename ...Args&amp;gt;
struct test{ //类模板加嵌套类
    struct innertest{
        innertest(){
            std::cout &amp;lt;&amp;lt;&quot;inndertest constructor  called\n&quot;;
        }
        inline void do_nothing()const { };
    };
    test(){
        std::cout &amp;lt;&amp;lt;&quot;test constructor called\n&quot;;
        m_innertest.do_nothing();
    }

    static innertest m_innertest;
};


struct test_n{ //普通类加嵌套类
    struct innertest_n{
        innertest_n(){
            std::cout &amp;lt;&amp;lt;&quot;inndertest_n constructor  called\n&quot;;
        }
        inline void do_nothing()const { };
    };
    test_n(){
        std::cout &amp;lt;&amp;lt;&quot;test_n constructor called\n&quot;;
        m_innertest_n.do_nothing();
    }

    static innertest_n m_innertest_n;
};


struct normal{ //普通类
    static int var;
};

 
template&amp;lt;typename T, typename ...Args&amp;gt;
typename test&amp;lt;T, Args...&amp;gt;::innertest test&amp;lt;T, Args...&amp;gt;::m_innertest; //类模板加嵌套类，得写两次模板参数。
/*因为格式是 类型名 作用域::变量名
前面的是类型名。后面的要先指定外层作用域（这时候就要加模板参数了）然后指定名字。所以看起来比较诡异。
*/
test_n::innertest_n test_n::m_innertest_n; //普通嵌套类。看起来比较正常。

int normal::var; //普通类。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;整理static作用：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;全局变量、静态全局变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;局部静态变量（一般为函数内的静态变量）在第一次使用时分配内存并初始化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修饰类成员函数：
&lt;ul&gt;
&lt;li&gt;该成员函数可以不通过对象调用，可以通过作用域运算符直接调用。&lt;/li&gt;
&lt;li&gt;静态成员函数 只能访问或修改静态成员变量和函数， 不会访问或者修改任何对象非static数据成员。因为类的静态成员变量和函数在类加载的时候就会分配内存，而非static成员属于对象，在对象创建的时候才会分配内存。（没有this指针）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;static成员函数不能被&lt;code&gt;const&lt;/code&gt;，&lt;code&gt;volatile&lt;/code&gt;修饰。且不可是虚函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修饰类成员变量
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;该成员变量所有该类对象共享。相当于带作用域的全局变量。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意类内局部变量不是类成员变量。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;由于static变量不属于任何一个对象，因此不会在构造函数中赋值，初始值是静态区的默认值0，后面需要在类外赋初值&lt;/li&gt;
&lt;li&gt;类中的变量可以被类和对象访问，初始值为0，可以在类外赋初始值。&lt;/li&gt;
&lt;li&gt;类内static变量在类外初始化。可以通过 &lt;em&gt;类名::变量名&lt;/em&gt; 访问，也可以通过 &lt;em&gt;对象::变量名&lt;/em&gt; 访问&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当&lt;code&gt;static const&lt;/code&gt;一起修饰时，可以在类中赋初始值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修饰全局变量
&lt;ul&gt;
&lt;li&gt;该变量和全局普通变量一样在全局数据区分配内存。&lt;strong&gt;但是静态全局变量作用域只在本文件。其他文件不可见&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修饰全局普通函数
&lt;ul&gt;
&lt;li&gt;全局静态函数 相对于普通函数，作用域只限制在本文件中；而&lt;strong&gt;普通的全局函数默认是extern的&lt;/strong&gt;，其他源文件也可以访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修饰局部变量
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;它从声明（但是必须运行）起驻留在全局数据区，直到程序运行结束。&lt;strong&gt;但其作用域为局部作用域&lt;/strong&gt;，当定义它的函数或语句块结束时，其作用域随之结束；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;局部静态变量的生存周期是从声明（但是必须程序执行到该对象的声明处）起至程序结束&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;同时局部静态变量只允许被初始化一次。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修饰类对象
&lt;ul&gt;
&lt;li&gt;普通对象可以通过extern跨文件访问，静态对象本文件访问&lt;/li&gt;
&lt;li&gt;静态对象如果在函数内，执行的时候才会构造，在函数外，全局static类对象在main前构造。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;外部链接性，内部链接性，无链接性&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;声明&lt;strong&gt;外部链接&lt;/strong&gt;的变量的方法是在代码块外面声明它. 此变量是全局变量,多文件中亦可用.&lt;/li&gt;
&lt;li&gt;声明&lt;strong&gt;内部链接&lt;/strong&gt;的变量的方法是在代码块外面声明它并加上&lt;code&gt;static&lt;/code&gt;限定符. 此变量是全局变量,但仅在本文件中可用. &lt;code&gt;static&lt;/code&gt;就是这样限定作用域在文件范围内的。因为static不在全局符号表。&lt;/li&gt;
&lt;li&gt;声明&lt;strong&gt;无链接&lt;/strong&gt;的变量的方法是在代码块里面声明它并加上&lt;code&gt;static&lt;/code&gt;限定符. 此变量是局部变量,但仅在本代码块中可用.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;符号表（csapp7.5)&lt;/h1&gt;
&lt;p&gt;每个可重定位目标模块&lt;code&gt;m&lt;/code&gt;都有一个符号表，它包含&lt;code&gt;m&lt;/code&gt;定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由模块&lt;code&gt;m&lt;/code&gt;&lt;strong&gt;定义&lt;/strong&gt;并能被其他模块引用的&lt;strong&gt;全局符号&lt;/strong&gt;。全局链接器符号对应于&lt;strong&gt;非静态的C函数和全局变量&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;由其他模块定义并被模块&lt;code&gt;m&lt;/code&gt;&lt;strong&gt;引用&lt;/strong&gt;的全局符号。这些符号称为&lt;strong&gt;外部符号&lt;/strong&gt;，对应于在&lt;strong&gt;其他模块中定义的非静态C函数和全局变量&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;只被模块&lt;code&gt;m&lt;/code&gt;定义和引用的&lt;strong&gt;局部符号&lt;/strong&gt;。它们对应于&lt;strong&gt;带static属性的C函数和全局变量&lt;/strong&gt;。这些符号在模块m中任何位置都可见，但是不能被其他模块引用。&lt;/li&gt;
&lt;li&gt;认识到本地链接器符号和本地程序变量不同是很重要的。.symtab中的&lt;strong&gt;符号表不包含对应于本地非静态程序变量的任何符号&lt;/strong&gt;。&lt;strong&gt;这些符号在运行时在栈中被管理&lt;/strong&gt;，链接器对此类符号不感兴趣。（&lt;strong&gt;链接器对局部变量不感兴趣&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;强符号 弱符号&lt;/h1&gt;
&lt;p&gt;在C语言中，编译器默认&lt;strong&gt;函数和初始化了的全局变量为强符号&lt;/strong&gt;（Strong Symbol），&lt;strong&gt;未初始化的全局变量为弱符号&lt;/strong&gt;（Weak Symbol）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;强符号之所以强，是因为它们拥有确切的数据，变量有值，函数有函数体；弱符号之所以弱，是因为它们还未被初始化，没有确切的数据。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;链接器会按照如下的规则处理被多次定义的强符号和弱符号：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不允许强符号被多次定义，也即不同的目标文件中不能有同名的强符号；如果有多个强符号，那么链接器会报符号重复定义错误。&lt;/li&gt;
&lt;li&gt;如果一个符号在某个目标文件中是强符号，在其他文件中是弱符号，那么选择强符号。&lt;/li&gt;
&lt;li&gt;如果一个符号在所有的目标文件中都是弱符号，那么选择其中占用空间最大的一个。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;链接器&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;链接器不仅仅去解析未定义的符号引用和替换符号地址，同时链接器还要完成程序中各目标文件的地址空间的组织，这涉及重定位工作。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说，链接器负责安排我们看到的各个段的位置。如果你想的话，可以修改链接器脚本让链接器把不同代码段混着摆。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://youtu.be/dOfucXtyEsU?t=1882&lt;/p&gt;
&lt;p&gt;linux的链接器在使用动态链接库的时候是惰性查找。也就是只为你调用了的函数进行查找。&lt;/p&gt;
&lt;p&gt;这样做的目的是加快启动时间。但是有一个问题，也就是第一次调用的时候会较慢，因为这时候这个地址是进行dll查找子程序的地址。之后再次调用的时候这个地址会被替换为函数真正地址。&lt;/p&gt;
&lt;p&gt;所以在实时性较高的场景不应该使用动态链接库。或显式使用&lt;code&gt;LD_BIND_NOW&lt;/code&gt;来指明立刻应用所有重定位并使用实际地址而不是通过解析器地址填充plt。&lt;/p&gt;
&lt;h1&gt;定义了两个vector，vector里面存的是类，是否可以直接使用memcpy去复制vector&lt;/h1&gt;
&lt;p&gt;答案是不能。&lt;/p&gt;
&lt;p&gt;首先，vector容器本身只有3个指针（start指向当前第一个数据也就是迭代器的begin位置，finish指向当前最后一个数据也就是迭代器的end位置，end_of_storage指向的是数据存储区的末尾，也就是capacity。）。所以你&lt;code&gt;sizeof(一个vector)&lt;/code&gt;得到的一定是3*8 = 24 （64位平台一个指针8字节)。所有的容器都有一个功能，就是帮你&lt;code&gt;&quot;new&quot;&lt;/code&gt;一块内存，然后把东西放进去。我们说过，STL是拷贝进来，拷贝出去。那他拷贝的东西放在哪了？我们说的&lt;code&gt;allocator&lt;/code&gt;分配子干什么了？答案是：它帮你从堆上分配了内存。因为他有做&lt;code&gt;malloc&lt;/code&gt;动作。笨想都知道是从堆分配。栈一共就那么大，你说是吧？&lt;strong&gt;所以&lt;code&gt;vector&lt;/code&gt;和原始数组不一样。&lt;code&gt;vector&lt;/code&gt;对象的地址和数据地址不是连续的。因为&lt;code&gt;vector&lt;/code&gt;储存数据的空间是通过&lt;code&gt;malloc&lt;/code&gt;单独分配的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;好了，继续我们的话题。&lt;/p&gt;
&lt;p&gt;所以假如有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt; a1 = {1,2,3,4,5,6,7,8,9,10};
    cout &amp;lt;&amp;lt; sizeof(a1) &amp;lt;&amp;lt; endl; //输出24
    vector&amp;lt;int&amp;gt; test1;
    memcpy(&amp;amp;test1, &amp;amp;test, sizeof(test)); //尝试使用memcpy拷贝。
    for (auto t : test1) {
        cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;它可以部分运行，但是会有double free问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;针对这行代码：&lt;code&gt;memcpy(&amp;amp;test1, &amp;amp;test, sizeof(test)); &lt;/code&gt;。我们之前提到过，&lt;code&gt;vector&lt;/code&gt;自己只有三根指针。你&lt;code&gt;sizeofvector&lt;/code&gt;是三根指针的大小，你对&lt;code&gt;vector&lt;/code&gt;取地址，取到的不是&lt;code&gt;vector&lt;/code&gt;数据的地址，仅仅是这个&lt;code&gt;vector&lt;/code&gt;对象的地址。（再次重复一次&lt;code&gt;vector&lt;/code&gt;对象只有三根指针）。&lt;/p&gt;
&lt;p&gt;所以我们现在知道了，&lt;strong&gt;这行代码是把test的三根指针，浅拷贝给了test1&lt;/strong&gt;。这时候，虽然&lt;code&gt;test&lt;/code&gt;里面的三个指针和&lt;code&gt;test1&lt;/code&gt;里面的三个指针地址不同（&lt;code&gt;test&lt;/code&gt;和&lt;code&gt;test1&lt;/code&gt;地址也不同），但是指针指向的数据是同一块。又因为我们后声明的&lt;code&gt;test1&lt;/code&gt;，导致&lt;code&gt;test1&lt;/code&gt;先被析构，&lt;code&gt;test1&lt;/code&gt;里面三个指针指向的数据（&lt;code&gt;vector&lt;/code&gt;里面存的数据）被释放（析构）。然后&lt;code&gt;test&lt;/code&gt;又执行释放（析构）动作，但是数据（资源）已经被释放了，所以导致了双重释放问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意这里的&lt;code&gt;test1&lt;/code&gt;是栈变量。它是定义。所以不用使用构造函数分配空间。&lt;code&gt;vector&lt;/code&gt;使用构造函数分配的空间是给储存的数据使用的。我们这里是浅拷贝指针给&lt;code&gt;test1&lt;/code&gt;对象而不是储存数据，所以不会出错。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以我们需要按照下面这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    vector&amp;lt;int&amp;gt; test = {1,2,3,4,5,6,7,8,9,10};
    cout &amp;lt;&amp;lt; sizeof(test) &amp;lt;&amp;lt; endl;
    vector&amp;lt;int&amp;gt; test1(test.size()); //必须分配空间
    memcpy(&amp;amp;test1[0], &amp;amp;test[0], sizeof(int) * test.size()); //必须取首地址，并且手动计算数据区大小
    for (auto t : test1) {
        cout &amp;lt;&amp;lt; t &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要害怕，我们一点一点来看。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q1: 为什么这时候&lt;code&gt;test1&lt;/code&gt;必须要使用构造函数分配内存？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为此时我们要复制的是数据本身。数据本身需要让&lt;code&gt;vector&lt;/code&gt;来储存，所以必须让&lt;code&gt;vector&lt;/code&gt;内部调用分配器帮我们从堆上面拿到用来储存数据的内存块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2: 为什么&lt;code&gt;test1&lt;/code&gt;和&lt;code&gt;test&lt;/code&gt;要取首地址？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们已经为&lt;code&gt;test1&lt;/code&gt;分配了储存数据的内存。我们提到过，&lt;code&gt;vector&lt;/code&gt;对象本身的地址和数据储存的地址是不连续的，是分开的。所以我们需要用这种方式获取他数据储存区的首地址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3: 为什么需要手动计算&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;唔，这个问题可能不需要回答，但我还是回答一下。我们计算的是每一个&lt;code&gt;int&lt;/code&gt;的大小（也就是原容器储存的对象大小）* 原容器储存了多少个对象（&lt;code&gt;size()&lt;/code&gt;函数返回的不是容器占用了多少字节，而是容器储存了多少个对象）&lt;/p&gt;
&lt;h1&gt;定义，声明&lt;/h1&gt;
&lt;h2&gt;变量类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;声明：是指出存储类型，并给存储单元指定名称。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“把一个C++名称引入或者重新引入到你的程序” （来自C++ template A.2)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义：是分配内存空间，还可为变量指定初始值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;extern关键字：通过使用extern关键字&lt;strong&gt;声明变量名，而不是定义它&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;要点1：声明不一定是定义&lt;/h3&gt;
&lt;p&gt;extern声明没有分配内存空间，所以不是定义；extern告诉编译器变量在其他地方定义了。&lt;/p&gt;
&lt;p&gt;例如：&lt;code&gt;extern int val;&lt;/code&gt; // 只是声明了变量&lt;code&gt;val&lt;/code&gt;，并没有分配内存空间，所以不是定义。&lt;/p&gt;
&lt;h3&gt;要点2：定义也是声明&lt;/h3&gt;
&lt;p&gt;定义变量的时候，同时给变量指出了存储类型，并给变量的存储单元指定了名称，所以也是声明。&lt;/p&gt;
&lt;p&gt;例如：&lt;code&gt;int val;&lt;/code&gt;// 声明了变量&lt;code&gt;val&lt;/code&gt;，也会分配内存空间，所以也是定义。&lt;/p&gt;
&lt;p&gt;例如：&lt;code&gt;vector&amp;lt;int&amp;gt; a;&lt;/code&gt;// 声明了&lt;code&gt;vector&lt;/code&gt;容器&lt;code&gt; a&lt;/code&gt;, 也会分配&lt;code&gt;vector&lt;/code&gt;对象的内存空间（提到过&lt;code&gt;vector&lt;/code&gt;本身24字节）。所以也是定义。&lt;/p&gt;
&lt;h3&gt;要点3：extern关键字标识的才是声明，其余都是定义&lt;/h3&gt;
&lt;p&gt;例如：
&lt;code&gt;extern int val;&lt;/code&gt; // 声明
&lt;code&gt;int val;&lt;/code&gt; // 定义&lt;/p&gt;
&lt;h3&gt;要点4：声明有指定初始值：如果指定了初始值，即使前面加了extern关键字，也是定义。&lt;/h3&gt;
&lt;p&gt;例如：&lt;code&gt;extern int val = 1&lt;/code&gt;; // 定义&lt;/p&gt;
&lt;h3&gt;要点5: 一个变量的定义永远只能有一个，但是可以有多个声明。&lt;/h3&gt;
&lt;h2&gt;函数类型&lt;/h2&gt;
&lt;h3&gt;要点1：函数原型（又称函数声明）：函数原型之于函数，相当于变量声明之于变量，只有函数头的就是函数原型。&lt;/h3&gt;
&lt;p&gt;例如：
&lt;code&gt;int function();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;extern int function();&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;要点2 ：函数定义：带有函数体的就是定义，&lt;/h3&gt;
&lt;p&gt;例如：
&lt;code&gt;int function() { return 0; }&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;注：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;函数原型的返回值类型与函数定义必须相同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数原型的形参表的类型与顺序必须与函数定义相同，但是函数原型可以不写形参名称，即便写了形参名称也可以和原函数不一样。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空形参&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C语言中：
&lt;code&gt;int func();&lt;/code&gt; // 表示可以有很多个参数
&lt;code&gt;int func(void);&lt;/code&gt; // 表示没有参数&lt;/li&gt;
&lt;li&gt;C++中，上述两行代码等价，且都表示没有参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在函数被调用之前必须先有函数原型，函数定义可以放在调用函数的后面。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int func(); // 必须在调用函数的前面
int main() { int ret = func(); }
int func() { return 0; } //可以在调用函数的后面
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;函数原型描述了函数接口，即函数如何与程序的其他部分交互。参数列表指出了何种信息将被传递给函数，函数类型指出了返回值的类型。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;extern的主要作用是什么?&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;置于变量或者函数前&lt;/strong&gt;: 表明该变量或者函数定义在别的文件中。（外部链接）
&lt;ul&gt;
&lt;li&gt;在变量或者函数之前加上&lt;code&gt;extern&lt;/code&gt;关键字表明这是一个声明, 其定义可能在其他文件处, &lt;strong&gt;注意不能对变量进行初始化或者对函数进行定义&lt;/strong&gt;, 否则表明这是一个定义而不是声明.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;extern&quot;C&quot;`&lt;/strong&gt;: 让编译器以 C 语言的命名规则来查找函数
&lt;ul&gt;
&lt;li&gt;extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后，会指示编译器这部分代码按C语言的进行编译，而不是C++的。由于C++支持函数重载，因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中，而不仅仅是函数名；而C语言并不支持函数重载，因此编译C语言代码的函数时不会带上函数的参数类型，一般只包括函数名。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;如何令 const 对象可以在多个文件中共享&lt;/h1&gt;
&lt;p&gt;默认情况下, &lt;code&gt;const&lt;/code&gt; 对象仅在本文件内有效, 我们可以通过 &lt;code&gt;extern&lt;/code&gt; 关键字来使得 &lt;code&gt;const&lt;/code&gt; 对象在多个文件中共享.&lt;/p&gt;
&lt;h1&gt;五（六）大内存区（代码段）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;BSS段&lt;code&gt;(.bss)&lt;/code&gt;：BSS段（bss segment）通常是指用来存放程序中&lt;strong&gt;未初始化的全局 和 静态变量 和 初始化为零值的全局 和 静态变量&lt;/strong&gt;的一块内存区域。在程序开始时通常会被清零。BSS是英文Block Started by Symbol的简称。BSS段属于&lt;strong&gt;静态内存分配&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;bss段不占用实际磁盘空间。只在段表中记录大小，在符号表中记录符号。当文件加载运行时，才分配空间以及初始化。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据段&lt;code&gt;(.data)&lt;/code&gt;：数据段（data segment）通常是指用来存放程序中&lt;strong&gt;已初始化为非零值的全局变量或者静态（全局）变量，全局常量数据&lt;/strong&gt;的一块内存区域。数据段属于&lt;strong&gt;静态内存分配&lt;/strong&gt;。[局部常量储存在栈段]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(.data)&lt;/code&gt;又可分为读写&lt;code&gt;（RW）&lt;/code&gt;区域和只读&lt;code&gt;（RO）&lt;/code&gt;区域。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RO&lt;/code&gt;段保存常量所以也被称为&lt;code&gt;.constdata&lt;/code&gt;  &lt;strong&gt;虚函数表在这&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;因为编译时可确定，且需要全局共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RW&lt;/code&gt;段则是普通非常量全局变量，&lt;strong&gt;静态变量就在其中&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码段&lt;code&gt;(.text)&lt;/code&gt;：代码段（code segment/text segment）通常是指用来存放**程序执行代码(汇编代码)**的一块内存区域。这部分区域的大小在程序运行前就已经确定，并且内存区域通常属于只读, 某些架构也允许代码段为可写，即允许修改程序。&lt;strong&gt;在代码段中，也有可能包含一些只读的常数变量，例如字符串常量等。常量变量也有可能在不同的架构上被单独分出来&lt;/strong&gt;。&lt;strong&gt;只要是函数，不管怎么样，统一都放在这里&lt;/strong&gt;。（这里有一段代码帮助我们初始化BSS段的内容为0）&lt;strong&gt;这块内存是共享的,当有多个相同进程(Process)存在时，共用同一个text段。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;堆（heap）：堆是用于存放进程运行中被动态分配的内存段，它的大小并不固定，可动态扩张或缩减。当进程调用&lt;code&gt;malloc&lt;/code&gt;等函数分配内存时，新分配的内存就被动态添加到堆上（堆被扩张）；当利用&lt;code&gt;free&lt;/code&gt;等函数释放内存时，被释放的内存从堆中被剔除（堆被缩减）&lt;strong&gt;低地址到高地址&lt;/strong&gt;。&lt;strong&gt;堆里的数据可共享&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈(stack)：是用户存放程序临时创建的局部变量，存放函数调用相关的参数、局部变量的值，以及在任务切换的上下文信息。栈区是由&lt;strong&gt;操作系统&lt;/strong&gt;分配和管理的区域。也就是说我们函数括弧&lt;code&gt;“{}”&lt;/code&gt;中定义的变量（&lt;strong&gt;但不包括static声明的变量，static意味着在数据段中存放变量&lt;/strong&gt;）。除此以外，在函数被调用时，其参数也会被压入发起调用的进程栈中，并且待到调用结束后，函数的返回值也会被存放回栈中。由于栈的后进先出的特点，所以栈特别方便用来保存/恢复调用现场。从这个意义上讲，我们可以把堆栈看成一个寄存、交换临时数据的内存区。&lt;strong&gt;高地址到低地址&lt;/strong&gt;。&lt;strong&gt;栈里放的都是局部变量，无法共享，函数结束时自动释放。&lt;strong&gt;这就是为什么全局和静态变量不在这里。&lt;/strong&gt;[局部常量在这里]。栈属于静态内存分配&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存映射段 （memory mapping segment ）： 在栈的下面是内存映射段。在这里，内核将文件的内容直接映射到内存。任何应用程序都可以通过Linux的&lt;code&gt;mmap()&lt;/code&gt;系统调用或Windows中的&lt;code&gt;CreateFileMapping()/MapViewOfFile()&lt;/code&gt;来进行这样的映射。内存映射是一种方便高效的文件I/O方式，所以它被用来加载动态库。还可以创建不对应任何文件的&lt;strong&gt;匿名内存映射&lt;/strong&gt;，而是将其用于存放程序数据。在Linux中，如果使用&lt;code&gt;malloc()&lt;/code&gt;申请一大块内存，C运行库将开辟一个这样的匿名映射，而不是开辟堆内存，这里的“大”意味着大于&lt;code&gt;MMAP_THRESHOLD&lt;/code&gt;字节，默认为128 kb，可通过&lt;code&gt;mallopt()&lt;/code&gt;调整。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内存映射分为公有和私有，匿名和具名&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;私有映射&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;共享映射&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;匿名映射&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;私有匿名映射 - 通常用于内存分配&lt;/td&gt;
&lt;td&gt;共享匿名映射 - 通常用于（有关联）进程间通信&lt;/td&gt;
&lt;td&gt;匿名映射没有对应的文件。这种映射的内存区域会被初始化成0。另一种看待匿名映射的角度（并且也接近于事实）是把它看成是一个内容总是被初始化为 0 的虚拟文件的映射。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;文件映射&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;私有文件映射 - 通常用于加载动态库&lt;/td&gt;
&lt;td&gt;共享文件映射 - 通常用于内存映射IO，（无关联）进程间通信。&lt;/td&gt;
&lt;td&gt;内存映射区域有实体文件与之关联。mmap系统调用将普通文件的一部分内容直接映射到调用进程的虚拟地址空间。一旦完成映射，就可以通过在相应的内存区域中操作字节来访问文件内容。这种映射也被称为基于文件的映射。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;私有匿名映射 (使用&lt;code&gt;mmap&lt;/code&gt;进行内存分配)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当使用参数 &lt;code&gt;fd=-1&lt;/code&gt; 且&lt;code&gt;flags=MAP_ANONYMOUS | MAP_PRIVATE&lt;/code&gt;时，创建的&lt;code&gt;mmap &lt;/code&gt;映射是私有匿名映射。私有匿名映射最常见的用途是在 glibc 分配大块内存中，当需要的分配的内存大于 &lt;code&gt;MMAP_THREASHOLD(128KB)&lt;/code&gt; 时，glibc会默认使用&lt;code&gt;mmap&lt;/code&gt;代替 &lt;code&gt;brk&lt;/code&gt; 来分配内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;共享匿名映射 （父子进程间通信）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当使用参数 &lt;code&gt;fd=-1&lt;/code&gt; 且 &lt;code&gt;flags=MAP_ANONYMOUS | MAP_SHARED&lt;/code&gt; 时，创建的&lt;code&gt;mmap&lt;/code&gt;映射是共享匿名映射。共享匿名映射让相关进程共享一块内存区域，通常用于父子进程的之间通信。&lt;/li&gt;
&lt;li&gt;创建共享匿名映射有如下两种方式：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fd=-1&lt;/code&gt; 且&lt;code&gt; flags= MAP_ANONYMOUS|MAP_SHARED&lt;/code&gt;。在这种情况下，&lt;code&gt;do_mmap_pgoff()-&amp;gt;mmap() &lt;/code&gt;函数最终调用 &lt;code&gt;shmem_zero_setup() &lt;/code&gt;来打开一个 &lt;code&gt;&quot;/dev/zero&quot; &lt;/code&gt;特殊的设备文件。&lt;/li&gt;
&lt;li&gt;直接打开 &lt;code&gt;&quot;/dev/zero&quot; &lt;/code&gt;设备文件，然后使用这个文件描述符来创建&lt;code&gt; mmap&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;私有文件映射（加载动态链接库）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;私有文件映射时 flags 的标志位被设置为 &lt;code&gt;MAP_PRIVATE&lt;/code&gt;，那么就会创建私有文件映射。私有文件映射的最常用的场景是加载动态共享库。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;共享文件映射 (读写文件，非亲属进程间通信)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建文件映射时 flags 的标志位被设置为 &lt;code&gt;MAP_SHARED&lt;/code&gt;，那么就会创建共享文件映射。如果 &lt;code&gt;prot &lt;/code&gt;参数指定了 &lt;code&gt;PROT_WRITE&lt;/code&gt;，那么打开文件需要制定&lt;code&gt;O_RDWR&lt;/code&gt;标志位。&lt;/li&gt;
&lt;li&gt;共享文件映射通常有如下场景：
&lt;ul&gt;
&lt;li&gt;读写文件：
&lt;ul&gt;
&lt;li&gt;把文件内容映射到进程地址空间，同时对映射的内容做了修改，内核的回写机制（writeback）最终会把修改的内容同步到磁盘中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;进程间通信：
&lt;ul&gt;
&lt;li&gt;进程之间的进程地址空间相互隔离，一个进程不能访问到另外一个进程的地址空间。如果多个进程都同时映射到一个相同的文件，就实现了多进程间的共享内存的通信。如果一个进程对映射内容做了修改，那么另外的进程是可以看到的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;栈的效率更高，因为栈是连续的，能更有效利用缓存。而且压栈和出栈都有对应机器指令。&lt;/h3&gt;
&lt;p&gt;https://blog.csdn.net/blunt1991/article/details/14005995&lt;/p&gt;
&lt;p&gt;面试题：&lt;/p&gt;
&lt;p&gt;基本变量存在哪里，创建类的对象时是存在堆上的吗？栈上能不能保存类对象，如果栈上能存，那类里的指针变量也是存在栈里的吗？&lt;/p&gt;
&lt;p&gt;答：不用new创建类对象的时候在栈上。栈可以保存类对象。如果栈上能存，类里的指针变量本身也是在栈里面的。指向的数据如果是new出来的那就是在堆上&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;几种存储类型在内存中，分别被分配在哪一段存储空间。&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auto&lt;/code&gt;存储类型：&lt;code&gt;auto&lt;/code&gt;只能用来标识局部变量的存储类型，对于局部变量，&lt;code&gt;auto&lt;/code&gt;是默认的存储类型，不需要显示的指定。因此，&lt;code&gt;auto&lt;/code&gt;标识的变量存储在栈区中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extern&lt;/code&gt;存储类型：&lt;code&gt;extern&lt;/code&gt;用来声明在&lt;strong&gt;当前文件中引用在当前项目中的其它文件中定义的全局变量&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;如果全局变量未被初始化，那么将被存在BBS区中，且在编译时，自动将其值赋值为0&lt;/li&gt;
&lt;li&gt;如果已经被初始化，那么就被存在数据区中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;register&lt;/code&gt;存储类型：声明为&lt;code&gt;register&lt;/code&gt;的变量在由内存调入到CPU寄存器后，则常驻在CPU的寄存器中，因此访问&lt;code&gt;register&lt;/code&gt;变量将在很大程度上提高效率，因为省去了变量由内存调入到寄存器过程中的好几个指令周期。在C++中，例如 &lt;code&gt;while(i--){};&lt;/code&gt; 对变量 &lt;code&gt;i&lt;/code&gt; 有频繁的操作，编译器会将其存储在寄存器中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt;存储类型：被声明为静态类型的变量，无论是全局的还是局部的，都存储在数据区中，其生命周期为整个程序
&lt;ul&gt;
&lt;li&gt;如果是静态局部变量，其作用域为一对&lt;code&gt;{}&lt;/code&gt;内。&lt;/li&gt;
&lt;li&gt;如果是静态全局变量，其作用域为当前文件。&lt;/li&gt;
&lt;li&gt;静态变量如果没有被初始化，则自动初始化为0。静态变量只能够初始化一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字符串常量：字符串常量存储在数据区中，其生存期为整个程序运行时间，但作用域为当前文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-06-%E6%9D%82%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20220724081541.png&quot; alt=&quot;QQ截图20220724081541&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-07-06-%E6%9D%82%E8%AE%B0.assets/QQ%E6%88%AA%E5%9B%BE20220805045004.png&quot; alt=&quot;QQ截图20220805045004&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不同的段会被分配到不同的页面，不同的页面会有不同的读写或执行权限&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;常量区进一步解释和常量折叠&lt;/h2&gt;
&lt;p&gt;c++ 中，一个 &lt;code&gt;const&lt;/code&gt; 不是必需创建内存空间，而在 c 中，一个 &lt;code&gt;const&lt;/code&gt; 总是需要一块内存空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常量分为全局常量和局部常量&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局常量：
&lt;ul&gt;
&lt;li&gt;是否要为 &lt;code&gt;const&lt;/code&gt;全局变量 分配内存空间，取决于这个全局常量的用途，如果是充当着一个值替换（将一个变量名替换为一个值），那么就不分配内存空间，不过当对这个全局常量取地址或者使用 &lt;code&gt;extern&lt;/code&gt; 时，会分配内存，存储在只读数据段，是不能修改的。因为全局变量在内存中的位置与全局常量一样，只不过没有 read only 属性，因此在这里也就一并提了，全局常量同样被分配到数据段上，但是可以修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const int a = 10;
int arr[a]; //这里的a就不分配内存，因为只是充当值替换。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PS：&lt;strong&gt;未初始化&lt;/strong&gt; 或 &lt;strong&gt;初始化为0&lt;/strong&gt; 的全局变量（包括全局常量）被分配在 .bss 段上，&lt;strong&gt;已初始化&lt;/strong&gt; 的被分配在 &lt;strong&gt;数据段&lt;/strong&gt; 上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部常量
&lt;ul&gt;
&lt;li&gt;对于基础数据类型，也就是 &lt;code&gt;const int a = 10&lt;/code&gt; 这种，编译器会把它放到符号表中，不分配内存，当对其取地址时，会在栈段分配内存。&lt;/li&gt;
&lt;li&gt;对于基础数据类型，如果用一个变量初始化 局部常量，如果 &lt;code&gt;const int a = b&lt;/code&gt;，那么也是会给 &lt;code&gt;a&lt;/code&gt; 在栈段分配内存。&lt;/li&gt;
&lt;li&gt;对于自定数据类型，比如类对象，那么也会在栈段分配内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常量折叠&lt;/h3&gt;
&lt;p&gt;https://www.cnblogs.com/stemon/p/4406824.html&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define PI 3.14
int main()
{
    const int r = 10;
    
    int p = PI;//这里在预编译阶段产生宏替换，PI直接替换为3.14，其实就是int p = 3.14
    int len = 2 * r;//这里会发生常量折叠，也就是说常量r的引用会替换成它对应的值，相当于int len = 2 * 10;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如上述代码中所述，常量折叠表面上的效果和宏替换是一样的，只是，“效果上是一样的”，而两者真正的区别在于，宏是字符常量，在预编译阶段的宏替换完成后，该宏名字会消失，所有对宏如PI的引用已经全部被替换为它所对应的值，编译器当然没有必要再维护这个符号。&lt;/li&gt;
&lt;li&gt;而常量折叠发生的情况是，对常量的引用全部替换为该常量如r的值，但是，常量名r并不会消失，编译器会把他放入到&lt;strong&gt;符号表&lt;/strong&gt;中，同时，如果后面有对其取地址，则会为该变量分配空间，栈空间或者全局空间。既然放到了符号表中，就意味着可以找到这个变量的地址。如果只单纯的替换，则不需要分配空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;符号表不是一张表，是一系列表的统称，这里的const常量，会把这个常量的名字、类型、内存地址、值都放到常量表中。符号表还有一个变量表，这个表放变量的名字、类型、内存地址，但是没有放变量的值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常量折叠说的是，在编译阶段，对该变量进行值替换，同时，如果后面有对其取地址，则该常量拥有自己的内存空间，并非像宏定义一样不分配空间。但是如果只单纯的替换，则不需要分配空间。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;口诀&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;内核权限高，所以是高地址部分（3-4）。用户权限低，所以是低地址部分（0-3）&lt;/li&gt;
&lt;li&gt;栈是系统给的，内核是高地址所以栈也是高地址，高—&amp;gt;低&lt;/li&gt;
&lt;li&gt;堆是程序员在代码写的，所以是贴着代码段。因为代码是第一个要有的东西，所以代码段在低地址也就是最开始。所以堆贴着代码这块所以是低地址，低-&amp;gt;高&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;指针指向的数据不一定在堆上，也可能在栈上。需要看指向的对象是不是new出来的&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int a = 10;
int* aptr = &amp;amp;a; //这时候指针指向的数据在栈上
int* bptr = new int(10); //这时候指针指向的数据在堆上
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;栈的大小 此处是用户栈&lt;/h2&gt;
&lt;p&gt;windows&lt;strong&gt;进程栈&lt;/strong&gt;一般是1MB&lt;/p&gt;
&lt;p&gt;Linux&lt;strong&gt;进程栈&lt;/strong&gt;一般是8MB&lt;/p&gt;
&lt;p&gt;我们上面那张图的一整个方框是一个进程地址空间。里面的栈就是&lt;strong&gt;进程的栈&lt;/strong&gt;大小。是动态调整（推指针）的但是最大是8MB。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;线程因为属于进程，所以线程栈是在进程地址空间的堆和栈中间的mmap内存映射段（因为比较大所以调用mmap函数后从mmap段拿出来）拿出来的。但是线程栈一旦创建之后大小就不能变了。很好理解，因为mmap调用的时候需要确定大小，调用之后内存空间固定，不能动态扩缩容。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;又因为是线程栈是从进程栈拿出来的，所以线程栈的大小和进程栈是有联系的。&lt;/p&gt;
&lt;p&gt;进程和线程的栈分别是多大呢？首先从我们熟悉的ulimit -s说起，熟悉linux的人都应该知道通过ulimit -s可以修改栈的大小，除此之外还有getrlimit/setrlimit两个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int getrlimit(int resource, struct rlimit *rlim);

int setrlimit(int resource, const struct rlimit *rlim);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两个函数当第一个参数传入RLIMIT_STACK时，可以设置和获取栈的大小，其作用和ulimit -s是一样的，只是单位不同，ulimit -s的单位是kB，而这两个函数的单位是B(字节)，详细使用方法请参考man手册。
最后还有线程的pthread_attr_setstacksize/pthread_attr_getstacksize。
&lt;strong&gt;使用setrlimit和使用ulimit -s设置栈大小效果相同，这两种方式都是针对进程栈大小设置，只不过前者只真对当前进程，后者针对当前shell；&lt;/strong&gt;
而线程栈大小的关系就相对比较复杂点，前文说过线程大小是静态的，是在创建时就确定了的，当然如果使用pthread_attr_setstacksize可以在创建线程时指定线程栈大小，但如果不指定线程栈的话其默认大小是什么情况呢？想要了解线程栈的大小就要看glibc的线程创建函数，具体就是pthread_create-&amp;gt;__pthread_create_2_1-&amp;gt;allocate_stack。具体代码还是比较复杂的，这里简化为一个伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;limit = getlimit(RLIMIT_STACK)
if (limit == RLIMIT_INFINITY)

    thread.rlimit = ARCH_STACK_DEFAULT_SIZE //2M

else if thread.rlimit &amp;lt; PTHREAD_STACK_MIN //16k

    thread.rlimit = PTHREAD_STACK_MIN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，线程默认栈大小和进程栈大小的关系：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果ulimit(setrlimit)设置大小大于16k，则线程栈默认大小由ulimit(setrlimit)决定；&lt;/li&gt;
&lt;li&gt;如果ulimit(setrlimit)设置大小小于16k，则线程栈默认大小为16；&lt;/li&gt;
&lt;li&gt;如果ulimit(setrlimit)设置大小为无限制，则线程栈默认大小为2M；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以我们如果使用ulimit设置进程栈大小是无限大其实栈大小反而相对比较小，这是为什么呢？前面我们已经讲过线程栈和进程栈的位置不同，线程栈是进程使用&lt;code&gt;mmap&lt;/code&gt;开辟出来的。，并且不会动态增加，所以不可能设置一个无限大小的线程栈。&lt;/p&gt;
</content:encoded></item><item><title>计算机网络 - 传输层1</title><link>https://sterben-01.github.io/posts/NET1/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/NET1/</guid><pubDate>Thu, 23 Jun 2022 13:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;计算机网络 - 传输层1&lt;/h1&gt;
&lt;h2&gt;传输层&lt;/h2&gt;
&lt;h3&gt;传输层的功能&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;传输层提供&lt;strong&gt;进程与进程&lt;/strong&gt;之间的逻辑通信。使用网络层的服务为应用层提供通信服务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复用和分用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复用：应用层所有的应用进程都可以通过传输层传输到网络层。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;分用：传输层从网络层收到的数据可以交付给不同的应用进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;对收到的报文进行差错检测&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;TCP&lt;/h2&gt;
&lt;h3&gt;三次握手&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220623022220.png&quot; alt=&quot;QQ截图20220623022220&quot; /&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ACK不消耗seq&lt;/code&gt;&lt;/strong&gt;，所以如果自己的上一个包是&lt;code&gt;ACK&lt;/code&gt;的话（报文不携带数据），这次的&lt;code&gt;seq&lt;/code&gt;还是上次的&lt;code&gt;seq&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图里说的上一次自己是ACK则这次seq就是上一次的seq，指的是纯ACK包。也就是报文不传输数据的时候&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里&lt;code&gt;ack = seq + 1&lt;/code&gt; 里面的1其实是接收到的数据的大小。 但是握手阶段不存在这个数据，为了更好理解为何握手/挥手阶段&lt;code&gt;seq/ack&lt;/code&gt;还是要&lt;code&gt;+1&lt;/code&gt;，我们统一给她一个解释：这个握手阶段除了&lt;code&gt;ack&lt;/code&gt;以外的包所搭载的数据叫ghost byte，大小为1。第二种解释是数据大小依旧为0。但是FIN/SYN按照一个字节算。所以+1&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ack&lt;/code&gt;确认号是我们下一次希望收到的发送数据的第一个字节的序号 。**当丢包发生的时候。&lt;code&gt;ack&lt;/code&gt;确认号就是接收方所期待的序号最小的没拿到的数据包。**所以这种确认机制可以实现累计确认。&lt;/p&gt;
&lt;p&gt;所以在&lt;strong&gt;握手阶段&lt;/strong&gt; 我们客户端首次发送的&lt;code&gt;seq = x&lt;/code&gt;，则我们下一次希望收到的发送数据的第一个字节的序号 应为&lt;code&gt;x+1&lt;/code&gt;  所以服务器发回的&lt;code&gt;ack = x + 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;ack&lt;/code&gt;计算方式是 &lt;code&gt;ack(out) = seq(last_time_in) + size_of_received_data + (in)SYN/FIN(1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; seq&lt;/code&gt;序列号是本次所发送数据的第一个字节的序号。假如我们发送 &lt;code&gt;1 2 3&lt;/code&gt; 所以&lt;code&gt;seq&lt;/code&gt;为&lt;code&gt;1&lt;/code&gt; 接着我们发送&lt;code&gt;4 5 6 &lt;/code&gt;所以&lt;code&gt;seq&lt;/code&gt;为&lt;code&gt;4 &lt;/code&gt;也就是&lt;code&gt;上一次序号+上一次自己的数据量 = 1 + 3 = 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;seq&lt;/code&gt;的计算方式是 &lt;code&gt;seq(out) = 上一次自己的seq + 上一次自己的size_of_data + 上一次自己的SYN/FIN(1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般来讲，对方发回的&lt;code&gt;ack&lt;/code&gt;就是自己下一次的&lt;code&gt;seq&lt;/code&gt;&lt;/strong&gt;  。&lt;/p&gt;
&lt;p&gt;因为采用了延迟确认，客户端连续多次发送数据后，服务器传回的&lt;code&gt;ack&lt;/code&gt;将会是最后一次接收到的客户端报文的&lt;code&gt;seq + 收到的data&lt;/code&gt;。如下图。最后一次接收到的&lt;code&gt;seq&lt;/code&gt;是5121，证明5121之前的数据包都拿到了，大小是1024。所以截止至客户端新发送数据之前，服务器回复的每一次&lt;code&gt;ack&lt;/code&gt;都是&lt;code&gt;5121+1024 = 6145&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意大小写。小写&lt;code&gt;ack&lt;/code&gt;是序号，大写&lt;code&gt;ACK&lt;/code&gt;指的是这个包是不是&lt;code&gt;ACK&lt;/code&gt;包&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以说&lt;code&gt;ACK = 1&lt;/code&gt;代表是确认包，所以里面会带有&lt;code&gt;ack&lt;/code&gt;确认序号。也就是希望对面继续发送的数据的第一个字节的序号。&lt;/p&gt;
&lt;h3&gt;序列号&lt;code&gt;seq&lt;/code&gt;解决乱序问题。&lt;code&gt;ack&lt;/code&gt;应答码解决丢包问题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220623195418.png&quot; alt=&quot;QQ截图20220623195418&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;延迟确认&lt;/h3&gt;
&lt;p&gt;简单的说，Delay Ack就是延时发送ACK，在收到数据包的时候，会检查是否需要发送ACK，如果需要的话，进行快速ACK还是延时ACK，在无法使用快速确认的条件下，就会使用Delay Ack。&lt;/p&gt;
&lt;p&gt;TCP在何时发送ACK的时候有如下规定：&lt;/p&gt;
&lt;p&gt;1.当有响应数据发送的时候，ACK会随着数据一块发送&lt;/p&gt;
&lt;p&gt;2.如果没有响应数据，ACK就会有一个延迟，以等待是否有响应数据一块发送，但是这个延迟一般在40ms~500ms之间，一般情况下在40ms左右，如果在40ms内有数据发送，那么ACK会随着数据一块发送，对于这个延迟的需要注意一下，这个延迟并不是指的是收到数据到发送ACK的时间延迟，而是内核会启动一个定时器，每隔200ms就会检查一次，比如定时器在0ms启动，200ms到期，180ms的时候data来到，那么200ms的时候没有响应数据，ACK仍然会被发送，这个时候延迟了20ms.&lt;/p&gt;
&lt;p&gt;3.如果在等待发送ACK期间，第二个数据又到了，这时候就要立即发送ACK！&lt;/p&gt;
&lt;p&gt;优点：减少了数据段的个数，提高了发送效率&lt;/p&gt;
&lt;p&gt;缺点：过多的delay会拉长RTT&lt;/p&gt;
&lt;h3&gt;累计确认&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;累计确认指的是TCP当前的ack应答码一定是保证了这个应答码之前的所有数据已经全部收到了。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以在丢包的时候，我们回复的ack应答码是丢失的数据的第一个字节（接收方所期待的序号最小的没拿到的数据包）。&lt;/li&gt;
&lt;li&gt;所以如果没有丢包而是丢了ACK应答报文，则由于滑动窗口的存在，所以我们可以用这种累计确认（下一次应答）来进行确认，而且不会进行数据重发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805034441.png&quot; alt=&quot;QQ截图20220805034441&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;四次挥手 （主动关闭的一方才有timewait）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220623024124-16559727402733.png&quot; alt=&quot;QQ截图20220623024124&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当客户端发送链接释放报文段之后，客户端停止发送数据。主动关闭TCP链接。也就是第一次FIN。之后客户端进入 &lt;code&gt;FIN_WAIT_1&lt;/code&gt; 状态。&lt;strong&gt;（此时仅仅表示客户端不需要发送数据了，但还可以接受数据）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;服务器接受到客户端发送的释放报文段后，回送一个ack报文段。客户到服务器的这个方向的连接也就被释放了。此时是&lt;strong&gt;半关闭状态&lt;/strong&gt;。服务端进入 &lt;code&gt;CLOSED_WAIT&lt;/code&gt; 状态。（&lt;strong&gt;此时表示为服务端知道了客户端不发送了，但是服务端可能还要发送&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;客户端收到服务端的 &lt;code&gt;ACK&lt;/code&gt; 应答报文后，之后进入 &lt;code&gt;FIN_WAIT_2&lt;/code&gt; 状态。&lt;/li&gt;
&lt;li&gt;服务器发送/处理完剩余数据后，也发送链接释放报文段。主动关闭TCP链接。也就是第二次FIN。之后服务端进入 &lt;code&gt;LAST_ACK&lt;/code&gt; 状态。（&lt;strong&gt;服务端处理完剩下的数据后，才发送FIN表示我也完事儿了。可以彻底关闭&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;客户端接受到后，回送一个ack报文段。进入 &lt;code&gt;TIME_WAIT&lt;/code&gt; 状态。服务器收到了 &lt;code&gt;ACK&lt;/code&gt; 应答报文后，就进入了 &lt;code&gt;CLOSED&lt;/code&gt; 状态，至此服务端已经完成连接的关闭。&lt;/li&gt;
&lt;li&gt;再等到时间等待计时器设置的2MSL（最长报文段寿命）后，客户端进入 &lt;code&gt;CLOSED&lt;/code&gt; 状态，链接彻底关闭。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果客户端发送的确认报文段&lt;/p&gt;
&lt;h3&gt;注意事项&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;主动关闭连接的，才有 &lt;code&gt;TIME_WAIT 状态&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四次挥手之所以为四次就是因为可以半关闭状态。也就是服务器可能还要发送一些数据给客户端。理论上四次挥手也可以换成三次。如果服务器在收到客户端的 &lt;code&gt;FIN&lt;/code&gt; 时没有更多数据或根本没有数据要发送，则可以将 ack 和 fin 合并为一个包。&lt;/strong&gt; 或者，因为延迟确认的特性，可以把ACK,FIN（第二次和第三次挥手)和要发的数据一起发过去，这样也是三次挥手。&lt;/li&gt;
&lt;li&gt;需要三次握手而不是两次握手的原因之一是：为了防止已失效的连接请求报文段突然又传送到了服务端，因而产生错误。
&lt;ul&gt;
&lt;li&gt;假设客户端先发了一个序列号为90的&lt;code&gt;SYN&lt;/code&gt;请求报文, 但是它在网络中某个节点被阻塞了, 然后客户端又发送了另一个新的&lt;code&gt;SYN&lt;/code&gt;请求报文, 序列号为100, 但是旧的请求报文比新的先到达服务端, 然后服务端回返回一个&lt;code&gt;SYN ACK&lt;/code&gt;报文, 其中&lt;code&gt;ACK&lt;/code&gt;确认号为91, 这显然是错的, 客户端收到该&lt;code&gt;ACK&lt;/code&gt;报文之后, 发现确认号为91而不是101, 判定这是一个历史连接, 客户端就发送一个&lt;code&gt;RST&lt;/code&gt;报文回去告知服务端, 终止这次连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在连接建立后，所有传送的报文段都必须把ACK置为1。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;TCP **不会为没有数据的&lt;code&gt;ACK&lt;/code&gt;**包进行重传。&lt;strong&gt;当没有数据的&lt;code&gt;ACK&lt;/code&gt; 包丢失了，就由对方重传对应的报文&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;握手阶段的前两次不可以携带数据。第三次&lt;code&gt;ACK&lt;/code&gt;可以携带数据。也就是&lt;code&gt;SYN=1&lt;/code&gt;的报文段不可以携带数据。&lt;/li&gt;
&lt;li&gt;挥手阶段的FIN包可以携带数据&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;挥手比握手多一次的原因就是，握手阶段，前两次无法发送数据，所以响应和发送（SYN和ACK）可以要连起来。挥手阶段一方关闭连接了另一方还可能发送。所以响应和发送（FIN和ACK）是分开的&lt;/h2&gt;
&lt;h2&gt;握手丢失会发生什么？&lt;/h2&gt;
&lt;h3&gt;第一次握手丢失了，会发生什么？ -- 客户端超时重传SYN，超过次数断开。&lt;/h3&gt;
&lt;p&gt;客户端想和服务端建立 TCP 连接的时候，首先第一个发的就是 SYN 报文，然后进入到 &lt;code&gt;SYN_SENT&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;在这之后，如果客户端迟迟收不到服务端的 SYN-ACK 报文（第二次握手），就会触发「超时重传」机制，重传 SYN 报文。&lt;/p&gt;
&lt;p&gt;不同版本的操作系统可能超时时间不同，有的 1 秒的，也有 3 秒的，这个超时时间是写死在内核里的，如果想要更改则需要重新编译内核，比较麻烦。&lt;strong&gt;重传的 SYN 报文的序列号都是一样的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后，客户端就会重发 SYN 报文，那到底重发几次呢？&lt;/p&gt;
&lt;p&gt;在 Linux 里，客户端的 SYN 报文最大重传次数由 &lt;code&gt;tcp_syn_retries&lt;/code&gt;内核参数控制，这个参数是可以自定义的，默认值一般是 5。&lt;/p&gt;
&lt;p&gt;通常，第一次超时重传是在 1 秒后，第二次超时重传是在 2 秒，第三次超时重传是在 4 秒后，第四次超时重传是在 8 秒后，第五次是在超时重传 16 秒后。没错，&lt;strong&gt;每次超时的时间是上一次的 2 倍&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当第五次超时重传后，会继续等待 32 秒，如果服务端仍然没有回应 ACK，客户端就不再发送 SYN 包，然后断开 TCP 连接。&lt;/p&gt;
&lt;p&gt;所以，总耗时是 1+2+4+8+16+32=63 秒，大约 1 分钟左右。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当第一次握手一直丢失时，发生的过程如下图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824195815.png&quot; alt=&quot;QQ截图20220824195815&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当客户端超时重传 3 次 SYN 报文后，由于 &lt;code&gt;tcp_syn_retries&lt;/code&gt;为 3，已达到最大重传次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到服务端的第二次握手（&lt;code&gt;SYN-ACK &lt;/code&gt;报文），那么客户端就会断开连接&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第二次握手丢失了，会发生什么？ -- 客户端超时重传SYN,服务端超时重传SYN-ACK，超过次数断开&lt;/h3&gt;
&lt;p&gt;当服务端收到客户端的第一次握手后，就会回 SYN-ACK 报文给客户端，这个就是第二次握手，此时服务端会进入 &lt;code&gt;SYN_RCVD&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;第二次握手的 &lt;code&gt;SYN-ACK&lt;/code&gt; 报文其实有两个目的 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二次握手里的 ACK， 是对第一次握手的确认报文；&lt;/li&gt;
&lt;li&gt;第二次握手里的 SYN，是服务端发起建立 TCP 连接的报文；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，如果第二次握手丢了，就会发送比较有意思的事情，具体会怎么样呢？&lt;/p&gt;
&lt;p&gt;因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文，所以，如果客户端迟迟没有收到第二次握手，那么客户端就觉得可能自己的 SYN 报文（第一次握手）丢失了，于是&lt;strong&gt;客户端就会触发超时重传机制，重传 SYN 报文&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然后，因为第二次握手中包含服务端的 SYN 报文，所以当客户端收到后，需要给服务端发送 ACK 确认报文（第三次握手），服务端才会认为该 SYN 报文被客户端收到了。&lt;/p&gt;
&lt;p&gt;那么，如果第二次握手丢失了，服务端就收不到第三次握手，于是&lt;strong&gt;服务端这边会触发超时重传机制，重传 SYN-ACK 报文&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 Linux 下，SYN-ACK 报文的最大重传次数由 &lt;code&gt;tcp_synack_retries&lt;/code&gt;内核参数决定，默认值是 5。&lt;/p&gt;
&lt;p&gt;因此，当第二次握手丢失了，客户端和服务端都会重传：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端会重传 SYN 报文，也就是第一次握手，最大重传次数由 &lt;code&gt;tcp_syn_retries&lt;/code&gt;内核参数决定；&lt;/li&gt;
&lt;li&gt;服务端会重传 SYN-ACK 报文，也就是第二次握手，最大重传次数由 &lt;code&gt;tcp_synack_retries&lt;/code&gt; 内核参数决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;当第二次握手一直丢失时，发生的过程如下图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824195907.png&quot; alt=&quot;QQ截图20220824195907&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当客户端超时重传 1 次&lt;code&gt; SYN&lt;/code&gt; 报文后，由于 &lt;code&gt; tcp_syn_retries&lt;/code&gt; 为 1，已达到最大重传次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到服务端的第二次握手（&lt;code&gt;SYN-ACK&lt;/code&gt; 报文），那么客户端就会断开连接。&lt;/li&gt;
&lt;li&gt;当服务端超时重传 2 次 &lt;code&gt;SYN-ACK&lt;/code&gt; 报文后，由于 &lt;code&gt;tcp_synack_retries&lt;/code&gt; 为 2，已达到最大重传次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到客户端的第三次握手（&lt;code&gt;ACK&lt;/code&gt; 报文），那么服务端就会断开连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第三次握手丢失了，会发生什么？ -- 服务端超时重传SYN-ACK，超过次数断开。因为客户端的纯ACK（不带数据的ACK）是不会重传的。如果此时客户端发送数据，则服务器会发送RST表示断开连接。&lt;/h3&gt;
&lt;p&gt;客户端收到服务端的 SYN-ACK 报文后，就会给服务端回一个 ACK 报文，也就是第三次握手，此时客户端状态进入到 &lt;code&gt;ESTABLISH&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文，所以当第三次握手丢失了，如果服务端那一方迟迟收不到这个确认报文，就会触发超时重传机制，重传 SYN-ACK 报文，直到收到第三次握手，或者达到最大重传次数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当第三次握手一直丢失时，发生的过程如下图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824200047.png&quot; alt=&quot;QQ截图20220824200047&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端超时重传 2 次 SYN-ACK 报文后，由于 &lt;code&gt;tcp_synack_retries&lt;/code&gt; 为 2，已达到最大重传次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到客户端的第三次握手（&lt;code&gt;ACK&lt;/code&gt; 报文），那么服务端就会断开连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;挥手丢失会发生什么？&lt;/h2&gt;
&lt;h3&gt;第一次挥手丢失了，会发生什么？ --客户端超时重传FIN，超过次数直接close&lt;/h3&gt;
&lt;p&gt;当客户端（主动关闭方）调用 close 函数后，就会向服务端发送 FIN 报文，试图与服务端断开连接，此时客户端的连接进入到 &lt;code&gt;FIN_WAIT_1&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;正常情况下，如果能及时收到服务端（被动关闭方）的 ACK，则会很快变为 &lt;code&gt;FIN_WAIT2&lt;/code&gt;状态。&lt;/p&gt;
&lt;p&gt;如果第一次挥手丢失了，那么客户端迟迟收不到被动方的 ACK 的话，也就会触发超时重传机制，重传 FIN 报文，重发次数由 &lt;code&gt;tcp_orphan_retries&lt;/code&gt; 参数控制。&lt;/p&gt;
&lt;p&gt;当客户端重传 FIN 报文的次数超过 &lt;code&gt;tcp_orphan_retries&lt;/code&gt; 后，就不再发送 FIN 报文，直接进入到 &lt;code&gt;close&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当第一次挥手一直丢失时，发生的过程如下图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824203900.png&quot; alt=&quot;QQ截图20220824203900&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当客户端超时重传 3 次 &lt;code&gt;FIN&lt;/code&gt; 报文后，由于 &lt;code&gt;tcp_orphan_retries&lt;/code&gt; 为 3，已达到最大重传次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到服务端的第二次挥手（&lt;code&gt;ACK&lt;/code&gt;报文），那么客户端就会断开连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第二次挥手丢失了，会发生什么？-- 客户端超时重传FIN，超过次数直接close。因为服务器的纯ACK不会重传&lt;/h3&gt;
&lt;p&gt;当服务端收到客户端的第一次挥手后，就会先回一个 ACK 确认报文，此时服务端的连接进入到 &lt;code&gt;CLOSE_WAIT&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;在前面我们也提了，ACK 报文是不会重传的，所以如果服务端的第二次挥手丢失了，客户端就会触发超时重传机制，重传 FIN 报文，直到收到服务端的第二次挥手，或者达到最大的重传次数。&lt;/p&gt;
&lt;p&gt;这里提一下，当客户端收到第二次挥手，也就是收到服务端发送的 ACK 报文后，客户端就会处于 &lt;code&gt;FIN_WAIT2&lt;/code&gt; 状态，在这个状态需要等服务端发送第三次挥手，也就是服务端的 FIN 报文。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824204227.png&quot; alt=&quot;QQ截图20220824204227&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于 close 函数关闭的连接，由于无法再发送和接收数据，所以&lt;code&gt;FIN_WAIT2&lt;/code&gt; 状态不可以持续太久，而 &lt;code&gt;tcp_fin_timeout&lt;/code&gt; 控制了这个状态下连接的持续时长，默认值是 60 秒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这意味着对于调用 close 关闭的连接，如果在 60 秒后还没有收到 FIN 报文，客户端（主动关闭方）的连接就会直接关闭。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是注意，如果主动关闭方使用 shutdown 函数关闭连接且指定只关闭发送方向，而接收方向并没有关闭，那么意味着主动关闭方还是可以接收数据的。如果主动关闭方一直没收到第三次挥手，那么主动关闭方的连接将会一直处于 &lt;code&gt;FIN_WAIT2&lt;/code&gt; 状态（&lt;code&gt;tcp_fin_timeout&lt;/code&gt; 无法控制 shutdown 关闭的连接）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824204236.png&quot; alt=&quot;QQ截图20220824204236&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当第二次挥手一直丢失时，发生的过程如下图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824204001.png&quot; alt=&quot;QQ截图20220824204001&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当客户端超时重传 2 次 &lt;code&gt;FIN&lt;/code&gt; 报文后，由于 &lt;code&gt;tcp_orphan_retries&lt;/code&gt; 为 2，已达到最大重传次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到服务端的第二次挥手（&lt;code&gt;ACK&lt;/code&gt; 报文），那么客户端就会断开连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第三次挥手丢失了，会发生什么？-- 服务端超时重传FIN，超过次数直接close&lt;/h3&gt;
&lt;p&gt;当服务端（被动关闭方）收到客户端（主动关闭方）的 FIN 报文后，内核会自动回复 ACK，同时连接处于 &lt;code&gt;CLOSE_WAIT&lt;/code&gt; 状态，顾名思义，它表示等待应用进程调用 close 函数关闭连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此时，内核是没有权利替代进程关闭连接，必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;服务端处于&lt;code&gt;CLOSE_WAIT&lt;/code&gt;状态时，调用了 close 函数，内核就会发出 FIN 报文，同时连接进入 &lt;code&gt;LAST_ACK&lt;/code&gt; 状态，等待客户端返回 ACK 来确认连接关闭。&lt;/p&gt;
&lt;p&gt;如果客户端迟迟收不到这个 ACK，服务端就会重发 FIN 报文，重发次数仍然由 &lt;code&gt;tcp_orphan_retrie&lt;/code&gt;s 参数控制，这与客户端重发 FIN 报文的重传次数控制方式是一样的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当第三次挥手一直丢失时，发生的过程如下图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824204407.png&quot; alt=&quot;QQ截图20220824204407&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当服务端重传第三次挥手报文的次数达到了 3 次后，由于 tcp_orphan_retries 为 3，达到了重传最大次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到客户端的第四次挥手（ACK报文），那么服务端就会断开连接。&lt;/li&gt;
&lt;li&gt;客户端因为是通过 close 函数关闭连接的，处于 FIN_WAIT_2 状态是有时长限制的，如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手（FIN 报文），那么客户端就会断开连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第四次挥手丢失了，会发生什么？-- 服务端超时重传FIN，超过次数断开。因为客户端的纯ACK不会重传。&lt;/h3&gt;
&lt;p&gt;当客户端收到服务端的第三次挥手的 FIN 报文后，就会回 ACK 报文，也就是第四次挥手，此时客户端连接进入 &lt;code&gt;TIME_WAIT&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;在 Linux 系统，&lt;code&gt;TIME_WAIT&lt;/code&gt; 状态会持续 2MSL 后才会进入关闭状态。&lt;/p&gt;
&lt;p&gt;然后，服务端（被动关闭方）没有收到 ACK 报文前，还是处于&lt;code&gt; LAST_ACK&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;如果第四次挥手的 ACK 报文没有到达服务端，服务端就会重发 FIN 报文，重发次数仍然由前面介绍过的 &lt;code&gt;tcp_orphan_retries&lt;/code&gt; 参数控制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当第四次挥手一直丢失时，发生的过程如下图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220824204725.png&quot; alt=&quot;QQ截图20220824204725&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当服务端重传第三次挥手报文达到 2 时，由于 tcp_orphan_retries 为 2， 达到了最大重传次数，于是再等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到客户端的第四次挥手（ACK 报文），那么服务端就会断开连接。&lt;/li&gt;
&lt;li&gt;客户端在收到第三次挥手后，就会进入 TIME_WAIT 状态，开启时长为 2MSL 的定时器，如果途中再次收到第三次挥手（FIN 报文）后，就会重置定时器，当等待 2MSL 时长后，客户端就会断开连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;关于 TIME_WAIT&lt;/h2&gt;
&lt;h3&gt;为什么需要 TIME_WAIT 状态？&lt;/h3&gt;
&lt;p&gt;需要 TIME-WAIT 状态，主要是两个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;防止历史连接中的数据，被后面相同四元组的连接错误的接收。&lt;/p&gt;
&lt;p&gt;服务端在关闭连接之前发送的报文，被网络延迟了。接着，服务端以相同的四元组重新打开了新连接，前面被延迟的报文这时抵达了客户端，而且该数据报文的序列号刚好在客户端接收窗口内，因此客户端会正常接收这个数据报文，但是这个数据报文是上一个连接残留下来的，这样就产生数据错乱等严重的问题。为了防止历史连接中的数据，被后面相同四元组的连接错误的接收，因此 TCP 设计了 TIME_WAIT 状态，状态会持续 &lt;code&gt;2MSL&lt;/code&gt; 时长，这个时间&lt;strong&gt;足以让两个方向上的数据包都被丢弃，使得原来连接的数据包在网络中都自然消失，再出现的数据包一定都是新建立连接所产生的。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保证「被动关闭连接」的一方，能被正确的关闭&lt;/p&gt;
&lt;p&gt;如果客户端（主动关闭方）最后一次 ACK 报文（第四次挥手）在网络中丢失了，那么按照 TCP 可靠性原则，服务端（被动关闭方）会重发 FIN 报文。假设客户端没有 TIME_WAIT 状态，而是在发完最后一次回 ACK 报文就直接进入 CLOSED 状态，如果该 ACK 报文丢失了，服务端则重传的 FIN 报文，而这时客户端已经进入到关闭状态了，在收到服务端重传的 FIN 报文后，就会回 RST 报文。服务端收到这个 RST 并将其解释为一个错误（Connection reset by peer），这对于一个可靠的协议来说不是一个优雅的终止方式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;简单来说就是，如果第四次握手丢了，服务端会超时重传FIN，如果没有TIME_WAIT（客户端直接close)则客户端会回复一个RST。不够优雅。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了防止这种情况出现，客户端必须等待足够长的时间确保对端收到 ACK，如果对端没有收到 ACK，那么就会触发 TCP 重传机制，服务端会重新发送一个 FIN，这样一去一来刚好两个 MSL 的时间。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么 TIME_WAIT 等待的时间是 2MSL？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MSL&lt;/code&gt; 是 Maximum Segment Lifetime，&lt;strong&gt;报文最大生存时间&lt;/strong&gt;，它是任何报文在网络上存在的最长时间，超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的，而 IP 头中有一个 &lt;code&gt;TTL&lt;/code&gt; 字段，是 IP 数据报可以经过的最大路由数，每经过一个处理他的路由器此值就减 1，当此值为 0 则数据报将被丢弃，同时发送 ICMP 报文通知源主机。&lt;/p&gt;
&lt;p&gt;MSL 与 TTL 的区别： MSL 的单位是时间，而 TTL 是经过路由跳数。所以 &lt;strong&gt;MSL 应该要大于等于 TTL 消耗为 0 的时间&lt;/strong&gt;，以确保报文已被自然消亡。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TTL 的值一般是 64，Linux 将 MSL 设置为 30 秒，意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒，如果超过了，就认为报文已经消失在网络中了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;TIME_WAIT 等待 2 倍的 MSL，比较合理的解释是： 网络中可能存在来自发送方的数据包，当这些发送方的数据包被接收方处理后又会向对方发送响应，所以&lt;strong&gt;一来一回需要等待 2 倍的时间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如，如果被动关闭方没有收到断开连接的最后的 ACK 报文，就会触发超时重发 &lt;code&gt;FIN&lt;/code&gt; 报文，另一方接收到 FIN 后，会重发 ACK 给被动关闭方， 一来一去正好 2 个 MSL。&lt;/p&gt;
&lt;p&gt;可以看到 &lt;strong&gt;2MSL时长&lt;/strong&gt; 这其实是相当于&lt;strong&gt;至少允许报文丢失一次&lt;/strong&gt;。比如，若 ACK 在一个 MSL 内丢失，这样被动方重发的 FIN 会在第 2 个 MSL 内到达，TIME_WAIT 状态的连接可以应对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;2MSL&lt;/code&gt; 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内，因为客户端的 ACK 没有传输到服务端，客户端又接收到了服务端重发的 FIN 报文，那么 2MSL 时间将重新计时&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 Linux 系统里 &lt;code&gt;2MSL&lt;/code&gt; 默认是 &lt;code&gt;60&lt;/code&gt; 秒，那么一个 &lt;code&gt;MSL&lt;/code&gt; 也就是 &lt;code&gt;30&lt;/code&gt; 秒。&lt;strong&gt;Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;半连接队列和全连接队列&lt;/h3&gt;
&lt;p&gt;在服务端当中，对socket执行bind方法可以绑定监听端口，然后执行&lt;code&gt;listen方法&lt;/code&gt;后，就会进入监听（&lt;code&gt;LISTEN&lt;/code&gt;）状态。内核会为每一个处于&lt;code&gt;LISTEN&lt;/code&gt;状态的&lt;code&gt;socket&lt;/code&gt; 分配两个队列，分别叫&lt;strong&gt;半连接队列和全连接队列&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220629093515.png&quot; alt=&quot;QQ截图20220629093515&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;半连接队列（SYN队列）&lt;/strong&gt;，服务端收到&lt;strong&gt;第一次握手&lt;/strong&gt;后，会将&lt;code&gt;这个连接对应的文件描述符&lt;/code&gt;加入到这个队列中，队列内的&lt;code&gt;文件描述符对应的连接&lt;/code&gt;都处于&lt;code&gt;SYN_RCVD&lt;/code&gt; 状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全连接队列（ACCEPT队列）&lt;/strong&gt;，在服务端收到&lt;strong&gt;第三次握手&lt;/strong&gt;后，会将半连接队列的&lt;code&gt;文件描述符&lt;/code&gt;取出，放到全连接队列中。队列里的&lt;code&gt;文件描述符对应的连接&lt;/code&gt;都处于 &lt;code&gt;ESTABLISHED&lt;/code&gt;状态。这里面的文件描述符，就&lt;strong&gt;等着服务端执行accept()后被取出了。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;全连接队列（&lt;code&gt;icsk_accept_queue&lt;/code&gt;）是个链表&lt;/strong&gt;，而&lt;strong&gt;半连接队列（&lt;code&gt;syn_table&lt;/code&gt;）是个哈希表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以建立连接的过程中根本不需要&lt;code&gt;accept()&lt;/code&gt; 参与， &lt;strong&gt;执行accept()只是为了从全连接队列里取出对应的文件描述符。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;listen&lt;/code&gt;函数的&lt;code&gt;backlog&lt;/code&gt;控制的是全连接队列（ACCEPT队列）队列的大小。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;为什么半连接队列要设计成哈希表&lt;/h4&gt;
&lt;p&gt;先对比下&lt;strong&gt;全连接里队列&lt;/strong&gt;，他本质是个链表，因为也是线性结构，说它是个队列也没毛病。它里面放的都是已经建立完成的连接，这些连接正等待被取走。而服务端取走连接的过程中，并不关心具体是哪个连接，只要是个连接就行，所以直接从队列头取就行了。这个过程算法复杂度为&lt;code&gt;O(1)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;半连接队列&lt;/strong&gt;却不太一样，因为队列里的都是不完整的连接，嗷嗷等待着第三次握手的到来。那么现在有一个第三次握手来了，则需要从队列里把相应IP端口的连接取出，&lt;strong&gt;如果半连接队列还是个链表，那我们就需要依次遍历，才能拿到我们想要的那个连接，算法复杂度就是O(n)。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而如果将半连接队列设计成哈希表，那么查找半连接的算法复杂度就回到&lt;code&gt;O(1)&lt;/code&gt;了。因此出于效率考虑，全连接队列被设计成链表，而半连接队列被设计为哈希表。&lt;/p&gt;
&lt;h4&gt;全连接队列满了会怎么样？&lt;/h4&gt;
&lt;p&gt;如果队列满了，服务端还收到客户端的第三次握手ACK，默认当然会丢弃这个ACK。但除了丢弃之外，还有一些附带行为，这会受 &lt;code&gt;tcp_abort_on_overflow&lt;/code&gt; 参数的影响。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tcp_abort_on_overflow&lt;/code&gt;设置为 0，全连接队列满了之后，会丢弃这个第三次握手ACK包，并且开启定时器，重传第二次握手的SYN+ACK，如果重传超过一定限制次数，还会把对应的&lt;strong&gt;半连接队列里的连接&lt;/strong&gt;给删掉。&lt;strong&gt;也就是丢弃链接&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220629094504.png&quot; alt=&quot;QQ截图20220629094504&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tcp_abort_on_overflow&lt;/code&gt;设置为 1，全连接队列满了之后，就直接发RST给客户端，效果上看就是连接断了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220629094528.png&quot; alt=&quot;QQ截图20220629094528&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;半连接队列要是满了会怎么样 （SYN攻击）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;一般是丢弃&lt;/strong&gt;，但这个行为可以通过 &lt;code&gt;tcp_syncookies&lt;/code&gt; 参数去控制。但比起这个，更重要的是先了解下半连接队列为什么会被打满。&lt;/p&gt;
&lt;p&gt;首先我们需要明白，一般情况下，半连接的&quot;生存&quot;时间其实很短，只有在第一次和第三次握手间，如果半连接都满了，说明服务端疯狂收到第一次握手请求，如果是线上游戏应用，能有这么多请求进来，那说明你可能要富了。但现实往往比较骨感，你可能遇到了&lt;strong&gt;SYN Flood攻击&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所谓&lt;strong&gt;SYN Flood攻击&lt;/strong&gt;，可以简单理解为，攻击方模拟客户端疯狂发第一次握手请求过来，在服务端憨憨地回复第二次握手过去之后，客户端死活不发第三次握手过来，这样做，可以把服务端半连接队列打满，从而导致正常连接不能正常进来。&lt;/p&gt;
&lt;p&gt;那这种情况怎么处理？有没有一种方法可以&lt;strong&gt;绕过半连接队列&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;有，&lt;code&gt;tcp_syncookies&lt;/code&gt;派上用场了。它被设置为1的时候，客户端发来&lt;strong&gt;第一次握手&lt;/strong&gt;SYN时，服务端&lt;strong&gt;不会将其放入半连接队列中&lt;/strong&gt;，而是直接生成一个&lt;code&gt;cookies&lt;/code&gt;，这个&lt;code&gt;cookies&lt;/code&gt;会跟着&lt;strong&gt;第二次握手&lt;/strong&gt;，发回客户端。客户端在发&lt;strong&gt;第三次握手&lt;/strong&gt;的时候带上这个&lt;code&gt;cookies&lt;/code&gt;，服务端验证到它就是当初发出去的那个，就会建立连接并放入到全连接队列中。可以看出整个过程不再需要半连接队列的参与。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220629094704.png&quot; alt=&quot;QQ截图20220629094704&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;cookies方案为什么不直接取代半连接队列？&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;目前看下来&lt;code&gt;syn cookies&lt;/code&gt;方案省下了半连接队列所需要的队列内存，还能解决 &lt;strong&gt;SYN Flood攻击&lt;/strong&gt;，那为什么不直接取代半连接队列？&lt;/p&gt;
&lt;p&gt;凡事皆有利弊，&lt;code&gt;cookies&lt;/code&gt;方案虽然能防 &lt;strong&gt;SYN Flood攻击&lt;/strong&gt;，但是也有一些问题。因为服务端并不会保存连接信息，所以如果传输过程中数据包丢了，也不会重发第二次握手的信息。&lt;/p&gt;
&lt;p&gt;另外，编码解码&lt;code&gt;cookies&lt;/code&gt;，都是比较&lt;strong&gt;耗CPU&lt;/strong&gt;的，利用这一点，如果此时攻击者构造大量的&lt;strong&gt;第三次握手包（ACK包）&lt;/strong&gt;，同时带上各种瞎编的&lt;code&gt;cookies&lt;/code&gt;信息，服务端收到&lt;code&gt;ACK包&lt;/code&gt;后&lt;strong&gt;以为是正经cookies&lt;/strong&gt;，憨憨地跑去解码（&lt;strong&gt;耗CPU&lt;/strong&gt;），最后发现不是正经数据包后才丢弃。&lt;/p&gt;
&lt;p&gt;这种通过构造大量&lt;code&gt;ACK包&lt;/code&gt;去消耗服务端资源的攻击，叫&lt;strong&gt;ACK攻击&lt;/strong&gt;，受到攻击的服务器可能会因为&lt;strong&gt;CPU资源耗尽&lt;/strong&gt;导致没能响应正经请求。&lt;/p&gt;
&lt;h3&gt;全连接队列，半连接队列总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每一个&lt;/strong&gt;&lt;code&gt;socket&lt;/code&gt;执行&lt;code&gt;listen&lt;/code&gt;时，内核都会自动创建一个半连接队列和全连接队列。&lt;/li&gt;
&lt;li&gt;第三次握手前，TCP连接会放在半连接队列中，直到第三次握手到来，才会被放到全连接队列中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;accept方法&lt;/code&gt;只是为了从全连接队列中拿出一条连接，本身跟三次握手几乎&lt;strong&gt;毫无关系&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;出于效率考虑，虽然都叫队列，但半连接队列其实被设计成了&lt;strong&gt;哈希表&lt;/strong&gt;，而全连接队列本质是链表。&lt;/li&gt;
&lt;li&gt;全连接队列满了，再来第三次握手也会丢弃，此时如果&lt;code&gt;tcp_abort_on_overflow=1&lt;/code&gt;，还会直接发&lt;code&gt;RST&lt;/code&gt;给客户端。&lt;/li&gt;
&lt;li&gt;半连接队列满了，可能是因为受到了&lt;code&gt;SYN Flood&lt;/code&gt;攻击，可以设置&lt;code&gt;tcp_syncookies&lt;/code&gt;，绕开半连接队列。&lt;/li&gt;
&lt;li&gt;客户端没有半连接队列和全连接队列，但有一个&lt;strong&gt;全局hash&lt;/strong&gt;，可以通过它实现自连接或TCP同时打开。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;杂项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同步位&lt;code&gt;SYN = 1&lt;/code&gt; 时表明这是一个连接请求/接受报文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;紧急位&lt;code&gt;URG = 1&lt;/code&gt; 的时候表明此报文段有紧急数据。不用在缓存队列中排队。配合紧急指针使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;紧急指针指向的是紧急数据在此报文段中的末位位置。假如紧急指针 = 100，则1~100为紧急数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复位&lt;code&gt;RST = 1&lt;/code&gt;的时候表明TCP连接出现严重错误。需要释放连接再重新连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;推送位&lt;code&gt;PSH = 1&lt;/code&gt;时表明接收方应尽快将数据交付给应用程序，不需要等到缓存填满再交付。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;终止位&lt;code&gt;FIN = 1&lt;/code&gt;时表明此报文段发送方数据已经发送完毕，需要释放连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bind()设置套接字的本地（源）地址。这是接收数据包的地址。套接字发送的数据包将此作为源地址，因此其他主机将知道将其数据包发送回哪里。 这句话的意思就是你接收到了数据包之后，bind告诉系统你发到这个端口和ip的数据包要发给某个特定的文件描述符。所以只有接受需求的时候才会用bind。客户端不写bind是因为内核自动选择了一个端口。 如果不需要接收，则套接字源地址是无用的。像 TCP 这样的协议需要启用接收才能正确发送，因为当一个或多个数据包到达时，目标主机会发回确认（即确认）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;被动接收的一端都要绑定一个端口，主动发送的一端一般都是系统分配端口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标准TCP只有单播因为是点对点，只有UDP有多播和组播&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用send函数仅仅是把数据拷贝到发送的缓存区。具体发送不是应用程序可控的。所以说send就算不返回错误也有可能发送失败。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果服务器重启，大量客户端重新连接进来，如果服务器处理accept队列不及时将会发生连接被拒绝的错误。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么TCP、UDP套接字服务器端需要绑定端口号客户端不需要？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;IP地址和端口号是用来标识具体某一台主机上的具体一个进程的。也就是说，端口号可以用来标识主机上的某一个进程。&lt;/li&gt;
&lt;li&gt;因此，操作系统需要对端口号进行管理，并且计算机中的端口号是有限的。&lt;/li&gt;
&lt;li&gt;如果不进行绑定，操作系统会随机生成一个端口号给服务器。如果操作系统给服务器分配这个端口号的同时，有其他程序也准备使用这个端口号或者说端口号已经被使用，则可能会导致服务器一直启动不起来。&lt;/li&gt;
&lt;li&gt;其次，服务器运行起来就不会在停止了，我们将服务器端的端口号绑定有助于有规划的对主机中的端口号进行使用。&lt;/li&gt;
&lt;li&gt;客户端需要主动向服务器端发送请求，因此客户端就需要知道服务器端的IP地址和端口号，如果不绑定让系统随机生成，客户端将无法知道服务器端的端口号，即使知道也需要每次都去获取。&lt;/li&gt;
&lt;li&gt;对于客户端来说，服务器端并不需要主动给客户端发送数据，客户端是主动的而服务器端是被动的。客户端给服务器端发送数据时，会将自己的IP地址和端口号一起发送过去，服务器端可以方便的找到客户端。&lt;/li&gt;
&lt;li&gt;同时，客户端并不是一直运行的，只需要每次系统随机分配即可。&lt;/li&gt;
&lt;li&gt;因此，服务器端需要绑定而客户端不需要绑定。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;TCP （传输控制协议）和 UDP （用户数据报协议）的区别&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;连接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;TCP 是&lt;strong&gt;面向连接&lt;/strong&gt;的，传输数据前先要建立连接。有拥塞控制，流量控制等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UDP 是无连接的。没有拥塞控制，流量控制等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据类型（传输方式）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP是字节流。流式传输，没有边界&lt;/li&gt;
&lt;li&gt;UDP是报文流。一个包一个包传输，可能丢失或乱序。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;服务对象&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP只支持一对一&lt;/li&gt;
&lt;li&gt;UDP支持一对一，一对多，多对一和多对多。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;可靠性&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP 是可靠交付数据的，数据可以无差错、不丢失、不重复、按需到达。&lt;/li&gt;
&lt;li&gt;UDP 是尽最大努力交付，不保证可靠交付数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;报文首部开销&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP 首部长度较长，会有一定的开销，首部在没有使用「选项」字段时是 20 个字节，如果使用了「选项」字段则会变长的。&lt;/li&gt;
&lt;li&gt;UDP 首部只有 8 个字节，并且是固定不变的，开销较小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;TCP保证可靠性&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;数据分段：在发送端对数据进行分段，在接收端重组，由TCP确认分段的大小并控制分段和重组&lt;/li&gt;
&lt;li&gt;确认应答（ACK）：接收端-&amp;gt;发送端（最后一个字节的序号+1）&lt;/li&gt;
&lt;li&gt;超时重传：发送端：维护一个超时定时器，如果在定时器超时之后没有收到想要的确认，重发分段&lt;/li&gt;
&lt;li&gt;滑动窗口：接收端一次接收发送端最大能发送数据的大小，保证处理能力不一样的两端不会数据（缓冲区）溢出&lt;/li&gt;
&lt;li&gt;失序处理：接收端将数据整理，把数据以正确的顺序交给应用层。&lt;/li&gt;
&lt;li&gt;重复处理：包因为延迟（超时时长定义有问题），导致接收端收到了重复的包，要丢弃重复的数据。&lt;/li&gt;
&lt;li&gt;数据校验：首部和尾部序号的校验，如果不对，会导致超时重传。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;MSS&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MSS是TCP的最大报文长度&lt;/li&gt;
&lt;li&gt;MSS不包括TCP头。也就是纯数据
&lt;ul&gt;
&lt;li&gt;举个例子。MTU是1500，标准情况下，MTU = IP头（20） + TCP头（20） + TCP数据（1460）&lt;/li&gt;
&lt;li&gt;这里的MSS就是1460&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MSS主要作用是为了避免TCP报文在IP层分片&lt;/strong&gt;。包括和滑动窗口配合&lt;strong&gt;与对方协商&lt;/strong&gt;自己可以接受多少数据。在TCP的分段中，每一个分段包都有自己的TCP头部，也方便重组。&lt;/li&gt;
&lt;li&gt;如果已经在TCP分段好的数据仍然大于MTU则还需要IP分片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MSS在三次握手阶段和每次发送的时候都会传递给对方。然后双方按照二者最小的那一个值做为通信的MSS值。这就叫MSS协商。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;MSS大于了协商值就会在TCP层分段&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MTU&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先我们应该还记得，IP是网络层，TCP是传输层。TCP把数据打包好给IP的网络层，IP网络层再次打包好放到数据链路层。&lt;strong&gt;这个MTU（最大传输单元）就是数据链路层的&lt;/strong&gt;。它规定了一个&lt;strong&gt;帧&lt;/strong&gt;有多大。&lt;/li&gt;
&lt;li&gt;一般来说它的大小是1500&lt;/li&gt;
&lt;li&gt;MTU = IP头+（TCP/UDP头+TCP/UDP数据）&lt;/li&gt;
&lt;li&gt;IP头大小没有特殊情况是20字节&lt;/li&gt;
&lt;li&gt;MTU大于1500了就要在IP层分片。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;IP层叫分片，TCP叫分段。&lt;/h3&gt;
&lt;h3&gt;不传递MSS会怎么样&lt;/h3&gt;
&lt;p&gt;没有接收到对方的MSS，本测按照默认值发送。(536字节)&lt;/p&gt;
&lt;p&gt;因为IP层的最小重组缓冲区是576，那么576-20（tcp头）-20（ip头）就是536&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/v2-8fc147d1e13653975c3ddd6101baee7d_r.jpg&quot; alt=&quot;v2-8fc147d1e13653975c3ddd6101baee7d_r&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220804211738.png&quot; alt=&quot;QQ截图20220804211738&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;为什么既然 IP 层会分片，为什么 TCP 层还需要 MSS 呢？&lt;/h2&gt;
&lt;p&gt;首先，我们上次提到过。TCP报文在TCP层分段的时候，每一段都会带着自己的TCP头（因为是给数据分段，MSS也是数据大小不包括TCP头，所以每一段数据切割好之后都会被加上TCP头做为一个独立报文。如果IP分片的话IP是不会给报文接TCP头的。只会给自己加上IP头。&lt;/p&gt;
&lt;p&gt;而且 IP层没有超时重传机制。需要依靠TCP。&lt;/p&gt;
&lt;p&gt;假如现在丢包了。因为TCP没分段，是交给了IP分片。所以整个数据只有一个TCP头（一个TCP序列号）。一旦丢包需要把整个一大块传给了IP的TCP报文全部重传（这里虽然可能由于数据过大，IP给数据分为了多片，但是这一堆分片的数据依旧是属于一个TCP报文的因为只占一个TCP序列号。举个例子就是说假如有6000字节，TCP没分段，给了IP，IP分成了4*1500。如果其中的一个1500丢了，整个6000全部重传）。&lt;/p&gt;
&lt;p&gt;如果TCP也分段了。因为避免了IP重新分片。所以IP拿到了这个报文直接就发出去了，而且这每一个小报文段都带有TCP的序列号。如果丢包了只需要传递丢的这一小块数据就可以了。因为每一小段都是完整报文段，有自己的序列号。&lt;/p&gt;
&lt;h3&gt;所以说TCP报文头没有IP报文头的（禁止分片/更多分片）的这个标志。因为IP分片的话数据是瞎瘠薄切。里面可能会有不完整的TCP报文段。可能切在中间了。所以到了之后需要合起来。TCP的数据不会被瞎瘠薄切，每一段都是完整的报文。所以他用序列号就可以了。（推测）&lt;/h3&gt;
&lt;h3&gt;MSS一定能避免IP层分片吗？ -- 不一定&lt;/h3&gt;
&lt;p&gt;不一定，因为不一定每一段路由的MTU都是1500，有可能小于1500.这时候可能需要再次IP分片。&lt;/p&gt;
&lt;h2&gt;UDP不分段，所以只能依靠IP层分片&lt;/h2&gt;
&lt;h1&gt;TCP-IP协议栈&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220804192446.png&quot; alt=&quot;QQ截图20220804192446&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;IP是网络层协议&lt;/h2&gt;
&lt;p&gt;IP报文头一般是20字节。除了可变部分以外。&lt;/p&gt;
&lt;h3&gt;IP分段 注意传输层TCP里面叫分段，网络层IP层叫分片。&lt;/h3&gt;
&lt;p&gt;网络层发现报文（IP头+（TCP/UDP头+TCP/UDP数据））大小大于1500，则需要分片。&lt;/p&gt;
&lt;h1&gt;TCP 超时重传&lt;/h1&gt;
&lt;p&gt;指的是发送方在超过一定时间之内没有收到对方的&lt;code&gt;ACK&lt;/code&gt;应答包的时候，会重发该数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有两种情况会触发超时重传&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据包丢失&lt;/li&gt;
&lt;li&gt;确认应答包丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220804223402.png&quot; alt=&quot;QQ截图20220804223402&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;RTT&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;数据发送时刻到接收到确认的时刻的差值&lt;/strong&gt;就是RTT&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220804223448.png&quot; alt=&quot;QQ截图20220804223448&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当超时时间 &lt;strong&gt;RTO 较大&lt;/strong&gt;时，重发就慢，丢了老半天才重发，没有效率，性能差；&lt;/li&gt;
&lt;li&gt;当超时时间 &lt;strong&gt;RTO 较小&lt;/strong&gt;时，会导致可能并没有丢就重发，于是重发的就快，会增加网络拥塞，导致更多的超时，更多的超时导致更多的重发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以&lt;strong&gt;超时重传时间 RTO 的值应该略大于报文往返 RTT 的值&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;快重传机制（Fast transmit属于拥塞控制。注意不是快恢复。）注意快重传是数据驱动的，超时重传是时间驱动的！一个是看应答码，一个是算时间。&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;TCP应答确认一定是有序的。即发送1234，如果2丢了则就算收到了34那么应答码&lt;code&gt;ack&lt;/code&gt;也依旧是2，当丢包发生的时候。&lt;code&gt;ack&lt;/code&gt;确认号就是接收方所期待的序号最小的没拿到的数据包。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805031036.png&quot; alt=&quot;QQ截图20220805031036&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在上图，发送方发出了 1，2，3，4，5 份数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一份 Seq1 先送到了，于是就 Ack 回 2；&lt;/li&gt;
&lt;li&gt;结果 Seq2 因为某些原因没收到，Seq3 到达了，于是还是 Ack 回 2；&lt;/li&gt;
&lt;li&gt;后面的 Seq4 和 Seq5 都到了，但还是 Ack 回 2，因为 Seq2 还是没有收到；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送端收到了三个 Ack = 2 的确认，知道了 Seq2 还没有收到，就会在定时器过期之前，重传丢失的 Seq2。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;最后，收到了 Seq2，此时因为 Seq3，Seq4，Seq5 都收到了，于是 Ack 回 6 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以，快速重传的工作方式是当收到三个相同的 ACK 报文时，会在定时器过期之前，重传丢失的报文段。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;快速重传机制只解决了一个问题，就是超时时间的问题，但是它依然面临着另外一个问题。就是&lt;strong&gt;重传的时候，是重传一个，还是重传所有的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举个例子，假设发送方发了 6 个数据，编号的顺序是 Seq1 ~ Seq6 ，但是 Seq2、Seq3 都丢失了，那么接收方在收到 Seq4、Seq5、Seq6 时，都是回复 ACK2 给发送方，但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的， 那是选择重传 Seq2 一个报文，还是重传 Seq2 之后已发送的所有报文呢（Seq2、Seq3、 Seq4、Seq5、 Seq6） 呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果只选择重传 Seq2 一个报文，那么重传的效率很低。因为假如 Seq3 报文也丢失了，还得在后续收到三个重复的 ACK3 才能触发重传。&lt;/li&gt;
&lt;li&gt;如果选择重传 Seq2 之后已发送的所有报文，虽然能同时重传已丢失的 Seq2 和 Seq3 报文，但是 Seq4、Seq5、Seq6 的报文是已经被接收过了，对于重传 Seq4 ～Seq6 折部分数据相当于做了一次无用功，浪费资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到，不管是重传一个报文，还是重传已发送的报文，都存在问题。&lt;/p&gt;
&lt;p&gt;为了解决不知道该重传哪些 TCP 报文，于是就有 &lt;code&gt;SACK&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h3&gt;SACK 方法（&lt;strong&gt;选择性确认&lt;/strong&gt;）&lt;/h3&gt;
&lt;p&gt;这种方式需要在 TCP 头部「选项」字段里加一个 &lt;code&gt;SACK&lt;/code&gt; 的东西，它&lt;strong&gt;可以将已收到的数据的信息发送给「发送方」&lt;/strong&gt;，这样发送方就可以知道哪些数据收到了，哪些数据没收到，知道了这些信息，就可以&lt;strong&gt;只重传丢失的数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说白了，就是一个数据段告诉对方我收到的是什么，如果触发了快重传则发送方看一眼这个SACK数据段缺哪块，就发送哪块。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如下图，发送方收到了三次同样的 ACK 确认报文，于是就会触发快速重发机制，通过 &lt;code&gt;SACK&lt;/code&gt; 信息发现只有 &lt;code&gt;200~299&lt;/code&gt; 这段数据丢失，则重发时，就只选择了这个 TCP 段进行重复。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805031827.png&quot; alt=&quot;QQ截图20220805031827&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Duplicate SACK (D-SACK)重复选择性确认&lt;/h3&gt;
&lt;p&gt;和SACK的主要区别是&lt;strong&gt;使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用场景1：丢包&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805033509.png&quot; alt=&quot;QQ截图20220805033509&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「接收方」发给「发送方」的两个 ACK 确认应答都丢失了，所以发送方超时后，重传第一个数据包（3000 ~ 3499）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;于是「接收方」发现数据是重复收到的，于是回了一个 SACK = 3000~3500&lt;/strong&gt;，告诉「发送方」 3000~3500 的数据早已被接收了，因为 ACK 都到了 4000 了，已经意味着 4000 之前的所有数据都已收到，所以这个 SACK 就代表着 &lt;code&gt;D-SACK&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这样「发送方」就知道了，数据没有丢，是「接收方」的 ACK 确认报文丢了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用场景2：延迟&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805033922.png&quot; alt=&quot;QQ截图20220805033922&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据包（1000~1499） 被网络延迟了，导致「发送方」没有收到 Ack 1500 的确认报文。&lt;/li&gt;
&lt;li&gt;而后面报文到达的三个相同的 ACK 确认报文，就触发了快速重传机制，但是在重传后，被延迟的数据包（1000~1499）又到了「接收方」；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所以「接收方」回了一个 SACK=1000~1500，因为 ACK 已经到了 3000，所以这个 SACK 是 D-SACK，表示收到了重复的包。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;这样发送方就知道快速重传触发的原因不是发出去的包丢了，也不是因为回应的 ACK 包丢了，而是因为网络延迟了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;总结：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;可以让「发送方」知道，是发出去的包丢了，还是接收方回应的 ACK 包丢了;&lt;/li&gt;
&lt;li&gt;可以知道是不是「发送方」的数据包被网络延迟了;&lt;/li&gt;
&lt;li&gt;可以知道网络中是不是把「发送方」的数据包给复制了;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;TCP滑动窗口&lt;/h1&gt;
&lt;p&gt;如果每一次发送数据都要进行一次ACK未免效率有点低。所以有了滑动窗口的概念。&lt;/p&gt;
&lt;p&gt;窗口大小就是指&lt;strong&gt;无需等待确认应答，而可以继续发送数据的最大值&lt;/strong&gt;。这个值在报文头会写。就是&lt;code&gt;window&lt;/code&gt;字段。这玩意和MSS没啥关系。&lt;strong&gt;因为滑动窗口是操作系统的缓存区大小&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设窗口大小为 &lt;code&gt;3&lt;/code&gt; 个 TCP 段，那么发送方就可以「连续发送」 &lt;code&gt;3&lt;/code&gt; 个 TCP 段，并且中途若有 ACK 丢失，可以通过「下一个确认应答进行确认」。如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805034441-165968923920913.png&quot; alt=&quot;QQ截图20220805034441&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通常窗口的大小是由接收方的窗口大小来决定的。但是也会针对网络拥塞情况进行适当缩小、&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;发送方滑动窗口分为四个部分 SWND&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805034735.png&quot; alt=&quot;QQ截图20220805034735&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;#1 是已发送并收到 ACK确认的数据：1~31 字节（这部分可以删除）&lt;/li&gt;
&lt;li&gt;#2 是已发送但未收到 ACK确认的数据：32~45 字节&lt;/li&gt;
&lt;li&gt;#3 是未发送但总大小在接收方处理范围内（接收方还有空间）：46~51字节&lt;/li&gt;
&lt;li&gt;#4 是未发送但总大小超过接收方处理范围（接收方没有空间）：52字节以后&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在下图，当发送方把数据「全部」都一下发送出去后，可用窗口的大小就为 0 了，表明可用窗口耗尽，在没收到 ACK 确认之前是无法继续发送数据了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805034928.png&quot; alt=&quot;QQ截图20220805034928&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在下图，当收到之前发送的数据 &lt;code&gt;32~36&lt;/code&gt; 字节的 ACK 确认应答后，如果发送窗口的大小没有变化，则&lt;strong&gt;滑动窗口往右边移动 5 个字节，因为有 5 个字节的数据被应答确认&lt;/strong&gt;，接下来 &lt;code&gt;52~56&lt;/code&gt; 字节又变成了可用窗口，那么后续也就可以发送 &lt;code&gt;52~56&lt;/code&gt; 这 5 个字节的数据了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805035006.png&quot; alt=&quot;QQ截图20220805035006&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;所以滑动窗口就是确认多少，滑动多少。&lt;/h4&gt;
&lt;h4&gt;接收方的窗口分为三个部分 RWND&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805035154.png&quot; alt=&quot;QQ截图20220805035154&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;#1 + #2 是已成功接收并确认的数据（等待应用进程读取）；&lt;/li&gt;
&lt;li&gt;#3 是未收到数据但可以接收的数据；&lt;/li&gt;
&lt;li&gt;#4 未收到数据并不可以接收的数据；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;接收方未按序收到的报文在什么位置？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805054344.png&quot; alt=&quot;QQ截图20220805054344&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最左边报文一定是尚未收到的报文&lt;/li&gt;
&lt;li&gt;如果31没到，32，33先到，就先把32，33缓存在接收缓冲区中，并且会发送3个对31的确认报文（快重传），当收到了31后，那么窗口就继续滑动到34的位置.&lt;/li&gt;
&lt;li&gt;注意ack包的确认号一定是下一个期望收到的数据的起始号。所以这里传回的ack不会是34而是31&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;TCP流量控制&lt;/h1&gt;
&lt;p&gt;我们刚才说了滑动窗口的主要作用是不用每次都等到ACK之后才能再次发送。但是滑动窗口的大小也不是固定的。因为可能我现在比较忙，处理不过来，如果我来不及确认，但是你一直在那发，就会出现超时重传。&lt;/p&gt;
&lt;p&gt;举个例子，你让我拆快递，没有滑动窗口就是我拆完一个，告诉你我拆完了，你再给我一个。有滑动窗口就是你给我了一张桌子，只要桌子没满你就一直在那放。但是桌子是固定大小的，可能我出去上了个厕所，虽然桌子没满但是我没在，你一直在这放导致很多快递来不及拆，你以为快递被人偷了就重新买了又放在了桌子上，很消耗资源。所以我让这个桌子是变长的。&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量，也就是避免「发送方」的数据填满「接收方」的缓存。这就是所谓的流量控制。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;滑动窗口的总大小一般情况下是固定的，但是可用窗口是变化的。所以接收方的大小就是 总大小 - 已接收的但是未被处理的数据的大小。一旦数据已被处理，则可用窗口会扩大（滑动）。&lt;/li&gt;
&lt;li&gt;发送方的大小就是总大小 - 已发送但未确认的数据的大小。一旦收到ACK确认则可用窗口会扩大（滑动）&lt;/li&gt;
&lt;li&gt;每一次接收端发送ACK的时候都会把自己的接受窗口大小&lt;code&gt;rwnd&lt;/code&gt;告知对方，这样的话对方就可以动态调整自己的发送窗口大小。&lt;/li&gt;
&lt;li&gt;一旦接收端发送的&lt;code&gt;rwnd&lt;/code&gt;为0，就发生了&lt;strong&gt;窗口关闭&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;窗口关闭&lt;/h3&gt;
&lt;p&gt;当接收方发送的ACK报文附带的&lt;code&gt;rwnd&lt;/code&gt;为0的时候，就发生窗口关闭。&lt;/p&gt;
&lt;p&gt;当接收方处理完了数据，&lt;code&gt;rwnd&lt;/code&gt;又有空间的时候，会再次发送一个ACK报文，告诉发送方自己的窗口大小，（可以发送数据）。&lt;strong&gt;但是一旦这个ACK报文丢了，就会出现死锁。因为发送方等着这个ACK才能发，接受方已经发了ACK迟迟没拿到数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这时候就有一个东西叫做持续计时器。&lt;strong&gt;只要 TCP 连接一方收到对方的零窗口通知，就启动持续计时器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果持续计时器超时，就会发送&lt;strong&gt;窗口探测 ( Window probe ) 报文&lt;/strong&gt;（这个报文携带1字节数据），而对方在确认这个探测报文时，给出自己现在的接收窗口大小。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果接收窗口仍然为 0，那么收到这个报文的一方就会重新启动持续计时器；&lt;/li&gt;
&lt;li&gt;如果接收窗口不是 0，那么死锁的局面就可以被打破了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;窗口探测的次数一般为 3 次，每次大约 30-60 秒（不同的实现可能会不一样）。如果 3 次过后接收窗口还是 0 的话，有的 TCP 实现就会发 &lt;code&gt;RST&lt;/code&gt; 报文来中断连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;就算接收方的&lt;code&gt;rwnd&lt;/code&gt;为0，接受方也必须接受零窗口探测报文段（ACK），确认报文段和携带紧急数据的报文段。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805054019.png&quot; alt=&quot;QQ截图20220805054019&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;窗口过小导致数据传输效率问题&lt;/h3&gt;
&lt;p&gt;假如说应用程序读取数据很少，则每一次的窗口的大小会越来越小。这样假如告诉你我窗口是1字节你再给我发1字节这样非常浪费资源。由此，我们可以有两个选择。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个是接收方窗口小于某个值的时候，就当做0处理，发送rwnd为0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当「窗口大小」小于 min( MSS，缓存空间/2 ) ，也就是小于 MSS 与 1/2 缓存大小中的最小值时，就会向发送方通告窗口为 &lt;code&gt;0&lt;/code&gt;，也就阻止了发送方再发数据过来。&lt;/li&gt;
&lt;li&gt;等到接收方处理了一些数据后，窗口大小 &amp;gt;= MSS，或者接收方缓存空间有一半可以使用，就可以把窗口打开让发送方发送数据过来。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另外一个是让发送方避免发送过小的数据包。（&lt;strong&gt;Nagle&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Nagle算法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果包长度达到&lt;code&gt;MSS&lt;/code&gt;（或含有&lt;code&gt;Fin&lt;/code&gt;包），立刻发送，否则&lt;strong&gt;等待&lt;/strong&gt;下一个包到来；如果下一包到来后两个包的总长度超过&lt;code&gt;MSS&lt;/code&gt;的话，就会进行拆分发送；&lt;/li&gt;
&lt;li&gt;等待超时（一般为&lt;code&gt;200ms&lt;/code&gt;），第一个包没到&lt;code&gt;MSS&lt;/code&gt;长度，但是又迟迟等不到第二个包的到来，则立即发送。&lt;/li&gt;
&lt;li&gt;一般telnet和ssh这种小包交互的时候会关闭这个算法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;TCP拥塞控制 拥塞窗口CWND(Congestion Window)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;在网络出现拥堵时，如果继续发送大量数据包，可能会导致数据包时延、丢失等，这时 TCP 就会重传数据，但是一重传就会导致网络的负担更重，于是会导致更大的延迟以及更多的丢包，这个情况就会进入恶性循环被不断地放大....&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，TCP 不能忽略网络上发生的事，它被设计成一个无私的协议，当网络发送拥塞时，TCP 会自我牺牲，降低发送的数据量。&lt;/p&gt;
&lt;p&gt;于是，就有了&lt;strong&gt;拥塞控制&lt;/strong&gt;，控制的目的就是&lt;strong&gt;避免「发送方」的数据填满整个网络。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拥塞窗口 cwnd&lt;/strong&gt;是&lt;strong&gt;发送方&lt;/strong&gt;维护的一个的状态变量，它会根据&lt;strong&gt;网络的拥塞程度动态变化的&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;cwnd = n意思是可以发送n个报文段（n个MSS）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在前面提到过发送窗口 &lt;code&gt;swnd&lt;/code&gt; 和接收窗口 &lt;code&gt;rwnd&lt;/code&gt; 是约等于的关系，那么由于加入了拥塞窗口的概念后，此时发送窗口的值是&lt;code&gt;swnd = min(cwnd, rwnd)&lt;/code&gt;，也就是拥塞窗口和接收窗口中的最小值。&lt;/p&gt;
&lt;p&gt;拥塞窗口 &lt;code&gt;cwnd&lt;/code&gt; 变化的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只要网络中没有出现拥塞，&lt;code&gt;cwnd&lt;/code&gt; 就会增大；&lt;/li&gt;
&lt;li&gt;但网络中出现了拥塞，&lt;code&gt;cwnd&lt;/code&gt; 就减少；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如何判断网络是否拥塞了呢？&lt;/h2&gt;
&lt;p&gt;很简单，超时重传了就会认为网络出现拥塞&lt;/p&gt;
&lt;h2&gt;有哪些拥塞控制算法？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;慢启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拥塞避免&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拥塞发生&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时重传&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快重传&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;快恢复 &lt;strong&gt;（注意不是快重传）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;慢启动（指数增长）&lt;/h3&gt;
&lt;p&gt;TCP 在刚建立连接完成后，首先是有个慢启动的过程，这个慢启动的意思就是一点一点的提高发送数据包的数量，如果一上来就发大量的数据，这不是给网络添堵吗？（TCP链接完成后无法确认网络状况是否良好，一次发了一大堆万一丢了又得重传很脑瘫。所以一开始一点一点地尝试发）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;慢启动算法规则：&lt;strong&gt;当发送方&lt;/strong&gt;每收到一个 ACK&lt;/strong&gt;，拥塞窗口 &lt;code&gt;cwnd &lt;/code&gt;的大小就会加 1。注意是每次收到一个ACK。所以是&lt;strong&gt;指数增长&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;p&gt;这里假定拥塞窗口 &lt;code&gt;cwnd&lt;/code&gt; 和发送窗口 &lt;code&gt;swnd&lt;/code&gt; 相等，下面举个栗子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接建立完成后，一开始初始化 &lt;code&gt;cwnd = 1&lt;/code&gt;，表示可以传一个 &lt;code&gt;MSS&lt;/code&gt; 大小的数据。&lt;/li&gt;
&lt;li&gt;当收到一个 ACK 确认应答后，cwnd 增加 1，于是一次能够发送 2 个&lt;/li&gt;
&lt;li&gt;当收到 &lt;strong&gt;2 个的 ACK&lt;/strong&gt; 确认应答后， cwnd &lt;strong&gt;增加 2&lt;/strong&gt;，于是就可以比之前多发2 个，&lt;strong&gt;所以这一次能够发送 4 个&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当这&lt;strong&gt;4 个的 ACK &lt;strong&gt;确认到来的时候，&lt;strong&gt;每个确认 cwnd 增加 1&lt;/strong&gt;， &lt;strong&gt;4 个确认 cwnd 增加 4&lt;/strong&gt;，于是就可以比之前多发 4 个，所以这一次能够&lt;/strong&gt;发送 8 个&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220809060754.png&quot; alt=&quot;QQ截图20220809060754&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;cwnd直到涨到慢启动阈值（Slow Start thresh）的时候会发生状态改变&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;cwnd&lt;/code&gt; &amp;lt; &lt;code&gt;ssthresh&lt;/code&gt; 时，使用慢启动算法。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;cwnd&lt;/code&gt; &amp;gt;= &lt;code&gt;ssthresh&lt;/code&gt; 时，就会使用「&lt;strong&gt;拥塞避免算法&lt;/strong&gt;」。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;拥塞避免算法（“加法增大”，线性增长）&lt;/h3&gt;
&lt;p&gt;顾名思义，结合前面我们提到过当拥塞窗口 &lt;code&gt;cwnd&lt;/code&gt; 「超过」慢启动门限 &lt;code&gt;ssthresh&lt;/code&gt; 就会进入拥塞避免算法，我们可以理解为：我们为了效率，肯定希望一次尽可能发送更多的数据包。但是为了不干扰他人，又不能像之前一样指数增长，那我们采用折中的方法，&lt;strong&gt;当收到一个 ACK 时，cwnd 增加 1。也就是线性增长&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是，就这么一直增长，迟早要进入拥堵阶段，也就是发生了丢包。这时候就需要重传了。当触发了重传机制的时候，也就是进入了下一个阶段：「拥塞发生」&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;拥塞发生&lt;/h3&gt;
&lt;p&gt;当网络出现拥塞，也就是会发生数据包重传，重传机制主要有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时重传&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快速重传&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;发生超时重传的拥塞发生算法：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ssthresh&lt;/code&gt; 设为 拥塞窗口的一半（&lt;code&gt;cwnd/2&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cwnd&lt;/code&gt; 恢复为&lt;code&gt; cwnd&lt;/code&gt; 初始化值，这里假定 cwnd 初始化值 1&lt;/li&gt;
&lt;li&gt;然后进入到&lt;strong&gt;慢启动&lt;/strong&gt;阶段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**因为慢启动会极大地减少数据流，所以会发生严重的网络卡顿。**所以会有快重传的拥塞发生算法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220809062600.png&quot; alt=&quot;QQ截图20220809062600&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;发生快速重传的拥塞发生算法：&lt;/h4&gt;
&lt;p&gt;前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候，ACK会发送丢了的那个包的序列号。如果接收端发现收到了三次前一个包的 ACK，于是发送端就会快速地重传，&lt;strong&gt;不必等待超时再重传&lt;/strong&gt;。TCP 认为这种情况不严重，因为大部分没丢，只丢了一小部分，则 &lt;code&gt;ssthresh&lt;/code&gt; 和 &lt;code&gt;cwnd&lt;/code&gt; 变化如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cwnd = cwnd/2&lt;/code&gt; ，也就是设置为原来的一半;（注意区别，超时重传的时候cwnd会恢复为默认值）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssthresh = cwnd/2&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;进入&lt;strong&gt;快速恢复算法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;快恢复&lt;/h3&gt;
&lt;p&gt;快速重传和快速恢复算法一般同时使用，快速恢复算法是认为，你还能收到 3 个重复 ACK 说明网络也不那么糟糕，所以没有必要像 &lt;code&gt;RTO&lt;/code&gt; 超时（重传超时）那么强烈。&lt;/p&gt;
&lt;p&gt;正如前面所说，进入快速恢复之前，&lt;code&gt;cwnd&lt;/code&gt; 和 &lt;code&gt;ssthresh&lt;/code&gt; 已被更新了：这俩都是原来&lt;code&gt;cwnd&lt;/code&gt;的一半。&lt;/p&gt;
&lt;p&gt;然后进入快恢复阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拥塞窗口 &lt;code&gt;cwnd = ssthresh + 3&lt;/code&gt; （ 3 的意思是快速重传时已经确认接收到了 3 个重复的ACK数据包）；&lt;/li&gt;
&lt;li&gt;重传丢失的数据包；&lt;/li&gt;
&lt;li&gt;如果再收到重复的 ACK，那么 cwnd 增加 1；&lt;/li&gt;
&lt;li&gt;如果收到新数据的 ACK 后，把 cwnd 设置为第一步中的 ssthresh 的值，原因是该 ACK 确认了新的数据，说明从 duplicated ACK 时的数据都已收到，该恢复过程已经结束，可以回到恢复之前的状态了，也即再次进入拥塞避免状态；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220809064515.png&quot; alt=&quot;QQ截图20220809064515&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;为什么快恢复长得和理论的图不太一样？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在快速恢复的过程中，首先 ssthresh = cwnd/2，然后 &lt;code&gt;cwnd = ssthresh + 3&lt;/code&gt;，表示网络可能出现了阻塞，所以需要减小 &lt;code&gt;cwnd &lt;/code&gt;以避免，加 3 代表快速重传时已经确认接收到了 3 个重复的ACK数据包；&lt;/li&gt;
&lt;li&gt;随后继续重传丢失的数据包，如果再收到重复的 ACK，那么 cwnd 增加 1。加 1 代表每个收到的重复的 ACK 包，都已经离开了网络。&lt;strong&gt;这个过程的目的是尽快将丢失的数据包发给目标。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果收到新数据的 ACK 后，把 cwnd 设置为第一步中的 ssthresh 的值，恢复过程结束。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;首先，快速恢复是拥塞发生后慢启动的优化，其首要目的仍然是降低 cwnd 来减缓拥塞，所以必然会出现 cwnd 从大到小的改变。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其次，过程2（cwnd逐渐加1）的存在是为了尽快将丢失的数据包发给目标，从而解决拥塞的根本问题（三次相同的 ACK 导致的快速重传），所以这一过程中 cwnd 反而是逐渐增大的。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;拥塞控制和流量控制的区别&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;流量控制，的目的就是避免「发送方」的数据填满「接收方」的缓存，但是并不知道网络的中发生了什么。&lt;strong&gt;理解为数据包可到达，但是避免接收方来不及处理&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;rwnd窗口由接收方调整&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拥塞控制，的目的就是避免「发送方」的数据填满整个网络。&lt;strong&gt;理解为避免数据包可能会发生不可到达，避免耗尽网络资源&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;cwnd窗口由发送方调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;面试题相关&lt;/h1&gt;
&lt;h2&gt;没有accept能否建立TCP链接？ -- 可以&lt;/h2&gt;
&lt;p&gt;上文的全连接队列和半连接队列提到了。第一次握手会放入半连接，三次握手后放入全连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;accept的目的仅仅是从全连接队列取出一条连接而已。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;accept 底层实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;accept()&lt;/code&gt;函数，就使用来 &lt;strong&gt;从 全连接队列 中 的队首 (队头) 位置取出来一项 (每一项都是一个已经完成三次握手的TCP连接)，返回给进程。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;accept&lt;/code&gt;会以文件描述符的形式返回一个套接字。这个套接字就是我们项目里的读写文件描述符。&lt;/li&gt;
&lt;li&gt;如果已完成连接队列是空的呢？
&lt;ul&gt;
&lt;li&gt;那么&lt;code&gt;accept()&lt;/code&gt;会一致卡在这里【休眠】等待，一直到已完成队列中有一项时才会被唤醒。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;没有listen能否建立TCP链接？ -- 不可以&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;服务端如果只 bind 了 IP 地址和端口，而没有调用 listen 的话，然后客户端对服务端发起了连接建立，服务端会回 RST 报文。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;listen底层实现&lt;/h3&gt;
&lt;p&gt;维护一个半连接队列和一个全连接队列。&lt;code&gt;listen&lt;/code&gt;调用后，监听到第一次握手就放入半连接队列，三次握手完毕后放入全连接队列。&lt;/p&gt;
&lt;h2&gt;FIN 报文一定得调用关闭连接的函数，才会发送吗？ -- 不一定&lt;/h2&gt;
&lt;p&gt;如果进程退出了，不管是不是正常退出，还是异常退出（如进程崩溃），内核都会发送 FIN 报文，与对方完成四次挥手。&lt;/p&gt;
&lt;h2&gt;TCP的第三次握手能不能携带数据？ -- 可以&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805001644.png&quot; alt=&quot;QQ截图20220805001644&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图可以看到，第二次我握手的时候客户端已经切换到ESTABLISHED所以可以携带数据。&lt;/p&gt;
&lt;h2&gt;客户端发送完第三次握手后，是不是不管服务器有没有收到，直接就发送数据？ -- 是的&lt;/h2&gt;
&lt;p&gt;不然不就是四次握手了么&lt;/p&gt;
&lt;h2&gt;如果因为各种原因，服务端并未收到客户端发来的第三次握手包，那客户端后续发送的数据，服务端如何处理？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;如果第三次握手包服务器没有收到，就直接发送数据，服务器将这个携带应用数据的包当做第三次握手（前提是这一个包中携带有ACK标记）。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么三次握手必须由客户端发起&lt;/h2&gt;
&lt;p&gt;因为客户端是主动端，服务端是被动端。&lt;/p&gt;
&lt;p&gt;服务端不知道客户端的IP地址。放到项目里就是我们监听的时候，客户端发送的数据包是含有自己的ip和端口地址的。所以我们可以利用。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;为什么要进行三次握手，两次或四次行不行&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;三次握手&lt;strong&gt;能防止历史连接的建立，能减少双方不必要的资源开销，能帮助双方同步初始化序列号&lt;/strong&gt;。序列号能够保证数据包不重复、不丢弃和按序传输。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「两次握手」：无法防止历史连接的建立，会造成双方资源的浪费，也无法可靠的同步双方序列号；
&lt;ul&gt;
&lt;li&gt;详细点就是说假如有一个客户端的历史已经失效的报文段突然到达，则服务端以为是客户端想要建立链接。如果是两次握手的话，服务端再发送一次就可以建立连接了。但是客户端并没有需求，造成了浪费也有可能发送错误数据。、&lt;/li&gt;
&lt;li&gt;第二点就是SYN SYNACK ACK这三个阶段中，客户端向服务端发送SYN，则服务端收到了并且也向客户端发送了SYN，然后客户端再次ACK才能保证双方都收到了序列号。不然无法保证客户端也受到了服务端的SYN报文。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;「四次握手」：三次握手就已经理论上最少可靠连接建立，所以不需要使用更多的通信次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么要四次挥手，三次两次行不行？&lt;/h2&gt;
&lt;p&gt;四次挥手之所以为四次就是因为可以半关闭状态。也就是服务器可能还要发送一些数据给客户端。理论上四次挥手也可以换成三次。&lt;strong&gt;如果服务器在收到客户端的 &lt;code&gt;FIN&lt;/code&gt; 时没有更多数据或根本没有数据要发送，则可以将 ack 和 fin 合并为一个包。或者，因为延迟确认的特性，可以把ACK,FIN（第二次和第三次挥手)和要发的数据一起发过去，这样也是三次挥手。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;服务器收到客户端的 FIN 报文时，内核会马上回一个 ACK 应答报文，&lt;strong&gt;但是服务端应用程序可能还有数据要发送，所以并不能马上发送 FIN 报文，而是将发送 FIN 报文的控制权交给服务端应用程序&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果服务端应用程序有数据要发送的话，就发完数据后，才调用关闭连接的函数；&lt;/li&gt;
&lt;li&gt;如果服务端应用程序没有数据要发送的话，可以直接调用关闭连接的函数，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从上面过程可知，**是否要发送第三次挥手的控制权不在内核，而是在被动关闭方（上图的服务端）的应用程序，因为应用程序可能还有数据要发送，由应用程序决定什么时候调用关闭连接的函数，当调用了关闭连接的函数，内核就会发送 FIN 报文了，**所以服务端的 ACK 和 FIN 一般都会分开发送。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两次挥手是自连接。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么每次建立 TCP 连接时，初始化的序列号都要求不一样呢？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为了防止历史报文被下一个相同四元组的连接接收（主要方面）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;为了安全性，防止黑客伪造的相同序列号的 TCP 报文被对方接收&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;初始化序列号不一样是防止历史报文被相同四元组接受，那么为什么还需要TIME_WAIT&lt;/h2&gt;
&lt;p&gt;因为序列号会被复用，不是无限的。&lt;/p&gt;
&lt;h2&gt;TIME_WAIT 过多有什么危害？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;第一是占用系统资源，比如文件描述符、内存资源、CPU 资源、线程资源等；&lt;/li&gt;
&lt;li&gt;第二是占用端口资源，端口资源也是有限的，一般可以开启的端口为 &lt;code&gt;32768～61000&lt;/code&gt;，也可以通过 &lt;code&gt;net.ipv4.ip_local_port_range&lt;/code&gt;参数指定范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如果已经建立了连接，但是客户端突然出现故障了怎么办？ - TCP KEEPALIVE&lt;/h2&gt;
&lt;p&gt;TCP 有一个机制是&lt;strong&gt;保活机制KEEPALIVE&lt;/strong&gt;。这个机制的原理是这样的：&lt;/p&gt;
&lt;p&gt;定义一个时间段，在这个时间段内，如果没有任何连接相关的活动，TCP 保活机制会开始作用，每隔一个时间间隔，发送一个探测报文，该探测报文包含的数据非常少，如果连续几个探测报文都没有得到响应，则认为当前的 TCP 连接已经死亡，系统内核将错误信息通知给上层应用程序。&lt;/p&gt;
&lt;p&gt;如果开启了 TCP 保活，需要考虑以下几种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种，对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应，这样 &lt;strong&gt;TCP 保活时间会被重置&lt;/strong&gt;，等待下一个 TCP 保活时间的到来。&lt;/li&gt;
&lt;li&gt;第二种，对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后，对端是可以响应的，但由于没有该连接的有效信息，&lt;strong&gt;会产生一个 RST 报文&lt;/strong&gt;，这样很快就会发现 TCP 连接已经被重置。&lt;/li&gt;
&lt;li&gt;第三种，是对端程序崩溃，或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后，石沉大海，没有响应，连续几次，达到保活探测次数后，&lt;strong&gt;TCP 会报告该 TCP 连接已经死亡&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;我们项目里有自己的超时检测机制，这个是HTTP应用层的的不是TCP。TCP的保活和HTTP保活不一样。我们的保活机制就是定时器，没有新的读写操作（请求）就调用关闭。&lt;/h4&gt;
&lt;h2&gt;HTTP keepalive 和 TCP keepalive如何理解&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TCP的保活是没有通信的时候，这时候也没断开连接，我们看一下对端是否活着。所以这个周期比较长。&lt;/li&gt;
&lt;li&gt;HTTP的keepalive是因为在关闭的时候，每一次请求答复后浏览器也就是客户端会主动关闭这个TCP链接。而如果打开了keepalive，则不会主动关闭这个链接。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP keepalive是用来检测的&lt;/li&gt;
&lt;li&gt;HTTP keep-alive 是用来设置生效（开关）的。关闭也就是直接收到请求就断开连接。如果打开了就不主动断开连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTP不带keepalive的话谁主动关闭？ 推测&lt;/h2&gt;
&lt;p&gt;我们知道HTTP的keepalive必须两端都开启才能生效。&lt;/p&gt;
&lt;p&gt;以下是个人测试：&lt;/p&gt;
&lt;p&gt;如果服务器关闭了keepalive，浏览器开启，则是浏览器主动发起FIN。&lt;/p&gt;
&lt;p&gt;通过知乎https://zhuanlan.zhihu.com/p/224595048 得到：如果浏览器关闭了keepalive，服务器开启，则是服务器主动发起FIN。&lt;/p&gt;
&lt;p&gt;那么可以推测出，是哪方关闭了，就是开启的那方主动关闭连接。&lt;/p&gt;
&lt;p&gt;那么如果都关闭呢？不知道了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220925035850.png&quot; alt=&quot;QQ截图20220925035850&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;accept(服务端)/connect(客户端)的成功返回 发生在三次握手的哪一步？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;客户端在第二次握手后&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端在第三次握手后&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单记住就是只要收到了对方的ACK就可以了。客户端是第二次握手收到服务端的ACK，服务端是第三次收到了客户端的ACK。&lt;/p&gt;
&lt;h2&gt;很多closewait状态是什么原因&lt;/h2&gt;
&lt;p&gt;**在被动关闭连接情况下，在已经接收到FIN，回复了ACK，但是还没有发送自己的FIN的时刻，连接处于CLOSE_WAIT状态。**所以原因可能是我方正在忙着读或者写，还没有及时处理断开连接的操作。也就是我方没有及时close。应该读到错误的时候自己也调用关闭。&lt;/p&gt;
&lt;h2&gt;很多FIN_WAIT_2状态是什么原因&lt;/h2&gt;
&lt;p&gt;和上面一样。被动关闭是closewait，主动关闭对应的就是fin_wait_2&lt;/p&gt;
&lt;h2&gt;shutdown和close的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shutdown()&lt;/code&gt;函数可以选择关闭全双工连接的读通道或者写通道，如果两个通道同时关闭，则这个连接不能再继续通信。shutdown()只会关闭连接，但是不会释放占用的文件描述符。所以即使使用了SHUT_RDWR类型调用shutdown()关闭连接，也仍然要调用close()来释放连接占用的文件描述符。&lt;strong&gt;如果有多进程/多线程共享同一个 socket，shutdown 则不管引用计数，直接使得该 socket 不可用，然后发出 FIN 报文，如果有别的进程企图使用该 socket，将会受到影响。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;close()&lt;/code&gt;函数会同时关闭全双工连接的读写通道，除了关闭连接外，还会释放套接字占用的文件描述符。如果有多进程/多线程共享同一个 socket，如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1，并不会导致 socket 不可用，同时也不会发出 FIN 报文，其他进程还是可以正常读写该 socket，直到引用计数变为 0，才会发出 FIN 报文。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;粗暴关闭 优雅关闭&lt;/h2&gt;
&lt;p&gt;如果客户端是用 &lt;code&gt;close&lt;/code&gt; 函数来关闭连接，那么在 TCP 四次挥手过程中，如果收到了服务端发送的数据，由于客户端已经不再具有发送和接收数据的能力，所以客户端的内核会回 RST 报文给服务端，然后内核会释放连接，这时就不会经历完成的 TCP 四次挥手，所以我们常说，调用 close 是粗暴的关闭。当服务端收到 RST 后，内核就会释放连接，当服务端应用程序再次发起读操作或者写操作时，就能感知到连接已经被释放了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是读操作，则会返回 RST 的报错，也就是我们常见的Connection reset by peer。&lt;/li&gt;
&lt;li&gt;如果是写操作，那么程序会产生 SIGPIPE 信号，应用层代码可以捕获并处理信号，如果不处理，则默认情况下进程会终止，异常退出。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;相对的，shutdown 函数因为可以指定只关闭发送方向而不关闭读取方向，所以即使在 TCP 四次挥手过程中，如果收到了服务端发送的数据，客户端也是可以正常读取到该数据的，然后就会经历完整的 TCP 四次挥手，所以我们常说，调用 shutdown 是优雅的关闭。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;客户端调用 close 了，断开的流程是什么？&lt;/h2&gt;
&lt;p&gt;服务端的&lt;code&gt;read&lt;/code&gt;会读到EOF。这时候可以调用close了（服务端发送FIN）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;客户端主动调用关闭连接的函数，于是就会发送 FIN 报文，这个  FIN 报文代表客户端不会再发送数据了，进入 FIN_WAIT_1 状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端收到了 FIN 报文，然后马上回复一个 ACK 确认报文，此时服务端进入 CLOSE_WAIT 状态**。在收到 FIN 报文的时候，TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中，服务端应用程序可以通过 read 调用来感知这个 FIN 包**，这个 EOF 会被&lt;strong&gt;放在已排队等候的其他已接收的数据之后（末尾）&lt;/strong&gt;，所以必须要继续读取read 接收缓冲区已接收的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调用 socket 的&lt;code&gt;close&lt;/code&gt;方法后，缓冲区中未发送完的数据不会丢。因为内核会把FIN包用EOF替换后插入到缓冲区的末尾。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接着，当服务端在 read 数据的时候，最后自然就会读到 EOF，接着 &lt;strong&gt;read() 就会返回 0，这时服务端应用程序如果有数据要发送的话，就发完数据后才调用关闭连接的函数，如果服务端应用程序没有数据要发送的话，可以直接调用关闭连接的函数&lt;/strong&gt;，这时服务端就会发一个 FIN 包，这个  FIN 报文代表服务端不会再发送数据了，之后处于 LAST_ACK 状态；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端接收到服务端的 FIN 包，并发送 ACK 确认包给服务端，此时客户端将进入 TIME_WAIT 状态；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端收到 ACK 确认包后，就进入了最后的 CLOSE 状态；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端经过 2MSL 时间之后，也进入 CLOSE 状态；&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET1.assets/QQ%E6%88%AA%E5%9B%BE20220805021651.png&quot; alt=&quot;QQ截图20220805021651&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;整理一下&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;浏览器关闭了页面，浏览器会调用&lt;code&gt;close&lt;/code&gt;。发送&lt;code&gt;FIN&lt;/code&gt;包&lt;/li&gt;
&lt;li&gt;服务器接收到了&lt;code&gt;FIN&lt;/code&gt;包，TCP协议栈会把这个&lt;code&gt;FIN&lt;/code&gt;包换成EOF结束符然后放到对应客户端读写描述符的接收缓冲区中。
&lt;ul&gt;
&lt;li&gt;我们知道每一个socket都是文件描述符。&lt;/li&gt;
&lt;li&gt;我们知道每个socket都有一个自己的缓冲区。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过读取（可能是epoll，可能是read，可能是select等等）我们能读取到这个&lt;code&gt;EOF&lt;/code&gt;，我们就知道了客户端想要关闭，不会再发送数据了。我们就可以这边准备进行关闭。&lt;/li&gt;
&lt;li&gt;我们读取到了&lt;code&gt;EOF&lt;/code&gt;可以调用&lt;code&gt;close&lt;/code&gt;进行关闭了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;服务端关闭了，客户端继续写会强制退出&lt;/h2&gt;
&lt;h2&gt;客户端关闭了写端，服务端继续写没问题，关闭了读写则服务器继续写也会触发异常&lt;/h2&gt;
&lt;h1&gt;客户端拔掉了网线会怎么样？&lt;/h1&gt;
&lt;h3&gt;首先，拔掉网线这个动作并不会影响 TCP 连接的状态。&lt;/h3&gt;
&lt;p&gt;TCP 连接在 Linux 内核中是一个名为 &lt;code&gt;struct socket&lt;/code&gt; 的结构体，该结构体的内容包含 TCP 连接的状态等信息。&lt;strong&gt;当拔掉网线的时候，操作系统并不会变更该结构体的任何内容，所以 TCP 连接的状态也不会发生改变。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;假设拔掉网线后有数据传输&lt;/h3&gt;
&lt;p&gt;在客户端拔掉网线后，服务端向客户端发送的数据报文会得不到任何的响应，在等待一定时长后，服务端就会触发&lt;strong&gt;超时重传&lt;/strong&gt;机制，重传未得到响应的数据报文。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果在服务端重传报文的过程中，客户端刚好把网线插回去了&lt;/strong&gt;，由于拔掉网线并不会改变客户端的 TCP 连接状态，并且还是处于 ESTABLISHED 状态，所以这时客户端是可以正常接收服务端发来的数据报文的，然后客户端就会回 ACK 响应报文。此时，客户端和服务端的 TCP 连接依然存在的，就&lt;strong&gt;感觉什么事情都没有发生。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是，&lt;strong&gt;如果如果在服务端重传报文的过程中，客户端一直没有将网线插回去&lt;/strong&gt;，服务端超时重传报文的次数达到一定阈值后，内核就会判定出该 TCP 有问题，然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了，于是&lt;strong&gt;服务端的 TCP 连接就会断开&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;而等客户端插回网线后，&lt;strong&gt;如果客户端向服务端发送了数据&lt;/strong&gt;，由于&lt;strong&gt;服务端已经没有与客户端相同四元祖的 TCP 连接&lt;/strong&gt;了，因此&lt;strong&gt;服务端内核就会回复 RST 报文，客户端收到后就会释放该 TCP 连接。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;此时，客户端和服务端的 TCP 连接都已经断开了。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;假设拔掉网线后，没有数据传输&lt;/h3&gt;
&lt;p&gt;针对拔掉网线后，没有数据传输的场景，还得看是否开启了 TCP keepalive 机制 （TCP 保活机制）。&lt;/p&gt;
&lt;p&gt;如果&lt;strong&gt;没有开启&lt;/strong&gt; TCP keepalive 机制，在客户端拔掉网线后，并且双方都没有进行数据传输，那么客户端和服务端的 TCP 连接将会一直保持存在。（因为我压根不知道你断开了没有。）&lt;/p&gt;
&lt;p&gt;而如果&lt;strong&gt;开启&lt;/strong&gt;了 TCP keepalive 机制，在客户端拔掉网线后，即使双方都没有进行数据传输，在持续一段时间后，TCP 就会发送探测报文：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;strong&gt;对端是正常工作&lt;/strong&gt;的。当 TCP 保活的探测报文发送给对端, 对端会正常响应，这样 &lt;strong&gt;TCP 保活时间会被重置&lt;/strong&gt;，等待下一个 TCP 保活时间的到来。&lt;/li&gt;
&lt;li&gt;如果&lt;strong&gt;对端主机崩溃，或对端由于其他原因导致报文不可达&lt;/strong&gt;。当 TCP 保活的探测报文发送给对端后，石沉大海，没有响应，连续几次，达到保活探测次数后，&lt;strong&gt;TCP 会报告该 TCP 连接已经死亡&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以，TCP 保活机制可以在双方没有数据交互的情况，通过探测报文，来确定对方的 TCP 连接是否存活。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;输入网址到显示网页的过程&lt;/h2&gt;
&lt;p&gt;首先浏览器将URL通过DNS协议（UDP连接）解析为IP地址&lt;/p&gt;
&lt;p&gt;交给TCP协议，建立链接&lt;/p&gt;
&lt;p&gt;IP层将TCP报文封装。发送&lt;/p&gt;
&lt;p&gt;到达对端后用ARP/RARP进行寻址。&lt;/p&gt;
&lt;p&gt;然后找到目标客户端。&lt;/p&gt;
&lt;p&gt;成功建立链接。&lt;/p&gt;
&lt;p&gt;发出HTTP请求，&lt;/p&gt;
&lt;p&gt;用到的协议：HTTP/S，DNS(UDP), TCP, IP, MAC, ARP, RARP&lt;/p&gt;
&lt;h2&gt;粘包怎么办&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;其实严谨一点不能叫解决粘包。应该叫实现功能。你硬要用字节流协议去做数据报协议的功能，你肯定要自己去解决怎么读取消息的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们提到过，调用了&lt;code&gt;send&lt;/code&gt;仅仅是把内容拷贝到内核缓冲区中而已，具体什么时候发送是TCP决定的。TCP是字节流，&lt;strong&gt;我们不能认为一个用户消息对应一个TCP报文，即 TCP不保证每一个报文是一个完整的数据报&lt;/strong&gt;。所以可能会发生粘包。尤其是有些小包数据会有Nagle算法优化导致多包一起发出去。或者是一个大报文被拆成了几个小报文。这样就可能我分了五次发送 1 2 3 4 5然后变成一个12345发出去。但是应用程序不知道怎么拆开。不知道是1 2还是1 23 45 还是...&lt;/p&gt;
&lt;p&gt;所以总结原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP面向字节流，一个用户消息不一定是一个TCP报文。一个报文可能有多个消息或者是多个报文对应一个消息。所以是因为：
&lt;ul&gt;
&lt;li&gt;多个小消息合并为大包发送（发送方粘包）&lt;/li&gt;
&lt;li&gt;大包拆分为小包（发送方粘包）&lt;/li&gt;
&lt;li&gt;接收端不及时接受导致多个包在一起。（接收方粘包）&lt;/li&gt;
&lt;li&gt;说白了就是不知道边界在哪。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;要解决这个问题，要交给&lt;strong&gt;应用程序&lt;/strong&gt;。&lt;/h3&gt;
&lt;p&gt;粘包的问题出现是因为不知道一个用户消息的边界在哪，如果知道了边界在哪，接收方就可以通过边界来划分出有效的用户消息。&lt;/p&gt;
&lt;p&gt;一般有三种方式分包的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;固定长度的消息；&lt;/li&gt;
&lt;li&gt;特殊字符作为边界；&lt;/li&gt;
&lt;li&gt;自定义消息结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;固定长度的消息：&lt;/h3&gt;
&lt;p&gt;这种是最简单方法，即每个用户消息都是固定长度的，比如规定一个消息的长度是 64 个字节，当接收方接满 64 个字节，就认为这个内容是一个完整且有效的消息。&lt;/p&gt;
&lt;p&gt;但是这种方式灵活性不高，实际中很少用。&lt;/p&gt;
&lt;h3&gt;特殊字符作为边界（HTTP就是这玩意）&lt;/h3&gt;
&lt;p&gt;我们可以在两个用户消息之间插入一个特殊的字符串，这样接收方在接收数据时，读到了这个特殊字符，就把认为已经读完一个完整的消息。&lt;/p&gt;
&lt;h3&gt;自定义消息结构&lt;/h3&gt;
&lt;p&gt;我们可以自定义一个消息结构，由包头和数据组成，其中包头包是固定大小的，而且包头里有一个字段来说明紧随其后的数据有多大。&lt;/p&gt;
&lt;p&gt;比如这个消息结构体，首先 4 个字节大小的变量来表示数据长度，真正的数据则在后面。当接收方接收到包头的大小（比如 4 个字节）后，就解析包头的内容，于是就可以知道数据的长度，然后接下来就继续读取数据，直到读满数据的长度，就可以组装成一个完整到用户消息来处理了。&lt;/p&gt;
&lt;p&gt;https://juejin.cn/post/7031925832167718943&lt;/p&gt;
&lt;h2&gt;服务端挂了，客户端的TCP链接还在吗？&lt;/h2&gt;
&lt;h3&gt;如果是服务端进程崩溃 -- 触发四次挥手&lt;/h3&gt;
&lt;p&gt;TCP 的连接信息是由内核维护的，&lt;strong&gt;所以当服务端的进程崩溃后，内核需要回收该进程的所有 TCP 连接资源&lt;/strong&gt;，于是内核会发送第一次挥手 FIN 报文，后续的挥手过程也都是在内核完成，并不需要进程的参与，&lt;strong&gt;所以即使服务端的进程退出了，还是能与客户端完成 TCP四次挥手的过程。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;如果是服务端主机宕机 -- 无法进行四次挥手。需要靠后续数据判断。&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;当服务端的主机发生了宕机，是没办法和客户端进行四次挥手的&lt;/strong&gt;，所以在服务端主机发生宕机的那一时刻，客户端是没办法立刻感知到服务端主机宕机了，&lt;strong&gt;只能在后续的数据交互中来感知服务端的连接已经不存在了&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;如果宕机后，客户端会发送数据： -- 触发超时重传后，重传次数达到阈值，内核判定链接有问题，主动断开。&lt;/h4&gt;
&lt;p&gt;在服务端主机宕机后，客户端发送了数据报文，由于得不到响应，在等待一定时长后，客户端就会触发&lt;strong&gt;超时重传&lt;/strong&gt;机制，重传未得到响应的数据报文。&lt;/p&gt;
&lt;p&gt;当重传次数达到达到一定阈值后，内核就会判定出该 TCP 连接有问题，然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了，于是客户端的 TCP 连接就会断开。&lt;/p&gt;
&lt;h4&gt;如果宕机后，客户端一直不发送数据： -- 需要看是否开启了TCP 的keepalive。没有开启则不会检测，开启了由于有探测报文则可以检测到。&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;strong&gt;没有开启&lt;/strong&gt; TCP keepalive 机制，&lt;strong&gt;在服务端主机发送宕机后，如果客户端一直不发送数据，那么客户端的 TCP 连接将一直保持存在&lt;/strong&gt;，所以我们可以得知一个点，&lt;strong&gt;在没有使用 TCP 保活机制，且双方不传输数据的情况下，一方的 TCP 连接处在 ESTABLISHED 状态时，并不代表另一方的 TCP 连接还一定是正常的。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而如果&lt;strong&gt;开启&lt;/strong&gt;了 TCP keepalive 机制，在服务端主机发送宕机后，即使客户端一直不发送数据，在持续一段时间后，&lt;strong&gt;TCP 就会发送探测报文&lt;/strong&gt;，探测服务端是否存活：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;strong&gt;对端是正常工作&lt;/strong&gt;的。当 TCP 保活的探测报文发送给对端, 对端会正常响应，这样 &lt;strong&gt;TCP 保活时间会被重置&lt;/strong&gt;，等待下一个 TCP 保活时间的到来。&lt;/li&gt;
&lt;li&gt;如果&lt;strong&gt;对端主机崩溃，或对端由于其他原因导致报文不可达&lt;/strong&gt;。当 TCP 保活的探测报文发送给对端后，石沉大海，没有响应，连续几次，达到保活探测次数后，&lt;strong&gt;TCP 会报告该 TCP 连接已经死亡&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，TCP keepalive 机制可以在双方没有数据交互的情况，通过探测报文，来确定对方的 TCP 连接是否存活。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;如果宕机后马上重启服务器， 重启服务器后客户端此时发送了数据： --由于原链接不存在，会返回RST。 客户端收到RST后断开连接。&lt;/h3&gt;
&lt;p&gt;如果服务端主机宕机后，然后马上重启了服务端，重启完成后，如果这时客户端发送了数据，由于服务端之前的连接信息已经不存在（ TCP 连接的数据结构已经丢失了），所以会回 RST 报文给客户端，客户端收到 RST 报文后，就断开连接。&lt;/p&gt;
&lt;h2&gt;客户端挂了，服务端的TCP链接还在吗？&lt;/h2&gt;
&lt;p&gt;和上面的服务端一样。&lt;/p&gt;
&lt;h1&gt;UDP和TCP哪个快&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;因为UDP不保证数据可以被准确无误的送达，所以没有那些重传和校验机制。自然在&lt;strong&gt;一般情况下UDP比TCP更快&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;但是一般情况下，UDP还是要做重传机制。这时候问题来了。&lt;strong&gt;如果现在我需要传一个特别大的数据包但是丢包了呢&lt;/strong&gt;？
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;TCP&lt;/code&gt;里，它内部会根据&lt;code&gt;MSS&lt;/code&gt;的大小&lt;strong&gt;分段&lt;/strong&gt;，这时候进入到IP层之后，每个包大小都不会超过&lt;code&gt;MTU&lt;/code&gt;，因此IP层一般不会再进行分片。这时候发生丢包了，只需要&lt;strong&gt;重传每个MSS分段&lt;/strong&gt;就够了。&lt;/li&gt;
&lt;li&gt;但对于&lt;code&gt;UDP&lt;/code&gt;，其本身并不会分段，如果数据过大，到了IP层，就会进行分片。此时发生丢包的话，再次重传，就会&lt;strong&gt;重传整个大数据包&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对于上面这种情况，&lt;strong&gt;使用UDP就比TCP要慢&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当然，解决起来也不复杂。这里的关键点在于是否实现了数据分段机制，&lt;strong&gt;使用UDP的应用层如果也实现了分段机制的话，那就不会出现上述的问题了&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;单判断协议在哪一层&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;知道&lt;code&gt;mac&lt;/code&gt;，是链路层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而且知道&lt;code&gt;ip&lt;/code&gt; ，是网络层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而且知道端口，是传输层&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显然，&lt;code&gt;icmp&lt;/code&gt;工作在网络层&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>计算机网络 - 数据链路层1</title><link>https://sterben-01.github.io/posts/NET2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/NET2/</guid><pubDate>Thu, 23 Jun 2022 13:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;计算机网络 - 数据链路层1&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h1&gt;数据链路层&lt;/h1&gt;
&lt;h2&gt;功能概述&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;为网络层提供服务。无确认无连接服务，有确认无连接服务，有确认面向连接服务。&lt;strong&gt;有连接一定有确认!&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;链路管理，即连接的建立、维持、释放（用于面向连接的服务）。&lt;/li&gt;
&lt;li&gt;组帧&lt;/li&gt;
&lt;li&gt;流量控制。&lt;/li&gt;
&lt;li&gt;差错控制。（帧错、位错）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;封装成帧&lt;/h3&gt;
&lt;p&gt;封装成帧就是在一段数据的前后部分添加首部和尾部，这样就构成了一个帧。接收端在收到物理层上交的比特流后，就能根据首部和尾部的标记，从收到的比特流中识别帧的开始和结束。接收端在收到物理层上交的比特流后，就能根据首部和尾部的标记，从收到的比特流中识别帧的开始和结束。
首部和尾部包含许多的控制信息，他们的一个重要作用:帧定界（确定帧的界限）。(首部和尾部包含许多的控制信息，他们的一个重要作用:帧定界(确定帧的界限)。
帧同步：接收方应当能从接收到的二进制比特流中区分出帧的起始和终止。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET2.assets/QQ%E6%88%AA%E5%9B%BE20220623174712.png&quot; alt=&quot;QQ截图20220623174712&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;流量控制&lt;/h3&gt;
&lt;p&gt;数据链路层的流量控制手段：接收方收不下就不回复确认&lt;/p&gt;
&lt;h3&gt;停止-等待协议&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;为了让接收方能够判断所受到的数据分组是否是重复的，需要给数据分组编号，由于停止-等待协议的停等特性，只需要1比特编号就可以了，即编号0或1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可以理解为停止-等待协议是特殊的滑动窗口。发送窗口大小 = 1，接收窗口大小 = 1&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;情况一：确认否认&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET2.assets/v2-2abce5cb7d9582ea392d8e0b017d10d2_r.jpg&quot; alt=&quot;v2-2abce5cb7d9582ea392d8e0b017d10d2_r&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送方给接收方发送数据分组（DATA），&lt;/li&gt;
&lt;li&gt;接收方收到后对其进行差错检测，若没有误码，则接收该数据分组，并给发送方发送确认分组（ACK）&lt;/li&gt;
&lt;li&gt;发送方收到对发送的数据分组（DATA）所对应的确认分组（ACK）后，才能发送下一个数据分组（DATA），假设这个DATA在传输过程中出现了误码&lt;/li&gt;
&lt;li&gt;接收方收到后对其进行差错检测，发现了误码，则丢弃该数据分组，并给发送方发送否认分组（NAK）&lt;/li&gt;
&lt;li&gt;发送方收到对发送的数据分组（DATA）所对应的否认分组（NAK）后，就知道了之前所发送的DATA出现了差错， 而被接收方拒绝，于是，立刻重传该DATA，因此，发送方每发送完一个DATA之后，并不能立刻将该DATA从缓存中删除，只有收到该DATA的ACK之后，才能将DATA从缓存中删除&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由此可知，发送方每发送完一个数据分组DATA后，就要停止发送DATA，等待ACK到达之后才能发送下一个DATA&lt;/p&gt;
&lt;h4&gt;情况二：超时重传&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET2.assets/v2-14723de917ff6259c0ec11b0b25732ec_r.jpg&quot; alt=&quot;v2-14723de917ff6259c0ec11b0b25732ec_r&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;情况三：ACK丢失&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET2.assets/v2-402e945fb81511be47075c1778a23632_r.jpg&quot; alt=&quot;v2-402e945fb81511be47075c1778a23632_r&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接收端丢弃DATA0的数据，重新发送对于DATA0的ACK确认。&lt;/p&gt;
&lt;h4&gt;情况四：ACK迟到&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-23-NET2.assets/v2-7a2ce992688a55b1e448e0d3aa650b6a_r.jpg&quot; alt=&quot;v2-7a2ce992688a55b1e448e0d3aa650b6a_r&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接收端端丢弃重复的DATA0。重新发送ACK0。发送端忽略收到的ACK0&lt;/p&gt;
</content:encoded></item><item><title>C++ STL - 1 - List</title><link>https://sterben-01.github.io/posts/STL1/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/STL1/</guid><pubDate>Sat, 11 Jun 2022 20:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++ STL - 1 - List&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h1&gt;List&lt;/h1&gt;
&lt;h2&gt;G2.9&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220611181104.png&quot; alt=&quot;QQ截图20220611181104&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;List自己本身只有一个指针指向了一个节点。它的数据&lt;code&gt;__list_node&lt;/code&gt;和数据结构&lt;code&gt;list&lt;/code&gt;是分开设计的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数据本身不做过多叙述。一个指针指向前一个节点，一个指针指向下一个节点，一块数据区域。&lt;/p&gt;
&lt;h3&gt;List的迭代器&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220611181259.png&quot; alt=&quot;QQ截图20220611181259&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意i++和++i的重载区别&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在C++中，由于&lt;code&gt;++i&lt;/code&gt;和&lt;code&gt;i++&lt;/code&gt;都只有一个参数，那么如何对这两种分别进行重载呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在C++中，规定了带有参数的是后置++，没有参数的是前置++。比如说
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;operator++(int) {}&lt;/code&gt;; //对 &lt;code&gt;i++&lt;/code&gt; 进行重载&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator++() {}&lt;/code&gt;;    //对 &lt;code&gt;++i&lt;/code&gt; 进行重载&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1.后置++的* 操作符不是解引用，而是调用了拷贝构造函数来制造一个副本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2.为了模拟C++的整数不能进行如下操作：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;(i++)++;    //不允许
i++++;     //不允许
(++i)++;    //允许
++++i;      //允许
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记住，i++是右值。++i是左值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220611181550.png&quot; alt=&quot;QQ截图20220611181550&quot; /&gt;&lt;/p&gt;
&lt;p&gt;List为了满足STL的前闭后开区间原则，会在末尾处添加一个空白的节点。什么是前闭后开？STL迭代器的&lt;code&gt;end()&lt;/code&gt;是超尾的。也就是指向最后一个元素的下一个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般来说。一开始我们看到的List本身的那一个指针就是指向的这个空节点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220611181805.png&quot; alt=&quot;QQ截图20220611181805&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;迭代器内部的node指针 &lt;code&gt;link_type node&lt;/code&gt;是他自己的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220611193702.png&quot; alt=&quot;QQ截图20220611193702&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们使用迭代器的时候一般会 &lt;code&gt;list&amp;lt;int&amp;gt;::iterator iter = list.begin();&lt;/code&gt; 或 &lt;code&gt;list&amp;lt;int&amp;gt;::iterator iter2(list.begin());&lt;/code&gt;这样来赋值。&lt;code&gt;begin()&lt;/code&gt;会返回一个对应类型的迭代器。如果放入这个例子的话就是将对应类型的迭代器赋值给了迭代器&lt;code&gt;iter&lt;/code&gt;自己的&lt;code&gt;node&lt;/code&gt;指针。然后进行操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220611215023.png&quot; alt=&quot;QQ截图20220611215023&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里这张图是&lt;code&gt;list&lt;/code&gt;本身。红框的&lt;code&gt;node&lt;/code&gt;不是迭代器的&lt;code&gt;node&lt;/code&gt;。是list自己的&lt;code&gt;node&lt;/code&gt;。我们可以看见&lt;code&gt;node&lt;/code&gt;指向了超尾。因为是双向循环链表，所以&lt;code&gt;begin&lt;/code&gt;就是&lt;code&gt;node&lt;/code&gt;指向的节点的&lt;code&gt;next&lt;/code&gt;部分。这里的&lt;code&gt;begin()&lt;/code&gt;和&lt;code&gt;end()&lt;/code&gt;里面，首先把指向节点的指针也就是&lt;code&gt;*node.next&lt;/code&gt;转型成&lt;code&gt;link_type&lt;/code&gt;(因为这个指针的类型是&lt;code&gt;void*&lt;/code&gt;) 然后转型为&lt;code&gt;iterator&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里是怎么把&lt;code&gt;link_type&lt;/code&gt;转换成&lt;code&gt;iterator&lt;/code&gt;类型的呢？这里他的返回值使用了构造函数进行隐式转换。我们看上上张图。通过&lt;code&gt;typedef&lt;/code&gt;我们知道&lt;code&gt;iterator&lt;/code&gt;就是&lt;code&gt;__list_iterator&lt;/code&gt;。然后我们发现 &lt;code&gt;__list_iterator&lt;/code&gt;类中的其中一个构造函数（上上张图的绿框）接受一个&lt;code&gt;link_type&lt;/code&gt;的参数。而刚好，我们&lt;code&gt;begin()&lt;/code&gt;函数的返回值刚好是&lt;code&gt;link_type&lt;/code&gt;。然后函数头声明的返回值是&lt;code&gt;iterator&lt;/code&gt;类型的。这时候就自动调用了&lt;code&gt;iterator(__list_iterator)&lt;/code&gt;类的接受link_type类型的参数的构造函数进行了隐式转换。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220611214054.png&quot; alt=&quot;QQ截图20220611214054&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;空链表长这样。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;总结：&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;list&lt;/code&gt;自己只有一个指针，是一个迭代器。一开始这个迭代器会指向&lt;code&gt;end&lt;/code&gt;。因为&lt;code&gt;begin()&lt;/code&gt;函数会去访问这个迭代器，找到这个迭代器指向的&lt;code&gt;list&lt;/code&gt;的实际节点的&lt;code&gt;next&lt;/code&gt;指针，把这个&lt;code&gt;next&lt;/code&gt;指针返回出来。也就是把&lt;code&gt;next&lt;/code&gt;指针指向的地址（下一个节点地址）拿回来。&lt;/p&gt;
&lt;h2&gt;增删时间复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;任何位置的插入删除：O(1)(假设告诉了你插入删除的位置，不需要你线查找再删除)&lt;/li&gt;
&lt;li&gt;头尾查询：O(1)&lt;/li&gt;
&lt;li&gt;其他位置查询：O（N）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;隐式类型转换&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class tmp{
    public:
    string _text;
    tmp(string c): _text(c) {}

};
class test{
    public:
    string _text;
    test(string a): _text(a){}
    /*explicit*/ test(tmp s): _text(s._text){} //test类的构造函数接受tmp类型的对象。
};

class twonumber{
    public:
    test ret(){
        test(tmp(&quot;cde&quot;));
        return(tmp(&quot;abc&quot;)); //此处返回的对象是一个tmp，但是函数头的返回类型却是test。因为这里使用了构造函数的隐式转换。也就是test类的构造函数接受tmp类型的对象
        //!由于return语句内是隐式转换，所以对应的构造函数如果有explicit关键字则无法进行隐式转换。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;有四种情况会发生隐式类型转换&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;混合类型的算数运算表达式。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int a = 3;
double b = 4.5;
a + b; // a将会被自动转换为double类型，转换的结果和b进行加法操作
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;不同类型的赋值操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int a = true ; ( bool 类型被转换为 int 类型)
int * ptr = null;（null被转换为 int *类型）
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;函数参数传值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void func( double a);
func(1); // 1被隐式的转换为double类型1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;函数返回值。（此处例子）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;double add( int a, int b)
{
return a + b;
} //运算的结果会被隐式的转换为double类型返回
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;哈希表&lt;/h1&gt;
&lt;h2&gt;线性探测：&lt;/h2&gt;
&lt;p&gt;我们有一个容器，把元素用hashfunction哈希过后，放入容器内。如果发生冲突就往后移动一个，直到找到空位。如果放入的元素数量等于了容器大小，就要放到一个新容器内，元素也需要rehash。&lt;strong&gt;在此情况下，最坏的时候就是线性搜索整个数组，平均情况就是巡访一半数组。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;二次探测：&lt;/h2&gt;
&lt;p&gt;在线性探测的基础上，冲突的时候并不单纯移动一个，而是用一个方程再次hash。&lt;strong&gt;如果我们假设桶的大小M为质数，而且永远保持负载系数在0.5以下（也就是说超过0.5就重新配置并重新整理桶中的元素），那么可以确定每插入一个新元素所需要的探测次数不多于2。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;STL 开链法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;哈希表的装bucket的是vector。vector的每一个下标对应的元素是一个bucket。&lt;/li&gt;
&lt;li&gt;bucket储存了一个单向链表。如果发生哈希冲突就挂在链表上。&lt;/li&gt;
&lt;li&gt;但是如果一直发生冲突，则链表会越来越长，效率会越来越低。所以&lt;strong&gt;一旦元素数量大于桶的数量，就会扩容&lt;/strong&gt;，大小为当前容量一倍的容量附近的质数。&lt;strong&gt;然后元素需要rehash&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;需要删除的元素只有在rehash的时候在会被删除。&lt;/li&gt;
&lt;li&gt;装桶的vector的大小是有提前定义的。质数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;deque&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;deque的核心是分段连续区间。这是依靠一个控制区块来完成的。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制区块可以是一个vector。vector的每个元素储存着一个指向实际数据区域的指针。&lt;/li&gt;
&lt;li&gt;deque自己的数据：
&lt;ul&gt;
&lt;li&gt;一个指向中控器的指针&lt;/li&gt;
&lt;li&gt;一个整数，表达的是中控器的大小&lt;/li&gt;
&lt;li&gt;一个迭代器，指向deque的第一个&lt;strong&gt;分配的&lt;/strong&gt;数据区域（&lt;code&gt;deque::begin()&lt;/code&gt;传回这个)&lt;/li&gt;
&lt;li&gt;一个迭代器，指向deque的最后一个&lt;strong&gt;分配的&lt;/strong&gt;数据区域（&lt;code&gt;deque::end()&lt;/code&gt;传回这个)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;deque自己的迭代器是不一样的：
&lt;ul&gt;
&lt;li&gt;一个指针指向当前数据 &lt;code&gt;cur&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个指针指向当前数据区块的头&lt;code&gt;first&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个指针指向当前数据区块的尾&lt;code&gt;last&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个指针指向中控器 (自己当前所在的位置)&lt;code&gt;node&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;增删时间复杂度&lt;/h2&gt;
&lt;p&gt;deque是两头开。所以除了中间以外，增删都是O(1)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;头部尾部插入删除：O(1)&lt;/li&gt;
&lt;li&gt;中间插入删除：O(N)&lt;/li&gt;
&lt;li&gt;查找：O(N)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-STL1.assets/QQ%E6%88%AA%E5%9B%BE20220916141142.png&quot; alt=&quot;QQ截图20220916141142&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;vector&lt;/h1&gt;
&lt;p&gt;vector自己是三根指针。储存数据的区域是一个连续的线性内存空间也就是个数组。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个迭代器&lt;code&gt;start&lt;/code&gt;指向内存分配起始位置（&lt;code&gt;vector::begin()&lt;/code&gt;返回这个）&lt;/li&gt;
&lt;li&gt;一个迭代器&lt;code&gt;finish&lt;/code&gt;指向当前最后一位数据的下一个位置（记住end是超尾，需要指向最后一个数据的下一位。&lt;code&gt;vector::end()&lt;/code&gt;返回这个）。&lt;code&gt;size&lt;/code&gt;就是这个迭代器和&lt;code&gt;start&lt;/code&gt;的距离。&lt;/li&gt;
&lt;li&gt;一个迭代器&lt;code&gt;end_of_storage&lt;/code&gt;指向内存分配的最后位置。(&lt;code&gt;capacity&lt;/code&gt;就是这个迭代器和&lt;code&gt;start的距离&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2倍扩容&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;增删时间复杂度&lt;/h2&gt;
&lt;p&gt;由于vector在除尾部外的位置对数据进行增添或删除会导致元素搬移，所以头部和中间都是O(N)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;头部插入删除：O(N)&lt;/li&gt;
&lt;li&gt;中间插入删除：O(N)&lt;/li&gt;
&lt;li&gt;尾部插入删除:O(1)&lt;/li&gt;
&lt;li&gt;查找：O(N)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;stack 容器适配器&lt;/h1&gt;
&lt;p&gt;栈的底层容器可以接受&lt;code&gt;deque&lt;/code&gt;或&lt;code&gt;vector&lt;/code&gt;或&lt;code&gt;list&lt;/code&gt;。默认是&lt;code&gt;deque&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;queue 容器适配器&lt;/h1&gt;
&lt;p&gt;队列的底层容器可以接受&lt;code&gt;deque&lt;/code&gt;或&lt;code&gt;list&lt;/code&gt;。默认是&lt;code&gt;deque&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;priority_queue 容器适配器&lt;/h1&gt;
&lt;p&gt;优先队列的底层是堆。底层容器接受&lt;code&gt;vector&lt;/code&gt;或&lt;code&gt;deque&lt;/code&gt;。默认是&lt;code&gt;vector&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优先队列的&lt;code&gt;top&lt;/code&gt;函数返回的是&lt;code&gt;const&lt;/code&gt;的值。所以无法直接修改在优先队列内的元素的值。如&lt;code&gt;pri_queue.top()++;&lt;/code&gt;这样的行为是禁止的。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;可以理解，因为堆是把整个元素当做排序依据的。也就是&lt;code&gt;元素&lt;/code&gt;无法更改。如果更改必然会对数据结构进行破坏。所以只能进行拿出，修改，再插入这样的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;构建优先队列需要堆排序。所以构建优先队列的时间复杂度是线性的&lt;code&gt;O(nlogn)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;优先队列采用堆排序，由于堆排序的特性，入队后的队列（最大堆/最小堆）&lt;strong&gt;只保证父节点比子节点大或者小，他不保证子节点的绝对顺序。&lt;/strong&gt; 所以如果把内容放入优先队列后，直接查看内存会发现并不保证绝对顺序。&lt;strong&gt;但是出队的时候是保证顺序的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用std::bind搭配priority_queue 和 vector的自定义排序。&lt;/h2&gt;
&lt;p&gt;注意优先队列的自定义排序器需要做为参数传入。注意在传入匿名对象的时候应使用花括号避免语义问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include&amp;lt;memory&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;algorithm&amp;gt;
using namespace std;

/*
text 我们的目的是传入一个数字X，让容器内的数字依据和X的差值进行排序。
*/
class mycomp{ //&amp;amp;priority_queue 的比较器
    public:
        bool operator()(int a, int b, int x){
            if(abs(a-x) &amp;gt;= abs(b-x)){
                return false;
            }
            return true;
        }
};
class mycomp1{ //&amp;amp;vector 的比较器。注意和优先队列是相反的。
    public:
        bool operator()(int a, int b, int x){
            if(abs(a-x) &amp;lt;= abs(b-x)){
                return false;
            }
            return true;
        }
};
void func(vector&amp;lt;int&amp;gt;&amp;amp; vec, int x){

    sort(vec.begin(), vec.end(), bind(mycomp1(), placeholders::_1, placeholders::_2, x)); //&amp;amp;使用bind对sort所需的自定义比较器参数进行绑定。

    auto cmp = [=](int a, int b) { return abs(a-x)&amp;lt; abs(b-x); };//&amp;amp;使用lambda做为自定义比较器
    priority_queue&amp;lt;int, vector&amp;lt;int&amp;gt;, decltype(cmp)&amp;gt; my_que{cmp}; //%注意，类型使用decltype自动推导，并且需要在优先队列构造函数内传入该比较器对象。
    //* 可以换成花括号。尤其是当要传入一个匿名对象的时候，防止语义问题。


    auto cmp1 = bind(mycomp(), placeholders::_1, placeholders::_2, x); //&amp;amp;使用bind生成的可调用对象做为自定义比较器
    priority_queue&amp;lt;int, vector&amp;lt;int&amp;gt;, decltype(cmp1)&amp;gt; my_que1(cmp1);//%注意，类型使用decltype自动推导，并且需要在优先队列构造函数内传入该比较器对象。

    priority_queue&amp;lt;int, vector&amp;lt;int&amp;gt;, decltype(bind(mycomp(), placeholders::_1, placeholders::_2, x))&amp;gt; my_que2(bind(mycomp(), placeholders::_1, placeholders::_2, x)); //直接写入。会非常长。


    //------测试部分---------
    for(int i = 0; i &amp;lt; vec.size(); i++){
        my_que1.push(vec[i]);
        my_que.push(vec[i]);
        my_que2.push(vec[i]);
    }

    cout &amp;lt;&amp;lt;&quot;myque&quot; &amp;lt;&amp;lt; endl;
    while(!my_que.empty()){
        cout &amp;lt;&amp;lt; my_que.top() &amp;lt;&amp;lt; endl;
        my_que.pop();
    }

    cout &amp;lt;&amp;lt; &quot;myque1&quot; &amp;lt;&amp;lt; endl;
    while(!my_que1.empty()){
        cout &amp;lt;&amp;lt; my_que1.top() &amp;lt;&amp;lt; endl;
        my_que1.pop();
    }

    cout &amp;lt;&amp;lt; &quot;myque2&quot; &amp;lt;&amp;lt; endl;
    while(!my_que2.empty()){
        cout &amp;lt;&amp;lt; my_que2.top() &amp;lt;&amp;lt; endl;
        my_que2.pop();
    }
}
int main(){
    vector&amp;lt;int&amp;gt;a {1,2,3,9,100,7,5};
    func(a, 2);
    cout &amp;lt;&amp;lt;&quot;vec&quot; &amp;lt;&amp;lt; endl;
    for(auto&amp;amp; i:a){
        cout &amp;lt;&amp;lt;i &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是简单的版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class mycomp{ //&amp;amp;priority_queue 的比较器
    public:
        bool operator()(int a, int b){
            if(abs(a-5) &amp;gt;= abs(b-5)){
                return false;
            }
            return true;
        }
};

int main(){
    priority_queue&amp;lt;int,vector&amp;lt;int&amp;gt;, mycomp&amp;gt;my_que{mycomp()}; //传入匿名对象使用花括号。
    my_que.push(10);


    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;set&lt;/h2&gt;
&lt;h3&gt;set不允许更改值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;因为set的&lt;code&gt;key&lt;/code&gt;和&lt;code&gt;data&lt;/code&gt;是同一个东西，也就是&lt;code&gt;value&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;我们是依靠这个值进行红黑树排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;但是set不允许更改值是因为迭代器返回的是常量迭代器也就是&lt;code&gt;const iterator&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;map不允许更改key是因为保存至红黑树的pair里面的key是const的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;set不会重复插入且不允许重复元素的原因&lt;/h3&gt;
&lt;p&gt;set使用的红黑树的&lt;code&gt;insert_unique&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;multi_set使用的红黑树的&lt;code&gt;insert_equal&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;map&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt;不允许重复key。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;multi_map&lt;/code&gt;允许重复key&lt;/p&gt;
&lt;h3&gt;map不会重复插入且不允许重复key的原因&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt;使用的红黑树的&lt;code&gt;insert_unique&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;multi_map&lt;/code&gt;使用的红黑树的&lt;code&gt;insert_equal&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++ STL - 2 - 迭代器设计思路。萃取。</title><link>https://sterben-01.github.io/posts/STL2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/STL2/</guid><pubDate>Sat, 11 Jun 2022 20:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++ STL - 2 - 迭代器设计思路。萃取。&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h1&gt;什么是萃取？&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;三个字：中间层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们这里先以侯捷老师的&lt;code&gt;iterator_traits&lt;/code&gt;做为例子。&lt;/p&gt;
&lt;p&gt;我们知道，&lt;code&gt;iterator&lt;/code&gt;迭代器本身有五个属性。我们也知道迭代器是一个&lt;code&gt;class&lt;/code&gt;。所以也就是迭代器类有五个&lt;code&gt;typedef&lt;/code&gt;。分别是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace std
{
    template &amp;lt;typename T&amp;gt;
    struct iterator_traits
    {
        typedef typename T::iterator_category   iterator_category;  //迭代器分类，比如正向迭代器，双向迭代器
        typedef typename T::value_type          value_type;  //迭代器指向的对应的数据类型，比如int string 等等
        typedef typename T::difference_type     difference_type; //用来表示两个迭代器之间的距离，因此也可以用来表示一个容器的最大容量
        typedef typename T::pointer             pointer; 
        typedef typename T::reference           reference; 
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;算法组想通过迭代器访问容器数据。假设我们的算法组就一定要问你，迭代器指向的数据的类型是什么，我们可以轻易地像这样获取：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list&amp;lt;int&amp;gt;::iterator::value_type
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会回答你是一个&lt;code&gt;int&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这样非常好。但是有一个问题。迭代器是一个泛化的指针。反过来说，指针就是一个退化的迭代器。&lt;/p&gt;
&lt;p&gt;我们发现在迭代器萃取类内部有一大堆的&lt;code&gt;typedef&lt;/code&gt;。我们正是通过这个来告诉外面你这个迭代器的&lt;code&gt;value_type&lt;/code&gt;是个什么玩意。&lt;/p&gt;
&lt;p&gt;举个小例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    test(){}
    typedef int inputtype;
};

int main(){
    test::inputtype x; //这句话等于告诉你 test类下面的inputtype这个东西是int 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这里不需要加&lt;code&gt;typename&lt;/code&gt;的原因是这不是模板类。类型是已经确定好的。后文会讲到。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是如果我们给算法传入的不是迭代器，而是一个指针怎么办？指针不是类，指针不是结构体，指针无法给自己弄一堆&lt;code&gt;typedef&lt;/code&gt;。也就是你问一个指针，问他你的&lt;code&gt;value_type&lt;/code&gt;是什么，他懵逼了，说我没有这玩意啊？怎么办&lt;/p&gt;
&lt;p&gt;这就是萃取的意义。我们可以间接地获取类型。也就是利用模板的特化包装一层，让算法统一去问萃取层，这个东西的&lt;code&gt;value_type&lt;/code&gt;是个什么玩意。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-12-STL2.assets/QQ%E6%88%AA%E5%9B%BE20220613172835.png&quot; alt=&quot;QQ截图20220613172835&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在图里，我们可以看见，如果传入的是迭代器，我们可以走到&lt;code&gt;1&lt;/code&gt;里面，直接问迭代器的&lt;code&gt;value_type&lt;/code&gt;是什么。&lt;/p&gt;
&lt;p&gt;如果传入的是一个指针，我们可以走到&lt;code&gt;2&lt;/code&gt;里面。人为地设定一个&lt;code&gt;value_type&lt;/code&gt;。也就是把&lt;code&gt;T&lt;/code&gt;提取出来。告诉算法组这个指向&lt;code&gt;T&lt;/code&gt;类型的指针的&lt;code&gt;value_type&lt;/code&gt;是&lt;code&gt;T&lt;/code&gt;类型&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class pclass{
public:
    typedef T valuetype;
};

template&amp;lt;typename T&amp;gt;
class pclass&amp;lt;T*&amp;gt;{
public:
    typedef T valuetype;
};

int main(){
    pclass&amp;lt;int&amp;gt;::valuetype x; //x的类型是int
    pclass&amp;lt;int*&amp;gt;::valuetype x; //x的类型还是int。 可以看成T* = int*, 拿掉*就是 T = int 所以还是int。
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
//基本类

class FLOATtype{
public:
    float _val;
    FLOATtype(){}
    FLOATtype(float x):_val(x){};
    float retval (float&amp;amp; num){
        cout &amp;lt;&amp;lt; _val*num &amp;lt;&amp;lt; endl;
        return _val*num;
    }
    //我们假设FLOATtype没有自己的获取输入输出类型的部分
};

class INTtype{
public:
    int _val;
    INTtype(){}
    INTtype(int x):_val(x){};
    int retval (int&amp;amp; num){
        cout &amp;lt;&amp;lt; _val*num &amp;lt;&amp;lt; endl;
        return _val*num;
    }
    //我们假设INTtype没有自己的获取输入输出类型的部分
};


class fucktype{
public:
    int _val;
    fucktype(){}
    fucktype(int x):_val(x){};
    int retval (int&amp;amp; num){
        cout &amp;lt;&amp;lt; _val*num &amp;lt;&amp;lt; endl;
        return _val*num;
    }
    typedef fucktype rettype; //我们假设fucktype有自己的获取输入输出类型的部分。所以我们加了typedef。
    typedef int inputtype;
};




//萃取 模板特化
/*
这里的例子看起来非常脱裤子放屁。但是对于迭代器萃取来讲完全不是。
我们知道迭代器是泛化的指针，也就是说指针是退化的迭代器。
我们知道迭代器是一个class 所以他们有能力定义自己的associate type。
举个例子。我们知道迭代器类里面有自己的value_type。所以我们可以直接问 list&amp;lt;int&amp;gt;::iterator你的valuetype是什么，像这样 list&amp;lt;int&amp;gt;::iterator::value_type。它会回答你它是int。
但是如果我们给算法传入的不是迭代器，而是一个指针怎么办？指针不是类，指针不是结构体，指针无法给自己弄一堆typedef。怎么办
这就是萃取的意义。我们可以间接问。就是放入萃取机。
*/
template &amp;lt;typename T&amp;gt;
class testTraits{ //默认版本。直接问对应类的输入输出类型。如果有的话
    public:
        typedef typename T::rettype rettype; //注意这里一定要加typename告诉编译器 T::rettype是一个类型。
        typedef typename T::inputtype inputtype; //注意这里一定要加typename告诉编译器 T::rettype是一个类型。告诉编译器T类下的inputtype代表的不是变量，代表的是类型。
};

template&amp;lt;&amp;gt; //模板全特化。我们假设知道inttype不是类，不能放typename 就好比指针。所以我们在这里给他包一层，显式告知对应的输入输出类型是什么。让有人问的时候转到这里告诉他对应类型
class testTraits&amp;lt;INTtype&amp;gt;{
    public:
        typedef int rettype;
        typedef int inputtype;
};

template&amp;lt;&amp;gt;
class testTraits&amp;lt;FLOATtype&amp;gt;{ //和上面一样。
    public:
        typedef float rettype;
        typedef float inputtype;
};

//使用

/*
在这个例子里面。我们对照着迭代器萃取器的例子做一个总结。
假如我们有几个类是有inputtype的。到时候直接问那个类就可以。但是如果是一个普通变量或者指针，没有inputtype的怎么办？
就好比我们的fucktype。我们在fucktype类里面有自己的typedef。所以可以直接问。
但是INTtype和FLOATtype没有，不能直接问。我们就通过模板特化来加一个中间层。所以萃取其实就是中间层。
*/

template&amp;lt;typename T&amp;gt;
class test{
    public:
    //注意这里testTraits&amp;lt;T&amp;gt;::rettype是做为函数的返回类型。所以必须要加typename。不然编译器会认为rettype是一个在testTraits里的成员变量。但是因为是把testTraits类里的rettype成员变量对应的typedef的东西当做类型来看待。所以需要加typename
    typename testTraits&amp;lt;T&amp;gt;::rettype retvall(T&amp;amp; obj, typename testTraits&amp;lt;T&amp;gt;::inputtype input){
        return obj.retval(input);
    }
};

int main(){
    INTtype intnum(5);
    int INTarg = 5;
    test&amp;lt;INTtype&amp;gt; test1;
    test1.retvall(intnum, INTarg);

    FLOATtype floatnum(5.5f);
    float FLOATarg = 8.8f;
    test&amp;lt;FLOATtype&amp;gt; test2;
    test2.retvall(floatnum, FLOATarg);

    fucktype fucknum(10);
    int fuckarg = 5;
    test&amp;lt;fucktype&amp;gt; test3;
    test3.retvall(fucknum, fuckarg);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类作用域&lt;/h2&gt;
&lt;p&gt;在类外部访问类中的名称时，可以使用类作用域操作符，形如&lt;code&gt;MyClass::name&lt;/code&gt;的调用通常存在三种：静态数据成员、静态成员函数和嵌套类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct MyClass {
    static int A;
    static int B();
    typedef int C; //这里就是typedef 也就是嵌套类型
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MyClass::A&lt;/code&gt;, &lt;code&gt;MyClass::B&lt;/code&gt;, &lt;code&gt;MyClass::C&lt;/code&gt;分别对应着上面三种。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;typedef&lt;/code&gt; 和作用域解析运算符&lt;code&gt;::&lt;/code&gt; 和 嵌套类&lt;/h2&gt;
&lt;p&gt;我们很难把class和namespace联系起来，但是这两个在抽象层次上其实是一个概念。&lt;/p&gt;
&lt;p&gt;作用域解析运算符::的作用就是制定某一个范围。但是一旦用在了嵌套类或者类内的typedef，我们就很难理解这层含义。&lt;/p&gt;
&lt;p&gt;我们都知道&lt;strong&gt;直接访问类静态成员必须使用作用域解析运算符&lt;code&gt;::&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是我们如果要通过作用域解析运算符访问非静态成员，可以吗？当然可以。只不过需要通过对象访问。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    class testinner{ //嵌套类
        public:
        int _val;
        testinner(){}
        testinner(int x):_val(x){}
    };
    test(){}
    typedef int testdef;


    int s;
};

int main()
{
    test::testinner tt1 = test::testinner(5); //OK
    test::testinner tt2; //OK 访问嵌套类
    test::testdef tt3 = 5; //OK 访问typedef。
    test::s = 5; //不行。直接访问的成员必须是静态成员。
    test t;
    t.test::s = 5; //OK 通过对象访问，但是还是脱裤子放屁加了作用域解析运算符。

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们可以理解为我们需要访问的&lt;code&gt;typede&lt;/code&gt;f和嵌套类在&lt;code&gt;test&lt;/code&gt;类的命名空间下。我们必须要告知编译器这东西在哪，所以需要通过作用域解析运算符去访问。&lt;/p&gt;
&lt;p&gt;至于&lt;code&gt;typedef&lt;/code&gt;和嵌套类是否是一个类的成员，我没有查到确切的说法。如果说他们不是成员吧，但是有成员的属性。要是说是成员吧，我们也可以直接访问。但是也有人说嵌套类的&lt;code&gt;static&lt;/code&gt;是隐式的。也有人把嵌套类当做一个&lt;code&gt;namespace&lt;/code&gt; 来看待。所以我的理解是&lt;strong&gt;不要把嵌套类和&lt;code&gt;typedef&lt;/code&gt;看做类成员。假设他们可以直接调用即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果嵌套类没有在外部类中实例化，则实例化外部类的时候不会实例化嵌套类内容。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;最后说一下必须显式使用&lt;code&gt;typename&lt;/code&gt;的情况。&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-12-STL2.assets/QQ%E6%88%AA%E5%9B%BE20220613134505-16551788472405.png&quot; alt=&quot;QQ截图20220613134505&quot; /&gt;&lt;/p&gt;
&lt;p&gt;来几个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
        struct obj{
            int _sb;
        };
        obj s;
        int _val;
        test(){}
        test(int x):_val(x){}
        test(int x, int y){
            _val = x;
            s._sb = y;
        }
};
template&amp;lt;typename T&amp;gt;
void func(){
    test t(5,8);
    typename T::obj* ptr = &amp;amp;t.s;
    //这里我们的意思是有一个指针ptr指向了T类里面的obj类型的对象。翻译成人话也就是ptr是一个T::obj类型的指针。但是如果有一个T类里面的obj是一个变量，如static int obj = 8
    //那么这就变成了变量乘法。会有歧义。所以使用typedef显式告知编译器 T::obj不是一个变量，而是一个类型。
    cout &amp;lt;&amp;lt; ptr-&amp;gt;_sb &amp;lt;&amp;lt; endl;
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能还不够？再来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef iterator_traits&amp;lt;T&amp;gt;::value_type value_type_anothername; 
//这句话的意思是value_type_anothername是在模板类iterator_traits中的 变量 value_type的别名。

//所以如果有
value_type_anothername name = &quot;foward_iterator&quot;;
//这样是不行的。因为value_type_anothername是一个变量不是类型。
//所以我们需要加typedef告知编译器这个是类型

typedef typename iterator_traits&amp;lt;T&amp;gt;::value_type value_type_anothername; 
//这句话的意思是value_type_anothername是iterator_traits&amp;lt;T&amp;gt;::value_type这个 类型 的别名。

//所以这样可以有
value_type_anothername name = &quot;foward_iterator&quot;;
//因为value_type_anothername 是个类型。就好比int string这种。

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还不够？再来：&lt;/p&gt;
&lt;p&gt;这是我们前文的例子。但是为什么这里不需要加typedef呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    typedef int inputtype;
};

int main(){
    test::inputtype x = 4; //这句话等于告诉你 test类下面的inputtype这个东西是int 
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先送上大佬文章一篇：https://feihu.me/blog/2014/the-origin-and-usage-of-typename/&lt;/p&gt;
&lt;p&gt;由于&lt;code&gt;test&lt;/code&gt;已经是一个完整的定义，因此编译期它的类型就可以确定下来，也就是说&lt;code&gt;test::inputtype&lt;/code&gt;这些名称对于编译器来说也是已知的。&lt;/p&gt;
&lt;p&gt;可是，如果是像&lt;code&gt;T::inputtype&lt;/code&gt;这样呢？&lt;code&gt;T&lt;/code&gt;是模板中的类型参数，它只有等到模板实例化时才会知道是哪种类型，更不用说内部的&lt;code&gt;inputtype&lt;/code&gt;。通过前面类作用域一节的介绍，我们可以知道，&lt;code&gt;T::inputtype&lt;/code&gt;实际上可以是以下三种中的任何一种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态数据成员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态成员函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嵌套类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;typename&lt;/code&gt;的作用，简单理解就是强制告诉编译器 &lt;code&gt;namespace::objname&lt;/code&gt;这个东西是一个类型名而不是变量名。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以在模板类中，如果想要告知编译器一个使用了&lt;code&gt;::&lt;/code&gt;作用域解析运算符的东西是类型，而不是变量，就需要加&lt;code&gt;typename&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果直接把这个东西当做一个类型来进行变量的声明，那就不需要搭配&lt;code&gt;typedef&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果需要把这个东西当做一个类型来赋予一个别名，那就需要使用&lt;code&gt;typedef&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，像这样就必须要加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class test{
    public:
    test(){}
    typedef int inputtype;
};


template&amp;lt;typename T&amp;gt;
class test1{
    public:
    test1(){};
    typedef typename T::inputtype inputtype; //注意这里，T::inputtype是个类型。比如test::inputtype 就是 int类型
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;限定名、非限定名&lt;/h2&gt;
&lt;p&gt;限定名(qualified name)，故名思义，是限定了命名空间的名称。看下面这段代码，&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;就是限定名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()  {
    std::cout &amp;lt;&amp;lt; &quot;Hello world!&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;前面都有&lt;code&gt;std::&lt;/code&gt;，它限定了&lt;code&gt;std&lt;/code&gt;这个命名空间，因此称其为限定名。&lt;/p&gt;
&lt;p&gt;如果在上面这段代码中，前面用&lt;code&gt;using std::cout;&lt;/code&gt;或者&lt;code&gt;using namespace std;&lt;/code&gt;，然后使用时只用&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;，它们的前面不再有空间限定&lt;code&gt;std::&lt;/code&gt;，所以此时的&lt;code&gt;cout&lt;/code&gt;和&lt;code&gt;endl&lt;/code&gt;就叫做非限定名(unqualified name)。&lt;/p&gt;
&lt;h2&gt;依赖名、非依赖名&lt;/h2&gt;
&lt;p&gt;依赖名(dependent name)是指依赖于模板参数的名称，而非依赖名(non-dependent name)则相反，指不依赖于模板参数的名称。看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt;
class MyClass {
    int i;
    vector&amp;lt;int&amp;gt; vi;
    vector&amp;lt;int&amp;gt;::iterator vitr;

    T t;
    vector&amp;lt;T&amp;gt; vt;
    vector&amp;lt;T&amp;gt;::iterator viter;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为是内置类型，所以类中前三个定义的类型在声明这个模板类时就已知。然而对于接下来的三行定义，只有在模板实例化时才能知道它们的类型，因为它们都依赖于模板参数&lt;code&gt;T&lt;/code&gt;。因此，&lt;code&gt;T&lt;/code&gt;, &lt;code&gt;vector&amp;lt;T&amp;gt;&lt;/code&gt;和&lt;code&gt;vector&amp;lt;T&amp;gt;::iterator&lt;/code&gt;称为依赖名。前三个定义叫做非依赖名。&lt;/p&gt;
&lt;p&gt;更为复杂一点，如果用了&lt;code&gt;typedef T U; U u;&lt;/code&gt;，虽然&lt;code&gt;T&lt;/code&gt;没再出现，但是&lt;code&gt;U&lt;/code&gt;仍然是依赖名。由此可见，不管是直接还是间接，只要依赖于模板参数，该名称就是依赖名。&lt;/p&gt;
</content:encoded></item><item><title>imgtemp</title><link>https://sterben-01.github.io/posts/imgtemp/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/imgtemp/</guid><pubDate>Sat, 11 Jun 2022 20:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;imgtemp&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-imgtemp.assets/61163969_p0.jpg&quot; alt=&quot;61163969_p0&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-11-imgtemp.assets/68214616_p0.png&quot; alt=&quot;68214616_p0&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++内存管理- 17</title><link>https://sterben-01.github.io/posts/memory7/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory7/</guid><pubDate>Mon, 06 Jun 2022 14:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++内存管理 - 17&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h1&gt;此笔记需要等待更新和确认。&lt;/h1&gt;
&lt;h2&gt;std::alloc 源码剖析&lt;/h2&gt;
&lt;p&gt;视频14~27没什么难度。略&lt;/p&gt;
&lt;h2&gt;源码剖析8&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-memory7.assets/QQ%E6%88%AA%E5%9B%BE20220606193011.png&quot; alt=&quot;QQ截图20220606193011&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有几个要点需要注意&lt;/p&gt;
&lt;p&gt;248行的&lt;code&gt;chunk&lt;/code&gt;已经是&lt;code&gt;char*&lt;/code&gt;*类型了。所以260行&lt;strong&gt;直接&lt;/strong&gt;将其&lt;code&gt;+n&lt;/code&gt;后转为&lt;code&gt;obj*&lt;/code&gt;类型。259~261行的要点是，首先261行的for循环是从1开始的。因为切割出来后将第一块直接返回回去。不需要再放回去切割。所以259行直接将分配过来的内存转为&lt;code&gt;obj*&lt;/code&gt;后返回。然后260行直接进行指针移动到第一个区块末尾。所以现在的空闲链表头是从&lt;code&gt;*my_free_list&lt;/code&gt;开始的。&lt;/p&gt;
</content:encoded></item><item><title>Linux 内存管理</title><link>https://sterben-01.github.io/posts/memory8/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory8/</guid><pubDate>Mon, 06 Jun 2022 14:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;Linux 内存管理&lt;/h1&gt;
&lt;p&gt;侯捷老师的视频G2.9&lt;code&gt;std alloc&lt;/code&gt;部分我们可以理解内存分配器是如何管理内存的。也就是维护一些具有固定内存大小的自由链表。如有分配需求就将其取下链表分配出去。如果不够就再调用&lt;code&gt;malloc&lt;/code&gt;进行申请。然后进行切割。&lt;/p&gt;
&lt;p&gt;这里的内存管理依旧是比较上层的。我们后来也提到了&lt;code&gt;malloc&lt;/code&gt;会使用libc的C运行时库里面的&lt;code&gt;mmap&lt;/code&gt;系统调用和&lt;code&gt;brk&lt;/code&gt;系统调用进行分配内存分配。大内存使用&lt;code&gt;mmap&lt;/code&gt;，小内存使用&lt;code&gt;brk&lt;/code&gt;。&lt;code&gt;malloc&lt;/code&gt;内部就是一个类似于std alloc的内存池机制但是更为复杂。但是这里所有的分配都是分配的虚拟内存。只有当分配后我们真正访问了这部分内存，然后触发缺页中断后才会真正分配物理内存给我们。&lt;/p&gt;
&lt;p&gt;这个时候我们可以继续深入探讨。那么&lt;code&gt;mmap&lt;/code&gt;和&lt;code&gt;brk&lt;/code&gt;又是如何分配的？这里就是使用了伙伴系统算法（buddy）和slab机制。这里就真的是进行物理内存的分配了。&lt;/p&gt;
&lt;h1&gt;buddy系统算法&lt;/h1&gt;
&lt;p&gt;在我个人理解中，这个依旧非常像&lt;code&gt;std alloc&lt;/code&gt;部分。&lt;/p&gt;
&lt;p&gt;伙伴系统算法(Buddy system)。把所有的空闲页框分组为11个块链表，每个块链表分别包含大小为1，2，4，8，16，32，64，128，256，512和1024个连续页框的页框块。最大可以申请1024个连续页框，对应4MB大小的连续内存。每个页框块的第一个页框的物理地址是该块大小的整数倍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-29-memory8.assets/QQ%E6%88%AA%E5%9B%BE20221008181830.png&quot; alt=&quot;QQ截图20221008181830&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;假设要申请一个256个页框的块，先从256个页框的链表中查找空闲块，如果没有，就去512个页框的链表中找，找到了则将页框块分为2个256个页框的块，一个分配给应用，另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块，继续向1024个页框的链表查找，如果仍然没有，则返回错误。页框块在释放时，会主动将两个连续的页框块合并为一个较大的页框块。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这部分和&lt;code&gt;std alloc&lt;/code&gt;几乎是一个原理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从上面可以知道Buddy算法一直在对页框做拆开合并拆开合并的动作。Buddy算法牛逼就牛逼在运用了世界上任何正整数都可以由2^n的和组成。这也是Buddy算法管理空闲页表的本质。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Buddy 系统解决了物理内存分配的外部碎片问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;slab&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;在Linux中，伙伴系统（buddy system）是以页为单位管理和分配内存。但是现实的需求却以字节为单位，假如我们需要申请20Bytes，总不能分配一页吧！那岂不是严重浪费内存。那么该如何分配呢？slab分配器就应运而生了，专为小内存分配而生。slab分配器分配内存以Byte为单位。但是slab分配器并没有脱离伙伴系统，而是基于伙伴系统分配的大内存进一步细分成小内存分配。
&lt;ul&gt;
&lt;li&gt;这部分是不是非常像&lt;code&gt;mmap&lt;/code&gt;和&lt;code&gt;brk&lt;/code&gt;之间的关系？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-29-memory8.assets/QQ%E6%88%AA%E5%9B%BE20221008183551.png&quot; alt=&quot;QQ截图20221008183551&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;slab分配需要解决的是内存的内部碎片问题。&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所谓内部碎片就是指被内核分配出去但是不能被利用的内存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而外部碎片是指由于频繁地申请和释放页框而导致的某些小的连续页框，比方只有一个页框，无法分配给需要大的连续页框的进程而导致的内存碎片。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;kmem_cache是链接在一起形成一个全局的双向链表。可以理解为一个内存池。每个内存池包含了一个slabs的链表，这通常是一段连续的内存块。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;slab是slab分配器的最小单位，在实现上一个slab有一个或多个连续的物理页组成（通常只有一页）。单个slab可以在slab链表之间移动，例如如果一个半满slab被分配了对象后变满了，就要从slabs_partial中被删除，同时插入到slabs_full中去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;object是内存池中提供的内存的单位。也就是对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存在3种slab：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slabs_full(完全分配的slab)
&lt;ul&gt;
&lt;li&gt;slab 中的所有对象被标记为使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;slabs_partial(部分分配的slab)
&lt;ul&gt;
&lt;li&gt;slab 中的对象有的被标记为使用，有的被标记为空闲。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;slabs_empty(空slab,或者没有对象被分配)。
&lt;ul&gt;
&lt;li&gt;slab 中的所有对象被标记为空闲。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了进一步解释，这里举个例子来说明，用struct kmem_cache结构描述的一段内存就称作一个slab缓存池。一个slab缓存池就像是一箱牛奶，一箱牛奶中有很多瓶牛奶，每瓶牛奶就是一个object。分配内存的时候，就相当于从牛奶箱中拿一瓶。总有拿完的一天。当箱子空的时候，你就需要去超市再买一箱回来。超市就相当于partial链表，超市存储着很多箱牛奶。如果超市也卖完了，自然就要从厂家进货，然后出售给你。厂家就相当于伙伴系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;个人理解。slab也是一种池化思想，依旧是比较直观的内存池思想，也就是内存池主动维护一块内存，有小块内存则通过小块的分配方法分配出去，交还的时候归还给内存池而不是操作系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;slab和buddy之间的关系 -- 互补关系&lt;/h1&gt;
&lt;p&gt;slab系统与buddy系统所要解决的问题是互补的，一个解决外部碎片一个解决内部碎片，slab是依赖buddy系统的。因为所有的内存都需要伙伴系统来分配，只不过是小块内存的时候通过slab来进行更细的划分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-29-memory8.assets/QQ%E6%88%AA%E5%9B%BE20221008183732.png&quot; alt=&quot;QQ截图20221008183732&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PS: std alloc。是不是非常像？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-29-memory8.assets/QQ%E6%88%AA%E5%9B%BE20221008181820.png&quot; alt=&quot;QQ截图20221008181820&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++内存管理- 16</title><link>https://sterben-01.github.io/posts/memory6/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory6/</guid><pubDate>Sat, 04 Jun 2022 14:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++内存管理 - 16&lt;/h1&gt;
&lt;h1&gt;此笔记需要等待更新和确认。&lt;/h1&gt;
&lt;h2&gt;New Handler&lt;/h2&gt;
&lt;p&gt;当使用&lt;code&gt;new&lt;/code&gt;申请内存失败时会抛出&lt;code&gt;&quot;bad alloc&quot;&lt;/code&gt;异常，此前会调用一个由&lt;code&gt;std::set_new_handler()&lt;/code&gt;指定的错误处理函数（&quot;new-handler&quot;）。&lt;/p&gt;
&lt;p&gt;如下是一个最简单的形式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void outOfMem(){
    std::cout&amp;lt;&amp;lt;&quot;Unable to alloc memory&quot;;
    std::abort();
}
int main(){
    std::set_new_handler(outOfMem);
    int *p = new int[100000000L];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当&lt;code&gt;new&lt;/code&gt;申请不到足够的内存时，它会&lt;strong&gt;不断地&lt;/strong&gt;调用&lt;code&gt;outOfMem&lt;/code&gt;。因此一个良好设计的系统中&lt;code&gt;outOfMem&lt;/code&gt;函数应该做如下几件事情之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使更多内存可用；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装一个新的&quot;new-handler&quot;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;卸载当前&quot;new-handler&quot;，传递&lt;code&gt;null&lt;/code&gt;给&lt;code&gt;set_new_handler&lt;/code&gt;即可；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;抛出&lt;code&gt;bad_alloc&lt;/code&gt;（或它的子类）异常；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不返回，可以&lt;code&gt;abort&lt;/code&gt;或者&lt;code&gt;exit&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于&lt;code&gt;abort&lt;/code&gt;, &lt;code&gt;exit&lt;/code&gt;, &lt;code&gt;terminate&lt;/code&gt;的区别：&lt;code&gt;abort&lt;/code&gt;会设置程序非正常退出，&lt;code&gt;exit&lt;/code&gt;会设置程序正常退出，当存在未处理异常时C++会调用&lt;code&gt;terminate&lt;/code&gt;， 它会回调由&lt;code&gt;std::set_terminate&lt;/code&gt;设置的处理函数，默认会调用&lt;code&gt;abort&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-memory6.assets/QQ%E6%88%AA%E5%9B%BE20220604101541.png&quot; alt=&quot;QQ截图20220604101541&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（23）-- 内存管理5</title><link>https://sterben-01.github.io/posts/OS26/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS26/</guid><pubDate>Sat, 04 Jun 2022 13:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存（23）-- 内存管理5&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;非连续分配管理方式（1）基本分页存储管理&lt;/h2&gt;
&lt;h3&gt;分页的目的是解决外部碎片。因为之前的分配方式强调必须连续。那我们让它不连续就可以了。&lt;/h3&gt;
&lt;h3&gt;什么是分页储存&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-OS26.assets/QQ%E6%88%AA%E5%9B%BE20220604075638.png&quot; alt=&quot;QQ截图20220604075638&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;进程（虚拟内存）分割出来的大小相等的块叫 页 或 页面（page）。物理内存中分割出来的大小相等的块，叫 页框 或 页帧 或...&lt;/strong&gt;（frame）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;进程的页面对应着内存的页框。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分割的意义就是操作系统会把进程的页面一个一个放入内存的页框。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;页面不必连续存放，可以分散放置。所以我们需要页表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个进程对应一张页表！每一个进程都有一个自己的页表。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;页表&lt;/h3&gt;
&lt;p&gt;**页表来负责把逻辑地址转换为物理地址。**也可以理解为查找进程分割的内存（虚拟地址） 可以或者已经 放在了哪些页框中（物理地址）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-OS26.assets/QQ%E6%88%AA%E5%9B%BE20220604080127.png&quot; alt=&quot;QQ截图20220604080127&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;页表是操作系统创建给进程的。储存在PCB中。进程的每一个页面对应着一个页表项，也就是告诉操作系统进程的页面存放在了操作系统的哪个页框中。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;每一个页表项占多少字节？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-OS26.assets/QQ%E6%88%AA%E5%9B%BE20220604080602.png&quot; alt=&quot;QQ截图20220604080602&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;页号不占用存储空间。因为页表项是连续的，所以页号可以是隐含的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-OS26.assets/QQ%E6%88%AA%E5%9B%BE20220604080758.png&quot; alt=&quot;QQ截图20220604080758&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;如何实现地址的转换&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-OS26.assets/QQ%E6%88%AA%E5%9B%BE20220604081134.png&quot; alt=&quot;QQ截图20220604081134&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-OS26.assets/QQ%E6%88%AA%E5%9B%BE20220604081228.png&quot; alt=&quot;QQ截图20220604081228&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;特点&lt;/h2&gt;
&lt;p&gt;没有外部碎片，每一个内碎片不超过页大小。&lt;/p&gt;
&lt;p&gt;一个程序不必连续存放。&lt;/p&gt;
&lt;p&gt;程序全部装入内存。&lt;/p&gt;
&lt;h2&gt;外部碎片(在分配单元间的未使用的内存)；内部碎片(在分配单元中未使用的内存)这里的碎片指的是物理内存。不是虚拟内存。&lt;/h2&gt;
</content:encoded></item><item><title>C++内存管理- 14~15</title><link>https://sterben-01.github.io/posts/memory5/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory5/</guid><pubDate>Sat, 04 Jun 2022 13:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++内存管理 - 14~15&lt;/h1&gt;
&lt;h1&gt;此笔记需要等待更新和确认。&lt;/h1&gt;
&lt;h2&gt;Static Allocator&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;如果我们的每一个class都想要分配内存池，那么我们就需要在每个class内部都写自己的内存分配函数。这样会很麻烦。所以我们应该把内存分配函数抽象出来，写入一个新的class。这样的class就叫做static allocator。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-memory5.assets/QQ%E5%9B%BE%E7%89%8720220604091838.png&quot; alt=&quot;QQ图片20220604091838&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们的allocator会被外面的类的对象调用。所以我们不需要再在allocator里面本身维护（储存）那个对象。所以也就不需要union来精简空间。仅需一个指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;详细说明：查看笔记内存管理12~13。在&lt;code&gt;airplane&lt;/code&gt;的例子中。对象&lt;code&gt;airplane&lt;/code&gt;里面有一个储存信息的结构体&lt;code&gt;airplaneRep&lt;/code&gt;和一个维护内存的union。**但是我们每次在外面new的时候new的是整个&lt;code&gt;airplane&lt;/code&gt;对象而不是&lt;code&gt;airplaneRep&lt;/code&gt;这个局部对象。**所以我们需要用union来使用嵌入式指针。&lt;/p&gt;
&lt;p&gt;但是这里我们new的是调用allocator的对象而不是allocator本身。因为allocator自己不存有对应的对象。因为allocator只实例化一次，然后类自己通过allocator申请内存空间。所以allocator本身不会存在一个区块既有指针又有对象的情况。因为对象拿到allocator返回的区块指针之后会直接将数据写入。而allocator本身没有对应的数据对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每个allocator 对象都是分配器，它体内维护一个freelist，不同的allocator objects维护着不同的freelists。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-memory5.assets/QQ%E6%88%AA%E5%9B%BE20220604091951.png&quot; alt=&quot;QQ截图20220604091951&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外部用的类使用起来非常简便。重载operator new 和 delete是非常模板化的。所以我们可以用macro 宏来进行进一步简化。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-memory5.assets/QQ%E6%88%AA%E5%9B%BE20220604094940.png&quot; alt=&quot;QQ截图20220604094940&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意红色框和蓝色框需要分开。因为蓝色框的部分在class外面，调用的时候需要类名。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;重要知识&lt;/h2&gt;
&lt;h3&gt;指针A给指针B赋值的意思是将指针A指向的地址赋给指针B&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int digit = 5;
int* a = &amp;amp;digit;
int* b = a; //指针a指向的地址赋给指针b 没有新对象产生所以不会调用拷贝构造。
int* c = &amp;amp;*a; //等同于将指针a先解引用（*a = 5），得到指针a指向的的值。然后把这个值取地址(&amp;amp;*a)赋值给指针c。

cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl; //打印b储存的地址。也就是b指向的地址。 等同于&amp;amp;digit
cout &amp;lt;&amp;lt; *b &amp;lt;&amp;lt; endl; //解引用b 打印b指向的地址的值。
cout &amp;lt;&amp;lt; &amp;amp;b &amp;lt;&amp;lt; endl; //打印b自己的地址。
cout &amp;lt;&amp;lt; &amp;amp;digit &amp;lt;&amp;lt; endl; //打印变量地址。

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;指针加法&lt;/h3&gt;
&lt;h4&gt;指针直接和数字相加：意思是指针当前指向的地址 + 对应指针类型的大小 * 数字。&lt;/h4&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int * p;
p = (int*)malloc(100); //分配100字节。
cout &amp;lt;&amp;lt; p&amp;lt;&amp;lt; endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。此处输出 0x771410
auto y = p + 1; //把p指向的地址偏移4位（因为int是4字节）后赋值给y。
printf(&quot;%#x&quot;,y); //打印y储存的地址。也就是指向的地址。此处输出 0x771414
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;也就是说指针直接和数字相加，可以理解为用指针操作数组。加几就是将指针往后移动几位。具体偏移量也就是对应对象的大小*移动位数。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;如果想要指定一个具体的偏移量应该怎么办？&lt;/h4&gt;
&lt;p&gt;首先，可以使用转型将指针转换为对应类型。然后再加数字。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj* p;
p = (obj*)malloc(sizeof(int) * 10);
cout &amp;lt;&amp;lt; p&amp;lt;&amp;lt; endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。 此处输出 0x771410
auto x = (char*)p + 1; //把p指向的地址偏移1位后赋值给x。因为char是1位。
printf(&quot;%#x\n&quot;,x); //打印x储存的地址。也就是指向的地址。此处输出 0x771411
auto y = (obj*)p + 1; //把p指向的地址偏移8位后赋值给x。因为obj是指针类型。8位。
printf(&quot;%#x\n&quot;,y); //打印y储存的地址。也就是指向的地址。此处输出 0x771418
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：指针类型转换指的是指针转换为对应类型的指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此处不要使用&lt;code&gt;cout&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;auto x = (char*)p&lt;/code&gt;的意思是把指针&lt;code&gt;p&lt;/code&gt;转为&lt;code&gt;char*&lt;/code&gt;类型的指针。不可以&lt;code&gt;auto x = (char)p&lt;/code&gt;。 这样做的意思是直接转换成了&lt;code&gt;char&lt;/code&gt;。当然，所有的指针都是8字节。记住，指针类型指的是告诉指针应该读取指针指向的地址之后多大的数据。&lt;/p&gt;
&lt;p&gt;怎么理解这个操作呢。我们可以想一下。如果 &lt;code&gt;a&lt;/code&gt;是&lt;code&gt;int*&lt;/code&gt;类型，也就是&lt;code&gt;int&lt;/code&gt;类型指针。意思是指针&lt;code&gt;a&lt;/code&gt;指向的数据是&lt;code&gt;int&lt;/code&gt;，所以需要以&lt;code&gt;4&lt;/code&gt;字节为一段进行读取。也就是一次读取四个字节。那么&lt;code&gt;a+1&lt;/code&gt;其实是对地址进行了&lt;code&gt;+4&lt;/code&gt;操作。因为指针+1的意思是将指针往后移动一个&lt;strong&gt;对象位。也就是指针当前指向的地址 + 对应指针类型的大小（此处为4） * 数量（此处为1）&lt;/strong&gt;。但是我们如果想要对地址&lt;code&gt;+1&lt;/code&gt;的话怎么办？我们可以把指针&lt;code&gt;a&lt;/code&gt;转换为&lt;code&gt;char*&lt;/code&gt;类型也就是&lt;code&gt;char&lt;/code&gt;类型指针。而不是&lt;code&gt;int&lt;/code&gt;类型指针。这样相当于告诉编译器，指针&lt;code&gt;a&lt;/code&gt;指向的数据是&lt;code&gt;char&lt;/code&gt;。所以需要以1字节为一段进行读取。也就是一次读取一个字节。所以此时&lt;code&gt;a+1&lt;/code&gt;就是对地址进行了&lt;code&gt;+1&lt;/code&gt;操作。&lt;strong&gt;也就是指针当前指向的地址 + 对应指针类型的大小（此处为1） * 数量（此处为1）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;移动一个对象位也就是移动的偏移量为对象大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int digit = 5;
int* a = &amp;amp;digit;
cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl;
auto x = (char*)a + 1;
printf(&quot;%#x\n&quot;,x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意！&lt;code&gt;(char*)a + 1&lt;/code&gt;往后移动1位的时候，&lt;code&gt;(char*)a&lt;/code&gt;的意思是把指针a看成指向&lt;code&gt;char&lt;/code&gt;类型的指针（告诉编译器一段是1）。所以&lt;code&gt;(char*)a + 1&lt;/code&gt;不是+2，还是+1。因为指针&lt;code&gt;a&lt;/code&gt;指向的地址根本没变。仅仅是看成了&lt;code&gt;char&lt;/code&gt;类型的指针后+1&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;详细解释&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-04-memory5.assets/QQ%E6%88%AA%E5%9B%BE20220606122223.png&quot; alt=&quot;QQ截图20220606122223&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为什么要在&lt;code&gt;allocator&lt;/code&gt;里面单独建立一个&lt;code&gt;obj&lt;/code&gt;类型的结构体？&lt;/strong&gt; 我们可以看到。在函数&lt;code&gt;allocate&lt;/code&gt;里面，我们使用了&lt;code&gt;obj*&lt;/code&gt;类型的指针。我们这个&lt;code&gt;obj&lt;/code&gt;类型只有一个指针，所以大小为8。我们看3的位置。申请出来的内存转型为&lt;code&gt;obj*&lt;/code&gt;类型后赋值给了&lt;code&gt;obj*&lt;/code&gt;类型的指针p。为什么要这么做？因为我们依旧使用了嵌入式指针的概念。把申请来的内存切为对应数量后，在每一个内存子块的前8个字节中填充为一个指向下一空闲位置的指针。我们必须要保证这个指针刚好读取出来指针首地址+8的位置。因为这一段是指针。所以类型一定要是&lt;code&gt;obj*&lt;/code&gt;来保证刚好使用了8个字节。这也回答了为什么必须要单独建立一个&lt;code&gt;obj&lt;/code&gt;类型的结构体。看2的位置还有两个变量。如果我们指针类型使用allocator类型，这个类型的大小为8+8+4 = 20。这意味着指针要占用分割出来的内存子块的前20个字节。这样做是无意义的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;整体顺序是什么？首先看3。我们使用&lt;code&gt;malloc&lt;/code&gt;向系统申请出对应大小的内存。然后把返回值转换为&lt;code&gt;obj*&lt;/code&gt;类型（前面提到过原因）。之后赋值给&lt;code&gt;freestore&lt;/code&gt;和&lt;code&gt;p&lt;/code&gt;。然后，我们看4。我们把一大块内存分割成对应数量的小块。看5。这里里面的&lt;code&gt;(char*)p + size&lt;/code&gt;的意思是把指针后移对应的大小。此处我们应该举个例子。假设我们的size是20个字节。系统一共分配了100个字节。我们要切成五块。所以地址应该是&lt;code&gt;0x01~0x20, 0x21~0x40, 0x41~0x60, 0x61~0x80, 0x81~0x100&lt;/code&gt;。（0x00在主流机器上一般对应空指针）。&lt;/p&gt;
&lt;p&gt;我们p一开始指向&lt;code&gt;0x01&lt;/code&gt;。我们想让&lt;code&gt;p-&amp;gt;next&lt;/code&gt;指向&lt;code&gt;0x21&lt;/code&gt;应该怎么办。这里利用了之前讲的移动指针的时候指定一个具体的偏移量。我们先把p看成&lt;code&gt;char&lt;/code&gt;类型的指针，然后&lt;code&gt;+size&lt;/code&gt;。这里是&lt;code&gt;+20&lt;/code&gt;。这样的话就是指针&lt;code&gt;p&lt;/code&gt;指向的地址后移&lt;code&gt;20（1*20）&lt;/code&gt;位。这里如果不进行转型直接&lt;code&gt;+20&lt;/code&gt;的话会变成&lt;code&gt;+160（8*20）。&lt;/code&gt;因为指针&lt;code&gt;obj*&lt;/code&gt;指向的数据的大小是8。&lt;/p&gt;
&lt;p&gt;然后我们就得到了 &lt;code&gt;0x01+20 = 0x21&lt;/code&gt;也就是&lt;code&gt;p-&amp;gt;next&lt;/code&gt;的地址。然后我们需要让从&lt;code&gt;0x21&lt;/code&gt;开始的内存的前&lt;code&gt;8&lt;/code&gt;个字节为我们储存的指针。也就是我们需要让编译器仅读取&lt;code&gt;0x21&lt;/code&gt;开始的内存的前&lt;code&gt;8&lt;/code&gt;个字节。也就是让指针&lt;code&gt;p-&amp;gt;next&lt;/code&gt;类型为&lt;code&gt;obj&lt;/code&gt;类型。这样读取指针&lt;code&gt;p-&amp;gt;next&lt;/code&gt;储存的的数据的时候可以获得正确的数据。所以我们还要进行一个&lt;code&gt;(obj*)&lt;/code&gt;的转型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（22）-- 内存管理4</title><link>https://sterben-01.github.io/posts/OS25/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS25/</guid><pubDate>Fri, 03 Jun 2022 16:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存（22）-- 内存管理4&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;动态分区分配算法&lt;/h2&gt;
&lt;h3&gt;&lt;img src=&quot;/assets/blog_res/2022-06-03-OS25%20(copy).assets/QQ%E6%88%AA%E5%9B%BE20220603134540.png&quot; alt=&quot;QQ截图20220603134540&quot; /&gt;&lt;/h3&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（21）-- 内存管理3</title><link>https://sterben-01.github.io/posts/OS24/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS24/</guid><pubDate>Thu, 02 Jun 2022 16:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存（21）-- 内存管理3&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;内存空间的分配与回收&lt;/h2&gt;
&lt;h3&gt;连续分配管理方式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;连续分配指的是为用户进程分配的内存空间必须是连续的。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;单一连续分配&lt;/h4&gt;
&lt;p&gt;在单一连续分配方式中，内存被分为系统区和用户区。&lt;strong&gt;系统区&lt;/strong&gt;通常位于内存的低地址部分，用于存放操作系统相关数据。&lt;strong&gt;用户区&lt;/strong&gt;用于存放用户进程相关数据。内存中&lt;strong&gt;只能有一道用户程序&lt;/strong&gt;，用户程序独占整个用户区空间。&lt;/p&gt;
&lt;p&gt;**优点：**实现简单。无外部碎片。可以采用覆盖技术扩充内存。不一定需要采取内存保护（例：早期的PC操作系统MS-DOS )。&lt;/p&gt;
&lt;p&gt;**缺点：**只能用于单用户、单任务的操作系统中。有内部碎片。存储器利用率极低。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内部碎片指的是分配给某进程的内存区域中，如果有些部分没有用上，就是内部碎片。如下图的蓝色部分就是内部碎片。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS24.assets/QQ%E6%88%AA%E5%9B%BE20220602131413.png&quot; alt=&quot;QQ截图20220602131413&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;固定分区分配&lt;/h4&gt;
&lt;p&gt;将整个用户空间划分为若干个固定大小的连续分区，在每个分区中只装入一道作业，这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式。一旦划分完成，系统运行期间不可以再重新划分，即 不可更改分区个数和大小。所以又被称之为静态分区、&lt;/p&gt;
&lt;p&gt;固定分区分配分为&lt;strong&gt;分区大小相等&lt;/strong&gt;和&lt;strong&gt;分区大小不等&lt;/strong&gt;两种方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分区大小相等
&lt;ul&gt;
&lt;li&gt;缺乏灵活性，比如很小的程序会浪费空间，大程序无法装入系统。但是很适合用于用一台计算机控制多个相同对象的场合（比如:钢铁厂有n个相同的炼钢炉，就可把内存分为n个大小相等的区域存放n个炼钢炉控制程序)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分区大小不等
&lt;ul&gt;
&lt;li&gt;增加了灵活性，可以满足不同大小的进程需求。根据常在系统中运行的作业大小情况进行划分(比如:划分多个小分区、适量中等分区、少量大分区)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS24.assets/QQ%E6%88%AA%E5%9B%BE20220602131658.png&quot; alt=&quot;QQ截图20220602131658&quot; /&gt;&lt;/p&gt;
&lt;p&gt;**通常，操作系统需要建立一个数据结构 -- 分区说明表，来实现各个分区的分配与回收。每个表项对应一个分区，通常按分区大小排列。每个表项包括对应分区的大小、起始地址、状态（是否已分配）。**当某用户程序要装入内存时，由操作系统内核程序根据用户程序大小检索该表,从中找到一个能满足大小的、未分配的分区，将之分配给该程序，然后修改状态为“已分配”。&lt;/p&gt;
&lt;p&gt;**优点：**实现简单，无外部碎片。&lt;/p&gt;
&lt;p&gt;**缺点：**1 .当用户程序太大时，可能所有的分区都不能满足需求，此时不得不采用覆盖技术来解决,但这又会降低性能。&lt;/p&gt;
&lt;p&gt;​			2.会产生内部碎片，内存利用率低。内部碎片即我们每个分区大小都是8，现在要插入一个大小为6的。剩余的2就是内部碎片。&lt;/p&gt;
&lt;p&gt;​			3. 分区总数固定，限制并发执行的进程数目。&lt;/p&gt;
&lt;h4&gt;动态分区分配&lt;/h4&gt;
&lt;p&gt;动态分区分配又称为可变分区分配。这种分配方式不会预先划分内存分区，而是在进程装入内存时，根据进程的大小动态地建立分区，并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。（eg:假设某计算机内存大小为64MB，系统区8MB，用户区共56 MB….）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;系统要用什么样的数据结构记录内存的使用情况?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;空闲分区表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS24.assets/QQ%E6%88%AA%E5%9B%BE20220602132254.png&quot; alt=&quot;QQ截图20220602132254&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空闲分区链&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS24.assets/QQ%E6%88%AA%E5%9B%BE20220602132300.png&quot; alt=&quot;QQ截图20220602132300&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当很多个空闲分区都能满足需求时，应该选择哪个分区进行分配?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用特定的动态分区分配算法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何进行分区的分配与回收操作?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如何分配？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除表项，或 更改空闲分区起始地址（分配出去）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何回收？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果回收区的后面有一个相邻的空闲分区，则两个相邻空间合并。&lt;/li&gt;
&lt;li&gt;如果回收区的前后都有相邻的空闲分区，则将三个相邻空间合并。&lt;/li&gt;
&lt;li&gt;如果回收区的前后都没有相邻的空闲分区，则新增空闲分区表项。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;动态分区分配又称为可变分区分配。这种分配方式不会预先划分内存分区，而是在进程装入内存时，根据进程的大小动态地建立分区，并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。&lt;/p&gt;
&lt;p&gt;动态分区分配没有内部碎片，但是有外部碎片。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外部碎片，是指内存中的某些空闲分区由于太小而难以利用。如下图的蓝色部分&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS24.assets/QQ%E6%88%AA%E5%9B%BE20220602133026.png&quot; alt=&quot;QQ截图20220602133026&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果内存中空闲空间的总和本来可以满足某进程的要求，但由于进程需要的是一整块连续的内存空间，因此这些“碎片”不能满足进程的需求。可以通过紧凑（拼凑，Compaction）技术来解决外部碎片。&lt;/p&gt;
&lt;h2&gt;外部碎片(在分配单元间的未使用的内存)；内部碎片(在分配单元中未使用的内存)。这里的碎片指的是物理内存。&lt;/h2&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（19）-- 内存管理1</title><link>https://sterben-01.github.io/posts/OS22/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS22/</guid><pubDate>Thu, 02 Jun 2022 14:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存（19）-- 内存管理1&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;操作系统如何进行内存管理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;操作系统负责内存的分配和回收&lt;/li&gt;
&lt;li&gt;操作系统需要提供某种技术从逻辑上对内存空间进行扩充（虚拟内存）&lt;/li&gt;
&lt;li&gt;操作系统需要提供地址转换功能。负责程序的逻辑地址与物理地址的转换。&lt;/li&gt;
&lt;li&gt;操作系统需要提供内存保护功能。保证各个进程在各自的储存空间内运行，互不干扰。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;内存保护&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;方法一：在CPU中设置一对上、下限寄存器，存放进程的上、下限地址。进程的指令要访问某个地址时，CPU检查是否越界。&lt;/li&gt;
&lt;li&gt;方法二：采用重定位寄存器（又称基址寄存器）和界地址寄存器（又称限长寄存器）进行越界检查。重定位寄存器中存放的是进程的起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS22.assets/QQ%E6%88%AA%E5%9B%BE20220602100242.png&quot; alt=&quot;QQ截图20220602100242&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（20）-- 内存管理2</title><link>https://sterben-01.github.io/posts/OS23/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS23/</guid><pubDate>Thu, 02 Jun 2022 14:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存（20）-- 内存管理2&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;内存空间的扩充&lt;/h2&gt;
&lt;h3&gt;覆盖技术&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;这个非常像笔记c++内存管理12~13中用union联合体来优化next指针的操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;覆盖技术的思想：将程序分为多个段（多个模块)。常用的段常驻内存，不常用的段在需要时调入内存。内存中分为一个“固定区”和若干个“覆盖区”。需要常驻内存的段放在“固定区”中，调入后就不再调出（除非运行结束)。不常用的段放在“覆盖区”，需要用到时调入内存，用不到时调出内存。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS23.assets/QQ%E6%88%AA%E5%9B%BE20220602111417.png&quot; alt=&quot;QQ截图20220602111417&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;必须由程序员声明覆盖结构，操作系统自动完成覆盖。缺点：对用户不透明，增加了用户变成负担。覆盖技术只用于早期的操作系统中。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;交换技术 （中级调度）&lt;/h2&gt;
&lt;p&gt;交换（对换）技术的设计思想：内存空间紧张时，系统将内存中某些进程暂时换出外存，把外存中某些已具备运行条件的进程换入内存（进程在内存与磁盘间动态调度)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;暂时换出外存等待的进程状态为挂起状态（挂起态，suspend)。挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;应该在外存（磁盘）的什么位置保存被换出的进程?
&lt;ul&gt;
&lt;li&gt;在具有对换功能的操作系统中，通常把磁盘空间分为文件区和对换区两部分。文件区主要用于存放文件，主要追求存储空间的利用率，因此对文件区空间的管理采用离散分配方式;对换区空间只占磁盘空间的小部分，被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度，因此对换区空间的管理主要追求换入换出速度，因此通常对换区采用连续分配方式。总之，对换区的I/o速度比文件区的更快。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;什么时候应该交换?
&lt;ul&gt;
&lt;li&gt;交换通常在许多进程运行且内存吃紧时进行，而系统负荷降低就暂停。例如:发现许多进程运行时经常发生缺页，就说明内存紧张，此时可以换出一些进程。如果缺页率明显下降，就可以暂停换出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;应该换出哪些进程?
&lt;ul&gt;
&lt;li&gt;可优先换出阻塞进程;可换出优先级低的进程;为了防止优先级低的进程在被调入内存后很快又被换出，有的系统还会考虑进程在内存的驻留时间....&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;PCB会常驻内存，不会被换出至外存&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS23.assets/QQ%E6%88%AA%E5%9B%BE20220602112031.png&quot; alt=&quot;QQ截图20220602112031&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（14）-- 死锁</title><link>https://sterben-01.github.io/posts/OS17/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS17/</guid><pubDate>Thu, 02 Jun 2022 12:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（14）-- 死锁&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;死锁、饥饿、死循环的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;死锁：各进程互相等待对方手里的资源，导致&lt;strong&gt;多个进程&lt;/strong&gt;都阻塞，无法向前推进的现象。&lt;/li&gt;
&lt;li&gt;饥饿：由于长期得不到想要的资源，导致&lt;strong&gt;某个进程&lt;/strong&gt;无法向前推进的现象。比如:在短进程优先（SPF）算法中，若有源源不断的短进程到来，则长进程将一直得不到处理机，从而发生长进程“饥饿”。&lt;/li&gt;
&lt;li&gt;死循环：某进程执行过程中一直跳不出某个循环的现象。有时是因为程序逻辑 bug 导致的，有时是程序员故意设计的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS17.assets/QQ%E6%88%AA%E5%9B%BE20220602080019.png&quot; alt=&quot;QQ截图20220602080019&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;死锁产生的必要条件&lt;/h2&gt;
&lt;p&gt;产生死锁必须同时满足以下四个条件，只要其中任一条件不成立，死锁就不会发生。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;互斥条件：只有对必须互斥使用的资源的争抢才会导致死锁（如哲学家的筷子、打印机设备)。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的（因为进程不用阻塞等这种资源）。&lt;/li&gt;
&lt;li&gt;不剥夺条件：进程所获得的资源在未使用完之前，不能由其他进程强行夺走，只能主动释放。&lt;/li&gt;
&lt;li&gt;请求和保持条件：进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又被其他进程占有，此时请求进程被阻塞，但又对自己已有的资源保持不放。&lt;/li&gt;
&lt;li&gt;循环等待条件：存在一种进程资源的循环等待链，链中的每一个进程己获得的资源同时被下一个进程所请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;**注意！发生死锁时一定有循环等待，但是发生循环等待时未必死锁（循环等待是死锁的必要不充分条件)。**如果同类资源数大于1，则即使有循环等待也未必发生死锁。&lt;/p&gt;
&lt;h2&gt;什么时候会发生死锁&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;对系统资源的竞争。各进程对不可剥夺的资源（如打印机）的竞争可能引起死锁，对可剥夺的资源(CPU)的竞争是不会引起死锁的。&lt;/li&gt;
&lt;li&gt;进程推进顺序非法。请求和释放资源的顺序不当，也同样会导致死锁。例如，并发执行的进程P1、P2分别申请并占有了资源R1、R2，之后进程P1又紧接着申请资源R2，而进程P2又申请资源R1，两者会因为申请的资源被对方占有而阻塞，从而发生死锁。&lt;/li&gt;
&lt;li&gt;信号量的使用不当也会造成死锁。如生产者-消费者问题中，如果实现互斥的P操作在实现同步的P操作之前，就有可能导致死锁。(可以把互斥信号量、同步信号量也看做是一种抽象的系统资源)。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总之，对不可剥夺资源的不合理分配，可能导致死锁。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;死锁的处理策略&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;预防死锁：破坏死锁产生的四个必要条件中的一个或者几个。&lt;/li&gt;
&lt;li&gt;避免死锁：使用某种方法防止系统进入不安全状态。从而避免死锁。&lt;/li&gt;
&lt;li&gt;死锁的检测和解除：允许死锁的发生，不过操作系统会负责检测出死锁的发生，然后采取某种措施解除死锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS17.assets/QQ%E6%88%AA%E5%9B%BE20220602080921.png&quot; alt=&quot;QQ截图20220602080921&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（16）-- 避免死锁</title><link>https://sterben-01.github.io/posts/OS19/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS19/</guid><pubDate>Thu, 02 Jun 2022 12:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（16）-- 避免死锁&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;银行家算法和安全序列&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS19.assets/QQ%E6%88%AA%E5%9B%BE20220602082955.png&quot; alt=&quot;QQ截图20220602082955&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS19.assets/QQ%E6%88%AA%E5%9B%BE20220602083039.png&quot; alt=&quot;QQ截图20220602083039&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS19.assets/QQ%E6%88%AA%E5%9B%BE20220602083209.png&quot; alt=&quot;QQ截图20220602083209&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（15）-- 预防死锁</title><link>https://sterben-01.github.io/posts/OS18/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS18/</guid><pubDate>Thu, 02 Jun 2022 12:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（15）-- 预防死锁&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;破坏互斥条件&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;只有对必须互斥使用的资源的争抢才会导致死锁。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果把只能互斥使用的资源改造为允许共享使用，则系统不会进入死锁状态。比如: SPOOLing技术。操作系统可以采用SPOOLing技术把独占设备在逻辑上改造成共享设备。比如，用SPOOLing技术将打印机改造为共享设备...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS18.assets/QQ%E6%88%AA%E5%9B%BE20220602081446.png&quot; alt=&quot;QQ截图20220602081446&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点：并不是所有的资源都可以被改造成可共享使用的资源。并且为了系统安全，很多地方必须保护互斥性。因此很多时候无法破坏互斥资源&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;破坏不剥夺条件&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;不剥夺条件：进程所获得的资源在未使用完之前，不能由其他进程强行夺走，只能主动释放。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;方案一：当某个进程请求新的资源得不到满足时，它必须立即释放保持的所有资源，待以后需要再重新申请。也就是说，即使某些资源尚未使用完，也需要主动释放，从而破坏了不可剥夺条件。&lt;/p&gt;
&lt;p&gt;方案二：当某个进程需要的资源被其他进程所占有的时候，可以由操作系统协助，将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级（比如:剥夺调度方式，就是将处理机资源强行剥夺给优先级更高的进程使用)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现较为复杂。&lt;/li&gt;
&lt;li&gt;释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源，如CPU。&lt;/li&gt;
&lt;li&gt;反复地申请和释放资源会增加系统开销，降低系统吞吐量。&lt;/li&gt;
&lt;li&gt;若采用方案一，意味着只要暂时得不到某个资源，之前获得的那些资源就都需要放弃，以后再重新申请。如果一直发生这样的情况，就会导致进程饥饿。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;破坏请求和保持条件&lt;/h2&gt;
&lt;p&gt;请求和保持条件：进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又被其他进程占有，此时请求进程被阻塞，但又对自己已有的资源保持不放。&lt;/p&gt;
&lt;p&gt;可以采用静态分配方法，即进程在运行前一次申请完它所需要的全部资源，在它的资源未满足前，不让它投入运行。一旦投入运行后，这些资源就一直归它所有，该进程就不会再请求别的任何资源了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点：有些资源可能只需要用很短的时间，因此如果进程的整个运行期间都一直保持着所有资源，就会造成严重的资源浪费，资源利用率极低。另外，该策略也有可能导致某些进程饥饿。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;破坏循环等待条件&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS18.assets/QQ%E6%88%AA%E5%9B%BE20220602082636.png&quot; alt=&quot;QQ截图20220602082636&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS18.assets/QQ%E6%88%AA%E5%9B%BE20220602082722.png&quot; alt=&quot;QQ截图20220602082722&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（17）-- 死锁的检测和解除</title><link>https://sterben-01.github.io/posts/OS20/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS20/</guid><pubDate>Thu, 02 Jun 2022 12:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（17）-- 死锁的检测和解除&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;死锁的检测&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS20.assets/QQ%E6%88%AA%E5%9B%BE20220602083607.png&quot; alt=&quot;QQ截图20220602083607&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS20.assets/QQ%E6%88%AA%E5%9B%BE20220602083721.png&quot; alt=&quot;QQ截图20220602083721&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;死锁的解除&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS20.assets/QQ%E6%88%AA%E5%9B%BE20220602083820.png&quot; alt=&quot;QQ截图20220602083820&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS20.assets/QQ%E6%88%AA%E5%9B%BE20220602083842.png&quot; alt=&quot;QQ截图20220602083842&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（18）-- 内存</title><link>https://sterben-01.github.io/posts/OS21/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS21/</guid><pubDate>Thu, 02 Jun 2022 12:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-内存（18）-- 内存&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;逻辑地址和物理地址&lt;/h2&gt;
&lt;p&gt;物理地址：内存的真实地址。&lt;/p&gt;
&lt;p&gt;逻辑地址：相对于进程的起始地址而言的地址。&lt;/p&gt;
&lt;p&gt;举例：如果CPU把一个进程的起始地址分配在物理地址的100位。那么该进程如果有一条指令：往地址为79的储存单元中写入10。这条指令中的地址为逻辑地址。所以对应着物理地址的179位。&lt;/p&gt;
&lt;h2&gt;物理地址和逻辑地址的装换&lt;/h2&gt;
&lt;h3&gt;绝对装入&lt;/h3&gt;
&lt;p&gt;绝对装入：在编译时，如果知道程序将放到内存中的哪个位置，编译程序将产生绝对地址的目标代码。装入程序按照装入模块中的地址，将程序和数据装入内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;绝对装入仅适用于单道程序环境&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;静态重定位&lt;/h3&gt;
&lt;p&gt;静态重定位：又称可重定位装入。编译、链接后的装入模块的地址都是从0开始的，指令中使用的地址、数据存放的地址都是相对于起始地址而言的逻辑地址。可根据内存的当前情况，将装入模块装入到内存的适当位置。装入时对地址进行“重定位”，将逻辑地址变换为物理地址（地址变换是在装入时一次完成的)。&lt;/p&gt;
&lt;p&gt;举例：如果装入的起始物理地址是100，则所有的地址相关参数都+100。比如把 往地址为79的储存单元中写入10 指令替换为 往地址为179的储存单元中写入10&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态重定位的特点是在一个作业装入内存时，必须分配其要求的全部内存空间，如果没有足够的内存，就不能装入该作业。作业一旦进入内存后，在运行期间就不能再移动，也不能再申请内存空间。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;动态重定位&lt;/h3&gt;
&lt;p&gt;动态重定位:又称动态运行时装入。编译、链接后的装入模块的地址都是从0开始的。装入程序把装入模块装入内存后，并不会立即把逻辑地址转换为物理地址，而是把地址转换推迟到指令真正要执行时才进行。因此装入内存后所有的地址依然是逻辑地址。这种方式需要一个重定位寄存器的支持。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;采用动态重定位的时候允许程序在内存中发生移动。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS21.assets/QQ%E6%88%AA%E5%9B%BE20220602095001.png&quot; alt=&quot;QQ截图20220602095001&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS21.assets/QQ%E6%88%AA%E5%9B%BE20220720035230.png&quot; alt=&quot;QQ截图20220720035230&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;从写程序到程序运行&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS21.assets/QQ%E6%88%AA%E5%9B%BE20220602095116.png&quot; alt=&quot;QQ截图20220602095116&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;链接的三种方式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-06-02-OS21.assets/QQ%E6%88%AA%E5%9B%BE20220602095635.png&quot; alt=&quot;QQ截图20220602095635&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++内存管理- 9~11</title><link>https://sterben-01.github.io/posts/memory3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory3/</guid><pubDate>Tue, 31 May 2022 09:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++内存管理 - 9~11&lt;/h1&gt;
&lt;h1&gt;此笔记需要等待更新和确认。&lt;/h1&gt;
&lt;h2&gt;重载 operator new， operator delete&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory3.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220531035654.png&quot; alt=&quot;微信图片_20220531035654&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意红色的是较为常见的重载成员函数中的operator new。绿色的是不常见的重载全局函数的operator new&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;重载全局operator new， operator delete&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory3.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220531040706.png&quot; alt=&quot;微信图片_20220531040706&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一定要注意，全局的&lt;code&gt;::operator new&lt;/code&gt;下面是&lt;code&gt;malloc&lt;/code&gt;。全局的&lt;code&gt;::operator delete&lt;/code&gt;下面是&lt;code&gt;free&lt;/code&gt;。所以全局&lt;code&gt;::operator new&lt;/code&gt;不构造对象，全局&lt;code&gt;::operator delete&lt;/code&gt;也不析构对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;p&gt;重载&lt;code&gt;operator new&lt;/code&gt;的时候一定要注意 &lt;strong&gt;它的返回类型必须是&lt;code&gt;void*&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;请注意，任何指针都可以赋值给&lt;code&gt;void&lt;/code&gt;指针。不需要转换。只获得变量/对象地址而不获得大小。 如以下代码：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void* test(int* a){
    *a = 4;
    return a;
}
int main(){
    int s = 1;
    int* b = &amp;amp;s;
    test(b);
    cout &amp;lt;&amp;lt; s &amp;lt;&amp;lt; endl;
}
//输出结果为4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory3.assets/QQ%E6%88%AA%E5%9B%BE20220531044057-16539902288687.png&quot; alt=&quot;QQ截图20220531044057&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;重载局部（某个类内）operator new， operator delete&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory3.assets/QQ%E6%88%AA%E5%9B%BE20220531041338.png&quot; alt=&quot;QQ截图20220531041338&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;注意重载局部operator new， operator delete必须要设置为静态函数&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注意重载局部operator new， operator delete必须要设置为静态函数。因为为对象分配内存的时候是正在创建对象，而没有对象无法调用类非静态成员。所以为了用类的函数进行内存分配，必须要把new和delete设置为类的静态函数这样可以在没有创建对象之前就调用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**任何类内的内存分配函数必须要声明为静态函数 static **&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态成员函数即使在类对象不存在的情况下也能被调用，静态函数只要使用类名加范围解析运算符 :: 就可以访问。new对象时，对象还没有，设置成类非静态成员函数是没意思的，隐藏的this指针无从下手，所以要声明为static函数。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此时，如果想使用全局的&lt;code&gt;operator new&lt;/code&gt;则需要加作用域访问运算符&lt;code&gt;:&lt;/code&gt;指定全局作用域&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这里多提一嘴。看一下这个的&lt;a href=&quot;https://blog.csdn.net/K346K346/article/details/49538975&quot;&gt;内存池实现&lt;/a&gt; 和 &lt;a href=&quot;https://blog.csdn.net/baidu_41388533/article/details/105648119&quot;&gt;这个&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;more effective c++ 条款8 了解new operator 和 operator new 的区别&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;注意，重载的类内operator new 和 delete是在我们使用&lt;code&gt;new&lt;/code&gt; （这个&lt;code&gt;new&lt;/code&gt;叫做 new operator）的时候，编译器在&lt;code&gt;new&lt;/code&gt;的底下会调用我们重载的operator new。我们本身如果直接调用&lt;code&gt;operator new&lt;/code&gt;不会调用构造函数，因为我们此时不能手动调用对象初始化所必须的构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;new&lt;/code&gt; operator会执行底层的&lt;code&gt;operator new&lt;/code&gt;，然后如memory1中提到的帮助我们转型和构造。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;delete&lt;/code&gt; operator会执行底层的&lt;code&gt;operator delete&lt;/code&gt;，然后如memory1中提到的帮助我们析构。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;所以，如果想分配内存并构造对象，则依旧使用&lt;code&gt;new&lt;/code&gt;。如果只想分配内存，不构造对象，则可以手动调用&lt;code&gt;operator new&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;所以，直接调动全局的&lt;code&gt;::operator new&lt;/code&gt;不会调用构造函数。因为它的下面只是&lt;code&gt;malloc&lt;/code&gt;。直接调用全局的&lt;code&gt;::operator delete&lt;/code&gt;不会调用析构函数。因为它的下面只是&lt;code&gt;free&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;顺便一提，根据标准，针对单&lt;/strong&gt;对象或数组的分配函数（也就是局部&lt;code&gt;operator new&lt;/code&gt; 和 &lt;code&gt;operator new[]&lt;/code&gt;）对这些函数不需要使用关键词 &lt;code&gt;static&lt;/code&gt;。不管是否显式使用（声明分配函数为静态），分配函数都是静态成员函数。&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory3.assets/QQ%E6%88%AA%E5%9B%BE20221217071624.png&quot; alt=&quot;QQ截图20221217071624&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;重载 operator new() delete()。这个地方指的是重载placement new&lt;/h3&gt;
&lt;p&gt;重载operator new() 的时候一定要注意，有多个版本的时候每一个重载版本都要有&lt;strong&gt;独特的不一样的参数列表&lt;/strong&gt;。而且参数列表的第一个参数必须是&lt;code&gt;size_t&lt;/code&gt; 剩下的参数必须不一样。 而且 &lt;strong&gt;它的返回类型必须是void&lt;/strong&gt;*，并不必须重载operator delete()。&lt;strong&gt;查看discord频道笔记。关键字new()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;记住笔记二中说的，placement new 是operator new的一种特殊形式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory3.assets/QQ%E6%88%AA%E5%9B%BE20220531042442.png&quot; alt=&quot;QQ截图20220531042442&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++内存管理- 12 ~ 13</title><link>https://sterben-01.github.io/posts/memory4/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory4/</guid><pubDate>Tue, 31 May 2022 09:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++内存管理 - 12~13&lt;/h1&gt;
&lt;h1&gt;此笔记需要等待更新和确认。&lt;/h1&gt;
&lt;h2&gt;Per-class allocator - 自己的内存池（1）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory4.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220531051103.png&quot; alt=&quot;微信图片_20220531051103&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;new char[chunk]是因为char是1个字节。举例，char[100]就是100字节。所以char[chunk]就是分配了对应大小的内存&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;实现过程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;预申请一个内存区chunk，将内存中按照对象大小划分成多个内存块block。&lt;/li&gt;
&lt;li&gt;维持一个&lt;strong&gt;空闲内存块链表&lt;/strong&gt;，通过指针相连，标记头指针为第一个空闲块。&lt;/li&gt;
&lt;li&gt;每次新申请一个对象的空间，则将该内存块从空闲链表中去除，更新空闲链表头指针。&lt;/li&gt;
&lt;li&gt;每次释放一个对象的空间，则重新将该内存块加到空闲链表头。&lt;/li&gt;
&lt;li&gt;如果一个内存区占满了 &lt;code&gt;if(!freestore)&lt;/code&gt;，则新开辟一个内存区，维持一个内存区的链表，同指针相连，头指针指向最新的内存区，新的内存块从该区内重新划分和申请。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory4.assets/QQ%E6%88%AA%E5%9B%BE20220531054937.png&quot; alt=&quot;QQ截图20220531054937&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更正，上一张图的第三部分的第一行。&lt;s&gt;设置释放节点的下一个节点为当前链表头&lt;/s&gt; 更正为 设置释放节点的下一个节点为原本的链表头。也就是让释放的内存区块和原本链表头连接上。然后让释放的内存区块成为新的链表头。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Per-class allocator - 自己的内存池（2）- 改进next指针&lt;/h2&gt;
&lt;p&gt;通过上面的实现，我们发现每一个区块都要维护一个自己的next指针还是有一定开销的。我们能不能把这个next指针优化掉呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们要引入一个概念，union联合体&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;union简而言之就是在union结构体中的变量，同一时刻只可以有一个变量有值。而且union结构体的大小是结构体中最大的那个元素的大小。也就是说，union结构体中变量是覆盖的，也就是共享一块内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;底层内存分配几乎都是用使用union联合体来节约空间。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为什么我们会想到使用这种方法呢？我们发现next指针只有三种情况会被用到：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;第一次分配对象，建立整个&lt;code&gt;freeStore&lt;/code&gt;的链表的时候，初始化各个实例的next指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次给申请者一个实例时，要修改&lt;code&gt;freeStore = freeStore-&amp;gt;next&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次归还一个实例时，链表插入操作要用到&lt;code&gt;next&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;简而言之，当区块被分配出去实际储存数据的时候，是不需要next指针的。只有区块处于空闲状态时或者将要处于空闲状态时才需要。&lt;strong&gt;也就是说区块的next指针和区块实际分配的内容是互斥的，两者不会同时存在。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样就是说我们可以在一块内存中，如果被分配出去了，那就将这块内存全部用来储存对象，如果没有被分配出去或者是归还了就用来储存next指针。&lt;/p&gt;
&lt;p&gt;我们说过了union的大小是结构体中最大的部分的大小。举个例子，如果有这样的union结构体&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;union test{
	int a;
	int * next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个union结构体会占用8字节(64位操作系统)。当分配出去的时候，8个字节用来储存对象。当空闲的时候，前四个字节用来储存指针。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这样的指针我们叫做嵌入式指针&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-31-memory4.assets/QQ%E6%88%AA%E5%9B%BE20220531114027.png&quot; alt=&quot;QQ截图20220531114027&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;union 和 类型截断 和 大端序 小端序&lt;/h1&gt;
&lt;p&gt;intel的机器是小端序。&lt;/p&gt;
&lt;p&gt;大端序就是假如输入是0x01020304，则保存也是0x01020304&lt;/p&gt;
&lt;p&gt;小端序就是反过来。变成0x04030201。&lt;/p&gt;
&lt;p&gt;所以类型截断的时候，比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;union test{
    long long a;
    short b;
}
int main(){
    test t;
    t.a = INT32_MAX;
    cout &amp;lt;&amp;lt; b &amp;lt;&amp;lt; endl; //输出-1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;long long&lt;/code&gt;是64字节，又是有符号数。所以是1个0和63个1。&lt;/p&gt;
&lt;p&gt;由于union的大小是结构中最大的数据的大小。所以现在union内存中是0111....1 一共64位。&lt;/p&gt;
&lt;p&gt;由于是小端序，也就是起始地址是我们的低位。所以16进制0xfdffff....f在储存中会变成0xffff....fd。所以会从低地址开始截断。也就是会截取出16个1。自然输出就是-1。&lt;/p&gt;
</content:encoded></item><item><title>C++内存管理- 7~8</title><link>https://sterben-01.github.io/posts/memory2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory2/</guid><pubDate>Mon, 30 May 2022 00:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++内存管理 - 7~8&lt;/h1&gt;
&lt;h1&gt;此笔记需要等待更新和确认。&lt;/h1&gt;
&lt;h2&gt;array new 和 array delete&lt;/h2&gt;
&lt;p&gt;用new和delete对数组进行操作时，会调用对应长度的次数的构造/析构函数 并分配/释放对应大小的内存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Complex* pca = new Complex[3]; //这里会调用三次构造函数
delete[] pca; //这里会调用三次析构函数。三次会指向不同的对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory2.assets/QQ%E6%88%AA%E5%9B%BE20220530022057.png&quot; alt=&quot;QQ截图20220530022057&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意 用new创建array的时候，我们无法使用构造函数对array进行赋值。所以这里默认调用的是无参构造函数。也所以我们必须设置默认无参构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory2.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220530023335.png&quot; alt=&quot;微信图片_20220530023335&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;构造从前到后，析构从后往前&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;operatior new&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;operator new&lt;/code&gt; 有三种形式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(1) void* operator new (std::size_t size);
(2) void* operator new (std::size_t size, const std::nothrow_t&amp;amp; nothrow_value) noexcept;
(3) void* operator new (std::size_t size, void* ptr) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;第一种形式分配size字节的存储空间，如果成功的话返回一个非空指针，指向分配空间第一个字节。如果失败的话，会抛出bad_alloc异常。&lt;/li&gt;
&lt;li&gt;第二种形式和第一种一样，差别在于，如果失败的话，不抛出异常，而是返回一个null指针。&lt;/li&gt;
&lt;li&gt;第三种形式只是返回ptr指针，并不分配内存空间。这里的ptr应该指向先前已经分配好的空间，这里的new调用对象的构造函数，在ptr指向的内存空间构造对象或对象数组。ptr指向的内存只要不释放，可以重复使用。所以这种用法一般在对象池或内存池实现中使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意, 此处提供的参数都是第二参数. 因为第一参数 size已经由编译器自动处理.&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;比如 placement new 的 &lt;code&gt;T* tptr = new(buf) T;&lt;/code&gt; 此处的&lt;code&gt;buf&lt;/code&gt; 是第二参数.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;placement new&lt;/h2&gt;
&lt;p&gt;placement new 的作用是在已有的内存区块上创建对象。也就是仅仅创建对象而不分配内存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object * p = new (address) ClassConstruct(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;address&lt;/code&gt;就是我们指定的内存首地址。&lt;/p&gt;
&lt;h3&gt;注意事项&lt;/h3&gt;
&lt;p&gt;（1）&lt;strong&gt;用定位放置new操作，既可以在栈(stack)上生成对象，也可以在堆（heap）上生成对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（2）使用语句&lt;code&gt;A* p=new (mem) A;&lt;/code&gt;定位生成对象时，指针&lt;code&gt;p&lt;/code&gt;和数组名&lt;code&gt;mem&lt;/code&gt;指向同一片存储区。所以，与其说定位放置new操作是申请空间，还不如说是利用已经请好的空间，真正的申请空间的工作是在此之前完成的。&lt;/p&gt;
&lt;p&gt;（3）使用语句&lt;code&gt;A* p=new (mem) A;&lt;/code&gt;定位生成对象时，会自动调用类&lt;code&gt;A&lt;/code&gt;的构造函数，但是由于对象的空间不会自动释放（对象实际上是借用别人的空间），所以必须显式的调用类的析构函数，如&lt;code&gt;p-&amp;gt;~A()&lt;/code&gt;。这一条主要的理解是：你&lt;code&gt;delete&lt;/code&gt;的时候&lt;code&gt;delete&lt;/code&gt;的一定是&lt;code&gt;mem&lt;/code&gt;。但是&lt;code&gt;delete mem&lt;/code&gt;并不会调用你&lt;code&gt;A&lt;/code&gt;的析构函数。&lt;/p&gt;
&lt;p&gt;（4）万不得已才使用placement new，只有当你真的在意对象在内存中的特定位置时才使用它。例如，你的硬件有一个内存映像的I/O记时器设备，并且你想放置一个Clock对象在哪那个位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory2.assets/QQ%E6%88%AA%E5%9B%BE20220530233747.png&quot; alt=&quot;QQ截图20220530233747&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在这张图里，侯捷老师使用了&lt;code&gt;operator new&lt;/code&gt;的第三种形式， 所以这里等同于&lt;code&gt;placement new&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可以理解为placement new 是operator new的一种特殊形式&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;placement new 在代码层面等同于 &lt;code&gt;new(p)&lt;/code&gt; 或 &lt;code&gt;::operator new(size, void*)&lt;/code&gt;&lt;/h3&gt;
&lt;h2&gt;为什么new delete 和 new[] delete[]需要配对使用？&lt;/h2&gt;
&lt;p&gt;我们经常要用到动态分配一个数组，也许是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string *psa = new string[10];      //array of 10 empty strings
int *pia = new int[10];           //array of 10 uninitialized ints
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面在申请一个数组时都用到了 &lt;code&gt;new []&lt;/code&gt; 这个表达式来完成，按照我们上面讲到的 new 和 delete 知识，第一个数组是 string 类型，分配了保存对象的内存空间之后，将调用 string 类型的默认构造函数依次&lt;strong&gt;初始化数组中每个元素&lt;/strong&gt;；第二个是申请具有内置类型的数组，分配了存储 10 个 int 对象的内存空间，但&lt;strong&gt;并没有初始化&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们在上一篇笔记中提到了。对于new关键字对于&lt;strong&gt;数组&lt;/strong&gt;加括号和不加括号的区别&lt;/p&gt;
&lt;p&gt;​	1.对于自定义类型数组来说没有区别，都是自动调用其默认构造函数来初始化&lt;/p&gt;
&lt;p&gt;​	2.对于原子（内置）类型来说加括号会初始化，而不加括号则不会初始化&lt;/p&gt;
&lt;p&gt;所以这里使用demo的时候是调用了默认构造函数并初始化，但是int不仅没有调用构造函数，也没有初始化。&lt;/p&gt;
&lt;p&gt;如果我们想释放空间了，可以用下面两条语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delete [] psa;
delete [] pia;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;都用到 &lt;code&gt;delete []&lt;/code&gt; 表达式，注意这地方的 [] 一般情况下不能漏掉！我们也可以想象这两个语句分别干了什么：第一个对 10 个 string 对象分别调用析构函数，然后再释放掉为对象分配的所有内存空间；第二个因为是内置类型不存在析构函数，直接释放为 10 个 int 型分配的所有内存空间。&lt;/p&gt;
&lt;p&gt;这里对于第一种情况就有一个问题了：&lt;strong&gt;我们如何知道 psa 指向对象的数组的大小？怎么知道调用几次析构函数？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意这里的做法因编译器而异。有多种实现方式。这仅仅是一种实现方式并拿来举例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个问题直接导致我们需要在 new [] 一个对象数组时，需要保存数组的维度，C++ 的做法是在分配数组空间时多分配了 4 个字节的大小，专门保存数组的大小，在 delete [] 时就可以取出这个保存的数，就知道了需要调用析构函数多少次了。&lt;/p&gt;
&lt;p&gt;还是用图来说明比较清楚，我们定义了一个类 &lt;code&gt;Demo&lt;/code&gt;，但不具体描述类的内容，这个类中有显示的构造函数、析构函数等。那么 当我们调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Demo *p = new Demo[3];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时需要做的事情如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory2.assets/QQ%E6%88%AA%E5%9B%BE20220530030154.png&quot; alt=&quot;QQ截图20220530030154&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这个图中我们可以看到申请时在数组对象的上面还多分配了 4 个字节用来保存数组的大小，但是最终返回的是对象数组的指针，而不是所有分配空间的起始地址。&lt;/p&gt;
&lt;p&gt;这样的话，释放就很简单了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delete []p;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要注意的两点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用析构函数的次数是从数组对象指针前面的 4 个字节中取出；&lt;/li&gt;
&lt;li&gt;传入 &lt;code&gt;operator delete[]&lt;/code&gt; 函数的参数不是数组对象的指针 p，而是 p的值减 4。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是我们如果在内置类型数组中不配对使用呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *pia = new int[10];
delete pia;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这肯定是没问题的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先。指针本身在栈上的。但是指向了堆上的区块地址。数组指针本身不清楚数组大小。但是指向的堆有相应的管理能力。在申请释放的时候，堆的区块自己会维护一个大小。所以对于数组指针来说 直接进行&lt;code&gt;delete&lt;/code&gt;会释放掉整个数组所占的内存。具体堆是如何做的，不同的编译器，操作系统会有不同的实现。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其次。因为我们使用的是内置类型，数组中储存的对象并没有在堆上分配额外的内存空间。所以不需要使用析构函数来释放对象中分配过的内存。直接调用&lt;code&gt;delete&lt;/code&gt;不会出现问题。&lt;/p&gt;
&lt;p&gt;但是，用第一张图的string为例。我们如果使用了delete而不是delete[]会有什么问题呢？首先，delete会释放掉数组的空间。然后只执行一次析构函数。执行的这次的析构函数会释放掉数组里面储存的&lt;strong&gt;string对象分配的空间&lt;/strong&gt;。可以理解为数组中存的元素自己本身又开辟了额外的空间。这样的话我们其实仅仅释放了数组的空间和一个string对象新开辟的空间。那么剩下两个string对象新开辟的空间将会因为没有调用析构函数而无法释放。&lt;/p&gt;
&lt;p&gt;上面我提到了在 &lt;code&gt;new []&lt;/code&gt; 时多分配 4 个字节的缘由，因为析构时需要知道数组的大小，但如果不调用析构函数呢（如内置类型，这里的 int 数组）？我们在 &lt;code&gt;new []&lt;/code&gt; 时就没必要多分配那 4 个字节， delete [] 时直接到第二步释放为 int 数组分配的空间。如果这里使用 &lt;code&gt;delete pia;&lt;/code&gt;那么将会调用 &lt;code&gt;operator delete&lt;/code&gt; 函数，传入的参数是分配给数组的起始地址，所做的事情就是释放掉这块内存空间。不存在问题的。&lt;/p&gt;
&lt;p&gt;这里说的使用 &lt;code&gt;new []&lt;/code&gt; 用 delete 来释放对象的提前是：对象的类型是内置类型或者是无自定义的析构函数的类类型！&lt;/p&gt;
&lt;p&gt;我们看看如果是带有自定义析构函数的类类型，用 &lt;code&gt;new []&lt;/code&gt; 来创建类对象数组，而用 delete 来释放会发生什么？用上面的例子来说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A *pAa = new class A[3];
delete pAa;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么 &lt;code&gt;delete pAa;&lt;/code&gt; 做了两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用一次 pAa 指向的对象的析构函数；&lt;/li&gt;
&lt;li&gt;调用 &lt;strong&gt;&lt;code&gt;operator delete(pAa);&lt;/code&gt;&lt;/strong&gt; 释放内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;显然，这里只对数组的第一个类对象调用了析构函数，后面的两个对象均没调用析构函数，如果类对象中申请了大量的内存需要在析构函数中释放，而你却在销毁数组对象时少调用了析构函数，这会造成内存泄漏。&lt;/p&gt;
&lt;p&gt;上面的问题你如果说没关系的话，那么第二点就是致命的了！直接释放 pAa 指向的内存空间，这个总是会造成严重的段错误，程序必然会奔溃！因为分配的空间的起始地址是 pAa 指向的地方减去 4 个字节的地方。你应该传入参数设为那个地址！（因为多了一个int的大小记录数组大小）&lt;/p&gt;
&lt;p&gt;同理，你可以分析如果使用 new 来分配，用 &lt;code&gt;delete []&lt;/code&gt; 来释放会出现什么问题？是不是总会导致程序错误？&lt;/p&gt;
&lt;p&gt;总的来说，记住一点即可：&lt;strong&gt;new/delete、new[]/delete[] 要配套使用总是没错的！&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++内存管理- 1~6</title><link>https://sterben-01.github.io/posts/memory1/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/memory1/</guid><pubDate>Mon, 30 May 2022 00:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++内存管理 - 1~6&lt;/h1&gt;
&lt;h1&gt;此笔记需要等待更新和确认。最好搭配深度探索6.1一起看&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory1.assets/QQ%E6%88%AA%E5%9B%BE20220529201640.png&quot; alt=&quot;QQ截图20220529201640&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory1.assets/QQ%E6%88%AA%E5%9B%BE20220529201640-16538734497572.png&quot; alt=&quot;QQ截图20220529201640&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory1.assets/QQ%E6%88%AA%E5%9B%BE20220529201724.png&quot; alt=&quot;QQ截图20220529201724&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;我们由上图可以看出malloc和free是比new delete更底层的东西。malloc和free属于c函数，而new和delete属于c++表达式。&lt;/h3&gt;
&lt;h2&gt;内存分配方式：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory1.assets/QQ%E6%88%AA%E5%9B%BE20220529202032-16538736739556.png&quot; alt=&quot;QQ截图20220529202032&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;malloc&lt;/code&gt;分配特定大小的内存。使用&lt;code&gt;malloc&lt;/code&gt;会返回一个未定类型的指针。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型决定了指向的内存空间的大小. 编译器依靠指针类型来决定到底读取到哪一个字节。因为你只能确定开始地址，如果不给指针类型，编译器不知道你要读取1个字节还是100个字节为止&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;new&lt;/code&gt;分配特定大小的内存。使用&lt;code&gt;new&lt;/code&gt;需要指定其类型。一般后接class名称指定类型。返回一个该类型的指针。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	&lt;strong&gt;注意： 如代码&lt;code&gt;A *a = new A()&lt;/code&gt; ，此处不仅仅分配了内存，而且调用了构造函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory1.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220530014049.png&quot; alt=&quot;微信图片_20220530014049&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;::operator new &lt;/code&gt;分配特定大小的内存。&lt;code&gt;::operator new&lt;/code&gt;其实就是包装过的&lt;code&gt;malloc&lt;/code&gt;。一样返回一个未定类型指针。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	&lt;strong&gt;记住。free和delete释放的是指针指向的地址，而不是指针本身。指针本身依旧存在。因此，指针会指向释放过的地址，可能引发错误。所以记得要将指针本身置空。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在如如下代码中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A *a = new A();
delete a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;delete&lt;/code&gt; 会先调用析构函数，再释放内存&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-29-memory1.assets/QQ%E6%88%AA%E5%9B%BE20220530014703.png&quot; alt=&quot;QQ截图20220530014703&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;new的原理&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用operator new函数申请空间
&lt;ol&gt;
&lt;li&gt;operator new还会调用malloc&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会用static_cast把malloc返回的void*强转为对应类型。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在申请的空间上执行构造函数（通过这个指针调用构造函数），完成对象的构造&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要看到重载&lt;code&gt;operator new&lt;/code&gt;返回&lt;code&gt;void*&lt;/code&gt;就懵逼了。因为&lt;code&gt;new expression&lt;/code&gt;不仅会寻找对应的&lt;code&gt;operator new()&lt;/code&gt;分配内存，还要构造对象并返回对应类型的指针。后面的两个步骤是编译器做的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;delete的原理&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在空间上执行析构函数，完成对象中资源的清理工作&lt;/li&gt;
&lt;li&gt;调用operator delete函数释放对象的空间
&lt;ol&gt;
&lt;li&gt;operator delete还会调用free&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;delete会发生隐式类型转换！！！详细看一下智能指针章节的最后一块&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;.....对于第一种（非数组）形式，&lt;em&gt;表达式&lt;/em&gt; 必须是指向对象类型的指针或可以按语境隐式转换到这种指针的类类型.........&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;new T[N]的原理&lt;/strong&gt; 这个叫array new&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用operator new[]函数，在operator new[]中实际调用operator new函数完成N个对象空间的申请&lt;/li&gt;
&lt;li&gt;在申请的空间上执行N次构造函数&lt;/li&gt;
&lt;li&gt;array new 的时候 内存块会记录你这个具体[]括号里面的是几。看一下下一篇。&lt;/li&gt;
&lt;li&gt;进阶：一般来说，如果new一个数组，数组里面储存的对象没有定义默认构造函数，则不需要调用对象的构造函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;delete[] T的原理&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在释放的对象空间上执行N次析构函数，完成N个对象中资源的清理&lt;/li&gt;
&lt;li&gt;调用operator delete[]释放空间，实际在operator delete[]中调用operator delete来&lt;/li&gt;
&lt;li&gt;进阶：一般来说，如果new一个数组，数组里面储存的对象没有定义默认析构函数，则不需要调用对象的析构函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;malloc/free和new/delete的区别:&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;共同点是:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;都是从堆上申请空间，并且需要用户手动释放。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不同的地方是:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;malloc和free是函数，new和delete是操作符（operator new(), operator delete())&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;malloc申请的空间不会初始化，new可以初始化&lt;/li&gt;
&lt;li&gt;malloc申请空间时，需要手动计算空间大小并传递，new只需在其后跟上空间的类型即可，因为他帮你包了一层&lt;/li&gt;
&lt;li&gt;malloc的返回值为void*, 在使用时必须强转，new不需要，因为new后跟的是空间的类型 ，因为他帮你包了一层&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;malloc申请空间失败时，返回的是NULL，因此使用时必须判空，new不需要，但是new需要捕获异常&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;申请自定义类型对象时，malloc/free只会开辟与销毁空间，不会调用构造函数与析构函数，而new在申请空间后会调用构造函数完成对象的初始化，delete在释放空间前会调用析构函数完成空间中资源的清理&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;new/delete比malloc和free的效率稍微低点，因为new/delete的底层封装了malloc/free&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;new申请的内存，能用free吗？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可以，new对应delete不可以张冠李戴。（malloc/free,new/delete必需配对使用）&lt;/li&gt;
&lt;li&gt;对于非内部数据类型的对象而言，光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数，对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符，不在编译器控制权限之内，不能够把执行构造函数和析构函数的任务强加于malloc/free。因此c++语言需要一个能完成动态内存分配和初始化工作的运算符new，以及一个能完成清理与释放内存工作的&lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;注意不要double-free&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对一个已经调用过&lt;code&gt;delete&lt;/code&gt;或&lt;code&gt;free&lt;/code&gt;清理过的区块再次调用&lt;code&gt;delete&lt;/code&gt;或&lt;code&gt;free&lt;/code&gt;会造成double-free错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是对一个空指针调用&lt;code&gt;delete&lt;/code&gt;或&lt;code&gt;free&lt;/code&gt;则不会发生任何事情。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 &lt;em&gt;表达式&lt;/em&gt; 求值为空指针值，那么不会调用析构函数，且可能会也可能不会调用解分配函数（这是未指明的），但默认的解分配函数保证在传递了空指针时不做任何事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此处空指针指的是如 &lt;code&gt;int* p = nullptr&lt;/code&gt;这样的显式置为0的指针&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;malloc的 cookie&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;有个问题是free怎么判断传给它的指针是指向能被析构的还是未定义内存&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个问题的答案是，cookie不仅会保存分配内存的大小，而且会用最后一个bit保存是否已分配。如果已分配，就是能被析构的。就是1，未分配也就是不能析构的就是0.&lt;/p&gt;
&lt;h2&gt;cookie是上下各4字节（一般情况）。&lt;/h2&gt;
&lt;p&gt;如果malloc8字节大小，则会有0x0-0x4的上cookie，0x4-0x12的数据, 0x12-0x15的下cookie。最后返回的是0x4也就是指向起始可用内存的指针。&lt;/p&gt;
&lt;p&gt;free释放时候看起始地址-4。看cookie。如果是已分配的则只需要把cookie最后一位修改为未分配即可。&lt;/p&gt;
&lt;p&gt;同时可以看这段内存的前一个分配内存的下cookie和下一个分配内存的上cookie。如果还有一些已经处于未分配的内存（已释放），则可以合并。&lt;/p&gt;
&lt;p&gt;CSAPP 9.9.6~9.9.11&lt;/p&gt;
&lt;h2&gt;内存延迟分配&lt;/h2&gt;
&lt;p&gt;Linux内核在用户申请内存的时候，只是给它分配了一个线性区（也就是虚存），并没有分配实际物理内存；只有当用户使用这块内存的时候，内核才会分配具体的物理页面给用户，这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区（也就是虚存），找到其所对应的物理页面，将其全部释放的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char *p=malloc(2048)    //这里只是分配了虚拟内存2048，并不占用实际内存。 
strcpy(p,&quot;123&quot;)			//分配了物理页面，虽只使用了3个字节，但内存还是为它分配了2048字节的物理内存。
free(p) 				//通过虚拟地址，找到其所对应的物理页面，释放物理页面，释放虚拟内存(线性区)。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道用户的进程和内核是运行在不同的级别，进程与内核之间的通讯是通过系统调用来完成的。进程在申请和释放内存，主要通过brk,sbrk,mmap,unmmap这几个系统调用，传递的参数主要是对应的虚拟内存。&lt;/p&gt;
&lt;p&gt;注意一点，在进程只能访问虚拟内存，它实际上是看不到内核物理内存的使用，这对于进程是完全透明的。&lt;/p&gt;
&lt;p&gt;也就是说，&lt;strong&gt;程序申请和操作的内存都是在虚拟内存上的，包括堆(heap)、栈(stack)等&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;new 一个对象时加括号和不加括号的区别:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于自定义类类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果该类没有定义构造函数（由编译器合成默认构造函数）也没有虚函数，那么&lt;code&gt;class c = new class;&lt;/code&gt;将不调用合成的默认构造函数，而&lt;code&gt;class c = new class();&lt;/code&gt;则会调用默认构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果该类没有定义构造函数（由编译器合成默认构造函数）但有虚函数，那么&lt;code&gt;class c = new class;&lt;/code&gt;和&lt;code&gt;class c = new class();&lt;/code&gt;一样，都会调用默认构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果该类定义了默认构造函数，那么&lt;code&gt;class c = new class;&lt;/code&gt;和&lt;code&gt;class c = new class();&lt;/code&gt;一样，都会调用默认构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于内置类型：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;int *a = new int;&lt;/code&gt;不会将申请到的int空间初始化，而&lt;code&gt;int *a = new int();&lt;/code&gt;则会将申请到的int空间初始化为0。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于自定义类类型的&lt;strong&gt;数组&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;无论是否使用（），都会自动调用其默认构造函数来初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string *psa = new string[10];  // 每个元素调用默认构造函数初始化 
string *psa = new string[10]();  // 每个元素调用默认构造函数初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于内置类型的&lt;strong&gt;数组&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;必须使用()来显示指定程序执行初始化操作，否则程序不执行初始化操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; int *pia = new int[10]; // 每个元素都没有初始化 
 int *pia2 = new int[10]();  // 每个元素初始化为0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;基本上只看这句话：对于自定义类型来说没有区别，都是使用默认构造函数。对于原子（内置）类型来说加括号会初始化&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;来一点&lt;code&gt;alloca&lt;/code&gt;、&lt;code&gt;malloc&lt;/code&gt;、&lt;code&gt;calloc&lt;/code&gt;、&lt;code&gt;realloc&lt;/code&gt;之间的区别&lt;/h2&gt;
&lt;h2&gt;&lt;code&gt;alloca&lt;/code&gt;向栈申请内存，作用域结束自动释放&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 分配速度快，但是可移植性差。
void* alloca(size_t size);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;malloc&lt;/code&gt;向堆申请内存，不初始化内容，需要手动释放&lt;/h2&gt;
&lt;p&gt;老生常谈了，不赘述了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 分配一块size字节大小的内存，并返回内存块起始位置的指针。
void* malloc (size_t size);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;calloc&lt;/code&gt;向堆申请内存，初始化内容为0，需要手动释放&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 分配一块内存，包含num个元素，每个元素size字节大小。
void* calloc (size_t num, size_t size);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意，参数和&lt;code&gt;malloc&lt;/code&gt;不同。&lt;code&gt;malloc&lt;/code&gt;提供的是size大小，而&lt;code&gt;calloc&lt;/code&gt;提供的是n个size大小&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;realloc&lt;/code&gt;更改已经配置的内存空间，即更改由&lt;code&gt;malloc()/calloc()/realloc()&lt;/code&gt;函数分配的内存空间的大小。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 改变指针ptr指向的内存块大小为size字节。原来内存块的位置也可能发生变化。
void* realloc (void* ptr, size_t new_size);
//ptr ：指向要重新分配的内存块的指针。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;ptr&lt;/code&gt;是&lt;code&gt;NULL&lt;/code&gt;，则相当于调用&lt;code&gt;malloc(new_size)&lt;/code&gt;, 成功返回首地址，失败返回NULL。&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;new_size == 0&lt;/code&gt; , 相当于调用&lt;code&gt;free(ptr)&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;ptr&lt;/code&gt;不是&lt;code&gt;NULL&lt;/code&gt;、&lt;code&gt;malloc&lt;/code&gt;、&lt;code&gt;calloc&lt;/code&gt; 、&lt;code&gt;realloc()&lt;/code&gt;的返回值，则报错&lt;code&gt;realloc invalid pointer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若&lt;code&gt;new_size &amp;lt; old_size&lt;/code&gt; ， 可能会丢失之前的部分数据&lt;/li&gt;
&lt;li&gt;若&lt;code&gt;new_size &amp;gt; old_size&lt;/code&gt; , 则会分配新的内存，&lt;strong&gt;若分配到新的内存块&lt;/strong&gt;，则**&lt;code&gt;ptr&lt;/code&gt;指向的内存被释放，&lt;code&gt;ptr&lt;/code&gt;成为野指针**。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void *ptr = realloc(ptr, new_size);&lt;/code&gt;：这样的写法，会造成&lt;strong&gt;内存泄露&lt;/strong&gt;。（若&lt;strong&gt;分配失败，返回NULL，而之前&lt;code&gt;ptr&lt;/code&gt;所指向的内存没有释放，且无法被访问&lt;/strong&gt;）。&lt;/li&gt;
&lt;li&gt;释放&lt;code&gt;realloc()&lt;/code&gt;过的&lt;code&gt;ptr&lt;/code&gt;时，要判断&lt;code&gt;new_size&lt;/code&gt;是否为0，否则会造成&lt;code&gt;double free&lt;/code&gt;错误。（&lt;code&gt;new_size&lt;/code&gt;为0时，相当于&lt;code&gt;free(ptr)&lt;/code&gt;,再次&lt;code&gt;free&lt;/code&gt;时错误）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;realloc()&lt;/code&gt;并&lt;strong&gt;不保证调整后的内存空间和原来的内存空间保持同一内存地址&lt;/strong&gt;，相反，&lt;code&gt;realloc&lt;/code&gt;返回的指针很可能指向一个新的地址
&lt;ul&gt;
&lt;li&gt;如果当前内存段后面有需要的内存空间，则直接扩展这段内存空间，&lt;code&gt;realloc()&lt;/code&gt;将返回原指针。&lt;/li&gt;
&lt;li&gt;如果当前内存段后面的空闲字节不够，那么就使用堆中的第一个能够满足这一要求的内存块，将目前的数据复制到新的位置，并将原来的数据块释放掉，返回新的内存块位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果内存不足，&lt;code&gt;realloc()&lt;/code&gt;失败，则**不释放旧内存块（此时，原来的指针仍然有效）**并返回空指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;c/c++风格指针类型转换&lt;/h1&gt;
&lt;p&gt;注意，在c++中，禁止指针类型的隐式转换。比如这样是不可以的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* ptr = malloc(sizeof(int));
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;所以必须强制转换为&lt;code&gt;int*&lt;/code&gt;类型
&lt;ul&gt;
&lt;li&gt;注意是转换为&lt;code&gt;int*&lt;/code&gt;而不是&lt;code&gt;int&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int* ptr = (int*)malloc(sizeof(int)); //C风格
int* ptr = static_cast&amp;lt;int*&amp;gt;(malloc(sizeof(int))); //C++风格
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;delete 后必须直接设置指针为nullptr，否则造成的垃圾值会导致意外情况。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class myobj {
public:
    int* ptrs;
    myobj(int val) :ptrs(new int(val)) {}
    ~myobj() {
        if (ptrs != nullptr) {
            delete ptrs;
            ptrs = nullptr;
        }
    }
};

int main() {
    myobj* ptr = new myobj(20);
    delete ptr;
    delete ptr;//额外一次
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码中，我们重复&lt;code&gt;delete&lt;/code&gt;居然会报错。按理来说不是应该第一次&lt;code&gt;delete&lt;/code&gt;之后，&lt;code&gt;ptrs&lt;/code&gt;被置为&lt;code&gt;nullptr&lt;/code&gt;了吗？&lt;/p&gt;
&lt;p&gt;问题在于，我们&lt;code&gt;delete&lt;/code&gt;的时候，不仅会调用析构函数&lt;code&gt;delete&lt;/code&gt;掉&lt;code&gt;ptrs&lt;/code&gt;，还会&lt;code&gt;free&lt;/code&gt;整个&lt;code&gt;myobj&lt;/code&gt;对象空间。&lt;/p&gt;
&lt;p&gt;所以我们给&lt;code&gt;ptrs&lt;/code&gt;设置为&lt;code&gt;nullptr&lt;/code&gt;后，整个外部的&lt;code&gt;ptr&lt;/code&gt;指向的内存空间已经被释放了。全部都是垃圾值。虽然此时&lt;code&gt;ptr&lt;/code&gt;指向的空间地址没变，但是整个空间本身被释放，已经是垃圾值了。在MSVC里，未初始化的堆内存会被设置为&lt;code&gt;0xd&lt;/code&gt;而不是&lt;code&gt;0x0&lt;/code&gt;，这就导致了if语句无效。所以再次&lt;code&gt;delete&lt;/code&gt;会有问题。&lt;/p&gt;
&lt;p&gt;如果我们在两次&lt;code&gt;delete&lt;/code&gt;中间插入一个&lt;code&gt;nullptr&lt;/code&gt;就不会有这个问题，因为此时ptr被置为了&lt;code&gt;0x0&lt;/code&gt;。而&lt;code&gt;delete&lt;/code&gt;一个&lt;code&gt;0x0&lt;/code&gt;空指针是不会有任何事情发生的。&lt;/p&gt;
&lt;p&gt;这就是说我们只要使用了&lt;code&gt;delete&lt;/code&gt;，一定要随后直接设置指针为&lt;code&gt;nullptr&lt;/code&gt;。&lt;/p&gt;
&lt;h1&gt;kmalloc和vmalloc&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kmalloc&lt;/code&gt;和&lt;code&gt;vmalloc&lt;/code&gt;是分配的是内核的内存,&lt;code&gt;malloc&lt;/code&gt;分配的是用户的内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kmalloc&lt;/code&gt;申请的是较小的连续的物理内存，&lt;strong&gt;内存物理地址上连续，虚拟地址上也是连续的&lt;/strong&gt;，使用的是内存分配器slab的一小片。申请的内存位于物理内存的直接映射区域（3G～3G+896M (high_memory)）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用&lt;code&gt;kzalloc&lt;/code&gt;申请内存的时候， 效果等同于先是用&lt;code&gt; kmalloc()&lt;/code&gt; 申请空间 , 然后用 &lt;code&gt;memset() &lt;/code&gt;来初始化&lt;/li&gt;
&lt;li&gt;注意&lt;code&gt;kmalloc&lt;/code&gt;最大只能开辟128k(32*PAGESIZE)-16，16个字节是被页描述符结构占用了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kmalloc&lt;/code&gt;是可能会睡眠的。---linux内核设计与实现 12.4&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;vmalloc&lt;/code&gt;用于申请较大的内存空间，虚拟内存是连续。申请的内存的则位于&lt;code&gt;vmalloc_start&lt;/code&gt;～&lt;code&gt;vmalloc_end&lt;/code&gt;之间（非连续内存区），与物理地址没有简单的转换关系，虽然在&lt;strong&gt;虚拟地址上它们是连续的，但是在物理上它们不连续&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vmalloc_start&lt;/code&gt; 是在(3G+896M)高端内存起始 + 8M空洞 的位置处起始&lt;/li&gt;
&lt;li&gt;一般情况下，只有硬件设备才需要物理地址连续的内存，因为硬件设备往往存在于MMU之外，根本不了解虚拟地址；但为了性能上的考虑，内核中一般使用 &lt;code&gt;kmalloc()&lt;/code&gt;，而只有在需要获得大块内存时才使用&lt;code&gt;vmalloc()&lt;/code&gt;，例如当模块被动态加载到内核当中时，就把模块装载到由&lt;code&gt;vmalloc()&lt;/code&gt;分配 的内存上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;vmalloc&lt;/code&gt;比&lt;code&gt;kmalloc&lt;/code&gt;要慢。因为由于 &lt;code&gt;vmalloc &lt;/code&gt;获得的物理内存页是不连续的，因此它只能将这些物理内存页一个一个地进行映射，在性能开销上会比直接映射大得多。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.cnblogs.com/wuchanming/p/4465155.html&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/alantu2018/p/9000778.html&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/macrossdzh/article/details/5958368&lt;/p&gt;
</content:encoded></item><item><title>Linux多进程开发-2.12-父子进程通过匿名管道通信</title><link>https://sterben-01.github.io/posts/nowcoder_2.13/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/nowcoder_2.13/</guid><pubDate>Fri, 20 May 2022 06:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;Linux多进程开发-2.12-父子进程通过匿名管道通信&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;一次性写入&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main(){
    
    int pipefd[2]; // 创建管道数组
    int ret = pipe(pipefd); //创建管道

    if(ret == -1){ //如果管道创建失败
        perror(&quot;pipe error&quot;);
        exit(0);
    }

       //必须要在fork之前创建管道。因为fork才能将文件描述符复制给子进程，这样才可以通信
    pid_t pid = fork(); //创建子进程
    if(pid &amp;gt; 0){ //fork返回的pid是子进程id。也就是在父进程中，这个数字一定是大于0的
        //父进程

        //从管道的读取端读取
        char buf[1024] = {0};
        int len = read(pipefd[0], buf, sizeof(buf)); //! read函数默认是阻塞的。如果子进程没有写入到管道的话，父进程会一直等待子进程把数据写入管道。


        printf(&quot;receive :%d , pid: %d\n&quot;, len, pid);



    }
    else if(pid == 0){ //如果pid是0，代表现在切换进了子进程。
        //子进程
        //管道写入端写入

        char* msg = &quot;hello world&quot;;

        int len = write(pipefd[1], msg, strlen(msg));
        printf(&quot;send :%d , pid: %d\n&quot;, len, getpid());
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;循环写入 - 这里去掉sleep会产生自己读取自己的问题&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main(){
    
    int pipefd[2]; // 创建管道数组
    int ret = pipe(pipefd); //创建管道

    if(ret == -1){ //如果管道创建失败
        perror(&quot;pipe error&quot;);
        exit(0);
    }

       //必须要在fork之前创建管道。因为fork才能将文件描述符复制给子进程，这样才可以通信
    pid_t pid = fork(); //创建子进程
    if(pid &amp;gt; 0){ //fork返回的pid是子进程id。也就是在父进程中，这个数字一定是大于0的
        //父进程

        //从管道的读取端读取
        char buf[1024] = {0};
        while(1){
            //父进程循环读取数据。不用sleep因为默认是阻塞
            int len = read(pipefd[0], buf, sizeof(buf)); //! read函数默认是阻塞的。如果子进程没有写入到管道的话，父进程会一直等待子进程把数据写入管道。
            printf(&quot;receive :%d , pid: %d, father pid: %d\n&quot;, len, pid, getpid());
            //printf(buf); 不要这样写。要按照下面去写。不要直接读变量，要按照字符串读写
            printf(&quot;%s\n&quot;, buf);
        }
    



    }
    else if(pid == 0){ //如果pid是0，代表现在切换进了子进程。
        //子进程
        //管道写入端写入
        //子进程循环写入
        while(1){
            char* msg = &quot;hello world&quot;;
            int len = write(pipefd[1], msg, strlen(msg));
            printf(&quot;send :%d , pid: %d\n&quot;, len, getpid());
            sleep(1);//sleep不然太快了
            

        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过使用单向管道解决问题（关闭读取程序的写入端和写入程序的读取端）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/*
*如果我们去掉了sleep(1)会有什么问题呢？
如果去掉了，那么可能子进程或者父进程执行了管道写入操作后，cpu没有切换至子进程。因为时间片太短了。
在写入操作后，因为没有切换至另一个进程，就会导致同一个进程在写入后又读取了对应管道的内容。这样就会产生自己写自己读的问题。
?问题在于，开发中不可以写sleep 怎么办呢？
其实一般情况下我们不会父子互相切换读写。所以这种情况不用特别考虑。
一般来说，如果是进程只进行单独的读操作或者写操作，我们可以把不需要的操作进行关闭
TODO  close(pipefd[0/1]);
*/

int main(){
    
    int pipefd[2]; // 创建管道数组
    int ret = pipe(pipefd); //创建管道

    if(ret == -1){ //如果管道创建失败
        perror(&quot;pipe error&quot;);
        exit(0);
    }

       //必须要在fork之前创建管道。因为fork才能将文件描述符复制给子进程，这样才可以通信
    pid_t pid = fork(); //创建子进程

    char buf[1024] = {0}; //! 缓冲区不能写在这。写在这的话进程共用一个而且不清除的话第二轮父进程读的时候因为里面有数据所以不会被阻塞

    if(pid &amp;gt; 0){ //fork返回的pid是子进程id。也就是在父进程中，这个数字一定是大于0的
        //父进程
        //char buf[1024] = {0};
        //从管道的读取端读取
        close(pipefd[1]); // 关闭父进程的写入操作
        while(1){

            //父进程循环读取数据。不用sleep因为默认是阻塞
            int len = read(pipefd[0], buf, sizeof(buf)); //! read函数默认是阻塞的。如果子进程没有写入到管道的话，父进程会一直等待子进程把数据写入管道。
            printf(&quot;father receive :%d , pid: %d, father pid: %d\n&quot;, len, pid, getpid());
            //printf(buf); 不要这样写。要按照下面去写。不要直接读变量，要按照字符串读写
            printf(&quot;%s\n&quot;, buf);
            bzero(buf, 1024); //如果缓冲区写在外面就必须要清零缓冲区
        }
    }
    else if(pid == 0){ //如果pid是0，代表现在切换进了子进程。
        //子进程
        //管道写入端写入
        //子进程循环写入
        //char buf[1024] = {0};
        close(pipefd[0]); // 关闭子进程的读取操作
        while(1){
            //写入
            char* msg = &quot;hello world i&apos;m child&quot;;
            int len = write(pipefd[1], msg, strlen(msg));
            printf(&quot;child send :%d , pid: %d\n&quot;, len, getpid());
            bzero(buf, 1024); //如果缓冲区写在外面就必须要清零缓冲区
        
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux多进程开发-2.15-有名管道</title><link>https://sterben-01.github.io/posts/nowcoder_2.15/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/nowcoder_2.15/</guid><pubDate>Fri, 20 May 2022 06:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;Linux多进程开发-2.15-有名管道&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;有名管道&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;有名管道以FIFO的文件形式存在于文件系统中。但是内容是在内存中。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;创建有名管道&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
int main(){

    int exist_or_not = access(&quot;2.14fifo&quot;, F_OK); //使用access函数判断文件。此处是判断是否存在。

    if(exist_or_not == -1){
        printf(&quot;管道不存在，创建管道&quot;);
        int ret = mkfifo(&quot;2.14fifo&quot;, 0664); //创建fifo文件
        if(ret == -1){
            perror(&quot;mkfifo&quot;);
            exit(0);
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;写入端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
//向管道中写入数据
int main(){
    //1. 创建管道文件
    int exist = access(&quot;2.14fifo&quot;, F_OK); //使用access函数判断文件。此处是判断是否存在。

    if(exist == -1){
        printf(&quot;管道不存在，创建管道&quot;);
        //2. 创建管道
        int ret = mkfifo(&quot;2.14fifo&quot;, 0664); //创建fifo文件
        if(ret == -1){ 
            //此处判断是否创建失败管道
            perror(&quot;mkfifo&quot;);
            exit(0);
        }
    }
    //3.以只写的方式打开管道
    int fd = open(&quot;2.14fifo&quot;, O_WRONLY);
    if(fd == -1){ 
        //此处判断是否打开失败
        perror(&quot;open&quot;);
        exit(0);
    }
    char buf[1024] = {0}; //创建字符串数组
    
    for(int i = 0; i &amp;lt; 100; i++){
        char buf[1024] = {0}; //创建字符串数组
        sprintf(buf, &quot;hello, %d\n&quot;, i);  //将字符串写入字符串数组
        write(fd, buf, strlen(buf)); //将字符串数组写入管道文件
        bzero(buf, 1024); //清空数组
        sleep(1);
        
    }
    close(fd); //关闭文件
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;读取端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
//从管道中读取数据
int main(){
    //1. 打开管道文件

    int fd = open(&quot;2.14fifo&quot;, O_RDONLY); //只读方式打开管道文件
    if(fd == -1){ 
        //如果打开失败
        perror(&quot;open&quot;);
        exit(0);
    }
    char buf[1024] = {0};
    //2. 读取数据
    while(1)
    {
        int len = read(fd, buf, sizeof(buf));
        if(len == -1)
        {
            perror(&quot;read&quot;);
            exit(0);
        }
        if(len == 0)
        {
            printf(&quot;写入端已经断开连接\n&quot;);
            break; //这里不可以用exit。因为如果用了exit，下面的close会运行不到。
        }
        printf(&quot;RECEIVE BUF : %s\n&quot;, buf);
        bzero(buf,1024); //清空数组
    }

    close(fd);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;h3&gt;有名管道的注意事项:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;一个为只读而打开一个管道的进程会被阻塞，直到另外一个进程为只写打开管道&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个为只写而打开一个管道的进程会被阻塞，直到另外一个进程为只读打开管道&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;读管道:&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;管道中有数据，read返回实际读取的字节数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管道中没有数据：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;管道的写入端被全部关闭，read返回0。（相当于读到了文件末尾）&lt;/li&gt;
&lt;li&gt;写入端没有被全部关闭，read会被阻塞等待。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;写管道：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;管道读端被全部关闭，进行异常终止 (收到SIGPIPE信号)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管道读端没有被全部关闭：&lt;/p&gt;
&lt;p&gt;1.管道已经满了，write会阻塞等待。&lt;/p&gt;
&lt;p&gt;2.管道没有满，write将数据写入，并且返回实际写入的字节数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（13）-- 用信号量实现进程互斥、同步、前驱关系</title><link>https://sterben-01.github.io/posts/OS16/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS16/</guid><pubDate>Fri, 20 May 2022 00:55:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（13）-- 用信号量实现进程互斥、同步、前驱关系&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;信号量&lt;/h2&gt;
&lt;p&gt;为了方便理解，我们可以认为 &lt;strong&gt;信号量的值 = 这种资源的剩余数量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以如果信号量的值小于0，说明此时有进程正在等待这个资源。&lt;/p&gt;
&lt;p&gt;P(S) 也就是wait(S) —— 申请一个资源S。如果资源不够就阻塞等待。&lt;/p&gt;
&lt;p&gt;V(S) 也就是signal(S) —— 释放一个资源S。如果有进程在等待该资源，则唤醒一个进程。&lt;/p&gt;
&lt;h2&gt;信号量机制实现进程互斥&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS16.assets/QQ%E6%88%AA%E5%9B%BE20220519195830.png&quot; alt=&quot;QQ截图20220519195830&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;信号量机制实现进程同步&lt;/h2&gt;
&lt;p&gt;实现进程同步的根本就是让必须后执行的代码需要获取到先执行的代码释放的资源后才能执行。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S = 0; //初始化信号量为0
P1(){
    代码1;
    代码2;
    V(S); //释放资源
    代码3;
}

P2(){
    P(S); //请求资源
    代码4;
    代码5;
    代码6;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到，如果想让代码4一定在代码2后面执行，我们可以设计为执行代码4需要一个资源，而这个资源必须要等到代码2结束运行后才能被代码2释放。这样能保证了执行的顺序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS16.assets/QQ%E6%88%AA%E5%9B%BE20220519201605.png&quot; alt=&quot;QQ截图20220519201605&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;更复杂的前驱关系&lt;/h3&gt;
&lt;h2&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS16.assets/QQ%E6%88%AA%E5%9B%BE20220519201942.png&quot; alt=&quot;QQ截图20220519201942&quot; /&gt;&lt;/h2&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS16.assets/QQ%E6%88%AA%E5%9B%BE20220519202020.png&quot; alt=&quot;QQ截图20220519202020&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（12）-- 信号量机制</title><link>https://sterben-01.github.io/posts/OS15/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS15/</guid><pubDate>Fri, 20 May 2022 00:25:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（12）-- 信号量机制&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=410446173&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;信号量&lt;/h2&gt;
&lt;p&gt;用户进程可以通过使用操作系统提供的一对原语(wait 和 signal)来对信号量进行操作。从而进行进程互斥和进程同步。&lt;/p&gt;
&lt;p&gt;信号量其实就是一个变量。可以是一个整数，也可以是更复杂的记录型变量。可以用一个信号量来表示系统中某种资源的数量。&lt;/p&gt;
&lt;h2&gt;整型信号量&lt;/h2&gt;
&lt;p&gt;用一个整数型变量做为信号量。用来表示系统中某种资源的数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;整型信号量不满足让权等待。会发生忙等。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对信号量只能进行三种操作：wait、signal和初始化。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS15.assets/QQ%E6%88%AA%E5%9B%BE20220519194058.png&quot; alt=&quot;QQ截图20220519194058&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;记录型信号量&lt;/h2&gt;
&lt;p&gt;因为整型信号量不满足让权等待，所以有记录型信号量。即用记录型数据结构表示的信号量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS15.assets/QQ%E6%88%AA%E5%9B%BE20220519194313.png&quot; alt=&quot;QQ截图20220519194313&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS15.assets/QQ%E6%88%AA%E5%9B%BE20220519194612.png&quot; alt=&quot;QQ截图20220519194612&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS15.assets/QQ%E6%88%AA%E5%9B%BE20220519194632.png&quot; alt=&quot;QQ截图20220519194632&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（10）-- 进程同步、进程互斥</title><link>https://sterben-01.github.io/posts/OS13/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS13/</guid><pubDate>Thu, 19 May 2022 01:25:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（10）-- 进程同步、进程互斥&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=415904629&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS13.assets/QQ%E6%88%AA%E5%9B%BE20220518210022.png&quot; alt=&quot;QQ截图20220518210022&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（11）-- 进程互斥的软件实现方法</title><link>https://sterben-01.github.io/posts/OS14/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS14/</guid><pubDate>Thu, 19 May 2022 01:25:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（11）-- 进程互斥的软件实现方法&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=415904629&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;单标志法 — 违背空闲让进的规则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS14.assets/QQ%E6%88%AA%E5%9B%BE20220518210320.png&quot; alt=&quot;QQ截图20220518210320&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;双标志先检查法 - 违背忙则等待原则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS14.assets/QQ%E6%88%AA%E5%9B%BE20220518210359.png&quot; alt=&quot;QQ截图20220518210359&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;双标志后检查法 - 违背了空闲让进和优先等待原则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS14.assets/QQ%E6%88%AA%E5%9B%BE20220518210512.png&quot; alt=&quot;QQ截图20220518210512&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Peterson算法 - 违背让权等待原则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS14.assets/QQ%E6%88%AA%E5%9B%BE20220518210611.png&quot; alt=&quot;QQ截图20220518210611&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-18-OS14.assets/QQ%E6%88%AA%E5%9B%BE20220518210622.png&quot; alt=&quot;QQ截图20220518210622&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（9）-- 时间片轮转、优先级、多级反馈队列调度算法</title><link>https://sterben-01.github.io/posts/OS12/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS12/</guid><pubDate>Wed, 18 May 2022 05:25:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（9）--时间片轮转、优先级、多级反馈队列调度算法&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=415904629&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;RR - 时间片轮转&lt;/h2&gt;
&lt;p&gt;如果时间片太大，使得每一个进程都可以在一个时间片内完成，则时间片轮转调度算法回退化为先来先服务调度算法，并且会增大进程响应时间。所以 &lt;strong&gt;时间片不能太大&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;另一方面，进程调度和切换是有时间代价的（保存，恢复运行环境）。所以如果时间片太小，会导致 &lt;strong&gt;进程切换过于频繁&lt;/strong&gt; 系统会花费大量的时间来处理进程切换，从而导致实际用于进程执行的时间比例减小。所以 &lt;strong&gt;时间片也不能太小&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS12.assets/QQ%E6%88%AA%E5%9B%BE20220518204217.png&quot; alt=&quot;QQ截图20220518204217&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;优先级调度算法&lt;/h2&gt;
&lt;p&gt;优先级也分为 &lt;strong&gt;静态优先级&lt;/strong&gt; 和 &lt;strong&gt;动态优先级&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;静态优先级：创建进程时确定，之后不变&lt;/p&gt;
&lt;p&gt;动态优先级：创建进程时有一个初始值。之后会根据情况动态地调整优先级。&lt;/p&gt;
&lt;p&gt;通常来说，系统进程优先级高于用户进程。前台进程优先级高于后台进程。&lt;/p&gt;
&lt;p&gt;就绪队列未必只有一个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS12.assets/QQ%E6%88%AA%E5%9B%BE20220518204319.png&quot; alt=&quot;QQ截图20220518204319&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;多级反馈队列调度算法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS12.assets/QQ%E6%88%AA%E5%9B%BE20220518204339.png&quot; alt=&quot;QQ截图20220518204339&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS12.assets/QQ%E6%88%AA%E5%9B%BE20220518204403.png&quot; alt=&quot;QQ截图20220518204403&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（8）-- FCFS、SJF、HRRN调度算法</title><link>https://sterben-01.github.io/posts/OS11/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS11/</guid><pubDate>Wed, 18 May 2022 04:25:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（8）--FCFS、SJF、HRRN调度算法&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=1425996158&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;FCFS - 先来先服务算法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS11.assets/QQ%E6%88%AA%E5%9B%BE20220518005059.png&quot; alt=&quot;QQ截图20220518005059&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;SJF - 短作业优先&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS11.assets/QQ%E6%88%AA%E5%9B%BE20220518005410.png&quot; alt=&quot;QQ截图20220518005410&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;HRRN - 高响应比优先&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS11.assets/QQ%E6%88%AA%E5%9B%BE20220518005700.png&quot; alt=&quot;QQ截图20220518005700&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-17-OS11.assets/QQ%E6%88%AA%E5%9B%BE20220518005727.png&quot; alt=&quot;QQ截图20220518005727&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（7）--进程调度的时机、切换与过程、方式</title><link>https://sterben-01.github.io/posts/OS10/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS10/</guid><pubDate>Fri, 13 May 2022 04:25:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（7）--进程调度的时机、切换与过程、方式&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=1425996158&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;进程调度（低级调度）的时机&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS10.assets/QQ%E6%88%AA%E5%9B%BE20220512235149.png&quot; alt=&quot;QQ截图20220512235149&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS10.assets/QQ%E6%88%AA%E5%9B%BE20220512235620.png&quot; alt=&quot;QQ截图20220512235620&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;什么是临界资源/临界区&lt;/h3&gt;
&lt;p&gt;临界资源：一个时间段之内只允许一个进程使用的资源。各个进程需要__互斥地__访问临界资源&lt;/p&gt;
&lt;p&gt;临界区：访问临界资源的那段代码&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内核程序临界区&lt;/strong&gt; 一般是用来访问某种 &lt;strong&gt;内核数据结构&lt;/strong&gt; 的。比如 &lt;strong&gt;进程的就绪队列&lt;/strong&gt; (由各个就绪进程的PCB组成)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果还没有退出临界区(还没解锁)就进行进程调度，但是进程调度相关的程序需要访问就绪队列，但是此时就绪队列被锁住了。因此无法顺利进行进程调度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS10.assets/QQ%E6%88%AA%E5%9B%BE20220512235525.png&quot; alt=&quot;QQ截图20220512235525&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程调度的方式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS10.assets/QQ%E6%88%AA%E5%9B%BE20220512235717.png&quot; alt=&quot;QQ截图20220512235717&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程的切换与过程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS10.assets/QQ%E6%88%AA%E5%9B%BE20220513000009.png&quot; alt=&quot;QQ截图20220513000009&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（6）--调度的概念、层次</title><link>https://sterben-01.github.io/posts/OS9%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS9%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81/</guid><pubDate>Fri, 13 May 2022 04:20:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（6）--调度的概念、层次&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=1425996158&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;调度的三个层次&lt;/h2&gt;
&lt;h3&gt;1. 高级调度&lt;/h3&gt;
&lt;p&gt;作业：一个具体的任务&lt;/p&gt;
&lt;p&gt;用户向系统提交一个作业 约等于 用户让操作系统启动一个程序（来处理一个具体的任务）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内存空间有限，有时无法将用户提交的作业全部放入内存。这时我们引入高级调度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;高级调度（作业调度）: 按照一定的原则从外存的作业后备队列中挑选一个作业调入内存，并且创建进程。每个作业只调入一次，调出一次。作业调入时会简历PCB，调出时才撤销PCB&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;2. 低级调度&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;低级调度（进程调度/处理机调度）: 按照某种策略从就绪队列中选取一个进程，将处理机分配给它&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;进程调度是操作系统中 &lt;strong&gt;最基本的一种调度，频率很高。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3. 中级调度&lt;/h3&gt;
&lt;p&gt;内存不够时，可将某些进程的数据调出外存。等内存空闲或者进程需要运行时再重新调入内存。暂时调到外存等待的进程状态为 &lt;strong&gt;挂起状态&lt;/strong&gt; 。被挂起的进程PCB会被组织成为 &lt;strong&gt;挂起队列&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中级调度（内存调度）：按照某种策略决定将哪个处于挂起状态的进程重新调入内存&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;补充知识：进程的挂起态和七状态模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512234319.png&quot; alt=&quot;QQ截图20220512234319&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512234406.png&quot; alt=&quot;QQ截图20220512234406&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（5）--线程的实现方式和模型</title><link>https://sterben-01.github.io/posts/OS8/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS8/</guid><pubDate>Thu, 12 May 2022 06:20:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（5）--线程的实现方式和模型&lt;/h1&gt;
&lt;h3&gt;线程的实现方式&lt;/h3&gt;
&lt;h4&gt;用户级线程（ULT)&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512010637.png&quot; alt=&quot;QQ截图20220512010637&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512011115.png&quot; alt=&quot;QQ截图20220512011115&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;内核级线程（KLT）&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512011509.png&quot; alt=&quot;QQ截图20220512011509&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;多线程模型&lt;/h3&gt;
&lt;h4&gt;一对一模型&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512011636.png&quot; alt=&quot;QQ截图20220512011636&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;多对一模型&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512011745.png&quot; alt=&quot;QQ截图20220512011745&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;多对多模型&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512012119.png&quot; alt=&quot;QQ截图20220512012119&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS8.assets/QQ%E6%88%AA%E5%9B%BE20220512012129.png&quot; alt=&quot;QQ截图20220512012129&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（4）--线程</title><link>https://sterben-01.github.io/posts/OS7/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS7/</guid><pubDate>Thu, 12 May 2022 06:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（4）--线程&lt;/h1&gt;
&lt;h2&gt;线程是程序执行流的最小单位,也是调度的基本单位。&lt;/h2&gt;
&lt;p&gt;线程是一个基本的CPU执行单元，线程是程序执行流的最小单位。引入线程之后，不仅仅是进程之间可以并发，进程内的各个线程之间也可以并发，从而进一步提升了系统的并发程度。使得一个进程内也可以并发处理各种任务&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们可以把线程理解为轻量级进程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引入线程后，进程只作为除CPU之外的系统资源的分配单元。如打印机，内存地址空间	都是分配给进程的。所以进程是资源分配的基本单位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-12-OS7.assets/QQ%E6%88%AA%E5%9B%BE20220512005852.png&quot; alt=&quot;QQ截图20220512005852&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>GCC基础</title><link>https://sterben-01.github.io/posts/gcc_basic/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/gcc_basic/</guid><pubDate>Thu, 12 May 2022 02:30:00 GMT</pubDate><content:encoded>&lt;h1&gt;GCC基础&lt;/h1&gt;
&lt;h2&gt;GCC处理流程&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.h 或 .c 或 .cpp &lt;/code&gt;通过预处理器得到预处理后的源代码 .i&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.i&lt;/code&gt; 通过编译器得到汇编代码&lt;code&gt; .s&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.s&lt;/code&gt; 通过汇编器得到目标代码 &lt;code&gt;.o&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.o&lt;/code&gt; 通过链接器得到可执行文件 &lt;code&gt;.exe 或 .out&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-11-gcc_basic.assets/gcc-1.png&quot; alt=&quot;gcc-1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;实例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gcc test.c -E -o test.i 注意-o指的是生成输出文件 后面的是输出文件名称
这里生成了预处理后的源代码
gcc test.i -S -o test.s
这里生成了汇编代码
gcc test.s -s -o test.o  注意这里是小s
这里生成了目标代码
如果test.c没有用到外部文件可以直接执行.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-11-gcc_basic.assets/gcc-2.png&quot; alt=&quot;gcc-2&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;静态库&lt;/h2&gt;
&lt;h3&gt;命名规则：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Linux: &lt;code&gt;libxxx.a&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​		&lt;code&gt;lib&lt;/code&gt;前缀是固定的&lt;/p&gt;
&lt;p&gt;​		&lt;code&gt;xxx&lt;/code&gt;是名字&lt;/p&gt;
&lt;p&gt;​		&lt;code&gt;.a&lt;/code&gt;后缀固定&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Windows: &lt;code&gt;libxxx.lib&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;静态库制作&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;gcc获得 &lt;code&gt;.o&lt;/code&gt; 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -c xxx.c xxx.c
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将&lt;code&gt; .o&lt;/code&gt; 文件打包，使用ar工具&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;  ar rcs libxxx.a xxx.o xxx.o
  r - 将文件插入备存文件中
  c - 建立备存文件
  s - 索引
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-11-gcc_basic.assets/QQ%E6%88%AA%E5%9B%BE20220519231928.png&quot; alt=&quot;QQ截图20220519231928&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;静态库特点：&lt;/h3&gt;
&lt;p&gt;静态库优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;静态库被打包到应用程序中加载速度快。&lt;/li&gt;
&lt;li&gt;发布程序无需提供静态库， &lt;strong&gt;移植性好&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;静态库缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;消耗系统资源，浪费空间和内存。因为函数相关的所有的库都被打包进可执行文件了。&lt;/li&gt;
&lt;li&gt;更新，发布，部署较为麻烦。更新以后需要重新编译：库是被复制到可执行文件中去了，如果某个库更新了，则与它相关的所有可执行文件都需要重新编译。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;静态库对函数库的链接是放在编译时期完成的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;动态库&lt;/h2&gt;
&lt;h3&gt;命名规则&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Linux: &lt;code&gt;libxxx.so&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lib&lt;/code&gt;前缀是固定的&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xxx&lt;/code&gt;是名字&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.so&lt;/code&gt;后缀固定&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在linux下是一个可执行文件&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Windows: &lt;code&gt;libxxx.dll&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;动态库制作&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;gcc得到&lt;code&gt;.o&lt;/code&gt;文件，需要添加 &lt;code&gt;-fpic 或 -fPIC &lt;/code&gt;参数得到和位置无关的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -c -fpic/-fPIC xxx.c xxx.c
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;gcc 得到动态库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -shared xxx.o xxx.o -o libxxx.so
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;注意事项&lt;/h3&gt;
&lt;p&gt;在include的头文件路径和执行文件不一致的时候需要加上 &lt;code&gt;-I&lt;/code&gt;参数指定头文件目录&lt;/p&gt;
&lt;p&gt;在动态库的路径和执行文件路径不一致的时候需要加上&lt;code&gt;-L&lt;/code&gt;参数指定动态库文件目录&lt;/p&gt;
&lt;p&gt;动态库存在需要设置环境变量的情况。这里不赘述&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-11-gcc_basic.assets/QQ%E6%88%AA%E5%9B%BE20220519232007.png&quot; alt=&quot;QQ截图20220519232007&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;使用示范&lt;/h3&gt;
&lt;h4&gt;动态库静态库相同&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;假设路径不同
gcc 执行文件名 -o 输出文件名 -I 头文件目录 -L 动态库目录 -l 动态库名称 注意不是文件名称是动态库名称
gcc main.c -o main -I ./include/ -L ./lib/ -l calc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;动态库特点：&lt;/h3&gt;
&lt;p&gt;动态库优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;动态库只有一份, 可以实现进程间资源共享（共享库）。&lt;/li&gt;
&lt;li&gt;更新，部署，发布简单。&lt;/li&gt;
&lt;li&gt;可以控制何时加载动态库。(可以在链接载入时完全由程序员在程序代码中控制。)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;动态库缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加载速度比静态库慢。&lt;/li&gt;
&lt;li&gt;发布程序时需要提供依赖的动态库。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;动态库把对一些库函数的链接载入推迟到程序运行的时期。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;区别与联系&lt;/h2&gt;
&lt;p&gt;二者的不同点在于代码被载入的时刻不同。
静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大。
动态库(共享库)的代码在可执行程序运行时才载入内存，在编译过程中仅简单的引用，因此代码体积比较小。&lt;/p&gt;
&lt;p&gt;不同的应用程序如果调用相同的库,那么在内存中只需要有一份该动态库(共享库)的实例。
&lt;strong&gt;静态库和动态库的最大区别,静态情况下,把库直接加载到程序中,而动态库链接的时候,它只是保留接口,将动态库与程序代码独立,这样就可以提高代码的可复用度，和降低程序的耦合度。&lt;/strong&gt;
静态库在程序&lt;strong&gt;编译&lt;/strong&gt;时会被连接到目标代码中，程序运行时将不再需要该静态库。
动态库在程序编译时并不会被连接到目标代码中，而是在程序&lt;strong&gt;运行&lt;/strong&gt;时才被载入，因此在程序运行时还需要动态库存在。&lt;/p&gt;
</content:encoded></item><item><title>makefile文件相关知识</title><link>https://sterben-01.github.io/posts/makefile/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/makefile/</guid><pubDate>Thu, 12 May 2022 02:30:00 GMT</pubDate><content:encoded>&lt;h1&gt;makefile文件相关知识&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=1342840047&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;文件命名&lt;/h3&gt;
&lt;p&gt;makefile或者Makefile&lt;/p&gt;
&lt;h3&gt;Makefile规则&lt;/h3&gt;
&lt;p&gt;一个makefile文件中可以有一个或者多个规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;目标 ...: 依赖...
	命令(Shell命令)
	...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目标：最终要生成的文件。&lt;strong&gt;（伪目标除外）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;依赖：生成目标所需要用到的文件或者是目标&lt;/p&gt;
&lt;p&gt;命令：通过执行命令对依赖操作生成目标（命令前必须用Tab缩进）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般来说，makefile中的其它规则一般都是为第一条规则服务的&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;h3&gt;自定义变量&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;变量名 = 变量值
var = hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;预定义变量&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;AR: 归档维护程序的名称，默认值为ar
CC: C编译器的名称，默认值为cc
CXX: C++编译器的名称，默认为g++
$@: 目标的完整名称
$&amp;lt;: 第一个依赖文件的名称
$^: 所有的依赖文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;获取变量的值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$(变量名)
例如：
var = hello
$(var)就会获取var的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;自动变量只能在规则的命令中使用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app:main.c a.c b.c
	$(CC) -c $^ -o $@
这里$^是获取所有的依赖。就是main.c a.c b.c
$@ 目标的完整名称就是app
注意这里第一行的依赖名称不可以使用自动变量如 $@ $&amp;lt; $^
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;makefile示例&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;尽量不要在第一个规则的命令里直接用一串的.c 因为makefile可以检测哪个文件改动了，针对那一个文件进行生成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果都写在一起，会每次都重新生成所有文件。因为命令在一行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果按照下图这种.o然后分开写则会分步执行。没有更改过的文件不会重新生成&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app:sub.o add.o mult.o div.o main.o //注意这里直接执行.o文件 但是我们没有，需要在下面规则写从.c到.o的方法
	gcc sub.o add.o mult.o div.o main.o -o app
sub.o:sub.c
	gcc -c sub.c -o sub.o
add.o:add.c
	gcc -c add.c -o add.o
mult.o:mult.c
	gcc -c mult.c -o mult.o
div.o:div.c
	gcc -c div.c -o div.o
main.o:main.c
	gcc -c main.c -o main.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现这样非常繁琐。我们可以换一种方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src = sub.o add.o mult.o div.o main.o
target = app
//app:sub.o add.o mult.o div.o main.o
	//gcc sub.o add.o mult.o div.o main.o -o app
$(target):$(src)
	$(CC) $(src) -o $(target)
sub.o:sub.c
	gcc -c sub.c -o sub.o
add.o:add.c
	gcc -c add.c -o add.o
mult.o:mult.c
	gcc -c mult.c -o mult.o
div.o:div.c
	gcc -c div.c -o div.o
main.o:main.c
	gcc -c main.c -o main.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现这样分开处理下面的依赖依然非常繁琐。我们可以引入模式匹配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%.o:%.c
这里的%通配符是匹配一个字符串。注意这里前后的两个%匹配的是同一个字符串。
所以我们可以开始这样写
%.o:%.c
	gcc -c $&amp;lt; -o $@ //这里$&amp;lt;指的是第一个依赖文件的名称。我们这儿只有一个所以就用这个

放入示例就是
%.o:%.c
	$(CC) -c $&amp;lt; -o $@
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后我们的makefile就会变成这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src = sub.o add.o mult.o div.o main.o
target = app
$(target):$(src)
	$(CC) $(src) -o $(target)
%.o:%.c
	$(CC) -c $&amp;lt; -o $@
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是到这儿了，我们还是有一个问题。第一行的&lt;code&gt;src&lt;/code&gt;依旧有一大串需要自己写。我们就引入函数功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(wildcard PATTERN...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数详细信息：&lt;/p&gt;
&lt;p&gt;​	功能：获取指定目录下指定类型的文件列表&lt;/p&gt;
&lt;p&gt;​	参数：&lt;code&gt;PATTERN&lt;/code&gt;指的是某个或者多个目录下的对应的某种类型的文件。如果有多个目录，一般使用空格间隔&lt;/p&gt;
&lt;p&gt;​	返回：得到的若干个文件的文件列表。文件名之间使用空格间隔&lt;/p&gt;
&lt;p&gt;​	示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(wildcard *.c ./sub/*.c) 这行指的是获取当前目录下所有的.c文件和 ./sub目录下所有的.c文件
返回值格式是
a.c b.c c.c d.c e.c f.c ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这里我们需要&lt;code&gt;.o&lt;/code&gt;文件。但是&lt;code&gt;.o&lt;/code&gt;文件还没有被生成，需要靠着下面的子规则生成。我们怎么办？我们可以引入匹配替换函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(patsubst &amp;lt;pattern&amp;gt;, &amp;lt;replacement&amp;gt;, &amp;lt;text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数详细信息：&lt;/p&gt;
&lt;p&gt;​	功能：查找&lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt;中的单词（单词以空格，tab，回车或换行分割）是否符合模式&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;。如果匹配，则以&lt;code&gt;&amp;lt;replacement&amp;gt;&lt;/code&gt;替换&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;可以包括通配符%表示任意长度的子串。如果&lt;code&gt;&amp;lt;replacement&amp;gt;&lt;/code&gt;中也包含% 那么&lt;code&gt;&amp;lt;replacement&amp;gt;&lt;/code&gt;中的这个%和&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;中的%将是一个子串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	返回：函数返回被替换过后的子字符串&lt;/p&gt;
&lt;p&gt;​	示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(patsubst %.c, %.o, x.c bar.c)
意思是将x.c 和bar.c里面的.c替换成.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这之后，我们就可以这样写了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src = $(wildcard ./*.c) //先获取路径下所有.c文件
objs = $(patsubst %.c, %.o, $(src)) //把.c都换成.o
target = app
$(target):$(objs)
	$(CC) $(objs) -o $(target)
%.o:%.c
	$(CC) -c $&amp;lt; -o $@
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;清理阶段&lt;/h2&gt;
&lt;p&gt;我们发现编译后生成了很多无用的中间文件 比如&lt;code&gt;.o&lt;/code&gt;文件。我们可以在上面的文件里面加入一个clean规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clean:
	rm $(objs) -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意，这里直接运行makefile是不会自动执行clean规则，因为makefile不会执行任何与第一条规则无关的指令&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以我们需要显式使用即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意。这里我们会生成一个&lt;code&gt;clean&lt;/code&gt;文件。这样做不仅没有意义，而且会导致报错如&lt;code&gt;clean已是最新&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们可以使用生成伪目标来解决这个问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.PHONY:clean
clean:
	rm $(objs) -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做就解决了所有问题&lt;/p&gt;
&lt;h2&gt;整体文件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;//获取变量阶段
src = $(wildcard ./*.c) //先获取路径下所有.c文件
objs = $(patsubst %.c, %.o, $(src)) //把.c都换成.o
target = app
//第一条规则（主规则）
$(target):$(objs)
	$(CC) $(objs) -o $(target)
//子规则（处理依赖文件）	
%.o:%.c
	$(CC) -c $&amp;lt; -o $@
//清理阶段
.PHONY:clean
clean:
	rm $(objs) -f
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C++ 重载 Operator new() delete()</title><link>https://sterben-01.github.io/posts/C++_operator_new/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/C++_operator_new/</guid><pubDate>Sun, 08 May 2022 01:30:00 GMT</pubDate><content:encoded>&lt;h1&gt;重载 Operator new() delete()&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=1342840047&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-07_C++_operator_new.assets/QQ%E6%88%AA%E5%9B%BE20220507233819.png&quot; alt=&quot;QQ截图20220507233819&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;重载operator new（）&lt;/h3&gt;
&lt;p&gt;重载operator new() 的时候一定要注意，有多个版本的时候每一个重载版本都要有独特的不一样的参数列表。而且参数列表的第一个参数必须是__size_t__&lt;/p&gt;
&lt;p&gt;剩下的参数必须不一样。&lt;img src=&quot;/assets/blog_res/2022-05-07_C++_operator_new.assets/QQ%E6%88%AA%E5%9B%BE20220507233809-16519848048475.png&quot; alt=&quot;QQ截图20220507233809&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示。有四个版本的operator new()重载。每一个版本的第一个参数都是size_t。剩余的参数都是不一样的参数。&lt;/p&gt;
&lt;h3&gt;重载Operator delete()&lt;/h3&gt;
&lt;p&gt;这一步不是必须的。因为不会被delete调用。主要是释放未能完全创建成功的对象所占用的内存。也就是构造函数出现异常的时候。&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（3）--进程控制</title><link>https://sterben-01.github.io/posts/OS6/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS6/</guid><pubDate>Tue, 03 May 2022 02:30:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（3）--进程控制&lt;/h1&gt;
&lt;h2&gt;进程控制&lt;/h2&gt;
&lt;h3&gt;进程控制是通过原语来实现的&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS6.assets/QQ%E6%88%AA%E5%9B%BE20220502223333.png&quot; alt=&quot;QQ截图20220502223333&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程创建原语&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS6.assets/QQ%E6%88%AA%E5%9B%BE20220502223353.png&quot; alt=&quot;QQ截图20220502223353&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程终止原语&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS6.assets/QQ%E6%88%AA%E5%9B%BE20220502223411.png&quot; alt=&quot;QQ截图20220502223411&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程阻塞和唤醒原语&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS6.assets/QQ%E6%88%AA%E5%9B%BE20220502223423.png&quot; alt=&quot;QQ截图20220502223423&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程切换原语&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS6.assets/QQ%E6%88%AA%E5%9B%BE20220502223442.png&quot; alt=&quot;QQ截图20220502223442&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS6.assets/QQ%E6%88%AA%E5%9B%BE20220502223506.png&quot; alt=&quot;QQ截图20220502223506&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程（2）--进程的状态与转换</title><link>https://sterben-01.github.io/posts/OS5%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS5%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81/</guid><pubDate>Tue, 03 May 2022 01:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程（2）--进程的状态与转换&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=30780806&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;进程的状态与转换&lt;/h2&gt;
&lt;p&gt;五状态模型：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS5.assets/QQ%E6%88%AA%E5%9B%BE20220502215008.png&quot; alt=&quot;QQ截图20220502215008&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS5.assets/QQ%E6%88%AA%E5%9B%BE20220502215018.png&quot; alt=&quot;QQ截图20220502215018&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-进程管理（1）--进程</title><link>https://sterben-01.github.io/posts/OS4/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS4/</guid><pubDate>Tue, 03 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-进程管理（1）--进程&lt;/h1&gt;
&lt;h2&gt;进程的特征&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;动态性。 进程是程序的一次执行过程，是动态地产生，变化和消亡的。&lt;strong&gt;动态性是进程最基本的特征。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并发性。内存中有多个进程实体。各个进程可以并发执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;独立性。进程是能够独立运行，独立获得资源，独立接受调度的基本单位。&lt;strong&gt;进程是资源分配，接受调度的基本单位。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步性。各个进程按照各自独立的，不可预知的速度向前推进，操作系统需要提供进程同步来解决异步问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结构性。每个进程都会配置一个PCB（进程控制模块）。结构上看，进程是由程序段，数据段，PCB组成。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;线程和进程的区别&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;线程是程序执行的最小单位，而进程是操作系统分配资源的最小单位。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个进程由一个或多个线程组成，线程是一个进程中代码的不同执行路线。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程之间相互独立，但同一进程下的各个线程之间共享程序的内存空间(包括代码段，数据集，堆等)及一些进程级的资源(如打开文件和信 号等)，某进程内的线程在其他进程不可见。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调度和切换：线程上下文切换比进程上下文切换要快得多。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS4.assets/QQ%E6%88%AA%E5%9B%BE20220502202633.png&quot; alt=&quot;QQ截图20220502202633&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-系统调用</title><link>https://sterben-01.github.io/posts/OS3/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS3/</guid><pubDate>Mon, 02 May 2022 23:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-系统调用&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=34834953&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS3.assets/QQ%E6%88%AA%E5%9B%BE20220502200904.png&quot; alt=&quot;QQ截图20220502200904&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;陷入指令是唯一一个只能在用户态执行而不可以在核心态执行的指令&lt;/h2&gt;
</content:encoded></item><item><title>王道操作系统笔记-中断与异常</title><link>https://sterben-01.github.io/posts/OS2/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS2/</guid><pubDate>Mon, 02 May 2022 22:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-中断与异常&lt;/h1&gt;
&lt;h2&gt;发生了中断，就意味着需要操作系统介入。&lt;/h2&gt;
&lt;h3&gt;用户态和核心态之间的切换是通过中断实现的。并且，中断是实现状态切换的唯一途径。&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;当中断发生时，CPU立即进入核心态。&lt;/li&gt;
&lt;li&gt;当中断发生后，当前运行的进程暂停运行，并由操作系统内核对中断进行处理。&lt;/li&gt;
&lt;li&gt;对于不同的中断信号，会进行不同的处理。&lt;/li&gt;
&lt;li&gt;CPU执行完每一条指令后都会检查是否有中断信号。除执行了关中断指令外。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS2.assets/QQ%E6%88%AA%E5%9B%BE20220502194750.png&quot; alt=&quot;QQ截图20220502194750&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS2.assets/QQ%E6%88%AA%E5%9B%BE20220502194903.png&quot; alt=&quot;QQ截图20220502194903&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS2.assets/QQ%E6%88%AA%E5%9B%BE20220502195155.png&quot; alt=&quot;QQ截图20220502195155&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS2.assets/QQ%E6%88%AA%E5%9B%BE20220502195208.png&quot; alt=&quot;QQ截图20220502195208&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>王道操作系统笔记-操作系统的运行机制与体系结构</title><link>https://sterben-01.github.io/posts/OS/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/OS/</guid><pubDate>Mon, 02 May 2022 21:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;王道操作系统笔记-操作系统的运行机制与体系结构&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=32574583&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;操作系统特征：并发，共享，虚拟，异步。&lt;/h2&gt;
&lt;p&gt;其中，共享分为__互斥共享方式(摄像头设备的共享使用)__ 和 &lt;strong&gt;同时共享方式(硬盘资源的共享使用)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;虚拟分为__空分复用(虚拟储存技术)__ 和 &lt;strong&gt;时分复用(虚拟处理器技术)&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;CPU指令分为特权指令和非特权指令&lt;/h2&gt;
&lt;p&gt;特权指令如内存清零指令&lt;/p&gt;
&lt;p&gt;非特权指令如普通运算指令&lt;/p&gt;
&lt;h2&gt;处理器状态分为用户态和核心态&lt;/h2&gt;
&lt;p&gt;处理器处于用户态的时候 CPU只能执行非特权指令&lt;/p&gt;
&lt;p&gt;处理器处于核心态的时候 CPU可以执行非特权指令和特权指令&lt;/p&gt;
&lt;h2&gt;所以程序内核程序和应用程序&lt;/h2&gt;
&lt;p&gt;内核程序可以执行特权指令和非特权指令。所以内核程序运行在核心态&lt;/p&gt;
&lt;p&gt;应用程序仅可以执行非特权指令。所以引用程序运行在用户态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS.assets/QQ%E6%88%AA%E5%9B%BE20220502185501.png&quot; alt=&quot;QQ截图20220502185501&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS.assets/QQ%E6%88%AA%E5%9B%BE20220502185640.png&quot; alt=&quot;QQ截图20220502185640&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2022-05-02-OS.assets/QQ%E6%88%AA%E5%9B%BE20220502185958.png&quot; alt=&quot;QQ截图20220502185958&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>网易云外链插入Jekyll博客教程和注意事项</title><link>https://sterben-01.github.io/posts/music-test/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/music-test/</guid><pubDate>Sun, 01 May 2022 07:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;网易云外链插入Jekyll博客教程和注意事项&lt;/h1&gt;
&lt;p&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot; src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=28723836&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;这里是教程：&lt;/p&gt;
&lt;p&gt;首先找到网易云的外链生成，这里不多说了。生成了的外链大概长这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=330 height=86
        src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=28723836&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有几个雷区：&lt;/p&gt;
&lt;p&gt;1：需要修改&lt;code&gt;width=330 height=86&lt;/code&gt;为 &lt;code&gt;width=&quot;330&quot; height=&quot;86&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;2：链接前面不要加http也不要加https，会自动解析。&lt;/p&gt;
&lt;p&gt;3：最后面一个&lt;code&gt;&amp;lt;\iframe&amp;gt;&lt;/code&gt;要和前面的&lt;code&gt;&amp;gt;&lt;/code&gt;中间隔一个空格。&lt;/p&gt;
&lt;p&gt;4：海外用户由于版权原因无法播放外链音频，表现形式为点不动。只能看见播放器。&lt;/p&gt;
&lt;p&gt;修改后的链接大概长这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe frameborder=&quot;no&quot; border=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; width=&quot;330&quot; height=&quot;86&quot;
        src=&quot;//music.163.com/outchain/player?type=2&amp;amp;id=28723836&amp;amp;auto=1&amp;amp;height=66&quot;&amp;gt; &amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>太脑瘫了！</title><link>https://sterben-01.github.io/posts/new/</link><guid isPermaLink="true">https://sterben-01.github.io/posts/new/</guid><pubDate>Sat, 30 Apr 2022 10:34:00 GMT</pubDate><content:encoded>&lt;h1&gt;折腾了五个小时发现为什么不能部署了&lt;/h1&gt;
&lt;p&gt;这里每次部署都会弹出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;remote: Permission to Sterben-01/Sterben-01.github.io.git denied to github-actions[bot]. 
fatal: unable to access https://github.com/Sterben-01/Sterben-01.github.io/: The requested URL returned error: 403 
Error: Process completed with exit code 128.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果折腾了五个小时偶然间发现我的仓库的Workflow permission 是tm只读的。赶紧改过来了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/blog_res/2021-04-30-new.assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220430053643.png&quot; alt=&quot;微信图片_20220430053643&quot; /&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>