(C/C++) 从错误到异常 (error->exception)

前言

在现代主流面向对象的编程语言中,异常是一个非常重要的机制。而历史悠久的C语言却没有这种机制。

大多数人都知道异常怎么使用,但是没思考过为什么会产生这种机制。

本文将从C语言时代的错误到C++时代的异常进行简单介绍和分析。

参考资料:“异常”是啥?何时用?如何用?很多人不懂,其实就一句话。学会这个,受用终身!

其实本文就是一个笔记

衍变

error neutrality

错误中立

允许该函数的子函数,运行中产生错误。

示例场景

读取一个指定文本的信息。

分析

在错误时,控制台会打印错误信息。

为了不让错误的状态继续运行,需要不断判断函数的返回值。

原因:

宏:errno 展开成一个每个线程独立的静态变量,或函数。运行的操作会把错误原因写入这个宏。

#include <error.h>
#include <stdio.h>
#include <stdlib.h>

char *read_file(const char *path) {
    
    
    FILE *fp = fopen(path, "r");
    if (!fp) {
    
    
        perror("open fail\n");
        return NULL;
    }

    size_t cap = 1023;
    // +1 ('\0')的预留位置
    char *str = malloc(cap + 1);
    if (!str) {
    
    
        fclose(fp);

        perror("malloc fail\n");
        return NULL;
    }

    cap = fread(str, sizeof(*str), cap, fp);
    // 读取0个,且是流错误
    if (!cap && ferror(fp)) {
    
    
        free(str);
        fclose(fp);

        perror("fread ferror fail\n");
        return NULL;
    }

    str[cap] = '\0';
    fclose(fp);
    return str;
}

int main(void) {
    
    
    char *str = read_file("h1ello.txt");
    if (!str) {
    
    
        perror("read fail\n");
        return EXIT_FAILURE;
    }

    printf("data:\n--------\n%s", str);
    free(str);
    return EXIT_SUCCESS;
}

exception neutrality

异常中立

允许该函数调用的子函数,以任何手段抛出异常。即异常透明。

示例场景

读取一个指定文本的信息。

分析

有了异常,消除了各种分支判断。

但是不处理具体异常所造成的异常状态,只是暴露在造成异常的地方和抛向调用方。

#include <cstdio>
#include <fstream>
#include <memory>

::std::unique_ptr<char[]> read_file(const char* path) {
    
    
    ::std::ifstream ifs;
    // 设置异常状态为位
    ifs.exceptions(::std::ios::failbit);
    ifs.open(path);

    ::size_t cap = 1023;
    ::std::unique_ptr<char[]> str(new char[cap + 1]);
    ifs.get(str.get(), cap + 1, 0);
    return str;
}

int main(void) {
    
    
    try {
    
    
        auto str = read_file("hello.txt");
        ::std::printf("data:\n--------\n%s", str.get());
        return EXIT_SUCCESS;
    } catch (const std::exception& e) {
    
    
        ::std::printf("error: %s\n", e.what());
        return EXIT_FAILURE;
    }
}

exception safety

异常安全

示例场景

给一组存储对象添加数据。

分析

将力度更小的操作组合。

通过 copy and swap 的操作保证强异常安全。

虽然一定程度上开销较大,但是代码简洁又安全。

三个级别

  1. no-throw guarantee (不会抛出异常)
  2. basic exception safety guarantee (没有资源泄露)
  3. strong exception safety guarantee (强异常安全)
#include <map>
#include <memory>

struct User {
    
    
    uint64_t uid;
    std::string name;
    int age;
};

std::map<uint64_t, std::shared_ptr<User>> users_by_uid;
std::multimap<std::string, std::shared_ptr<User>> users_by_name;
std::multimap<int, std::shared_ptr<User>> users_by_age;

// 事务机制。要么全部更新,要么什么都不更新 (保证外部调用的原子性)
// (强异常安全) copy and swap
void add_user(...) {
    
    
    auto user = std::make_shared<User>();
    // copy
    auto tmp_by_uid = users_by_uid;
    auto tmp_by_name = users_by_name;
    auto tmp_by_age = users_by_age;

    // operator may exception
    // basic exception safety guarantee
    tmp_by_uid.insert({
    
    user->uid, user});
    tmp_by_name.insert({
    
    user->name, user});
    tmp_by_age.insert({
    
    user->age, user});

    // swap (no-throw guarantee)
    tmp_by_uid.swap(users_by_uid);
    tmp_by_name.swap(users_by_name);
    tmp_by_age.swap(users_by_age);
}

总结与思考

异常的出现,让代码简洁又安全。

编程时候应该保持错误/异常中立的思想。

使用异常,能够抛向调用方,让调用方去处理这种异常错误。这也是异常这种机制产生的原因之一。

我们什么时候需要处理异常?只有当异常出现的地方,能够处理异常,才去处理异常。

比如:

vector 内存扩容失败了,那就原地处理,防止内存泄露。

一个网站的请求超时了,操作的具体子函数本身无法处理,就抛出异常,让上级调用者判断处理。




END

猜你喜欢

转载自blog.csdn.net/CUBE_lotus/article/details/131015612
今日推荐