C++ set 交, 并, 差集用法

有时做数据过滤时, 需要使用到集合的交,并,差集. 下面用一个例子记录一下用法:

#include <algorithm>
#include <iostream>
#include <set>
#include <vector>

using namespace std;

int main(void) {
    set<int> a{1, 2, 3};
    set<int> b{2, 3, 4};
    set<int> c;

    // 交集: 2, 3
    set_intersection(a.begin(), a.end(), b.begin(), b.end(),
                     inserter(c, c.begin()));
    c.clear();

    // 并集: 1, 2, 3, 4
    set_union(a.begin(), a.end(), b.begin(), b.end(), inserter(c, c.begin()));
    c.clear();

    // 差集: 1
    set_difference(a.begin(), a.end(), b.begin(), b.end(),
                   inserter(c, c.begin()));
    c.clear();

    // 差集: 4
    set_difference(b.begin(), b.end(), a.begin(), a.end(),
                   inserter(c, c.begin()));

    auto it = c.find(5);
    if (it == c.end()) {
        cout << "c don't have element 5 " << endl;
    }

    int n = c.erase(4);
    cout << n << endl;
}

如果觉得有帮助, 可以扫描右边的微信打赏码支持一下.

C++ exception

C++ 通过异常来传播错误. 通过异常的方式, 错误会一层一层的向上抛出. 带来的问题是: 如果捕获不及时, 函数调用栈会不断弹出, 直到捕获或程序退出为止. 这种让错误不断蔓延的方式, 很容易造成资源泄漏的问题. 如: 在代码中检查返回值进行错误处理的方法, 将面临抛异常带来的风险. 但通过RAII处理资源释放, 能解决部分问题. 通过返回值检查错误抛异常两种方式混合进行错误处理将增加不必要的复杂度. 既然C++选择了异常错误处理的方式, 写代码时也遵循C++的逻辑为好.

下面是实现异常的实例:

#include <iostream>

class an_execption: public std::exception {
    public:
        const char* what() const noexcept override {
            return "I an exception";
        }
};

int main(void) {
    try {
        throw an_execption();
    } catch(std::exception &e) {
        std::cout << e.what() << std::endl;
    }

    return 0;
}

如果觉得有帮助, 可以扫描右边的微信打赏码支持一下.

C++ new错误处理

在C++中处理内存分配错误大概有两种方法:

  • 使用try...catch
#include <iostream>

int main(void) {
    try {
        long long int n = 1024 * 1024 * 1024, m = 1024 * 1024;
        auto a = new int[n * m];
    } catch(std::bad_alloc &e) {
        std::cout << "catch bad_alloc exception" << std::endl;
    } catch(std::exception &e) {
        std::cout << e.what() << std::endl;
    } catch(...) {
        std::cout << "unknow error" << std::endl;
    }

    return 0;
}
  • 使用nothrow, 错误处理流程与C语言一样
#include <iostream>

int main(void) {
    long long int n = 1024 * 1024 * 1024, m = 1024 * 1024;
    auto a = new(std::nothrow) int[n * m];
    if (a == nullptr) {
        std::cout << "nullptr" << std::endl;
    }

    return 0;
}

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ 同步阻塞日志库

日志用于记录程序的运行情况. 当程序出现问题时, 第一时间会查看日志进行排查, 通过异常信息快速定位问题. 今天就写一个同步阻塞日志库, 下面是我认为一个日志库应该要有的最基本的功能:

  • 日志输出到文件或标准输出. 日志输出到文件是为了在非调试环境中出问题时能够获取到日志信息.
  • 输出到文件达到设置大小重写. 避免日志文件过大占用空间.
  • 支持日志级别. 因为打印日志会占用很多cpu资源, 在非调试环境中运行, 需要设置较高的日志级别.
  • 线程安全. 避免日志错乱.
  • 同步阻塞. 异步写日志可能造成日志丢失, 不利于排查问题.
  • 完善的日志排查信息: 时间, 文件名, 函数名, 行号等. 有利于快速定位打印日志的地方.

实现代码比较简单就不贴了, 完整代码. 下面是实例代码:

#include "logger.hpp"

int main(void) {
    /* const char *filepath = "./logger.log"; */
    /* logger::set_filepath(filepath); */
    /* logger::set_size(1024); */
    logger::set_level(LOGGER_LV_ALL);

    _TRACE_LOG("hello ", "world");
    _DEBUG_LOG("hello ", "world");
    _INFO_LOG("hello ", "world");
    _WARN_LOG("hello ",  "world");
    _ERROR_LOG("hello ", "world");
    _FATAL_LOG("hello ", "world");
}

输出:

[T] [Apr 16 16:59:34] [m.c] [main:12]: hello world
[D] [Apr 16 16:59:34] [m.c] [main:13]: hello world
[I] [Apr 16 16:59:34] [m.c] [main:14]: hello world
[W] [Apr 16 16:59:34] [m.c] [main:15]: hello world
[E] [Apr 16 16:59:34] [m.c] [main:16]: hello world
[F] [Apr 16 16:59:34] [m.c] [main:17]: hello world

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ SFINAE

