C++ part third

智能指针

RAII与引用计数

  • 引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
  • 在传统 C++ 中,『记得』手动释放资源,总不是最佳实践。因为我们很有可能就忘记了去释放资源而导致泄露。 所以通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII 资源获取即初始化技术。C++11 引入智能指针的概念,让程序员不再需要关心手动释放内存。使用它们需要包含头文件memory 。

std::unique_ptr

  • std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。
    1
    2
    3
    4
    5
    6
    7
    std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
    std::unique_ptr<int> pointer2 = pointer; // 非法
    make_unique 并不复杂,C++11 没有提供 std::make_unique ,可以自行实现:
    template<typename T, typename ...Args>
    std::unique_ptr<T> make_unique( Args&& ...args ) {
    return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
    }
  • 既然是独占,换句话说就是不可复制。但是,我们可以利用 std::move 将其转移给其他的 unique_ptr 。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #include <memory>
    struct Foo {
    Foo() { std::cout << "Foo::Foo" << std::endl; }
    ~Foo() { std::cout << "Foo::~Foo" << std::endl; }
    void foo() { std::cout << "Foo::foo" << std::endl; }
    };
    void f(const Foo &) {
    std::cout << "f(const Foo&)" << std::endl;
    }
    int main() {
    std::unique_ptr<Foo> p1(std::make_unique<Foo>());
    // p1 不空, 输出
    if (p1) p1->foo();
    {
    std::unique_ptr<Foo> p2(std::move(p1));
    // p2 不空, 输出
    f(*p2);
    // p2 不空, 输出
    if(p2) p2->foo();
    // p1 为空, 无输出
    if(p1) p1->foo();
    p1 = std::move(p2);
    // p2 为空, 无输出
    if(p2) p2->foo();
    std::cout << "p2 被销毁" << std::endl;
    }
    // p1 不空, 输出
    if (p1) p1->foo();
    // Foo 的实例会在离开作用域时被销毁
    }
  • 此外,由于独占, std::unique_ptr 不会有引用计数的开销,因此常常是首选。

std::shared_ptr

  • std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete ,当引用计数变为零的时候就会将对象自动删除。
  • 但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。
  • std::make_shared 就能够用来消除显式的使用 new ,会分配创建传入参数中的对象, 并返回这个对象类型的 std::shared_ptr 指针。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    #include <memory>
    void foo(std::shared_ptr<int> i) {
    (*i)++;
    }
    int main() {
    // Constructed a std::shared_ptr
    auto pointer = std::make_shared<int>(10);
    foo(pointer);
    std::cout << *pointer << std::endl; // 11
    // The shared_ptr will be destructed before leaving the scope
    return 0;
    }
  • std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数, 并通 过 use_count() 来查看一个对象的引用计数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    auto pointer = std::make_shared<int>(10);
    auto pointer2 = pointer; // 引用计数+1
    auto pointer3 = pointer; // 引用计数+1
    int *p = pointer.get(); // 这样不会增加引用计数
    std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
    std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
    std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
    pointer2.reset();
    std::cout << "reset pointer2:" << std::endl;
    std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
    std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已
    std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
    pointer3.reset();
    std::cout << "reset pointer3:" << std::endl;
    std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
    std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
    std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3

std::weak_ptr

  • std::shared_ptr 引入了引用成环的问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    struct A;
    struct B;
    struct A {
    std::shared_ptr<B> pointer;
    ~A() {
    std::cout << "A 被销毁" << std::endl;
    }
    };
    struct B {
    std::shared_ptr<A> pointer;
    ~B() {
    std::cout << "B 被销毁" << std::endl;
    }
    };
    int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->pointer = b;
    b->pointer = a;
    // 结果是A和B依然不能被销毁,因为二者间形成了一个环,导致两个shared_ptr引用计数都为1
    }
  • 解决这个问题的办法就是使用弱引用指针 std::weak_ptr , std::weak_ptr 是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加。
  • 对于上面的代码,将 A 或 B 中的任意一个 std::shared_ptr 改为 std::weak_ptr 即可解决问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct A {
    std::shared_ptr<B> pointer;
    ~A() {
    std::cout << "A 被销毁" << std::endl;
    }
    };
    struct B {
    std::weak_ptr<A> pointer;
    ~B() {
    std::cout << "B 被销毁" << std::endl;
    }
    };
    // 将B的智能指针改为weak_ptr,这样程序结束时A的引用计数就会变成0而销毁,B的引用计数随之变为0而销毁
  • std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它可以用于检查std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false ,否则返回 true ;除此之外,它也可以用于获取指向原始对象的 std::shared_ptr 指针,其 lock() 方法在原 始对象未被释放时,返回一个指向原始对象的 std::shared_ptr 指针,进而访问原始对象的资源,否则返回默认构造的 std::shared_ptr (即未托管任何指针)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    std::weak_ptr<int> b;
    {
    auto a = std::make_shared<int>(1);
    b = a;
    if(auto c = b.lock()) { // 输出
    std::cout << *c << std::endl;
    }
    }
    if(auto c = b.expired()) { // 输出
    std::cout << "b is expired" << std::endl;
    }

模板与泛型编程

