C++ -- 异常

C++中的异常是用于处理程序执行过程中出现的错误情况。通过异常处理,程序可以在遇到错误时优雅地处理这些问题,而不是直接崩溃。

C语言处理错误的方式

C语言传统的处理错误的方式主要有两种:

  • 终止程序:使用如assert这样的宏来检查错误条件,并在条件不满足时直接终止程序。这种方式非常直接但粗暴,不考虑程序的恢复或错误处理,用户难以接受。
  • 返回错误码:函数通过返回值来指示是否成功执行,并将错误码(通常是整数)存储在全局变量(如errno)中或通过其他方式返回。这种方式需要程序员手动检查错误码,并查找对应的错误原因,增加了代码的复杂性和出错的可能性。

示例1:

#include <assert.h>int main()
{int a = 3;assert(a < 5); // 条件成立,不终止程序assert(a > 5); // 条件不成立,终止程序return 0;
}

示例2:

#include <stdio.h>
#include <errno.h>
#include <string.h>int readFile(char *filename, char *buffer, size_t bufferSize) {FILE *file = fopen(filename, "r");if (file == NULL) {return -1; // 文件打开失败}if (fgets(buffer, bufferSize, file) == NULL) {fclose(file);return -2; // 读取失败}fclose(file);return 0; // 成功
}int main() {char buffer[100];if (readFile("example.txt", buffer, sizeof(buffer)) != 0) {printf("Error reading file.\n");return 1;}printf("%s\n", buffer);return 0;
}

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

C++异常概念

C++中的异常是一种处理程序运行期间发生的意外或错误情况的机制。当函数遇到无法处理的错误时,可以抛出一个异常对象,让函数的直接或间接调用者捕获并处理这个异常。异常提供了一种结构化的方式来处理程序中的错误情况,提高了程序的健壮性和可维护性。

异常的用法

C++中异常的使用主要涉及三个关键字:trycatchthrow

  • throw:用于抛出异常。当程序遇到错误情况时,可以使用throw关键字后跟一个异常对象(可以是任何类型的对象,但通常是派生自std::exception或其子类的对象)来抛出异常。
  • trytry块用于包裹可能抛出异常的代码。当try块中的代码抛出异常时,控制权会转移到紧随其后的catch块(如果有的话)。
  • catchcatch块用于捕获并处理异常。可以有多个catch块来捕获不同类型的异常,或者使用一个catch(...)块来捕获所有类型的异常。
