《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 context
   47 |     Lock m2(m1);
      |               ^
main.cpp:37:5: note: declared private here
   37 |     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.mfbz.cn/a/293438.html

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

相关文章

第11课 实现桌面与摄像头叠加

在上一节&#xff0c;我们实现了桌面捕获功能&#xff0c;并成功把桌面图像和麦克风声音发送给对方。在实际应用中&#xff0c;有时候会需要把桌面与摄像头图像叠加在一起发送&#xff0c;这节课我们就来看下如何实现这一功能。 1.备份与修改 备份demo10并修改demo10为demo11…

Python数据分析从入门到进阶:分类算法

数据分析是处理和解释数据以发现有用信息和洞察的过程。其中&#xff0c;分类算法是数据分析领域的一个重要组成部分&#xff0c;它用于将数据分为不同的类别或组。 本文将介绍分类算法的基本概念和进阶技巧&#xff0c;以及如何在Python中应用这些算法&#xff0c;包括示例代…

01.微服务架构优缺点、服务拆分和远程调用

1.认识微服务 随着互联网行业的发展&#xff0c;对服务的要求也越来越高&#xff0c;服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢&#xff1f; 1.0.学习目标 了解微服务架构的优缺点 1.1.单体架构 单体架构&#xff1a;将业务的所有…

企业招聘信息查询API:招聘市场情报站,一键了解就业机会

前言 在当今这个信息爆炸的时代&#xff0c;快速、准确地获取企业招聘信息对于求职者来说至关重要。为了满足这一需求&#xff0c;企业招聘信息查询API应运而生&#xff0c;它为求职者提供了一个便捷、高效的平台&#xff0c;帮助用户快速了解企业的招聘动态。本文将详细介绍企…

众和策略:全国期货市场成交量同比增长25%

近来&#xff0c;我国期货业协会发布2023年12月全国期货商场生意状况数据。以单边核算&#xff0c;12月全国期货生意商场成交量为6.91亿手&#xff0c;成交额为480437.25亿元&#xff0c;同比分别添加4.48%和0.08%&#xff0c;环比分别下降12.58%和12.49%。2023年1-12月&#x…

AI写作生成器哪个好用一点,试试下面这五款

AI写作生成器在当今信息时代的快速发展中&#xff0c;成为了许多人的选择。然而&#xff0c;面对市场上众多的AI写作生成器&#xff0c;我们很难判断哪个更好用一点。为了解决这个问题&#xff0c;本文将介绍并使用下面这五款AI写作生成器&#xff0c;以帮助大家做出更明智的选…

使用 SpringSecurity 发送POST请求出现 403

问题场景 在使用 SpringSecurity 时对一些访问权限进行了设置, 在用户请求资源时出现了403错误 , 通过检查代码发现请求权限是开放的, 并且切换成 GET 请求也是可以通过, 换成POST 请求就无法通过。 解决方法 在 SpringSecurity 中关闭 CSRF 因为 前端向后台发送 post 请求…

【kettle】pdi/data-integration 打开ktr文件报错“Unable to load step info from XML“

一、报错内容&#xff1a; Unable to load step info from XML step nodeorg.pentaho.di.core.exception.KettleXMLException: Unable to load step info from XMLat org.pentaho.commons.launcher.Launcher.main (Launcher.java:92)at java.lang.reflect.Method.invoke (Met…

2023年终总结(脚踏实地,仰望星空)

回忆录 2023年&#xff0c;经历非常多的大事情&#xff0c;找工作、实习、研究生毕业、堂哥结婚、大姐买车、申博、读博、参加马拉松&#xff0c;有幸这一年全家人平平安安&#xff0c;在稳步前进。算是折腾的一年&#xff0c;杭州、赣州、武汉、澳门、珠海、遵义来回跑。完成…

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 纠正 新版百度、百家…

晨控 CK-FR08-A01 与汇川 H5U 系列 PLC 通讯手册

晨控 CK-FR08-A01 与汇川 H5U 系列 PLC 通讯手册 准备阶段 软件 &#xff1a; AutoShop PLC &#xff1a; H5U-1614MTD-A8 读写器&#xff1a; CK-FR08-A01 交换机&#xff1a; 标准POE交换机 电源 &#xff1a; 24V直流电源 简介 CK-FR08-A01 是一款基于射频识别技…

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

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

Kodi 开源多媒体播放器

Kodi (原名 XBMC) 是一款经典开源免费、跨平台且极其强大专业的多媒体影音播放器&#xff0c;包含专业的影音内容管理以及解码播放功能于一体&#xff0c;提供适合在手机/电视/投影/大屏幕上显示的全屏界面&#xff0c;无线手机遥控操作方式&#xff0c;以及功能相当丰富的插件…

第三部分使用脚手架:vue学习(66-69)

文章目录 66.props配置67 mixin混入68 插件69 scoped样式 66.props配置 props配置&#xff0c;说白了就是调用子组件&#xff0c;传参数用的。 父组件的写法&#xff1a;传参。传参必须加引号&#xff0c;否则报错。 子组件的写法&#xff1a;接收。接受有3种方式&#xff0c…

【观察】Aginode安捷诺:坚守“长期主义”,服务中国数字经济

毫无疑问&#xff0c;随着整个社会加速数字化转型&#xff0c;尤其是5G、人工智能、大数据等技术兴起&#xff0c;以及智慧医疗、智慧金融、智能制造等应用加速落地&#xff0c;算力网络在经济社会发展中扮演了愈来愈重要的角色&#xff0c;成为支撑数字经济蓬勃发展的“新引擎…

快手推荐算法工程师三面回顾

快手三次技术面试一次HR面试的简单回顾&#xff0c;希望对大家有所启发。 一面 面试官一上来就让写算法题&#xff0c;第一个是计算岛屿数量&#xff0c;第二个是最长回文字串。 然后就是介绍自己的论文。对于论文的工作&#xff0c;面试官只是在问关于论文的问题&#xff0…

STM32F4XX使用SWO实现printf功能

一 名词说明 SWO&#xff1a;Serial Wire Output&#xff0c;串行线输出 ITM&#xff1a;Instrumentation Trace Macrocell&#xff0c;仪器跟踪宏单元二 使用软件 1 keil 2 JLinkSWOViewer三 swo实现代码 #include "stm32f4xx_hal.h" #include "stdio.h&quo…

babel执行流程

babel简单执行流程 为了验证方便 这边 使用的命令是 babel src/index.js --out-file lib/index.compiled.js,这样可以定位 babel 中的files.js 相对目录比较简单 执行scripts 中的 build 命令 执行 package.json 中的scripts 命令 <!-- package.json 中的命令 -->"…

一款适用于低功耗应用 高效降压转换器TPS62621YFFR 基本原理及主要参数介绍

TPS62621YFFR提供针对电池供电的便携式应用优化的高频同步降压DC/DC转换器。适用于低功耗应用&#xff0c;支持高达600毫安的负载电流&#xff0c;并允许使用低成本的芯片电感器和电容器。 该设备具有2.3 V至5.5 V的宽输入电压范围&#xff0c;支持由具有扩展电压范围的锂离子…

分布式锁Lock4J 使用总结

Lok4j 简介 lock4j是一个分布式锁组件&#xff0c;其提供了多种不同的支持以满足不同性能和环境的需求。 立志打造一个简单但富有内涵的分布式锁组件。 特点 简单易用&#xff0c;功能强大&#xff0c;扩展性强。支持redission,redisTemplate,zookeeper。可混用&#xff0c…
最新文章