您现在的位置是:首页 >技术杂谈 >(C/C++) 从错误到异常 (error->exception)网站首页技术杂谈

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

天赐细莲 2024-07-22 18:01:02
简介(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
");
        return NULL;
    }

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

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

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

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

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

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

    printf("data:
--------
%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:
--------
%s", str.get());
        return EXIT_SUCCESS;
    } catch (const std::exception& e) {
        ::std::printf("error: %s
", 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

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。