9.3 顺序容器操作
9.3.1 向顺序容器添加元素
除 array 外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。如下图所示:
当使用上图所示的操作时,必须记得不同容器使用不同的策略来分配元素空间空间,而这些策略直接影响性能。
使用 push_back
push_back 将一个元素追加到一个容器的尾部。
// 从标准输入中读取数据,将每个单词放到容器尾部
string word;
while(cin >> word) {
container.push_back(word);
}
string 是一个字符容器,所以也可以用 push_back 在 string 末尾添加字符:
void pluralize(size_t cnt, string &word)
{
if (cnt > 1)
word.push_back('s'); // 等价于 word += 's'
}
容器元素是拷贝
当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际放入到容器中的是对象值的一个拷贝,而不是对象本身。容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。
使用 push_front
list、forward_list、deque 容器支持 push_front 操作,将元素插入到容器头部:
list<int> ilist;
// 将元素添加到 ilist 开头
for(size_t ix = 0; ix != 4; ++ix)
ilist.push_front(ix);
在容器中的特定位置添加元素
insert 成员允许在容器中任意位置插入 0 个或多个元素。
每个 insert 函数都接受一个迭代器作为其第一个参数。迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。insert 函数将元素插入到迭代器所指定的位置之前。
slist.insert(iter, "Hello!"); // 将 "hello!"插入到 iter 之前的位置
vector<string> svec;
list<string> slist;
// 等价于 slist.push_front("Hello!")
slist.insert(slist.begin(), "Hello!");
// 合法的插入,但耗时
svec.insert(svec.begin(), "Hello!");
插入范围内元素
insert 可以接受更多的参数:
-
接受一个元素数目和一个值,将指定数量的元素添加到指定位置之前,这些元素按照给定值初始化:
sevc.insert(sevc.end(), 10, "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());
C++11 标准中,接受元素个数或范围的 insert 版本返回指向第一个新加入元素的迭代器。如果范围为空,不插入任何元素,insert 操作会将第一个参数返回。
使用 insert 的返回值
通过使用 insert 的返回值,可以在容器中一个特定位置反复插入元素
list<string> lst; auto iter = lst.begin(); while(cin >> word) iter = lst.insert(iter, word); // 等价于调用 push_front
使用 emplace 操作
emplace 操作构造而不是拷贝元素。当调用一个 emplace 成员函数时,是将参数传递给元素类型的构造函数。emplace 成员使用这些参数在容器管理的内存空间中直接构造元素。
例如,假定 c 保存 Sales_data 元素
// 在 c 的末尾添加一个 Sales_data 对象 // 使用三个参数的 Sales_data 构造函数 c.emplace_back("978-0590353403", 24, 15.99); // 错误:没有接受三个参数的 push_back 版本 c.push_back("978-0590353403", 24, 15.99); // 正确:创建一个临时的 Sales_data 对象传递给 push_back c.push_back(Sales_data("978-0590353403", 24, 15.99));
emplace 函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配:
// iter 指向 c 中的一个元素,其中保存了 Sales_data 元素 c.emplace_back(); // 使用 Sales_data 的默认构造函数 c.emplace(iter, "999-99999999"); // 使用了 Sales_data(string) c.emplace_front("978-0590353403", 24, 15.99); // 使用了接受三个参数的 Saled_data 构造函数
9.3.2 访问元素
上图列出了可以用来在顺序容器中访问元素的操作:
// 在解引用一个迭代器或调用 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);
auto val4 = *c.back();
}
访问成员函数返回的是引用
容器中访问元素的成员函数(即,front、back、下标和 at)返回的都是引用。
if (!c.empty())
{
c.front() = 42; // 将 c 的首元素赋值为 42
auto &v = c.back(); // 获取 c 的尾元素
v = 1024; // 将尾元素修改为 1024
auto v2 = c.back(); // v2 不是一个引用,它是 c.back() 的一个拷贝
v2 = 0; // 未改变 c 中的元素
}
如果我们使用 auto 变量来保存这些函数的返回值,并且希望使用此变量来改变元素的值,必须将变量定义为引用类型。
下标操作和安全的随机访问
下标运算符接受一个下标参数,返回容器中位置的元素的引用。给定下标必须在范围内(即,大于等于 0 且小于容器的大小)。编译器并不检测下标越界。
9.3.3 删除元素
上图列出了删除容器元素的方式。注意,删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它们是存在的。
pop_front 和 pop_back 成员函数
不能对一个空容器执行弹出操作,这些操作返回 void,如果需要弹出的元素的值,就必须在执行弹出操作之前保存它:
while(!ilist.empty())
{
process(ilist.front()); // 对 ilist 的首元素进行一些处理
ilist.pop_front(); // 完成处理后删除首元素
}
从容器内部删除一个元素
成员函数 erase 从容器中指定位置删除元素。可以删除一个由迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的 erase 都返回指向删除的(最后一个)元素之后位置的迭代器。
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;
删除多个元素
接受一对迭代器的 erase 版本允许我们删除一个范围内的元素:
// 删除两个迭代器表示的范围内的元素
// 返回指向最后一个被删元素之后的位置的迭代器
elem1 = slist.erase(elem1, elem2); // 调用后,elem1 == elem2
其中 elem1 指向我们要删除的第一个元素,elem2 指向要删除的最后一个元素之后的位置。
为了删除一个容器中的所有元素,有两种方式:
slist.clear();
slist.erase(slist.begin(), slist.end());
9.3.4 特殊的 forward_list 操作
单向链表插入或删除元素的操作与其他容器不同,如上图所示。
当在 forward_list 中添加或删除元素时,必须关注两个迭代器——一个指向要处理的元素,另一个指向其前驱。
forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_begin(); // 获取 flst 的首前元素
auto curr = flst.begin(); // 获取 flst 中的第一个元素
while (curr != flst.end()) // 仍有元素要处理
{
if (*curr % 2) // 若元素为奇数
curr = flst.erase_after(prev); // 删除它并移动 curr
else
{
prev = curr; // prev 指向 curr 之前的元素
++curr; // 移动迭代器 curr,指向下一个元素
}
}
9.3.5 改变容器大小
如图所示,可以用 resize 来增大或缩小容器,array 不支持 resize操作。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:
list<int> ilist(10, 42); // 10 个元素,初始值 42
ilist.resize(15); // 将 5 个值为 0 的元素添加到容器中
ilist.resize(25, -1); // 将 10 个值为 -1 的元素附加到容器末尾
ilist.resize(5); // 从 ilist 末尾删除 20 个元素
9.3.6 容器操作可能使迭代器失效
一个失效的指针、引用或迭代器将不再表示任何元素。
当向容器添加元素后:
- 如果容器是 vector 或 string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效;
- 对于 deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针很不会失效;
- 对于 list 和 forward_list ,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
当删除一个元素后:
- 对于 list 和 forward_list ,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效;
- 对于 deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除 deque 的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响;
- 对于 vector 和 string,指向被删除元素之前元素的迭代器、引用和指针仍然有效。注意,当我们删除元素时,尾后迭代器总是会失效。
管理迭代器
当使用迭代器(或指向容器元素的引用或指针)是,最小化要求迭代器必须保持有效的程序片段是一个好的方法。由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确的重新定位迭代器。
编写改变容器的循环程序
添加/删除 vector、string 和 deque 元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用和指针
// 傻瓜循环,删除偶数元素,复制每个奇数元素
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
auto iter = vi.begin(); // 调用 begin 而不是 cbegin,因为我们要改变 vi
while (iter != vi.end())
{
if (*iter % 2) // 如果是奇数
{
iter = vi.insert(iter, *iter); // 复制当前元素
iter += 2; // 向前移动迭代器,跳过当前元素以及插入到它之前的元素
}
else
{
iter = vi.erase(iter); // 删除偶数元素
// 不应向前移动迭代器,iter 指向我们删除的元素之后的元素
}
}
不要保存 end 返回的迭代器
添加/删除 vector 或 string 的元素后,或在 deque 中首元素之外任何位置添加/删除元素后,原来 end 返回的迭代器总是会失效。因此,添加/删除元素的循环程序必须反复调用 end,而不能在循环之前保存 end 返回的迭代器,一直当做容器末尾使用。
// 灾难:此循环的形式为未定义的
auto begin = v.begin();
auto end = v.end();
while (begin != end) // 做一些处理
{
// 插入新值,对 begin 重新赋值,否则的话它就是失效
++begin; // 向前移动 begin,因为我们想在此元素之后插入元素
begin = v.insert(begin, 42); // 插入新值
++begin; // 向前移动 begin 跳过我们刚刚加入的元素
}
改进的方法如下:
// 更安全的做法:在每个循环步都重新计算 end
auto begin = v.begin();
while (begin != v.end)
{ // 做一些处理
// 插入新值,对 begin 重新赋值,否则的话它就是失效
++begin; // 向前移动 begin,因为我们想在此元素之后插入元素
begin = v.insert(begin, 42); // 插入新值
++begin; // 向前移动 begin 跳过我们刚刚加入的元素
}