《STL源码剖析》随笔

第二章 空间配置器

  内存分配过程
    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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值