首页 C++11 的常用特性
文章
取消

C++11 的常用特性

前言

C++ 11 常用的特性有:

  • auto:自动类型推导,可以根据等号右边的类型推导出变量的类型,简化代码。
  • decltype 关键字:可以根据表达式的类型推导出变量或函数的返回值类型。
  • lambda:匿名函数,可以在需要的地方定义和使用函数,提高灵活性和效率。
  • 智能指针:自动管理内存的指针,可以避免内存泄漏和悬空指针。
  • nullptr:空指针字面量,可以替代0或NULL,避免歧义。
  • 范围for循环:可以遍历容器或数组中的所有元素,简化代码。
  • 右值引用:移动语义,可以减少不必要的拷贝和构造,提高性能。
  • 无序容器:基于哈希表实现的容器,比如 unordered_map 和 unordered_set ,可以提供快速的查找和插入操作。
  • 线程支持:语言级别的多线程库,可以方便地创建和管理线程,实现并发编程。
  • constexpr:常量表达式,可以在编译期计算出常量值,提高效率。
  • long long类型:可以表示 64 位整数4。
  • 可变模板:可以定义接受任意数量和类型参数的模板类或函数

auto

auto 关键字是 C++ 11 引入的一个新特性,它可以让编译器根据右边的类型推导出变量的类型。这样可以简化代码,避免重复写出变量的类型。例如:

1
2
3
auto x = 10; // x 的类型为 int
auto y = 3.14; // y 的类型为 double
auto z = x + y; // z 的类型为 double

使用 auto 关键字时,有一些注意事项:

  • auto 变量必须在定义时初始化。

  • 定义在一个 auto 序列的变量必须始终推导成同一类型。

    如果你在一行代码中用 auto 声明了多个变量,并用逗号分隔,那么这些变量必须是同一种类型。例如:

    1
    2
    
    auto a = 1, b = 2, c = 3; // 正确,a、b、c 都是 int 类型
    auto x = 1, y = 2.0, z = 'a'; // 错误,x、y、z 不是同一种类型
    

    如果你想声明不同类型的变量,你可以用不同的行或者用花括号初始化。例如:

    1
    2
    3
    4
    5
    
    auto a = 1; // int 类型
    auto b = 2.0; // double 类型
    auto c = 'a'; // char 类型
      
    auto x{1}, y{2.0}, z{'a'}; // x 是 int 类型,y 是 double 类型,z 是 char 类型
    
  • 如果初始化表达式是引用,则去除引用语义,除非 auto 带上 & 号。

    如果你用 auto 声明一个变量,并用一个引用类型的表达式来初始化它,那么这个变量的类型会是引用类型的基础类型。例如:

    1
    2
    3
    
    int a = 10; // a 是 int 类型
    int &b = a; // b 是 int & 类型,也就是 int 的引用
    auto c = b; // c 的类型会被推导为 int,而不是 int &(去除引用)
    

    如果你想保留引用的语义,你可以在 auto 后面加上 & 符号。例如:

    1
    2
    3
    4
    
    int a = 10; // a 是 int 类型
    int &b = a; // b 是 int & 类型,也就是 int 的引用
    auto &c = b; // c 的类型会被推导为 int &(保留引用)
      
    
  • 如果初始化表达式是 const 或 volatile,则除去 const/volatile 语义,除非 auto 带上 & 号。

    如果你用 auto 声明一个变量,并用一个 const 或 volatile 类型的表达式来初始化它,那么 auto 会自动去掉 const 或 volatile 修饰符,除非你在 auto 后面加上 & 符号,表示你想要保留 const 或 volatile 语义。

    例如:

    1
    2
    3
    
    const int a = 10; // a 是一个 const int 类型的变量
    auto b = a; // b 的类型由 auto 决定,因为 a 是 const 类型,所以 auto 会去掉 const 语义,b 的类型是 int
    auto &c = a; // c 的类型由 auto 决定,因为 a 是 const 类型,并且 auto 后面加了 & 符号,所以 auto 会保留 const 语义,c 的类型是 const int &
    
  • 初始化表达式为数组时,auto 关键字推导类型为指针,除非 auto 带上 & 号。

    如果你用 auto 声明一个变量,并用一个数组来初始化它,那么 auto 会自动推导出指针类型,指向数组的第一个元素,除非你在 auto 后面加上 & 符号,表示你想要保留数组类型。

    举个例子:

    1
    2
    3
    
    int a[5] = {1, 2, 3, 4, 5}; // a 是一个 int 类型的数组
    auto b = a; // b 的类型由 auto 决定,因为 a 是数组类型,所以 auto 会推导出指针类型,b 的类型是 int *
    auto &c = a; // c 的类型由 auto 决定,因为 a 是数组类型,并且 auto 后面加了 & 符号,所以 auto 会保留数组类型,c 的类型是 int (&)[5]
    

    此时变量的含义为:

    • a 是一个数组,你可以用下标来访问它的元素,比如 a[0] 表示第一个元素,a[4] 表示最后一个元素。
    • b 是一个指针,你可以用 * 来解引用它,得到它所指向的值,比如 *b 表示第一个元素。你也可以用 ++ 或 – 来移动指针的位置,比如 ++b 就会让 b 指向第二个元素。
    • c 是一个数组的引用,你可以像操作数组一样操作它,比如 c[0] 表示第一个元素,c[4] 表示最后一个元素。
  • 函数或者模板参数不能被声明为 auto

