【视频+文字讲解】C++那些事之彻底搞懂STL HashTable

news2024/10/7 16:20:44

C++那些事之彻底搞懂STL HashTable

  • C++那些事之彻底搞懂STL HashTable

  • 1.常用容器

    • 1.1 接口层

  • 2.HashTable原理

    • 2.1 _Hash_node结构

    • 2.2 hashtable实现

    • 2.3 如何确定桶?

    • 2.4 如何确定在桶中的位置?

  • 3.Rehash

    • 3.1 计算新桶大小

    • 3.2 判断是否需要rehash

    • 3.3 rehash

最近的繁星计划有一个task是阅读hashtable源码,看到一些朋友的提问,这里将总结一些面试常考点,以及看完hashtable你必须要掌握的几点内容。

  • unordered_xxx容器的h1、h2分别是什么?分别在什么场景下使用,在代码当中是如何使用的?起了什么作用?

  • unordered_xxx在hashtable中存储的key、value分别是什么?

  • hashtable的_Hash_node里面存储的是什么?key?value?还是?

  • hashtable的底层算法逻辑是?桶?怎么设计?

  • 桶的扩容机制是什么?

  • rehash是干什么用的,什么场景下触发?

2700d540ab4160d9499f775b05faa775.png

对应的视频教程:5c390036805a9753b200077c894caed8.jpeg

注:期待更多内容,欢迎加入知识星球呀~4bce90788dacfd9f633473ddcbf128a2.jpeg

1.常用容器

  • unordered_map

  • unordered_multimap

  • unordered_set

  • unordered_multiset

1.1 接口层

// unordered_map
template<typename _Key,
   typename _Tp,
   typename _Hash = hash<_Key>,
   typename _Pred = std::equal_to<_Key>,
   typename _Alloc = std::allocator<std::pair<const _Key, _Tp> >,
   typename _Tr = __umap_traits<__cache_default<_Key, _Hash>::value>>
  using __umap_hashtable = _Hashtable<_Key, std::pair<const _Key, _Tp>,
                                      _Alloc, __detail::_Select1st,
              _Pred, _Hash,
              __detail::_Mod_range_hashing,
              __detail::_Default_ranged_hash,
              __detail::_Prime_rehash_policy, _Tr>;

// unordered_multimap.
template<typename _Key,
   typename _Tp,
   typename _Hash = hash<_Key>,
   typename _Pred = std::equal_to<_Key>,
   typename _Alloc = std::allocator<std::pair<const _Key, _Tp> >,
   typename _Tr = __ummap_traits<__cache_default<_Key, _Hash>::value>>
  using __ummap_hashtable = _Hashtable<_Key, std::pair<const _Key, _Tp>,
         _Alloc, __detail::_Select1st,
         _Pred, _Hash,
         __detail::_Mod_range_hashing,
         __detail::_Default_ranged_hash,
         __detail::_Prime_rehash_policy, _Tr>;

// unordered_set
  template<typename _Value,
    typename _Hash = hash<_Value>,
    typename _Pred = std::equal_to<_Value>,
      typename _Alloc = std::allocator<_Value>,
    typename _Tr = __uset_traits<__cache_default<_Value, _Hash>::value>>
    using __uset_hashtable = _Hashtable<_Value, _Value, _Alloc,
     __detail::_Identity, _Pred, _Hash,
     __detail::_Mod_range_hashing,
     __detail::_Default_ranged_hash,
     __detail::_Prime_rehash_policy, _Tr>;
// unordered_multiset
  template<typename _Value,
    typename _Hash = hash<_Value>,
    typename _Pred = std::equal_to<_Value>,
    typename _Alloc = std::allocator<_Value>,
    typename _Tr = __umset_traits<__cache_default<_Value, _Hash>::value>>
    using __umset_hashtable = _Hashtable<_Value, _Value, _Alloc,
      __detail::_Identity,
      _Pred, _Hash,
      __detail::_Mod_range_hashing,
      __detail::_Default_ranged_hash,
      __detail::_Prime_rehash_policy, _Tr>;

对应到_Hashtable类:

template<typename _Key, typename _Value, typename _Alloc,
    typename _ExtractKey, typename _Equal,
    typename _H1, typename _H2, typename _Hash,
    typename _RehashPolicy, typename _Traits>
    class _Hashtable

我们把上面的模版参数一个个对应上。