SFINAE的全称是: Substitution Failure Is Not An Error. 在进行模板匹配时, 可能会出现错误. 而当出现错误时, 不抛出错误, 而是继续去匹配别的模板. 这种情况就是SFINAE. 下面看一个例子:

#include <iostream>

// 仅仅是为了获取T的类型
template <bool B, typename T = void> struct enable_if {};
template <typename T> struct enable_if<true, T> { using type = T; };

// 判断是否有函数A()
template <typename T> struct has_func_A {
    // decltype返回最后一个表达式的结果, 即: bool()对象的类型
    // test的int参数是为了使用可变参数函数,因为模板函数的优先级大于可变参数函数
    template <typename C>
    static constexpr decltype(std::declval<C>().A(), bool()) test(int) {
        return true;
    }

    // black hole
    template <typename C> static constexpr bool test(...) { return false; }

    static constexpr bool value = test<T>(int());
};

// black hole
void O(...) { std::cout << "O" << std::endl; }

// 根据是否存在函数A, 来生成不同的重载函数
template <class T>
typename enable_if<has_func_A<T>::value, int>::type A(const T &obj) {
    return obj.A();
}

template <class T>
typename enable_if<!has_func_A<T>::value, void>::type A(const T &obj) {
    return O(obj);
}

class B {};

class C {
    public:
    int A() const {
        std::cout << "C" << std::endl;
        return 0;
    }
};

int main(void) {
    B b;
    C c;
    A<B>(b); // 输出: O
    A<C>(c); // 输出: C
}

上面的例子, 通过编译器类型推倒机制, 根据不同的条件生成不同的模板函数, 实现灵活的函数重载. 如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ std::forward

std::forward的作用是实现参数转发功能. 我们知道参数可能是引用或右值引用, 在函数内部再次传递该参数数时, 我们希望参数还是保持原来的状态. 那就需要用使用std::forward进行参数转发. 下面看一个例子更加直观:

#include <iostream>

struct A {
    A(int &&n) { std::cout << "rvalue = " << n << std::endl; }
    A(int &n) { std::cout << "lvalue = " << n << std::endl; }
};

template <typename T, typename U> void forward(U &&u) {
    T(std::forward<U>(u));
}

template <typename T, typename P, typename... U> void forward(P && p, U &&... u) {
    T(std::forward<P>(p));
    forward<T>(std::forward<U>(u)...);
}

int main() {
    int i = 1;
    forward<A>(i, 2, 3);
}

输出:

lvalue = 1
rvalue = 2
rvalue = 3

从上面的输出可以看出, 变量 i 传入 forward 函数前是左值, 再次传入 A 的构造函数时是左值引用. 而右值 2和3 直到传入 A 的构造函数时都是右值引用. 如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

参考:

C++ std::weak_ptr

在C++中std::shared_ptr可以实现多个对象共享同一块内存, 但存在循环引用的问题. 即两个shared_ptr互相指向对方, 导致引用计数无法被递减到0, 造成内存泄露. 下面看一个有问题的例子:

#include <iostream>
#include <memory>

class Any {
  public:
    friend bool set_relationship(std::shared_ptr<Any> & p1,
                                 std::shared_ptr<Any> & p2) {
        if (!p1 || !p2) {
            return false;
        }

        p1->m_another = p2;
        p2->m_another = p1;
        return true;
    }

  private:
    std::shared_ptr<Any> m_another;
};

int main(void) {
    auto p1 = std::make_shared<Any>();
    auto p2 = std::make_shared<Any>();
    set_relationship(p1, p2);
    return 0;
}

通过valgrind 工具进行内存检查:

==1996== LEAK SUMMARY:
==1996==    definitely lost: 32 bytes in 1 blocks
==1996==    indirectly lost: 32 bytes in 1 blocks
==1996==      possibly lost: 0 bytes in 0 blocks
==1996==    still reachable: 0 bytes in 0 blocks
==1996==         suppressed: 0 bytes in 0 blocks

使用shared_ptr导致循环引用, 的确造成了内存泄露.

下面看一个使用weak_ptr的例子:

#include <iostream>
#include <memory>

class Any {
  public:
    Any(int a) : m_a(a) {}

    friend bool set_relationship(std::shared_ptr<Any> &p1,
                                 std::shared_ptr<Any> &p2) {
        if (!p1 || !p2) {
            return false;
        }

        // weak_ptr重载的赋值运算符中可以接收shared_ptr对象
        p1->m_another = p2;
        p2->m_another = p1;
        return true;
    }

    void show(void) { std::cout << m_a << std::endl; }

    std::weak_ptr<Any> &get_ptr(void) { return m_another; }

  private:
    int m_a;
    std::weak_ptr<Any> m_another;
};

int main(void) {
    auto p1 = std::make_shared<Any>(1);
    auto p2 = std::make_shared<Any>(2);
    set_relationship(p1, p2);

    assert(p1.use_count() == 1);
    assert(p2.use_count() == 1);

    p1->get_ptr().lock()->show(); // 输出: 2
    return 0;
}

通过valgrind 工具进行内存检查:

