C++ - 智能指针 - auto_ptr - unique_ptr - std::shared_ptr - weak_ptr

前言

C++当中的内存管理机制需要我们自己来进行控制,比如 在堆上 new 了一块空间,那么当这块空间不需要再使用的时候。我们需要手动 delete 掉这块空间,我们不可能每一次都会记得,而且在很大的项目程序当中,造成内存泄漏也是不少了。

C++ 不像 Java一样,有 gc,也就是垃圾回收站器,因为 Java 在操作系统之上还有一层虚拟机,这层虚拟机可以理解为运行的一个进程,所有的Java 程序都是在这个 虚拟机 之上运行的。在虚拟机当中就有 这个 gc 在运作。

gc 简单来说就是把所有动态开辟都记录起来,当不需要使用的时候就自动释放了。

但是 Java 在实现这个 虚拟机 是有消耗的,所以在特别需要效率的项目上很多都用的是 C++,比如在游戏开发,服务器当中。

而且,在C++ 当中 我们自己控制 delete 空间的话,就算是我们想起来 要控制,程序当中设计得复杂,我们都不能完全的控制,比如下述:
 

pair<int, int>* pa = new pair<int,int>;func();delete pa;

如果没有 func()函数的话,那么最后肯定是能释放掉的,但是,现在有一个不确定因素在 func()函数当中,我们不清楚在func()函数当中到底做了什么,我们就不能 100% 没问题。假设在func()当中抛异常,被主函数当中捕获了,那么就有可能会直接跳过这个 delete。

虽然,抛异常的跳过的问题,我们可以 先 delete 需要释放的空间,然后再抛出异常,解决很多场景。但是有一个场景是不能解决的:

new 也是可能会抛出异常的,虽然概率很低,但是也是有可能的,那么下面的场景:

 如果 p1 的new 抛出异常,那么还好,没有什么问题,如果是 p2 抛出异常的话,那么 p1 开的空间应该怎么解决呢?

如果此时我们再把 func()函数加上:

pair<int, int>* p1 = new pair<int,int>;
pair<int, int>* p2 = new pair<int,int>;try{func();
}catch(...){delete p1;delete p2;throw ....................
}delete pa;

 如果 func()函数抛出异常,那么就要 delete p1 和 p2。

那如果还不嫌麻烦,如果在多来几个呢? p3  p4  p5 p6 ····························

基于上述问题,就搞出了 智能指针。

智能指针

 智能指针虽然听上去很高端,但实际很简单。

就更之前在 各种库当中实现的迭代器是一样,把指针包装了一下,在其中实现各个函数,比如指针需要的 operator*()   ,  operator->() 函数等等,最重要的是实现 ~析构函数,他是我们自动 释放空间的核心,因为在创建类的对象之后,编译器会自动的调用这个对象的析构函数,用于析构这个对象和对象空间。

template<class T>
class SmartPtr
{
public:// RAII// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源// 1、RAII管控资源释放// 2、像指针一样SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete:" << _ptr << endl;delete _ptr;}
private:T* _ptr;
};

 这样的话,只需要用这个指针构造 一个指针指针对象,那么就会自动维护这个指针:
 

pair<int, int>* p1 = new pair<int,int>;
SmartPtr sp1(p1);

此时,p1 指针维护的空间我就不需要手动释放了,自己就会释放。

当然上述的实现,是分开的,我们可以不用 p1 传进去,直接把 new pair<int,int>; 当参数传进去就行,因为 new 本身返回的就是 这个 空间的指针。

SmartPtr<pair<string, string>> sp2(new pair<string, string>);
SmartPtr<pair<string, string>> sp3(new pair<string, string>);

所以说,虽然 C++ 没有 gc ,但是有智能指针,我们可以自己实现,自己实现的智能指针没有什么问题的话,这个智能指针是可以帮助我们解决绝大部分的内存泄漏问题的,异常也不用怕了。

 有人把这种机制叫做 RAII

RAII 

 RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术

 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

