C++ 哈希表超全详解:从底层实现到封装 myunordered_map/myunordered_set


观众老爷们大家好 我是邪修KING 本文属于系列C++ 进阶篇 ,欢迎来到C++进阶篇博客 C++重点语法运用! 本文属于 《C++ 进阶篇系统教程》第 8 篇 ,上一篇我们讲透了红黑树的自平衡机制与 STL 选型逻辑,今天我们进入另一个核心数据结构 ------哈希表 (Hash Table)。它是 C++11 新增的unordered_map/unordered_set的底层实现,平均 O (1) 的插入、查找、删除性能,让它成为处理海量数据的首选!

前言

很多人觉得哈希表简单,不就是数组加链表吗?但真正理解哈希表的设计精髓并不容易:为什么要取质数作为表长?负载因子为什么控制在 1?迭代器如何跨桶遍历?如何用一颗通用哈希表同时实现unordered_map和unordered_set?

本文将从哈希表核心概念出发,详细讲解哈希函数设计、哈希冲突解决、负载因子与扩容机制,然后一步步实现一个工业级的链地址法哈希表,最后复用这个哈希表封装出myunordered_map和myunordered_set,完整复刻 STL 的设计思想!

一、什么是哈希表?

哈希表(Hash Table)又称散列表,是一种通过哈希函数 将关键字(Key)映射到数组下标,从而实现 O (1) 平均时间复杂度访问的数据结构。

它的核心思想是:建立 Key 与存储位置的直接映射关系,不需要像数组那样遍历查找,也不需要像二叉树那样比较大小,直接通过计算就能得到数据的存储位置。

1.1 最基础的哈希:直接定址法

直接定址法是最简单的哈希方法,它直接用关键字本身或关键字的线性函数作为哈希地址:

典型应用:计数排序与字符统计

参考387.字符串中的第一个唯一字符-leetcode

cpp 复制代码
class Solution {
public:
	int firstUniqChar(string s) {
		// 每个字⺟的ascii码-'a'的ascii码作为下标映射到count数组,数组中存储出现的次数
		int count[26] = {0};
		// 统计次数
		for(auto ch : s)
		{
			count[ch-'a']++;
		}
		for(size_t i = 0; i < s.size(); ++i)
		{
			if(count[s[i]-'a'] == 1)
				return i;
		}
		return -1;
	}
};

优点:计算简单,没有哈希冲突,速度极快。

缺点:只适用于关键字范围非常集中的场景,如果关键字范围分散(比如手机号、身份证号),会造成极大的内存浪费。

二、哈希表的核心问题

2.1 哈希冲突(Hash Collision)

当两个不同的关键字通过哈希函数计算出相同的哈希地址时,就发生了哈希冲突。

比如我们用h(key) = key % 11作为哈希函数:

hash(19) = 19 % 11 = 8

hash(30) = 30 % 11 = 8

19 和 30 两个不同的 key 映射到了同一个下标 8,这就是哈希冲突。

注意:哈希冲突是不可避免的,无论多么优秀的哈希函数,都只能减少冲突的概率,不能完全消除冲突。因此,哈希表必须有完善的冲突解决机制。

2.2 负载因子(Load Factor)

负载因子是衡量哈希表拥挤程度的重要指标,定义为:

负载因子 = 哈希表中已存储的元素个数 / 哈希表的大小

负载因子与哈希冲突的关系:

1.负载因子越大 → 哈希表越拥挤 → 冲突概率越高 → 性能越差

2.负载因子越小 → 哈希表越稀疏 → 冲突概率越低 → 空间利用率越低

不同冲突解决方法的负载因子阈值:

1.开放定址法 :负载因子必须小于 1(一般控制在 0.7 左右)

2.链地址法 :负载因子可以大于 1(STL 控制在 1 左右)

扩容情况:当负载因子超过阈值时,哈希表会进行扩容:创建一个更大的新表,将旧表中的所有元素重新哈希到新表中,然后释放旧表。

三、哈希函数设计

一个好的哈希函数应该满足两个条件:

1.均匀性 :将关键字尽可能均匀地分布在哈希表的所有位置上

2.计算效率:计算速度快,尽可能简单