==4775== HEAP SUMMARY:
==4775==     in use at exit: 0 bytes in 0 blocks
==4775==   total heap usage: 3 allocs, 3 frees, 72,768 bytes allocated
==4775==
==4775== All heap blocks were freed -- no leaks are possible

通过 weak_ptr 能够解决 shared_ptr 循环引用带来的内存露问题. 如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ std::shared_ptr

在C++中shared_ptrstd::unique_ptr类似. std::shared_ptrstd::unique_ptr的主要区别在于前者是使用了引用计数, 可以跟踪引用数目. 这意味着, 可以有多个std::shared_ptr实例指向同一块动态分配的内存, 当最后一个引用对象离开其作用域时, 才会释放这块内存. 值得注意的是, std::shared_ptr不能用于管理C语言风格的动态数组. 下面看一使用的例子:

#include <cassert>
#include <iostream>
#include <memory>

class Any {};

int main(void) {
    auto p1 = std::make_shared<Any>();
    assert(p1.use_count() == 1);
    {
        auto p2 = p1;
        std::shared_ptr<Any> p3;
        p3 = p1;
        assert(p1.use_count() == 3);
        assert(p2.use_count() == 3);
        assert(p3.use_count() == 3);
    }
    assert(p1.use_count() == 1);
    return 0;
}
  • shared_ptr 智能指针只能通过赋值构造函数才能增加引用计数. 如果同一指针分别赋值给不同的 shared_ptr 会造成double free的错误.
  • 对同一个对象共享所有权的 shared_ptr 在多个线程上的析构不需要外部加锁保护, 因为引用计数本身修改是原子操作.

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ std::unique_ptr

在C++中使用智能指针能有效避免空指针和悬垂指针出现, 使用智能指针也能减少心智负担. 下面将讲解一下std::unique_ptr智能指针. 该智能指针用于处理只有一个实例拥有所有权的情况, 可以通过所有权转移的方式移交资源. 下面看一下它的使用方法:

#include <iostream>
#include <memory>
#include <cassert>

class Any {
  public:
    friend std::ostream &operator<<(std::ostream &out, const Any &a);
};

std::ostream &operator<<(std::ostream &out, const Any &a) {
    out << "I am Any";
    return out;
}

int main(void) {
    std::unique_ptr<Any> a{new Any};
    std::cout << *a << std::endl;

    std::unique_ptr<Any> a2; // 初始化为nullptr

    // a2 = a; // 非法, 不允许左值赋值
    a2 = std::move(a);
    assert(a == nullptr);
    assert(a2 != nullptr);

    // C++14 可以使用 make_unique函数
    auto a3 = std::make_unique<Any>();
    std::cout << *a3 << std::endl;

    // 创建一个包含实例的数组
    auto a4 = std::make_unique<Any[]>(2);
    std::cout << a4[0] << std::endl;
    std::cout << a4[1] << std::endl;
    return 0;
}

默认使用newdelete来分配和释放内存, 也可以修改该行为. 下面看一个例子:

template <typename T> auto dealloc = [](T *p) { free(p); };

int main(void) {
    int *p = (int *)malloc(sizeof(int));
    *p = 1;
    std::unique_ptr<int, decltype(dealloc<int>)> ptr{p, dealloc<int>};
    std::cout << *ptr << std::endl; // 输出: 1
}

常用方法:

  • release(): 返回该对象的管理的指针, 同时释放所有权.
  • reset(): 释放管理的内存, 同时也可以传递一个新对象的指针.
  • swap(): 交换所管理的对象.
  • get(): 返回对象管理的指针.
  • get_deleter(): 返回析构其管理对象调用的指针.

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ RAII

RAII(Resource Acquisition Is Initialization)即资源获取即初始化, 这里的资源是: 内存, 网络, 文件等对象. 一个资源的生命周期一般包括: 获取资源, 使用资源, 释放资源三个步骤. 不同的编程语言对资源的管理也离不开这三个方面, 其中释放资源应该是最头疼的. C语言采用纯手动回收; Go语言采用的是垃圾回收器; Rust语言采用所有权机制回收(没有垃圾回收器的性能损耗, 却能达到其效果); 而C++即可以手动回收, 也可以通过RAII的方式进行回收. 其中RAII的回收机制和Rust的回收机制有很多相似的地方. 下面看一个例子:

#include <iostream>

class resource {
    int *m_array;

    public:
        resource(const resource&) = delete;
        void operator= (const resource&) = delete;

        resource(int size) {
            m_array = new int[size];
        }

        ~resource() {
            delete []m_array;
        }
};

int main() {
    resource r(100);
}

上面是通过RAII方式构造一个类的典型写法. 整个过程包括以下步骤:

  • 设计一个类对资源进行封装
  • 在构造函数中初始化资源
  • 在析构函数中释放资源

这个过程很简单却无比强大, 我们在使用该类时不需要关心内部资源释放问题, 编译器和作用域规则将接管对象的资源管理细节.

那使用RAII带来什么好处呢?

  • 从繁琐的资源管理中脱身
  • 即使发生异常, 资源也能正确释放
  • 有效的避免空指针引发的bug

RAII典型应用:

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.