《Effective C++》《Resource Management》

文章目录

  • 13、term13:Use objects to manage resources
  • 14、term14:Think carefully about copying behavior in resource-managing classes
  • 15、term15:Provide access to raw resources in resource-managing classes
    • 法一: 使用智能指针的get进行显示转换
    • 法二:使用智能指针的解引用进行隐式转换
    • 法三:自己实现get进行显示转换
    • 法四:自己实现operator() 进行隐式转换
    • 结论:
  • 16、term16:Use the same form in corresponding uses of new and delete
  • 17、term17:Stored newed objects in smart pointers in standalone statements
  • 4、总结
  • 5、参考

系统资源是一个很大的概念,例如内存,文件描述器,网络都算是系统的资源;不管是何种资源,为了保证系统能够安全,高效地运行,在你不使用他的时候,你要及时地将他还给操作系统。

13、term13:Use objects to manage resources

首先我们写一个root class

class Investment{...};
Investment* createInvestment();void f(){Investment* pInv = createInvestment();//调用factory函数...delete pInv //释放pInv所指向的对象
}

动态分配对象时,对象存储在heap上,若不及时或者忘了delete对象指针,会造成内存泄露
即便最后没有忘记delete对象指针,在函数运行到delete语句之前,可能会遇到以下状况使得delete语句不被执行:

  • new和delete之间有一个过早的return;
  • new和delete位于某个循环内,该循环由于某个continue、break或者goto过早退出;
  • delete语句之前抛出异常,直接跳转到异常处理函数;

当然,我们在编程时可以特意防止这一类错误,但是在后期的维护中,可能会添加return 语句,continue语句,也有可能f()会抛出一个异常,导致pInv所指向的对象不能正确被释放。
为了解决此类问题,C++提供了智能指针解决这个问题:在分配资源时,资源动态分配与heap内,在控制流离开那个区域时被释放。
常见的资源管理对象有auto_ptr、shared_ptr
首先介绍auto_ptr

void f()
{std::auto_ptr<Investment> pInv(createInvestment());...
}

从auto_ptr的使用。我们获得了两点启示:

  • 获得资源后立即放进资源的管理对象内,createInvestment()返回的资源当作auto_ptr的初始值
  • 资源的管理对象执行析构函数来释放资源,一旦离开作用域,自动调用析构函数将资源释放。
    但是auto_ptr存在一个缺陷,不能让多个auto_ptr同时指向同一个对象,若通过copy构造函数,copy assignment操作符复制他们,复制所得到的指针获得资源的唯一拥有权。
    举个栗子:
void f()
{std::auto_ptr<Investment> pInv1(createInvestment());//pInv1指向createInvestment()的返回物std::auto_ptr<Investment> pInv2(pInv1);//pInv2指向对象,pInv1设为null;pInv1 = pInv2;//pInv1指向对象,pInv2设为null;...
}

对于其容器一些正常的“复制行为”,auto_ptr是无法满足这个需求的。
shared_ptr
shared_ptr是auto_ptr的替代方案,他能持续追踪共有多少对象指向某笔资源,并在无人指向他时自动删除资源。
举个栗子:

void f()
{std::tr1::shared_ptr<Investment> pInv(createInvestment());...
}void f()
{std::tr1::shared_ptr<Investment> pInv1(createInvestment());//pInv1指向createInvestment()的返回物std::tr1::shared_ptr<Investment> pInv2(pInv1);//pInv2,pInv1指向同一个对象pInv1 = pInv2;//pInv2,pInv1指向同一个对象...//当pInv2,pInv1被销毁,他们所指向的对象就会自动销毁
}

14、term14:Think carefully about copying behavior in resource-managing classes

资源的类型并非都是heap_based,有时候需要建立自己的资源管理类
举个栗子:

#include <iostream>
using namespace std;class Lock
{
public:explicit Lock(int* pm): m_p(pm){lock(m_p);}~Lock(){unlock(m_p);}private:int *m_p;void lock(int* pm){cout << "Address = " << pm << " is locked" << endl;}void unlock(int *pm){cout << "Address = " << pm << " is unlocked" << endl;}
};int main()
{int m = 5;Lock m1(&m);return 0;
}