auto 的应用场景

  • 当你不知道或者不想写出变量的具体类型时,可以用 auto 来简化代码,让编译器自动推导类型。
  • 当你需要声明一个复杂的类型,比如迭代器、模板类、函数指针等时,可以用 auto 来避免写出冗长的类型名。
  • 当你需要根据不同的条件来初始化变量时,可以用 auto 来保证变量的类型和初始化表达式一致。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 不知道或者不想写出具体类型
auto x = 3.14; // x 的类型是 double
auto y = "hello"; // y 的类型是 const char *

// 声明复杂的类型
std::vector<int> v = {1, 2, 3};
auto it = v.begin(); // it 的类型是 std::vector<int>::iterator
auto f = std::sqrt; // f 的类型是 double (*)(double)

// 根据条件初始化变量
int a = 10;
double b = 20.0;
bool c = true;
auto d = c ? a : b; // d 的类型是 double

delctype

C++11 的 decltype 是一个关键字,用来在编译时推导出一个表达式的类型。它可以用在任何需要一个类型的地方,比如声明变量、函数模板、尾随返回类型等。

  • 声明变量:

    1
    2
    
    int var;
    decltype(var) x = 10; // x 的类型是 int
    
  • 函数模板/尾随返回类型:

    1
    2
    3
    4
    
    template <typename Container, typename Index>
    auto authAndAccess(Container &c, Index i) -> decltype(c[i]) {
      return c[i];
    }
    

delctype 和 auto 的区别

decltype 和 auto 都是 C++ 11 新增的关键字,都用于自动类型推导,但是它们的语法格式和推导规则是有区别的。

  • auto 是根据变量的初始值来推断类型,而 decltype 是根据表达式来推断类型,不需要计算表达式的值。
  • auto 会忽略顶层 const 和引用,而 decltype 会保留顶层 const 和引用。
  • auto 可以用于函数参数和返回类型,而 decltype 只能用于返回类型。

举个例子:

1
2
3
4
5
int i = 0;
const int& a = i;
auto b = a; // b 的类型是 int
auto &c = a; // c 的类型是 const int&
decltype(auto) d = a; // d 的类型是 const int&

有了 auto 为什么还要 decltype

decltype 是为了解决泛型编程中有些类型由模板参数决定而难以(甚至不可能)表示的问题。它可以精确地推导出表达式的类型,包括顶层 const 和引用。这样可以避免类型信息的丢失,保持类型的一致性。

比如,如果你想写一个泛型函数,根据不同的容器和索引返回对应的元素,你可能会这样写:

1
2
3
4
5
template<typename Container, typename Index>
auto get(Container& c, Index i) -> decltype(c[i])
{
    return c[i];
}

这里用到了 decltype 来推导返回类型,因为不同的容器可能有不同的元素类型,而且还要考虑是否是引用或者 const。如果用 auto 来推导返回类型,就会忽略掉这些信息。

