STL源码剖析---vector::insert()

写在前面

对于数组,如果我们去实现按位置 position 插入 n 个值为 x 的元素,怎么做?假设空间充足,一般的做法是:[position,last)所有元素顺次后移 n 个位置,然后 [position, position+n) 填充成 x 。常规思维也能达到目的,但是大师到底是大师,他们重重考虑了效率,面对最适合的情形总是选择最优的解决办法。稍后我们就可以看到 Alex Stepanov 以及 Matt AusternDavid Musser 等巨匠他们是如何实现的。

如果对于这一块知识有任何意见或建议的话,欢迎在博客下方留言或者直接致电 [email protected]。源码剖析的道路崎岖难行,但是欲带皇冠必承其重,这是我们应该付出的而且值得的代价。

一、储备知识

在 SGI STL中,Alex Stepanov 以及 Matt AusternDavid Musser 等巨匠严格将空间分配和回收对象的构造和析构区分开来。当我们去new(new operator)一个对象的时候,步骤是这样的:

  • 配置内存。 operator new (类比于malloc)
  • 构造对象。 placement new(定位new)
  • 返回动态内存首地址。(动态内存是通过指针或引用调用)

我们也很简单就将 new operator 和 operator new 区分开了啊。new operator 负责内存申请和对象构造,operator new 只负责内存申请类似于 malloc 。配置内存交付给空间配置器 alloc 来完成,在本篇博客主要讨论的是面对已经拿到的内存,如何进行对象的构造,也即完成 placement new 的功能。空间配置器 alloc 是一个很庞大的内容,本博客不予展开。

现在我们已经拿到申请到手的堆内存了,如何使用它?也就是说,我跳过空间配置器没有进行谈论。

1.1 迭代器的类型

迭代器一共有5种(InputIterator / OutputIterator / ForwardIterator / BidirectionalIterator / RandomIterator)。迭代器是一种模仿部分或全部 raw pointer 行为的对象。(行为有p->   *p   ++或--   p[n]   p+n或p-n   p1-p2   p1

任何一个迭代器,其类型应该永远落在“该迭代器所隶属之各种类型中最强化的那个”。意思就是说呢,如果一个迭代器类型是RandomIterator,那么不要把它称作 InputIterator(虽然也没错)。不可以将InputIterator赋值给RandomIterator。这里需要指明另一个重要的概念,向上转换向下转换的知识点。

向上转换:派生类--->基类;向下转换(不建议): 基类--->派生类。向上转换一定是安全的,无非就是派生类中的非基类部分丢失了而已,向下转换不一定是安全的,因为派生类如果有所扩展,会导致派生类的非基类部分残缺。

注意揣摩源码参数列表的迭代器类型,不是随便给定的,必须达到最低要求。

1.2 低层次函数

低层次函数面对的是未初始化的内存,操作未初始化内存和已初始化内存是否有区别视情况而定。 C++中认为一切变量都是对象,int型变量也是对象。作为对象,就自然而然有4个特殊的成员函数,default ctor、copy ctor、operator assignment、dtor,Alex Stepanov 以及 Matt AusternDavid Musser 等巨匠通过类型萃取,能够判断一个对象的4个成员函数是否是 trivial 。

为什么要分的这么细呢?如果一个对象的构造函数没有什么实际意义,那么就不要调用;直接调用memmove(内存直接复制行为)乃是最佳选择。怎么理解呢?就拿内置类型 int 来说吧,进行内存复制和调用构造没区别啊,所以选择内存复制因为它快。不得不感叹 Alex Stepanov 以及 Matt AusternDavid Musser 等巨匠的思维是如此的缜密

//拷贝函数:完成源区间[first, last)的内容向目标区间[result, result + (last-first) )的拷贝。为copy函数提供服务。
template
ForwardIterator
uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator result);

//填充函数I :完成将目的区间[first, last)全部填充成x。为fill函数提供服务。
template
void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x);

//填充函数II :完成将目的区间[first, first+n)全部填充成x。为fill_n函数提供服务。
template
ForwardIterator
uninitialized_fill_n(ForwardIterator first, Size n, const T& x);

1.3 高层次函数

高层次函数享受低层次函数提供的服务,也就是说低层次函数有很多运行流程,流程之一就是调用上层函数。

//重载或多载函数有特化版本可以被调用,是不会优先调用模板版本的。
//将源区间[first, last)的内容向目标区间[result, result+(last-first))进行复制。
template
inline OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result);
//下面是copy的两个特例化版本。
inline char* copy(const char* first, const char *last, char *result);
inline wchar_t* copy(const wchar_t *first, const wchar_t *last, const wchar_t *result);


//经过uninitialized_fill函数的过滤,能够确定调用时只是进行简单的值填充。将[first, last)填充成value。
template
void fill(ForwardIterator first, ForwardIterator last, const T& value)
{
	for(; first != last; ++first)
		*first = value;
}

