❤️ STL 序列式容器 vector 超硬核源码剖析 ❤️

Posted 哪吒要进厂啦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️ STL 序列式容器 vector 超硬核源码剖析 ❤️相关的知识,希望对你有一定的参考价值。

前言

国庆七天假宛如一天假,定睛一看又要上班…
国庆档电影院热闹了不少,《长津湖》真的值回票价,吴京真不错,意外的是易烊千玺竟然演出了小戏骨的感觉,整部电影很有感染力,点赞!
外滩一如既往的人山人海,不过每次抱着去看人的心态还是能玩得开心的,节日就要有节日的氛围
总之,国庆快乐最重要!也祝各位节日快乐!祖国生日快乐! ❤️

❤️❤️❤️ 如果本文对你有所帮助,请不要忘了点赞、关注、收藏哦!灰常感谢! ❤️❤️❤️

第4章 序列式容器

4.1 容器的概念与分类

容器,置物之所也。
研究数据的特定排列方式,以利于搜寻或排序或其它特殊目的,这一专门学科我们称为数据结构(Data Structures)。大学信息类相关教育里面,与编程最有直接关系的科目,首推数据结构与算法(Algorithms)。几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL 容器即是将运用最广的一些数据结构实现出来(图4-1)。未来,在每五年召开一次的C++标准委员会的会议中,STL容器的数量还有可能增加。
众所周知,常用的数据结构不外乎array(数组)、list(链表)、tree(树)、stack(堆栈)、queue(队列)、hash table(散列表)、set(集合)、map(映射表)···等等。根据“数据在容器中的排列”特性,这些数据结构分为序列式(sequence)和关联式(associative)两种。本章探讨序列式容器,下一章探讨关联式容器。容器是大多数人对STL的第一印象,这说明了容器的好用与受欢迎。容器也是许多人对STL 的唯一印象,这说明了还有多少人利器(STL)在手而未能善用。

这里所谓的衍生,并非派生(inheritance)关系,而是内含(containment)关系。例如 heap内含一个 vectorpriority-queue 内含一个 heapstackqueue 都含一个dequeset/map/multiset/multimap 都内含一个 RB-treehast_x 都内含一个hashtable

4.1.1 序列式容器

4.2 vector

4.2.1 vector概述

vector 的数据安排以及操作方式,与 array非常相似。两者的唯一差别在于空间的运用的灵活性。array 是静态空间,一旦配置了就不能改变;要换个大(或小)一点的房子,可以,一切琐细得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。vector 是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector 的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始就要求一个大块头 array了,我们可以安心使用vector,吃多少用多少。vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦 vector旧有空间满载,如果客户端每新增一个元素, vector内部只是扩充一个元素的空间,实为不智,因为所谓扩充空间(不论多大),一如稍早所说是“配置新空间/数据移动/释还旧空间”的大工程,时间成本很高,应该加人某种未雨绸缪的考虑。稍后我们便可看到 SGI vector的空间配置策略。

4.2.2 vector定义摘要

以下是 vector定义的源代码摘录。虽然STL规定,欲使用 vector者必须先包括< vector>,但 SGISTL将 vector实现于更底层的<stl_ vector.h>

// alloc 是 SGI STL 的空间配置器
template<class T, class Alloc = alloc>
class vector {
public:
    typedef T value_type;
    typedef value_type* pointer;
    typedef value_type* iterator;
    typedef value_type& reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;

protected:
    // simple_alloc 是 SGI STL 的空间配置器,是源码所有容器都使用这个接口
    typedef simple_alloc<value_type, Alloc> data_allocator;

    iterator start;           //表示目前使用空间的头
    iterator finish;          //表示目前使用空间的尾
    iterator end_of_storage;  //表示目前可用空间的尾

    void insert_aux(iterator position, const T& x);
    void deallocate() {
        if(start)
            data_allocator::deallocate(start, end_of_storage - start);
    }
    void fill_initialize(size_type n, const T& value) {
        start = allocate_and_fill(n, value);
        finish = start + n;
        end_of_storage = finish;
    }

public:
    iterator begin() { return start; }
    iterator end() { return finish; }
    size_type size() const {
        return size_type(end() - begin());
    }
    size_type capacity() const {
        return size_type(end_of_storage - begin());
    }
    bool empty() const { return begin() == end(); }

    reference operator[](size_type n) {
        return *(begin() + n);
    }
    
    vector() : start(0), finish(0), end_of_storage(0) {}
    vector(size_type n, const T& value) {
        fill_initialize(n, value);
    }
    vector(int n, const T& value) {
        fill_initialize(n, value);
    }
    vector(long n, const T& value) {
        fill_initialize(n, value);
    }
    explicit vector(size_type n) {
        fill_initialize(n, T());
    }

