除 array 外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。表9.5 列出了向顺序容器(非array)添加元素的操作。
当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。
在一个vector或 string 的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。
而且,向一个vector或 string 添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
使用push_back
除 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。
由于string是一个字符容器,我们也可以用push_back在string末尾添加字符∶
void pluralize(size_t cnt, string &word){
if (cnt > 1)
word.push_back('s'); // 等价于word += 's'
}
容器元素是拷贝
当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。
使用push_front
除了push_back,list、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部:
list<int> ilist;
for (size_t ix = 0; ix != 4; ix++){
ilist.push_front(ix);
}
此循环将元素 0、1、2、3 添加到ilist 头部。每个元素都插入到list的新的开始位置(new beginning)。即,当我们插入1时,它会被放置在0之前,2 被放置在1之前,依此类推。因此,在循环中以这种方式将元素添加到容器中,最终会形成逆序。在循环执行完毕后,ilist 保存序列 3、2、1、0。
注意,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之前的位置将一个值为"Hello"的 string插入到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!");
插入范围内元素
除了第一个迭代器参数之外,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 版本返回指向第一个新加入元素的迭代器。(在旧版本的标准库中,这些操作返回void。)如果范围为空,不插入任何元素,insert操作会将第一个参数返回。
使用insert的返回值
通过使用insert的返回后,可以在容器中一个特定的位置反复地插入元素
list<string> lst;
auto iter = lst.begin();
while (cin >> word){
iter = iter.insert(iter, word); // 等价于push_front
}
在循环之前,我们将iter初始化为lst.begin()。
第一次调用 insert 会将我们刚刚读入的 string插入到iter所指向的元素之前的位置。insert 返回的迭代器恰好指向这个新元素。我们将此迭代器赋予iter 并重复循环,读取下一个单词。只要继续有单词读入,每步 while 循环就会将一个新元素插入到iter 之前,并将iter 改变为新加入元素的位置。此元素为(新的)首元素。因此,每步循环将一个新元素插入到list 首元素之前的位置。
使用emplace操作
新标准引入了三个新成员——emplace_front、emplace 和 emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert 和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。
当调用 push 或 insert 成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。
而当我们调用一个emplace 成员函数时,则是将参数传递给元素类型的构造函数。emplace 成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定c保存 Sales_data元素∶
// 在c的末尾构造一个Sale_data对象
// 使用三个参数的Sales_data构造函数
c.emplace_back("978-0590353403", 25, 15.99);
// 错误
c.push_back("978-0590353403", 25, 15.99);
// 正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sale_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-99999999"); // 使用Sales_data(string)
// 使用Sales_data的接受一个ISBN、一个count和一个price的构造函数
c.emplace_front("978-083846378", 25, 15.99);