//经过uninitialized_fill_n函数的过滤,能够确定调用时只是进行简单的值填充。将[first, first+n)填充成value。
template
OutputIterator fill_n(OutputIterator first, Size n, const T& value)
{
	for(; n > 0; --n, ++first)
		*first = value;
	return first;
}

二、vector::insert()源码剖析

伤其十指不如断其一指,我将源码剖析的范围局限在 vector 剩余空间充足的情况,剩余空间不充足就需要进行扩容(日后再说)。

//将n个元素x插到position位置之前。//STL规定,区间描述时一律左闭右开(半开半闭),[start, finish)
template
void vector::insert(iterator position, size_type n, const T& x)
{
	if(n != 0)	//元素个数大于0才需要进行操作。
	{
		if(size_type(end_of_storage - finish) >= n)
		{
			T x_copy = x;
			const size_type elems_after = finish - position;	//计算插入点之后的元素个数。
			iterator old_finish = finish;	//记下原先区间内末尾元素的下一位置。
			if( elems_after > n)	//插入点之后现有元素个数 大于新增元素个数(n)。
			{
				//将源区间[finish-n, finish)填充到目标区间[finish, finish+n)。
				uninitialized_copy(finish-n, finish, finish);	
				finish += n;	//尾端标记后移。
	//将源区间[position,old_finish-n)从逆向填充到目标区间[old_finish-n, old_finish)。
				copy_backward(position, old_finish-n, old_finish);	
				//将目标区间[position, position+n)填充成x_copy。
				fill(position, position+n, x_copy);
			}
			else	//插入点之后现有元素个数 小于等于 新增元素个数(n)。
			{
				//将[finish, finish+(n-elems_after))全部填充成x_copy。
				uninitialized_fill_n(finish, n-elems_after, x_copy);
				finish += n- elems_after;	//尾端标记后移。
				//将源区间[position, old_finish)填到目标区间[finish, finish+(old_finish-position))
				uninitialized_copy(position, old_finish, finish);
				finish += elems_after;	//尾端标记后移。
				//将目标区间[position, old_finish)填充成x_copy。
				fill(position, old_finish, x_copy);
			}
		}
		else
		{
			......	//因为剩余空间不足,所以需要进行扩容。此部分源码日后再剖析。
		}
	}
}

2.1 插入点之后元素个数大于新增元素个数 

代码片段告诉我们,一共分三步:

if( elems_after > n)	//插入点之后现有元素个数 大于新增元素个数(n)。
{
	//将源区间[finish-n, finish)填充到目标区间[finish, finish+n)。
	uninitialized_copy(finish-n, finish, finish);	
	finish += n;	//尾端标记后移。
//将源区间[position,old_finish-n)从逆向填充到目标区间[old_finish-n, old_finish)。
	copy_backward(position, old_finish-n, old_finish);	
	//将目标区间[position, position+n)填充成x_copy。
	fill(position, position+n, x_copy);
}

第一步:在源区间末尾切出一块同待插入区间等长的部分,整体后移;因为源区间末尾之后面对的是未初始化的内存,所以调用 uninitialized_copy 。

STL源码剖析---vector::insert()_第1张图片

 第二步:第一步的剩余部分逆向拷贝的方式,整体右移;因为是对已初始化空间的操作而且是逆向拷贝,所以调用copy_backward 。

STL源码剖析---vector::insert()_第2张图片

第三部分:第二部分的空洞部分由待插入序列进行填充。因为是对已初始化空间的操作,所以调用 fill 。

STL源码剖析---vector::insert()_第3张图片

2.2 插入点之后的元素个数小于等于新增元素个数 

else	//插入点之后现有元素个数 小于等于 新增元素个数(n)。
{
	//将[finish, finish+(n-elems_after))全部填充成x_copy。
	uninitialized_fill_n(finish, n-elems_after, x_copy);
	finish += n- elems_after;	//尾端标记后移。
	//将源区间[position, old_finish)填到目标区间[finish, finish+(old_finish-position))
	uninitialized_copy(position, old_finish, finish);
	finish += elems_after;	//尾端标记后移。
	//将目标区间[position, old_finish)填充成x_copy。
	fill(position, old_finish, x_copy);
}

第一步:整体后移;因为面对的是未初始化的内存,所以调用 uninitialized_fill_n 。 

STL源码剖析---vector::insert()_第4张图片

第二步: 将待插入区间的相应部分进行填充,因为面对的是未初始化的内存,所以调用 uninitialized_copy 。

STL源码剖析---vector::insert()_第5张图片

 第三步:将待插入区间的剩余部分进行填充,因为面对的是已初始化的内存,所以调用 fill 。

STL源码剖析---vector::insert()_第6张图片

三、参考资料

【1】STL模板库中vector的用法

【2】详解vector内部机制

你可能感兴趣的:(STL源码剖析---vector::insert())