CC++ & Algorithm

C++自定义异常:自己定义错误类型

较难13
语言版本:C++Python
概述:通过继承标准异常类,我们可以创建专属的异常类型,像做自己的个性化报警器。

学会自己造“专属报警器”——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_errorruntime_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 对象一直存在,这个指针就是有效的。


相关知识点指引

  • 标准异常体系exceptionlogic_error, runtime_error → 更具体的子类(如 invalid_argument, out_of_range)。自定义异常通常继承自 exception 或其中的某类。
  • 异常安全性:抛出异常后,程序要保证资源不泄漏(比如动态分配的内存被释放),这需要结合智能指针或RAII。
  • noexcept 关键字:告诉编译器这个函数不会抛异常,有助于优化。what() 通常标记为 noexcept
  • 多态和虚函数override 和虚函数机制让你可以用基类指针捕获所有派生异常。

下次你写程序时,如果觉得标准异常不够“贴心”,就大胆造一个自己的“专属警报器”吧!就像给自己写一个“零花钱不足”、“血条归零”或者“密码错误”的提醒,让你的程序更有人情味。

例题精讲

1单选题

在C++中,定义自定义运行时异常时,最适合继承的标准异常类是?

Astd::exception
Bstd::runtime_error
Cstd::logic_error
Dstd::bad_alloc
2判断题

自定义异常类如果不重写 what() 方法,编译时将会报错。

3填空题
请补全下面的自定义异常类,使其继承自 std::runtime_error,并提供一个无参构造函数,调用基类构造函数传递固定错误信息 "Division by zero"。
class DivByZero : public ___ {
public:
    DivByZero() : ___("Division by zero") {}
};
4单选题

关于自定义异常的抛出与捕获,以下说法正确的是?

A抛出自定义异常时,必须使用 throw 关键字,且抛出的必须是对象。
B用基类引用捕获自定义异常时,无法直接访问派生类特有的成员。
C派生类异常对象被捕获时,会自动转换为基类对象,导致派生类数据丢失。
D在函数声明中可以使用 throw() 列表指定可能抛出的自定义异常类型。
5判断题

自定义异常类中 what() 方法必须是虚函数,才能通过基类引用实现多态调用。