lamda 匿名函数

C++11 中的 lambda 表达式是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法。它可以使程序员定义没有名字的临时函数,该函数是一次性执行的,既方便了编程,又能防止别人的访问。

lambda 表达式具体形式如下:

1
[capture] (parameters)->return-type {body}

如果没有参数,空的圆括号 () 可以省略。返回值也可以省略,如果函数体只有一条 return 语句,则返回值类型由编译器推断出来。

capture 是捕获列表,用于指定 lambda 表达式可以访问哪些外部变量以及如何访问它们。有以下几种形式:

  • [] 空捕获列表,表示不捕获任何变量。
  • [x, &y] 按值捕获 x ,按引用捕获 y 。
  • [&] 按引用捕获所有外部变量。
  • [=] 按值捕获所有外部变量。
  • [&, x] 按引用捕获所有外部变量,但按值捕获 x 。
  • [=, &z] 按值捕获所有外部变量,但按引用捕获 z

一个简单的 lambda 表达式,用于计算两个数的和:

1
2
auto plus = [] (int v1, int v2) -> int { return v1 + v2; };
int sum = plus (1, 2); // sum = 3

一个使用 lambda 表达式作为参数的例子,用于对一个 vector 进行排序:

1
2
3
4
5
6
7
8
9
10
11
#include <vector>
#include <algorithm>
struct Student {
    std::string name;
    int score;
};

// 按分数从高到低排序
std::sort(students.begin(), students.end(), [] (const Student& a, const Student& b) -> bool {
    return a.score > b.score;
});

一个使用捕获列表的例子,用于访问外部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
int main() {
    int x = 10;
    auto add_x = [x] (int y) -> int { return x + y; }; // 按值捕获 x
    std::cout << add_x(5) << std::endl; // 输出 15
    x = 20;
    std::cout << add_x(5) << std::endl; // 输出还是 15,因为 x 的值在定义 lambda 表达式时就已经确定了

    int z = 10;
    auto add_z = [&z] (int y) -> int { return z + y; }; // 按引用捕获 z
    std::cout << add_z(5) << std::endl; // 输出 15
    z = 20;
    std::cout << add_z(5) << std::endl; // 输出 25,因为 z 的值在调用 lambda 表达式时才确定
}

智能指针