模板函数与模板类

  • 模板函数是一种特殊类型的函数,它可以接受多种类型的参数。这是通过在编译时生成不同版本的函数来实现的。
    1
    2
    3
    4
    template <typename T>
    T add(T a, T b) {
    return a + b;
    }
  • 在这个例子中,add函数是一个模板函数,它可以接受任何类型的参数(只要该类型支持+运算符)
    1
    2
    3
    4
    5
    6
    7
    8
    template <typename T>
    class MyArray {
    public:
    T arr[10];
    T get(int index) {
    return arr[index];
    }
    };
  • 在这个例子中,MyArray是一个模板类,它有一个类型为T的数组成员。
  • 模板类和模板函数一样,都是在编译时生成的。这就像是你有一个食谱,你可以用它来做不同口味的蛋糕。

模板特化:特例处理

  • 模板特化(Template Specialization)是模板编程中的一个高级特性,它允许你为某些特定类型或条件提供特殊的实现。
  • 何时使用模板特化当你有一个通用模板,但需要为某个特定类型提供不同的实现时,模板特化就派上了用场。
    1
    2
    3
    4
    5
    6
    7
    8
    template <>
    class MyArray<bool> {
    public:
    uint8_t arr[2]; // 使用位来存储bool值,以节省空间
    bool get(int index) {
    return arr[index / 8] & (1 << (index % 8));
    }
    };
  • 在这个例子中,我们为bool类型提供了一个特化的MyArray类,以节省存储空间。
  • 模板特化是一种非常强大的工具,但它也是一把双刃剑。正确使用时,它可以大大提高代码的效率和可读性。但如果滥用,可能会导致代码变得复杂和难以维护。
  • 编译时常量(Compile-time Constants)
  • 通过使用constexpr,你可以确保某些计算在编译时完成,从而提高运行时性能。
    1
    2
    3
    4
    template <typename T>
    constexpr T square(T x) {
    return x * x;
    }
  • 在这个例子中,square函数是一个constexpr函数,这意味着当你用编译时常量作为参数调用它时,所有的计算都会在编译时完成。
  • 模板和constexpr的结合使用可以产生高度优化的代码,这在高性能计算和嵌入式系统编程中是非常有价值的。

泛型编程:一份代码,多重任务

  • 泛型编程(Generic Programming)是一种编程范式,旨在通过一份代码来处理多种类型或多种情况。这种做法的目的是为了提高代码的复用性(Reusability)、可维护性(Maintainability)和类型安全性(Type Safety)。
    1
    2
    3
    4
    template <typename T>
    T add(T a, T b) {
    return a + b;
    }
  • 这个简单的 add 函数可以用于整数、浮点数、字符串等,无需为每种类型编写单独的函数。
    1
    2
    3
    4
    5
    6
    7
    8
    template <typename T>
    auto print(T value) {
    if constexpr (std::is_integral_v<T>) {
    std::cout << "Integral: " << value << std::endl;
    } else {
    std::cout << "Non-integral: " << value << std::endl;
    }
    }
  • 这里,if constexpr允许我们在同一个函数中处理不同的类型,而不需要多个函数重载或特化版本。
  • SFINAE:编译时决策
  • SFINAE允许你在编译时根据类型的特性来选择合适的函数重载。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
    void bar(T value) {
    std::cout << "Integral: " << value << std::endl;
    }
    template <typename T, std::enable_if_t<!std::is_integral_v<T>, int> = 0>
    void bar(T
    value) {
    std::cout << "Non-integral: " << value << std::endl;
    }
  • 这里,std::enable_if_t用于在编译时启用或禁用某个函数重载。
  • 类型萃取与SFINAE:完美搭档
  • 类型萃取和SFINAE经常一起使用,以实现更复杂的编译时逻辑。
    1
    2
    3
    4
    5
    6
    7
    8
    template <typename T>
    auto baz(T value) -> std::enable_if_t<std::is_integral_v<T>, T> {
    return value * 2;
    }
    template <typename T>
    auto baz(T value) -> std::enable_if_t<!std::is_integral_v<T>, T> {
    return value;
    }
  • 这里,我们使用了返回类型后置(Trailing Return Type)和std::enable_if_t,以便在编译时选择合适的函数版本。
  • 这样的编程方式就像是你有一个“魔术师”的帽子,你可以从中拉出任何你需要的东西,而不必为每种情况都准备一个不同的帽子。

静态多态:模板与函数重载

  • 静态多态则是在编译时(Compile-time)解析,因此没有运行时的性能开销。这是通过模板和函数重载(Function Overloading)来实现的。
    1
    2
    3
    4
    5
    6
    7
    template <typename T>
    void print(T value) {
    std::cout << value << std::endl;
    }
    void print(int value) {
    std::cout << "Integer: " << value << std::endl;
    }

STL

容器

  • 顺序容器(sequence container):每个元素都有固定的位置,位置取决于插入时间和地点,与元素的值无关
    • vector
    • deque(double end queue)
    • list
    • string
    • forward_list
  • 关联容器(associated container):元素位置取决于元素的值,和插入顺序无关。
    • set/multiset
    • map/multimap
  • 无序式容器(unordered container
    • unordered_map/unordered_multimap
    • unordered_set/unorder_multiset

迭代器

  • iter.begin() / iter.end()
  • ++iter
  • ( * iter)
  • iter != iter.end()
  • new_iter = iter