double Division(int a, int b)
{if (b == 0)throw "Division by zero condition!"; // 抛出异常return (double)a / (double)b;
}int main()
{try {Division(1, 0); // 会抛出异常Division(2, 1); // 不会抛出异常}catch(const char* errmsg) // 捕获异常{std::cout << errmsg << std::endl;}// 如果没有异常,则向下执行。如果有异常,被捕获处理后,也向下执行// ...  std::cout << "end" << std::endl;return 0;
}

异常的抛出和捕获

异常的抛出和匹配原则

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这拷贝的临时对象会在被catch以后被销毁。(这里的处理类似于函数的传值返回:catch 接收 throw 抛出的对象 作为参数。这个过程可能会触发移动语义(¬‿¬))。
  4. catch(...) 可以捕获任意类型的异常对象。(使用这一捕获方式,我们就不知道被捕获对象的具体类型)。
  5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配:可以抛出派生类对象,使用基类捕获(这里的应用后面详细讲解)。
  1. 激活与throw对象匹配的catch处理代码
#include <iostream>double Division(int a, int b)
{if (b == 0)throw "Division by zero condition!"; // 抛出异常return (double)a / (double)b;
}int main()
{try {Division(1, 0); // 会抛出异常}catch (int errnum){std::cout << "int errnum = " << errnum << std::endl;}catch(const char* errmsg) // 捕获异常{std::cout << "const char* errmsg = " << errmsg << std::endl;}catch (...){std::cout << "..." << std::endl;}std::cout << "end" << std::endl;return 0;
}

运行结果:

const char* errmsg = Division by zero condition!
end
  1. 匹配且离抛出异常位置最近的catch处理代码

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

  1. 首先,需要确认抛出异常的代码逻辑是否位于 try 块内部。只有在 try 块内部抛出的异常才会被查找匹配的 catch 子句。如果不在 try 块内部,则不会进行任何异常处理,异常会直接沿调用栈向上抛出。
  2. 如果当前函数栈中有匹配的,则会跳转执行匹配的catch代码块;如果当前函数栈中没有匹配的,就继续在调用函数的栈中进行查找匹配的catch
  3. 如果到达main()函数的栈中,还没有匹配的catch,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。在实际应用中,main()最后都要加上catch(...)捕获任意类型的异常,否则当有异常没有被捕获,程序就会直接终止(例如,服务器是一直在运行中的,如果遇到一个异常就要停掉服务器,代价比较大)。
  4. 当找到匹配的catch子句并处理以后,会执行catch子句后面的代码。

示例:

#include <iostream>double Division(int a, int b)
{if (b == 0)throw "Division by zero condition!"; // 抛出异常return (double)a / (double)b;
}void Func()
{try{Division(1, 0);}catch (char ch){std::cout << "char ch" << std::endl;}
}int main()
{try {Func();}catch (const char* errmsg){std::cout << errmsg << std::endl;}catch (...) {std::cout << "unkown exception" << std::endl;}std::cout << "end" << std::endl;return 0;
}

分析:

  1. 首先,Division() 函数中,throw抛出异常的代码逻辑没有位于 try 块内部,则不会进行任何异常处理,异常会直接沿调用栈向上抛出,也就是Func()函数栈中。
  2. Func()函数栈中没有匹配的catchchar 并不匹配const char*类型),就继续在调用函数main()的栈中进行查找匹配的catch
  3. 到达main()函数的栈中,有匹配的catch(类型为const char*),则捕获异常并处理异常。
  4. 当找到匹配的catch子句并处理以后,会执行catch子句后面的代码。

有了上面的知识,匹配原则的第二点应该就能理解了(匹配且离抛出异常位置最近的catch处理代码)。

抛出派生类对象,使用基类捕获

抛出派生类对象并使用基类捕获是一种常见的异常处理模式,它利用了C++中的继承关系和多态性。通过这种方式,可以在不同的层次上处理不同类型的异常,同时保持代码的灵活性和扩展性。

抛出派生类对象,使用基类捕获的使用场景

如果你抛出了一个派生类的对象,并使用基类捕获异常,那么这个异常是可以被捕获的。这是因为派生类的对象可以被视为基类的对象,即派生类对象可以隐式转换为基类对象。

使用场景

  1. 统一异常处理

    • 当你不知道具体抛出哪种类型的异常时,可以使用基类来捕获所有可能的派生类异常。
    • 例如,在一个模块中,你可能需要处理多种不同类型的运行时错误,可以使用 std::runtime_error 作为基类来捕获所有派生类异常。
  2. 分层处理

    • 在多层调用中,可以在不同的层次上捕获和处理不同类型的异常。
    • 下层函数可以抛出具体的异常类型,而上层函数可以使用基类来捕获这些异常,并进行统一处理。
  3. 异常类型扩展

    • 当需要添加新的异常类型时,可以继承已有的基类,而不需要修改现有的捕获逻辑。
    • 这样可以保持代码的可扩展性,未来添加的新异常类型可以无缝集成到现有系统中。

示例代码

假设我们有以下异常类的定义:

#include <iostream>
#include <exception>class BaseException : public std::exception {
public:virtual const char* what() const noexcept = 0;
};class DerivedException1 : public BaseException {
public:const char* what() const noexcept override {return "Derived Exception 1";}
};class DerivedException2 : public BaseException {
public:const char* what() const noexcept override {return "Derived Exception 2";}
};

抛出派生类对象

