[C++] 万字 - C++异常处理分析介绍: 异常概念、异常抛出与捕获匹配原则、重新抛出、异常安全、异常体系...

news/2024/9/12 18:39:15/

|wide


C语言 错误处理方式

在C语言中, 代码发生错误一般会有两种处理方式:

  1. 终止程序.

    比如 直接使用assert()断言. 或者直接崩溃

  2. 返回、设置错误码

    C语言某些函数执行失败, 但是结果不足以导致致命问题时, 就会将错误码设置在errno中. 用户可以通过strerr(errno)来获取错误信息.

但是这些针对错误的处理方式, 不灵活. 严重的错误直接就是崩溃, 没有一点回转的余地. 虽然程序出现问题很可能跟编写有关, 不过还是灵活一些比较好.

C++ 异常

异常概念

由于C语言中针对错误的处理不灵活. 所以C++引入了异常的概念.

异常是什么?

异常是一种处理错误的方式, 当一个函数 发现自己无法处理的错误时就可以 抛出异常, 让函数的直接或间接的 调用者 处理这个错误.

double Division(int a, int b) {// 当b == 0时抛出异常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);
}void Func() {int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main() {try {Func();}catch (const char* errmsg) {cout << errmsg << endl;}return 0;
}

这段代码就可以展现出最简单的 抛异常、捕捉异常、处理异常的场景.

那么 这段代码执行会出现什么现象呢?

|inline

这段代码, 我们在main()try作用域中调用了Func(), 在Func()中调用了Division()计算两数相除.

执行代码后, 可以发现 当遇到除零错误时, 会打印一个字符串. 且 这个字符串就是throw "Division by zero condition!";中的字符串. 并且, 没有返回错误退出信息.

如果将Func()try中移除, 又会是什么结果呢?

|inline

此时 发生除零错误, 进程会直接被abort终止. 退出信息为134. 这是编译器帮忙强制终止了

从这里可以看出, C++异常是如何处理的:

  1. throw

    throw是一个关键词, 用来抛出异常, 可以抛出 任意类型. 上述例子中:

    if(b == 0); throw "Division by zero condition!";

    就是在发生除零错误时, 抛出异常"Division by zero condition!"

  2. try

    try也是一个关键词, 一般来说可能会抛出异常的代码, 都放在try块中. 放在try块中的代码, 通常被成为 保护代码. 在本例中:

    Func()try中时, 可以捕获到异常并处理

    不在try中时, 不会捕获异常.

  3. catch

    catch同样是一个关键词, 是用来捕获throw抛出的异常的. 在本例中:

    catch (const char* errmsg)捕获const char*类型的异常. catch的异常类型 必须与throw的异常类型相同. 否则无法捕获目标异常.

    catch块中的代码, 为 捕获到异常后 要做的处理.

    可以有多个catch针对不同的异常进行捕获, 但是 多个catch中不能设置相同类型的异常

    即, 如果像这样设置catch:

    try {
    }
    catch (const char* e){
    }
    catch (const char* e){
    }
    

    在一些编译器中会报错, 最少也是一个警告:

    |inline

    这就表示, 第二个catch (const char* e)捕获不到const char*类型的异常.

这就是C++异常处理的最基本的概念.

异常的使用

上面介绍了 最基础的异常的使用.

不过, 想要用好异常, 还有许多的细节 和 用法需要了解.

1. 异常的抛出 与 捕获 的匹配原则

  1. 异常是通过抛出 对象 而引发的, 该 对象的类型 决定了应该激活哪个catch的处理代码

    即, catch的异常类型 必须与throw的异常类型相同. 否则无法捕获目标异常.

  2. 被选中的处理代码是调用链中 与该对象类型匹配 且离抛出异常位置最近 的那一个

    这句话是什么意思呢?

    来分析这一段代码:

    #include <iostream>using std::cout;
    using std::endl;
    using std::cin;double Division(int a, int b) {// 当b == 0时抛出异常if (b == 0) {try {throw "Division by zero condition!";}catch (const char* errmsg) {cout << "Division 捕获了 const char* 异常: " << errmsg << endl;return 1;}}elsereturn ((double)a / (double)b);
    }void Func() {int len, time;cin >> len >> time;try {cout << Division(len, time) << endl;} catch (const int errI) {cout << "Func 捕获了 const int 异常: " << errI << endl;} catch (const char* errS) {cout << "Func 捕获了 const char* 异常: " << errS << endl;} 
    }int main() {try {Func();}catch (const char* errmsg) {cout << "main 捕获了 const char* 异常: " << errmsg << endl;}return 0;
    }
    

    这段代码, 如果发生除零错误, 会触发哪个catch捕获异常呢?

    |wide

    很明显是Divison()内部的catch (const char* errmsg)会捕捉到. 因为 捕捉异常类型与抛出异常类型匹配 且离抛出异常位置最近

    如果 将Division()内部的catch (const char* errmsg)改为catch (const int errI), 那么又会被哪个catch捕捉到呢?

    |inline

    没错, 就是Func()内部的catch (const char* errS)

  3. throw抛出异常时, 编译器会生成一个 异常对象的临时拷贝. 就像函数返回那样. 所以可以正常的被更上层的函数栈捕捉到.

  4. 使用catch(...)可以 捕获任意类型的异常.

    也就是说, 使用catch(...)之后, 只要 有异常在此之前没有被捕获, 就 会捕获此异常.

    比如可以用这段代码来实验一下:

    #include <iostream>using std::cout;
    using std::endl;
    using std::cin;class Exc1 {
    private:int _excID;
    };class Exc2 {
    private:int _excID;
    };double Division(int a, int b) {// 当b == 0时抛出异常if (b == 0) {throw "Division by zero condition!";}else if (b == 1) {throw 1024;}else if (b == 2) {throw 'b';}else if (b == 3) {throw Exc1();}else if (b < 0) {throw Exc2();}elsereturn ((double)a / (double)b);
    }void Func() {int len, time;cin >> len >> time;try {cout << Division(len, time) << endl;} catch (const int errI) { 		// 捕获const整型异常cout << "Func 捕获了 const int 异常: " << errI << endl;} catch (const char errC) {		// 捕获const字符异常cout << "Func 捕获了 const char 异常: " << errC << endl;} 
    }int main() {while(true) {try {Func();}catch (const char* errmsg) {	// 捕获const字符串异常cout << "main 捕获了 const char* 异常: " << errmsg << endl;}catch (...) {cout << "main 捕获了 未知异常" << endl;}}return 0;
    }
    

    |inline

    const char*const intconst char这三种类型的异常, 我们分别在main()Func()中指定捕捉了.

    而 在传入的 b == 3 和 b < 0 时, 抛出的Exc1对象和Exc2对象异常 并没有指定捕捉.

    但是, 他们却执行了cout << "main 捕获了 未知异常" << endl;这个语句.

    即, 执行了catch(...)内的处理.

    这可以说明, catch(...)可以捕捉任意类型的异常. 但是由于没有指定类型, 所以不知道捕捉到的究竟是什么类型的异常.

  5. 虽说 catch的异常类型 必须与throw的异常类型相同. 否则无法捕获目标异常.

    但实际上, 那只是一般情况下. 除此之外, 还存在一个特例:

    如果catch捕捉基类异常, 那么 除了可以捕捉到throw抛出的 此基类异常外, 还可以捕捉到throw抛出的 此基类的派生类异常

    比如这段代码:

    #include <iostream>using std::cin;
    using std::cout;
    using std::endl;class faClass {
    private:size_t _faExcID;
    };class sonClass: public faClass { 
    private:size_t _sonExcID;
    };double Division(int a, int b) {// 当b == 0时抛出异常if (b == 0) {throw "Division by zero condition!";}else if (b == 1) {throw faClass();}else if (b < 0) {throw sonClass();}elsereturn ((double)a / (double)b);
    }void Func() {int len, time;cin >> len >> time;cout << Division(len, time) << endl;
    }int main() {while (true) {try {Func();}catch (const char* errmsg) {cout << errmsg << endl;}catch (const faClass& e) {cout << "main 捕获到faClass类异常 或 以faClass为基类的派生类异常" << endl;}}return 0;
    }
    

    这段代码:

    1. b传入0,throw "Division by zero condition!"
    2. b传入1,throw faClass()
    3. b传入负数,throw sonClass().sonClassfaClass的派生类

    而除了catch(const char* errmsg)之外, 只catch(const faClass& e)

    那么, 这段代码发生各种异常的结果是什么?

    |inline

    可以看到, 当b传入负数1时, 都会执行catch (const faClass& e)内的处理动作.

    这其实就证明了, 如果catch捕捉基类异常, 那么 除了可以捕捉到throw抛出的 此基类异常外, 还可以捕捉到throw抛出的 此基类的派生类异常

    这个特性非常的有用! 经常在开发中使用!

2. 在函数调用链中 异常栈展开匹配原则

  1. 首先检查throw本身是否在try块内部.

    如果是, 则在当前函数栈帧 查找匹配的catch语句

    如果当前函数栈帧有匹配的, 则 跳到catch的地方进行处理

  2. 如果当前函数栈帧内没有匹配的, 则 退出当前函数栈, 继续在外层调用函数的栈中进行查找匹配的catch

    此操作, 会一直退出到main()函数栈帧中

  3. 如果到达main函数的栈, 依旧没有匹配的catch, 则 终止进程

  4. 整个沿着调用链 向更外层调用函数的栈帧中查找匹配的catch的行为, 被称为 栈展开

  5. 为了避免 由于异常没有匹配的catch导致进程终止, 所以 都会在最后 使用catch(...)捕获未知异常

  6. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行

此原则中, 前三条原则 即为栈展开的过程.

假如存在Func1()Func2()Func3():

void Func3() {throw "Throw an exception directly!";cout << "hello Func3" << endl;
}void Func2() {Func3();cout << "hello Func2" << endl;
}void Func1() {Func2();cout << "hello Func1" << endl;
}

且,main()函数呢存在以下内容:

int main() {try {Func1();}catch (const char* errmsg) {cout << errmsg << endl;}cout << "hello main" << endl;return 0;
}

那么,throw异常之后, 栈展开的过程大概为这样的:

C++异常处理有一个 非常麻烦 的点.

throw之后, 如果找到对应类型的catch, 会直接跳转到对应函数栈帧内执行catch子句, 执行完之后 会留在此函数内继续向下执行. 而不是回到throw继续向下执行.

执行上面的代码也可以证明这一点:

|inline

Func1()Func2()Func3()中, 都有一句cout语句. 但是, 只执行了main()中的cout语句.

这样很可能会造成什么后果呢? 可以看一看这段代码:

#include <iostream>using std::cout;
using std::endl;void Func1() {// new一块空间int* arr = new int[20480];throw "Throw an exception directly!";cout << "hello Func1" << endl;delete[] arr;
}int main() {while (true) {try {Func1();}catch (const char* errmsg) {cout << errmsg << endl;}cout << "hello main" << endl;}return 0;
}

这段代码执行之后, 会发生什么后果?

没错, 内存泄漏! 非常严重的内存泄漏!

可以看到, 名为exception_test.exe的内存占用, 在疯狂的上涨.

这是比较明显的内存泄漏. 这是什么原因呢?

其实是因为, 我们new int[20480]出来的空间, 没有被delete[]掉.

但是Func1()函数中,new[]之后 函数结束前 明明delete[]了, 为什么没有delete[]掉呢?

就是因为Func1()函数内throw之后, 直接跳到了main()catch的位置, 并且留在了main()中没有回到throw那里. 所以 在throw之后的 delete[]语句根本就没有执行. 就 造成了内存泄漏

当前阶段, 如何正确的解决这个问题呢?

如果存在new之后会throw的可能, 就需要直接在Func1()内 将throw放在try块内, 就地catch处理并返回:

void Func1() {// new一块空间int* arr = new int[20480];try {throw "Throw an exception directly!";}catch (const char* errmsg) {cout << errmsg << endl;delete[] arr;return;}// 这里也要delete[]// 因为在其他场景中, 可能并不一定会throwdelete[] arr;
}

这样, 就不会发生内存泄漏了:

这是当前阶段最简单的处理方式, 不过太难用.

3. 异常的重新抛出

观察这段代码:

double Division(int a, int b) {// 当b == 0时抛出异常if (b == 0) {throw "Division by zero condition!";}return (double)a / (double)b;
}void Func() {int* array = new int[10];try {int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (...) {cout << "delete []" << array << endl;delete[] array;throw;}cout << "delete []" << array << endl;delete[] array;
}int main() {try {Func();}catch (const char* errmsg) {cout << errmsg << endl;}return 0;
}

Func()new了一个数组. 但是在delete[]之前又有可能throw异常.

此异常的处理动作, 已经在 main()函数中实现了.

但是, 由于还有new出来的空间未delete, 又不得不在Func()函数内添加一个try{...}catch{...}

不过, 这里的捕获异常 可以使用catch(...)捕获任意异常, 并且不处理异常, 只将未释放的空间delete掉, 然后再将异常原从新抛出

抛出之后, 会再沿着调用链进行栈展开寻找对应的catch, 找到真正处理此异常的catch再处理掉异常

上面例子中, Func()catch(...)子句中的 throw 即为 重新抛出异常

由于这里使用的是 catch(...) 没有指定类型捕获, 所以 throw; 就可以重新抛出

如果指定了类型 catch(const char* errmsg), 就需要 throw errmsg; 来实现重新抛出.

4. 异常安全

关于异常的使用, 有一些情况下需要非常的小心:

  1. 构造函数的作用是 完成对象的构造和初始化, 最好 不要在构造函数中抛出异常, 否则 可能导致对象不完整或没有完全初始化

  2. 析构函数的作用是 完成资源的清理, 最好 不要在析构函数内抛出异常, 否则 可能导致资源泄漏(内存泄漏、句柄未关闭等)

  3. 还有就是, 在newdelete中抛出了异常, 导致 内存泄漏. 在lockunlock之间抛出了异常 导致死锁.

    这些问题的更加好用的解决, 需要用到智能指针.

5. C++标准库的异常体系

在介绍 异常的抛出与捕获匹配原则时, 介绍过 如果catch捕捉基类异常, 那么 除了可以捕捉到throw抛出的 此基类异常外, 还可以捕捉到throw抛出的 此基类的派生类异常

并且也举了简单的例子证明.

所以, C++委员会就根据此原则, 实现了一个 异常类体系.

说明白一点, 就是 C++委员会 实现了许多的类 来对应C++可能发生的所有错误, 被称为 异常类. 这些异常类, 都来派生于一个基类 std::exception.

|inline

文档中对此类的描述是:

标准库中所有组件抛出的异常对象都派生于此类. 因此, 通过捕获此类, 就可以捕获所有标准异常

此类中, 除了构造、析构等成员函数之外, 还实现了一个共其派生类重写的成员函数what()

what()

|inline

what() 有什么用呢?

由于标准异常有很多, 而且都可以通过捕获std::exception来捕获.

所以, 捕获到之后要分辨 捕获到的究竟是什么异常是很麻烦的

所以, std::exception提供了what()函数. 它需要实现的作用是: 获取标识异常的字符串

设置成虚函数, 就是为了让派生类重写此函数, 实现不同派生类可以 返回标识其本身的字符串.

也就是说, 通过捕获std::exception捕获到异常对象之后, 可以调用其成员函数what()并接收其返回值, 来知道捕获到的是什么异常.

C++标准库中的异常类

|inline

C++标准库中, 实现了许多的异常类.

  1. bad_alloc

    分配内存失败时, 抛出的异常.

  2. bad_cast

    动态转换时, 抛出的异常

  3. out_of_range

    越界访问时, 抛出的异常

这张图, 可以用来表示 C++标准库中的异常类体系:

6. 自定义异常体系

实际的项目开发中, 许多的公司都会自己定义一套异常体系 来进行规范的异常管理.

原因嘛, 用一句话总结 大概就是: 标准库的异常体系无法满足需求.

这里有一个 自定义异常体系的例子:

#include <iostream>
#include <string>
#include <unistd.h>using std::string;
using std::cout;
using std::endl;class Exception {
public:Exception(const string& errmsg, int id) : _errmsg(errmsg), _id(id) {}virtual string what() const {return _errmsg; }protected:string _errmsg;int _id;
};class SqlException : public Exception {
public:SqlException(const string& errmsg, int id, const string& sql): Exception(errmsg, id), _sql(sql) {}virtual string what() const {string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}private:const string _sql;
};class CacheException : public Exception {
public:CacheException(const string& errmsg, int id) : Exception(errmsg, id) {}virtual string what() const {string str = "CacheException:";str += _errmsg;return str;}
};class HttpServerException : public Exception {
public:HttpServerException(const string& errmsg, int id, const string& type): Exception(errmsg, id), _type(type){}virtual string what() const {string str = "HttpServerException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};void SQLMgr() {if (rand() % 7 == 0) {throw SqlException("权限不足", 100, "select * from name = '张三'");}else {cout << "Sql Success" << endl;}
}void CacheMgr() {if (rand() % 5 == 0) {throw CacheException("权限不足", 101);}else if (rand() % 6 == 0) {throw CacheException("数据不存在", 102);}else {cout << "Cache Success" << endl;}SQLMgr();
}void HttpServer() {if (rand() % 3 == 0) {throw HttpServerException("资源请求错误", 103, "get");}else if (rand() % 4 == 0) {throw HttpServerException("权限不足", 104, "post");}else {cout << "Http Success" << endl;}CacheMgr();
}int main() {srand(time(0));while (true) {// 此代码中 唯一一个不能跨平台的函数sleep(), 这里用的是 Linux环境// Windows 平台 需要将其换为 Sleep(1000);// 并将 头文件 unistd.h 换为 Windows.hsleep(1);try {HttpServer();}catch (const Exception& e) {// 多态cout << e.what() << endl;}catch (...) {cout << "Unkown Exception" << endl;}}return 0;
}

我们先分析一下这段代码:

  1. 首先, 代码实现了 4 个类: 1个基类, 3个派生类

    基类 Exception

    成员变量: _errmsg, string类型 用于存储异常信息. _id, int类型 用于存储异常代码

    成员函数: what(), 返回异常信息, 用于获取当前异常类

    派生类1 SqlException

    成员变量: 除继承于基类的 _errmsg_id 之外. _sql, const string类型 用于存储 异常sql指令

    成员函数: 重写what(), 返回异常信息, 包括 所属类_errmsg_sql

    派生类2 CacheException

    成员变量: 除继承于基类的 _errmsg_id 之外, 无其他成员变量.

    成员函数: 重写what(), 返回异常信息, 包括 所属类_errmsg

    派生类3 HttpServerException

    成员变量: 除继承于基类的 _errmsg_id 之外. _type, const string类型 用于存储 发生异常的服务类型

    成员函数: 重写what(), 返回异常信息, 包括 所属类type_errmsg

  2. 其次, 实现了三个函数 用来模拟不同的服务的异常场景

    SqlMgr():

    模拟数据库管理时的异常场景:

    随机数 % 7 == 0 执行 throw SqlException. 来模拟数据库管理时权限不足的场景.

    异常信息: 权限不足, 异常代码: 100, 异常Sql语句: select * from name = '张三'

    CacheMgr():

    模拟缓存管理时的异常场景:

    随机数 % 5 == 0 执行 throw CacheException("权限不足", 100)

    异常信息: 权限不足, 异常代码: 101

    随机数 % 6 == 0 执行 throw CacheException("数据不存在", 101)

    异常信息: 数据不存在, 异常代码: 102

    最后, 调用 SqlMgr()

    HttpServer():

    模拟HTTP服务可能发生的异常场景:

    随机数 % 3 == 0 执行 HttpServerException("请求资源不存在", 200, "get")

    异常信息: 资源请求错误, 异常代码: 103, 异常服务类型: get

    随机数 % 4 == 0 执行 HttpServerException("权限不足", 100, "post")

    异常信息: 权限不足, 异常代码: 104, 异常服务类型: post

    最后, 调用CacheMgr()

  3. 最后, 主函数内 死循环模拟服务器运行.

    try块内调用HttpServer(), 而HttpServer()内调用CacheMgr(), CacheMgr()内调用SqlMgr(), 模拟服务运行的流程.

    catch (const Exception& e)及代码块实际作用是 捕获三种异常类, 并多态调用不同异常类对象的what() 接受打印相关异常信息

    catch (...)及代码块捕获其他异常, 输出未知异常

至此, 整个代码分析结束, 从HttpServer() 层层调用到 SqlMgr(), 每个函数内都有概率抛出不同的异常, 抛出之后会在 main内被捕获并处理.

查看执行结果:

|inline

从结果可以看到, 每次循环 每层调用都有一定的概率抛异常. 并且都会在main函数内被捕捉到并处理.

图中, 光标可能在某行停顿1-2s, 说明此时并没有异常抛出


在添加一个SeedMsg()函数, 模拟发送信息异常.

void SeedMsg(const string& str) {if (rand() % 2 == 0) {throw HttpServerException("SeedMsg::网络错误", 105, "put");}else if (rand() % 4 == 0) {throw HttpServerException("SeedMsg::你已经不是对方好友", 106, "post");}else {cout << "消息发送成功!->" << str << endl;}
}

main() 函数try块中执行的函数 换为此函数, 查看执行:

|inline

并尝试, 将 网络错误异常的处理方式 改为 发生异常之后 直接重试再发送10次.

其实很简单, 只需要改动main函数内容就可以(不过要先在 Exception类中添加一个成员函数getid()):

int main() {srand(time(0));while (true) {// 此代码中 唯一一个不能跨平台的函数sleep(), 这里用的是 Linux环境// Windows 平台 需要将其换为 Sleep(1000);// 并将 头文件 unistd.h 换为 Windows.hsleep(1);try {for(int i = 1; i <= 10; i++) {try {SeedMsg("你好啊?");// 能走到这里 一定发送成功// 直接break跳出 for循环break;}catch (const Exception& e) {if (e.getid() == 105) {// 针对 103 异常处理cout << "网络错误, 重发送, 第 " << i << " 次" << endl;continue;}else {// 不是此异常, 重新抛出throw e;}}}}catch (const Exception& e) {// 多态cout << e.what() << endl;}catch (...) {cout << "Unkown Exception" << endl;}}return 0;
}

执行结果:

inline

这里的关键点就是, 异常的重新抛出, 还有 SeedMsg()之后的break.

由于需要特定的处理 _id105的异常, 所以需要先就近catch一下, 然后判断异常代码, 进行处理.

而, 由于只处理105异常, 其他异常就需要再次抛出, 让其他地方处理.

然后, SeedMsg()之后的break. SeedMsg()正常返回, 就一定会顺着向下走, 也表示这发送成功, 就不需要继续for循环, 所以直接break. 如果抛异常, 则会跳过break, 然后异常被下面的catch子句捕获.


介绍到这里, C++关于异常的内容 就暂时介绍完了.

感谢阅读~


http://www.ppmy.cn/news/789285.html

相关文章

LangChain: 大语言模型的新篇章

本文介绍了LangChain框架&#xff0c;它能够将大型语言模型与其他计算或知识来源相结合&#xff0c;从而实现功能更加强大的应用。接着&#xff0c;对LangChain的关键概念进行了详细说明&#xff0c;并基于该框架进行了一些案例尝试&#xff0c;旨在帮助读者更轻松地理解LangCh…

虾皮物流怎么收费?收费标准是什么?

虾皮物流怎么收费的? 虾皮物流收费标准是什么&#xff1f; 今天我们就讨论下这个问题 首先虾皮收费是分两部分的&#xff1a;一个是首重&#xff0c;另外一部分是续重。那么我们按照SLS的标准来说下首重和续重。虾皮对于大部分国家首重都是10G&#xff0c;续重同样是10G。看…

搬家公司怎么收费 搬家收费标准

搬家不单要断舍离还要花钱&#xff0c;在一个地方呆久了&#xff0c;物品又多&#xff0c;一般都是等到不得不搬的情况下才选择搬&#xff0c;搬过家的都知道实在太难了。那么普通搬家一次大概多少钱呢&#xff1f;搬家公司怎么收费&#xff1f;以什么来算&#xff1f;费用问题…

服务器维保价格标准_服务器维护收费标准

{"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],"search_count":[{"count_phone":6,"count":6}]},"card":[{"des":"云服务器 ECS(Elastic Compute Service)是一…

爬虫的收费标准

前言 很多人知道&#xff0c;Python能做Web开发&#xff0c;能做数据分析&#xff0c;但是想必大家更知道Python最牛的是做爬虫吧&#xff0c;没有对手&#xff0c;真的YYDS。 我身边很多的Python开发者不管是明里暗里都给我晒了他们收益截图&#xff0c;少的几千&#xff0c…

积极应对快递服务风险,顺丰保价帮助用户保障利益

虽然我国快递服务水平在全球处于前列&#xff0c;不过出现丢件、物品损坏或者延误等情况还是时有发生&#xff0c;这些快递事故目前来说在快递行业还是无法完全避免的现象。如何帮助用户避免这些事故因此带来的损失&#xff0c;也是快递企业需要做好的事情。顺丰一直以来积极应…

如何更好地获得顺丰理赔,这些注意事项要知道

现在快递已经成为大家日常生活中不可或缺的重要服务&#xff0c;不过一些人在享受快递带来便利的同时&#xff0c;也会因为丢件、物品损坏或者遗失等快递事故而出现经济损失&#xff0c;在这种情况下&#xff0c;大家可以要求快递公司赔偿。顺丰在国内算是服务品质非常靠前的行…

aws上采用tidb和原生使用aws rds价格的比较。兼数据分析性能的测试

作者&#xff1a; tidb狂热爱好者 原文来源&#xff1a; https://tidb.net/blog/ef242615 项目背景介绍 有一个20t-30t的历史库需要做数据分析&#xff0c;节能减排&#xff0c;减容增效。今年大环境不好&#xff0c;aws的费用又是出奇的贵。 历史库的作用是公司近1年的订…

学无止境·MySQL(3-2)

单表查询试题 单表题目一1、创建表2、查询出部门编号为30的所有员工3、所有销售员的姓名、编号和部门编号4、找出奖金高于工资的员工5、找出奖金高于工资60%的员工。6、 找出部门编号为10中所有经理&#xff0c;和部门编号为20中所有销售员的详细资料。7、找出部门编号为10中所…

射频学习(DAY1)时域和频域、数字预失真

信号可以从时域和频域两个方面进行分析。 时域是指信号随时间的变化情况。在时域分析中&#xff0c;我们观察和记录信号随时间的变化&#xff0c;包括信号的振幅、波形、周期性等。时域分析的常见方法包括时域图、波形图和功率谱密度图。时域分析可以提供关于信号的时间特性和…

leetcode 1331. 数组序号转换

题目描述解题思路执行结果 leetcode 1331. 数组序号转换 题目描述 数组序号转换 给你一个整数数组 arr &#xff0c;请你将数组中的每个元素替换为它们排序后的序号。 序号代表了一个元素有多大。序号编号的规则如下&#xff1a; 序号从 1 开始编号。 一个元素越大&#xff0c;…

使用python打印异常

在Python中&#xff0c;可以使用try-except语句来捕获和处理异常。try块中的代码会被执行&#xff0c;如果发生了异常&#xff0c;那么异常将被except块中的代码捕获并处理。下面是一个简单的示例&#xff0c;展示了如何打印异常信息&#xff1a; try:# 可能会发生异常的代码a…

如何查看电脑端口 计算机常被利用的端口号介绍

电脑端口就是常被有心人利用&#xff0c;对我们造成安全隐患的一种&#xff0c;了解一些端口方面的知识还是很有必要的&#xff0c;下面为大家介绍下如何查看电脑常用计算机端口号的方法&#xff0c;由此需求的朋友可以参考下&#xff0c;或许对大家有所帮助 网络基础设施漏洞是…

电脑端口号怎么查看?运行cmd命令查看电脑端口的方法图解

我们在进行某些操作的时候需要限制或开放计算机端口&#xff0c;那么&#xff0c;如何查看电脑端口号呢&#xff1f;针对此问题&#xff0c;本文就为大家介绍运行cmd命令查看电脑端口的方法&#xff0c;有兴趣的朋友们可以了解下 对于电脑爱好者来说&#xff0c;电脑端口也是必…

如何查看计算机中的端口占用情况,电脑如何查看端口是否被占用?CMD查看端口占用开放情况...

端口是很多软件和服务用于通讯的&#xff0c;但是实际上会有很多软件或服务端口使用同一个端口&#xff0c;这就导致一个端口被占用后&#xff0c;另一个也需要该端口的应用无法正常工作&#xff0c;那么如何查看端口被占用呢&#xff1f;这里我们可以使用CMD命令来查看。 查看…

计算机端口25,在Windows 下关闭21\23\25端口的方法 -电脑资料

在Windows 下关闭21\23\25端口的方法,有时候为了安全我们需要禁止一些端口21端口主要用于FTP(File Transfer Protocol,文件传输协议)服务, 端口说明:21端口主要用于FTP(File Transfer Protocol,文件传输协议)服务,FTP服务主要是为了在两台计算机之间实现文件的上传与下载,…

如何关闭电脑端口 关闭不常用端口的2种方法

服务器的安全对于每个站长或管理员都十分重要。通过关闭不常用的端口&#xff0c;可以有效的将黑客拒之千里之外&#xff0c;下面为大家介绍下常用关闭端口的2种方法&#xff0c;感兴趣的朋友不妨参考下&#xff0c;希望对大家有所帮助 通常电脑每一项系统服务都有一个对应端口…

如何禁用计算机开放的端口,关闭端口命令,教您如何关闭电脑80端口

有很多用户都知道80端口是上网使用最多的协议&#xff0c;网页服务默认端口都是80&#xff0c;不过有一些木马专门针对80端口攻击计算机&#xff0c;为了保证电脑的安全&#xff0c;很多用户都会把80端口关闭&#xff0c;那么如何关闭80端口呢?下面&#xff0c;小编给大家分享…

电脑常用端口号作用

21端口&#xff1a;主要用于文件传输服务&#xff0c;主要是为了在两台计算机之间实现文件的上传与下载&#xff0c;一台计算机作为客户端&#xff0c;另一台计算机作为服务器&#xff0c;可以采用匿名登录和授权用户名与密码登录两种方式登录服务器。目前&#xff0c;通过FTP服…

如何查看电脑的网络端口?

1、WindowsR调出运行界面。 2、然后输入cmd&#xff0c;点击确定进入DOS命令。 3、进入DOS命令后&#xff0c;输入netstat/n&#xff0c;然后按回车键。 4、输入netstat /n&#xff0c;按回车键后&#xff0c;就可以看到电脑的端口了。 整个过程中对应的TCP状态如下&#xff1a…