C++小碗菜

34k 词

一些C++中经常会接触的概念,包括中间件,数据结构封装等模块

了解原理,实现基本功能,实际运用的时候还需要根据具体需求进行扩展和优化

一般肯定直接用STL封装好的

内存池 Memory Pool

内存池是一种预先分配一大块内存,然后按需分配小块内存的技术

内存池可以减少频繁的内存分配和释放操作从而提高性能

用一个栈实现:

classDiagram
    class MemoryPool {
        char* _pool
        size_t _objectsize
        size_t _totalsize
        stack~void*~ _freelist
        void* allocate()
        void deallocate(void* ptr)
    }
    
    MemoryPool --* "1" char : 管理
    MemoryPool --* "1" stack : 包含
    
    class FreeListStack {
        <>
        void*类型
        push(void* ptr)
        pop() void*
        top() void*
        empty() bool
    }
    
    MemoryPool --> FreeListStack : 使用
flowchart TD
    A[开始] --> B{根据类型创建内存池}
    B -->|分配| C[调用 allocate]
    B -->|释放| D[调用 deallocate]
    
    C --> E{_freelist为空?}
    E -->|是| F[抛出 bad_alloc]
    E -->|否| G[从栈顶取指针]
    G --> H[返回指针]
    
    D --> I[指针压入栈]
    I --> J[返回]
    
    F --> K[结束]
    H --> K
    J --> K

如果是多线程的情况,需要在用户应用层进行线程安全的一系列操作


共享指针 Shared Pointer

shared_ptr(共享智能指针)是C++标准库提供的一种智能指针,用于自动管理动态分配的内存资源。它采用引用计数机制,记录有多少个shared_ptr指向同一个对象。当最后一个shared_ptr被销毁或重置时,其所管理的对象会被自动删除,从而有效防止内存泄漏。shared_ptr支持拷贝和赋值操作,允许多个指针共享同一资源。使用shared_ptr能简化资源生命周期管理,是现代C++中避免原始指针风险的重要工具。

这里用一个struct块来模拟引用计数,实际上也可以直接用一个int*来替代。

classDiagram
    class MySharedPtr~T~ {
        T* ptr
        controlblock* ctrl_blk
        void Release()
        int use_count() const
        T* get() const
        void reset(T* p)
    }
    
    class controlblock {
        int count_ref
        controlblock()
    }
    
    MySharedPtr --o controlblock : 拥有
    MySharedPtr --> T : 管理

函数封装 Function Encapsulation

四种函数封装方式

  1. 函数指针
  2. 仿函数(函数对象)
  3. std::function
  4. lambda表达式
  • 性能敏感场景:优先考虑函数指针或模板仿函数
  • 回调系统:使用std::function统一接口
  • 局部算法:lambda表达式最合适
  • 兼容C代码:必须使用函数指针

双向链表容器 List

有节点定义,迭代器类和容器类


双端队列容器 Deque

deque 通常由一系列固定大小的数组块组成,这些块通过一个中央数组进行管理
整个结构使得在两端扩展的时候不需要再重新分配整个容器的内存

简化版:环形缓冲区deque

支持随机访问元素
支持在两端频繁插入和删除元素


BST和AVL实现map

stl中的map底层是用红黑树实现的,红黑树的插入删除操作相对复杂,这里用普通的二叉搜索树和自平衡二叉搜索树实现一个mapmap 是一种键值对数据结构,在许多算法和系统设计中都有广泛的应用。通过封装不同的树结构,map 提供了高效的元素查找、插入和删除操作。

1. 基于二叉搜索树 (BST) 的实现

二叉搜索树(BST)是一种特殊的二叉树,其中每个节点的值都大于左子树的所有节点,并且小于右子树的所有节点。这样的性质使得二叉搜索树能够支持高效的查找、插入和删除操作。在最理想的情况下,BST 能够以对数时间复杂度 O(log n) 进行这些操作。然而,若树不平衡,最坏的情况会退化为链表,导致操作时间复杂度为 O(n)。

2. 基于 AVL 树的实现

为了克服普通二叉搜索树在最坏情况下的退化问题,我们可以使用 AVL 树,它是一种自平衡的二叉搜索树。AVL 树通过保持每个节点的平衡因子(左右子树的高度差不超过 1)来确保树的平衡性。这种平衡策略使得 AVL 树在任何时候都保持较为理想的树形结构,从而保证查找、插入和删除操作都能在 O(log n) 的时间复杂度内完成。


无序映射表 unordered_map

哈希表是现代编程中最高效的数据结构之一,其核心在于通过哈希函数实现键到存储位置的直接映射。这里实现的开链法哈希表采用中心数组加链表的结构:哈希函数将键转换为数组索引,冲突元素通过链表连接,在保证空间效率的同时维持了平均O(1)的查询性能。

负载因子监控与自动扩容机制是保持高性能的关键。当元素密度超过阈值时,系统执行rehash操作——创建更大的桶数组并重新分配所有元素,这一过程虽然代价高昂,但确保了哈希表长期运行的高效性。

迭代器的实现展示了哈希表遍历的复杂性:它不仅要遍历链表,还要在桶间智能跳转。


单例模式 Singleton

单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。该模式在需要控制资源访问(如数据库连接、日志对象、配置管理器等)的场景中尤为常用。在 C++ 中实现线程安全的单例需要考虑构造时机、多线程竞争以及资源释放等问题,不同的实现方式各有特点。

以下代码展示了三种典型的单例模式实现:基于双重检查锁定的懒汉式、饿汉式静态初始化,以及使用 std::call_once 的线程安全懒汉式。代码中包含了详细的注释,可供参考。

这里提供模板单例,可快速复用,提高效率。

使用时,public继承并在子类构造和析构里实现自己的逻辑

注意,关于静态成员变量的定义位置,需要根据类的类型来区分:对于非模板类,静态成员变量必须在一个单独的cpp文件中定义和初始化,这是为了避免多重定义链接错误,遵循单一定义规则;

而对于模板类,静态成员变量则应该在头文件中定义,因为模板需要在使用时实例化,编译器必须看到完整的定义才能为不同的模板参数生成相应的代码,如果放在cpp文件中,其他包含头文件的编译单元将无法找到该静态成员的定义而导致链接错误。这种区别是C++模板编程与非模板编程的重要不同之处。

不过,C++17引入了inline变量,可以让非模板类的静态成员也在头文件中定义