容器类_Key_Value_Alloc_ExtractKey_Equal_H1_H2_Hash_RehashPolicy_Traits
unordered_map_Keystd::pair<const _Key, _Tp>std::allocator<std::pair<const _Key, _Tp>>__detail::_Select1ststd::equal_to<_Key>std::hash<_Key>__detail::_Mod_range_hashing__detail::_Default_ranged_hash__detail::_Prime_rehash_policy__umap_traits
unordered_multimap_Keystd::pair<const _Key, _Tp>std::allocator<std::pair<const _Key, _Tp>>__detail::_Select1ststd::equal_to<_Key>std::hash<_Key>__detail::_Mod_range_hashing__detail::_Default_ranged_hash__detail::_Prime_rehash_policy__ummap_traits
unordered_set_Value_Valuestd::allocator<_Value>__detail::_Identitystd::equal_to<_Value>std::hash<_Value>__detail::_Mod_range_hashing__detail::_Default_ranged_hash__detail::_Prime_rehash_policy__uset_traits
unordered_multiset_Value_Valuestd::allocator<_Value>__detail::_Identitystd::equal_to<_Value>std::hash<_Value>__detail::_Mod_range_hashing__detail::_Default_ranged_hash__detail::_Prime_rehash_policy__umset_traits

上表中列出的模板参数包括:

  • _Key:关联容器中的键类型。

  • _Value:关联容器中的值类型。

  • _Alloc:用于内存分配的分配器类型。

  • _ExtractKey:从键值对中提取键的函数对象类型。

  • _Equal:判断键是否相等的函数对象类型。

  • _H1:第一个哈希函数对象类型。

  • _H2:第二个哈希函数对象类型。

  • _Hash:哈希函数对象类型。

  • _RehashPolicy:重新哈希策略类型。

  • _Traits:特定容器特性的类型。

注意:_Hash是在无缓存hashcode情况下去使用,参考:

struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, _Hash, false>

2.HashTable原理

2.1 _Hash_node结构

在_Hashtable类中有一个__node_type,这个对应_Hash_node

using __node_type = __detail::_Hash_node<_Value, __hash_cached::value>;

_Hash_node有两个:

  • 存储hashcode

  • 不存储hashcode

+-----------------------------------+
|         _Hash_node_base            |
+-----------------------------------+
| - next: _Hash_node*                |
+-----------------------------------+

              ^
              |
              |

+-----------------------------------+
|         _Hash_node_value_base     |
+-----------------------------------+
| - _M_storage: __aligned_buffer<_Value> |
+-----------------------------------+
| + _M_valptr(): _Value*             |
| + _M_valptr() const: const _Value* |
| + _M_v(): _Value&                  |
| + _M_v() const: const _Value&      |
+-----------------------------------+

              ^
              |
              |
 
+--------------------------------+
|          _Hash_node            |
+--------------------------------+
| - _M_hash_code:  std::size_t   |
+--------------------------------+

2.2 hashtable实现

以find为例:

// 省略template
find(const key_type& __k)
{
  // 计算键的哈希码
  __hash_code __code = this->_M_hash_code(__k);
  
  // 根据哈希码计算桶的索引
  std::size_t __n = _M_bucket_index(__k, __code);
  
  // 在指定桶中查找节点
  __node_type* __p = _M_find_node(__n, __k, __code);
  
  // 如果找到节点,返回指向该节点的迭代器;否则返回尾后迭代器
  return __p ? iterator(__p) : end();
}
  1. 首先,通过调用 _M_hash_code 方法计算键的哈希码 __code。哈希码是根据键的值计算得到的,用于确定键在哈希表中的存储位置。

  2. 接下来,通过调用 _M_bucket_index 方法根据键的哈希码计算桶的索引 __n。桶是哈希表中的存储单元,每个桶可以存储一个或多个节点。通过将键的哈希码与桶的数量取模,可以确定键应该存储在哪个桶中。

  3. 然后,通过调用 _M_find_node 方法在指定的桶中查找节点。该方法会遍历桶中的节点,并根据节点的键和哈希码与目标键进行比较,找到匹配的节点。

  4. 如果找到了匹配的节点,就创建一个迭代器 iterator,指向该节点,并返回该迭代器。否则,返回尾后迭代器 end(),表示未找到匹配的节点。

2.3 如何确定桶?

里面有几个比较重要的函数:

// 接口
size_type
_M_bucket_index(__node_type* __n) const noexcept
{ return __hash_code_base::_M_bucket_index(__n, _M_bucket_count); }

