C++自定义异常:自己定义错误类型
较难13学会自己造“专属报警器”——C++自定义异常
为什么需要自定义异常?
学校里的广播会播放不同铃声:上课铃、下课铃、火灾警报声。每种铃声代表不同的事件。C++中的异常也类似,有各种标准异常(比如bad_alloc内存不足),但有时你需要一种特别的铃声,比如“温度过高”、“密码错误”等。这时候就可以自定义异常——自己造一个“专属报警器”。
标准异常虽然种类不少,但没法覆盖所有情况。比如你的程序检查学生零花钱是否够买零食、判断游戏角色血量是否归零,这些场景需要更具体的错误描述。自定义异常就能帮你做到:出错时,让程序说人话,而不是只给个笼统的提示。
怎么自定义一个异常类?
1. 从 exception 类继承
C++标准库已经准备好了一个基类叫 exception,它就像一张空白设计图。你只需要从它“派生”一个新类,然后自己加上想要的功能。
#include <exception> // 必须包含这个头文件
using namespace std;
class 你的类名 : public exception {
// 在这里添加成员变量和函数
};
注意一定要用 public 继承,否则无法通过多态捕获。
2. 重写 what() 函数
what() 是 exception 类中最重要的虚函数,它返回一个 const char* 类型的错误描述。你需要在自定义类中重写它,告诉程序你的“报警器”应该喊出什么内容。
const char* what() const noexcept override {
return "这是我的专属错误信息";
}
noexcept表示这个函数不会抛出任何异常(安全承诺)。override告诉编译器你要覆盖父类的虚函数,写错名字会报错。
3. 设计构造函数,存储错误信息
你需要一个构造函数来接收错误描述,并保存到成员变量中。常用的保存方式有两种:
- C风格字符串数组(像下面例子那样,需要自己管理内存,适合初学者理解底层)
- C++的
std::string(更安全、更方便,推荐,但需要包含<string>)
为了简化,我们先看一个用C风格字符串的版本(保留原有例子):
#include <iostream>
#include <exception>
#include <cstring> // 提供 strcpy
using namespace std;
// 自定义异常类:负数异常
class NegativeError : public exception {
private:
char message[100]; // 存储错误信息的字符数组
public:
// 构造函数:传入错误描述并复制到成员变量
NegativeError(const char* msg) {
strcpy(message, msg); // 复制字符串,注意长度不要超过99
}
// 重写 what() 函数
const char* what() const noexcept override {
return message;
}
};
生活小例子:如果零花钱不够买零食,你可以定义
MoneyNotEnough异常,构造函数里传“你的零花钱只有5元,但薯片要8元”。
在异常里存更多信息
除了错误描述,你还可以让自定义异常携带错误码、发生位置的行号、相关数值等。就像报警器不仅能喊“着火了”,还能显示火源位置。
下面我们扩展 NegativeError,加上一个错误码:
class NegativeError : public exception {
private:
char message[100]; // 错误描述
int errorCode; // 错误码,比如 1001 表示"负数输入"
public:
// 构造函数:传入描述和错误码
NegativeError(const char* msg, int code) {
strcpy(message, msg);
errorCode = code;
}
// what() 返回描述
const char* what() const noexcept override {
return message;
}
// 额外函数:获取错误码
int getCode() const {
return errorCode;
}
};
这样你在 catch 之后不仅能打印错误信息,还能根据错误码做不同的处理:
catch (const NegativeError& e) {
cout << "错误描述:" << e.what() << endl;
cout << "错误码:" << e.getCode() << endl;
if (e.getCode() == 1001) {
// 做特殊处理
}
}
也许你会想:为什么不用标准异常里的 logic_error 或 runtime_error? 它们虽然也能定义错误信息,但没法像自定义类那样自由添加额外的成员变量。自定义异常就像你亲手搭建的乐高房子,想加窗户、加烟囱都可以。
抛出和捕获自定义异常
抛出的写法
和抛标准异常完全一样,用 throw 关键字:
double calculateScore(int wrongCount) {
if (wrongCount < 0) {
throw NegativeError("错题数量不能为负数!", 1001); // 抛出带描述和错误码
}
return 100.0 - wrongCount * 5.0; // 假设每题扣5分
}
捕获的写法
捕获自定义异常时,必须用引用类型 const 异常类名& e,这样才能正确调用 what() 和多态,也避免拷贝开销。
try {
double score = calculateScore(-2);
cout << "得分:" << score << endl;
}
catch (const NegativeError& e) {
cout << "自定义错误:" << e.what() << endl;
cout << "错误码:" << e.getCode() << endl;
}
如果你想“一网打尽”所有异常(包括标准异常和自定义异常),可以捕获基类 exception:
catch (const exception& e) {
cout << "捕获到异常:" << e.what() << endl;
}
这样,只要是继承自 exception 的异常都会被抓住。但缺点是你无法再访问自定义类中独有的成员(比如 getCode()),除非你用 dynamic_cast 再转换(不推荐初学者搞复杂)。
完整可运行示例(带错误码)
下面这个程序模拟了“学生考试分数计算”——如果输入负数交卷,就抛出自定义异常。
#include <iostream>
#include <exception>
#include <cstring>
using namespace std;
// 自定义异常:考试分数异常
class ScoreError : public exception {
private:
char desc[100]; // 错误描述
int errCode; // 错误码:1=负数,2=超过100分
public:
ScoreError(const char* msg, int code) {
strcpy(desc, msg);
errCode = code;
}
const char* what() const noexcept override {
return desc;
}
int getCode() const {
return errCode;
}
};
double computeScore(int wrongCount) {
if (wrongCount < 0) {
throw ScoreError("错题数不能为负!", 1);
}
if (wrongCount > 20) {
throw ScoreError("错题太多,不及格!", 2);
}
return 100.0 - wrongCount * 5.0;
}
int main() {
try {
int wrong = -3; // 模拟输入错误数据
double finalScore = computeScore(wrong);
cout << "最终得分:" << finalScore << endl;
}
catch (const ScoreError& e) {
cout << "考试异常:" << e.what() << endl;
cout << "错误码:" << e.getCode() << endl;
if (e.getCode() == 1) {
cout << "提示:检查输入是否有误。" << endl;
}
}
catch (const exception& e) { // 捕获其他标准异常
cout << "未知异常:" << e.what() << endl;
}
cout << "程序正常结束。" << endl;
return 0;
}
运行结果:
考试异常:错题数不能为负!
错误码:1
提示:检查输入是否有误。
程序正常结束。
新手常犯的错误
❌ 1. what() 返回临时变量的地址
const char* what() const noexcept override {
string temp = "临时字符串";
return temp.c_str(); // 危险!temp离开函数就被销毁,返回了野指针
}
✅ 改正:保证返回的字符串在对象生命周期内有效,比如用成员变量存储。
❌ 2. 忘记写 public 继承
class MyError : exception { // 默认是 private 继承,无法用基类指针捕获
};
✅ 改正:写成 class MyError : public exception。
❌ 3. 没有包含 <exception> 头文件
如果只用了标准异常,编译器可能会自动包含,但自定义异常必须显式 #include <exception>。
❌ 4. 用 strcpy 时忘记考虑字符串长度,导致缓冲区溢出
使用C风格数组时,记得检查长度或用更安全的 strncpy。如果要省心,可以用 string 成员变量(后面会介绍)。
用 string 更简单(进阶一点)
如果你觉得 char[] 和 strcpy 麻烦,可以用 std::string 来存储错误信息,省去拷贝的麻烦:
#include <string>
class MyError : public exception {
private:
string msg; // 错误信息,用string存储
public:
MyError(const string& str) : msg(str) {} // 构造函数初始化
const char* what() const noexcept override {
return msg.c_str(); // 返回C风格字符串
}
};
注意 what() 返回的是 msg.c_str(),只要 MyError 对象一直存在,这个指针就是有效的。
相关知识点指引
- 标准异常体系:
exception→logic_error,runtime_error→ 更具体的子类(如invalid_argument,out_of_range)。自定义异常通常继承自exception或其中的某类。 - 异常安全性:抛出异常后,程序要保证资源不泄漏(比如动态分配的内存被释放),这需要结合智能指针或RAII。
noexcept关键字:告诉编译器这个函数不会抛异常,有助于优化。what()通常标记为noexcept。- 多态和虚函数:
override和虚函数机制让你可以用基类指针捕获所有派生异常。
下次你写程序时,如果觉得标准异常不够“贴心”,就大胆造一个自己的“专属警报器”吧!就像给自己写一个“零花钱不足”、“血条归零”或者“密码错误”的提醒,让你的程序更有人情味。
例题精讲
在C++中,定义自定义运行时异常时,最适合继承的标准异常类是?
自定义异常类如果不重写 what() 方法,编译时将会报错。
请补全下面的自定义异常类,使其继承自 std::runtime_error,并提供一个无参构造函数,调用基类构造函数传递固定错误信息 "Division by zero"。
class DivByZero : public ___ {
public:
DivByZero() : ___("Division by zero") {}
};关于自定义异常的抛出与捕获,以下说法正确的是?
自定义异常类中 what() 方法必须是虚函数,才能通过基类引用实现多态调用。