使用 stack 的时候,有这样的困惑,为什么取栈顶元素的接口是这样的:
#include <stack> #include <iostream> int _tmain(int argc, _TCHAR* argv[]) { std::stack<int> si; for (int i = 0; i < 10; i++) { si.push(i); } while(!si.empty()) { int i = si.top(); //取栈顶元素 si.pop(); //删掉栈顶元素 std::cout << i << std::endl; } return 0; }
为什么不能 int i = si.pop(); 这样多方便啊!答案是 stack 作为一个通用的标准库成员,在面对异常时必须做到两点。
1. 异常安全。异常不会导致它本身处于一种错误状态或者导致数据丢失或是资源泄漏。
假设栈内部用数组实现,不考虑栈是否为空,T i = st.pop(); 的pop() 的实现如下:
template <typename T> T stack<T>::pop() { //假设数据存储于数组data中,top代表栈顶位置 return data[top--]; }
如果 T 的拷贝构造函数抛出异常(临时对象以data[top] 构造),返回对象失败,而在 stack 内部,top已经改变了位置,栈顶元素就丢失了!
所以调整栈顶位置必须在所有数据拷贝完成之后,不想像这样按值返回。
2. 异常透明。客户代码(它存放的类型T的实现)抛出的异常,不应该被吃掉,应该透明的传递给客户。
如果用数组来实现 stack 的赋值运算符,在拷贝数据的时候会写类似这样的循环:
template <typename T> stack<T>& stack<T>::operator=(const stack<T>* rhs) { for (int i = 0; i< rhs.top; i++) data[i] = rhs.data[i]; }
T 的赋值运算符可能在循环运行到一半的时候抛出异常,这时一部分数据已经成功赋值,另一部分还没有。要保证数据一致性,只能用catch(...) 捕获异常,忽略这个异常,继续赋值。(其实也可以新的赋值回去老的嘛?)这样的话,相当于赋值运算法抛出的异常被吃掉了。一般来说,异常代表某些不该发生的事情发生了,应该让客户知道才对。
所以 stack 不用数组实现(而且数组大小固定,不适合stack)。如果stack 内部的数据用指针存放,使用临时缓冲区来接收数据,在这个过程中处理异常,然后让pdata指向分配的内存。这种方式叫做:copy & swap。
// pdata 代表指向存放数据内存的指针,top 代表栈顶元素的偏移量 template <typename T> stack<T>& stack<T>::operator=(const stack<T>& rhs) { T* ptemp = new T[rhs.top]; try { for(int i=0; i<rhs.top; i++) *(ptemp+i) = *(rhs.pdata+i); } catch(...) // 捕捉可能出现的异常 { delete[] ptemp; throw; // 重新抛出 } delete[] pdata; // 释放当前的内存 pdata = ptemp; // 让 pdata 指向赋值成功的内存块 }
使用异常而不是在返回值报告错误的价值:
1. 使用异常报告错误可以避免污染函数界面
2. 如果你希望报告比较丰富的错误信息,使用一个异常对象比简单的返回值要有效的多,而且避免了返回复杂对象造成的开销
3. 有些错误不合适用返回值来报告。比如动态分配内存函数。
比如 C 中的 malloc 函数返回 NULL 来代表动态内存分配错误,但是程序员常常会忘记在 malloc 之后检查返回值。C++ 的new 在分配内存失败时会抛出std::bad_alloc,所以可以安全的使用 int* p = new int; 返回的指针p。
4. 提供一个通用的手段让构造函数可以方便的报告错误,因为构造函数没有返回值。
P.S. 永远不要让析构函数抛出异常!
evil p = new evil[10]; delete[] p;
会调用10次evil类的析构函数来释放资源,假设中间某一次析构函数抛出异常,和上面“用数组来实现 stack 的赋值运算符”的情况类似,delete[] 要么不捕获这个异常,遵从异常透明,但是导致资源没有被释放完,资源泄露;要么捕获这个异常继续后面的释放,但是违反了“异常透明”原则。
【参考】
Solmyr 的小品文系列之七:异常