CC++ & Algorithm

你的程序为什么一遇到“烂路”就崩溃?来给它系上C++安全带

你有没有注意过,手机地图导航遇到前方施工,它会提前提醒你“减速慢行”,而不是直接黑屏死机?但很多初学C++的人写的程序,遇到除数为0、数组越界、文件打不开,就像一脚踩空摔下台阶——整个程序“啪”地一下没了,连个提示都不给。

这不是你代码写得不够好,而是你根本没想过:程序也会被“绊倒”。而C++给出的解决方式,就是异常处理:try-catch

异常不是bug,是意外

先厘清一个概念。很多人把“异常”和“bug”混为一谈,其实不是一回事。

Bug是你代码逻辑写错了,比如少写个分号、用错了变量名。那是编译阶段的错,编译器直接甩脸子。

异常是程序运行过程中遇到的“预料之外的情况”,比如:

  • 除数是0
  • 用户输入了非法数据
  • 要打开的文件不存在
  • 内存不够用

这些情况不是你写错代码,而是程序的“路况”变差了。程序要知道如何在烂路上安全行驶,而不是直接翻车。

安全带的结构:try → throw → catch

想象你走在路上,遇到一块翘起的地砖。

一个没有准备的人:直接绊倒,膝盖磕破,躺在地上哭。

一个有准备的人:脚刚碰到地砖,立刻身体前倾、手撑地面、顺势翻滚——虽然有点狼狈,但站起来拍拍灰,继续走。

在C++里,这个过程就是:

  1. try:把可能出问题的代码圈起来,相当于“前方路段,谨慎驾驶”。
  2. throw:你自己发现不对劲,主动喊一声“我要摔了!”——这就是抛出异常。
  3. catch:另一个模块听到喊声,伸手把你扶住——这就是捕获异常。

看一个最简单的除法例子:

int a = 10, b = 0;

try {
    if (b == 0) {
        throw "除数不能为0!";
    }
    int result = a / b;
    cout << "结果是:" << result << endl;
}
catch (const char* msg) {
    cout << "错误:" << msg << endl;
}

cout << "程序继续运行,没有崩溃!" << endl;

这里的关键在于:计算机不会自动知道“除数是0”这个错误。你必须在除法前自己判断,然后主动 throw。就像地砖不会自己躲开,你得自己留心看路。

很多新手犯的第一个错误就是:只写 try,不写 throw。结果程序照样崩溃,catch 根本不执行。记住,try 只是“监控区域”,真正抛出异常的是 throw

一个经典考题:try-catch语法到底怎么写?

有个题目很有迷惑性,它问以下哪个语法是正确的:

A. try { } catch (int e) { }
B. try { } catch { }
C. try { } catch (int e); { }
D. try { } catch (int e) { } finally { }

正确答案是A。

这个题考的就是最基础的语法结构。我们来拆一下:

  • B错catch 后面必须跟一个带类型的参数。你可以写 catch (int),但不能只写 catch {}。有人可能会说,catch (...) 不是可以省略吗?注意,catch (...) 是三个点的省略号,那是特殊语法,表示“捕获所有异常”,它的参数不是“省略”而是“通配”。B选项硬要把 catch 后面直接跟大括号,那叫非法。
  • C错catch (int e); 后面多了一个分号。分号让整个语句结构被割裂,大括号跟 catch 没关系了,语法直接报错。
  • D错:C++里没有 finally。很多从Java过来的同学会顺手写 finally,但C++的清理工作不靠这个,而是靠析构函数和RAII。这一点特别容易踩坑。

所以A完全正确:try 块里写可能出错的代码,catch 块后面括号里声明异常类型和变量名,然后大括号里写处理逻辑。

这道题其实告诉我们:语法是地基。你不能只背概念,得写出能编译通过的代码。

不同异常,不同处理:多个catch的匹配顺序

现实中的“绊倒”五花八门:石头、香蕉皮、没关好的井盖。你不可能用同一套反应去对付所有情况。C++里你可以准备多个 catch,每种类型对应一种处理方式。

try {
    if (choice == 1) throw 404;
    else if (choice == 2) throw "网络错误";
    else throw 3.14;
}
catch (int code) {
    cout << "整数错误:" << code << endl;
}
catch (const char* msg) {
    cout << "字符串错误:" << msg << endl;
}
catch (...) {
    cout << "未知异常" << endl;
}

注意两个细节:

第一,catch (...) 必须放在最后。它是个兜底网,如果把具体类型写在它后面,那些具体类型就永远接不到异常了,因为 ... 会把所有异常都抢走。

