使用vector和string时需注意的问题

栏目: 编程工具 · 发布时间: 6年前

内容简介:使用vector和string时需注意的问题

随着对STL的频繁使用和着迷,越发意识到更深层次理解STL的重要性。使用STL正确得到结果是一回事,而高效地使用STL得到结果又是另一回事。因此,拿起了Scott Meyers大神的《Effective STL》,结合自己平时遇到的问题,作一些总结和记录。

使用reserve避免不必要的重新分配

我们知道,STL容器最大的进步之一是它们会自动增长以便容纳下所放入的数据,只要没有超出它们的最大限制(可以使用max_size()成员函数查看)就可以。vector和string的增长过程是:

(1)分配一块大小为当前容量的某个倍数的新内存,在大多数实现中,vector和string的容量每次以2的倍数增长,即每当容器需要扩张时,它们的容量即加倍。

(2)把容器的所有元素从旧的内存复制到新的内存中。

(3)析构掉旧内存中的对象。

(4)释放旧内存。

可以看出,这个过程会非常耗时。因此,可以使用reserve成员函数来讲重新分配的次数减少到最低限度,从而避免了重新分配和迭代器/引用失效带来的开销。

因此,当一个元素需要被插入而容器的容量不够时,就会发生重新分配过程。比如:创建一个1到1000之间的vector ,如果采用这样的方式:

vector<int> ivec;
for(int i=1;i<=1000;i++)
    ivec.push_back(i);

对于大多数STL实现,该循环在进行过程中将导致2到10次重新分配。而如果改用reserve。

vector<int> v;
v.reserve(1000);
for(int i=1;i<=1000;++i)
    v.push_back(i);

那么,在循环过程中,将不会再发生重新分配。

所以,当能确切知道或大致预计容器中最终会有多少元素时,使用reserve。如果不知道,可以先预留足够大的空间,然后,当所有数据都加入以后,再去除多余的容量。

把vector和string数据传递给旧的C API

在使用C/C++时,我们发现很多时候还有旧的C API的身影,它们使用数组和char*指针而不是vector或string对象来进行数据交换。因此,在使用STL时,也必须处理好与旧的C API之间的关系。

比如,有一个vector v,而需要得到一个指向v中数据的指针,从而可把v中的数据作为数组来对待,那么只需要使用&v[0]就可以了。而对于string s,对应的形式是s.c_str()。

所以,对于给定的C API:

void doSomething(const int* pInts, size_t numInts);

可以这样使用:

if(!v.empty()){
    doSomething(&v[0], v.size());
}

请注意,不要使用v.begin()来代替&v[0],因为begin的返回值是一个迭代器,不是指针;当需要一个指向vector中的数据的指针时,永远不应该使用begin()。(可以使用&*v.begin())

但是,上述方法对string却是不可靠的。原因如下:

(1)string中的数据不一定存储在连续的内存中;

(2)string的内部表示不一定是以空字符结尾的。

所以,对于给定的C API:

void doSomething(const char* pString)

可以这样使用:

doSomething(s.c_str())

避免使用vector

作为一个STL容器,vector

有两点问题。首先,它不是一个STL容器。其次,它并不存储bool。除此之外,一切正常。

vector 不是一个STL容器,原因是一个对象要成为STL容器,必须满足的一个条件是,支持operator[]。也就是当使用operator[]取得了容器Container

中的一个T对象,那么可以通过取它的地址得到一个指向该对象的指针。

因此,如果vector 是一个容器,那么下面这段代码必须可以被编译:

vector<bool> v;
bool *p = &v[0];
但是,它不能通过编译。原因是,vector 是一个假的容器,它并不真的储存bool,相反,为了节省空间,它储存的是bool的紧凑表示。在一个典型的实现中,储存在”vector”中的每个”bool”仅占一个二进制位,一个8位的字节可容纳8个”bool”。在内部,vector

使用了与位域一样的思想,来表示它所存储的那些bool;实际上它只是假装存储了这些bool。

