异常
函数出了错,怎么告诉调用它的人?老办法是返回一个特殊的值,比如「算不出来就返回 0」。可要是 0 本身也是一个正常的答案呢?
1#include <iostream>2#include <vector>3 4double average(const std::vector<double>& temps) {5 if (temps.empty()) {6 return 0;7 }8 double total = 0;9 for (double t : temps) {10 total += t;11 }12 return total / temps.size();13}14 15int main() {16 std::vector<double> winter{-3, 1, 2};17 std::vector<double> nothing;18 std::cout << "冬天平均:" << average(winter) << '\n';19 std::cout << "没有数据:" << average(nothing) << '\n';20 return 0;21}
冬天平均:0 没有数据:0
两个 0,一个是真的平均气温,一个是「根本没有数据」,调用的人分不出来。而且就算分得出来,也得记着每次调用都检查。C++ 的另一个办法是异常(exception):出了错,函数不返回,而是「抛出」一个异常,交给知道怎么处理的人。
throw 和 try / catch
1#include <iostream>2#include <stdexcept>3#include <vector>4 5double average(const std::vector<double>& temps) {6 if (temps.empty()) {7 throw std::runtime_error("没有数据");8 }9 double total = 0;10 for (double t : temps) {11 total += t;12 }13 return total / temps.size();14}15 16int main() {17 std::vector<double> nothing;18 try {19 double avg = average(nothing);20 std::cout << "平均:" << avg << '\n';21 } catch (const std::runtime_error& e) {22 std::cout << "出错了:" << e.what() << '\n';23 }24 std::cout << "程序继续\n";25 return 0;26}
出错了:没有数据 程序继续
三样新东西:
throw std::runtime_error("没有数据");建一个异常对象,把它抛出。average到这里就不往下走了,也不返回值。std::runtime_error在<stdexcept>里,括号里是说明出了什么错的文字。try { … }围住可能出错的代码。catch (const std::runtime_error& e) { … }跟在try后面,接住这种异常。e.what()取回当初写的那段文字。
抛出以后,try 里剩下的语句全跳过,直接去 catch;catch 做完,程序从整个 try / catch 后面接着走。所以「平均:」那一行没有显示,「程序继续」显示了。
没接住会怎样
要是没有人接,异常一路往外传,传出了 main,程序就结束了:
1#include <iostream>2#include <stdexcept>3#include <vector>4 5double average(const std::vector<double>& temps) {6 if (temps.empty()) {7 throw std::runtime_error("没有数据");8 }9 double total = 0;10 for (double t : temps) {11 total += t;12 }13 return total / temps.size();14}15 16int main() {17 std::vector<double> nothing;18 double avg = average(nothing);19 std::cout << "平均:" << avg << '\n';20 std::cout << "结束\n";21 return 0;22}
terminate called after throwing an instance of 'std::runtime_error'
what(): 没有数据
Aborted (core dumped)发生了什么: 抛出了一个 std::runtime_error,没有 catch 接住它,程序就结束了(std::terminate)。
这三行和你在自己电脑上用 g++ 编出来的程序一样:terminate called after throwing an instance of 'std::runtime_error' 说抛出了一个 std::runtime_error 没人接,于是调用了 std::terminate;what(): 后面是那段文字;Aborted (core dumped) 是程序被强行结束了。「结束」没有显示。
异常不能当作没看见:要么接住,要么程序结束。这比返回一个谁都可能忘了检查的 0 好。
标准库自己会抛
第 11 课说过:v.at(5) 越界会抛出 std::out_of_range,没人处理就结束程序。第 5 课的 std::stoi 碰上不是数字的文字,也会抛出 std::invalid_argument。现在你能接住它们了:
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int main() {7 std::vector<int> prices{12, 30, 8};8 try {9 std::cout << prices.at(5) << '\n';10 } catch (const std::exception& e) {11 std::cout << "下标不对:" << e.what() << '\n';12 }13 try {14 int n = std::stoi("十二");15 std::cout << n << '\n';16 } catch (const std::exception& e) {17 std::cout << "不是数字:" << e.what() << '\n';18 }19 std::cout << "两个错都接住了\n";20 return 0;21}
下标不对:vector::_M_range_check: __n (which is 5) >= this->size() (which is 3) 不是数字:stoi 两个错都接住了
这里接的是 const std::exception&。std::exception 是标准库所有异常的基类(第 23 课的继承):std::runtime_error、std::out_of_range、std::invalid_argument 都是它的派生类,所以一个 catch (const std::exception& e) 全都接得住,e.what() 调到的是各自的版本(第 24 课的虚函数)。stoi 的 what() 很短,只有一个 stoi。
按引用接(&):不复制,也不会把派生类对象切成基类对象;加 const,因为只是看看。这是接异常的老规矩。
几个 catch
一个 try 后面可以跟好几个 catch,按不同的错做不同的事。抛出时从上往下比,第一个对得上的接住,后面的不看:
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int seat_number(const std::string& text) {7 int n = std::stoi(text);8 if (n < 1 || n > 30) {9 throw std::runtime_error("没有这个座位");10 }11 return n;12}13 14int main() {15 std::vector<std::string> tickets{"12", "abc", "45"};16 for (const auto& t : tickets) {17 try {18 int seat = seat_number(t);19 std::cout << "座位 " << seat << '\n';20 } catch (const std::invalid_argument& e) {21 std::cout << t << " 不是数字\n";22 } catch (const std::runtime_error& e) {23 std::cout << t << ":" << e.what() << '\n';24 }25 }26 return 0;27}
座位 12 abc 不是数字 45:没有这个座位
"abc" 让 stoi 抛出 invalid_argument,第一个 catch 接住;"45" 让 seat_number 自己抛出 runtime_error(「没有这个座位」是这个程序自己的规矩,用 runtime_error),第一个对不上,第二个接住。try 在循环体里,所以一张票出错,下一张照样处理。
正因为第一个对上的就接住,具体的 catch 要写在前面,std::exception 这种什么都接的写在最后。还有一个 catch (...),三个点,什么类型都接,但拿不到 what(),只能放在最后一个。
这段程序显示什么?
1#include <iostream>2#include <stdexcept>3 4void check(int n) {5 std::cout << "a";6 if (n > 2) {7 throw std::runtime_error("太大");8 }9 std::cout << "b";10}11 12int main() {13 try {14 check(1);15 check(5);16 check(2);17 std::cout << "c";18 } catch (const std::exception& e) {19 std::cout << "d";20 }21 std::cout << "e\n";22 return 0;23}
check(1) 显示 ab;check(5) 显示 a 就抛出了,b 不显示;try 里剩下的 check(2) 和 c 全跳过,去 catch 显示 d;最后接着走,显示 e。
路上的对象会析构
异常从 throw 跳到 catch,中间可能隔着好几层函数。这几层里的局部对象怎么办?C++ 保证:一层一层退出时,每层的局部对象都会析构,和函数正常结束时一样。这叫栈展开。
1#include <iostream>2#include <stdexcept>3#include <string>4 5class Cup {6public:7 Cup(const std::string& name) : name_(name) {8 std::cout << "拿出" << name_ << '\n';9 }10 ~Cup() {11 std::cout << "收起" << name_ << '\n';12 }13 14private:15 std::string name_;16};17 18void make_tea() {19 Cup cup{"茶杯"};20 throw std::runtime_error("水不够");21 std::cout << "倒茶\n";22}23 24int main() {25 try {26 Cup tray{"托盘"};27 make_tea();28 } catch (const std::exception& e) {29 std::cout << "接住:" << e.what() << '\n';30 }31 return 0;32}
拿出托盘 拿出茶杯 收起茶杯 收起托盘 接住:水不够
点单步,走到第 20 行的 throw 以后一步一步看:下一步不是 catch,而是 make_tea 里 cup 的析构函数;再下一步,main 的 try 里 tray 的析构函数;然后才到 catch。先收起茶杯、再收起托盘,和它们建出来的顺序相反。
所以第 21 课的智能指针、第 11 课的 vector 碰上异常也不会泄漏:它们都是局部对象,析构函数照样运行。
自己的异常类
标准的异常类够用,但有时想让异常多带点东西,或者让 catch 能分清「这是银行的错」。自己写一个类,继承 std::runtime_error 就行:
1#include <iostream>2#include <stdexcept>3#include <string>4 5class BankError : public std::runtime_error {6public:7 BankError(const std::string& message, int short_by)8 : std::runtime_error(message), short_by_(short_by) {}9 int short_by() const {10 return short_by_;11 }12 13private:14 int short_by_;15};16 17class Account {18public:19 Account(int balance) : balance_(balance) {}20 void withdraw(int amount) {21 if (amount > balance_) {22 throw BankError("余额不足", amount - balance_);23 }24 balance_ -= amount;25 std::cout << "取出 " << amount << ",剩 " << balance_ << '\n';26 }27 28private:29 int balance_;30};31 32int main() {33 Account account{100};34 try {35 account.withdraw(30);36 account.withdraw(100);37 } catch (const BankError& e) {38 std::cout << e.what() << ",还差 " << e.short_by() << '\n';39 } catch (const std::exception& e) {40 std::cout << "别的错:" << e.what() << '\n';41 }42 return 0;43}
取出 30,剩 70 余额不足,还差 30
BankError 的构造函数在初始化列表里调用基类的构造函数 std::runtime_error(message)(第 23 课),what() 就有了;自己再加一个成员 short_by_,记着差多少。因为它是 std::runtime_error 的派生类,别处只写了 catch (const std::exception& e) 的代码也接得住它。
注意: throw 后面其实可以是任何值,throw 42;、throw std::string("错了"); 都行。可是那样就没有 what(),catch (const std::exception&) 也接不住。习惯上只抛标准异常或它们的派生类。
什么时候用异常
异常适合「这里处理不了、也不该发生」的错:函数做不成它答应的事,又不知道该怎么办,就交给上面。构造函数是最典型的地方,它没有返回值,参数不对时只能抛出,这样就不会有一个半坏的对象:
1#include <iostream>2#include <stdexcept>3 4class Discount {5public:6 Discount(int percent) : percent_(percent) {7 if (percent < 0 || percent > 100) {8 throw std::invalid_argument("折扣要在 0 到 100 之间");9 }10 }11 double apply(double price) const {12 return price * (100 - percent_) / 100;13 }14 15private:16 int percent_;17};18 19int main() {20 try {21 Discount sale{20};22 std::cout << sale.apply(50) << '\n';23 Discount wrong{120};24 std::cout << wrong.apply(50) << '\n';25 } catch (const std::invalid_argument& e) {26 std::cout << e.what() << '\n';27 }28 return 0;29}
40 折扣要在 0 到 100 之间
wrong 根本没建成,后面也就没机会用到一个错的折扣。
| 情况 | 用什么 |
|---|---|
| 很常见、调用的人马上就要处理(找不到、输入不对) | 返回值,比如 find 返回 end() |
| 少见、这里处理不了(参数不对、资源没有了) | throw 异常 |
| 程序自己写错了(下标算错) | 修代码,别靠 catch 掩盖 |
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。检查只比较输出,所以这几道题要在 catch 里显示一行,证明真的接住了。
接住越界的下标
✓第二次点单时下标是 7,menu.at(7) 会抛出异常,程序结束。在循环体里加上 try / catch,接住 std::out_of_range,显示 没有下标 7。程序要显示下面两行。
红茶 没有下标 7
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int main() {7 std::vector<std::string> menu{"咖啡", "红茶", "果汁"};8 std::vector<int> picks{1, 7};9 for (int i : picks) {10 std::cout << menu.at(i) << '\n';11 }12 return 0;13}
除数不能是 0
✓main 已经准备好接异常了,可 divide 碰到 0 不会抛出。让 divide 在 b 是 0 时抛出 std::runtime_error,文字是 除数不能是 0。程序要显示下面两行。
5 出错了:除数不能是 0
1#include <iostream>2#include <stdexcept>3#include <vector>4 5int divide(int a, int b) {6 return a / b;7}8 9int main() {10 std::vector<int> divisors{2, 0};11 for (int b : divisors) {12 try {13 std::cout << divide(10, b) << '\n';14 } catch (const std::runtime_error& e) {15 std::cout << "出错了:" << e.what() << '\n';16 }17 }18 return 0;19}
修好它:具体的 catch 写在前面
✓「八」不是数字,本该显示 不是数字:八,可现在显示的是 出错了,编译器还给了一个警告。只调整代码的顺序,让程序显示下面两行。
16 不是数字:八
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int main() {7 std::vector<std::string> inputs{"8", "八"};8 for (const auto& text : inputs) {9 try {10 int n = std::stoi(text);11 std::cout << n * 2 << '\n';12 } catch (const std::exception& e) {13 std::cout << "出错了\n";14 } catch (const std::invalid_argument& e) {15 std::cout << "不是数字:" << text << '\n';16 }17 }18 return 0;19}
这一课学会了
- 出了错,函数可以
throw一个异常,比如throw std::runtime_error("没有数据");(<stdexcept>),不用返回一个容易弄混的特殊值。 try { … }围住可能出错的代码,catch (const std::runtime_error& e)接住,e.what()是那段文字;接住以后从try/catch后面接着走。- 没人接住,程序结束:
terminate called after throwing an instance of …、what():、Aborted (core dumped)。 v.at()和std::stoi自己会抛(out_of_range、invalid_argument),现在能接住了。catch (const std::exception& e)接住所有标准异常;几个catch时第一个对上的接住,具体的写在前面,catch (...)放最后。- 抛出以后,从
throw到catch路上每一层的局部对象都会析构(栈展开)。 - 自己的异常类继承
std::runtime_error,构造函数写: std::runtime_error(message)。 - 少见、这里处理不了的错用异常;常见的情况用返回值;程序写错了就修代码。