void functionThatThrows() {throw DerivedException1(); // 抛出派生类异常
}void anotherFunctionThatThrows() {throw DerivedException2(); // 抛出另一个派生类异常
}

使用基类捕获

int main() {try {functionThatThrows();} catch (const BaseException& e) {std::cerr << "Caught base exception: " << e.what() << std::endl;}try {anotherFunctionThatThrows();} catch (const BaseException& e) {std::cerr << "Caught base exception: " << e.what() << std::endl;}return 0;
}

代码分析

  1. 定义异常类

    • BaseException 是一个抽象基类,定义了一个纯虚函数 what()
    • DerivedException1DerivedException2BaseException 的派生类,实现了 what() 函数。
  2. 抛出异常

    • functionThatThrowsanotherFunctionThatThrows 分别抛出 DerivedException1DerivedException2
  3. 捕获异常

    • main 函数中,使用 BaseException 类型的 catch 子句来捕获所有派生类异常。

注意事项

  1. 类型信息丢失

    • 使用基类捕获可能会丢失具体的派生类类型信息。如果需要区分具体的派生类异常,可以在捕获基类异常后进一步判断具体的派生类类型。
  2. 多态性

    • 利用多态性,可以在捕获基类异常后调用虚函数来获取具体的信息。

异常的重新抛出

有可能单个的 catch 不能完全处理一个异常,在进行一些校正处理以后(没有处理完全),希望再交给更外层的调用链函数来处理,catch 则可以通过重新抛出将异常传递给更上层的函数进行处理。