位域与bool相似,它只能表示两个可能的值,但是在bool与看似bool的位域之间有一个重要的区别:可以创建一个指向bool的指针,而指向单个位的指针则是不允许的。

所以,在上述的实验中,vector

::operator[]需要返回一个指向单个位的引用,而这样的引用却不存在。

那么。当需要vector

的时候,应该使用什么呢?

标准库提供了两种选择,可以满足绝大多数的需求。第一种是deque 。deque几乎提供了vector所提供的一切,而deque 是一个STL容器,并且确实存储bool。只是需要注意的是,deque中的元素的内存不是连续的,所以不能把deque

中的数据传递给一个期望bool数组的C API。

第二种方案时bitset。bitset不是STL容器,但它是标准C++库的一部分。与STL容器不同的是,它的大小(即元素的个数)在编译时就确定了,所以它不支持插入和删除元素。而且,它不支持迭代器。但是,与vector 一样,它使用了一种紧凑表示,只为包含所包含的每个值提高一位空间,它提供了vector 所特有的flip成员函数,以及其他一些特有的、对位的集合有意义的成员函数。在不需要迭代器和动态改变大小的环境下,bitset很适合需要。

string实现的多样性

几乎每个string实现都包含如下信息:

(1)字符串的大小,即所包含的字符的个数。

(2)用于存储该字符串中字符的内存的容量。

(3)字符串的值。

除此之外,一个string可能还包含:

(1)它的分配子的一份拷贝。

(2)对值的引用计数。

不同的string实现以不同的方式来组织这些信息。下面以4种不同的string实现方式来说明。它们来源于四种STL实现。

实现A

每个string对象包含其分配子的一份拷贝、该字符串的大小、它的容量以及一个指针,该指针指向一块动态分配的内存,其中包含了引用计数和字符串的值。

使用vector和string时需注意的问题

在该实现中,使用默认分配子的string对象是一个指针的4倍。若使用了自定义的分配子,则string对象会更大一些,多出的部分取决于分配子对象的大小。

实现B

在实现B中,string对象与指针大小相同,因为它只包含一个指向结构的指针。B的string所指向的对象中包含了该字符串的大小、容量和引用计数,以及一个指向动态分配的内存的指针,该内存中存放了字符串的值。

使用vector和string时需注意的问题

实现C

在实现C中,string对象的大小总是与指针的相同,该指针指向一块动态分配的内存,其中包含了与该字符串相关的一切数据:它的大小、容量、引用计数和值。

使用vector和string时需注意的问题

实现D

实现D的string对象是指针大小的7倍(仍然假定使用的是默认的分配子)。这一实现不使用引用计数,但是每个string对象内部包含一块内存,最大可容纳15个字符的字符串。因此,小的字符串可以完整地存放在string对象中,这通常被称为“小字符串优化”特性。而当一个string的容量超过15时,该内存的起始部分被当做一个指向一块动态分配的内存的指针,而该string的值就在这块内存中。

使用vector和string时需注意的问题

比较

(1)在以引用计数为基础的设计方案中,string对象之外的一切可以被多个string对象(如果它们有同样的值)。所以,实现A比实现B或C提供了较小的共享能力。尤其是,实现B和C可以共享string的大小和容量,从而减少每个对象存储这些数据的平均开销。

(2)创建一个新的字符串值可能需要零次、一次或两次动态分配。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

逆向工程权威指南

逆向工程权威指南

Dennis Yurichev(丹尼斯) / 安天安全研究与应急处理中心 / 人民邮电出版社 / 2017-3-1 / 168

逆向工程是一种分析目标系统的过程,旨在于识别系统的各组件以及组件间关系,以便于通过其它形式、或在较高的抽象层次上,重建系统的表征。 本书专注于软件的逆向工程,是写给初学者的一本经典指南。全书共分为12个部分,共102章,涉及X86/X64、ARM/ARM-64、MIPS、Java/JVM等重要话题,详细解析了Oracle RDBMS、Itanium、软件狗、LD_PRELOAD、栈溢出、EL......一起来看看 《逆向工程权威指南》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具