3.1 最常用:除法散列法(除留余数法)

除法散列法是最常用的哈希函数,公式为:

hash(key)=key%M

其中M是哈希表大小

为什么 M 要取质数?

如果 M 是 2 的幂(如 16、32、64),那么key % M等价于取 key 的二进制后 X 位,高位完全不参与计算,会导致大量冲突。比如:

1.M=16(2⁴),63 的二进制是00111111,31 的二进制是00011111

2.63 % 16 = 15,31 % 16 = 15,两个完全无关的数发生了冲突

如果 M 是质数,那么 key 的所有位都会参与取模运算,哈希值会更加均匀。因此,哈希表的大小通常取质数。

Java HashMap 的优化

Java HashMap 为了提高性能,将哈希表大小设为 2 的幂,用位运算key & (M-1)代替取模运算(位运算比取模快得多)。为了解决高位不参与计算的问题,它会将 key 的高 16 位和低 16 位进行异或(这里不做过多解释,C++内容)

3.2 其他哈希函数(了解即可)

1.乘法散列法 :h(key) = floor(M * ((A * key) % 1.0)),其中 A 是黄金分割点 0.6180339887

2.全域散列法 :随机选择哈希函数,防止恶意构造冲突数据

3.BKDR 哈希:专门用于字符串的哈希函数,hash = hash * 131 + ch,效果非常好

四、哈希冲突的解决方法

解决哈希冲突主要有两种方法:开放定址法和链地址法。

4.1 开放定址法

开放定址法的核心思想是:所有元素都存储在哈希表数组中 ,当发生冲突时,按照某种规则找到下一个空位置存储。

线性探测

优点:实现简单,不需要额外的指针

缺点:存在堆积问题 ------ 连续冲突的位置会形成一个 "堆积区",后续插入的元素会继续加剧堆积,导致性能急剧下降。

二次探测

开放定址法的删除问题

开放定址法不能直接删除元素,因为删除后会导致后续冲突元素的查找链断裂。解决方法是给每个位置增加一个状态标识:

EMPTY:空位置

EXIST:有元素

DELETE:已删除

删除元素时,只需要将状态改为DELETE,查找时遇到DELETE继续向后探测,遇到EMPTY才停止。

4.1.1开放定址法代码实现

开放定址法在实践中,不如下⾯讲的链地址法,因为开放定址法解决冲突不管使⽤哪种⽅法,占⽤的

都是哈希表中的空间,始终存在互相影响的问题。所以开放定址法,我们简单选择线性探测实现即

可。

开放定址法的哈希表结构

cpp 复制代码
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};
template<class K, class V>
class HashTable
{
	private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0; // 表中存储数据个数
};

4.1.2扩容

4.2 链地址法(拉链法)

链地址法是 STL unordered_map/unordered_set采用的冲突解决方法,核心思想是:哈希表数组中存储链表的头指针 ,所有哈希地址相同的元素都链接到同一个链表中。

插入示例如图

优点

没有堆积问题,冲突处理简单

负载因子可以大于 1,空间利用率高

删除元素方便,直接删除链表节点即可

缺点 :需要额外的指针空间,缓存局部性较差(链表节点在内存中不连续)

极端情况优化:链表转红黑树

如果某个桶的链表过长(Java HashMap 中阈值为 8),查找效率会下降到 O (n)。为了解决这个问题,Java 8 及以上版本会将长度超过 8 的链表转换为红黑树,将查找效率提升到 O (logn)。

五、链地址法哈希表的完整实现

下面我们实现一个工业级的链地址法哈希表,包含插入、查找、删除、扩容、迭代器等核心功能。

5.1 哈希函数仿函数-key不能取模问题

当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加⼀个仿函数,这个仿函

数⽀持把key转换成⼀个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数

就⽤默认参数即可,如果这个Key不能转换为整形,我们就需要⾃⼰实现⼀个仿函数传给这个参数,实

现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整形值不同。string

做哈希表的key⾮常常⻅,所以我们可以考虑把string特化⼀下

我们用仿函数实现哈希函数,支持任意类型的 key,并对 string 类型进行特化,使用 BKDR 哈希算法:

cpp 复制代码
// 通用哈希函数仿函数
template <class K>
struct HashFunc {
    size_t operator()(const K& key) const {
        return (size_t)key;
    }
};

// string类型特化:BKDR哈希算法
template <>
struct HashFunc<string> {
    size_t operator()(const string& key) const {
        size_t hash = 0;
        for (char ch : key) {
            hash = hash * 131 + ch; // 131是经验质数,冲突率低
        }
        return hash;
    }
};

5.2节点结构与哈希表类框架

cpp 复制代码
namespace hash_bucket {

// 哈希表节点结构
template <class T>
struct HashNode {
    T _data;          // 存储的数据
    HashNode* _next;  // 下一个节点指针

    HashNode(const T& data)
        : _data(data)
        , _next(nullptr)
    {}
};

// 前置声明:哈希表类,用于迭代器的友元声明
template <class K, class T, class KeyOfT, class Hash>
class HashTable;

} // namespace hash_bucket

5.3 迭代器实现(核心难点)

哈希表的迭代器是单向迭代器,只支持++操作,不支持--。迭代器需要封装两个指针:

_node:指向当前节点的指针

_pht:指向哈希表对象的指针(用于跨桶遍历)

迭代器类实现

cpp 复制代码
template <class K, class T, class Ptr, class Ref, class KeyOfT, class Hash>
struct HTIterator {
    typedef HashNode<T> Node;
    typedef HTIterator<K, T, Ptr, Ref, KeyOfT, Hash> Self;
    typedef HashTable<K, T, KeyOfT, Hash> HashTableType;

    Node* _node;                // 当前节点指针
    const HashTableType* _pht;  // 哈希表对象指针

    // 构造函数
    HTIterator(Node* node, const HashTableType* pht)
        : _node(node)
        , _pht(pht)
    {}

    // 解引用
    Ref operator*() const {
        return _node->_data;
    }

    // 箭头运算符
    Ptr operator->() const {
        return &_node->_data;
    }

    // 前置++:核心逻辑
    Self& operator++() {
        if (_node->_next != nullptr) {
            // 情况1:当前桶还有下一个节点,直接移动
            _node = _node->_next;
        } else {
            // 情况2:当前桶走完了,找下一个非空桶
            KeyOfT kot;
            Hash hs;
            // 计算当前节点所在的桶下标
            size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();
            hashi++; // 从下一个桶开始找

            // 遍历找到第一个非空桶
            while (hashi < _pht->_tables.size()) {
                if (_pht->_tables[hashi] != nullptr) {
                    _node = _pht->_tables[hashi];
                    return *this;
                }
                hashi++;
            }

            // 所有桶都走完了,指向end()
            _node = nullptr;
        }
        return *this;
    }

    // 相等判断
    bool operator==(const Self& s) const {
        return _node == s._node;
    }

    // 不等判断
    bool operator!=(const Self& s) const {
        return _node != s._node;
    }
};

迭代器核心逻辑详解

1.当前桶有下一个节点 :直接将_node指向_node->_next即可

2.当前桶走完了

计算当前节点所在的桶下标

从下一个桶开始遍历,找到第一个非空桶

将_node指向该桶的第一个节点

如果所有桶都走完了,将_node置为nullptr(即 end ())

5.4 哈希表类完整实现