size_type
_M_bucket_index(const key_type& __k, __hash_code __c) const
{ return __hash_code_base::_M_bucket_index(__k, __c, _M_bucket_count); }

// 实现
__hash_code
_M_hash_code(const _Key& __k) const
{ return _M_h1()(__k); }

std::size_t
_M_bucket_index(const _Key&, __hash_code __c,
    std::size_t __n) const
{ return _M_h2()(__c, __n); }

计算hashcode是通过h1计算出来,随后依据hashcode查找桶的index,此时通过h2计算,对于map与set(这里简称,都对应的是unordered_xxx)来说,H1是不一样,分别是Hash(key)、Hash(value),而h2是一样的,都是_Mod_range_hashing:

struct _Mod_range_hashing
{
  typedef std::size_t first_argument_type;
  typedef std::size_t second_argument_type;
  typedef std::size_t result_type;

  result_type
  operator()(first_argument_type __num,
       second_argument_type __den) const noexcept
  { return __num % __den; }
};

这里呢使用了operator()重载,所以上面的计算桶的公式就出来了。

buckek_index = (h1(key)) % _M_bucket_count;

2.4 如何确定在桶中的位置?

直接贴一下代码吧,_M_find_node会去查找目标节点之前的节点,这样方便单链表的插入、删除操作。

__node_type* _M_find_node(size_type __bkt, const key_type& __key,
                          __hash_code __c) const
{
  // 查找目标节点之前的节点
  __node_base* __before_n = _M_find_before_node(__bkt, __key, __c);

  // 如果找到了目标节点之前的节点,返回目标节点
  if (__before_n)
    return static_cast<__node_type*>(__before_n->_M_nxt);

  // 否则返回 nullptr,表示未找到目标节点
  return nullptr;
}

__node_base* _M_find_before_node(size_type __n, const key_type& __k,
                                 __hash_code __code) const
{
  // 获取桶中的首节点
  __node_base* __prev_p = _M_buckets[__n];
  
  // 如果桶中没有节点,返回 nullptr
  if (!__prev_p)
    return nullptr;

  // 遍历桶中的节点,查找目标节点之前的节点
  for (__node_type* __p = static_cast<__node_type*>(__prev_p->_M_nxt);; __p = __p->_M_next())
  {
    // 判断当前节点是否与目标节点匹配
    if (this->_M_equals(__k, __code, __p))
      return __prev_p;

    // 如果当前节点没有下一个节点,或者下一个节点所在桶的索引不等于当前桶索引,结束遍历
    if (!__p->_M_nxt || _M_bucket_index(__p->_M_next()) != __n)
      break;

    // 更新目标节点之前的节点为当前节点
    __prev_p = __p;
  }
  
  // 返回 nullptr,表示未找到目标节点之前的节点
  return nullptr;
}

到这里是不是非常清楚hashtable的实现呢?是的,首先是确定桶,再次是个链表然后查找到当前key的前一个节点,有了这个接口,你会发现插入、删除、查找都非常简单了。

3.Rehash

紧接着,还有个比较重要的称为rehash,相信大家很清楚rehash,当散列表的冲突到达一定程度,那么就需要重新将key放到合适位置,这里封装成了一个rehash policy:

struct _Prime_rehash_policy
{
   //...
};

rehash操作中提到:桶的大小(bucket size) 默认通常是最小的素数,从而保证装载因子(load factor 容器当前元素数量与桶数量之比。)足够小。装载因子用来衡量哈希表满的程度,最大加载因子默认值为1.0.

_Prime_rehash_policy(float __z = 1.0)
: _M_max_load_factor(__z), _M_next_resize(0) { }

当哈希冲突的时候,怎么rehash呢?

3.1 计算新桶大小

std::size_t
_Prime_rehash_policy::_M_next_bkt(std::size_t __n) const
{
  // Optimize lookups involving the first elements of __prime_list.
  // (useful to speed-up, eg, constructors)
  static const unsigned char __fast_bkt[12]
    = { 2, 2, 2, 3, 5, 5, 7, 7, 11, 11, 11, 11 };
  if (__n <= 11)
    {
      _M_next_resize =
        __builtin_ceil(__fast_bkt[__n] * (long double)_M_max_load_factor);
      return __fast_bkt[__n];
    }
  const unsigned long* __next_bkt =
    std::lower_bound(__prime_list + 5, __prime_list + _S_n_primes, __n);
  _M_next_resize =
    __builtin_ceil(*__next_bkt * (long double)_M_max_load_factor);
  return *__next_bkt;
}