    ~vector() {
        destroy(start, finish);  //全局变量
        deallocate();
    }

    reference front() { return *begin(); }
    reference back() { return *(end() - 1); }
    void push_back(const T& x) {
        if (finish != end_of_storage) {  //还有备用空间
            construct(finish, x);  //全局函数
            ++finish;
        } else  //无备用空间
            insert_aux(end(), x);  //成员函数,后续会分析
    }
    
    void pop_back() {
        --finish;
        destroy(finish);    //finish->~T 这里仅仅是调用指针finish所指对象的析构函数,不能释放内存
    }
  
    iterator erase(iterator position) {
        //如果移除的不是最后一个元素
        if (position + 1 != end())
            copy(position + 1, finish, position);  //全局函数

        --finish;
        destroy(finish);
        return position;
    }
    void resize(size_type new_size, const T& x) {
        if (new_size < size()) 
            erase(begin() + new_size, end());
        else
            insert(end(), new_size - size(), x);
    }
    void resize(size_type new_size) { resize(new_size, T()); }
    void clear() { erase(begin(), end()); }

protected:
    //配置空间并填满内容
    iterator allocate_and_fill(size_type n, const T& x) {
        iterator result = data_allocator::allocate(n);
        {
            uninitialized_fill_n(result, n, x);
            return result;
        }
    }

4.2.3 vector的迭代器

vector维护的是一个连续线性空间,所以不论其元素型别为何,普通指针都可以作为 vector的迭代器而满足所有必要条件,因为 vector迭代器所需要的操作行为, 如 operator*, operator->, operator++, operator--, operator+, operator-,operator+=, operator-=,普通指针天生就具备。 vector支持随机存取,而普通指针正有着这样的能力。所以, vector提供的是 Random Access Iterators。

template <class T, class Alloc = alloc>
class vector {
public:
	typedef T value_type;
	typedef value_type* iterator;	// vector的迭代器是普通指针
...
}

根据上述定义,如果客户端写出这样的代码:

vector<int>::iterator ivite;
vector<Shape>::iterator svite;

ivite的型别其实就是int*, svite的型别其实就是 Shape*。

4.2.4 vector的数据结构

vector所采用的数据结构非常简单:线性连续空间。它以两个迭代器 startfinish分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器end_of_ storage指向整块连续空间(含备用空间)的尾端:

template<class T, class Alloc = alloc>
class vector {
protected:
	iterator start;				//表示目前使用空间的头
	iterator finish;			//表示目前使用空间的尾
	iterator end_of_storage;	//表示目前可用空间的尾
...
}

为了降低空间配置时的速度成本, vector实际配置的大小可能比客户端需求量更大一些,以备将来可能的扩充。这便是容量( capacity)的观念。换句话说,一个 vector的容量永远大于或等其大小。一旦容量等于大小,便是满载,下次再有新增元素,整个 vector就得另觅居所,见图4-2运用 start, finish,end_of_storage三个迭代器,便可轻易地提供首尾标示、大小、容量、空容器判断、注标([1)运算子、最前端元素值、最后端元素值…等机能

4.2.5 vector的构造与内存管理

千头万绪该如何说起?以客户端程序代码为引导,观察其所得结果并实证源代码,是一个良好的学习路径。下面是一个小小的测试程序,我的观察重点在构造的元素的添加,以及大小、容量的变化:

// filename: 4vector-test. cpp
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
	//int i;
	vector<int>iv(2, 9);
	cout << "size = " << iv.size() << endl;			// size=2
	cout << "capacity = " << iv.capacity() << endl;	// capacity=2

	iv.push_back(1);
	cout << "size = " << iv.size() << endl;			// size=3
	cout << "capacity = " << iv.capacity() << endl; // capacity=4

	iv.push_back(2);
	cout << "size = " << iv.size() << endl;			// size=4
	cout << "capacity = " << iv.capacity() << endl; // capacity=4

	iv.push_back(3);
	cout << "size = " << iv.size() << endl;			// size=5
	cout << "capacity = " << iv.capacity() << endl; // capacity=8

	iv.push_back(4);
	cout << "size = " << iv.size() << endl;			// size=6
	cout << "capacity = " << iv.capacity() << endl; // capacity=8

	for (int i = 0; i < iv.size(); ++i)
	{
		cout << iv[i] << ' ';						// 9 9 1 2 3 4
	}
	cout << endl;

	iv.push_back(5);
	cout << "size = " << iv.size() << endl;			// size=7
	cout << "capacity = " << iv.capacity() << endl; // capacity=8

	for (int i = 0; i < iv.size(); ++i)
	{
		cout << iv[i] << ' ';						// 9 9 1 2 3 4 5
	}
	cout << endl;

	iv.pop_back();
	iv.pop_back();
	cout << "size = " << iv.size() << endl;			// size=5
	cout << "capacity = " << iv.capacity() << endl; // capacity=8

	iv.pop_back();
	cout << "size = " << iv.size() << endl;			// size=4
	cout << "capacity = " << iv.capacity() << endl; // capacity=8

	vector<int>::iterator ivite = find(iv.begin(), iv.end(), 1);
	if (ivite != iv.end())
	{
		iv.erase(ivite);
	}
	cout << "size = " << iv.size() << endl;			// size=3
	cout << "capacity = " << iv.capacity() << endl; // capacity=8

	for (int i = 0; i < iv.size(); ++i)
	{
		cout << iv[i] << ' ';						// 9 9 2 
	}
	cout << endl;

	ivite = find(iv.begin(), iv.end(), 2);
	if (ivite != iv.end())
	{
		iv.insert(ivite, 3, 7);
	}

	cout << "size = " << iv.size() << endl;			// size=6
	cout << "capacity = " << iv.capacity() << endl; // capacity=8
	for (int i = 0; i < iv.size(); ++i)
	{
		cout << iv[i] << ' ';						// 9 9 7 7 7 2 
	}
	cout << endl;

	iv.clear();
	cout << "size = " << iv.size() << endl;			// size=0
	cout << "capacity = " << iv.capacity() << endl; // capacity=8

	//system("pause");
	return 0;
}

vector缺省使用alloc(第二章)作为空间配置器,并据此另外定义了一个data_allocator,为的是更方便以元素大小为配置单位。

uninitialized_fill_n()会根据第一参数的型别特性( type traits,3.37节),决定使用算法fiill_n()或反复调用 construct()来完成任务(见2.3节描述)。
当我们以 push_back()将新元素插入于 vector尾端时,该函数首先检查是否还有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器 finish,使vector变大;如果没有备用空间了,就扩充空间(重新配置、移动数据、释放原空间)。

void push_back(const T& x) {
	if (finish != end_of_storage) {	// 还有备用空间
		construct(finish, x);		// 全局函数, 见2.2.3节
		++finish;					// 调整水位高度
	}
	else							// 已无备用空间
	{
		insert_aux(end(), x);		// vector member function,见以下列表
	}
}

template <class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x)
{
	if (finish != end_of_storage) {		//还有备用空间
		// 在备用空间起始处构造一个元素, 并以 vector最后一个元素值为其初值
		construct(finish, *(finish - 1);
		// 调整水位
		++finish;
		T x_copy = x;
		copy_backward(position, finish - 2, finish - 1);
		*position = x_copy;
	}
	else {								// 已无备用空间
		const size_type old_size = size();
		const size_type len = old_size = 0 ? 2 * old_size; 1;
		//以上配置原则:如果原大小为0,则配置1(个元素大小)
		//如果原大小不为0,则配置原大小的两倍
		//前半段用来放置原数据,后半段准备用来放置新数据

		iterator new_ start = data_allocator::allocate(len);//实际配置
		iterator new_finish = new_start;
		try {
			// 将原 vector的内容拷贝到新 vector
			new finish = uninitialized_copy(start, position, new_start);
			//为新元素设定初值
			construct(new_finish, x);
			//调整水位
			++new_finish;
			//将原 vector的备用空间中的内容也忠实拷贝过来(侯捷疑惑:啥用途 ? )
			new_finish = uninitialized_copy(position, finish, new_finish);
		}
		catch (...) {
			//"commit or roll back" semantics
			destroy(new_start, new_finish);
			data_allocator::deallocate(new_start, len);
			throw;
		}

		// 析构并释放原 vector
		destroy(begin(), end());
		deallocate();

		//调整迭代器, 指向新 vector
		start = new_start;
		finish = new_finish;
		end_of_storage = new_start + len;
	}
}
	

注意,所谓动态增加大小,并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此对 vector的任何操作,一旦引起空间重新配置,指向原 vector的所有迭代器就都失效了。这是程序员易犯的一个错误,务需小心。

4.2.6 vector的元素操作

vector所提供的元素操作很多,无法在有限篇幅中一一讲解其实也没有这种必要。为搭配先前对空间配置的讨论,我挑选数个相关函数作为解说对象,这些函数也出现在先前的测试程序中。

//将尾端元素拿掉,并调整大小
void pop_back() {
	--finish;				// 将尾端标记往前移一格, 表示将放弃尾端元素
	destroy(finish);		// destroy是全局函数,见第2章
}

// 清除[ first, last)中的所有元素
iterator stl源码剖析-序列式容器 之 list

硬核来袭 | 2 万字 + 10 图带你手撕 STL 关联式容器源码

《STL源码剖析》——第四章序列容器

硬核来袭 | 2 万字 + 10 图带你手撕 STL 关联式容器源码

STL学习笔记--4序列式容器之vector

STL之vector