cpp 复制代码
template <class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable {
    // 迭代器类作为友元,可以访问私有成员
    template <class K, class T, class Ptr, class Ref, class KeyOfT, class Hash>
    friend struct HTIterator;

    typedef HashNode<T> Node;

public:
    // 迭代器类型定义
    typedef HTIterator<K, T, T*, T&, KeyOfT, Hash> Iterator;
    typedef HTIterator<K, T, const T*, const T&, KeyOfT, Hash> ConstIterator;

    // 构造函数
    HashTable() {
        // 初始大小为第一个质数53
        _tables.resize(__stl_next_prime(0), nullptr);
        _n = 0;
    }

    // 析构函数
    ~HashTable() {
        // 释放所有桶的链表
        for (size_t i = 0; i < _tables.size(); i++) {
            Node* cur = _tables[i];
            while (cur != nullptr) {
                Node* next = cur->_next;
                delete cur;
                cur = next;
            }
            _tables[i] = nullptr;
        }
    }

    // 插入元素
    pair<Iterator, bool> Insert(const T& data) {
        KeyOfT kot;
        // 先查找是否已存在
        Iterator it = Find(kot(data));
        if (it != End()) {
            return make_pair(it, false); // 已存在,插入失败
        }

        Hash hs;
        // 负载因子==1时扩容
        if (_n == _tables.size()) {
            // 扩容:创建新表,大小为下一个质数
            vector<Node*> new_tables(__stl_next_prime(_tables.size() + 1), nullptr);

            // 将旧表的节点移动到新表(不需要重新创建节点,效率更高)
            for (size_t i = 0; i < _tables.size(); i++) {
                Node* cur = _tables[i];
                while (cur != nullptr) {
                    Node* next = cur->_next;
                    // 重新计算哈希值
                    size_t hashi = hs(kot(cur->_data)) % new_tables.size();
                    // 头插到新表
                    cur->_next = new_tables[hashi];
                    new_tables[hashi] = cur;

                    cur = next;
                }
                _tables[i] = nullptr; // 旧表指针置空
            }

            // 交换新旧表
            _tables.swap(new_tables);
        }

        // 计算哈希值
        size_t hashi = hs(kot(data)) % _tables.size();
        // 头插新节点
        Node* new_node = new Node(data);
        new_node->_next = _tables[hashi];
        _tables[hashi] = new_node;
        _n++;

        return make_pair(Iterator(new_node, this), true);
    }

    // 查找元素
    Iterator Find(const K& key) {
        KeyOfT kot;
        Hash hs;
        size_t hashi = hs(key) % _tables.size();

        // 遍历对应桶的链表
        Node* cur = _tables[hashi];
        while (cur != nullptr) {
            if (kot(cur->_data) == key) {
                return Iterator(cur, this);
            }
            cur = cur->_next;
        }

        // 没找到,返回end()
        return End();
    }

    // 删除元素
    bool Erase(const K& key) {
        KeyOfT kot;
        Hash hs;
        size_t hashi = hs(key) % _tables.size();

        Node* prev = nullptr;
        Node* cur = _tables[hashi];
        while (cur != nullptr) {
            if (kot(cur->_data) == key) {
                // 找到要删除的节点
                if (prev == nullptr) {
                    // 删除的是头节点
                    _tables[hashi] = cur->_next;
                } else {
                    // 删除的是中间节点
                    prev->_next = cur->_next;
                }

                delete cur;
                _n--;
                return true;
            }

            prev = cur;
            cur = cur->_next;
        }

        // 没找到,删除失败
        return false;
    }

    // 迭代器接口
    Iterator Begin() {
        if (_n == 0) {
            return End();
        }

        // 找到第一个非空桶的第一个节点
        for (size_t i = 0; i < _tables.size(); i++) {
            if (_tables[i] != nullptr) {
                return Iterator(_tables[i], this);
            }
        }

        return End();
    }

    Iterator End() {
        return Iterator(nullptr, this);
    }

    ConstIterator Begin() const {
        if (_n == 0) {
            return End();
        }

        for (size_t i = 0; i < _tables.size(); i++) {
            if (_tables[i] != nullptr) {
                return ConstIterator(_tables[i], this);
            }
        }

        return End();
    }

    ConstIterator End() const {
        return ConstIterator(nullptr, this);
    }

    // 获取元素个数
    size_t Size() const {
        return _n;
    }

    // 判断是否为空
    bool Empty() const {
        return _n == 0;
    }

private:
    // SGI-STL质数表:用于哈希表扩容
    inline unsigned long __stl_next_prime(unsigned long n) const {
        static const int __stl_num_primes = 28;
        static const unsigned long __stl_prime_list[__stl_num_primes] = {
            53, 97, 193, 389, 769,
            1543, 3079, 6151, 12289, 24593,
            49157, 98317, 196613, 393241, 786433,
            1572869, 3145739, 6291469, 12582917, 25165843,
            50331653, 100663319, 201326611, 402653189, 805306457,
            1610612741, 3221225473, 4294967291
        };

        // 找到第一个大于等于n的质数
        const unsigned long* first = __stl_prime_list;
        const unsigned long* last = __stl_prime_list + __stl_num_primes;
        const unsigned long* pos = lower_bound(first, last, n);
        return pos == last ? *(last - 1) : *pos;
    }

    vector<Node*> _tables; // 哈希表数组(存储链表头指针)
    size_t _n = 0;         // 已存储的元素个数
};