我们实际上把管理一份资源的责任托管给了一个对象。对象是给编译器进行管理。也就是利用了对象无论以什么方式离开了作用域都会调用其析构函数的特点。

也就是获取到资源的时候,马上初始化。

这样做的好处是

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
     

 智能指针像指针一样使用

 其实上述也说过了,智能指针的构造函数,是利用指针来构造这个智能指针的对象,析构函数是释放这个指针指向的空间。

那么,指针的使用还有 operator*()   operator->()  operator[]() 这样子的函数要支持。

实现就更迭代器的实现是一样的,可读可写。

template<class T>
class SmartPtr
{
public:// RAII// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源// 1、RAII管控资源释放// 2、像指针一样SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

智能指针之间的赋值问题(拷贝问题)

 如下面两个智能指针:

	SmartPtr<string> sp1(new string("xxxxx"));SmartPtr<string> sp2(new string("yyyyy"));

我们把两个指针 delete 的地址打印一下,输出:

 两个空间都释放了。

但是,如果上述的两个智能指针对象,进行相互赋值操作的话,就会出现问题:
 

	SmartPtr<string> sp1(new string("xxxxx"));SmartPtr<string> sp2(new string("yyyyy"));sp1 =sp2;

此时输出:
 

 发现,两次释放的是一个空间,这就出问题了啊。

首先,是对一块空间析构了两次,我们说对同一块空间析构两次是可能会出现问题的。

其次是,原本是两个智能指针维护的是两块空间,但是现在我们只是放了一块,另一块空间没有释放就造成了内存泄漏。

其实是因为我们没有写 拷贝构造函数,如果我们没有写拷贝构造函数的话,编译器就会自己实现一个 默认的浅拷贝的 拷贝构造函数。按照之前我们应该自己用现代写法来实现了一下 拷贝构造函数。

但是,在智能指针当中我们就不能 用上述深拷贝的方式来解决,因为我们写的智能指针,本质上就是要模拟实现指针来实现的。而原生指针在赋值这一块,本来就是浅拷贝。


auto_ptr

 为了解决上述所说的 内存管理问题,在C++98 当中就提出了 auto_ptr 。它可以解决上述的问题,但是这个 auto_ptr 有一个非常大的坑,具体请看下述:

 为了演示 到底是没有释放空间,我们需要创建一个类来帮助我们,因为 ,如果是我们自己实现的智能指针,那么我们可以在 析构的时候打印一下,来提示我们此时发生了空间的释放,但是在库当中的实现的 auto_prt 我们没办法打印,但是, delete 一个对象就调用这个对象的析构函数,所以我们实现一个对象的析构函数,在其中打印这个 以下提示我们就行:
 

class A
{
public:A(int a = 1):_a(a){cout << "A(int a = 1)" << endl;}~A(){cout << "~A()" << endl;}private:int _a;
};

如何使用 auto_ptr 呢?如下所示:
 

std::auto_ptr<A> sp1(new A(1));

auto_ptr 是一个模版类,模版参数只需要给出 需要管理的类型即可,不需要给出指针(如上述的 int )。auto_ptr 的构造函数,可以直接传入这个 需要管理的空间的 地址,像上述我们就直接使用 new 开空间后返回这个空间的地址来实现。

 输出:
 

发现,没有显示释放这个空间我们都释放了这个空间。

 如果我们进行像上述在拷贝问题当中实现的赋值一样的,如下代码所示:
 

	auto_ptr<A> aptr1(new A(1));auto_ptr<A> aptr2(new A(1));aptr1 = aptr2;

输出:
 

A(int a = 1)
A(int a = 1)
~A()
~A()

发现,它也正常释放了。

 我们要发现他的问题,就得知道他做了什么,如下例子阐述:

 如上述所示,我们用 ap1 构造了 ap3,也是相当于是 赋值了,那么此时发生了什么呢?

其实 auto_ptr 指针在赋值这一块,做了一件事情 ----- 管理权转移

 如上述例子,他把 原本属于 ap1 的空间,转给了 赋值之后的 ap3 管理了,所以,我们看到上述的 ap1 指向的是 empty。

