从零开始 C++----- 十三【C++ 数据结构】哈希表从原理到手撕实现(开放定址 + 链地址全覆盖)

系列文章目录

提示:这里是系列文章的专栏

并不喜欢吃鱼的C++专栏


提示:以下是文章目录哦!

文章目录

目录

系列文章目录

前言

[一. 哈希表基础核心概念](#一. 哈希表基础核心概念)

[1.1 哈希表定义与核心思想](#1.1 哈希表定义与核心思想)

[1.2 直接定址法](#1.2 直接定址法)

原理

典型场景

[力扣 -字符串中的第一个唯一字符](#力扣 -字符串中的第一个唯一字符)

优缺点总结

[1.3 哈希冲突(哈希碰撞)](#1.3 哈希冲突(哈希碰撞))

[1.4 负载因子](#1.4 负载因子)

[1.5 关键字转整数规则](#1.5 关键字转整数规则)

[二. 常见哈希函数详解](#二. 常见哈希函数详解)

[2.1 除留余数法(除法散列法)](#2.1 除留余数法(除法散列法))

公式

原理本质

避坑要点

[2.2 乘法散列法(了解)](#2.2 乘法散列法(了解))

[2.3 全域散列法(了解)](#2.3 全域散列法(了解))

[2.4 其他哈希方法](#2.4 其他哈希方法)

[三. 哈希冲突解决方案一:开放定址法](#三. 哈希冲突解决方案一:开放定址法)

[3.1 核心思想](#3.1 核心思想)

[3.2 三种探测方式](#3.2 三种探测方式)

[3.2.1 线性探测](#3.2.1 线性探测)

[3.2.2 二次探测](#3.2.2 二次探测)

[3.2.3 双重散列](#3.2.3 双重散列)

[3.3 开放定址法特殊状态设计](#3.3 开放定址法特殊状态设计)

[3.4 哈希仿函数与 string 哈希特化](#3.4 哈希仿函数与 string 哈希特化)

[三、重点:string 特化版本 HashFunc](#三、重点:string 特化版本 HashFunc)

[3.5 质数扩容表](#3.5 质数扩容表)

[3.6 开放定址法完整实现(带详细注释)](#3.6 开放定址法完整实现(带详细注释))

[3.7 开放定址法难点总结](#3.7 开放定址法难点总结)

[四. 哈希冲突解决方案二:链地址法(拉链法 / 哈希桶)](#四. 哈希冲突解决方案二:链地址法(拉链法 / 哈希桶))

[4.1 核心思想](#4.1 核心思想)

[4.2 特性对比](#4.2 特性对比)

[4.3 极端场景优化](#4.3 极端场景优化)

[4.4 链地址法完整实现(带详细注释)](#4.4 链地址法完整实现(带详细注释))

[4.5 链地址法扩容优势](#4.5 链地址法扩容优势)

[五. 哈希表核心难点深度拆解](#五. 哈希表核心难点深度拆解)

[六. 知识点总结与面试高频考点](#六. 知识点总结与面试高频考点)

[6.1 两种冲突解决方式对比](#6.1 两种冲突解决方式对比)

[6.2 哈希函数设计原则](#6.2 哈希函数设计原则)

[6.3 面试高频问答](#6.3 面试高频问答)

[6.4 实际应用场景](#6.4 实际应用场景)


前言

提示:这里可以添加本文要记录的大概内容:

哈希表是数据结构中空间换时间的经典代表,凭借平均 O (1) 的增删查效率,成为算法刷题、STL 底层(unordered_map/unordered_set)、工程开发的核心结构。很多开发者只会直接调用库函数,却不懂哈希映射原理、哈希冲突成因、负载因子作用,更不会手写底层实现

本文基于 C++ 模板从零拆解哈希表完整知识体系:从哈希基础概念、常用哈希函数,到开放定址法(线性探测)、链地址法(哈希桶)两种冲突解决方案,附带完整可运行代码 + 逐行详细注释,对负载因子扩容、质数表设计、字符串哈希转换、删除状态标记等难点层层拆分,帮你彻底吃透哈希表底层逻辑,搞定面试手撕与原理问答


提示:以下是本篇文章正文内容

一. 哈希表基础核心概念

1.1 哈希表定义与核心思想

哈希也叫散列,是一种特殊的数据组织方式。 本质核心:通过哈希函数,把关键字 Key 和数组存储位置建立固定映射关系。插入数据时用哈希函数算出下标存入,查找时同样通过函数直接计算位置,实现快速访问,理想情况下做到 O (1) 查找效率


1.2 直接定址法

原理

当关键字范围高度集中、离散度小时,使用直接定址法最简单高效: 直接用关键字本身、或关键字偏移量,作为数组下标进行存储

典型场景

  1. 关键字集中在 [0,99],直接开 100 大小数组,key 就是下标
  2. 小写字母 a~z,用 字符ASCII - 'a'ASCII 映射为 0~25 下标

力扣 -字符串中的第一个唯一字符

优缺点总结

  • 优点:实现简单、无哈希冲突、访问极快
  • 缺点:关键字范围分散时极度浪费内存,甚至内存无法承受

1.3 哈希冲突(哈希碰撞)

当关键字范围分散,不能用直接定址法时,引入哈希函数 h(key),把 key 映射到哈希表 [0,M) 下标区间。

哈希冲突定义:两个不同的 Key,经过哈希函数计算后,映射到了同一个存储位置。

理想中可以设计完美哈希函数完全避免冲突,但实际工程中冲突不可避免。我们只能:

  1. 设计优秀哈希函数,尽量减少冲突;
  2. 配套成熟的冲突解决策略。

1.4 负载因子

负载因子也叫载荷因子、装载因子,英文 load factor

公式:

特性规律:

  1. 负载因子越大 → 哈希冲突概率越高 → 空间利用率越高
  2. 负载因子越小 → 哈希冲突概率越低 → 空间利用率越低

开放定址法负载因子必须小于 1;链地址法负载因子可以大于 1。工程中一般控制负载因子在 0.7~1 之间触发扩容,平衡冲突与空间


1.5 关键字转整数规则

哈希映射计算依赖整数下标,若 Key 不是整型(string、日期、自定义类型),需要先通过哈希算法转换成一个合法整型,再做取模映射。后续所有哈希函数讨论,都默认 Key 已经转为整型值


二. 常见哈希函数详解

一个优秀哈希函数目标:让所有关键字等概率、均匀散列分布在哈希表各个位置,降低冲突概率。

2.1 除留余数法(除法散列法)

公式

h(key)=key%M

M 为哈希表容量,取余数作为存储下标。

原理本质

相当于保留 key 二进制 / 十进制后若干位,后几位相同的 key 一定会冲突。

避坑要点

  1. 尽量不要让 M 为 2 的幂、10 的幂
  2. 教材推荐:M 取不接近 2 整数次幂的质数
  3. 工程灵活用法:Java HashMap 刻意用 2 的整数次幂做容量,利用位运算替代取模提升效率,再通过高低位异或让哈希值分布更均匀,属于实战优化,不必死扣书本理论。

2.2 乘法散列法(了解)

对哈希表容量 M 无特殊要求。 步骤:

  1. 关键字乘常数 A(0<A<1),取出小数部分
  2. 用 M 乘以小数部分,向下取整为哈希下标

公式: h(key)=floor(M×((A×key)%1.0)) 业界常用黄金分割点:A=(5​−1)/2≈0.618

2.3 全域散列法(了解)

若哈希函数固定公开,容易被恶意构造数据,让所有 key 映射到同一位置,造成严重哈希攻击

解决思路:引入随机性 ,初始化时随机选一个散列函数使用,攻击者无法预判。 公式: hab​(key)=((a×key+b)%P)%M P 选大质数,a、b 随机选取;注意:初始化选定后全程固定,不能每次增删查都换函数,否则查找失败

2.4 其他哈希方法

教材中还有平方取中法、折叠法、随机数法、数学分析法等,多用于特定业务场景,常规哈希表开发只需掌握除留余数法即可


三. 哈希冲突解决方案一:开放定址法

3.1 核心思想

所有元素全部存放在哈希表数组内部 ; 发生冲突时,按照固定规则向后探测,找到一个空闲位置存入。 特点:负载因子严格小于 1

3.2 三种探测方式

3.2.1 线性探测

冲突后从当前位置开始,依次向后逐个探测,到表尾则循环绕到表头

公式:

缺点:容易产生群集 / 堆积现象,连续冲突位置会扎堆,后续冲突都争抢后方位置,查找效率下降

下面演示 {19,30,5,36,13,20,21,12} 等这一组值映射到M=11的表中
1.题目条件拆解

  • 哈希表容量:M=11(下标 0~10)
  • 待插入数据:{19, 30, 5, 36, 13, 20, 21, 12}
  • 哈希函数:h(key) = key % 11

2.先算每个 key 的初始哈希位置

h(19) = 8, h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =
10,h(12) = 1

3.逐个插入过程(按题目给的顺序)

3.2.2 二次探测

为改善线性探测堆积问题,采用平方跳跃探测

公式:

正负双向跳跃,有效缓解群集问题
下面演示 {19,30,52,63,11,22} 等这⼀组值映射到M=11的表中

1.题目条件拆解

  • 哈希表容量:M=11(下标 0~10)
  • 待插入数据:{19, 30, 52, 63, 11, 22}
  • 哈希函数:h(key) = key % 11

2.先算每个 key 的初始哈希位置

h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0

3.按顺序插入,一步步看二次探测的过程

3.2.3 双重散列

用两个哈希函数:

  1. h1(key) 计算初始位置;
  2. h2(key) 计算探测偏移量。

公式: hashi​=(hash0​+i×h2​(key))%M 要求 h2​(key) 与容量 M 互质,保证能遍历到哈希表所有位置
下面演示 {19,30,52,74} 等这一组值映射到M=11的表中,设 h 2 ( key ) = key %10 + 1

3.3 开放定址法特殊状态设计

不能直接物理删除元素,否则会打断线性探测路径,导致后续元素查找失败。 引入三种状态:

  • EMPTY:位置为空,从未存放数据
  • EXIST:元素正常存在
  • DELETE:元素已逻辑删除,位置保留,不阻断探测路径

3.4 哈希仿函数与 string 哈希特化

普通整型可以直接强转做哈希,string 等类型需要自定义哈希转换。 采用 BKDR 哈希:乘质数累加字符 ASCII,让每个字符和顺序都参与计算,避免简单累加带来的冲突

哈希仿函数完整代码

先看通用模板:HashFunc<K>

1. 它是什么?

这是一个仿函数(函数对象) ,本质就是个重载了operator()的结构体,所以它可以像函数一样被调用

2. 它的作用

对于intlongchar这类本身就是整数的类型,直接把 key 转成size_t(无符号整数)返回就行

  • 比如int key = 100,直接返回(size_t)100,哈希表再用这个值对容量取模,就能得到下标
  • size_t是无符号整数,避免负数下标,是 C++ 里专门给数组下标用的类型

3. 为什么要这么写?

因为哈希表的底层逻辑,是把 key 映射成数组下标,下标必须是无符号整数

  • 整型 key:本身就是数字,直接转成无符号数就能用
  • 非整型 key(比如string):没法直接转,需要额外处理,这就是下面特化版本要做的事

重点:string 特化版本 HashFunc<string>

1. 为什么 string 不能直接像 int 那样转?

string本质是一串字符,比如"abc",你没法直接把它当成一个数字用。 如果直接简单累加字符的 ASCII 值,会有个致命问题:

  • 比如"abc""cba",字符相同顺序不同,累加结果会一样,哈希冲突严重
  • 再比如"ab""ba",也会出现同样的问题。

所以我们需要一个更聪明的算法,让不同顺序、不同字符的字符串,得到不同的哈希值 ,这就是BKDR哈希

2. BKDR 哈希的原理

初始化哈希值为 0,从 0 开始计算

  • auto e : key:遍历字符串里的每个字符,比如"abc"会依次取'a''b''c'
  • hash *= 131:乘一个质数 131(也可以用 13331、31 等,都是工程里常用的质数)
  • hash += e:加上当前字符的 ASCII 值

举个例子,算一下"abc"的过程:

  1. 初始hash = 0
  2. 'a'hash = 0 * 131 + 'a' = 97
  3. 'b'hash = 97 * 131 + 'b' = 97*131 + 98 = 12807 + 98 = 12905
  4. 'c'hash = 12905 * 131 + 'c' = 12905*131 + 99 = 1690555 + 99 = 1690654

再算一下"cba"

  1. 初始hash = 0
  2. 'c'hash = 0 * 131 + 'c' = 99
  3. 'b'hash = 99 * 131 + 'b' = 99*131 + 98 = 12969 + 98 = 13067
  4. 'a'hash = 13067 * 131 + 'a' = 13067*131 + 97 = 1711777 + 97 = 1711874

可以看到,"abc""cba"得到了完全不同的哈希值,避免了简单累加的冲突问题

3. 为什么选 131 这个质数?

  • 质数的特性是,它和其他数相乘时,能让结果分布更均匀,减少哈希冲突;
  • 131 是个工程里很常用的质数,和size_t的范围适配,溢出后也能得到比较均匀的分布;
  • 其他常用的还有 31、13331,效果都差不多,选 131 是个约定俗成的写法

三、重点:string 特化版本 HashFunc<string>

3.5 质数扩容表

为了让哈希表容量始终为质数、减少除留余数法冲突,借鉴 SGI STL 预设质数表,每次扩容取下一个更大质数

3.6 开放定址法完整实现(带详细注释)

cpp 复制代码
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

// 哈希位置状态
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 Hash = HashFunc<K>>
class HashTable
{
public:
    // 获取下一个质数(STL质数表)
    inline unsigned long __stl_next_prime(unsigned long n)
    {
        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
        };
        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;
    }

    // 构造:初始化质数容量
    HashTable()
    {
        _tables.resize(__stl_next_prime(0));
    }

    // 插入键值对
    bool Insert(const pair<K, V>& kv)
    {
        // 重复元素不插入
        if (Find(kv.first))
            return false;

        // 负载因子 >=0.7 触发扩容
        if (_n * 10 / _tables.size() >= 7)
        {
            HashTable<K, V, Hash> newHT;
            // 扩容到下一个质数
            newHT._tables.resize(__stl_next_prime(_tables.size() + 1));
            //  rehash:旧表有效数据重新映射插入新表
            for (size_t i = 0; i < _tables.size(); ++i)
            {
                if (_tables[i]._state == EXIST)
                {
                    newHT.Insert(_tables[i]._kv);
                }
            }
            // 交换新旧表
            _tables.swap(newHT._tables);
        }

        Hash hash;
        // 初始哈希位置
        size_t hash0 = hash(kv.first) % _tables.size();
        size_t hashi = hash0;
        size_t i = 1;

        // 线性探测:找空闲位置
        while (_tables[hashi]._state == EXIST)
        {
            hashi = (hash0 + i) % _tables.size();
            ++i;
        }

        // 存入数据并标记状态
        _tables[hashi]._kv = kv;
        _tables[hashi]._state = EXIST;
        ++_n;
        return true;
    }

    // 查找key
    HashData<K, V>* Find(const K& key)
    {
        Hash hash;
        size_t hash0 = hash(key) % _tables.size();
        size_t hashi = hash0;
        size_t i = 1;

        // 遇到EMPTY停止查找
        while (_tables[hashi]._state != EMPTY)
        {
            if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
            {
                return &_tables[hashi];
            }
            // 继续线性探测
            hashi = (hash0 + i) % _tables.size();
            ++i;
        }
        return nullptr;
    }

    // 逻辑删除:只改状态不删数据
    bool Erase(const K& key)
    {
        HashData<K, V>* ret = Find(key);
        if (ret == nullptr)
            return false;

        ret->_state = DELETE;
        --_n;
        return true;
    }

private:
    vector<HashData<K, V>> _tables;
    size_t _n = 0; // 有效元素个数
};

3.7 开放定址法难点总结

  1. 必须设置 DELETE 状态,防止探测路径断裂;
  2. 负载因子控制在 0.7,平衡冲突与空间;
  3. 扩容必须用质数表,降低除留余数法冲突;
  4. 线性探测简单但易堆积,二次 / 双重探测优化堆积问题。

四. 哈希冲突解决方案二:链地址法(拉链法 / 哈希桶)

4.1 核心思想

哈希底层是指针数组 ,每个位置称为一个桶; 映射到同一位置的冲突元素,挂载成单链表挂在桶下

所有冲突元素不占用哈希表数组本身空间,而是用链表链式存储

4.2 特性对比

  1. 负载因子可以大于 1,无严格上限
  2. 不存在开放定址法的群集堆积问题
  3. 删除节点直接物理释放,不需要 DELETE 标记
  4. STL unordered_map 底层默认采用链地址法

4.3 极端场景优化

个别桶链表过长时,查找效率退化到 O (n); Java8 HashMap 优化:链表长度超过阈值自动转为红黑树,把复杂度降到 O (logN)

4.4 链地址法完整实现(带详细注释)

cpp 复制代码
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

// 哈希桶链表节点
template<class K, class V>
struct HashNode
{
    pair<K, V> _kv;
    HashNode<K, V>* _next;

    HashNode(const pair<K, V>& kv)
        :_kv(kv), _next(nullptr)
    {}
};

// 链地址哈希表
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    // 获取下一个质数
    inline unsigned long __stl_next_prime(unsigned long n)
    {
        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
        };
        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;
    }

    // 构造:初始化指针数组为空
    HashTable()
    {
        _tables.resize(__stl_next_prime(0), nullptr);
    }

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

    // 插入元素
    bool Insert(const pair<K, V>& kv)
    {
        Hash hs;
        size_t hashi = hs(kv.first) % _tables.size();

        // 负载因子等于1 触发扩容
        if (_n == _tables.size())
        {
            // 新建更大容量指针数组
            vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
            // 遍历旧表,节点原地迁移 rehash
            for (size_t i = 0; i < _tables.size(); ++i)
            {
                Node* cur = _tables[i];
                while (cur)
                {
                    Node* next = cur->_next;
                    // 重新计算新下标
                    size_t newHash = hs(cur->_kv.first) % newtables.size();
                    // 头插到新表对应桶
                    cur->_next = newtables[newHash];
                    newtables[newHash] = cur;
                    cur = next;
                }
                _tables[i] = nullptr;
            }
            _tables.swap(newtables);
        }

        // 头插法插入链表
        Node* newnode = new Node(kv);
        newnode->_next = _tables[hashi];
        _tables[hashi] = newnode;
        ++_n;
        return true;
    }

    // 查找key
    Node* Find(const K& key)
    {
        Hash hs;
        size_t hashi = hs(key) % _tables.size();
        Node* cur = _tables[hashi];
        // 遍历当前桶链表
        while (cur)
        {
            if (cur->_kv.first == key)
                return cur;
            cur = cur->_next;
        }
        return nullptr;
    }

    // 删除key
    bool Erase(const K& key)
    {
        Hash hs;
        size_t hashi = hs(key) % _tables.size();
        Node* prev = nullptr;
        Node* cur = _tables[hashi];

        while (cur)
        {
            if (cur->_kv.first == 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;
    }

private:
    vector<Node*> _tables; // 桶数组:存链表头指针
    size_t _n = 0;         // 总有效元素个数
};

4.5 链地址法扩容优势

扩容时不新建节点,直接把旧表链表节点重新计算哈希、迁移到新表,节省内存开销和创建销毁开销,效率更高。


五. 哈希表核心难点深度拆解

  1. 开放定址法为什么要 DELETE 状态? 直接清空位置会截断线性探测路径,导致后续元素查找不到;逻辑标记删除保留探测链路。

  2. 哈希表容量为什么优先选质数? 除留余数法中,质数可以让 key 散列更均匀,避免 2 的幂、10 的幂带来的高位失效、集中冲突问题。

  3. string 哈希为什么不能直接累加 ASCII? 不同字符串字符相同、顺序不同时,累加和一致,冲突严重;BKDR 乘质数加权,让字符顺序和每个字符都参与哈希计算,分布更均匀。

  4. 开放定址负载因子 <1,链地址可以> 1? 开放定址所有元素挤在数组里,满了就无位置探测;链地址用链表挂载,一个桶可以挂无限元素,不受容量物理限制。

  5. Java HashMap 为什么用 2 的幂次容量? 利用位运算替代取模,计算更快;再通过高低位异或扰动,弥补 2 的幂次哈希分布不均的缺陷,属于工程实战优化。


六. 知识点总结与面试高频考点

6.1 两种冲突解决方式对比

| 方式 | 存储结构 | 负载因子 | 冲突表现 | 删除方式 | 工程常用度 |
| 开放定址法 | 数组内部存储 | <1 | 易堆积 | 逻辑标记删除 | 较少 |

链地址法 数组 + 链表 可 > 1 无堆积 物理直接删除 STL 底层常用

6.2 哈希函数设计原则

  1. 让关键字所有位都参与计算;
  2. 映射结果均匀散列,避免集中扎堆;
  3. 计算尽量高效,兼顾速度与冲突率。

6.3 面试高频问答

  1. 什么是哈希冲突?怎么解决?
  2. 负载因子的作用?为什么要扩容?
  3. 开放定址法删除为什么不能直接清空?
  4. 链地址法扩容怎么做 rehash?
  5. string 如何自定义哈希函数减少冲突?

6.4 实际应用场景

算法刷题两数之和、字符统计; STL unordered_map/unordered_set 底层; 业务缓存、路由映射、去重场景等

相关推荐
少司府1 小时前
C++进阶:多态
c语言·开发语言·c++·多态·抽象类·虚函数·虚表指针
愿天垂怜1 小时前
【C++脚手架】etcd 的介绍与使用
java·linux·服务器·c语言·c++·中间件·etcd
小则又沐风a1 小时前
进程篇: 进程概念的补充(了解环境变量和虚拟地址空间)
linux·运维·服务器·c++
郝学胜-神的一滴1 小时前
[简化版 GAMES 101] 计算机图形学 11:频域·卷积·抗锯齿
c++·unity·图形渲染·opengl·three·unreal
valan liya1 小时前
C++ 继承
开发语言·c++
噜噜大王_1 小时前
C++ 类和对象(下):初始化列表、static、友元与匿名对象全解
c++
lDevinl1 小时前
【无标题】
数据结构·c++·青少年编程
zincsweet1 小时前
System V 共享内存:原理剖析、代码架构分析与双端通信实战
linux·c++
wunaiqiezixin10 小时前
如何在C++中创建和管理线程
c++