函数中的 __fast_bkt 数组存储了一些预定义的较小的素数值,用于优化对前几个元素的查找。

如果 __n 小于等于 11,函数会直接返回 __fast_bkt[__n],并计算 _M_next_resize 的值,这个值表示下一次重新调整大小(resize)的阈值。

如果 __n 大于 11,函数将使用 std::lower_bound__prime_list 数组中查找大于等于 __n 的第一个素数。__prime_list 是另一个数组,它存储了一系列素数值。然后函数会计算 _M_next_resize 的值,并返回找到的素数。

该函数的目的是根据给定的哈希表大小 __n,计算出下一个合适的哈希桶(bucket)的大小,并返回该大小。

写个小函数,测试一下,假设hashtable依次初始化0个元素、1个元素,依次类推,用i表示,下面的结果为bucket的数量,可以看到跟前面的__fast_bkt__prime_list是对的上的。

➜  practice ./a.out                   
=====i = 0====
1
=====i = 1====
2
=====i = 2====
2
=====i = 3====
3
=====i = 4====
5
=====i = 5====
5
=====i = 6====
7
=====i = 7====
7
=====i = 8====
11
=====i = 9====
11
=====i = 10====
11
=====i = 11====
11
=====i = 12====
13
=====i = 13====
13
=====i = 14====
17
=====i = 15====
17
=====i = 16====
17
=====i = 17====
17
=====i = 18====

3.2 判断是否需要rehash

函数接受三个参数:__n_bkt 表示当前的哈希桶数量,__n_elt 表示当前的元素数量,__n_ins 表示待插入的元素数量。

std::pair<bool, std::size_t>
_Prime_rehash_policy::
_M_need_rehash(std::size_t __n_bkt, std::size_t __n_elt, std::size_t __n_ins) const
{
  if (__n_elt + __n_ins >= _M_next_resize)
  {
    long double __min_bkts = (__n_elt + __n_ins) / (long double)_M_max_load_factor;
    if (__min_bkts >= __n_bkt)
    {
      return std::make_pair(true, _M_next_bkt(std::max<std::size_t>(__builtin_floor(__min_bkts) + 1, __n_bkt * _S_growth_factor)));
    }

    _M_next_resize = __builtin_floor(__n_bkt * (long double)_M_max_load_factor);
    return std::make_pair(false, 0);
  }
  else
  {
    return std::make_pair(false, 0);
  }
}

static const std::size_t _S_growth_factor = 2;

函数首先通过比较 __n_elt + __n_ins_M_next_resize 的大小来判断是否需要重新散列。_M_next_resize 是在之前的操作中计算得到的下一次重新调整大小的阈值。如果 __n_elt + __n_ins 大于等于 _M_next_resize,则表示需要重新散列。

接着,函数计算 __min_bkts,即根据当前元素数量和负载因子 _M_max_load_factor 估计的最小所需桶数量。如果 __min_bkts 大于等于 __n_bkt,则说明当前桶数量已经不足以满足最小所需桶数量,需要重新散列。此时,函数会调用 _M_next_bkt 函数来计算下一个合适的桶数量,并返回 std::make_pair(true, p),其中 p 是下一个合适的桶数量。

如果 __min_bkts 小于 __n_bkt,则说明当前桶数量仍然足够,不需要重新散列。此时,函数会更新 _M_next_resize__n_bkt * _M_max_load_factor,表示下一次重新调整大小的阈值,并返回 std::make_pair(false, 0),表示不需要重新散列。

如果 __n_elt + __n_ins 小于 _M_next_resize,则说明当前元素数量未达到重新散列的阈值,不需要重新散列。函数直接返回 std::make_pair(false, 0),表示不需要重新散列。

所以,我们知道了扩容桶的规则是什么了。

long double __min_bkts = (当前元素数量 + 插入的元素数量) / (long double)_M_max_load_factor;
std::max<std::size_t>(__builtin_floor(__min_bkts) + 1, n * 2)

3.3 rehash

rehash最终会通过辅助函数_M_rehash_aux完成rehash操作。

  • 分配新桶

  • 数据迁移

注意哨兵节点或者说dummy节点_M_before_begin,每建立一个新bucket,第一个节点永远是_M_before_begin。对应代码为:__new_buckets[__bkt] = &_M_before_begin;