C++ 11 智能指针是一种新的特性,它可以自动管理堆内存的申请和释放,避免内存泄漏。C++ 11 提供了三种智能指针:std::shared_ptr, std::unique_ptr, std::weak_ptr。它们的区别和用法如下:

  • std::shared_ptr 是最常用的智能指针类型,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。当引用计数为零时,内存资源被释放。

    创建 shared_ptr 的方法:

    • 方式一:直接使用 new 操作符初始化 shared_ptr,例如:shared_ptr<string> pTom{new string("tom")};
    • 方式二:先创建一个空的 shared_ptr,然后使用 reset 方法指定内存资源,例如:shared_ptr<string> pTom; pTom.reset(new string("tom"));
    • 方式三:使用 make_shared 函数模板创建 shared_ptr,例如:shared_ptr<string> pTom = make_shared<string>("tom");

    推荐使用方式三,因为它更快(一次复制),更安全

    使用 shared_ptr 时,可以像普通指针一样访问其所指向的对象,例如:cout << *pTom << endl; 或者 pTom->size();

  • std::unique_ptr 是一种独占式的智能指针,它不允许拷贝和赋值,只有一个智能指针可以拥有同一个内存资源。当智能指针销毁时,内存资源被释放。

    创建和使用 unique_ptr 的方法如下:

    • 创建一个非空的 unique_ptr 对象,需要在构造函数中传递一个原始指针,例如:
    1
    2
    
    // 通过原始指针创建一个 unique_ptr 对象
    std::unique_ptr<int> uptr(new int(3));
    
    • 使用 std::make_unique 函数来创建一个 unique_ptr 对象,这样可以避免直接使用 new 操作符,例如:
    1
    2
    
    // 通过 std::make_unique 函数创建一个 unique_ptr 对象
    std::unique_ptr<int> uptr = std::make_unique<int>(3);
    
    • 使用 unique_ptr 对象时,可以通过解引用操作符(*)或箭头操作符(->)来访问它所指向的对象,例如:
    1
    2
    3
    
    // 访问 unique_ptr 所指向的对象
    std::cout << *uptr << std::endl; // 输出 3
    uptr->do_something(); // 调用对象的成员函数
    
    • 如果需要将 unique_ptr 对象传递给其他函数或容器,不能直接复制它,因为它不支持复制语义。只能通过移动语义来转移所有权,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    // 将 unique_ptr 对象移动给另一个 unique_ptr 对象
    std::unique_ptr<int> uptr2 = std::move(uptr); // uptr2 接管了 uptr 的所有权,uptr 变为空
    
    // 将 unique_ptr 对象移动给一个函数的参数
    void foo(std::unique_ptr<int> p) {
        // 在函数内部使用 p
    }
    
    foo(std::move(uptr2)); // 将 uptr2 的所有权转移给 p,uptr2 变为空
    
    // 将 unique_ptr 对象移动给一个容器的元素
    std::vector<std::unique_ptr<int>> vec;
    vec.push_back(std::move(uptr)); // 将 uptr 的所有权转移给 vec 的元素,uptr 变为空
    
  • std::weak_ptr 是一种辅助式的智能指针,它不会增加引用计数,也不会影响内存资源的释放。它只是观察其他 shared_ptr 所管理的内存资源,并可以在需要时转换为 shared_ptr 来使用。

    创建和使用 weak_ptr 的方法如下:

    • 创建一个 weak_ptr 对象,需要从一个已经存在的 shared_ptr 对象构造,例如:
    1
    2
    3
    
    // 通过 shared_ptr 对象创建一个 weak_ptr 对象
    std::shared_ptr<int> sptr = std::make_shared<int>(3);
    std::weak_ptr<int> wptr(sptr); // wptr 指向 sptr 所指向的对象,但不增加引用计数
    
    • 使用 weak_ptr 对象时,不能直接访问它所指向的对象,因为它不保证对象的有效性。需要先将它转换为一个临时的 shared_ptr 对象,然后通过这个 shared_ptr 对象来访问,例如:
    1
    2
    3
    4
    5
    6
    7
    
    // 通过 weak_ptr 对象访问它所指向的对象
    if (auto tmp = wptr.lock()) { // 尝试将 wptr 转换为 shared_ptr 对象
       std::cout << *tmp << std::endl; // 输出 3
       tmp->do_something(); // 调用对象的成员函数
    } else {
       std::cout << "The object is expired." << std::endl; // 如果转换失败,说明对象已经被释放
    }
    
    • 使用 weak_ptr 的目的是为了避免 shared_ptr 之间形成循环引用导致内存泄漏。如果两个或多个 shared_ptr 对象相互指向对方,那么它们的引用计数永远不会变为零,即使没有其他外部的 shared_ptr 指向它们。这时可以将其中一个或多个 shared_ptr 替换为 weak_ptr 来打破循环引用,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    // 使用 weak_ptr 来打破循环引用
    class A;
    class B;
    
    class A {
    public:
       std::shared_ptr<B> bptr;
       ~A() {
           std::cout << "A is destroyed." << std::endl;
       }
    };
    
    class B {
    public:
       std::weak_ptr<A> aptr; // 使用 weak_pt 而不是 shared_pt
       ~B() {
           std::cout << "B is destroyed." << std::endl;
       }
    };
    
    int main() {
       auto a = std::make_shared<A>();
       auto b = std::make_shared<B>();
       a->bptr = b; // a 指向 b
       b->aptr = a; // b 指向 a
    
    } // 离开作用域后,a 和 b 都被销毁了
    
    

为什么要使用智能指针而不是普通指针

