顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系的到了元素如何存储、访问、添加以及删除。
向顺序容器中添加元素
除了array之外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。下表列出了向顺序容器中添加元素的操作。
向顺序容器添加元素的操作 |
操作会改变容器的大小;array不支持这些操作。 forward_list有自己专有版本的insert和emplace; forward_list不支持push_back和emplace_back vector和string不支持push_front和emplace_front c.push_back(t) 在c的尾部创建一个值为t或由args创建的元素,返回void c.emplace_back(args) c.push_front(t) 在c的头部创建一个值为t或由args创建的元素,返回void c.emplace_front(args) c.insert(p,t) 在迭代器p指向的元素之前创建一个值为t或由args创建的元素,返回指向新添加的元素的迭代器 c.emplace(p,args) c.insert(p,n,t) 在迭代器p指向的元素之前插入n个值为t的元素。返回指向新添加的第一个元素的迭代器;若n为0,则返回p c.insert(p,b,e) 将迭代器b和e指向的范围内的元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素,返回指向新添加的第一个元素的迭代器;若范围为空,则返回p c.insert(p,il) il是一个花括号包围的元素值列表,将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的第一个元素的迭代器:若列表为空,则返回p 向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效。 |
当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。在一个vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
使用push_back
我们看到push_back将一个元素追加到一个vector的尾部。除array和forward_list之外,每个顺序容器(包括string类型)都支持push_back。
例如,下面的循环每次读取一个string到word中,然后追加到容器尾部:
//从标准输入读取数据,将每个单词放到容器末尾
string word;
while(cin>>word)
container.push_back(word);
对push_back的调用在container尾部创建了一个新的元素,将container的size增大了1。该元素的值为word的一个拷贝,container的类型可以是list、vector或deque。
关键概念:当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。
使用push_front
除了push_back,list、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部:
list<int> ilist;
//将元素添加到Ilist开头
for(size_t ix=0;ix!=4;++ix)
ilist.push_front(ix);
此循环将元素0、1、2、3添加到ilist头部。每个元素都插入到list的新的开始位置。即,当我们插入1时,它会被放置在0之前,2被放置在1之前,依次类推。因此,在循环中以这种方式将元素添加到容器中,最终会形成逆序。
注意:deque像vector一样提供了随机访问元素的能力,但它提供了vector所不支持的push_front。deque保证在容器首部进行插入和删除元素的操作都只花费常数时间。与vector一样,在deque首尾之外的位置插入元素会很耗时。
在容器中的特定位置添加元素
push_back和push_front操作提供了一种方便地在顺序容器尾部或头部插入单个元素的方法。insert成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。vector、deque、list和string都支持insert成员。forward_list提供了特殊版本的insert成员。
每个insert函数都接受一个迭代器作为其一个参数。迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所有insert函数将元素插入到迭代器所指定的位置之前。例如,下面的语句
slist.insert(iter,"Hello!"); //将hello添加到iter之前的位置
虽然某些容器不支持push_front操作,但他们对于insert操作并无类似的限制(插入开始位置)。因此我们可以将元素插入到容器的开始位置,而不必担心容器是否支持push_front:
vector<string> svec;
list<string> slist;
//等价于调用slist.push_front("Hello!");
slist.insert(slist.begin(),"Hello!");
//vector不支持push_front,但我们可以插入到begin()之前
//警告:插入到vector末尾之外的任何位置都可能很慢
svec.insert(svec.begin(),"Hello!");
将元素插入到vector、deque和string中的任何位置都是合法的。然而,这样做可能很耗时。
插入范围内元素
除了第一个迭代器参数之外,insert函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素够按给定值初始化:
svec.insert(svec.end(),10,"Anna");
这行代码将10个元素插入到svec的末尾,并将所有元素都初始化为string“Anna”。
接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定位置之前:
vector<string> v={"quasi","simba","frollo","scar"};
//将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin(),v.end()-2,v.end());
slist.insert(slist.end(),{"these","words","will","go","at","the","end"});
//运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(),slist.begin(),slist.end());
如果我们传递给insert一对迭代器,它们不能指向添加元素的目标容器。
使用insert的返回值
通过使用insert的返回值,可以容器中一个特定的位置反复插入元素:
list<string> lst;
auto iter=lst.begin();
while(cin>>word)
iter=lst.insert(iter,word); //等价于调用push_front
在循环之前,我们将iter初始化为lst.begin()。第一次调用insert会将我们刚刚读入的string插入到iter所指向的元素之前的位置。insert返回的迭代器恰好指向这个新元素。我们将此迭代器赋予iter并重复循环,读取下一个单词。
使用emplace操作
新标准引入了三个成员——emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定的位置之前或容器尾部。
当调用push或insert成员函数时,我们将元素类型对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定c保存Sales_data元素:
//在c的末尾构造一个Sales_data对象
//使用三个参数的Sales_data的构造函数
c.emplace_back("978-0590353403",25,15.99);
//错误:没有接受三个参数的push_back版本
c.push_back("978-0590353403",25,15.99);
//正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sales_data(("978-0590353403",25,15.99));
其中对emplace_back的调用和第二个push_back调用都会创建新的Sales_data对象。在调用emplace_back时,会在容器管理的内存空间中直接创建对象。而调用push_back则会创建一个局部临时对象,并将其压入容器中。
emplace函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配:
//iter指向c中一个元素,其中保存了Sales_data元素
c.emplace_back();//使用Sales_data的默认构造函数
c.emplace(iter,"999-999999999"); //使用Sales_data(string)
//使用Sales_data的接受一个ISBN、一个count和一个price的构造函数
c.emplace_front("978-0590353403",25,15.99);
emplace函数在容器中直接构造元素,传递给emplace函数的参数必须与元素类型的构造函数相匹配。
访问元素
下表列出了我们可以用来在顺序容器值访问元素的操作。如果容器中没有元素,访问操作的结果是未定义的。
包括array在内的每个顺序容器都有一个front成员函数,而除了forward_list之外的所以顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用:
//在解引用一个迭代器或调用front或back之前检查是否有元素
if(!c.empty()){
//val和val2是c中第一个元素值的拷贝
auto val=*c.begin(),val2=c.front();
//val3和val4是c中最后一个元素值的拷贝
auto last=c.end();
auto val3=*(--last); //不能递减forward_list迭代器
auto val4=c.back(); //forward_list不支持
此程序用两种不同方式来获取c中的首元素和尾元素的引用。直接的方式hit调用front和back。而间接的方法是通过解引用begin返回的迭代器来获得首元素的引用,以及通过递减然后解引用end返回的迭代器来获取尾元素的引用。
在顺序容器中访问元素的操作 |
at和下标操作是适用于string、vector和array back不适用于forward_list。 c.back() 返回c中尾元素的引用。若c为空,函数行为未定义 c.front() 返回c中首元素的引用。若c为空,函数行为未定义 c[n] 返回c中下标为n的元素的引用,n是一个无符号整数。若n>c.size(),则函数的行为未定义 c.at[n] 返回下标为n的元素的引用。如果下标越界,则抛出一个out_of_range异常 对一个空容器调用front和back,就像使用一个越界的下标一样 |
访问成员函数返回的是引用
在容器中访问元素的成员函数(即,front、back、下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的值。
if(!c.empty()){
c.front()=42; //将42赋予c中的第一个元素
auto &v=c.back(); //获得指向最后一个元素的引用
v=1024; //改变c中的元素
auto v2=c.back(); //v2不是一个引用,它是c.back()的一个拷贝
v2=0; //未改变c中的元素
}
与往常一样,如果我们使用auto变量来保存这些还是的返回值,并且希望使用此变量来改变元素的值,必须记得将变量定义为引用类型。
下标操作和安全的随机访问
提供快速随机访问的容器(string、vector、deque和array)也都提供下标运算符。就像我们已经看到的那样,下标运算符接受一个下标参数,返回容器中该位置的元素的引用。
我们希望确保下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但如果下标越界,at会抛出一个out_of_range异常:
vector<string> svec; //空vector
cout<<svec[0]; //运行时错误:svec中没有元素
cout<<svec.at[0]; //抛出一个out_of_range异常
删除元素
与添加元素的多种方式类似,(非array)容器也有多种删除元素的方式。如下表所示:
顺序容器的删除操作 |
这些操作会改变容器的大小,所以不适用于array forward_list有特殊版本的erase forward_list不支持pop_back;vector和string不支持pop_front c.pop_back() 删除c中尾元素,若c为空,则函数行为未定义,函数返回void c.pop_front() 删除c中首元素,若c为空,则函数行为未定义,函数返回void c.erase(p) 删除迭代器p所指的元素,返回以指向被删除元素之后的迭代器,若p指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则函数的行为未定义 c.erase(b,e) 删除迭代器b和e所指定范围内的元素,返回一个指向最后一个被删除元素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器 c.clear() 删除c中的所以元素,返回void 删除deque中除首位元素之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。 |
pop_front和pop_back成员函数
pop_front和pop_back成员函数分别删除首元素和尾元素。与vector和string不支持push_front一样,这些类型也不支持pop_front。类似的,forward_list不支持pop_back。与元素访问成员函数类似,不能对一个空容器执行弹出操作。
这些操作返回void,如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它:
while(!ilist.empty()){
process(ilist.front()); //对ilist的首元素进行一些处理
ilist.pop_front(); //完成处理后删除首元素
}
从容器内部删除一个元素
成员函数erase从容器中指定位置删除元素,我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除的(最后一个)元素之后位置的迭代器。即,若j是i之后的元素,那么erase(i)将返回指向j的迭代器。
例如,下面的循环删除一个list中的所有奇数元素:
list<int> lst=(0,1,2,3,4,5,6,7,8,9};
auto it=lst.begin();
while(it!=lst.end())
if(*it%2)
it=lst.erase(it); //删除此元素
else
++it;
每个循环步中,首先检查当前元素是否是奇数,如果是,就删除该元素,并将it设置为我们所删除的元素之后的元素。如果*it为偶数,我们将it递增,从而在下一步循环检查下一个元素。
删除多个元素
接受一对迭代器的erase版本允许我们删除一个范围内的元素:
//删除两个迭代器表示的范围内的元素
//返回指向最后一个被删除元素之后位置的迭代器
elem1=slist.erase(elem1,elem2); //调用后,elem1==elem2
迭代器elem1指向我们要删除的第一个元素,elem2指向我们要删除的最后一个元素之后的位置。
为了删除一个容器中的所有元素,我们既可以调用clear,也可以用begin和end获得的迭代器作为参数调用erase:
slist.clear() ;//删除容器中的所有元素
slist.erase(slist.begin(),slist.end()); //等价调用