从C语言到C++_35(异常)C++异常的使用+异常体系+异常优缺点

news2024/11/25 10:16:00

目录

1. 异常的基本使用

1.1 异常的概念

1.2 异常的抛出和匹配原则

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

1.4 异常的重新抛出

1.5 异常的安全问题

1.6 C++98和C++11的异常规范

2. 自定义异常体系

2.1 异常继承体系

2.2 异常体系中的重新抛出

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

4. C++异常的优缺点和总结

5. 笔试选择题

答案及解析

本篇完。


1. 异常的基本使用

C语言传统处理错误的方式:

① 终止程序
比如空指针解引用,除0等异常发生时,程序会直接终止,但是这种方式对于用户来说难以接受,会导致整个进程挂掉。

② 返回错误码
比如打开文件,还有Linux中创建线程等C函数接口,调用后会返回一个返回值,如果发生错误会将错误码返回并放入到全局的errno中。

程序员需要自己去查找对应的错误,非常不直观。

1.1 异常的概念

异常也是一种处理错误的方式。
当一个函数发生异常后,就会将错误抛出,让该函数的直接或间接的调用者处理这个错误。

throw:当问题出现时,程序会抛出一个异常,通过throw关键字完成。

try:try 块中的代码标识将被激活的特定异常,激活的异常才会被抛出,后面跟着的一个或者多个catch块才能捕获该异常。
catch:在想要处理异常的地方,通过catch关键字捕获异常,然后执行相应的代码,可以有多个catch进行捕获。

写一个函数用来执行两个数相除,当除0时抛异常:

double Division(int a, int b)
{
	if (b == 0)
	{
		throw "Divide by Zero Error";
	}
	else
	{
		return ((double)a / (double)b);
	}
}
  • throw抛出的异常必须是一个对象,可以是自定义类型的对象,也可以是内置类型的对象。

调用这个函数然后试着捕捉异常:

#include <iostream>
using namespace std;

double Division(int a, int b)
{
	if (b == 0)
	{
		throw "Divide by Zero Error";
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	cout << Division(3, 0) << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch(const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "unknown exception" << endl;
	}
	return 0;
}

try块中的代码抛出异常,catch块捕获异常并处理。

  • try块中抛出异常后,只有一个catch块会捕获异常,也只执行一个catch块代码,执行完后跳过所有catch块继续执行后面的代码。
  • catch捕获异常时,会根据( )中的"形参"匹配抛出对象的类型。
  • 如果没有合适的catch匹配,"形参"为(…)的catch就会捕获抛出的任意异常。

1.2 异常的抛出和匹配原则

先把异常的抛出和匹配原则列出来,下面有代码例子。

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

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

③ 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似 于函数的传值返回)

④ catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。

⑤ 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。

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

上面代码发生除0错误时,抛出的异常改成int类型对象:

#include <iostream>
using namespace std;

