前言
在现代主流面向对象的编程语言中,异常是一个非常重要的机制。而历史悠久的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 的操作保证强异常安全。
虽然一定程度上开销较大,但是代码简洁又安全。
三个级别:
- no-throw guarantee (不会抛出异常)
- basic exception safety guarantee (没有资源泄露)
- 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 内存扩容失败了,那就原地处理,防止内存泄露。
一个网站的请求超时了,操作的具体子函数本身无法处理,就抛出异常,让上级调用者判断处理。