Effective STL 第一章:容器(四)

5 篇文章 0 订阅

第5条:区间成员函数优先于与之对应的单元素成员函数

1,区间成员函数是指使用两个迭代器参数来确定该成员操作所执行的区间的这类函数

如:vector的insert方法:v1.insert(v1.end(), v2.begin(), v2.end());

 

2,相比于单元素成员函数,使用区间成员函数的好处

2.1,可以少写一点代码(如上面的插入操作,若使用单元素成员函数就得写一个显示的for循环进行处理)

2.2,通常使用区间成员函数会得到意图清晰和更加直接的代码,增加了程序的可读性

2.3,通常区间成员函数会使你的效率更优(后面着重看下效率的问题)

 

3,当处理标准序列容器时,为了取得同样的效果,使用单元素的成员函数比使用区间成员函数需要更多的调用内存分配子,更频繁的拷贝对象或者做更多的冗余操作

如:将int数组拷贝到一个vector的前端

实现1:(区间成员函数实现)

int data[numValues];

vector<int> v;

v.insert(v.begin(), data, data + numValues);

 

实现2:(通过显示循环调用单元素成员函数实现)

vector<int>::iterator insertPos(v.begin());

for (int i = 0; i < numValues; ++i)

{

insertPos = v.insert(insertPos, data[i]);

++insertPos;

}

此处必须记下insert的返回值以供下次循环时使用,若不记录更新insertPos,则可能会出现两个问题:

问题一:第一次循环迭代后的所有循环迭代都将导致不可预料的行为,因为每次insert都有可能使insertPos无效

问题二:插入总发生在vector的最前面,结果数组中的值被以相反的顺序拷贝到了v中,与实现意图不相符

 

实现3:(把循环替换为copy调用实现)

copy(data, data + numValues, insert(v, v.begin()));

当copy模板被实例化之后,基于copy的代码和使用显式循环的代码几乎是相同的,本质上同实现2

 

效率分析:

同使用区间成员函数的insert相比,使用单元素版本的insert共有三个方面影响了效率

第一,不必要的函数调用。

          把numValues个元素逐个插入到v中导致了对insert的numValues次调用,而区间形式的insert只做一次函数调用,节省了numValues - 1次

          当然若在使用单元素成员函数的时候使用内联可能会避免该处的效率损失,但实际中不见得会使用内联

第二,v中已有的元素被频繁的移动到插入后他们所处的位置,此处的开销很大

          每次调用insert插入新元素到v中,插入点之后的每个元素都要向后移动一个位置,以便为新元素腾出空间;我们向v前端插入numValues个元素,意味着v中插入点之后的每个元素都要向后移动numValues个位置,每次调用insert,每个元素需向后移动一个位置,所以每个元素将移动numValues次,如果插入前v中有n个元素,则有n*numValues次移动

          此处v中存储的是int类型,所以移动可能最终是调用memmove实现,这样开销还会小点,实际中我们可能存储的是用户自定义的类型(比如某个类对象),这样每次移动都会调用该类型的赋值操作符或拷贝构造函数(大多数情况下会调用赋值操作符,但每次vector中的最后一个元素被移动时,将会调用该元素的拷贝构造函数),所以通常情况下,把numValues个元素逐个插入到含有n个元素的vector的前端将会有n*numValues此函数调用的代价(insert函数调用),(n - 1)*numValues次赋值操作符和numValues次拷贝构造函数的代价(移动元素的代价)

          但C++标准要求区间insert函数把现有容器中的元素直接移动到他们最终的未知数,即只需要付出每个元素移动一次的代价共计n次移动,numValues次调用该容器中元素类型的拷贝构造函数以及复制操作符的代价就可以了,相比较而言,区间insert减少了n*(numValues - 1)次移动,若numValues 是100,相当于减少了99%的移动

          需要指出的是区间insert函数仅当能够确定两个迭代器之间的距离而不会失去他们的位置时,才可以一次就把元素移动到期最终位置上,这几乎总是可能的,因为所有的前向迭代器都提供了这一功能,但也有例外,实际上,不提供这一功能的编制迭代器仅有输入和输出迭代器,所以除非传入区间形式insert的是输入迭代器(例如istream_iterator,输出迭代器无此问题,因为输出迭代器不能用来标明一个区间)的时候,区间成员函数相比于单元素成员函数才没有移动元素上的效率优势

第三,单元素成员函数方式在内存分配上效率更低,同事还可能产生拷贝问题

          我们知道,当把一个元素插入到vector中,如果vector的内存已满,则vector会分配更大的容量(通常会翻倍,或者以2的倍数扩大容量)的新内存,把vector中的元素从旧内存拷贝到新内存中,销毁旧内存中的元素,并释放旧内存,然后把要插入的元素加入进来,因此插入numValues个新元素最多可导致log2(numValues)次新的内存分配,但使用区间插入的方法,在开始插入前就可以知道需要多少新内存,所以不必要多次重新分配内存(内存分配是很影响性能的,这个优势对性能的提升是很可观的)

          string管理内存的方式和vector相同,所以也适用重新分配内存的论断;deque管理内存的方式和vector不同,所以重新分配内存的论断对于deque不适用

          对于list,由于链表的工作方式,拷贝和内存分配问题不再出现,但是会有对list中某些节点的next和prev指针的重复的,多余的复制操作问题

         

          如上图,每当有元素插入链表中时,含有这一元素的节点必须设定它的next和prev指针,新节点前面的节点(记为B)必须设定自己的next指针,新节点后面的节点(记为A)必须设定自己的prev指针。

          当调用list的单元素insert把一系列节点逐个加入进来时,出了最后一个新节点,其余的所有节点都要把其next指针赋值两次(一次指向A,另一次指向它之后插入的节点);每次有新节点在A前面插入时,A会把其prev指针指向新指针;如果A前面插入了numValues个指针,那么对所插入的节点的next指针会有numValues - 1次多余的赋值,对A的prev指针会有numValues - 1次赋值,共计2 * (numValues - 1)次不必要的指针赋值(虽然指针赋值代价不大,但能避免干嘛不避免)

        区间形式的insert一开始就知道最终插入多少节点,所以可以避免不必要的指针赋值

 

4,支持区间的成员函数

4.1,区间创建:

所有标准容器都提供了如下形式的构造函数:

container::container(InputIterator begin, InputIterator end);

//InputIterator表示任何类型的输入迭代器都可接受

//如果传入的迭代器是istream_iterator或istreambuf_iterator时,可能会遇到C++的分析机制问题

 

4.2,区间插入:

所有标准序列容器都提供了如下形式的insert:

void container::insert(iterator position, InputIterator begin, InputIterator end);

关联容器利用比较函数来决定元素插入到何处,它们提供了一个省去position参数的函数原型:

void container::insert(InputIterator begin, InputIterator end);

 

4.3,区间删除:

所有标准序列容器都提供了如下形式的erase,但序列和关联容器返回值不同:

序列容器:iterator container::erase(iterator begin, iterator end);

关联容器:void container::erase(iterator begin, iterator end);

vector和string中内存的反复分配对erase不适用,因为vector和string的内存会自动增长以容纳新元素,但当元素数目减少时内存却不会自动减少

 

4.4,区间赋值

所有标准容器都提供了如下形式的assign:

void container::assign(InputIterator begin, InputIterator end);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值