 在 auto_ptr 当中的 拷贝构造函数,相当于是实现了 移动构造的玩法,直接交换 ap1 和 ap3 当中的指针变量,把 ap1 指向 ap3 当中的空,把 ap3 当中的指针指向 ap1 原本维护的指针。

 但是只是相当于,这里并不是移动构造的玩法,移动构造是把 右值 当中的 将亡值(即将释放的值),把其中的资源直接转移到 新的 值 来维护。如果不是 将亡值 ,比如是 左值,我们是不敢直接转移的,因为 此时左值在后序程序当中很有可能会用到。

但是 auto_ptr 当中也就相当于是 强制进行 移动构造,不管是左值还是右值,这么左的风险,在上述也说过了,ap1 是左值,在后序是很有可能会用到了,这就是它最大的问题。

 举例:

如果是不懂的人,对 赋值之后的 ap1 进行操作的话,编译器甚至都不会报错,但是一运行程序就会出事

	auto_ptr<A> ap1(new A(1));auto_ptr<A> ap2(new A(1));auto_ptr<A> ap3(ap1);// 此时把 _a 成员变量修改为了 public:ap1->_a++;ap2->_a++;

 成功生成解决方案:

 出事儿:

 auto_ptr 其实是一段失败的代码,但是已经有人使用 autp_ptr 写了项目,委员会不敢删,所以延续至今,但是在一般实践,公司当中明确规定不能使用 auto_ptr 。

 auto_ptr 模拟实现

// C++98 管理权转移 auto_ptr
namespace bit
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;// 一定要置空// 不然两个对象管理同一块空间// 在析构函数释放的时候就会释放两次// 就会报错}auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

 unique_ptr

 在C++11 当中提供更加稳定的 unique_ptr 智能指针。

cplusplus.com/reference/memory/unique_ptr/

 unique_ptr 的解决方案的话,非常的简单粗暴,就是直接不允许你进行 unique_ptr 智能指针之间的赋值拷贝。

	unique_ptr<A> up1(new A(1));unique_ptr<A> up2(new A(1));up1 = up2;

 编译报错:
 

 error C2280: “std::unique_ptr<A,std::default_delete<A>> &std::unique_ptr<A,std::default_delete<A>>::operator =(const std::unique_ptr<A,std::default_delete<A>> &)”: 尝试引用已删除的函数

 unique_ptr的模拟实现

对于 unique_ptr 的模拟实现很简单,我们在类当中说过,要想某一个函数不给外部使用,右里那个三种方式,第一种是只声明不实现;第二种是 把这个函数用 private 修饰:第三种是在函数之后写上 "= delete" 意思就是把这个函数删除掉。

但是第一种,值声明不实现,别人可以在外部给你实现这个函数,所以建议用后面的两种方法。 

template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(const unique_ptr<T>&sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;private:T* _ptr;
};

std::shared_ptr

当然,上述的 unique_ptr 版本的智能指针只是给我们手撕智能指针的模版,我们手撕一个 unique_ptr 非常简单。

但是 unique_ptr 比较不能解决 指针 赋值的问题。

所以,就有了 shared_ptr 智能指针的出现。

		shared_ptr<A> sp1(new A(1));shared_ptr<A> sp2(new A(1));sp1 = sp2;sp1->_a++;sp2->_a++;cout << sp1->_a << endl;cout << sp2->_a << endl;

 输出:
 

A(int a = 1)
A(int a = 1)
~A()
3
3
~A()

shared_ptr 模拟实现

 解决指针拷贝问题

 上述几种不同的 智能指针不同就不同在 拷贝构造函数 和 operator=() 两个函数的实现不同,

所以我们主要实现 shared_ptr 的拷贝构造函数,operator=()当中的 可以服用拷贝构造函数。

用一个shared_ptr 指针给另一个 shared_ptr  赋值和 拷贝构造,其中肯定是要 两个 智能指针管理两块空间的,主要是要解决 两个对象析构两次会报错的问题。

我们选择使用 引用计数的方式来解决:

也就是记录一下有多少 引用 引用了当前空间。在 某一个 智能指针 对象析构的时候,不先释放空间,先判断 当前引用计数是不是 0 ,如果是 大于 0的,说明当前 不止当前智能指针 指向这块空间,那么就 --引用计数;如果当前引用计数 是 0 ,那么说明当前就本 智能指针 指向这块空间,就可以 释放这块空间。