具体的操作见下面注释:

void
_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
           _H1, _H2, _Hash, _RehashPolicy, _Traits>::
    _M_rehash_aux(size_type __n, std::true_type)
{
  __bucket_type *__new_buckets = _M_allocate_buckets(__n);
  __node_type *__p = _M_begin();
  _M_before_begin._M_nxt = nullptr;
  std::size_t __bbegin_bkt = 0; // 前一个桶的index
  while (__p)
  {
    __node_type *__next = __p->_M_next();
    std::size_t __bkt = __hash_code_base::_M_bucket_index(__p, __n);
    if (!__new_buckets[__bkt])    // 桶不存在
    {
      __p->_M_nxt = _M_before_begin._M_nxt;  // __p链接上旧桶的第一个有效节点
      _M_before_begin._M_nxt = __p;  // _M_before_begin链接上__p
      __new_buckets[__bkt] = &_M_before_begin; // 新桶的第一个节点为_M_before_begin
      if (__p->_M_nxt) // 前一个旧桶有节点
        __new_buckets[__bbegin_bkt] = __p; // 当前__p为旧桶的第一个有效节点
      __bbegin_bkt = __bkt; // 更新桶的index
    }
    else // 桶存在 头插法 插入当前节点
    {
      __p->_M_nxt = __new_buckets[__bkt]->_M_nxt; 
      __new_buckets[__bkt]->_M_nxt = __p;
    }
    __p = __next;
  }

  _M_deallocate_buckets();
  _M_bucket_count = __n;
  _M_buckets = __new_buckets;
}

caac8821aca9beba99b3cf95fab36673.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/766636.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Github实时数据分析与可视化训练营火热开启!免费领取5000元云上资源

此次训练营内容基于GitHub Archive公开数据集&#xff0c;通过DataWorks将GitHub中的项目、行为等20多种事件类型数据实时采集至Hologres进行分析&#xff0c;同时使用DataV内置模板&#xff0c;快速搭建实时可视化数据大屏&#xff0c;从开发者、项目、编程语言等多个维度了解…

vscode 使用ssh进行远程开发 (remote-ssh)

介绍 visual studio code remote - ssh 可以通过ssh连接远程主机、虚拟机&#xff0c;打开远程文件夹&#xff0c;并利用vscode 的插件优势进行远程开发、调试等。 步骤 一、配置环境 因为remote-ssh 的ssh连接是基于openssh实现的&#xff0c;以及后续我们需要使用生成ss…

手把手教你搭建SpringCloud项目(七)集成Consul服务注册中心

一、了解Consul 这篇文章学习另外一个服务注册中心Consul&#xff0c;那什么是Consul&#xff1f; Consul是一个服务网格&#xff08;微服务间的 TCP/IP&#xff0c;负责服务之间的网络调用、限流、熔断和监控&#xff09;解决方案&#xff0c;它是一个一个分布式的&#xff…

[PCIE体系结构导读]PCIE总结(二)

PMCR&#xff08;Power Management Capabilities Register&#xff09;和PMCSR&#xff08;Power Management Control and Status Register) PMCR寄存器由16位组成&#xff0c;其中所有位和字段都是只读的。该寄存器的主要木得是记录当前PCIe设备的物理属性&#xff0c;系统软件…

vue2的 element 表格单元格合并

<template><div><el-table show-summary :summary-method"getSummaries" :span-method"objectSpanMethod" :data"tableData" row-key"id" ref"tableDom" border><el-table-column label"序号&quo…

10-2. 数组 Array 的实例方法

本文并没有写出数组的全部使用方法&#xff0c;想看全的可以看一下这个 Array - JavaScript | MDN 目录 1 常用内置方法 1.1 合并数组 concat() 1.2 复制元素覆盖到指定位置 copyWithin() 1.3 返回其他变量的数组形式 Array.from() 1.3.1 基本使用 1.3.2 将伪数组…

Hive Metastore、Hive server和Hive thrift服务

Hive Metastore Hive Metastore是Hive的核心元数据管理服务,它提供了元数据的持久化存储和访问控制的能力,使得 Hive 成为一个强大的数据仓库和分析平台,适用于处理大数据和进行复杂的数据查询与分析任务。 Apache Hive是一个建立在 Apache Hadoop 上的数据仓库和分析工具…

java多线程之FutureTask、Future、CompletableFuture