double Division(int a, int b)
{
	if (b == 0)
	{
		//throw "Divide by Zero Error";
		throw 7;
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	cout << Division(3, 0) << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch(const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (int errid)
	{
		cout << "错误码: " << errid << endl;
	}
	catch (...)
	{
		cout << "unknown exception" << endl;
	}
	return 0;
}

抛出的异常只匹配了catch(const int errid)块。

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

一般是这样的:

#include <iostream>
using namespace std;

double Division(int a, int b)
{
	if (b == 0)
	{
		//throw "Divide by Zero Error";
		throw 7;
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	try
	{
		cout << Division(3, 0) << endl;
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (int errid)
	{
		cout << "(Func中捕获)错误码: " << errid << endl;
	}
}

int main()
{
	try
	{
		Func();
	}
	catch(const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (int errid)
	{
		cout << "错误码: " << errid << endl;
	}
	catch (...)
	{
		cout << "unknown exception" << endl;
	}
	return 0;
}

③ 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似 于函数的传值返回)

④ catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。

⑤ 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。

演示下④:

上面代码其它地方不变,throw一个键值对:

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

1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则
调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的
catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异
常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

1.4 异常的重新抛出

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

看一段类似上面的代码,只是在Func函数里改动了,main函数最后打印个return 0;:

#include <iostream>
using namespace std;

double Division(int a, int b)
{
	if (b == 0)
	{
		throw "Divide by Zero Error";
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
	int* array = new int[10];
	cout << Division(3, 0) << endl;

	cout << "delete []" << array << endl;
	delete[] array;
}

int main()
{

	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (int errid)
	{
		cout << "错误码: " << errid << endl;
	}
	catch (...)
	{
		cout << "unknown exception" << endl;
	}
	cout << "return 0;" << endl;
	return 0;
}

 这种情况就会导致内存泄漏,所以就要使用异常的重新抛出,改下Func函数:

void Func()
{
 	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
	// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。
	int* array = new int[10];
	try 
	{
		cout << Division(3, 0) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw; // 捕获什么抛出什么
	}

	cout << "delete []" << array << endl;
	delete[] array;
}

1.5 异常的安全问题

如果上面的Func函数new了三个空间呢,这样delete三次可以吗?:

void Func()
{
	int* array = new int[10];
	int* array2 = nullptr;
	try
	{
		array2 = new int[10];
		try
		{
			cout << Division(3, 0) << endl;
		}
		catch (...)
		{
			cout << "delete []" << array << endl;
			delete[] array;
			cout << "delete2 []" << array2 << endl;
			delete[] array2;
			throw; // 捕获什么抛出什么
		}
	}
	catch (...)
	{
		// 捕获new的异常
	}
	cout << "delete []" << array << endl;
	delete[] array;
	cout << "delete2 []" << array2 << endl;
	delete[] array2;
}

答案是不行的,new失败也是抛异常,如果前面new的成功,后面new失败了呢,前面的谁释放?

所以这里可以类似这样解决:

void Func()
{
	int* array = new int[10];
	int* array2 = nullptr;
	try
	{
		array2 = new int[10];
		try
		{
			cout << Division(3, 0) << endl;
		}
		catch (...)
		{
			cout << "delete []" << array << endl;
			delete[] array;
			cout << "delete2 []" << array2 << endl;
			delete[] array2;
			throw; // 捕获什么抛出什么
		}
	}
	catch (...)
	{
		// 捕获new的异常
	}
	cout << "delete []" << array << endl;
	delete[] array;
	cout << "delete2 []" << array2 << endl;
	delete[] array2;
}

这只是new两次就已经这么挫了,三次或更多次呢?,所以不适合这样解决,要使用到下一篇学的智能指针RAII解决。

异常安全:

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

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


C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄
漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。

1.6 C++98和C++11的异常规范

在C++中异常经常会导致资源泄漏,比如在new和delete中抛出了异常,导致内存泄漏等等。

尤其是调用非常复杂的时候,后面写程序的人使用了前面程序的接口,但是并不知道前面程序的接口会抛异常,或者是抛什么异常,此时后面的程序员就无法处理抛出的异常,就可能导致资源泄漏等问题。

为了减少因为异常而导致的资源泄漏等问题,C++98时,C++委员会提出了一套建议性规范:

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

void func() throw(A, B, C, D);
// 表示这个函数会抛出A/B/C/D中的某种异常。

void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数只会抛出bad_alloc的异常

② 函数的后面接throw(),表示这个函数不抛异常。

void* operator delete(std::size_t size, void* ptr) throw();

③ 若无异常接口声明,则此函数可能抛出任何类型的异常。

void* operator delete(std::size_t size, void* ptr);

这个规范出发点是好的,可以让我们明确会抛出什么异常,进行相应的处理。

但是这个形式非常繁琐,throw(异常类型),有些异常类型是非常复杂的,为了写这个可能发生的异常类型,需要花费更多代价。

所以这个建议性的规范很少有人在用,因为它只是一个建议,而且有没有可能你跟我说你不会抛异常,但是你抛了呢?所以不使用也不会报错。

为了让异常声明更加简洁,C++11对此做出了相应的改进:

  • 在不会抛出异常的函数后加关键字:noexcept
thread() noexcept;
thread(thread&& x) noexcept;

上面代码表示这个两个函数不会抛出异常。这样一来确实简洁了许多,只是在会抛异常的函数中,需要我们自己搞明白会抛什么异常,这很考验写代码的人的素质,因为也只是个建议。所以这还是很少人用,但是建议大家尽量都跟着规范走。

2. 自定义异常体系

这里带着大家看看公司中自定义的异常体系的框架是怎么弄的,在其中讲讲异常的安全和规范等。

因为C++提供的异常体系不是很好用,实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。

前面的⑤:实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获,这个在实际中非常实用。

这也是继承和多态的一个重要应用、

2.1 异常继承体系

服务器开发中通常使用的异常继承体系的父类:

class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}

	virtual string what() const
	{
		return _errmsg;
	}

	int getid() const
	{
		return _id;
	}
protected:
	string _errmsg;   // 错误信息
	int _id;          // 错误码
};

创建一个异常的基类,如上所示,包含异常信息和异常编号两个成员,注意到一个虚函数what。

做一个项目就会分很多个组,比如网络组,缓存组,数据库组等等,每个组都写一个异常的类,

这些类就会继承上面的类,服务器开发中通常使用的异常继承体系:

#include <iostream>
#include <string>
#include <windows.h> // Sleep
#include <time.h> // time
using namespace std;
// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}

	virtual string what() const
	{
		return _errmsg;
	}

	int getid() const
	{
		return _id;
	}

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;
	}
protected:
	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;
	}
protected:
	// stack<string> _stPath; // 加上堆栈信息可以查看上下文
};

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;
	}