运行结果如下:

Address = 0x7fff0b0a385c is locked
Address = 0x7fff0b0a385c is unlocked...Program finished with exit code 0
Press ENTER to exit console.

这符合预期,当m1获得资源的时候,将之锁住,而m1生命周期结束后,也将资源的锁释放。
注意到Lock类中有一个指针成员,那么如果使用默认的析构函数、拷贝构造函数和赋值运算符,很可能会有严重的bug。

我们不妨在main函数中添加一句话,变成下面这样:

int main()
{int m = 5;Lock m1(&m);Lock m2(m1);
}

再次运行,可以看到结果:

Address = 0x7ffc56116f34 is locked
Address = 0x7ffc56116f34 is unlocked
Address = 0x7ffc56116f34 is unlocked...Program finished with exit code 0
Press ENTER to exit console.

可见,锁被释放了两次,这就出问题了。原因是析构函数被调用了两次,在main()函数中生成了两个Lock对象,分别是m1和m2,Lock m2(m1)这句话使得m2.m_p = m1.m_p,这样这两个指针就指向了同一块资源。根据后生成的对象先析构的原则,所以m2先被析构,调用他的析构函数,释放资源锁,但释放的消息并没有通知到m1,所以m1在随后析构函数中,也会释放资源锁。
如果这里的释放不是简单的一句输出,而是真的对内存进行操作的话,程序就会崩溃。
归根到底,是程序使用了默认了拷贝构造函数造成的(当然,如果使用赋值运算的话,也会出现相同的bug),那么解决方案就是围绕如何正确摆平这个拷贝构造函数(和赋值运算符)。
书中提出了四种解决方案:
方案一:很简单直观,就是干脆不让程序员使用类似于Lock m2(m1)这样的语句,一用就报编译错。这可以通过自己写一个私有的拷贝构造函数和赋值运算符的声明来解决。注意这里只要写声明就行了(见条款6)。

    // 私有拷贝构造函数声明,删除拷贝构造函数Lock(const Lock&) = delete;   // 私有赋值运算符声明,删除赋值运算符Lock& operator=(const Lock&) = delete;   

或者:

    // 私有拷贝构造函数声明,只是声明,但是不定义;Lock(const Lock&) ;   // 私有赋值运算符声明,只是声明,但是不定义;Lock& operator=(const Lock&) ;   

这样编译就不会通过了:

main.cpp: In function ‘int main():
main.cpp:47:15: error:Lock::Lock(const Lock&)’ is private within this context47 |     Lock m2(m1);|               ^
main.cpp:37:5: note: declared private here37 |     Lock(const Lock&);

