第二章 空间配置器
内存分配过程
STL的内存分配分为两级。
第一级是在所需分配内存大于128字节时。
此时只使用malloc分配内存,当malloc无法取得有效内存时,则会调用名为’内存不足处理例程’,调用该函数后依旧无法成功取得内存,就抛出异常。
第二级是在所需分配内存小于128字节时。
第二级则是为了解决内存碎片问题。其维护16个自由链表,分别是 8~128字节中8的倍数,这些内存被初始化为一个链表,当需要分配n字节时,n被上对其到8的倍数,然后从对应的链表中弹出一块内存使用(比如需要45字节,其从48字节内存的链表中分出一块内存)。
这里不会产生空间的浪费,这些内存是被复用的,是一个union结构,当这个内存在链表中时,这块内存存储指向下一块内存的指针,当这块内存被分配给用户时,这块内存则被用来存储用户的数据。
当自由链表不足时,则会调用某个函数填充链表以供使用。
对象构造过程
首先是POD类型,POD类型则是:一个类必然拥有 无效的 构造、析构、复构、赋值等操作。这里的无效的原术语是trivial,意指不重要。关于其何为无效,在另一篇笔记深度探索C++对象模型中第二章第二条说明什么时候是一个有效的。
换一种不严谨方式的来讲,一个类不是派生类,没有虚函数,任何一个成员对象也没有用户提供的构造、析构、复构、赋值等函数时,这就是一个POD类型。
当是一个POD类型时,采取更效率的操作,直接为每个成员依次值赋值即可,因为他们的构造是不重要的。
当不是一个POD类型时,采取安全的操作,会稳妥的调用复构、赋值等函数,因为其有用户提供的操作。
第三章 迭代器与traits
大概就是通过 模板特化 和 typedef 来声明内嵌类型,来把不同的类型中的类型名称化作统一的符号处理,使用时通过模板类型iterator_traits模板来提取出所需要的对应类型使用。
迭代器类型则通过在使用对应的算法时,通过传递一个迭代器空对象,通过这个空对象的不同类型来区别使用何种类型的算法(单向 双向 随机访问等区别)。
第四章 序列式容器
不知道这章笔记怎么写呢,细节全在代码中体现,所以这里就只写一些概念以及使用方法或者注意事项?
1.vector
vector内部实现很简单,一块类似数组的空间,优势体现在尾部的插入删除,虽然头部也能插入,但是效率明显不佳。
主要就是一个空间的增长,元素在尾部插入时,当其备用空间不足,空间就会增长,增长过程是寻找一个新的足够的空间,空间的大小是当前已有元素需要的空间*2,然后把旧空间的数据搬到新空间,新元素也插入到新空间,释放掉旧空间。
如果在头部插入,虽然后成功,但是成功的代价就是搬移到新空间,先插入新元素,再把旧元素搬过去,在中间插入同理。
这个过程中,如果发生异常,至少会保证插入的之前的数据的完整性。
关于vector的迭代器,因为vector内部本质也是一个数组,所以迭代器直接使用指针计算实现。
至于插入删除元素是否导致迭代器的失效,就要取决于迭代器指向的元素是否被迁移了,比如说在元素中间插入导致插入点之后的元素被移动,或者搬移到新的空间,地址变了,指向原先地址的迭代器也就失效了。
2.list
list在标准库中是一个双向链表,本身没什么好说的,但是list提供的操作空间比较大,比如说将一个链表中的一整段接合到另一个链表。
list的迭代器,也是通过链表中的指针跳转,插入删除操作也都不会导致迭代器失效,因为一个节点要么存在要么消失,或者其内部数据被改变,但迭代器指向的节点地址是不会变的。
3.deque
相对于vector,deque是双向增长,即头尾插入很快,中间插入慢,deque也没有所谓的备用空间,删除元素可能导致一块区域被释放,申请元素可能导致一块区域被申请。
书中建议最好使用vector,而不是deque,对于deque的排序,可以先完整复制到vector,排序后再复制到deque。
deque的实现,是在内存中离散的内存块,所有的内存块都是固定程度大小的。存储固定数量的元素,而为了管理这些离散的内存块,出现了一个所谓的map,保存这些离散内存块的地址,这个map有点像vector,有了备用空间的概念,头尾都有备用空间来存储新的内存块地址,当map头部或者尾部满,就把map迁移到新的空间,但是离散的内存块本身却没有移动,移动的只有map中离散内存块的那些地址。
deque的迭代器有些复杂,为了遍历这些离散的内存块,迭代器内部,
首先有一个成员指向map的某个元素,这个元素就是某个离散的内存块地址。
还有两个成员分别指向该内存块中第一个有效元素,和最后一个有效元素。
第四个成员指向当前遍历的那个元素。
4.stack
stack直接在deque的基础上,堵上头尾的进出口,只在尾部插入删除,就实现了栈。
stack不允许遍历所以也没有迭代器。
5.queue
队列先进先出,默认也使用deque实现,当然在使用的时候也可传递list作为实现。
队列不允许遍历所以也没有迭代器。
6.heap
heap就是利用完全二叉树的概念直接在数组上进行操作。
标准库提供了几个函数。
void make_heap(RandomIt first, RandomIt last);
// 接受一段区间来建立一个大根堆,可以是数组地址,也可是vector迭代器
void push_heap(RandomIt first, RandomIt last);
// 向堆中插入一个元素,并且调整相关元素位置,使其符合堆的规则
void pop_heap(RandomIt first, RandomIt last);
// 移除堆中最大的那个元素,但是移除不是真的移除,因为把数组看作一个完全二叉树,所以最大的那个元素被放在完全二叉树的最后
// 也就是数组中最后的那个位置,需要手动移出。
// 接下来堆会重排,使用数组符合堆的规则
void sort_heap(RandomIt first, RandomIt last);
// 当不断调用pop_heap,那么数组中第一大、第二大的元会依次挪往数组中倒数第一、倒数第二的位置,如此就实现了堆排序。
7.priority_queue
这是一个优先级队列,直接使用前一节的堆函数实现,默认容器使用vector。
8.slist
这是一个单向链表,不明白其迭代器和结构为什么要实现的这么复杂。
一个迭代器基类,一个节点的基类。一个迭代器派生类,一个节点的派生类。
迭代器基类指向一个节点基类,节点基类只内含一个next指针。
迭代器的派生类继承基类,在其基础上实现了一些解引用、自增的操作。
节点的派生类则是基类基础上增加了数据成员。
迭代器的派生类节点的派生类没有关系,有关系的只有迭代器的基类指向节点的基类。
2021年2月5日21:30:49
第五章 关联式容器
本章主要讲解了红黑树以及采用拉链法的哈希表。
并在这两个结构上,分别实现了set、map、multiset、multimap等四个结构,这些实现基本都是封装红黑树和哈希表实现的。
其中红黑树版本的兼具自动排序功能,而hash版本的则没有。
在红黑树和哈希表内部,为了区分实现set、map是否可重复插入的版本,都分别实现了insert_unique、insert_equal,的插入函数。
笔记内容主要是红黑树插入节点部分的概念,以及hash表一些实现方式,没有任何关于代码的东西。
1.红黑树相关概念
红黑树的规则:
①节点非红即黑
②根节点必须黑
③若节点为红,子节点必须黑。
④任一节点到叶子节点的任何的叶子路径,所含黑节点数必须相同。
其中三四条可以换成另一种理解方式,他们是等价的:
③新节点之父必须黑。
④新节点必须为红。
红黑树插入的几种情况:
首先设:
新节点为X
其父节点为P
祖父节点为G
曾祖父节点GG
伯父节点(父节点的兄弟节点)S
外侧插入:在G左孩子的左孩子插入,或者右孩子的右孩子插入
内侧插入:在G左孩子的右孩子插入,或者右孩子的左孩子插入
情况①:S为黑色,且是外侧插入:P和G做一次单旋转,同时更改P和G的颜色
情况②:S为黑色,且是内侧插入:P、X做一次单旋转,同时更改G和X的颜色,然后G再旋转一次,类似平衡二叉树的双旋转。
情况③:S为红色,且是外侧插入,GG为黑:P和G旋转一次,改变X的颜色
情况④:S为红色,且是外侧插入,GG为红:P和G旋转一次,改变X的颜色,旋转后,GG和G都是红色,违反了红黑树的规则。为了避免这种情况,一个解决方法是,在插入节点的过程中,只要碰到一个节点的两个子节点都为红,就把这个节点改为红色,两个子节点都改为红色
情况⑤:S为红色,且是内侧插入:这种情况书上没有说…,之后其他地方看看吧,看的时候以为懂了,一细想好多问题啊。
2.哈希表
在STL中,提前提供了一组质数,从53到4294967291等28个至少。
不论是哈希表的初始长度,还是哈希表重整后的长度,都是这些质数中的一个。因为质数在计算上有较好的效率。
哈希表使用vector以及拉链法实现,其中每个元素都是一个node节点,类似链表的形式,默认情况下每个vector[index]都是NULL,当一个元素插入后,通过求模,得到对应的索引,直接在对应的vector[index]上插入新的节点,如果这个vector[index]里有东西了,那么就通过链表的方式把他们链接起来。不过STL中将这个链表称为桶。
而哈希表只能支持一些整型,诸如int、short、char、long,以及对应的无符号的类型。
还支持字符串类型。
除此之外的其他类型都不支持,需要自己第实现hash函数并传递。
3.其他
至于在红黑树和哈希表上实现的那些结构。
set、map、multiset、multimap
hash_set、hash_map、hash_multiset、hash_multimap
其中带multi的是可插入重复元素的。
带hash的是哈希表版本实现的。
2021年2月6日19:43:02
第六章 算法
不知道写什么呢,那么多函数抄一遍也没意思。
很多算法中,一般都有两个版本,一个提供默认的行为,一个可以传递一个自定义的仿函数。比如说有的算法默认比较两个成员哪个小小,然后可以自定义传递一个函数,比较哪个大。
质变算法,意指会改变被操作的元素对象,这类算法也提供两个版本,一个是直接在原来的地方修改,另一个是在副本上进行修改。比如说replace()和replace_copy()。
关于copy函数,会有一个类似第二章中POD类型的行为,对于那些有有效赋值成员函数的类来说,会老实调用赋值运算符,而对于没有的,则会直接采取内存拷贝的方法。而且使用copy函数时,最好搭配插入迭代器使用,见第八章。
关于set结构,有并集、交集、差集、对称差集等几个函数,不过他们只能用于红黑树版本的,hash版本的不能使用。
关于remove函数,她们把要移除的元素放到结构的尾部,并没有真正删除,需要调用erase函数来真正移除。
关于sort函数,首先要求是随机迭代器,list等是不能使用的。在sort内部,使用了快排、插排、堆排,当递归层次过深时使用堆排,元素极少时使用插排。
第七章 仿函数
关于仿函数,主要就是在类内部,重载operator()(参数){函数体},然后就可以把一个类对象当作一个函数一样调用。
1.作为谓词的两个底层结构
在STL中,很多算法函数要求提供谓词,这个谓词是一个函数,谓词分为一元和二元,一元即一个参数,二元即两个参数。通过参数返回一个值,大多是bool类型,比如说排序一个自定义类型,那么可能需要自己提供一个比较大小的二元谓词。
这两个结构在本章及第八章中随处可见,主要是为了方便取得一些函数的参数类型或者返回值类型。
他们都继承这两个结构之一,然后在需要的时候,取得需要的类型。详见后。
template <class Arg, class Result>
struct unary_function
{
typedef Arg argument_type;
typedef Result result_type;
};
template <class Arg1, class Arg2, class Result>
struct binary_function
{
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
};
2.算术类仿函数
加法:plus<T>
减法:minus<T>
乘法:multiplies<T>
除法:divides<T>
模取:modulus<T>
否定:negate<T>
template <class T>
struct plus : public binary_function<T, T, T>
{
T operator()(const T& x. const T& y) const (return x + y;)
}
使用时
plus<int> a;
cout << a(3,5);
cout << plus<int>()(3,5); // 这一行先用plus<int>()返回一个临时对象,然后用临时对象调用operator()(3,5)
其余类型都类似。
3.比较类仿函数
等于:equal_to<T>
不等:not_equal_to<T>
大于:greater<T>
大于等于:greater_equal<T>
小于:less<T>
小于等于:less_equal<T>
template <class T>
struct equal_to: public binary_function<T, T, bool>
{
bool operator()(const T& x. const T& y) const (return x == y;)
}
其余类型都类似,使用方式也和plus一样。
第八章 配接器
关于配接器有三个部分的内容。
一是容器配接。
二是迭代器。
三是函数。
1.容器配接
容器配接就是第二章的queue和stack,他们都是直接使用deque来完成的。
2.迭代器配接
这里有三种,分别是插入迭代器、反向迭代器、输入/输出迭代器。
①插入迭代器
插入迭代器名为迭代器,但实际上是一个类。
插入迭代器又分了三种,头部插入、尾部插入、普通插入。这里只写其中一个。
以下代码中,一个类back_insert_iterator,一个函数back_inserter。
在back_insert_iterator类中,构造函数取得一个容器对象,接下来重载赋值运算符,对容器的赋值操作会导致调用容器的push_back函数。这样就通过赋值一个back_insert_iterator对象,实现了对容器的尾插入操作。解引用、自增则是没什么用的操作。
在back_inserter函数中,传递一个容器,创建并返回一个back_insert_iterator的对象,就不需要亲自创建对象。
当这个back_insert_iterator对象被传递给copy等函数,copy函数内部对这个对象赋值,间接调用容器的插入函数。
前向插入迭代器和普通插入迭代器,也只是在内部container->push_front(value)、container->insert(value)
而已。
template <class Container> // 这个参数是迭代器的容器类型
class back_insert_iterator // 尾部插入
{
protected:
Container container;
...
...
public:
explicit back_insert_iterator(Container& x):container(&x){};
back_insert_iterator<Container>& operator=(const typename Contain::value_type& value)
{
container->push_back(value);
return *this;
}
back_insert_iterator<Container>& operator*(){return *this;}
back_insert_iterator<Container>& operator++(){return *this;}
back_insert_iterator<Container>& operator++(int){return *this;}
....
}
template <class Container>
inline back_insert_iterator<Container> back_inserter(Container& x)
{
return back_insert_iterator<Container>(x);
}
②反向迭代器
正向迭代器是 第一个元素 到 超尾元素。
反向迭代器是 最后一个元素 到 超前元素。
反向迭代器的实现只是对正向迭代器的包装,如vector,见后代码。
而对于reverse_iterator类,通过接受一个正向迭代器来构造。
通过第三章中的类型萃取,取得迭代器指向类型 之各种相关类型。
为了让迭代器行为看起来像 最后一个元素 到 超前元素。对反向迭代器的解引用是先将正向迭代器的超尾递减,变成最后一个元素,然后返回,反向迭代器就实现了。
同样自增自减的行为则是反过来,反向迭代器的自增操作 转而调用 正向迭代器的自减操作,自减操作同理。
...
class vector
{
reverse_iterator rbegin() {return reverse_iterator(end());}
reverse_iterator rend() {return reverse_iterator(begin());}
}
...
template <class Iterator>
class reverse_iterator
{
public:
...
typedef typename iterator_traits<Iterator>::iterator_category iterator_category;
typedef typename iterator_traits<Iterator>::pointer pointer;
typedef typename iterator_traits<Iterator>::reference reference;
...
protected:
iterator current; // 对应的正向迭代器
public:
reference operator*() const
{
Iterator tmp = current;
return *--tmp;
}
reverse_iterator& operator++()
{
--current;
return *this;
}
}
③输入输出流迭代器
对于输入流迭代器,一个成员存储一个流对象.
其自增操作,会调用一个read函数,read中则是将流的数据读出来。
解引用操作则导致用户拿到流读出的数据。
这两个组合起来,就把一个流对象的行为表现成了迭代器的行为。
对于输出迭代器,这里没有抄代码,其主要原理是重载operator=(const T& value);
每次对输出迭代器的赋值,都会导致数据流向其内部的流对象。
template <class T,...>
class istream_iterator
{
protected:
istream * stream;
T value;
bool end_marker
...
void read()
{
end_marker = (*stream) ? true : false;
if(end_marker) *stream>>value;
end_marker = (*stream) ? true : false;
}
public:
istream_iterator():stream(&cin), end_marker(false){}
istream_iterator(istream & s):stream(&s) {read();}
reference operator*()const {return value;}
istream_iterator<T, ...>& opearator++()
{
read();
return *this;
}
}
3.函数配接
将这里主要讲了
①使用bind1st和bind2ed对函数参数进行提前绑定,返回一个仿函数
bind1st将第一个形参和实参提前绑定,bind2ed则是第二形参。
假设现在要把一个二元函数的第一形参提前绑定。
这里有一个bind1st的函数,还有一个binder1st的类。
bind1st接受那个函数,和那个函数第一形参的实参,内部先取得第一参数的类型,然后构造一个binder1st的类对象。
在binder1st类内部,一个成员存储那个函数,一个成员存储那个第一参数的实参,接着重载operator()),这样就把一个二元函数封装成了一个一元函数对象,第一实参在这个函数对象内部存储,第二实参在调用operator()时传递,而在operator()内部,调用那个函数,分别传递两个参数。
在外部,就像是调用了一个一元函数一样。
bind2ed同理。
template <class Operation, class T>
inline binder1st<Operation> bind1st(const Operation& op, const T& x)
{
typedef typename Operation::first_argument_type& arg1_type;
return binder1st<Operation>(op, static_cast<arg1_type>(x)); // 这里书中是C风格转型,为了清晰,我把它换成了c++形式的
}
template <class Operation>
class binder1st: public binary_function<tepename Operation::second_argument_type>
{
protected:
Operation op; // 那个要绑定参数的函数
typename Operation::first_argument_type value;
public:
binder1st(const Operation& x, const typename Operation::first_argument_type& y):op(x), value(y);
typename Operation::result_type operator()(const typename Operation::second_argument_type& x)
{
return op(value, x);
}
}
②ptr_fun将一个普通函数将函数包装成函数对象的形式
这个简单,不想抄代码了,像bind1st那样,没有提前存储参数,只是把那个函数存起来,重载operator(),在调用operator()的时候传递参数即可,只不过,身份从函数变成了一个类对象,一个仿函数。
③mem_fun和mem_fun_ref将成员函数包装成仿函数
这个形式同样和bind1st类似,区别在于,成员函数需要一个已存在的对象来调用,同样是一个类重载operator(),在内部存储了那个成员函数的地址,而在operator(T& obj)时,可以传递一个对象,内部则通过类似obj.*fun
的形式,通过类对象调用那个成员函数。
for_each(v.begin(), v.end(), mem_fun(&classname::member_function));
mem_fun返回一个仿函数对象
这个仿函数对象的operator()的重载像是:operator()(typename v::value_type& obj);
在内部,obj.*member_function();
2021年2月10日20:12:55