掘根教你拿捏C++异常(try,catch,throw,栈解退,异常规范,异常的重新抛出)

news/2024/4/15 5:07:58/文章来源:https://blog.csdn.net/2301_80224556/article/details/136547467

在介绍异常之前,我觉得很有必要带大家了解一下运行时错误和c++异常出现之前的处理运行时错误的方式。这样子能更深入的了解异常的作用和工作原理

运行阶段错误

我们知道,程序有时候会遇到运行阶段错误,导致程序无法正常运行下去

C++在运行时可能会出现多种错误,这些错误被称为运行时错误或异常。

以下是一些常见的C++运行时错误:

  1. 数组越界:当程序尝试访问数组的索引超过数组范围时,会发生数组越界错误。这可能导致程序崩溃或产生未定义行为。

  2. 空指针引用:当程序试图访问一个空指针指向的内存位置时,会发生空指针引用错误。这通常是因为没有正确初始化指针或者指针被删除后仍然被引用。

  3. 除以零:当程序试图执行除以零的操作时,会发生除以零错误。这通常会导致程序崩溃或产生未定义行为。

  4. 内存泄漏:当程序分配了内存但没有正确释放时,会发生内存泄漏。如果内存泄漏问题严重,会导致程序在运行过程中耗尽可用内存而崩溃。

  5. 类型转换错误:当程序试图执行非法或不兼容的类型转换时,会发生类型转换错误。这可能导致程序产生不正确的结果或崩溃。

  6. 文件操作错误:当程序试图打开一个不存在的文件、读取写入超过文件范围的数据或者在不允许的情况下对文件进行操作时,会发生文件操作错误。

为了处理这些运行时错误,可以使用C++的异常机制。通过捕获和处理异常,可以使程序在出现错误时能够优雅地处理,并在需要时进行错误恢复或退出。

异常出现之前的处理错误的方式

讨论异常之前,先来看看程序员可使用的一些处理运行时错误的方法

先来看一个除0错误

cout<<7/x;

如果x=0,这条语句就变成了7/0,但是我们知道0是不能做被除数的

如果我们不做任何检查和处理,看看编译器会怎么处置这么一种情况

对于被0除的情况,很多新式编译器通过生成一个表示无穷大的特殊浮点值来处理,cout将这种值显示为Inf,inf,INF或者类似的东西;

而其他编译器可能生成在发生被0除时崩溃的程序。

那我们怎么来防止这种错误的发生呢?有两种最常用的方法

调用abort()

我们先来了解一下abort函数吧!

abort()简介

在C++中,abort()函数用于终止程序的执行。

abort()函数位于头文件<cstdlib>中,函数原型如下:

void abort (void);

调用abort()函数会导致程序立即终止,并生成一个异常终止信号并将其发送到标准错误流。

程序的终止是非正常的,它不会执行任何的析构函数、清理操作等。

通常情况下,abort()函数被用于处理严重错误或异常情况,强制终止程序以避免进一步的损害。

示例使用abort()函数:

#include <cstdlib>int main() {int a = 0;if (a == 0) {abort();  // 如果 a 等于 0,强制终止程序}return 0;
}

在上面的示例中,如果a的值等于0,则调用abort()函数终止程序的执行。

运行结果如下

 解决问题

对于上面那个问题,处理的方式之一就是,如果x==0,就调用abort函数

#include<iostream>
using namespace std;
#include <cstdlib>int main() {int a;scanf("%d", &a);if (a == 0) {abort();  // 如果 a 等于 0,强制终止程序}cout<< 7 / a;return 0;
}

我们输入1,运行结果是

 

我们输入0,运行结果是

返回错误码

一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。

比如ostream类的get()成员通常返回下一个输入字符的ASCII码,但到文件尾时,将返回特殊值EOF.

我们还可以看个更通俗易懂的例子