 但是,在类当中 应该使用什么 成员变量来 记录这个 引用计数呢?普通的变量可能是不能满足的,如果是 非静态的变量,每一个是存储在各个类当中的,我们要实现统一计数的话,非常的麻烦。

所以,我们使用静态的成员变量来存储这个 引用计数,因为 静态的成员变量不单单属于某一个对象,而是属于这个整个类,你可以理解为静态的成员变量是属于每一个对象的,每一个对象共用一个 静态成员变量。

 类似这样:

 代码实现:
 

public:// 构造函数shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}// 析构函数~shared_ptr(){// -- 引用计数 之后 如果 == 0 ,就可以释放空间了if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}}// 拷贝构造函数shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);}// operator=() 函数实现shared_ptr<T>& operator=(const shared_ptr<T>& sp){// 先判断 赋值和被赋值的两个指针是否是重复的// 是就直接返回if (_ptr == sp._ptr)return *this;// 判断当前 赋值指针在赋值出去之前// 是否是所维护空间的唯一指针if (--(*_pcount) == 0){delete _ptr;delete _pcount;}// 开始赋值_ptr = sp._ptr;_pcount = sp._pcount;// 引用计数++++(*_pcount);return *this;}private:T* _ptr;int* _pcount;};

如上所示,operator=()函数才是最难实现的,我们要判断当前对象,也就是被赋值 的对象 在当前赋值之前,是否 右引用其他对象,如果引用了其他的对象,那么要先 -- 引用计数,看-- 之后的引用计数是否 == 0,如果是 0 ,就要把原有的 空间给释放掉,然后才能进行赋值操作,赋值就简单了,直接把 赋值对象 当中的 两个成员赋值过来,然后在 ++ 引用计数即可。

