1.1 空间配置器

STL的实现角度而言,第一个需要介绍的就是空间配置器,因为整个STL的操作对象(所有的数值)都存放在容器之内,而容器一定需要配置空间以置放资料。并且allocator称之为“空间配置器”而不是”内存配置器“的原因是因为空间不一定是内存,也可以是其他的辅助空间比如磁盘等。

1.1.1空间配置器的标准接口

allocator::value_type
allocator::pointer
allocator::const_pointer
allocator::reference
allocator::const_reference
allocator::size_type
allocator::difference_type
allocator::rebind(一个嵌套的class template。class rebind< U >//拥有唯一成员other,那是一个typedef 代表 allocator< U >)
allocator::allocator() //默认构造函数
allocator::allocator(const allocator&)//拷贝构造
template< class U> allocator::allocator(const allocator< U >&)//泛化的拷贝构造 使用类型U来完成
allocator::~allocator() //析构函数
pointer allocator::address(reference x) const//返回某个对象的地址
const_pointer allocator::address(reference x) const//返回某个对象的地址(此时无法赋值)
pointer allocator::allocate(size_type n,const void* = 0)// 配置空间,足以存储n个类型为T的对象。第二个参数是个提示。实现上可能会利用它来增进区域性(locality)或完全忽略
void allocator::deallocate(pointer p ,size_type n)//归还先前配置的空间
size_type allocator::max_size() const //返回可成功配置的最大量
void allocater::construct(pointer p, const T& x)//等同于new(const void*)p) T(x)
void allocater::destory(pointer p)//等同于p->~T()

2.2 具有次配置力的SGI空间配置器

SGI STL的配置器与众不同,其名称是alloc而非allocator,使用SGI配置器的写法如下:

vector<int,std::alloc> iv; //in GCC 默认也是缺省值

2.2.1 SGI 的标准空间配置器 std::allocator

虽然SGI也定义有一个符合部分标准、名为allocator的配置器,但SGI从未使用,其只是简单的封装了::operator new::operator delete,效率不佳。
其源代码实现如下:

#ifndef DEFALLOC_H
#define DEFALLOC_H#include <new.h>
#include <stddef.h>
#include <stdlib.h>
#include <limits.h>
#include <iostream.h>
#include <algobase.h>template <class T>
inline T* allocate(ptrdiff_t size, T*) {set_new_handler(0);T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));//这里直接调用的是new操作符if (tmp == 0) {cerr << "out of memory" << endl; exit(1);}return tmp;
}template <class T>
inline void deallocate(T* buffer) {::operator delete(buffer);//这里直接使用的是delete操作
}emplate <class T>
class allocator {
public:
//这里各种type的设计缘由 在第3章中讲解typedef T value_type;typedef T* pointer;typedef const T* const_pointer;typedef T& reference;typedef const T& const_reference;typedef size_t size_type;typedef ptrdiff_t difference_type;pointer allocate(size_type n){   return ::allocate((difference_type)n, (pointer)0);  }//分配空间void deallocate(pointer p) { ::deallocate(p); }//释放空间pointer address(reference x) { return (pointer)&x; }//获得空间首地址const_pointer const_address(const_reference x) { return (const_pointer)&x; }size_type init_page_size(){ return max(size_type(1), size_type(4096/sizeof(T))); }size_type max_size() const{ return max(size_type(1), size_type(UINT_MAX/sizeof(T))); }
};
//特化版本
class allocator<void> {
public:typedef void* pointer;
};

2.2.2 SGI特殊的空间配置器 std::alloc

直接使用new(底层实现使用malloc)和直接使用delete(底层实现使用free)没有考虑到效率上的优化,SGI另有法宝供本身内部使用。
一般而言,内存配置操作和释放操作是这样的:

class Foo{...};
Foo *pf = new Foo;//配置内存 然后构造对象
delete pf;//将对象析构 然后释放内存

