STL容器元素、错误处理、异常处理及扩展全解析
立即解锁
发布时间: 2025-08-22 00:43:48 阅读量: 1 订阅数: 16 


深入解析C++标准库:从入门到精通
### STL容器元素、错误处理、异常处理及扩展全解析
#### 1. 容器元素要求
STL容器、迭代器和算法都是模板,能处理预定义或用户定义类型,但元素需满足一定要求。
##### 1.1 基本要求
- **可复制或移动**:元素类型需隐式或显式提供复制或移动构造函数,生成的副本应与源等价,即任何相等性测试都返回相等,且源和副本行为相同。
- **可赋值**:元素必须能通过赋值运算符进行(移动)赋值,容器和算法会用赋值运算符用新元素覆盖旧元素。
- **可销毁**:元素必须能通过析构函数销毁,当元素从容器中移除时,容器会销毁其内部副本,因此析构函数不能是私有的,且像在C++中常见的那样,析构函数不能抛出异常。
这些操作通常会为任何类隐式生成,只要没有定义这些操作的特殊版本,且没有特殊成员破坏这些操作的合理性,类就会自动满足这些要求。
##### 1.2 其他可能要求
- **默认构造函数**:对于序列容器的某些成员函数,默认构造函数必须可用。例如,创建非空容器或增加元素数量而不指定新元素的值时,会调用元素类型的默认构造函数创建这些元素。
- **相等性测试**:对于一些操作,必须定义 `==` 运算符进行相等性测试,尤其是在搜索元素时。对于无序容器,如果元素不支持 `==` 运算符,可以提供自己的等价定义。
- **排序准则**:对于关联容器,元素必须提供排序准则的操作,默认是 `<` 运算符,由 `less<>` 函数对象调用。
- **哈希函数和等价准则**:对于无序容器,必须为元素提供哈希函数和等价准则。
#### 2. 值语义与引用语义
通常,所有容器都会创建其元素的内部副本,并返回这些元素的副本,这意味着容器元素与放入容器的对象相等但不相同。修改容器中的元素实际上是修改副本,而不是原始对象,这就是STL容器提供的值语义。但在实践中,可能也需要引用语义,即容器包含对其元素对象的引用。
##### 2.1 值语义的优缺点
- **优点**:
- 复制元素简单。
- 引用容易出错,需要确保引用不指向已不存在的对象,还需处理可能出现的循环引用。
- **缺点**:
- 复制元素可能导致性能不佳,甚至可能无法复制。
- 无法同时在多个容器中管理同一个对象。
##### 2.2 实现引用语义的方法
- **普通指针**:使用指针作为元素是实现引用语义的明显方法,但普通指针存在问题,如它们所指向的对象可能已不存在,比较时比较的是指针而不是对象,因此使用普通指针作为容器元素时要非常小心。
- **智能指针**:更好的方法是使用智能指针,如C++标准库自TR1起提供的 `shared_ptr` 类,它可以共享同一个对象。此外,还可以使用 `std::reference_wrapper<>` 类让STL容器持有引用。
#### 3. STL中的错误处理
STL的设计目标是追求最佳性能而非最高安全性,因此几乎不进行错误检查。在STL被纳入C++标准库之前,曾讨论是否引入更多错误检查,但多数人因以下原因决定不引入:
- **性能问题**:错误检查会降低性能,而速度仍是程序的普遍目标,良好的性能是STL的设计目标之一。
- **灵活性**:如果更注重安全性而非速度,可以通过添加包装器或使用STL的特殊版本来实现。但如果所有基本操作都内置了错误检查,就无法为了获得更好的性能而避免错误检查。
使用STL时,若违反前置条件会导致未定义行为。例如,索引、迭代器或范围无效时,结果是未定义的。不使用安全版本的STL时,通常会导致未定义的内存访问,从而产生不良副作用甚至崩溃。
使用STL时需满足以下要求:
- **迭代器有效**:迭代器必须在使用前初始化,且可能会因其他操作而变得无效。例如,对于向量和双端队列,插入、删除元素或重新分配内存时迭代器会无效;对于无序容器,重新哈希(可能是插入操作的结果)时迭代器会无效。
- **迭代器不指向超尾位置**:指向超尾位置的迭代器没有可引用的元素,因此不允许调用 `*` 或 `->` 运算符,这对于 `end()`、`cend()` 和 `rend()` 容器成员函数的返回值尤其适用。
- **范围有效**:指定范围的两个迭代器必须指向同一个容器,且第二个迭代器必须可以从第一个迭代器到达。
- **多源范围元素数量**:如果使用多个源范围,第二个及后续范围通常必须至少有与第一个范围一样多的元素。
- **目标范围足够**:目标范围必须有足够的元素可以被覆盖,否则必须使用插入迭代器。
以下是一个展示可能错误的示例代码:
```cpp
// stl/iterbug.cpp
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> coll1; // empty collection
vector<int> coll2; // empty collection
// RUNTIME ERROR:
// - beginning is behind the end of the range
vector<int>::iterator pos = coll1.begin();
reverse (++pos, coll1.end());
// insert elements from 1 to 9 into coll1
for (int i=1; i<=9; ++i) {
coll1.push_back (i);
}
// RUNTIME ERROR:
// - overwriting nonexisting elements
copy (coll1.cbegin(), coll1.cend(),
// source
coll2.begin());
// destination
// RUNTIME ERROR:
// - collections mistaken
// - cbegin() and cend() refer to different collections
copy (coll1.cbegin(), coll2.cend(),
// source
coll1.end());
// destination
}
```
由于这些错误在运行时发生,而不是在编译时,它们会导致未定义行为。使用STL时容易犯错,且STL并不强制保护你不犯错,因此至少在软件开发期间使用“安全”的STL是个好主意。例如,Cay Horstmann引入了第一个安全STL版本,“STLport” 几乎可在任何平台上免费使用,此外,库供应商现在也提供标志来启用 “更安全” 模式,尤其是在开发期间应启用。
#### 4. STL中的异常处理
STL几乎从不检查逻辑错误,因此由于逻辑问题,STL本身几乎不会生成异常。实际上,标准仅要求两个函数调用可能直接导致异常:`at()` 成员函数(下标运算符的检查版本)和 `reserve()`(如果传递的元素大小超过 `max_size()`)。除此之外,标准仅要求可能发生常见的标准异常,如内存不足时的 `bad_alloc` 或用户定义操作的异常。
在C++98标准化过程的很长一段时间里,对于异常发生时STL组件的行为没有明确定义,每个异常都会导致未定义行为,甚至销毁STL容器时如果在其某个操作期间抛出异常也会导致未定义行为,因此在需要保证和定义行为时,STL是无用的,因为甚至无法展开栈。
在C++98标准化过程中,处理异常是最后讨论的话题之一,找到一个好的解决方案并不容易,原因如下:
- **安全程度难以确定**:很难确定C++标准库应提供的安全程度。例如,在向量的任何位置插入新元素应该要么成功要么没有效果,但通常在复制后续元素以腾出空间时可能会发生异常,从这种异常中完全恢复是不可能的。为了实现这个目标,插入操作需要将向量的每个元素复制到新的存储中,这会严重影响性能。如果像STL一样将良好的性能作为设计目标,就无法在所有情况下提供完美的异常处理,必须找到一个满足两者需求的折衷方案。
- **性能担忧**:虽然有保证、定义明确的异常行为且没有显著的性能损失比异常可能导致系统崩溃要好,但有人担心处理异常的代码会对性能产生不利影响,这与实现最佳性能的
0
0
复制全文
相关推荐