protected:
	const string _type;
};

void SQLMgr() // 下面的缓存层没出问题就调数据库层
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}

	cout << "本次请求成功" << endl; // 最后调用的,前面都没出异常
}
void CacheMgr() // 下面的网络层没出问题就调缓存层
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 200);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 201);
	}
	SQLMgr();
}
void HttpServer() //网络层
{
	srand(time(0)); // 模拟
	if (rand() % 3 == 0)
	{
		//throw HttpServerException("请求资源不存在", 100, "get");
		throw HttpServerException("网络错误", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}

int main()
{
	while (1)
	{
		Sleep(1000);
		try
		{
			HttpServer(); // 先调用网络层
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
			// 记录日志
		}
		catch (...) // 守住底线
		{
			cout << "unknown exception" << endl;
		}
	}
	return 0;
}

抛出的异常是不同类型的对象,但是它们都是派生类,继承基类Exception

当某个函数中抛出异常以后,后面的代码就不再执行了,直接去匹配相应的catch,当前栈帧中匹配不到就跳到上一层栈帧中去匹配。

在main函数中,将try块和catch块放在死循环中,每隔1s执行一次。

catch的类型和所有抛出的异常对象都不是一个类型,但是它是所有异常对象基类的引用。

此时就实现了多态,调用的只是基类Exception中的成员函数what(),但是执行的逻辑就是派生类中what()的逻辑。

从这里也可以看出异常存在的意义:

拿我们经常使用的微信来举例,当网络不好的时候,消息就会发不出去,此时程序就会抛一个异常,表示当前网络状态不佳。如果这个异常没有捕获,而且按照C语言对错误的处理方式,此时微信就崩了,直接退出。采用C++的异常处理机制,会将抛出的这个异常捕获,然后进行处理,比如尝试多次发送等等操作。重点是微信程序不会退出,仍然可以继续运行。

 在实际应用中,很多情况下我们是不希望程序产生异常就让它结束的,而是让它继续运行,并且将异常处理。

2.2 异常体系中的重新抛出

上面异常体系中的网络层:

void HttpServer() //网络层
{
	srand(time(0)); // 模拟
	if (rand() % 3 == 0)
	{
		//throw HttpServerException("请求资源不存在", 100, "get");
		throw HttpServerException("网络错误", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}

网络错误一次就抛出异常吗?不应该再重试几次吗,此时异常的重新抛出就派上用场了:

void SeedMsg(const string& s)
{
	// 要求出现网络错误重试三次
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("网络错误", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}

	cout << "发送成功: " << s << endl;
}
void HttpServer()
{
	// 要求出现网络错误,重试3次
	string str = "等下吃什么";
	int n = 3;
	while (n--)
	{
		try
		{
			SeedMsg(str); // 重试3次
			// 没有发生异常
			break;
		}
		catch (const Exception& e)
		{
			if (e.getid() == 100 && n > 0)// 网络错误 且 重试3次内
			{
				continue;
			}
			else
			{
				throw e; // 重新抛出
			}
		}
	}
}

int main()
{
	while (1)
	{
		Sleep(1000);
		try
		{
			HttpServer(); // 先调用网络层
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
			// 记录日志
		}
		catch (...) // 守住底线
		{
			cout << "unknown exception" << endl;
		}
	}
	return 0;
}

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

C++标准库采用的就是抛派生类对象异常,捕获基类对象的方式,也是利用了继承和多态。

C++ 提供了一系列标准的异常,定义在exception中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

可以看到,C++标准库中,也是通过捕获基类对象来打印各种派生类的异常信息的,但是它的设计并不够好,信息表达不清楚等等,所以实际中很多公司都会像上面一样自定义一套异常继承体系。

4. C++异常的优缺点和总结

C++异常优点:

① 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。


② 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,而C++异常会直接销毁栈帧去异常的上一层匹配catch。


③ 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。


④ 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

C++异常缺点:

① 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。


② 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。


③ C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。


④ C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。

⑤ 异常的规范只是建议,异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。

总结:C++目前异常的缺点只有①比较麻烦,②也有RAII解决了,其它都是小问题,所以异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外其它OO(Object Oriented)语言基本都是用异常处理错误,这也可以看出这是大势所趋。

5. 笔试选择题

1. 对关于异常说法不正确的是()

A.异常是程序处于一种非法的情况,严重时可能会导致程序崩溃

B.throw可以抛出任意类型的异常

C.异常产生的后果不严重时可以不用处理

D.对于throw所抛出的异常,必须要进行捕获,否则代码最后会崩溃

2. 下列关于异常处理的描述中,理解不正确的是: ()

A.C++语言的异常处理机制通过3个保留字throw、try和catch实现

B.任何需要检测的语句必须在try语句块中执行,并由throw语句抛出异常

C.throw语句抛出异常后,catch利用数据类型匹配进行异常捕获

D.一旦catch捕获异常,不能将异常用throw语句再次抛出

3. 如何捕获异常可以使得下面代码通过编译? ()

class A
{
public:
	A() {}
};

void foo()
{
	throw new A;
}

A.catch (A x)

B.catch (A * x)

C.catch (A & x)

D.以上都不是

4. 对于异常的捕获列表说法不正确的是()

A.在捕获列表中可以使用基类的引用捕获所有子类的异常对象

B.捕获列表中捕获到的是异常本身

C.catch(...)可以捕获到任意类型的异常

D.捕获列表是按照抛出异常的类型进行捕获的

答案及解析

1. C

A:正确,异常是程序可能会有安全隐患,异常一旦发生,程序就是非法情况

B:正确,throw抛出的是某种类型的异常,捕获时要按照类型进行捕获

C:错误,只要程序中有异常存在,就必须要处理

D:正确

2. D

A:正确,参考异常的抛出与捕获

B:正确,对于有可能会抛出异常的代码,都应该放在try中尝试进行捕获,只有放在try中,throw 语句抛出的异常    才可能会捕获到

C:正确,所有异常都是按照类型进行捕获的

D:错误,有时捕获异常并不是为了处理异常,而是要做一些其他事情,做完后需要将异常重新抛出,交给该异常的处理位置去处理

3. B

异常是按照类型来捕获的,throw后抛出的是A*类型的异常,因此要按照指针方式进行捕获

4. B

A:该条描述不是很严谨,应该是用基类的const类型引用,可以捕获所有的子类的异常对象

B:错误,捕获列表中捕获到的是异常的一份拷贝,因为异常对象在出其函数作用域前要销毁掉

C:正确,catch(...)是万能的捕获方式,任意类型的异常都可以捕获到

D:正确,异常是按照类型捕获的

本篇完。

下一篇:(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr。(使用和模拟实现)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每天一分享#读up有感#——云原生——持续学习

今日话题&#xff0c;云原生&#xff0c;看到两位大佬&#xff0c;就一起做下学习记录&#xff0c;爱了爱了。 江湖有缘&#xff0c;江湖见 https://blog.csdn.net/jks212454?typeblog 时间周期 第一篇文章&#xff1a;2021.04.17 粗略算&#xff0c;大佬不到2年就十万了…

YOLO目标检测——水果蔬菜数据集下载分享

水果蔬菜数据集共同90000图片&#xff0c;131类别分别存放在不同文件中&#xff0c;可应用于果蔬分类和种类识别等等 数据集点击下载&#xff1a;YOLO水果蔬菜数据集90000图片131类别.rar

【Linux的成长史】Linux的发展史

&#x1f3ac; 博客主页&#xff1a;博主链接 &#x1f3a5; 本文由 M malloc 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;LeetCode刷题集 数据库专栏 初阶数据结构 &#x1f3c5; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如…

有哪些CAD转低版本软件?教你版本转换方法

当不同的人使用不同版本的CAD软件时&#xff0c;可能会出现不兼容的情况。例如&#xff0c;我们可能在使用较新版本的CAD软件&#xff0c;但是其他人可能仍在使用较早版本的软件。在这种情况下&#xff0c;就需要将您的CAD文件转换为较早的版本&#xff0c;也就是低版本&#x…

RT_Thread内核机制学习(一)

ARM架构及汇编 ARM芯片属于精简指令集计算机&#xff08;RISC&#xff1a;Reduced Instruction Set Computer&#xff09;&#xff0c;它所使用的指令比较简单&#xff0c;有如下特点&#xff1a; 对内存只有读、写指令。对于数据的运算实在CPU内部实现。使用RISC指令的CPU复杂…

0基础学习VR全景平台篇 第92篇:智慧景区-智慧景区常见问题

Q&#xff1a;怎么编辑景区里面各个景点的介绍和推荐该景点A&#xff1a;在下方素材栏中该景点&#xff08;素材&#xff09;的右上角选择【编辑场景】里面就可以在场景介绍中编辑该场景的介绍并且在该选项中可以将此场景设置为推荐景点。 Q&#xff1a;景区项目可不可以离线浏…

【位运算进阶之----右移(>>)】

&#x1f604;嘻嘻&#xff0c;朋友们&#xff0c;大家好&#xff01;昨天我们学习了左移&#xff0c;今天我们来谈谈右移>>。 ⭐️简单来说&#xff0c;右移就是将一个数二进制表达整体向右移动&#xff0c;也就是去掉一个数的二进制表达的末位&#xff0c;右移一位就去…

apex和pl/sql学习记录2

验证后过程函数代码插眼儿 -- 登录后验证过程3 create or replace PROCEDURE TEST_USER_WXX3_PRO ASV_USER_ID NUMBER(20);V_ROLE_ID NUMBER(20);V_PERM_ID NUMBER(20);V_DEPT_ID NUMBER(20);V_USER_NAME NVARCHAR2(64);V_JOB_NUMBER NVARCHAR2(32);V_M…

破除“中台化”误区,两大新原则考核中后台

近年来&#xff0c;“中台化”已成为许多企业追求的目标&#xff0c;旨在通过打通前后台数据和业务流程&#xff0c;提升运营效率和创新能力。然而&#xff0c;在实施过程中&#xff0c;一些误解可能导致“中台化”未能如预期般发挥作用。本文将探讨这些误解&#xff0c;并提出…

Exploring Unreal Engine New Free Archviz Explorer Project 视频笔记

链接&#xff1a; https://www.bilibili.com/video/BV1Q34y1Z7he/ 场景中没有太阳&#xff0c;也没有定向光 该蓝图用来控制光线的显示 删除这个蓝图 添加这个蓝图 顶部会出现时间滑块 该项目还有扩展插件&#xff0c;用户可以自由下载 它是由一个8k的卫星图做的地形底图 …

『PyQt5-基础篇』| 05 Qt Designer保存的.ui文件如何生成.py文件?

05 Qt Designer保存的.ui文件如何生成.py文件? 1 使用Qt Designer设计一个简单的界面2 UI文件转PY文件2.1 方法一:直接使用命令2.2 方法二:直接调用PyUIC5工具3 运行转换后的py文件.ui文件是用Qt Designer设计的界面保存后的文件;保存后我们需要把这个文件转换成.py 文件,…

为什么要进行管网水位监测,管网水位监测的作用是什么

管网水位监测是城市排水系统管理的重要手段&#xff0c;对于保障城市排水设施安全运行和提升城市管理水平具有重要意义。通过对排水管网的水位进行实时监测和分析&#xff0c;能够及时发现问题并采取措施&#xff0c;提高排水系统的运行效率和管理水平。本文将详细介绍为什么要…

情感书单视频做怎么制作?几个步骤轻松生成

在当今数字化的时代&#xff0c;制作情感书单视频已经成为了一种流行的方式来分享个人阅读心得。然而&#xff0c;制作这样的视频并不是一件简单的事情。本文将介绍制作情感书单视频的步骤&#xff0c;并讨论需要注意的事项。 准备工作 在制作情感书单视频之前&#xff0c;最好…

空时自适应处理用于机载雷达——额外的性能结果(Matla代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

③matlab向量和矩阵

目录 手动输入数组 创建等间距向量 数组创建函数 手动输入数组 1.背景 单个称为标量的数值实际上是一个 11 数组&#xff0c;也即它包含 1 行 1 列。 任务 创建一个名为 x 并且值为 4 的变量。 2.您可以使用方括号创建包含多个元素的数组。 x [3 5] x 3 5 任务 …

馈纸式扫描仪贵到让人咋舌,您知道原因吗?

嘿&#xff0c;话说到馈纸式扫描仪&#xff0c;给它一个字就是&#xff1a;“贵”&#xff01;我在某宝上查了一下&#xff0c;最便宜的都快500元了&#xff0c;某东上更是贵得让人咋舌&#xff0c;动不动就几千上万&#xff0c;甚至几十万的都有&#xff0c;而一般的平板扫描仪…

Redis的数据结构与单线程架构

"飞吧&#xff0c;去寻觅红色的流星" Redis中的五种数据结构和编码 Redis是一种通过键值对关系存储数据的软件&#xff0c;在前一篇中&#xff0c;我们可以使用type命令实际返回当前键所对应的数据结构类型&#xff0c;例如: String\list\hash\set等等。 但…

如何在钉钉内跳转自己的网页链接

1.跳转网页转码(工具地址) 原网页:https://www.baidu.com/ 转码后:https%3A%2F%2Fwww.baidu.com%2F 2:地址拼接(官方跳转地址:dingtalk://dingtalkclient/page/link?urlURL&pc_slidetrue) 替换URL: dingtalk://dingtalkclient/page/link?urlhttps%3A%2F%2Fwww.baidu.co…

路径规划 | 图解Theta*算法(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 A*算法的局限性2 Theta*算法原理图解3 Bresenham视线法4 算法仿真测试4.1 算法流程图4.2 ROS C 实现4.3 Python实现4.4 Matlab实现 0 专栏介绍 &#x1f525;附C/Python/Matlab全套代码&#x1f525;课程设计、毕业设计、创新竞赛必备&#xff01;详细介绍全…

为什么这3类人,一定要选择无代码开发?

一个人也能开发出一个软件&#xff1f;这或许难以想象的&#xff0c;但无代码技术的问世&#xff0c;让这一切都成为现实。 可能很多人对“无代码”还是不太熟悉&#xff0c;但大家一定都听说过“码农”这个词&#xff0c;而无代码开发技术的出现&#xff0c;可以让我们摆脱这…