一般new算式内含两阶段操作:(1)调用::operator new配置内存;(2)调用Foo::Foo()构造对象内容。
delete算式也有两阶段操作:(1)调用Foo::~Foo()将对象析构;(2)调用::operator delete释放内存。

SGI的精妙之处在于将内存分配和对象构造以及内存释放和对象析构两个部分分开。

2.2.3 构造和析构基本工具:construct( )和destroy()

下面是<stl_construct>的内容:
这里是在内存构造完之后的工作以及在内存释放前的工作。

#include <new.h>__STL_BEGIN_NAMESPACEtemplate <class T>
inline void destroy(T* pointer) {pointer->~T();//由于使用的是定位new运算符(实际上借用了实现准备好的内存),我们需要手动调动析构函数
}template <class T1, class T2>
inline void construct(T1* p, const T2& value) {new (p) T1(value);//使用了定位new运算符 在p指向的内存上构造T1::T1(value)
}template <class ForwardIterator>
inline void
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
//如果元素的数值型别(value type) 有重要的析构函数for ( ; first < last; ++first)destroy(&*first);
}//如果元素的数值型别的析构函数不重要 则不需要做什么 使用默认析构就好
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;//询问是否有重要的析构(针对在堆上分配内存的类)__destroy_aux(first, last, trivial_destructor());
}template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
//destroy的第二版本 结构两个迭代器,此函数设法找出元素的数值类型
//进而利用_type_traits<>求取适当措施__destroy(first, last, value_type(first));
}//以下是destory()第二版本 针对迭代器为char* 和wchar_t*的特化版
inline void destroy(char*, char*) {}
inline void destroy(wchar_t*, wchar_t*) {}

2.2.4 空间的配置与释放 std::alloc

完成了内存配置后的对象构造行为和内存释放钱的对象析构行为,然后再来看内存的配置和释放。
头文件<stl_alloc.h>SGI设计版本主要考虑的情况如下:
a. 向system heap要求空间,
b.考虑多线程状态
c.考虑内存不足时的应变措施。
d.考虑过多“小型区块”可能造成的内存碎片问题。

考虑到小型区块可能造成的内存破碎的问题,SGI设计了双层配置器,第一层配置器直接使用malloc()free(),第二级配置器则视情况采用不同的策略:当配置区块超过128bytes时,视为“足够大”,此时调用第一级配置器;
当配置器小于128bytes时,视为“过小”,为降低额外负担,便采用复杂的memory pool(内存池)整理方式,而不再求助于第一级配置器。

是否开放第二级配置器取决于 __USE_MALLOC是否被定义:

template<class T, class Alloc>
class simple_alloc {public:static T *allocate(size_t n){ return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); }static T *allocate(void){ return (T*) Alloc::allocate(sizeof (T)); }static void deallocate(T *p, size_t n){ if (0 != n) Alloc::deallocate(p, n * sizeof (T)); }static void deallocate(T *p){ Alloc::deallocate(p, sizeof (T)); }
};

其内部四个成员函数其实都是单纯的转调用,调用传递给配置器(可能是第一级也可能是第二级)的成员函数。这个接口使配置器的配置单位从bytes转为个别元素的大小sizeof(T))。SGI STL容器全部使用这个simple_alloc接口。(vector)就是。

一、二级配置器的关系、接口包装、实际运用方式如下图:

2.2.5 第一级配置器 __malloc_alloc_template剖析

源码:

#if 0
#   include <new>
#   define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
#   include <iostream.h>
#   define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
#endif
// malloc-based allocator 通常比稍后介绍的defalut alloc 速度慢//注意 无“template型别参数” 至于“非型别参数”inst 则完全,没派上用场template <int inst>
class __malloc_alloc_template {private:static void *oom_malloc(size_t);static void *oom_realloc(void *, size_t);#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUGstatic void (* __malloc_alloc_oom_handler)();
#endifpublic:static void * allocate(size_t n) //第一级配置器直接使用malloc()
{void *result = malloc(n);//以下无法满足需求时 改用oom_malloc()if (0 == result) result = oom_malloc(n);return result;
}static void deallocate(void *p, size_t /* n */)
{free(p); //第一级配置器直接使用free()
}static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{void * result = realloc(p, new_sz);//第一级配置器直接使用realloc()//以下无法满足需求时 改用oom_realloc()if (0 == result) result = oom_realloc(p, new_sz);return result;
}//以下仿真C++的set_new_handler() 换句话说 你可以通过它 指定自己的
//out - of - memory handler
static void (* set_malloc_handler(void (*f)()))()
{void (* old)() = __malloc_alloc_oom_handler;__malloc_alloc_oom_handler = f;return(old);
}
};// malloc _alloc out of memory handling
//初值为0 有待客户端设定
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{void (* my_malloc_handler)();void *result;for (;;) {   //不断尝试释放、配置、再释放、再配置my_malloc_handler = __malloc_alloc_oom_handler;//如果“内存不足处理例程”未被客户端指定 则直接抛出异常if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }(*my_malloc_handler)();     //调用处理例程 企图释放内存result = malloc(n); //再次尝试配置内存if (result) return(result);}
}template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{void (* my_malloc_handler)();void *result;for (;;) {   //不断尝试释放、配置、再释放、再配置my_malloc_handler = __malloc_alloc_oom_handler;if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }(*my_malloc_handler)();  //调用处理例程 企图释放内存result = realloc(p, n); //再次尝试配置内存if (result) return(result);}
}
//注意 以下直接将参数inst 指定为0
typedef __malloc_alloc_template<0> malloc_alloc;

2.2.6 第二级配置器 __default_alloc_template剖析

第二级配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的不仅是碎片,配置时的额外负担(overhead)也是一个大问题,额外负担永远无法避免,因为系统要靠多出来的空间来管理内存。

 union obj {union obj * free_list_link;char client_data[1];    /* The client sees this.        */};

这里存在的一些问题:
书上给的解释是这样的:“上述obj所用用的是union,由于union之故,从其第一字段观之,obj可被视为—个指针,指向相同形式的另一个obj。从其第二字段观之,obj可被视为一个指针,指向实际区块。一物二用的结果是,不会为了维护链表所必须的指针而造成内存的另一种浪费。
第一句话很好理解,在此就不再赘述,本文将探讨的重点放在第二句话,即其为何指向实际区块。
现在看下面一段代码的运行结果:

如图所示,指针p所存的地址为0x008ff7a0即为数组m1首元素地址,同时data首元素地址与数组m首元素地址相同。
对此,我的理解是:
此时test = (union obj*)m;
使得test指向数组m所在空间,则data[0]所占空间为m[0]所占空间,于是data所指空间即为m所指空间。
由上述可知在第二级空间配置器中,自由链表某节点client_data所指地址即为该区块首地址。

实现的部分源码:

 enum {__ALIGN = 8};//小型区块的上调边界enum {__MAX_BYTES = 128};//小型区块的上限enum {__NFREELISTS = __MAX_BYTES/__ALIGN};// free-list 个数//以下是第二级配置器//注意 无“template型别参数” 且 第二参数完全没派上用场//第一参数用于多线程环境下 本书不讨论
template <bool threads, int inst>
class __default_alloc_template {private:
// ROUND_UP() 将bytes上调至8的倍数static size_t ROUND_UP(size_t bytes) {return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));}union obj {  // free-list 的节点构造union obj * free_list_link;char client_data[1];    /* The client sees this.        */};// 16个free-liststatic obj * __VOLATILE free_list[__NFREELISTS]; // 以下函数根据区块大小 决定使用第n号free-list n 从1 起算 static  size_t FREELIST_INDEX(size_t bytes) {return (((bytes) + __ALIGN-1)/__ALIGN - 1);}// 返回一个大小为n的对象 并可能加入大小为n的其他区块到free - liststatic void *refill(size_t n);// 配置一块大空间 可容纳nobjs个大小为“size”的区块// 如果配置nobjs个区块有所不便 nobjs可能会降低static char *chunk_alloc(size_t size, int &nobjs);// Chunk allocation state.static char *start_free; //内存池起始位置,只在chunk_alloc()中变化static char *end_free;   //内存池结束位置,只在chunk_alloc()中变化static size_t heap_size;static void * allocate(size_t n){ ...}
static void  deallocate(void *p, size_t n){ ...}
static void * reallocate(void *p, size_t old_sz,size_t new_sz);
};// 以下是static data member的定义与初值设定
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj * __VOLATILE
__default_alloc_template<threads, inst> ::free_list[
# ifdef __SUNPRO_CC__NFREELISTS
# else__default_alloc_template<threads, inst>::__NFREELISTS
# endif
] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

2.2.7 空间配置函数 allocate()

static void * allocate(size_t n){obj * __VOLATILE * my_free_list;obj * __RESTRICT result;if (n > (size_t) __MAX_BYTES) {return(malloc_alloc::allocate(n));    //大于128就调用第一级配置器//注意在前面有typedef __malloc_alloc_template<0> malloc_alloc;}//寻找16个free list中适当的一个my_free_list = free_list + FREELIST_INDEX(n);result = *my_free_list;if (result == 0) {//没有找到可用的free-list 准备重新填充 free-listvoid *r = refill(ROUND_UP(n));  //下节详述return r;}//调整free list*my_free_list = result -> free_list_link;// 内存区块被使用了的话 free list 就不再指向改内存 return (result);//此时拨出的内存则可以使用placehold new运算符进行使用};

区块自free list 调出的操作 如图所示:

2.2.8 空间释放函数deallocate()

//p 不可以是 0
static void deallocate(void *p, size_t n){obj *q = (obj *)p;obj * __VOLATILE * my_free_list;//大于128就调用第一级配置器if (n > (size_t) __MAX_BYTES) {malloc_alloc::deallocate(p, n);  //使用free()释放内存return;}my_free_list = free_list + FREELIST_INDEX(n);//调整free list 回收区块q -> free_list_link = *my_free_list; //重新将free_list_link 指向这块内存区块 收回内存*my_free_list = q;}static void * reallocate(void *p, size_t old_sz, size_t new_sz);} ;

区块回收纳 free list的操作 如图所示:

2.2.9 重新填充free_list


// 返回一个大小为n的对象 并且有时候会为适当的free list增加节点
// 假设 n 已经适当上调至 8 的倍数

template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{int nobjs = 20;//调用 chunk_alloc(),尝试取得nobjs个区块作为free list的新节点// 注意参数nobjs是 pass by referencechar * chunk = chunk_alloc(n, nobjs);    //下节详述obj * __VOLATILE * my_free_list;obj * result;obj * current_obj, * next_obj;int i;
//如果只获得一个区块 这个区块就分配给调用者用 free list 无新节点if (1 == nobjs) return(chunk);
//否则准备调整free list 纳入新节点my_free_list = free_list + FREELIST_INDEX(n);//以下在chunk 空间内建立 free listresult = (obj *)chunk; //这一块准备返回给客户端//以下引导free list 指向新配置的空间(取自内存池)*my_free_list = next_obj = (obj *)(chunk + n);for (i = 1; ; i++) {  //从1开始 因为第0个将返回给客端current_obj = next_obj;next_obj = (obj *)((char *)next_obj + n);if (nobjs - 1 == i) {current_obj -> free_list_link = 0;break;} else {current_obj -> free_list_link = next_obj;}}return(result);
}

2.2.10 内存池(memory pool)

从内存池中取空间给free list 使用,是chunk_alloc()的工作:

//假设size 已经适当上调至8的倍数
//注意参数nobjs是pass by reference
template <bool threads, int inst>
char*
__default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{char * result;size_t total_bytes = size * nobjs;  //需要的总共空间size_t bytes_left = end_free - start_free;    //内存池剩余空间if (bytes_left >= total_bytes) {   //内存池剩余空间完全满足需求量result = start_free;start_free += total_bytes;return(result);} else if (bytes_left >= size) {//内存池剩余空间不能完全满足需求量 但足够供应一个(含)以上的区块nobjs = bytes_left/size;//计算可以供给的区块total_bytes = size * nobjs;result = start_free;start_free += total_bytes;return(result);} else {//内存池剩余空间连一个区块的大小都无法提供size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);// Try to make use of the left-over piece.if (bytes_left > 0) {//内存池还有一些零头,先配给适当的free list//首先寻找适当的free listobj * __VOLATILE * my_free_list =free_list + FREELIST_INDEX(bytes_left);//调整free list 将内存池中的残余空间编入((obj *)start_free) -> free_list_link = *my_free_list;*my_free_list = (obj *)start_free;}//配置heap空间 用来补充内存池start_free = (char *)malloc(bytes_to_get);if (0 == start_free) {//heap空间不足 malloc失败int i;obj * __VOLATILE * my_free_list, *p;// 试着检视我们手上拥有的东西 这不会造成伤害 我们不打算尝试配置//较小的区块 因为那在多进程机器上容易导致灾难//以下搜寻适当的free list//所谓适当是指“尚有未用区块 且区块足够大”之free listfor (i = size; i <= __MAX_BYTES; i += __ALIGN) {my_free_list = free_list + FREELIST_INDEX(i);p = *my_free_list;if (0 != p) {//free list内尚有未用区块//调整free list 以释放出未用区块*my_free_list = p -> free_list_link;start_free = (char *)p;end_free = start_free + i;//递归调用自己 为了修正nobjsreturn(chunk_alloc(size, nobjs));//注意:任何残余零头终将被编入适当的free list中备用}}end_free = 0;  // 如果出现意外(山穷水尽 到处都没内存可用了)// 调用第一级配置器 看看out of memory机制能否尽点力start_free = (char *)malloc_alloc::allocate(bytes_to_get);//这会导致抛出异常(exception)或内存不足的情况获得改善}heap_size += bytes_to_get;end_free = start_free + bytes_to_get;//递归调用自己 为了修正nobjsreturn(chunk_alloc(size, nobjs));}
}

一个例子:

2.3 内存基本处理工具

2.3.1 uninitialized_copy

template <class InputIterator, class ForwardIterator>
inline ForwardIteratoruninitialized_copy(InputIterator first, InputIterator last,ForwardIterator result) {return __uninitialized_copy(first, last, result, value_type(result));
}

uninitialized_copy()让我们能够将内存的配置与对象的构造行为分离开来,第三个迭代器result即是目的地址,其默认的大小大于等于last-first。其复制对象会调用copy construct来实现。

2.3.2 uninitialized_fill

template <class ForwardIterator, class T>
inline void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x) {__uninitialized_fill(first, last, x, value_type(first));
}

[first,last]范围内产生x元素的复制品。使用copy construct()来构造对象。

2.3.3 uninitialized_fill_n

template <class ForwardIterator, class Size, class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n,const T& x) {return __uninitialized_fill_n(first, n, x, value_type(first));
}

其在[first,first + n]的区间内构造值为xn个对象。使用copy construct()来构造对象。

以下是三个函数的执行过程,其对是否是POD(C struct)类进行了不同的处理:
a. 如果是POD则使用直接复制手法(高效率)。
b. 如果不是POD则需要一个一个元素的进行构造,无法批量进行。

STL源码剖析(第二章 空间配置器)相关推荐

  1. STL源码剖析-第一章STL概论与版本简介

    系列文章目录 第一章:STL概论与版本简介 文章目录 系列文章目录 前言 一.STL是什么? 二.STL六大组件 1.引入库 前言 源码之前 了无秘密 这本书不适合C++ 初学者,不适合 Generi ...

  2. STL源码剖析 第二次温习 细节审核

    临时对象的产生 临时对象也叫做 无名对象,(使用pass by value的方式会引发copy的操作,于是产生一个临时的对象),造成效率的负担,但是可以可以制造一些临时对象 在型别的后面 直接加上() ...

  3. STL源码剖析 第八章 配接器

    设计模式:将一个类的接口转化为另外一个类的接口 配接器的概观和分类 改变仿函数接口  函数配接器  :queue和stack 通过修饰deque函数接口来实现 改变容器接口      容器配接器  : ...

  4. 《STL源码剖析》读书笔——(1)空间配置器

    第二章 空间配置器(allocator) 空间配置器按我的理解就是C++ STL进行内存管理的组件(包括内存的申请和释放):当然,不只是内存,还可以向硬盘申请空间: 我主要看了内存的配置与释放(这里& ...

  5. stl源码剖析_《STL源码剖析》学习笔记

    第二章 空间配置器 简述空间配置器: 关于一级空间配置器: 直接使用malloc.free.realloc进行内存管理操作.且在内存不足时,会陷入oom_malloc,即模拟C++的set_new_h ...

  6. STL源码剖析学习二:空间配置器(allocator)

    STL源码剖析学习二:空间配置器(allocator) 标准接口: vlaue_type pointer const_pointer reference const_reference size_ty ...

  7. STL源码剖析 空间配置器 查漏补缺

    ptrdiff_t含义 减去两个指针的结果的带符号整数类型 ptrdiff_t (Type support) - C 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云 std::set_new_ ...

  8. STL源码剖析——空间配置器

    目录 构造和析构基本工具:construct() 和 destroy() 空间的配置与释放:std::alloc 二级空间配置器简述 空间配置函数allocate() 空间释放函数deallocate ...

  9. SGI STL源码剖析——空间配置器

    SGI STL源码剖析--空间配置器 前言 空间配置器 SGI空间配置器 内存配置和对象构造 构造和析构 空间的配置和释放 第一级配置器 第二级配置器 空间配置 重新填充 重中之重的内存池 前言    ...

最新文章

  1. python爬虫requests json与字典对象互相转换
  2. 阿姨帮万勇:O2O产品的颠覆与布局,阿姨帮未来发展战略独家披露
  3. [云炬创业基础笔记]第十一章创业计划书测试5
  4. UVA-1 #1. A + B Problem
  5. VC屏幕截图,保存为Bmp文件
  6. [react] 使用webpack打包React项目,怎么减小生成的js大小?
  7. HEC-RAS二维溃坝洪水模拟(尾矿库)
  8. 洛谷P2038 无线网络发射器选址 水题 枚举
  9. Android IPC —— AIDL的原理
  10. 市政管理学试题及答案
  11. 网络安全技术第六章——第一节计算机病毒及其防治(中毒表现、计算机病毒概述、计算机病毒分类、病毒传播路径、计算机病故防治建议、我之前写过一篇这个,有需要的可以去看一下)
  12. 如何减少电气设备漏电问题,其解决方案有哪些?
  13. 亚创集团任命联合创始人兼总裁欧阳云为CEO 重申继续推进IPO
  14. ##英语文本的单词统计 排序并输出前5%
  15. dubbo源码之拦截调用
  16. 计算机网络——网络基础_网络命令的使用(配置主机IP、ipconfig、ping、tracert、arp实战)
  17. Agent and recipient nouns
  18. 微软PowerApps整合PowerBI
  19. 等保测评合规意味着什么,你有没有想太多了
  20. python输出结果_python输出结果

热门文章

  1. 工具变量:Shock-IV中预处理平衡的必要性
  2. Premiere Pro之添加配音(十七)
  3. (原创)LEON3入门教程(一):什么是LEON3?需要哪些开发工具和软件?
  4. 浅析BMS上电源芯片SBC应用
  5. D19:Duplicate Number(重复数字,翻译+题解)
  6. 【Verilog】CRC校验码生成器原理及verilog实现
  7. 大数据架构师入门学习
  8. UG/NX10二次开发学习视频目录整理(KF篇)
  9. JS中的函数定义方式及全局函数
  10. 【网络进阶】五种IO网络模型(一)