#include <iostream>int divide(int a, int b) {if (b == 0) {// 返回错误码 1,表示除数为零错误return 1;}// 执行除法操作并返回结果return a / b;
}int main() {int a = 10;int b = 0;int result = divide(a, b);if (result == 0) {// 处理除法操作成功的情况std::cout << "除法操作结果:" << result << std::endl;} else {// 处理除数为零错误std::cout << "除数为零错误!" << std::endl;}return 0;
}

在上面的示例中,divide()函数用于执行两数相除操作,并检查除数是否为零。如果除数为零,则返回错误码1,否则返回相除的结果。在main()函数中,首先将10除以0,然后根据返回的错误码来判断函数执行的状态。如果返回的错误码为0,则说明除法操作成功,可以打印结果。否则,说明除数为零错误,可以相应地处理错误情况。

 需要注意的是,上面的示例只是简单示例,实际应用中可能有更复杂的错误码及错误处理机制。

解决问题

对于上面那个问题,我们直接定义一个判断函数即可

#include<iostream>
using namespace std;int A(int a)
{if (a == 0)return 0;elsereturn 1;
}
int main() {int a;scanf("%d", &a);int b = A(a);if(b==1)cout<< 7 / a;elsecout << "a不能为0" << endl;}

这样子就解决了 

C++异常概念

相信看完上面的例子,你已经知道异常的作用大概是什么了

异常是相对较新的C++功能,有些老式编译器可能没有实现。另外有些编译器默认关闭这种特性,你可能需要使用编译器选项来启用它

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

对异常的处理有3部分组成

  • 引发异常(throw)
  • 使用处理程序捕获异常(catch)
  • 使用try块(try)

throw

当程序出现问题时,可以通过throw关键字抛出一个异常。

throw关键字表示引发异常,紧随其后的值指出了异常的特征,它的类型也叫异常类型

异常类型可以是任何类型,但是通常是类类型

throw语句实际上是跳转,即命令程序跳到另一条语句(跳到catch块)。

throw通常会被放在一个函数里,而这个函数通常被放在try块里面,执行throw语句类似于执行返回语句,因为它也会终止函数的执行,

但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数,把控制权交回给这个函数。然后再在有控制权的这个函数里寻找与引发异常类型匹配的异常处理程序(即catch块)

throw异常;

catch

如果try块中发生错误,则可以在catch块中定义对应要执行的代码块。

程序使用异常处理程序(也叫catch块)来捕获异常,异常处理程序位于要处理问题的程序中。

异常处理程序以关键字catch开头,随后是位于括号的类型声明,它指出了这个catch块要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。

catch(这个catch块对应的异常类型)
{
处理异常的措施
}

这样子感觉有点像函数定义啊,但是它不是函数定义 

catch关键字和异常类型用作标签,指出当异常被引发时(即执行了throw语句后),程序应该跳到这个位置执行。

try

try块是由关键字try指示的,关键字try后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常(即表明这个代码块里有throw语句)

try
{
//里面含有throw语句
}

该代码块在执行时将进行异常错误检测,try块里面通常含有throw语句,后面通常跟着一个或多个catch块。

如下所示:

try
{
含throw语句    
//被保护的代码
}
catch (ExceptionName e1)
{//catch块
}
catch (ExceptionName e2)
{//catch块
}
catch (ExceptionName eN)
{//catch块
}

使用介绍

我们还是以上面那个除0错误为例来展示异常机制 

#include<iostream>
using namespace std;int A(int a)
{if (a == 0)throw"a不能为0";elsereturn a;
}
int main() {int a;scanf("%d", &a);try{int b = A(a);}catch (const char* s){cout << s << endl;}cout << 7 / a;}

我们来看看这里面的机制是怎么样的呢?

如果输入0,a被传进入A函数,触发了throw语句,throw将其后面的字符串抛出,回到try块下面开始寻找与字符串类型相符合的catch块,然后执行其中的内容

我们如果输入1,a被传进A函数,并没有触发throw语句,所以程序将跳过try块后面的catch块,直接处理程序后面的第一条语句

执行throw语句类似于执行返回语句,因为它也会终止函数的执行,

但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数,把控制权交回给这个函数。然后再在有控制权的这个函数里寻找与引发异常类型匹配的异常处理程序(即catch块)

在这个例子中throw将程序控制权返回给main函数,程序将在main函数寻找与引发异常类型匹配的异常处理程序(即catch块)

异常的用法

异常的抛出和捕获

异常的抛出和捕获的匹配原则:

准则一

异常是通过抛出异常类型(通常是对象)而引发的,该异常类型(通常是对象)决定了应该激活哪个catch的处理代码,如果抛出的异常对象没有捕获,或是没有匹配类型的捕获,那么程序会终止报错。

相匹配的例子

#include<iostream>
using namespace std;
int A(int a)
{if (a == 0)throw"a不能为0";elsereturn a;
}
int main() {int a;scanf("%d", &a);try{int b = A(a);}catch (const int* s){cout << "第一个" << endl;}catch (const char* s){cout << "第二个" << endl;}cout << 7 / a;
}

没有相匹配的例子

#include<iostream>
using namespace std;
int A(int a)
{if (a == 0)throw"a不能为0";elsereturn a;
}
int main() {int a;scanf("%d", &a);try{int b = A(a);}catch (const int* s){cout << "第一个" << endl;}catch (const float* s){cout << "第二个" << endl;}cout << 7 / a;
}

准则二

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

看个例子

#include<iostream>
using namespace std;int A(int a)
{if (a == 0)throw"a不能为0";elsereturn a;
}
int main() {int a;scanf("%d", &a);try{int b = A(a);}catch (const char* s){cout << "第一个" << endl;}catch (const char* s){cout << "第二个" << endl;}cout << 7 / a;}

异常只能被第一个捕获

准则三

抛出异常对象后,编译器总会生成一个异常对象的拷贝,即使异常规范和catch块指定的是引用,这个拷贝的临时对象会在被catch以后销毁。(类似于函数的传值返回)

class AA{...}
...
void A()
{
AA a;
if(...)
{
throw a;
}
...
}try
{
A();
}
catch(AA&t)
{...}

t将指向a的副本而不是a本身。这是件好事,因为函数A执行完后a将不复存在。 

准则四

catch(...)可以捕获任意类型的异常,但捕获后无法知道异常错误是什么。

catch(...)通常被放在众多catch块的最后面,防止程序因为没有相匹配的异常类型而导致停止运行

#include<iostream>
using namespace std;
int A(int a)
{if (a == 0)throw"a不能为0";elsereturn a;
}
int main() {int a;scanf("%d", &a);try{int b = A(a);}catch (char a){cout << "第一个" << endl;}catch (...){cout << "未知错误" << endl;}cout << 7 / a;
}

准则五

实际异常的抛出和捕获的匹配原则有个例外,捕获和抛出的异常类型并不一定要完全匹配,可以抛出派生类对象,使用基类进行捕获,这个在实际中非常有用。但是也会带来一些问题。

#include<iostream>
using namespace std;
class AA
{};
class BB:public AA
{};
class CC:public BB
{};
int A(int a)
{CC m;if (a == 0)throw m;elsereturn a;
}
int main() {int a;scanf("%d", &a);try{int b = A(a);}catch (AA&t)//改成BB&t或者CC&t也可以匹配到{cout << "第一个" << endl;}cout << 7 / a;
}

  

使用基类引用则可以捕获抛出的该基类和所有派生类的异常,而使用派生类引用则只能捕获它所属类及从这个类派生来的类对象。

而引发的异常对象将先和第一个与之匹配的catch块捕获。这意味着catch块的排列顺序应该和派生顺序相反。

如果有一个异常类继承层次结构,应该这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面

#include<iostream>
using namespace std;
class AA
{};
class BB:public AA
{};
class CC:public BB
{};
int A(int a)
{BB m;if (a == 0)throw m;elsereturn a;
}
int main() {int a;scanf("%d", &a);try{int b = A(a);}catch (CC&t){cout << "第一个" << endl;}catch (BB& t)//改成BB&t或者CC&t也可以匹配到{cout << "第二个" << endl;}catch (AA& t){cout << "第三个" << endl;}cout << 7 / a;
}

 栈解退

假设 try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。

这涉及到栈解退(unwinding the stack),下面进行介绍。

c++函数调用和返回

首先来看一看C++通常是如何处理函数调用和返回的。

C++通常通过将信息放在栈中来处理函数调用。

具体地说,程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行

另外,函数调用将函数参数放到栈中。在栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到栈中,依此类推。

当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。

因此,函数通常都返回到调用它的函数,依此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。

栈解退

现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序(即catch块),而不是函数调用后面的第一条语句。

这个过程被称为栈解退。

引发机制一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用放在栈中的对象。(功能基本相同,范围不同)

如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。

 

栈解退的基本过程

  1. 当异常被抛出后,首先检查throw本身是否在try块内部,编译器就要先找到try块在的那个函数栈里,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。
  2. 如果当前函数栈没有找到try块,则退出当前函数栈,继续在上一个调用函数栈中进行查找try块。依次类推,直到找到了try语句,然后在try块下面,找到匹配的catch子句并处理以后,会沿着catch子句后面继续执行。
  3. 如果到达main函数的栈,依旧没有找到try块,则终止程序。

比如下面的代码中main函数中调用了func3,func3中调用了func2,func2中调用了func1,在func1中抛出了一个string类型的异常对象。

void func1()
{throw string("这是一个异常");
}
void func2()
{func1();
}
void func3()
{func2();
}
int main()
{try{func3();}catch (const string& s){cout << "错误描述:" << s << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}


当func1中的异常被抛出后:

首先会检查throw本身是否在try块内部,这里由于try块不在func1函数内,因此会退出func1所在的函数栈,继续在上一个调用函数栈中进行查找,即func2所在的函数栈。
由于func2中也没有找到try块,因此会继续在上一个调用函数栈中进行查找,即func3所在的函数栈。
func3中也没有找到try块,于是就会在main所在的函数栈中进行查找,最终在main函数栈中找到了try块,然后在try块后面寻找匹配的catch。
这时就会跳到main函数中对应的catch块中执行对应的代码块,执行完后继续执行该代码块后续的代码

异常的重新抛出

有时候单个的catch可能不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,比如最外层可能需要拿到异常进行日志信息的记录,这时就需要通过重新抛出将异常传递给更上层的函数进行处理。

但如果直接让最外层捕获异常进行处理可能会引发一些问题。比如:

void func1()
{throw string("这是一个异常");
}
void func2()
{int* array = new int[10];func1();//do something...delete[] array;
}
int main()
{try{func2();}catch (const string& s){cout << s << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}


其中func2中通过new操作符申请了一块内存空间,并且在func2最后通过delete对该空间进行了释放,但由于func2中途调用的func1内部抛出了一个异常,这时会直接跳转到main函数中的catch块执行对应的异常处理程序,并且在处理完后继续沿着catch块往后执行。

这时就导致func2中申请的内存块没有得到释放,造成了内存泄露。这时可以在func2中先对func1抛出的异常进行捕获,捕获后先将申请到的内存释放再将异常重新抛出,这时就避免了内存泄露。比如:

void func2()
{int* array = new int[10];try{func1();//do something...}catch (...){delete[] array;throw; //将捕获到的异常再次重新抛出}delete[] array;
}



说明一下:

func2中的new和delete之间可能还会抛出其他类型的异常,因此在fun2中最好以catch(...)的方式进行捕获,将申请到的内存delete后再通过throw重新抛出。
重新抛出异常对象时,throw后面可以不用指明要抛出的异常对象(正好也不知道以catch(...)的方式捕获到的具体是什么异常对象)。

异常安全

将抛异常导致的安全问题叫做异常安全问题,对于异常安全问题下面给出几点建议:

  1. 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
  2. 析构函数主要完成对象资源的清理,最好不要在析构函数中抛出异常,否则可能导致资源泄露(内存泄露、句柄未关闭等)。
  3. C++中异常经常会导致资源泄露的问题,比如在new和delete中抛出异常,导致内存泄露,在lock和unlock之间抛出异常导致死锁,C++经常使用RAII的方式来解决以上问题。

异常规范

为了让函数使用者知道某个函数可能抛出哪些类型的异常,C++标准规定:

在函数的后面接throw(type1, type2, ...),列出这个函数可能抛掷的所有异常类型。

在函数的后面接throw()或noexcept(C++11),表示该函数不抛异常。

若无异常接口声明,则此函数可以抛掷任何类型的异常。(异常接口声明不是强制的)

比如:

//表示func函数可能会抛出A/B/C/D类型的异常
void func() throw(A, B, C, D);
//表示这个函数只会抛出bad_alloc的异常
void* operator new(std::size_t size) throw(std::bad_alloc);
//表示这个函数不会抛出异常
void* operator new(std::size_t size, void* ptr) throw();



自定义异常体系


实际中很多公司都会自定义自己的异常体系进行规范的异常管理。

公司中的项目一般会进行模块划分,让不同的程序员或小组完成不同的模块,如果不对抛异常这件事进行规范,那么负责最外层捕获异常的程序员就非常难受了,因为他需要捕获大家抛出的各种类型的异常对象。

因此实际中都会定义一套继承的规范体系,先定义一个最基础的异常类,所有人抛出的异常对象都必须是继承于该异常类的派生类对象,因为异常语法规定可以用基类捕获抛出的派生类对象,因此最外层就只需捕获基类就行了。


最基础的异常类至少需要包含错误编号和错误描述两个成员变量,甚至还可以包含当前函数栈帧的调用链等信息。该异常类中一般还会提供两个成员函数,分别用来获取错误编号和错误描述。比如:

class Exception
{
public:Exception(int errid, const char* errmsg):_errid(errid), _errmsg(errmsg){}int GetErrid() const{return _errid;}virtual string what() const{return _errmsg;}
protected:int _errid;     //错误编号string _errmsg; //错误描述//...
};


其他模块如果要对这个异常类进行扩展,必须继承这个基础的异常类,可以在继承后的异常类中按需添加某些成员变量,或是对继承下来的虚函数what进行重写,使其能告知程序员更多的异常信息。比如:

class CacheException : public Exception
{
public:CacheException(int errid, const char* errmsg):Exception(errid, errmsg){}virtual string what() const{string msg = "CacheException: ";msg += _errmsg;return msg;}
protected://...
};
class SqlException : public Exception
{
public:SqlException(int errid, const char* errmsg, const char* sql):Exception(errid, errmsg), _sql(sql){}virtual string what() const{string msg = "CacheException: ";msg += _errmsg;msg += "sql语句: ";msg += _sql;return msg;}
protected:string _sql; //导致异常的SQL语句//...
};


说明一下:

异常类的成员变量不能设置为私有,因为私有成员在子类中是不可见的。
基类Exception中的what成员函数最好定义为虚函数,方便子类对其进行重写,从而达到多态的效果。

异常的优缺点

异常的优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用等信息,这样可以帮助更好的定位程序的bug。
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误码,最终最外层才能拿到错误。
  3. 很多的第三方库都会使用异常,比如boost、gtest、gmock等等常用的库,如果我们不用异常就不能很好的发挥这些库的作用。
  4. 很多测试框架也都使用异常,因此使用异常能更好的使用单元测试等进行白盒的测试。
  5. 部分函数使用异常更好处理,比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

异常的缺点:

C++异常的缺点包括以下几点:

  1. 性能开销:在抛出异常时,C++需要执行一些额外的操作,比如堆栈展开和资源清理。这些操作可能会导致性能下降,尤其是在频繁出现异常的情况下。

  2. 不适合底层开发:在底层开发中,对性能要求非常高,因此异常处理可能不适用。异常处理需要一定的处理逻辑和系统开销,这对于低级别的系统编程来说可能是不可接受的。

  3. 可能引发资源泄露:如果异常没有被正确处理,可能会导致资源泄露。如果在异常发生时没有及时释放资源,可能会导致内存泄露、文件句柄泄露等问题。

  4. 可能引发不确定行为:在异常发生时,如果没有适当的处理,程序可能会进入不确定的状态。这可能导致程序崩溃、数据损坏或其他不可预测的行为。

  5. 不够直观:异常处理可能使代码变得复杂,不够直观。异常的处理逻辑通常分散在代码中的多个地方,这可能使代码难以阅读和维护。

  6. 可能导致资源泄漏:当程序在异常处理过程中退出时,可能会导致未释放的资源,导致资源泄漏。这是因为异常处理通常是在函数调用堆栈展开时进行的,当异常处理结束后,函数调用堆栈将不再展开,从而导致资源泄漏。

  7. 异常会导致程序的执行流乱跳,并且非常的混乱,这会导致我们跟踪调试以及分析程序时比较困难。

总的来说,C++异常处理机制在某些情况下可能会导致性能下降、不确定行为和资源泄露等问题。因此,在使用异常处理时需要谨慎,并在适当的情况下选择合适的替代方案。

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

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

相关文章

Leetcode - 周赛387

目录 一&#xff0c;3069. 将元素分配到两个数组中 I 二&#xff0c;3070. 元素和小于等于 k 的子矩阵的数目 三&#xff0c;3071. 在矩阵上写出字母 Y 所需的最少操作次数 四&#xff0c;3072. 将元素分配到两个数组中 II 一&#xff0c;3069. 将元素分配到两个数组中 I 本…

消息队列-kafka-消息发送流程(源码跟踪) 与消息可靠性

官方网址 源码&#xff1a;https://kafka.apache.org/downloads 快速开始&#xff1a;https://kafka.apache.org/documentation/#gettingStarted springcloud整合 发送消息流程 主线程&#xff1a;主线程只负责组织消息&#xff0c;如果是同步发送会阻塞&#xff0c;如果是异…

【Sql Server】存储过程的创建和使用事务,常见运用场景,以及目前现状

欢迎来到《小5讲堂》&#xff0c;大家好&#xff0c;我是全栈小5。 这是《Sql Server》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对…

2024宠物行业未来发展趋势:京东宠物健康(宠物营养保健和医疗)市场品类数据分析报告

近段时间&#xff0c;广州某知名宠物医院的医疗事故正在被大众热议&#xff0c;也让越来越多从业者开始关心宠物医疗行业的未来形势。 在2022年下半年&#xff0c;京东平台专门设立了一个一级大类目&#xff1a;宠物健康&#xff08;将其从原本的宠物生活类目中独立出来&#…

[递归、搜索、回溯]----递归

前言 作者&#xff1a;小蜗牛向前冲 专栏&#xff1a;小蜗牛算法之路 专栏介绍&#xff1a;"蜗牛之道&#xff0c;攀登大厂高峰&#xff0c;让我们携手学习算法。在这个专栏中&#xff0c;将涵盖动态规划、贪心算法、回溯等高阶技巧&#xff0c;不定期为你奉上基础数据结构…

OpenCASCADE+Qt创建建模平台

1、建模平台效果 2、三维控件OCCWidget 将V3d_View视图与控件句柄绑定即可实现3d视图嵌入Qt中&#xff0c;为了方便也可以基于QOpenGLWidget控件进行封装&#xff0c;方便嵌入各种窗体使用并自由缩放。 #ifndef OCCTWIDGET_H #define OCCTWIDGET_H#include <QWidget> #i…

产业园区如何实现数字化运营管理?

​在数字化浪潮席卷全球的今天&#xff0c;产业园区正经历着前所未有的变革&#xff0c;数字化运营管理成为各个园区转型升级的发力方向&#xff0c;它不仅能够提升园区的运营管理效率&#xff0c;还能够帮助园区提高服务效能、实现精准招商、增强决策效率&#xff0c;从而全面…

开源模型应用落地-工具使用篇-Ollama(六)

一、前言 在AI大模型百花齐放的时代&#xff0c;很多人都对新兴技术充满了热情&#xff0c;都想尝试一下。但是&#xff0c;实际上要入门AI技术的门槛非常高。除了需要高端设备&#xff0c;还需要面临复杂的部署和安装过程&#xff0c;这让很多人望而却步。不过&#xff0c;随着…

算法学习04:双指针、位运算

算法学习04&#xff1a;双指针、位运算 文章目录 算法学习04&#xff1a;双指针、位运算前言须要记忆的模版&#xff1a;一、双指针1.例题1注意&#xff1a;两个指针在一个序列 2.例题2 二、位运算1.例题1注意&#xff1a;从0开始数“第一位” 2.例题2注意&#xff1a;lowbit操…

蓝桥杯刷题(二)

参考大佬代码&#xff1a;&#xff08;区间合并二分&#xff09; import os import sysn, L map(int, input().split()) # 输入n,len arr [list(map(int, input().split())) for _ in range(n)] # 输入Li,Si def check(Ti, arr, L)->bool:sec [] # 存入已打开的阀门在…

如何恢复未保存的 Excel 文件

本周我们将 Office 恢复系列扩展到 Excel 恢复&#xff0c;并提出了最常见的问题&#xff1a;如何恢复 Excel 文件&#xff1f; 与 Office Word 不同&#xff0c;Excel 完全是关于表格和计算的。在处理Excel文件时&#xff0c;您可能会遇到更多问题。与往常一样&#xff0c;我们…

【JavaEE进阶】CSS选择器的常见用法

CSS选择器的主要功能就是选中页面指定的标签元素&#xff0c;选中了元素&#xff0c;才可以设置元素的属性。 CSS选择器主要有以下几种: 标签选择器类选择器id选择器复合选择器通配符选择器 接下来用代码来学习这几个选择器的使用。 <!DOCTYPE html> <html lang&q…

macos docker baota 宝塔 搭建 ,新增端口映射

拉取镜像仅拉取镜像保存到本地&#xff0c;不部署容器&#xff0c;仅需拉取一次&#xff0c;永久存储到本地镜像列表 docker pull akaishuichi/baota-m1:lnmp 其他可参考&#xff1a;宝塔面板7.9.2docker镜像发布-集成LN/AMP支持m1/m2 mac版本 - Linux面板 - 宝塔面板论坛 运行…

单细胞联合BulkRNA分析思路|加个MR锦上添花,增强验证~

今天给大家分享一篇IF7.3的单细胞MR的文章&#xff0c;2023年12月发表在Frontiers in Immunology&#xff1a;An integrative analysis of single-cell and bulk transcriptome and bidirectional mendelian randomization analysis identified C1Q as a novel stimulated risk…

JAVA虚拟机实战篇之内存调优[4](内存溢出问题案例)

文章目录 版权声明修复问题内存溢出问题分类 分页查询文章接口的内存溢出问题背景解决思路问题根源解决思路 Mybatis导致的内存溢出问题背景问题根源解决思路 导出大文件内存溢出问题背景问题根源解决思路 ThreadLocal占用大量内存问题背景问题根源解决思路 文章内容审核接口的…

python界面开发 - Menu (popupmenu) 右键菜单

文章目录 1. python图形界面开发1.1. Python图形界面开发——Tkinter1.2. Python图形界面开发——PyQt1.3. Python图形界面开发——wxPython1.4. Python图形界面开发—— PyGTK&#xff1a;基于GTK1.5. Python图形界面开发—— Kivy1.6. Python图形界面开发——可视化工具1.7. …

汽车大灯尾灯的车灯罩破损破裂裂纹等问题用什么胶可以修复??

汽车大灯尾灯破裂可以使用硅酮玻璃胶或者环氧树脂胶进行修复。 环氧树脂胶的优点主要包括&#xff1a; 粘接力强&#xff1a;环氧树脂胶也具有很高的粘接力&#xff0c;可以有效地将裂缝两侧的材料粘合在一起&#xff0c;确保牢固和持久的修复效果。内聚强度大&#xff1a;环…

Linux-信号3_sigaction、volatile与SIGCHLD

文章目录 前言一、sigaction__sighandler_t sa_handler;__sigset_t sa_mask; 二、volatile关键字三、SIGCHLD方法一方法二 前言 本章内容主要对之前的内容做一些补充。 一、sigaction #include <signal.h> int sigaction(int signum, const struct sigaction *act,struc…

1、Ajax、get、post、ajax,随机颜色

一、Ajax初始 1、什么是Ajax&#xff1f; 异步的JavaScript和xml 2、xml是什么&#xff1f; 一种标记语言&#xff0c;传输和存储数据----------现在用JSON传输数据 3、Ajax的作用 局部加载 可以使网页异步更新 4、Ajax的原理或者步骤(6步) 创建Ajax对象 if (window.X…

Whisper实现语音识别转文本

#教程 主要参考开源免费离线语音识别神器whisper如何安装&#xff0c; OpenAI开源模型Whisper——音频转文字 Whisper是一个开源的自动语音识别系统&#xff0c;它在网络上收集了680,000小时的多语种和多任务监督数据进行训练&#xff0c;使得它可以将多种语言的音频转文字。…