扩容优化说明

我们在扩容时,直接移动旧表的节点到新表,而不是重新创建节点,这样可以避免大量的内存分配和释放,大大提高扩容效率。这是工业级哈希表的标准做法。

六、封装 myunordered_map 和 myunordered_set

和红黑树封装 map/set 一样,我们通过KeyOfT 仿函数解耦哈希表和存储的数据类型,用同一个哈希表类同时实现myunordered_map和myunordered_set。

6.1 封装 myunordered_set

myunordered_set是 key 模型,只存储 key,且 key 不能修改:

cpp 复制代码
#include "HashTable.h"

namespace bit {

template <class K, class Hash = HashFunc<K>>
class unordered_set {
private:
    // 仿函数:从数据中提取key(set直接返回自身)
    struct SetKeyOfT {
        const K& operator()(const K& key) const {
            return key;
        }
    };

    // 哈希表实例化:第二个模板参数是const K,保证key不能修改
    typedef hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> HT;
    HT _ht;

public:
    // 迭代器类型定义
    typedef typename HT::Iterator iterator;
    typedef typename HT::ConstIterator const_iterator;

    // 迭代器接口
    iterator begin() {
        return _ht.Begin();
    }

    iterator end() {
        return _ht.End();
    }

    const_iterator begin() const {
        return _ht.Begin();
    }

    const_iterator end() const {
        return _ht.End();
    }

    // 插入元素
    pair<iterator, bool> insert(const K& key) {
        return _ht.Insert(key);
    }

    // 查找元素
    iterator find(const K& key) {
        return _ht.Find(key);
    }

    // 删除元素
    bool erase(const K& key) {
        return _ht.Erase(key);
    }

    // 获取元素个数
    size_t size() const {
        return _ht.Size();
    }

    // 判断是否为空
    bool empty() const {
        return _ht.Empty();
    }
};

} // namespace bit

6.2 封装 myunordered_map

myunordered_map是 key-value 模型,存储pair<const K, V>,key 不能修改,value 可以修改,并且支持\[\]运算符:

cpp 复制代码
#include "HashTable.h"

namespace bit {

template <class K, class V, class Hash = HashFunc<K>>
class unordered_map {
private:
    // 仿函数:从pair中提取key(返回pair的first)
    struct MapKeyOfT {
        const K& operator()(const pair<const K, V>& kv) const {
            return kv.first;
        }
    };

    // 哈希表实例化:第二个模板参数是pair<const K, V>,保证key不能修改
    typedef hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> HT;
    HT _ht;

public:
    // 迭代器类型定义
    typedef typename HT::Iterator iterator;
    typedef typename HT::ConstIterator const_iterator;

    // 迭代器接口
    iterator begin() {
        return _ht.Begin();
    }

    iterator end() {
        return _ht.End();
    }

    const_iterator begin() const {
        return _ht.Begin();
    }

    const_iterator end() const {
        return _ht.End();
    }

    // 插入元素
    pair<iterator, bool> insert(const pair<K, V>& kv) {
        return _ht.Insert(kv);
    }

    // 查找元素
    iterator find(const K& key) {
        return _ht.Find(key);
    }

    // 删除元素
    bool erase(const K& key) {
        return _ht.Erase(key);
    }

    // []运算符:最常用接口
    V& operator[](const K& key) {
        // 插入(key, 默认值),如果已存在则插入失败
        pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
        // 返回value的引用
        return ret.first->second;
    }

    // 获取元素个数
    size_t size() const {
        return _ht.Size();
    }

    // 判断是否为空
    bool empty() const {
        return _ht.Empty();
    }
};

} // namespace bit

注意:key 的不可修改性

1.unordered_set的哈希表第二个模板参数是const K,所以迭代器返回的是const K&,不能修改

2.unordered_map的哈希表第二个模板参数是pair<const K, V>,所以pair的first是const K不能修改,second是V可以修改