#include <iostream>
double Division(int a, int b)
{if (b == 0)throw "Division by zero condition!";return (double)a / (double)b;
}void Func()
{int* arr = new int[10];try {std::cout << Division(1, 0) << std::endl;}catch (...) {std::cout << "delete[] " << arr << std::endl;delete[] arr;// 这里并没处理Division()实际抛出的异常throw;}// ...std::cout << "delete[]" << arr << std::endl;delete[] arr;
}int main()
{try {Func();}catch(const char* errmsg) {// 这里处理Division()抛出的异常std::cout << errmsg << std::endl;}return 0;
}

补充:try(...); throw; 两者配合使用时,try 捕获什么类型的异常,throw 便会抛出什么类型的异常。

异常安全

  1. 构造函数的主要任务是初始化对象。如果在构造函数中抛出异常,而对象的部分成员已经初始化,这可能会导致对象处于不一致的状态。当这种对象被销毁时,其析构函数可能会尝试访问那些未完全初始化的成员,从而引发更多的问题。
  2. 析构函数主要完成资源的清理。最好不要在析构函数内抛出异常,否则可能导致资源泄露(内存泄漏、句柄未关闭等)。
  3. C++中异常经常会导致资源泄露的问题,比如:在 newdelete 中抛出了异常,导致内存泄露;在 lockunlock 之间抛出了异常,导致死锁。C++经常使用RAII来解决以上问题(关于RAII,智能指针章节会进行讲解~)。

示例一:构造函数:

double Division(int a, int b)
{if (b == 0)throw "Division by zero condition!"; // 抛出异常return (double)a / (double)b;
}class AA
{
public:AA(){std::cout << "AA()" << std::endl;_ptr1 = new int;Division(1, 0);_ptr2 = new int;}~AA(){std::cout << "~AA()" << std::endl;delete _ptr1;delete _ptr2;}
private:int* _ptr1;int* _ptr2;
};int main()
{try{AA a;}catch(...){std::cout << "处理异常" << std::endl;}return 0;
}

运行结果:

AA()
处理异常

代码分析

  1. 构造函数中的异常抛出

    • AA 类的构造函数中调用了 Division(1, 0),该函数会在 b == 0 的情况下抛出异常。
    • 当异常抛出时,构造函数不会继续执行,因此 _ptr2 不会被初始化(实际上某些编译器会将其初始化为nullptr)。
  2. 异常处理

    • 异常被抛出后,程序会跳转到最近的 catch 块进行处理。
    • 因为异常是在构造函数中抛出的,构造函数没有完成,所以对象 a 并没有完全构造完毕。
  3. 析构函数的调用

    • 在构造函数中抛出异常的情况下,对象 a 并没有完全构造完成,因此它的析构函数不会被调用。
    • 在 C++ 中,如果一个对象的构造函数抛出异常,该对象就不会被视为已经构造完成,析构函数也不会被调用

总结

  1. 构造函数中的异常抛出

    • 如果在构造函数中抛出异常,对象不会被视为完全构造完成。
    • 因此,析构函数不会被调用。
  2. 对象状态

    • 对象 a 在构造函数抛出异常后不会被完全构造完成,因此它在 catch 块中不存在完全构造的状态。
    • 析构函数不会被调用,导致 _ptr1 没有被释放,从而产生内存泄漏。

示例二:析构函数:

double Division(int a, int b)
{if (b == 0)throw "Division by zero condition!"; // 抛出异常return (double)a / (double)b;
}class AA
{
public:AA(){_ptr1 = new int;_ptr2 = new int;std::cout << "AA()" << std::endl;}~AA(){delete _ptr1;Division(1, 0);delete _ptr2;std::cout << "~AA()" << std::endl;}
private:int* _ptr1;int* _ptr2;
};int main()
{try{AA a;}catch (...){std::cout << "处理异常" << std::endl;}return 0;
}

代码分析:

  1. 析构函数中的异常抛出

    • 析构函数中调用了 Division(1, 0),如果 b == 0,则抛出异常。
    • 一旦析构函数抛出异常,程序会终止,并且不会执行析构函数中剩余的代码。
  2. 资源释放

    • 析构函数中的异常导致 _ptr2 没有被删除,从而导致内存泄漏。
    • 这是不推荐的做法,因为析构函数应该始终正确地释放资源,而不会抛出异常。

示例三:及时析构

错误示例:

#include <iostream>double Division(int a, int b)
{if (b == 0)throw "Division by zero condition!";return (double)a / (double)b;
}
void Func()
{int* arr = new int[10];int len, time;std::cin >> len >> time;std::cout << Division(len, time) << std::endl;// ...std::cout << "delete[]" << arr << std::endl;delete[] arr;
}int main()
{try {Func();}catch(const char* errmsg) {std::cout << errmsg << std::endl;}return 0;
}

此时如果Division() 函数执行时抛出异常,就会导致 arr 申请的资源没有被释放,造成内存泄漏。

正确示例:

void Func()
{int* arr = new int[10];try {int len, time;std::cin >> len >> time;std::cout << Division(len, time) << std::endl;}catch (...) {std::cout << "delete[] " << arr << std::endl;delete[] arr;throw;}// ...std::cout << "delete[]" << arr << std::endl;delete[] arr;
}

示例四:手动处理异常的缺点

如果遇到以下场景,处理异常就会比较棘手:

void Func()
{int* arr1 = new int[10];int* arr2 = new int[20];int* arr3 = new int[30];// ...delete[] arr1;delete[] arr2;delete[] arr3;
}

这里 arr1 arr2 arr3 在使用 new 申请空间时可能会抛出异常(当空间不足时),这样就有四种情况:

  • arr1 抛出异常,不需要释放空间。
  • arr2 抛出异常,需要释放 arr1 申请的空间。
  • arr3 抛出异常,需要释放 arr1arr2 申请的空间。
  • 没有异常抛出,需要释放 arr1arr2arr3 申请的空间。

那我们在处理异常时,就需要像下面类似写法,才能保证异常安全:

void Func()
{int* arr1 = new int[10];int* arr2;int* arr3;try{arr2 = new int[20];try{arr3 = new int[30];}catch(...){delete[] arr1;delete[] arr2;throw;}}catch(...){delete[] arr1;throw;}// 没有异常delete[] arr1;delete[] arr2;delete[] arr3;
}

这样处理起来会比较麻烦,其实通过 智能指针 可以很好地解决这个问题。

在没有设计出 智能指针 时,为了解决这个问题,就提出了 异常规范 的概念。

异常规范

C++98

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常类型。这可以通过在函数后面接 throw(类型列表) 来实现,列出函数可能抛出的所有异常类型。
  2. 如果一个函数声明为 throw(),表示该函数不会抛出任何异常。
  3. 如果一个函数没有声明异常规范,则此函数可以抛掷任何类型的异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
问题

尽管异常规范在C++98中提供了对异常抛出类型的声明,但它存在一些问题:

  1. 开发人员误用

    • 如果开发人员声明了一个函数不会抛出异常(throw()),但实际上该函数却抛出了异常,编译器只会发出警告而不是错误。
  2. 内部函数抛出异常

    • 即使函数本身没有抛出异常,但如果它调用了其他可能抛出异常的函数,那么整个函数仍然可能抛出异常。
  3. 非强制性

    • 异常规范只是一个建议,编译器不会强制执行。
      但是这个规范在实际应用中,还存在很多问题:

开发人员认为该函数不会抛异常 throw(),但实际上却抛出了异常

double Division(int a, int b) throw()
{if (b == 0)throw "Division by zero condition!";return (double)a / (double)b;
}int main()
{try {Division(1, 0);}catch(const char* errmsg) {std::cout << errmsg << std::endl;}return 0;
}

此时编译器不会报错,而只是警告(有些程序员会不处理警告>﹏<),例如:

 warning C4297: “Division”: 假定函数不引发异常,但确实发生了

函数本体没有出现异常,但内部函数抛出了异常。

double Division(int a, int b) throw(const char*)
{if (b == 0)throw "Division by zero condition!";return (double)a / (double)b;
}void func() throw()
{// ...Division(1, 0);
}int main()
{try {func();}catch(const char* errmsg) {std::cout << errmsg << std::endl;}return 0;
}

此时编译器不会报错,而只是警告( ̄へ ̄),例如:

warning C4290: 忽略 C++ 异常规范,但指示函数不是 __declspec(nothrow)

并且这个异常规范只是一个建议,并不是强制的,所以这个异常规范并不能很好地解决异常安全的问题。

C++11

C++11 引入了 noexcept 关键字来替代旧的异常规范,并且提供了更好的语义和性能优化。

即, 对于可能会抛异常的函数,什么都不要写。确定不抛异常的函数,要加一个 noexcept

1. noexcept 的使用

noexcept 关键字用于声明一个函数不会抛出异常。如果一个函数被声明为 noexcept,那么编译器可以对其进行优化,例如减少异常处理的开销。

2. 语法
// 不会抛出异常
void thread() noexcept;
void thread(thread&& x) noexcept;

如果一个函数没有显式声明 noexcept,则默认行为是它可以抛出任何类型的异常。

3. 与 throw() 的区别
  • noexcept 更加明确地表达了函数不会抛出异常的意图。
  • noexcept 可以用在表达式中,例如 noexcept(expr),用于检测一个表达式是否会抛出异常。
  • noexcept 提供了更好的语义,并且编译器可以利用这一信息进行优化。
4. 使用 noexcept 表达式
#include <iostream>
#include <stdexcept>bool isSafe(int a, int b) noexcept {return b != 0;
}double safeDivision(int a, int b) noexcept(isSafe(a, b)) {return static_cast<double>(a) / static_cast<double>(b);
}int main() {try {std::cout << "Result: " << safeDivision(1, 0) << std::endl;} catch (const std::exception& e) {std::cerr << "Caught exception: " << e.what() << std::endl;}return 0;
}

总结

  1. C++98 中的异常规范

    • 使用 throw(类型列表) 来声明函数可能抛出的异常类型。
    • 使用 throw() 表示函数不会抛出异常。
    • 但存在开发人员误用和内部函数抛出异常的问题。
  2. C++11 中的 noexcept

    • 更加明确地表达了函数不会抛出异常的意图。
    • 可以用在表达式中,例如 noexcept(expr)
    • 提供了更好的语义,并且编译器可以利用这一信息进行优化。

通过使用 noexcept,可以更好地表达函数的异常抛出特性,并且允许编译器进行更有效的优化。这使得代码更易于理解和维护。

标准库异常体系

C++标准库提供了一系列标准的异常类,这些类定义在<exception>头文件中,并以父子类层次结构组织起来。主要的异常类包括:

  • std::exception:所有标准异常类的基类,提供了what()方法用于返回异常的描述信息。
  • std::bad_alloc:当内存分配失败时抛出的异常。
  • std::bad_cast:在使用dynamic_cast进行向下转换时,如果转换失败则抛出此异常。
  • std::bad_typeid:在使用typeid运算符时,如果操作数是一个多态类型对象的指针或引用,但该对象不是当前处理的多态类型或其派生类型之一,则抛出此异常。
  • std::bad_function_call:当尝试调用一个空的std::function对象时抛出的异常。
  • std::invalid_argument:当传递给函数的参数无效时抛出的异常。
  • std::out_of_range:当尝试访问超出有效范围的元素时抛出的异常(如std::vector的越界访问)。
    在这里插入图片描述

异常的优缺点

优点

  • 清晰的错误信息:异常对象可以包含丰富的错误信息,相比错误码的方式可以清晰准确的展示出错误的各种信息,有助于快速定位问题。
  • 便于错误传播:异常可以自动向上层传播,直到找到相应的处理代码,减少了错误处理的代码量。
  • 结构化错误处理:通过trycatchthrow关键字,可以结构化地处理错误情况,提高了代码的可读性和可维护性。
  • 第三方库的支持:很多第三方库(如 Boost、Google Test、Google Mock 等)都内置了异常处理机制,如果你使用这些库,那么自然也需要使用异常来处理错误情况。
  • 部分函数使用异常会更好处理,比如构造函数没有返回,不方便使用错误码方式处理。比如T& operator 这样的函数,如果 pos 越界了只能使用异常或者终止程序处理(使用错误码不方便表示),没办法通过返回值表示错误。

返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,我么需要层层返回错误,最外层才能拿到错误信息,如:

int ConnnectSql()
{// 用户名密码错误if (...)return 1;// 权限不足if (...)return 2;
}
int ServerStart() {if (int ret = ConnnectSql() < 0)return ret;int fd = socket()if(fd < 0return errno;
}
int main()
{if (ServerStart() < 0)...return 0;
}

代码分析:

  1. 这段伪代码我们可以看到 ConnnectSql 中出错了,先返回给 ServerStartServerStart 再返回给main函数,main函数再针对问题处理具体的错误。
  2. 如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。

缺点

  • 执行流混乱:异常可能导致程序的执行流变得难以预测,增加了调试的难度。当异常被抛出时,程序会立即跳转到最近的 catch 块,这打破了正常的控制流。这种“乱跳”使得程序的逻辑变得难以追踪,尤其是在复杂的多层嵌套调用中。
  • 性能开销:异常处理机制本身需要一定的性能开销,特别是在频繁抛出和捕获异常的情况下。
  • 资源管理问题:在异常发生时,如果资源(如内存、文件句柄等)没有得到正确释放,可能会导致资源泄漏等问题。因此,在使用异常时需要注意资源的管理和释放。
  • 标准库异常体系定义不完善:C++标准库的异常体系定义并不完善,导致开发者往往需要自己定义异常体系,这容易造成混乱。通常,团队内部或项目中采用统一的异常体系,减少混乱。
  • 异常规范:不规范的异常使用会导致代码难以维护和理解。为了保证代码质量,需要遵循一定的异常规范。

总结

尽管C++中的异常处理机制存在一些潜在的缺点,但其提供的清晰的错误信息、便于错误传播、结构化错误处理以及对特殊函数的支持,使得异常处理在许多场景下都是一个非常有用的工具。通过合理使用异常处理,并结合RAII等技术,可以编写出既健壮又易维护的代码。


今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/435070.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RTX 5090、5080规格完整曝光,来看来看

近日&#xff0c;科技圈内再掀波澜&#xff0c;有知名博主独家揭秘了英伟达即将推出的RTX 5090与RTX 5080两款高端显卡的详尽规格&#xff0c;预示着显卡市场即将迎来新一轮的性能飞跃与定位分化。 据最新披露的信息&#xff0c;这两款显卡均采用了先进的PG144/145-SKU30 PCB设…

如何借助Java批量操作Excel文件?

最新技术资源&#xff08;建议收藏&#xff09; https://www.grapecity.com.cn/resources/ 前言 | 问题背景 在操作Excel的场景中&#xff0c;通常会有一些针对Excel的批量操作&#xff0c;批量的意思一般有两种&#xff1a; 对批量的Excel文件进行操作。如导入多个Excel文件…

若依--Request.js

编写一个request.js的基本类&#xff0c;封装一些信息&#xff0c;比如请求地址、响应时间、携带的token参数等等。 //创建一个axios实列这里的 import.meta.env.VITE_APP_BASE_API 表示这个基础 URL 的值来自于环境变量。通常&#xff0c;这种做法用于将不同环境&#xff08;…

vue3结合 vue-router和keepalive实现路由跳转保持滚动位置不改变(超级简易清晰)

1.首先我们在路由跳转页面设置keepalive(Seeall是我想实现结果的页面) 2. 想实现结果的页面中如果不是全屏实现滚动而是有单独的标签实现滚动效果

docker - 迁移和备份

文章目录 1、docker commit1.1、查询 容器 docker ps1.2、docker commit zookeeper zookeeper:3.4.13 2、docker save -o2.1、宿主机 切换到 /opt 目录下2.2、将镜像保存到 宿主机/opt目录下 3、docker load -i 对某一个容器修改完毕以后&#xff0c;我们可以把最新的容器部署到…

HTML5实现好看的唐朝服饰网站模板源码2

文章目录 1.设计来源1.1 网站首页1.2 唐装演变1.3 唐装配色1.4 唐装花纹1.5 唐装文化 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.ne…

Spring Boot实战:构建在线商城系统

1 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;规范化管理。这样的大环境让那些止步不前&#…

iLogtail 进化论:重塑可观测采集的技术边界

作者&#xff1a;余韬(迅飞) 采集代理发展回顾 iLogtail 作为一款开创性的轻量级日志采集器&#xff0c;历经 13 载风雨&#xff0c;始终致力于高效地从多元化的数据源中萃取、处理可观测信息&#xff0c;并无缝传输至阿里云日志服务或各类日志分析平台。今年&#xff0c;适逢…

矩阵奇异值

一、ATA 任给一个矩阵A&#xff0c;都有&#xff1a; ATA 为一个对称矩阵 例子&#xff1a;A为一个mn的矩阵&#xff0c;A的转置为一个nm的矩阵 对称矩阵的重要性质如下&#xff1a; ① 对称矩阵的特征值全为实数&#xff08;实数特征根&#xff09; ② 任意一个n阶对称矩阵…

《黑神话:悟空》天命人速通法宝 | 北通鲲鹏20智控游戏手柄评测

《黑神话:悟空》天命人速通法宝 | 北通鲲鹏20智控游戏手柄评测 哈喽小伙伴们好&#xff0c;我是Stark-C~ 截止目前&#xff0c;《黑神话:悟空》已经面世一个多月&#xff0c;不知道还有多少天命人没有通关呢&#xff1f; 作为国内首款真正意义上的3A大作&#xff0c;《黑神话…

实验一 网络基础及仿真模拟软件Packet Tracer 入门

实验一 网络基础及仿真模拟软件Packet Tracer 入门 【实验目的】 一、认识 Packet Tracer 。 二、学习使用 Packet Tracer 进行拓扑的搭建。 三、学习使用 Packet Tracer 对设备进行配置&#xff0c;并进行简单的测试。 【实验内容和结果】 一、拖放设备和布置线缆 二、用…

Redis系列补充:聊聊布隆过滤器(go语言实践篇)

1 介绍 布隆过滤器&#xff08;Bloom Filter&#xff09;是 Redis 4.0 版本之后提供的新功能&#xff0c;我们一般将它当做插件加载到 Redis Service服务器中&#xff0c;给 Redis 提供强大的滤重功能。 它是一种概率性数据结构&#xff0c;可用于判断一个元素是否存在于一个集…

vscode 顶部 Command Center,minimap

目录 vscode 顶部 Command Center 设置显示步骤: minimap设置 方法一:使用设置界面 方法二:使用命令面板 方法三:编辑 settings.json 文件 左侧目录树和编辑器字体不一致: OPEN EDITORS vscode 顶部 Command Center Visual Studio Code (VSCode) 中的 Command Ce…

高胜率TPS交易策略:轻松应对市场波动

原本基于美国经济数据&#xff0c;市场预期美联储不会那么迅速放宽货币政策&#xff0c;然而&#xff0c;最新美联储官员的表态却显著提升了市场对于加速降息的预期。只能说市场果然没有那么好预测呀&#xff0c;作为交易者&#xff0c;咱们只能不断提升自己的技术&#xff0c;…

掌握流程图设计:5款高效流程图软件推荐

在现代办公环境中&#xff0c;流程图制作软件是提高工作效率和组织能力的重要工具。无论是用于项目管理、业务流程优化&#xff0c;还是技术文档编写&#xff0c;流程图都能帮助我们更清晰地理解和传达复杂的信息。然而&#xff0c;面对市面上琳琅满目的流程图制作软件&#xf…

Java零工市场小程序如何改变自由职业者生活

如今&#xff0c;自由职业者越来越多&#xff0c;他们需要找到合适的工作机会&#xff0c;Java零工市场小程序&#xff0c;为自由职业者提供了一个方便、快捷的寻找工作机会的方式&#xff0c;这样一来&#xff0c;改变了自由职业者找寻工作的方式&#xff0c;也提高了他们的收…

【WPF】桌面程序开发之窗口的用户控件详解

使用Visual Studio开发工具&#xff0c;我们可以编写在Windows系统上运行的桌面应用程序。其中&#xff0c;WPF&#xff08;Windows Presentation Foundation&#xff09;项目是一种常见的选择。然而&#xff0c;对于初学者来说&#xff0c;WPF项目中xaml页面的布局设计可能是一…

Type-C接口桌面显示器的优势

随着科技的飞速发展&#xff0c;电子设备的连接性、便捷性和高效性成为了消费者关注的重点。在这个背景下&#xff0c;Type-C接口桌面显示器以其卓越的性能和广泛的兼容性&#xff0c;正逐步成为市场上的主流选择。本文将深入探讨Type-C接口桌面显示器的优势、应用场景、市场现…

【期刊】论文索引库-SCI\SSCI\IE\南大核心\北大核心\CSCD等

外文期刊检索 SCI SCI即《科学引文索引》(Science Citation Index),是由美国科学信息研究所(Institute for Scientific Information)创建于1961年,收录文献的作者、题目、源期刊、摘要、关键词,不仅可以从文献引证的角度评估文章的学术价值,还可以迅速方便地组建研究课…

17年数据结构考研真题解析

第一题&#xff1a; 解析&#xff1a; 我们说递归要找出口&#xff0c;这道题的出口是sum<n&#xff0c;经过观察可以得知&#xff1a;sum123。。。k 设第k次循环跳出&#xff0c;则有sum123。。。k<n k<,很显然答案选B 第二题&#xff1a; 解析&#xff1a; 第一句&a…