使用智能指针而不是普通指针的原因有以下几点:

  • 智能指针可以自动管理内存的分配和释放,避免手动调用 new 和 delete,从而减少内存泄漏和空悬指针的风险 。

    空悬指针是指指针指向的内存空间已被释放或不再有效。这种情况可能导致程序出错或崩溃。

    空悬指针与野指针有所不同:

    • 空悬指针是指向被释放或无效的内存的指针
    • 野指针式不确定其具体指向的指针,通常来自于未初始化的指针
    • 空悬指针和野指针都可能导致程序错误或崩溃,所以应该避免使用它们
  • 智能指针可以提供更高级的功能,如引用计数,所有权转移,自定义删除器等,使得内存管理更灵活和安全 。

  • 智能指针可以与 C++ 标准库中的容器和算法协作,实现更高效和简洁的编程。

假设我们有一个类 Person,它有一个成员变量 name,我们想要在堆上创建一个 Person 对象,并打印它的名字。如果我们使用普通指针,我们需要这样写:

1
2
3
4
5
6
7
8
9
10
11
class Person {
public:
    string name;
    Person(string n) : name(n) {}
};

int main() {
    Person* p = new Person("Alice"); // 在堆上分配内存
    cout << p->name << endl; // 打印名字
    delete p; // 释放内存
}

这段代码看起来很简单,但是有几个潜在的问题:

  • 如果忘记调用 delete,就会造成内存泄漏。
  • 如果在 delete 之前 p 被赋值为另一个指针或者 nullptr,就会造成空悬指针。
  • 如果有多个指针指向同一个对象,就需要小心地管理它们的生命周期,避免重复释放或提前释放。

如果我们使用智能指针,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
class Person {
public:
    string name;
    Person(string n) : name(n) {}
};

int main() {
    unique_ptr<Person> p = make_unique<Person>("Alice"); // 使用 make_unique 函数创建一个 unique_ptr 对象
    cout << p->name << endl; // 打印名字
} // 出了作用域后,p 自动调用析构函数释放内存

这段代码看起来更简洁和安全,它避免了上述的问题:

  • 不需要手动调用 delete,unique_ptr 会自动管理内存的分配和释放。
  • 不需要担心空悬指针,unique_ptr 是独占所有权的指针,不能被复制或赋值。
  • 不需要担心重复释放或提前释放,unique_ptr 只有一个拥有者,在合适的时机销毁对象。

weak_ptr 不影响对象的生命周期

weak_ptr 不影响对象的生命周期是指 weak_ptr 不会延长或缩短对象的存在时间。对象的生命周期只取决于引用计数,而 weak_ptr 不会增加引用计数。当所有的 shared_ptr 都被销毁时,对象也会被销毁,不管有没有 weak_ptr 指向它。这样可以避免循环引用导致的内存泄漏。

例如,假设我们有两个类 A 和 B,它们互相持有对方的 shared_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class B; // 前向声明

class A {
public:
    shared_ptr<B> bptr;
    A() { cout << "A constructor" << endl; }
    ~A() { cout << "A destructor" << endl; }
};

class B {
public:
    shared_ptr<A> aptr;
    B() { cout << "B constructor" << endl; }
    ~B() { cout << "B destructor" << endl; }
};

如果我们在 main 函数中这样写:

1
2
3
4
5
6
int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->bptr = b;
    b->aptr = a;
}

那么我们会发现,当 a 和 b 出了作用域后,并没有调用 A 和 B 的析构函数,也就是说,它们所指向的对象并没有被销毁。这是因为 a 和 b 互相持有对方的 shared_ptr,导致引用计数永远不为零。

如果我们把其中一个类改成使用 weak_ptr 而不是 shared_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class B; // 前向声明

class A {
public:
    weak_ptr<B> bptr;
    A() { cout << "A constructor" << endl; }
    ~A() { cout << "A destructor" << endl; }
};

class B {
public:
    shared_ptr<A> aptr;
    B() { cout << "B constructor" << endl; }
    ~B() { cout << "B destructor" << endl; }
};

那么我们会发现,当 a 和 b 出了作用域后,就调用了 A 和 B 的析构函数,也就是说,它们所指向的对象被正常地销毁了。这是因为 weak_ptr 不会增加引用计数,所以当 a 或者 b 被销毁时,它们所指向的对象的引用计数就变为零了。

weak_ptr 不影响对象的生命周期