前面已经在多线程创建的时候有提到Future和FutureTask的简单用法&#xff0c;这里详细介绍下FutureTask以及CompletableFuture 一、FutureTask 1、FutureTask简介 FutureTask除了实现Future接口外&#xff0c;还实现了Runnable接口。因此&#xff0c;FutureTask可以交给 Exe…

回溯法实现N皇后问题

题1&#xff1a;&#xff08;需打印矩阵&#xff09; 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#…

命名空间,缺省参数与函数重载

目录 一&#xff0c;命名空间 1.何为命名空间 2.命名空间的使用 ​编辑 4.::作用域限定符 3.命名空间的展开 全局展开&#xff1a; 局部展开&#xff1a; 4.嵌套命名空间 二&#xff0c;缺省参数与函数重载 1.什么是缺省参数 2.什么是函数重载 3.两者的冲突 一&…

内部数据泄露:保护数据安全的挑战与解决方案

导语&#xff1a; 在当今数字化时代&#xff0c;数据是企业的核心资产之一。然而&#xff0c;随着科技的快速发展和信息的日益增长&#xff0c;数据安全问题也日益突出。其中&#xff0c;内部数据泄露成为企业所面临的重大挑战之一。本文将探讨内部数据泄露的危害&#xff0c;…

简直太高效了!一篇文章帮你快速了解企业如何实现无纸化办公

随着科技的发展和信息技术的普及&#xff0c;无纸化办公已经成为了现代企业的一个趋势。无纸化办公即指在企业日常工作中&#xff0c;尽量减少或不使用纸张作为工作载体&#xff0c;通过电子邮件、电子文档、电子表格等工具实现信息的传递和共享。无纸化办公不仅有利于环保&…

路由器隔绝广播,为什么还要VLAN?

路由器能隔绝广播&#xff0c;那要VLAN有什么用&#xff0c;既配置了VLAN又划分在不同的网段是不是有些多余了&#xff1f; 题主的意思是不要VLAN&#xff0c;可以吗&#xff1f; 当然可以。可以是可以&#xff0c;但是每台主机都能接收到路由器同一个接口&#xff08;三层/路…

如何正确有效的学习java前端(合集)

大量阅读 我是一个劲头十足的读者。所以&#xff0c;我的第一个关于学习JavaScript的技巧就是关于阅读&#xff0c;这绝不是巧合。书籍和其他的资源(如文章)可以在很大程度上帮助你学习JavaScript。通过实践学习&#xff0c;书籍是我学习新学科最喜欢的方式。在学习JavaScript的…

如何快速新建Linux虚拟机,安装linux系统步骤

1、安装好VMware workstation 软件&#xff1a;vmware workstation centos 7 2、点击创建新的虚拟机 3、选择安装操作系统&#xff0c;&#xff08;我这里使用centos-7 最简安装包&#xff0c;这种系统开销比较小&#xff0c;对机器性能要求不高&#xff09; 硬盘根据实际…

【C++修炼之路】内存管理

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;C修炼之路 文章目录 一、C/C 内存分布二、考题三、C语言动态内存管理方式四、C内存管理方式1、对内置类型2、对自定义类型 五、C对动态管理的升级六…

vue使用echarts根据页面大小 echarts窗口自适应

1. 使用window.onresize var myChart echarts.init(document.getElementById(myChart)); window.onresize () > {myChart.resize() }优点&#xff1a; 可以根据窗口大小实现自适应 缺点&#xff1a; window.onresize是绑定到window上的&#xff0c;切换vue页面时监听依…

WEB APIs day4 (1)

一、日期对象 1.实例化 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevi…

做私域选个微还是企微,哪个有优势?

做私域&#xff0c;你必须要有一个&#xff0c;引流新客户及留存老客户的地方。 于是&#xff0c;就有很多人讨论或者纠结&#xff1a;做私域&#xff0c;选择个人微信&#xff1f;还是企业微信&#xff1f; 让我们一起来看看个人微信和企业微信在功能和使用上有哪些区别&…

什么是 DevOps?看这一篇就够了!

目录 一、前因 二、记忆 三、他们说…… 3.1、Atlassian 回答“什么是 DevOps&#xff1f;” 3.2、微软回答“什么是 DevOps&#xff1f;” 3.3、AWS 回答“什么是 DevOps&#xff1f;” 四、DevOps 文化 4.1、什么是文化&#xff1f; 4.2、什么是 DevOps 文化&#xf…