这和 STL 的行为完全一致,保证了哈希表的结构不会被破坏。

七、测试代码

cpp 复制代码
#include <iostream>
#include <string>
#include "MyUnorderedSet.h"
#include "MyUnorderedMap.h"
using namespace std;
using namespace bit;

// 测试unordered_set
void TestUnorderedSet() {
    unordered_set<int> s;
    int arr[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14, 3, 3, 15};
    for (int e : arr) {
        s.insert(e);
    }

    cout << "unordered_set大小:" << s.size() << endl;
    cout << "unordered_set遍历:";
    for (int x : s) {
        cout << x << " ";
    }
    cout << endl;

    // 查找测试
    auto it = s.find(5);
    if (it != s.end()) {
        cout << "找到5" << endl;
    }

    // 删除测试
    s.erase(5);
    cout << "删除5后遍历:";
    for (int x : s) {
        cout << x << " ";
    }
    cout << endl;
}

// 测试unordered_map
void TestUnorderedMap() {
    unordered_map<string, string> dict;
    dict.insert({"sort", "排序"});
    dict.insert({"left", "左边"});
    dict.insert({"right", "右边"});

    // []运算符测试
    dict["left"] = "左侧"; // 修改已有元素
    dict["insert"] = "插入"; // 插入新元素
    dict["string"]; // 插入默认值

    cout << endl << "unordered_map遍历:" << endl;
    for (auto& p : dict) {
        cout << p.first << ": " << p.second << endl;
    }

    // 词频统计测试
    unordered_map<string, int> freq;
    string words[] = {"hello", "world", "hello", "c++", "hello", "world"};
    for (auto& word : words) {
        freq[word]++;
    }

    cout << endl << "词频统计:" << endl;
    for (auto& p : freq) {
        cout << p.first << ": " << p.second << endl;
    }
}

int main() {
    TestUnorderedSet();
    TestUnorderedMap();
    return 0;
}

八、哈希表和红黑树怎么选择

九、面试高频考点总结

哈希冲突的解决方法:开放定址法(线性探测、二次探测)和链地址法,重点讲链地址法的原理和优缺点

负载因子的作用:衡量哈希表拥挤程度,超过阈值会触发扩容

为什么哈希表大小取质数:让 key 的所有位都参与取模运算,减少冲突

哈希表迭代器的实现:单向迭代器,跨桶遍历的逻辑

unordered_map 和 map 的区别:底层实现、时间复杂度、有序性、适用场景

链地址法的扩容过程:创建新表,重新哈希所有元素,交换新旧表

哈希表是处理海量数据的首选数据结构,也是unordered_map/unordered_set的底层实现,更是校招面试的必考题。理解了哈希表的设计思想,你就掌握了 STL 关联容器的半壁江山!

本文属于 《C++ 进阶篇系统教程》第 8 篇,下一篇我们会讲解 C++11 的核心新特性 ------ 智能指针,搞懂如何解决内存泄漏问题。

关注我,第一时间收到更新,不用自己零散找资料,跟着系列系统学,少走 90% 的弯路!

相关推荐
secret_to_me1 小时前
buildRoot编译rootfs实战
linux·c语言·c++·ubuntu·电脑·buildroot
凡人叶枫1 小时前
Effective C++ 条款01:视 C++ 为一个语言联邦
linux·开发语言·c++·effective c++·编程范式·语言联邦
QiLinkOS1 小时前
合肥气链科技有限公司本质总结
c++·科技·算法·gitee·开源
Yuk丶1 小时前
厌倦了假AI对话?本地 LLM 语音对话 + 口型同步系统 2.0(已开源!)
c++·人工智能·语言模型·开源·ue4·语音识别·游戏开发
kyle~1 小时前
ROS2---零拷贝
linux·c++·机器人·ros2
Ricky_Theseus1 小时前
栈 & 队列 应用场景
数据结构·c++
薇茗1 小时前
【C++】类与对象 核心篇
开发语言·c++
ouliten1 小时前
C++笔记:偏现代C++日志系统
c++·笔记
猪脚饭还是好吃的1 小时前
【分享】C4droid 安卓C++编译器 手机编程超便捷
android·c++·智能手机