 而且,还需要注意,自己给自己赋值的情况:比如 sp1 和 sp2 都指向了 同一块空间,那么对于 :
sp1 = sp1 和 sp2 =sp1 两者实际上都是自己给自己赋值,在上述的代码当中不进行判断的话,是没有什么问题的,但是,在上述判断之前空间,和赋值新的空间的操作都是多余做的了,所以我们可以 在函数的开口就判断,是不是自己给自己赋值的情况。

如果 有一个 sp3 委会一块空间,这块空间没有被其他指针来维护的话, 假设现在有 sp3 = sp3 这样的赋值的话,如果没有特殊判断来终止赋值的话,sp3 维护的资源 就会在 operator=() 当中 被释放掉,然后在把 sp3 赋值给 sp3 的话,就是一个已经被释放了的空间地址,当我们再次使用这个 sp3 的时候就会出问题。

可以利用 资源来判断是不是 同一块空间,也可以用 计数来判定也是可以的:

// 判断当前对象当中的原生指针指向的空间是否和 
// 当前要赋值的指针指向的空间是相同的
if(_ptr == sp._ptr_return *this;

 shared_ptr 完整代码实现:

template<class T>class shared_ptr{public:// RAII// 像指针一样shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);}// sp1 = sp5// sp6 = sp6// sp4 = sp5shared_ptr<T>& operator=(const shared_ptr<T>& sp){// 先判断 赋值和被赋值的两个指针是否是重复的// 是就直接返回if (_ptr == sp._ptr)return *this;// 判断当前 赋值指针在赋值出去之前// 是否是所维护空间的唯一指针if (--(*_pcount) == 0){delete _ptr;delete _pcount;}// 开始赋值_ptr = sp._ptr;_pcount = sp._pcount;// 引用计数++++(*_pcount);return *this;}// 返回引用计数int use_count() const{return *_pcount;}// 拿到原生指针T* get() const{return _ptr;}private:T* _ptr;int* _pcount;};

循环引用问题(用 weak_ptr 指针)

 shared_ptr 指针几乎没缺点,但是也不是意味着 完全没有缺点的,如下所示:
 

struct Node
{A _val;Node* _next;Node* _prev;
};int main()
{shared_ptr<Node> sp1(new Node);shared_ptr<Node> sp2(new Node);sp1->_next = sp2;sp2->_prev = sp1;return 0;
}

向上述的结点的指针链接 是非常 常规的操作,但是上述的操作就有问题,虽然上述能够自动释放空间,但是 以为 _next  和 _prev 两个指针的类型但是 Node*,不是 shared_ptr 智能指针类型的,所以这里是经典的类型不匹配。

有人就想到,把 _next  和 _prev 的指针类型改为 shared_ptr<Node> 的不就行了吗?确实可以:

struct Node
{A _val;shared_ptr<Node> _next;shared_ptr<Node> _prev;
};

但是,当我们把调用的函数都输出一遍,输出:

 只调用了 构造函数,但是 析构函数是没有 调用的,也就是说此时发生了 内存泄漏,而且,我们惊讶的发现,当只调用     sp1->_next = sp2; 的时候,就没有问题,正常释放两个空间当中的内容:
 

A(int a = 1)
A(int a = 1)
~A()
~A()

 所以,问题就出在     sp1->_next = sp2;     sp2->_prev = sp1; 这两句当中当中和,我们先把两个结点 链接关系画一下:

如上述所示,因为在 sp1._next 和 sp2._prev 各自都链接上了   对方的结点空间,所以 ,两个结点空间的 引用计数 静态变量 就会 ++ ,现在两个空间的 静态空间变量都是 2 了。

当主函数当中执行完毕之后,因为 sp1 比 sp2 先声明,所以 sp2 要先析构;sp2 析构,sp2 空间上的 引用计数就 -- 到1;然后 sp1 析构,也是一样的过程;sp2 和 sp1 析构之后如下所示:

 此时,就出现了 循环引用 的场景 ,_prev 是在 sp2 这个空间当中的 ,_next 是在 sp1 这个空间当中的,那么此时 不管谁先释放了,谁都不愿先调用自己的析构函数,因为:

  • 如果想要析构 _prev ,那么就要析构 第二个结点空间 的时候才会析构到 _prev ;
  • 但是,要想析构到 第二个结点空间,就得先析构 _next 所在的第一个结点空间,而第一个 结点所在空间要析构,又要 _prev 先析构,这不就死锁了吗?

 两个都不能先析构,那么就不能析构了。

 循环引用的场景介绍:

在外部有两个 智能指针,维护这两个不同的空间;在这两个空间当中,又各自有一个指针,你的指针管理着我的空间,我的指针管理着你的空间。


所以,为了解决循环引用问题,专门写了一个指针叫做 weak_ptr

weak_ptr 不是智能指针,而是 为了专门解决 循环引用问题,而专门写出来的一个 指针。

 所以我们发现,在官方库当中  weak_ptr 都没有 指针类型传参的构造函数:
 

 weak_ptr 实现,就是不使用 引用计数,不参与资源释放的管理,但是可以访问其中的资源。

 如果上述的 sp1 和 sp2 当中的空间都没有使用引用计数来管理的话,当 第一步 sp2 和 sp1 释放的时候,就会直接把这两个空间给释放了。就没有有后续 _next 和 _prev 两个指针的事情了。

 所以,weak_ptr 只是拿到智能指针当中的 指针,可以访问这个原生指针,但是对于这个指针维护的空间,和引用计数, 我 weak_ptr 是完全不管的,我值管访问,对于空间的维护是其他 智能指针管理的。

而且,虽然 weak_ptr 不支持用原生指针来构造对象,但是支持用 shared_ptr 智能指针来构造对象,所以,我们可像下面一样写:

struct Node
{A _val;weak_ptr<Node> _next;weak_ptr<Node> _prev;
};int main()
{shared_ptr<Node> sp1(new Node);shared_ptr<Node> sp2(new Node);sp1->_next = sp2;sp2->_prev = sp1;return 0;
}

输出:

A(int a = 1)
A(int a = 1)
~A()
~A()

 

 weak_ptr 完整代码:

template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}// 下述的拷贝构造函数 和 operator=()函数// 都不管其中的 空间释放 引用计数等等weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}// 下述是指针操作T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;// 不使用引用计数};
}