一般来说,如果您想要实现共享所有权,那么使用 shared_ptr 是合适的。但是,如果您只想要观察或访问一个对象,而不想影响它的生命周期,那么使用 weak_ptr 是更好的选择。特别是当您面对以下情况时,使用 weak_ptr 可以避免循环引用或内存泄漏:

  • 当您有一个类似树或图的数据结构时,其中的节点可能需要指向它们的父节点或兄弟节点。这种情况下,可以使用 shared_ptr 来指向子节点,而使用 weak_ptr 来指向父节点或兄弟节点。
  • 当您有一个观察者模式的设计时,其中有一个主题对象和多个观察者对象。这种情况下,可以使用 shared_ptr 来管理主题对象的生命周期,而使用 weak_ptr 来让观察者对象订阅主题对象。
  • 当您有一个缓存系统时,其中有一些键值对需要被存储和检索。这种情况下,可以使用 shared_ptr 来保存值对象的指针,并且在缓存中保存相应的 weak_ptr。这样可以避免缓存中的过期数据占用内存。

nullptr

nullptr 是 C++11 新增加的一个关键字,用于表示空指针。它是 std::nullptr_t 类型的一个右值常量,可以用来初始化任何指针类型或者指向成员的指针类型。它比 NULL 更明确地表达了空指针的含义,而不会和整数 0 混淆。

std::nullptr_t 是 C++11 中定义的一个类型,它是空指针字面量 nullptr 的类型。它不是指针类型或者指向成员的指针类型,但是它可以隐式地转换为任何这样的类型。它的大小等于 void* 的大小。有时候,我们可以用 std::nullptr_t 作为函数参数的类型,来区分 nullptr 和其他值的重载。

一个 std::nullptr_t 的例子是,我们可以用它来定义一个函数,专门处理 nullptr 作为参数的情况。例如:

1
2
3
4
5
6
7
void f(int); // 处理整数
void f(void*); // 处理非空指针
void f(std::nullptr_t); // 处理空指针

f(42); // 调用 f(int)
f(nullptr); // 调用 f(std::nullptr_t)
f(NULL); // 也调用 f(std::nullptr_t),因为 NULL 可以转换为 nullptr

另一个 std::nullptr_t 的例子是,我们可以用它来判断某些对象是否有效。例如:

1
2
3
4
std::function<void()> func; // 定义一个函数对象
if (func == nullptr) { // 判断函数对象是否为空
    std::cout << "func is empty\n";
}

为什么要传入空指针:

传入空指针的意义取决于具体的情况和语言。一般来说,空指针表示没有指向任何对象或者有效的地址。有时候,传入空指针是为了表示某种特殊的状态或者条件,例如函数参数的可选性,或者链表的结尾。有时候,传入空指针是因为程序员的疏忽或者错误,导致在运行时出现异常或者崩溃。因此,在使用空指针时,要注意检查其合法性和合理性。

nullptr 和 NULL 的不同

nullptr 是 C++11 中引入的一个关键字,它是 std::nullptr_t 类型的一个字面量,表示空指针。NULL 是一个宏,通常被定义为 0 或者 (void*)0,也表示空指针。两者都可以隐式地转换为任何指针类型或者指向成员的指针类型。

例如:

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

void f(int x) {
    std::cout << "f(int): " << x << "\n";
}

void f(int* x) {
    std::cout << "f(int*): " << x << "\n";
}

int main() {
    f(0); // 调用 f(int)
    f(NULL); // 也调用 f(int),因为 NULL 被定义为 0
    f(nullptr); // 调用 f(int*),因为 nullptr 是空指针字面量
}

如果你想让 NULL 调用 f(int),你可以显式地转换 NULL 为 int 类型,例如:

f(static_cast<int*>(NULL)); // 调用 f(int*)

但是,这样做可能会降低代码的可读性和一致性,所以建议你使用 nullptr 来表示空指针。

写在最后

感谢你在茫茫人海中找到我🕵🏼

🎉你是第 个读者

㊗️ 你平安喜乐,顺遂无忧!

希望你读完有所收获~

🥂🥂🥂

本文由作者按照 CC BY 4.0 进行授权

C++ 封装、继承 与 多态

Linux 的 epoll