当然也可以像书上写的一样,写一个Uncopyable的类,把它作为基类。在基类中把它的拷贝构造函数和赋值运算写成私有的(为了防止生成基类的对象,但又想允许派生类生成对象,可以把构造函数和析构函数的修饰符变成protected。
然后

class Uncopyable
{
protected:Uncopyable(){}~Uncopyable(){}
private:Uncopyable(const Uncopyable&);Uncopyable& operator= (const Uncopyable&);
};class Lock: public Uncopyable{}

方案二:使用shared_ptr来进行资源管理,但还有一个问题,我想在生命周期结束后调用Unlock的方法,其实shared_ptr里面的删除器可以帮到我们。

class Lock
{
public:explicit Lock(int *pm): m_p(pm, unlock){}
private:shared_ptr<int> m_p;
}

这样在Lock的对象的生命周期结束后,就可以自动调用unlock了。tr1::shared_ptr允许指定所谓的“删除器”,当一个函数或者函数对象引用次数为0时就会调用这个删除器。
在条款十三的基础上,我改了一下自定义的shared_ptr,使之也支持删除器的操作了,代码如下:

#ifndef MY_SHARED_PTR_H
#define MY_SHARED_PTR_H#include <iostream>
using namespace std;typedef void (*FP)();    template <class T>
class MySharedPtr
{private:T *ptr;size_t *count;FP Del; // 声明一个删除器static void swap(MySharedPtr& obj1, MySharedPtr& obj2){std::swap(obj1.ptr, obj2.ptr);std::swap(obj1.count, obj2.count);std::swap(obj1.Del, obj2.Del);}public:MySharedPtr(T* p = NULL): ptr(p), count(new size_t(1)),Del(NULL){}// 添加带删除器的构造函数MySharedPtr(T* p, FP fun): ptr(p), count(new size_t(1)), Del(fun){}MySharedPtr(MySharedPtr& p): ptr(p.ptr), count(p.count), Del(p.Del){++ *p.count;}MySharedPtr& operator= (MySharedPtr& p){if(this != &p && (*this).ptr != p.ptr){MySharedPtr temp(p);swap(*this, temp);}return *this;}~MySharedPtr(){if(Del != NULL){Del();}    reset();}T& operator* () const{return *ptr;}T* operator-> () const {return ptr;}T* get() const {return ptr;}void reset(){-- *count;if(*count == 0){delete ptr;ptr = 0;delete count;count = 0;//cout << "真正删除" << endl;}}bool unique() const{return *count == 1;}size_t use_count() const {return *count;}friend ostream& operator<< (ostream& out, const MySharedPtr<T>& obj){out << *obj.ptr;return out;}};#endif /* MY_SHARED_PTR_H */

方案三:复制底部资源,就是将原来的浅拷贝转换成深拷贝,需要自己显示定义拷贝构造函数和赋值运算符。这个也在之前的条款说过了,放到这里,其实就是在拷贝的时候对锁的计数次数进行+1,析构函数里就是对锁的计数次数进行-1,如果减到0就去unlock(其实思想还是类似于shared_ptr进行资源管理)

方案四:转移底部资源的控制权,这就是auto_ptr干的活了,在第二个方法中把shared_ptr换成auto_ptr就行了。

15、term15:Provide access to raw resources in resource-managing classes

资源管理类很好,它们是我们对抗资源泄漏的堡垒。排除此等泄漏是良好设计系统的根本性质。在一个完美的世界中,你需要依赖这样的类来同资源进行交互,而不是访问原生(raw)资源而玷污你的双手。但是世界不是完美的,许多API会直接引用资源,所以除非你放弃使用这样的API(这是不实际的想法),你将会绕开资源管理类而时不时的处理原生资源。

//创建一个类
class Investment {};//创建一个函数,会返回一个Investment指针对象
Investment* createInvestment();//我们使用shared_ptr类来管理获得得到的Investment动态对象
std::shared_ptr<Investment> pInv(createInvestment());//见条款13//假设你希望以某个函数处理 Investment 对象,如:
int daysHeld(const Investment* pi);
//如下我们这样调用它是错误的。
int days=daysHeld(pInv); 

代码将不能通过编译:因为dayHeld想要使用一个原生Investment*指针,这里却传递了一个shared_ptr类型的对象。
解决方法:显示转换和隐式转换
  我们需要一种方法将一个RAII类对象(在这个例子中是shared_ptr)转换成它所包含的原生资源类型。有两种常见的方法来实现它:显示转换隐式转换

法一: 使用智能指针的get进行显示转换

shared_ptr和auto_ptr都提供了一个get成员函数来执行显示转换,也就是返回智能指针对象内部的原生指针(的复件。
以shared_ptr的get函数为例,shared_ptr有一个get函数,可以直接得到内部的资源,如:

//get:得到pInv内部的Investment指针
int days=daysHeld(pInv.get());

法二:使用智能指针的解引用进行隐式转换

如果你觉得显式转换不好,可能会增加泄漏内部资源的可能性,那么可以使用隐式转换函数。
事实上像所有的智能指针一样,shared_ptr和auto_ptr也重载了指针的解引用运算符(operator->和operator*),这就允许将其隐式的转换成底层原生指针:

class Investment { 
public: bool isTaxFree() const;...
};
Investment* createInvestment(); // 工厂函数
std::shared_ptr<Investment> 
pi1(createInvestment()); // 管理资源
bool taxable1 = !(pi1->isTaxFree()); //由operator->访问资源
// via operator->
...
std::auto_ptr<Investment> pi2(createInvestment()); // 使用auto_ptr管理资源
bool taxable2 = !((*pi2).isTaxFree()); // 由operator*访问资源

法三:自己实现get进行显示转换

因为有时候获取RAII对象中的原生资源是必要的,一些RAII类的设计者通过提供一个隐式转换函数来顺利达到此目的。举个例子,考虑下面的字体RAII类,字体对于C API来说是原生数据结构:

FontHandle getFont();           //得到某种字体
void releaseFont(FontHandle fh);//释放字体
//FontHandle资源管理类
class Font 
{
public:explicit Font(FontHandle fh) :f(fh) {}~Font() { releaseFont(f); }private:FontHandle f;//原始字体资源
};

假设有大量的字体相关的C API用于处理FontHandles,因此会有频繁的需求将Font对象转换成FontHandles对象。

//Font类可以提供一个显示的转换函数,比如说:get:
class Font {
public:
...
FontHandle get() const { return f; } // 显示转换函数
...
};

不幸的是,如果它们想同API进行通讯,每次都需要调用get函数:

void changeFontSize(FontHandle f, int newSize); // C API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); // 明白地将Font转换为FontHandle

某些程序员会发现显示请求这些转换是如此令人不愉快以至于不想使用RAII类。但是这会增加泄漏字体资源的机会,这正是设计Font类要预防的事情。

法四:自己实现operator() 进行隐式转换

另一种办法是令 Font 提供隐式转换函数,转型为 FontHandle:

class Font {
public:...operator FontHandle() const // 隐式转换函数{ return f; }...
};//这使客户调用C API的调用变得轻松且自然:Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); // 将Font隐式转换为FontHandle//但这个隐式转换增加了出错的机会。
//举个例子,客户端本来想要一个Font却创建了一个FontHandle:
Font f1(getFont());
...
FontHandle f2 = f1; // 本意是要拷贝一个Font对象,反而将f1隐式转换为其底部的FontHandle,然后才复制它。

上面的程序拥有一个被Font对象 f1管理的FontHandle,但是直接使用f2也可以获得这个FontHandle。这就不好了。例如:当f1被销毁,字体资源被释放,f2就变成了悬挂指针。
补充说明:

  • (1)隐式转换和显示转换如何选择
    提供从RAII类对象到底层资源的显示转换(通过一个get成员函数)还是提供隐式转换依赖于设计出来的RAII类需要执行的特殊任务以及使用的场景。最好的设计看上去要遵守条款18的建议:使接口容易被正确使用,很难被误用。通常情况下,像get一样的显示转换函数会是更好的选择,因为它减少了类型误转换的机会。然而有时候,使用隐式类型转换的自然特性会使局面发生扭转。
  • (2)封装和原始资源背道而驰?
    函数返回一个RAII类中的原生资源同封装是背道而驰的,这已经发生了。这不是设计的灾难,RAII类的存在不是用来封装一些东西;他们的存在是用来保证资源的释放会发生。如果需要,资源封装可以在这个基本功能之上进行实现,但这不是必要的。此外,一些RAII类将实现的真正封装同底层资源非常松散的封装组合到一块。举个例子:shared_ptr封装了所有的引用计数,但是仍然可以非常容易的访问它所包含的原生指针。像一些设计良好的类,它隐藏了客户没有必要看到的东西,但是它提供了客户端确实需要访问的东西。

结论:

(1)APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理的资源”的办法。
(2)对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便

16、term16:Use the same form in corresponding uses of new and delete

这个条款强调了new与delete一一对应的重要性,即new对应delete,new[]对应delete[]。

  • 首先看一下new与new[]之间的区别:

new用于分配单个对象的内存,而new[]用于分配对象数组的内存。当使用new时,它只调用对象的构造函数;而当使用new[]时,它会依次调用每个对象的构造函数。
此外,new和new[]在内部实现上也有所不同。new和delete通常通过调用自定义的内存管理器来分配和释放内存,而new[]和delete[]则通常通过调用标准库中的数组分配函数来分配和释放内存。

在这里插入图片描述

  • 再看一下delete与delete[]之间的区别:

delete 和 delete[] 在 C++ 中用于释放动态分配的内存。它们之间的主要区别在于如何处理内存块

  • delete 用于释放单个对象。当你使用 new 创建一个对象时,应该使用 delete 来释放它。
  • delete[] 用于释放对象数组。当你使用 new[] 创建一个对象数组时,应该使用 delete[] 来释放它。

如果你错误地使用 delete 来释放对象数组,或者使用 delete[] 来释放单个对象,可能会导致未定义的行为,包括内存泄漏、程序崩溃或其他问题。
举个反面栗子:

std::string* stringArray = new std::string[100];
std::string* strPtr = new std::string("Hello, world!");
...
delete stringArray;
delete[]  strPtr;

错误使用会产生未定义的行为。

举正面的栗子:

std::string* stringArray = new std::string[100];
std::string* strPtr = new std::string("Hello, world!");
...
delete strPtr;
delete[]  stringArray;

另外减少对数组的需求,因为数组名就是一个地址,如果对字符串数组做出typedef的操作,很有可能让人误用。
举个反面栗子:

//数组的typedef
typedef std::string AddressLines[4];
std::string* pal = new AddressLines;
...
delete pal;     //产生未定义的行为
delete [] pal;//vector的typedef
typedef std::vector<std::string> AddressLines;  
std::string* pal = new AddressLines;  
...
delete pal;  // 正确使用

所以为了避免诸如此类的错误,尽量不要对数组做typedefs的动作,可以讲本例的AddressLines定义为strings组成的一个vector,这样直接删除pal就ok,不会引起不必要的误会。
正面的栗子:

//vector的typedef
typedef std::vector<std::string> AddressLines;  
std::string* pal = new AddressLines;  
...
delete pal;  // 正确使用

17、term17:Stored newed objects in smart pointers in standalone statements

最好以独立语句将newed对象置入智能指针,否则,可能导致资源泄漏。
举个栗子:

//对于下述函数:int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw,int priority);//processWidget决定对其动态分配得来的Widget运用智能指针
//(这里采用tr1::shared_ptr),考虑调用processWidget://反面例子:
processWidget(new Widget,priority());//不能通过编译

tr1::shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,将得自"new Widget"的原始指针转换为processWidget所要求的tr1::shared_ptr。

//如这样写就能通过编译:
processWidget(std::shared_ptr<Widget>(new Widget),priority());

但上述调用却可能泄漏资源。
编译器产出一个processWidget调用码之前,必须首先核算即将被传递的各个实参。上述第二实参只是一个单纯的对priority函数的调用,但第一实参std::shared_ptr(new Widget)由两部分组成:

1.执行"new Widget"表达式
2.调用tr1::shared_ptr构造函数

因此在调用processWidget之前,编译器必须创建代码,做如下三件事情(不分次序):

  • 调用prioriry()函数
  • 执行new Widget
  • 调用shared_ptr的构造函数

processWidget的参数执行顺序
C++对于函数参数的调用顺序会不同,C++不像java和C#那样以特定的次序完成函数参数的核算
在上面的processWidget函数调用中,我们可以确定“new Widget”一定是在shared_ptr的构造函数前执行的,但是prioriry()函数的调用次序我们就不一定知晓了
因此在参数执行次序中一共会有下面3种情况:
情况①:

  • 调用prioriry()函数
  • 执行new Widget
  • 调用shared_ptr的构造函数

情况②:

  • 执行new Widget
  • 调用prioriry()函数
  • 调用shared_ptr的构造函数

情况③:

  • 执行new Widget
  • 调用shared_ptr的构造函数
  • 调用prioriry()函数

分析情况②:如果在调用prioriry()函数的时候程序抛出了异常,那么new Widget返回的指针将会丢失,没有被放入到shared_ptr的构造函数中,那么就造成内存泄漏了。
解决办法:以独立语句将newed对象置入智能指针
在上面的分析中,我们可以看到在“资源创建(new)”和“资源被使用”之间如果发生了异常,那么就会造成资源泄漏;避免这类问题就是分离语句,将“创建的对象”与“放入智能指针对象”这两个步骤合成一步完成,而不是在函数调用中完成。
例如,下面的函数调用就不会产生错误:

std::tr1::shared_ptr<Widget> pw(new Widget); //以单独语句存储对象
processWidget(pw, priority()); //安全调用函数

总结:
以独立语句将newed对象存储于(置于)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以觉察的资源泄漏。

4、总结

书山有路勤为径,学海无涯苦作舟。

5、参考

4.1 《Effective C++》

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

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

相关文章

Redis 连接 命令

目录 1.Redis Echo 命令 - 打印字符串简介语法可用版本: > 1.0.0返回值: 返回字符串本身。 示例 2.Redis Select 命令 - 切换到指定的数据库简介语法可用版本: > 1.0.0返回值: 总是返回 OK 。 示例 3.Redis Ping 命令 - 查看服务是否运行简介语法可用版本: > 1.0.0返回…

Apache 网页优化

目录 1.网页压缩与缓存 1.1 网页压缩 1. gzip 介绍 2. Http的压缩过程 3. Apache的压缩模块 4. mod_deflate模块 1.2 网页缓存 1. 配置 mod_expires 模块启用 2. 隐藏版本信息 2.1 配置Apache隐藏版本信息 2.2 Apache 防盗链 1. 配置防盗链 2.检查是否安装mod_re…

景联文科技GPT教育题库:AI教育大模型的强大数据引擎

GPT-4发布后&#xff0c;美国奥数队总教练、卡耐基梅隆大学数学系教授罗博认为&#xff0c;这个几乎是用“刷题”方式喂大的AI教育大模型的到来&#xff0c;意味着人类的刷题时代即将退出历史舞台。 未来教育将更加注重学生的个性化需求和多元化发展&#xff0c;借助GPT和AI教育…

你想过在 C++ 中使用契约和反射特性吗?

以下内容为本人的学习笔记&#xff0c;如需要转载&#xff0c;请声明原文链接微信公众号「ENG八戒」https://mp.weixin.qq.com/s/fOEG22dQqKSpsZmk8z6w6g ISO/IEC C 技术委员会主持人 Herb Sutter 报告称&#xff0c;C26 将具有新的功能&#xff0c;包括契约和反射。 该委员会…

Java异常简单介绍

文章目录 1. 异常分类和关键字1.1 分类1.2 关键字 2. Error2.1 Error定义2.2 常见的Error2.2.1 VirtualMachineError2.2.2 ThreadDeath2.2.3 LinkageError2.2.4 AssertionError2.2.5 InternalError2.2.6 OutOfMemoryError2.2.6.1 OOM原因2.2.6.2 OutOfMemoryError会导致宕机吗 …

大创项目推荐 深度学习卫星遥感图像检测与识别 -opencv python 目标检测

文章目录 0 前言1 课题背景2 实现效果3 Yolov5算法4 数据处理和训练5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **深度学习卫星遥感图像检测与识别 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐…

Leetcode算法系列| 11. 盛最多水的容器

目录 1.题目2.题解C# 解法一&#xff1a;暴力C# 解法二&#xff1a;双指针&#xff08;左指针大于右指针&#xff0c;left&#xff09;C# 解法三&#xff1a;双指针优化&#xff08;左指针小于等于最小高度&#xff0c;left&#xff09;Java 解法一&#xff1a;双指针Python3 解…

经常使用耳机对耳朵听力有影响吗?戴哪种耳机不伤耳朵听力?

经常使用耳机容易引起末梢感受器官受损&#xff0c;可能造成内耳功能损伤&#xff0c;出现耳聋、耳鸣等听力的适应性下降的症状&#xff0c;建议使用耳机时间不要过长&#xff0c;并且音量不要过大。如果想保护听力的话&#xff0c;建议选择骨传导耳机&#xff0c;骨传导耳机通…

单片机快速入门

参考连接&#xff1a; 安装MinGW-64&#xff08;在win10上搭建C/C开发环境&#xff09;https://zhuanlan.zhihu.com/p/85429160MinGW-64; 链接&#xff1a;https://pan.baidu.com/s/1oE1FmjyK7aJPnDC8vASmCg?pwdy1mz 提取码&#xff1a;y1mz --来自百度网盘超级会员V7的分享C…

rotate-captcha-crack项目重新训练百度旋转验证码角度预测模型

参考&#xff1a; building-powerful-image-classification-models-using-very-little-data.html https://github.com/Starry-OvO/rotate-captcha-crack &#xff08;主&#xff09;作者思路&#xff1a;https://www.52pojie.cn/thread-1754224-1-1.html 纠正 新版百度、百家…

低成本总线技术——LIN总线协议规范介绍

关注菲益科公众号—>对话窗口发送 “CANoe ”或“INCA”&#xff0c;即可获得canoe入门到精通电子书和INCA软件安装包&#xff08;不带授权码&#xff09;下载地址。 本篇文章主要介绍LIN总线协议规范。 数据帧的结构 LIN的数据帧包括报头&#xff0c;响应两大部分。而报头…

Visual Studio 2013 “即将退休”

新年快乐&#xff01; 这也是向各位开发者提醒 Visual Studio 支持生命周期中即将到来的好时机。 对 Visual Studio 2013 的支持即将在今年(2024年)的4月9日结束。如果你正在使用旧版本的 Visual Studio&#xff0c;我们强烈建议您升级您的开发环境到最新的 Visual Studio 20…

浏览器是如何渲染页面的

浏览器是个极其复杂的程序&#xff0c;这里只是挑几个和前端息息相关的重要内容来说 在学习如何渲染之前需要知道一个浏览器浏览器会有多个进程&#xff0c;其中主要进程有浏览器进程&#xff0c;网络进程&#xff0c;渲染进程这里我们主要学习内容就发生在渲染进程。当渲染进程…

【解决openGauss无法使用gs_check等服务器端命令问题】

【解决openGauss无法使用gs_check等服务器端命令问题】 一、问题描述二、问题原因三、解决方法 一、问题描述 [ommopengauss03 ~]$ gs_check -i CheckCPU Parsing the check items config file successfully [GAUSS-53026]: ERROR: Execute SSH command on host 192.168.56.19…

给出一句话来描述想要的图片,就能从图库中搜出来符合要求的

介绍 地址&#xff1a;https://github.com/mazzzystar/Queryable The open-source code of Queryable, an iOS app, leverages the OpenAIs CLIP model to conduct offline searches in the Photos album. Unlike the category-based search model built into the iOS Photos…

项目使用PowerJob

新一代的定时任务框架——PowerJob 简介 PowerJob是基于java开发的企业级的分布式任务调度平台&#xff0c;与xxl-job一样&#xff0c;基于web页面实现任务调度配置与记录&#xff0c;使用简单&#xff0c;上手快速&#xff0c;其主要功能特性如下&#xff1a; 使用简单&…

如何在 Windows 上从电脑硬盘恢复照片

如今&#xff0c;随着相机设备的普及&#xff0c;您可以轻松地一次拍摄一堆照片&#xff0c;将它们传输到硬盘上&#xff0c;然后再拍摄更多照片。但是&#xff0c;如果您的所有照片意外丢失在驱动器中怎么办&#xff1f;你能恢复它们吗&#xff1f; 在本指南中&#xff0c;我…

EasyRecovery2024永久免费版电脑数据恢复软件

EasyRecovery是一款操作安全、价格便宜、用户自主操作的非破坏性的只读应用程序&#xff0c;它不会往源驱上写任何东西&#xff0c;也不会对源驱做任何改变。它支持从各种各样的存储介质恢复删除或者丢失的文件&#xff0c;其支持的媒体介质包括&#xff1a;硬盘驱动器、光驱、…

postgresql可视化导入csv文件

不需要在命令行copy了&#xff0c;只需简单点几下 1.在数据库下建一个schema 右击选中数据库-new schema 2.双击你创建的schema&#xff0c;出现tables 3.右击tables&#xff0c;选择import wizard 4.选择你想导入的文件格式&#xff0c;之后一直next 5.选择你的文件所在位置…

C语言实用第三方库Melon开箱即用之多线程模型

在之前的文章中&#xff08;开发利器——C 语言必备实用第三方库&#xff09;&#xff0c;笔者介绍了一款Linux/UNIX下C语言库Melon的基本功能&#xff0c;并给出了一个简单的多进程开箱即用的例子。 本文将给大家介绍Melon中多线程的使用方法。 在Melon中有三种多线程模式&a…