 boost库简介

 Boost库_百度百科 (baidu.com)

他其实是在 C++11 出来之前,由 C++委员会当中的一些成员,创建了 Boost 库社区,在这个社区当中探索出了很多 有用的语法,比如:右值引用,等等。

在 boost 库当中也诞生了 更好的 智能指针:

 C++11 当中相当于是沿用了 boost 库当中的一些智能指针,进行了一些细节上的修改,基本上属于是 cv了。

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

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

相关文章

milvus测试

milvus测试 目标 其实&#xff0c;我应该弄明白他的输入输出分别是什么&#xff1f; 输入是图片&#xff0c;图片经过ml模型进行特征提取&#xff0c;再在milvus中进行存储或者检索 部署 ✘ delldell-Precision-3630-Tower  /nvme/baum/git-project/milvus   master …

SpringBoot 如何使用 Ehcache 作为缓存

使用Spring Boot Sleuth进行分布式跟踪 在现代分布式应用程序中&#xff0c;跟踪请求和了解应用程序的性能是至关重要的。Spring Boot Sleuth是一个分布式跟踪解决方案&#xff0c;它可以帮助您在分布式系统中跟踪请求并分析性能问题。本文将介绍如何在Spring Boot应用程序中使…

选择适合变更管理的产品开发工具的要点和建议

什么是变更管理&#xff1f; 变更管理是指导组织改进的学科。由于可观察到的行为变化&#xff0c;它会导致永久性变化。它确保您的组织以彻底、有序和可持续的方式学习和改进。成功的改进项目需要个人和团队保持一致&#xff0c;他们有共同的愿景&#xff0c;他们知道如何定义…

MidJourney | AI绘画也有艺术

免费绘画&#xff0c;体验更多AI可关&注公&众&号&#xff1a;AI研究工厂

大数据学习(2)Hadoop-分布式资源计算hive(1)

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 承认自己的无知&#xff0c;乃是开启智慧的大门 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博>主哦&#x…

如何将gif变成视频?3个转换方法

如何将gif变成视频&#xff1f;没错&#xff0c;GIF是一种动态图片格式&#xff0c;与视频在本质上有所区别。在一些自媒体平台上&#xff0c;我们无法直接分享GIF格式的图片&#xff0c;但可以将其转换为视频格式后再进行分享。因此&#xff0c;当我们想要分享我们喜欢的GIF图…

香港硬防服务器的防御有什么优缺点?

​  在选择服务器时&#xff0c;安全性是一个重要的考虑因素。而对于那些需要高级防御功能的用户来说&#xff0c;香港硬防服务器可能是一个不错的选择。它也有一些优缺点需要考虑。 香港硬防服务器优点&#xff1a; 强大的硬件资源&#xff1a;香港硬防服务器拥有足够的硬件…

nginx如何安装 以及nginx的配置文件

Nginx 网站服务 是一个高性能 轻量级web服务软件&#xff0c; 高新能&#xff1a;对http并发连接的处理能很高&#xff0c;单台处理器可支持30000-50000个并发请求&#xff0c;&#xff08;一般设置在20000个左右&#xff09; 轻量级&#xff1a;nginx软件很小&#xff0c;安装…

ChromeDriver驱动最新版下载

下载地址ChromeDriver - WebDriver for Chrome - Downloads selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 113 Current browser version is 117.0.5938.150 with binar…

vim基础指令(自用)

这个是自己随便写的&#xff0c;类似于笔记 vim 多模式编辑器 查看指令&#xff1a; gg&#xff1a; 定位光标到最开始行 shift(按)g 定位到最结尾行 nshift(按)g 定位到任意行 shift&#xff04; 定位到本行结尾 0 定位到本行开头 w&#xff1a;跨单词移动 h.j.k,l: 左下上右 …

各种业务场景调用API代理的API接口教程

API代理的API接口在各种业务场景中具有广泛的应用&#xff0c;本文将介绍哪些业务场景可以使用API代理的API接口&#xff0c;并提供详细的调用教程和代码演示&#xff0c;同时&#xff0c;我们还将讨论在不同场景下使用API代理的API接口所带来的好处。 哪些业务场景可以使用API…

42. QT中开发Android配置QFtp功能时遇到的编译问题

1. 说明 此问题仅适用在QT中开发Android程序时&#xff0c;需要适用QFtp功能的情况。一般情况下&#xff0c;如果开发的是Windows或者Linux系统下的程序&#xff0c;可能不会出现该问题。 2. 问题 【Android】在将QFtp的相关代码文件加入到项目中后&#xff0c;编译项目时会…

PyTorch 入门

一、说明 深度学习是机器学习的一个分支&#xff0c;其中编写的算法模仿人脑的功能。深度学习中最常用的库是 Tensorflow 和 PyTorch。由于有各种可用的深度学习框架&#xff0c;人们可能想知道何时使用 PyTorch。以下是人们更喜欢使用 Pytorch 来完成特定任务的原因。 Pytorch…

BGP服务器租用腾讯云和阿里云价格对比

BGP云服务器像阿里云和腾讯云均是BGP多线网络&#xff0c;速度更快延迟更低&#xff0c;阿里云BGP服务器2核2G3M带宽优惠价格108元一年起&#xff0c;腾讯云BGP服务器2核2G3M带宽95元一年起&#xff0c;阿腾云atengyun.com分享更多云服务器配置如2核4G、4核8G、8核16G等配置价格…

【编程技巧】用size_t定义数量有什么好处

使用 size_t 来定义数量有几个好处&#xff1a; 平台无关性&#xff1a;size_t 是一个无符号整数类型&#xff0c;其大小适应当前编译环境的体系结构&#xff0c;通常是足够大以容纳目标平台上的最大对象大小。这使得代码在不同平台上更具可移植性。 正确性和安全性&#xff…

2023版 STM32实战7 通用同步/异步收发器(串口)F103/F407

串口简介和习惯 -1-通用同步异步收发器 (USART) 能够灵活地与外部设备进行全双工数据交换&#xff0c;满足外部设备对工业标准 NRZ 异步串行数据格式的要求。 -2-硬件流控制一般是关闭的 -3-波特率指单位时间传输bit个数 -4-数据位一般是8位 -5-一般无校验位 编写代码思路 -1-参…

使用wireshark解析ipsec esp包

Ipsec esp包就是ipsec通过ike协议协商好后建立的通信隧道使用的加密包&#xff0c;该加密包里面就是用户的数据&#xff0c;比如通过的语音等。 那么如何将抓出来的esp包解析出来看呢&#xff1f; 获取相关的esp的key信息. 打开wireshark -> edit->preferences 找到pr…

功能测试复习

一。测试流程 1.需求评审 确保各部门需求理解一致 2.计划编写 测什么&#xff0c;谁来测&#xff0c;怎么测 3.用例设计 验证项目是否符合需求的操作文档 4.用例执行 项目模块开发完成开始执行用例文档实施测试 5.缺陷管理 对缺陷进行…

深入探讨芯片制程设备:从原理到实践

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 在现代科技领域&#xf…

使用chat-GPT接口提取合同中关键信息

1 业务需求 目前公司有几千份合同&#xff0c;而且还会不断的增长&#xff1b;现在需要将合同中的关键信息提取出来给业务使用&#xff0c;业务现在需要将这些关键字段信息录入存档到档案系统&#xff1b;人工去阅读整个合同去提取这些信息&#xff0c;是很浪费人力的&#xff…