第二,匹配是按顺序进行的。这一点在继承关系下尤其重要。比如你有一个 Base 类和一个 Derived 类,如果你先 catch (Base)catch (Derived),那么 Derived 的异常会被 Base 接住,你永远无法针对派生类做特殊处理。所以“子类在前,父类在后”是铁律。

一道判断题:一个try可以对应多个catch吗?

再看另一道题:一个try块可以对应多个catch块,用于捕获不同类型的异常。对还是错?

答案是:正确。而且这不仅是C++的规则,Python、Java、C#也都是这么设计的。但要注意,虽然多个catch允许,同一个异常类型不能写两次。你写两次相同类型,编译器要么报错,要么第二个永远不执行。别干那种“我多写几个catch总有一个能接到”的事。

这道判断题的重点在于理解:异常处理的核心思想是按类型分流。不同类型的异常代表不同的问题,不同的处理逻辑让程序可以更优雅地恢复。比如 catch (int code) 处理错误代码,catch (const char* msg) 处理错误信息,catch (FileNotOpen& e) 处理文件打不开——一切井然有序。

抛什么?类型要匹配

有这样一个常见错误:

try {
    throw 42;           // 整数
}
catch (const char* s) { // 字符串类型
    cout << s;
}

这段代码看起来没什么问题,但运行时程序会直接终止,因为 catch 没接住 int 类型的异常。抛出的是 int,你却想用 const char* 去接,就像一个人喊“我的脚扭了”,你递过去一张创可贴——根本不对症。

所以当你写 catch 时,一定要确认你捕获的类型和 throw 的类型一致。如果想捕获所有异常,就写 catch (...)

完整的例子:零花钱发放系统

我们来写一个稍微实际点的例子。老师给30元,让小明发给10个同学,每人10元。很明显钱不够。用异常处理来应对:

int total_money = 30;
int student_count = 10;

try {
    if (student_count <= 0) {
        throw "学生人数不能为零或负数";
    }
    if (total_money < student_count * 10) {
        throw 500;  // 错误代码500表示“钱不够”
    }
    cout << "小明领到零花钱10元" << endl;
}
catch (const char* msg) {
    cout << "错误:" << msg << endl;
}
catch (int code) {
    if (code == 500) {
        cout << "错误代码500:老师给的钱不够,总共只有" << total_money << "元!" << endl;
    }
}
catch (...) {
    cout << "发生了未预料的错误" << endl;
}

cout << "程序运行完毕,没有崩溃!" << endl;

运行结果是:

错误代码500:老师给的钱不够,总共只有30元!
程序运行完毕,没有崩溃!

这个例子里,throw 500 抛出一个整数,对应的 catch (int code) 接收它。你看,同样是错误,字符串和整数代表了不同的语义:字符串适合描述问题,整数适合作为错误代码。根据实际情况选择合适的类型。

别滥用try-catch

最后我想泼点冷水。try-catch 好用,但别把它当万能胶。

有些人写代码,喜欢把所有逻辑塞进 try 里,然后 catch (...) 吞掉所有异常,程序照跑不误。结果是什么?程序确实没崩溃,但逻辑已经错了,你压根不知道哪里出了问题。这就好比一个人摔倒了,朋友扶起来说“没事没事”,但其实腿已经断了。你只是假装没事,问题还在那。

正确的做法是:在可能出错的边界处主动 throw,用精确类型的 catch 去处理,让异常信息真实地传递上来。catch (...) 只能是最后的兜底,而不是默认选项。

更进一步,C++标准库提供了 std::exception 及其派生类,你可以直接 throw std::runtime_error("出错了"),然后 catch (std::exception& e) 捕获。这样能携带更丰富的错误信息。再配合栈展开机制,异常抛出时沿途的局部变量会自动销毁,资源不会泄漏——这又是另一个层次的话题了。

小结

try-catch 是C++给程序系的一条安全带。它让你在遇到除数为0、数据异常、资源不足时,不至于整个程序崩溃倒地,而是能优雅地站起来,说一句“这里有问题”,然后继续运行。

但安全带好不好用,取决于你系得好不好:该 throw 时要果断,catch 类型要匹配,多个 catch 顺序要正确,兜底的 ... 放最后。别把它当成掩盖 bug 的工具,它只是让程序在“烂路”上也能安全行驶。

下次你写的程序再也不突然闪退,而是打印出一句清晰的错误提示,恭喜你,你已经学会系安全带了。


关于作者

我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。

这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。

如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)

这篇文章对你有帮助吗?

有用 100%没用 0%

想系统学习这个知识点?查看完整知识点 →