【智能指针】—— 我与C++的不解之缘(三十三)

一、智能指针的使用

还记得,在异常学习的时候,我们分析出了一个问题

double Divide(int x, int y)
{if (y == 0){throw string("the y is zero");}return (double)x / double(y);
}
void test(int x, int y)
{int* arr = new int[10];Divide(x, y);delete[] arr;cout << "delete[] arr" << endl;
}int main()
{while (1){int x, y;cin >> x >> y;try{test(x, y);}catch (const string& str){cout << str << endl;}catch (...){cout << "unknown exception" << endl;}}return 0;
}

在上述代码中,Divide函数,如果y==0就会抛出异常,并且在该函数内没有捕获该异常,就继续将异常抛给外层调用的函数test,在testnew了一个int数组,但是没有捕获Divide函数抛出的异常,程序直接接跳到main函数当中去,就导致申请的空间资源没有被释放。

在这里插入图片描述

在异常学习中,我们的解决方法就是在test函数中捕获Divide函数抛出的异常,进行资源的释放再将异常重新抛出。

void test(int x, int y)
{int* arr1 = new int[10];try{Divide(x, y);}catch (...){delete[] arr;cout << "delete[] arr" << endl;throw;}delete[] arr;cout << "delete[] arr" << endl;
}

在这里插入图片描述

这样看起来我们似乎解决了这一个问题,但是如果我们开辟了两个数组呢?

void test(int x, int y)
{int* arr1 = new int[10];int* arr2 = new int[10];try{Divide(x, y);}catch (...){delete[] arr1;cout << "delete[] arr1" << endl;delete[] arr2;cout << "delete[] arr2" << endl;throw;}delete[] arr1;cout << "delete[] arr1" << endl;delete[] arr2;cout << "delete[] arr2" << endl;
}

在上述代码中,如果我们在new第二个数组时,new抛异常了呢?(这里如果第一个new抛异常,那没啥问题)

我们总不能再给第二个new套一层try吧,那不现实;如果申请了多个资源,每一个都要套上try那太不现实了。

在当时我们也没有解决这一个问题,但是有了智能指针那就好很多了。

先来看一下什么是智能指针

二、RAII和智能指针

RAII思想

  • RAIIResource Acquisition Is Initialization的缩写,它是一个管理资源的类的设计思想;本质上就是利用对象生命周期来管理获取到的动态资源,避免发生内存泄露(这里资源指内存、文件指针、网络连接、互斥锁等等)
  • RAII思想:在获取到资源时把资源委托给一个对象,接着控制对资源的访问,这样资源在该对象的生命周期内始终是有效的,最后该对象生命周期结束,在析构的时候释放了资源;这样我们就保证了资源的正常释放,就可以结局上述资源泄露的问题。

那什么意思呢?

简单来说就是,我们不要去主动管理这些动态资源了,把这些动态资源交个一个对象去管理,这样出了这个对象的作用域,该对象的析构函数就会把这些动态资源自动释放,就不需要我们自己去释放了。

template<class T>
class smartptr
{
public:smartptr(T* ptr):_ptr(ptr){}~smartptr(){delete[] ptr;cout << "delete []" << endl;}private:T* _ptr;
};

有了上面代码,我们就可以创建smartptr来帮助我们管理动态资源

void test(int x, int y)
{smartptr<int> arr1(new int[10]);smartptr<int> arr2(new int[10]);Divide(x, y);
}

在这里插入图片描述

可以看到,无论Divide是否抛异常,我们申请的资源都能成功释放(因为arr1arr2出了作用域就调用析构函数,就会对资源进行释放

智能指针思想

通过观察上述代码,我们可以发现一个问题:如何访问动态资源呢?

我们将动态资源交给一个对象去管理,是可以解决资源泄露的问题;但是我们如何去访问这个动态资源呢?

所以为了方便我们访问动态资源,智能指针就还要实现重载operator*operator->opeartor[]这些运算符

这里这种思想就类似于迭代器,我们可以像指针一样去访问迭代器,这里智能指针也一样,我们也要可以像指针一样去访问智能指针。

template<class T>
class smartptr
{
public:smartptr(T* ptr):_ptr(ptr){}T& operator*(){return *_ptr;}T& operator[](size_t i){return _ptr[i];}T operator->(){return _ptr;}~smartptr(){delete[] _ptr;cout << "delete []" << endl;}
private:T* _ptr;
};

这里其实还存在一个致命的问题,那就是拷贝的问题:

对于我们自己申请的资源,我们就行拷贝(赋值)时,就是简单的值拷贝,并且释放的时候我们就只需要释放一个即可

但是如果使用智能指针,拷贝肯定不能使用深拷贝(我们想要的就是值拷贝),那我们该如何去释放这个资源呢?

**同一个资源是不能释放两次的;**现在来看C++库里面的智能指针是如何解决这一问题的。

三、C++标准库中智能指针的使用

C++标准库中智能指针都在这个头文件下;

智能指针有很多种,除了weak_ptr以外都符合RAII和可以像指针一样访问的行为,在原理上来说就是解决拷贝的问题不同。

auto_ptr

auto_ptr:这是C++98中设计出来的智能指针,它解决拷贝问题的方法就是在拷贝时将被拷贝对象资源的管理权转移给拷贝对象(简单来说就是,我把资源转给你,我自己置为nullptr),这个可以说很糟糕,它会把被拷贝对象悬空,我们再访问就会报错;这里不推荐使用auto_ptr

这里为了观察就简单实现一个Date

struct Date
{Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}int _year;int _month;int _day;
};
int main()
{auto_ptr<Date> ap1(new Date);auto_ptr<Date> ap2(ap1);//拷贝之后ap1被置为空return 0;
}

在这里插入图片描述

~Date()

最后资源也是释放一次。

unique_ptr

unique_ptrC++11设计出来的,它解决拷贝问题的方法就简单粗暴了,直接不支持拷贝,只支持移动;

在不需要拷贝的场景下就推荐使用unique_ptr

通过看文档可以发现,它的拷贝构造和拷贝赋值是delete掉的。

在这里插入图片描述

在这里插入图片描述

int main()
{unique_ptr<Date> up1(new Date);//unique_ptr<Date> up2(up1);//不支持拷贝//支持移动,但移动之后up1也置为空,使用要小心unique_ptr<Date> up2(move(up1));return 0;
}

shared_ptr

前面两个智能指针,一个拷贝是管理权转移,应该干脆就不支持,那还是没有到达我们想要的结果,现在来看shared_ptr

shared_ptrC++11设计的智能指针,它支持拷贝,也支持移动;

需要拷贝的场景就需要它了。(其底层使用引用计数来实现的

int main()
{shared_ptr<Date> sp1;shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp1);//查看当前有多少对象管理这一资源cout << sp1.use_count() << endl;sp1->_year = 2025;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;return 0;
}
3
2025
2025
2025
~Date()

weak_ptr也是C++11设计的智能指针,它和上述智能指针不同,不支持RAII它不能直接管理资源;

weak_ptr实现本质上是为了解决shared_ptr循环引用导致内存泄露的问题。

删除器

智能指针的析构默认是进行delete来释放资源,那也就是说,如果我们将不是new的资源交给智能指针,在析构的时候就会崩溃;

所以智能指针在构造时就支持给一个删除器。

本质上所谓的删除器就是一个可调用对象,这个可调用对象中实现我们需要是释放资源的方式;

当我们在构造智能指针时,给了删除器,在智能指针析构时就会调用删除器去释放资源。

而我们又经常使用new[],所以unique_ptrshared_ptr都特化了一个[]的版本,我们在使用时只需类型给成T[]即可;(例如:unique_ptr<Date[]> up1(new Date[10])shared_ptr<Date[]> sp1(new Date[10])

int main()
{unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);//删除器//删除器是一个可调用对象,那我们就可以使用仿函数、函数指针、lambda来做删除器//仿函数unique_ptr<Date, DeleteArr<Date>> up2(new Date[3]);//类模版这里要传的是类型shared_ptr<Date> sp2(new Date[3], DeleteArr<Date>());//构造函数这里我们要传对象//函数指针//这里类模版要传的是类型,而根据函数类型又没有办法得到函数,构造也要显示传递unique_ptr<Date, void(*)(Date*)> up3(new Date[3],DeleteArrFunc<Date>);shared_ptr<Date> sp3(new Date[3], DeleteArrFunc<Date>);//lambdaauto del = [](Date* ptr) {delete[] ptr; };//这里我们没办法指定lambda的类型,所以要先创建一个lambda对象然后使用decltype来推它的类型unique_ptr<Date, decltype(del)> up4(new Date[3], del);//shared_ptr就很好用了,只用在构造函数时传递即可shared_ptr<Date> sp4(new Date[3], [](Date* ptr) {delete[] ptr; });return 0;
}

这里shared_ptr1除了可以使用指向资源的指针构造,还可以使用make_shared有初始化资源对象的值直接进行构造。

shared_ptrunique_ptr都支持operator bool的类型转换,如果智能指针对象是一个空的对象就返回false;赋值返回true。(这样我们就可以直接对智能指针对象进行判断是否为空)。

四、智能指针的实现原理

首先对于auto_ptr,它的拷贝是管理权转移,感觉很不符合逻辑;而unique_ptr是直接不支持拷贝,这两种智能指针实现起来还是非常简单,这里就不详细叙述了;

	template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& p):_ptr(p._ptr){p._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (*this != ap){if (_ptr)delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T& operator*(){return *_ptr;}T& operator[](size_t i){return _ptr[i];}T* operator->(){return _ptr;}~auto_ptr(){if (_ptr)delete _ptr;}private:T* _ptr;};template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}unique_ptr(unique_ptr<T>& p) = delete;unique_ptr<T>& operator=(unique_ptr<T>& p) = delete;unique_ptr(unique_ptr<T>&& p):_ptr(p._ptr){p._ptr = nullptr;}T& operator*(){return *_ptr;}T& operator[](size_t i){return _ptr[i];}T* operator->(){return _ptr;}private:T* _ptr;};

share_ptr实现原理

shared_ptr使用了引用计数,简单来说呢,我们不光要在智能指针中记录要管理的资源,还要记录一下当前管理资源的智能指针的个数,所以这里我们需要一个引用计数;

那如何去实现这个引用计数呢?

这里首先肯定不能在类中存放一个值;

那静态成员变量是否可以呢?

显然是不可以的,因为静态成员变量是属于类的,我们想要的引用计数是和管理资源有关的,所以这里我们就只能存放int*,采用堆上动态开辟的方式,在构造的时候开辟一块空间,在拷贝的时候,将指针值拷贝给另一个对象,并且对值进+1

那这样多个shared_ptr管理一块资源时,当析构的时候就--引用计数,当引用计数减到0时,表示当前析构的就是管理这一块资源的最后一个智能指针对象,就要析构资源。

在这里插入图片描述

在这里插入图片描述

OK呢,那现在就来简简单单手搓一个简易版shared_ptr出来

首先对于shared_ptr的成员:T*的指针、int*pcount引用计数;

简单的operator*operator->operator[]这些就不解释了,直接看代码:

	template<class T>class shared_ptr{T& operator*(){return *_ptr;}T& operator[](size_t i){return _ptr[i];}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};

构造函数

对于构造函数,首先就是默认构造,我们直接将_ptr_pcount赋值为nullptr即可。

然后就是:我们在创建一个shared_ptr的智能指针对象时,要做的就是开辟一块引用计数的空间,并赋值成1(表示当前有一个对象管理这一块资源);然后把传过来的一块资源赋值给_ptr(让ptr指向要管理的资源即可)。

		shared_ptr():_ptr(nullptr), _pcount(nullptr){}shared_ptr(T* ptr):_ptr(ptr){_pcount = new int(1);}

拷贝构造

对于拷贝构造,当我们调用拷贝构造时,就表明我们要将一个智能指针对象管理的资源共享给另一个智能指针对象,此时我们的引用计数要进行+1

这里因为我们调用拷贝构造时,我们当前对象是没有管理任何资源的(_ptrpcount都为nullptr),我们才能直接将被拷贝对象sp_ptr_pcount直接赋值给我们*this_ptr_pcount

		shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}

拷贝赋值

对于拷贝赋值,它拷贝构造那样,可以直接进行赋值操作;

当我们调用拷贝赋值时,我们当前对象可能是管理着其他资源的;所以我们要先将当前管理的资源进行处理(如果有其他对象管理着这一块资源,那--引用计数即可;如果没有其他对象管理这一块资源,我们还要对其进行释放)。

这一块对资源进行处理的操作,我们可以发现和析构函数的逻辑一样,所以我们可以将其单独写成一个函数release

		void release(){(*_pcount)--;if (*_pcount == 0){delete _ptr;delete _pcount;}_ptr = nullptr;_pcount = nullptr;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){release();_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;return *this;}

析构函数

对于析构函数,它的逻辑就是--引用计数,如果引用计数减到0,那就释放资源;(逻辑和上面拷贝赋值处理资源的逻辑一样

这里就可以直接复用release

		~shared_ptr(){release();}

这里对于拷贝赋值和析构函数这里,博主还有一种想法:

我们在拷贝赋值的参数那里让他传值传参,这样就会调用一次拷贝构造,构造了一个临时对象sp

我们再让this指向的_ptr_pcountsp进行一下交换,这样出了拷贝赋值函数,sp会自动调用析构函数;

这样我们只需要在析构函数内部实现处理资源的操作就OK了。

		void swap(shared_ptr<T>& sp){std::swap(_ptr, sp._ptr);std::swap(_pcount, sp._pcount);}shared_ptr<T>& operator=(shared_ptr<T> sp){swap(sp);return *this;}~shared_ptr(){(*_pcount)--;if (*_pcount == 0){delete _ptr;delete _pcount;}_ptr = nullptr;_pcount = nullptr;}

上面这种方法,博主在vector模拟实现时有所耳闻,也是非常好理解的

我们拷贝赋值的参数写的是shared_ptr<T>,这样在传参时是传值传参,就会去调用拷贝构造,构造一个新的对象sp指向被调用对象管理的资源;

sp的作用域就是operator=函数内,所以我们把this的指向的对象和生成的形参对象sp进行交换(值交换);

这样出了作用域sp要调用析构函数,就会把*this对象原来管理的资源进行处理;

到这里,简易版的shared_ptr就实现完成了

现在我们来加上删除器

删除器

删除器,我们要想在类中可以调用这个删除器,那我们要将删除器存下来;

shared_ptr我们只需要在构造函数中传递就可以了,那我们构造函数如下面所示

		template<class D>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}

但是但是,对于这个类型D我们只在构造函数中指定它是什么啊,那在类中如何去存储这个删除器呢?

真的要在模版参数那里多一个吗?库里面也没有多这一个模版参数啊.

这里就使用function包装器就可以解决问题了

因为我们的删除器肯定都是没有返回值(void),且参数肯定是T*,所以使用function<void(T*)>包装即可。

这里在展示代码之前,罗列几个要注意的点:

  • 在拷贝赋值时,我们要将删除器一同传递过去;(不同的类型,删除器不一样
  • 在析构逻辑中,我们直接调用删除器去资源,但是对于引用计数的修改还是需要我们就行操作的。
  • 删除器我们要给缺省值,当我们在构造函数不穿第二个参数时,我们默认的删除器是delete的。
	template<class T>class shared_ptr{public:explicit shared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}void swap(shared_ptr<T>& sp){std::swap(_ptr, sp._ptr);std::swap(_pcount, sp._pcount);std::swap(_del, sp._del);}shared_ptr<T>& operator=(shared_ptr<T> sp){swap(sp);return *this;}~shared_ptr(){(*_pcount)--;if (*_pcount == 0){//delete _ptr;_del(_ptr);delete _pcount;}_ptr = nullptr;_pcount = nullptr;}T& operator*(){return *_ptr;}T& operator[](size_t i){return _ptr[i];}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr;};};

五、shared_ptr循环引用问题

对于shared_ptr大多数情况下已经可以去管理资源了,支持RAII也支持拷贝;

但是有一种特殊情况,循环引用的场景下,还是会遇到问题的;(会导致资源没得到释放)

struct ListNode
{int _date;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prve;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prve = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}

在这里插入图片描述

可以看到,在上述代码中,我们让n1->_next指向n2n2->_prve_prve指向n1

这样指向之后我们发现n1n2的引用计数都变成了2,并且知道程序结束,也没有释放资源。

这种情况,就是我们所说的内存泄露。

我们现在来分析一下,为什么会造成内存泄露呢?

在这里插入图片描述

如上图所示,n1->_next = n2n2->_next = n1之后,n1n2对应的引用计数都+1,变成了2

那我们现在n1n2调用析构:
在这里插入图片描述

n1n2调用析构之后,其对应的引用计数-1减到了1,没有减到0,这两块空间还没有释放;

那我们右边节点什么时候释放呢,左边节点中的_next管理着,左边节点中_next析构后,右边节点就释放了;

那左边节点的_next什么时候释放呢?,那要等到左边节点析构,右边节点的_prve管理着,等右边节点_prve析构,左边节点就释放了。

左边节点右边的_prve管理着,右边节点左边的_next管理着,那这样都在等对方析构,而谁都不会释放,就形成了循环引用,从而导致内存泄露。

weak_ptr解决循环引用问题

那这个问题如何解决呢?

要像解决这个问题,我们来看一下这个问题的本质是什么?

**那就是我们将n1->_next绑定n2节点时,n2节点的引用计数会+1;将n2->_prve绑定n1节点时,n1几点的引用计数会+1。**这样就导致我们在析构n1n2时,引用计数-1之后不等于0,就无法释放资源。

简单来说,就是n1->_nextn2->_prve参与了资源的管理。

c++11还有一种智能指针weak_ptr,它就是专门来解决这个问题的。

我们先来看一下weak_ptr

在这里插入图片描述

其实通过观察weak_ptr的构造函数和赋值重载就可以发现,它支持使用shared_ptr去构造和赋值;

但是它有一个特点,我们将shared_ptr的智能指针对象赋值给weak_ptr,我们shared_ptr对象的计数引用不会变化(weak_ptr不会参与shared_ptr的管理资源)。

那这样,我们再看上述问题,我们将ListNode结构体中_next_prve的类型改成weak_ptr

那这样,将n1->_next绑定n2节点时,n2节点的引用计数不会+1

n2->_prve绑定n1节点时,n1节点的引用计数不会+1

这样我们在析构n1n2时,引用计数-1就等于0,就会释放资源。

struct ListNode
{int _date;//std::shared_ptr<ListNode> _next;//std::shared_ptr<ListNode> _prve;std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prve;~ListNode(){cout << "~ListNode()" << endl;} 
};
int main()
{std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prve = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}

在这里插入图片描述

可以看到引用计数并没有+1,也成功析构,没有造成资源泄露。

weak_ptr

这里简单了解有效weak_ptr

  • weak_ptr不支持RAII,也不支持访问资源,我们在文档中也会发现weak_ptr构造没有支持绑定资源,而是支持绑定到shared_ptr;在绑定到shared_ptr时,不会增加shared_ptr的引用计数。
  • weak_ptr没有重载operator*operator->,它不参与资源管理(如果weak_ptr绑定的shared_ptr已经析构了,那如果再去访问就和危险);
  • weak_ptr支持了expired检查指向的资源是否过期,use_count也支持获取shared_ptr的引用计数。
  • weak_ptr还支持了lock,它可以返回shared_ptr;如果资源已经释放,那返回的就是空对象;如果没有释放,那返回的shared_ptr可以进行访问资源。

六、内存泄露

什么是内存泄漏?

这个问题之前就有所耳闻;内存泄漏指因为疏忽或者错误造成程序没有释放已经不再使用的内存,(右边就是忘记释放或者异常导致未能释放);

内存泄漏指的实际上是程序分配某部分内存以后,因为某种失误,导致失去了对这些内存的控制,造成了内存的浪费

内存泄漏的危害

在我们平常写代码过程中,我们感受不到内存泄漏带来的影响;因为我们写的普通程序,它运行一会就结束了;

进程正常结束,页表映射关系解除,物理内存也就释放了;

但是对于一些长期运行的内存,如果出现内存泄漏,那影响很大了,就好比操作系统后台服务器、长时间运行的客户端;如果出现内存泄漏,就导致可用的内存不断减少,运行速度越来越慢,最后卡死。

现在已经支持一些对于内存泄露的检查

但是我们能够避免内存泄露还是尽量去避免的。

养成良好的习惯,申请的内存空间记得去释放;

如果遇到异常问题,可能我们无法彻底去解决,那就使用智能指针去解决

当然使用智能指针要注意避免循环引用的情况。

对于定期使用内存泄露工具检测,尤其是在一个项目上线之前。

简单总结

内存泄漏非常常见,我们尽可能的去避免;

  • 在写代码是去预防:使用智能指针去管理资源;
  • 事后查错:使用泄漏检测工具去定期检查。

七、shared_ptr线程安全问题

这部分呢,在博主学习了linux线程之后,再来叙述。

八、Boost库

最后,我们来了解一下Boost

Boost库是c++语言标准库提供的扩展的一些C++程序库,Boost社区建立的初衷之一就是为了c++标准化工作提供参考。

Boost社区的发起人Dawes本人就是C++委员会的成员之一。

Boost库的开发中Boost社区在这个方向上取得了丰硕的成果。

  • C++98有了第一个智能指针auto_ptr

  • C++boost库给出了更多实用的智能指针scoped_ptr/scoped_array和shared_ptr/shared_array/weak_ptr

  • C++ TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版。

  • C++ 11,引⼊了unique_ptrshared_ptrweak_ptr。需要注意的是unique_ptr对应boost的

漏,那影响很大了,就好比操作系统后台服务器、长时间运行的客户端;如果出现内存泄漏,就导致可用的内存不断减少,运行速度越来越慢,最后卡死。

现在已经支持一些对于内存泄露的检查

但是我们能够避免内存泄露还是尽量去避免的。

养成良好的习惯,申请的内存空间记得去释放;

如果遇到异常问题,可能我们无法彻底去解决,那就使用智能指针去解决

当然使用智能指针要注意避免循环引用的情况。

对于定期使用内存泄露工具检测,尤其是在一个项目上线之前。

简单总结

内存泄漏非常常见,我们尽可能的去避免;

  • 在写代码是去预防:使用智能指针去管理资源;
  • 事后查错:使用泄漏检测工具去定期检查。

到这里本篇关于智能指针的讲解就结束了,感谢各位的支持!!!
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

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

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

相关文章

Hadoop+Spark 笔记 2025/4/21

读书笔记 定义 1. 大数据&#xff08;Big Data&#xff09; - 指传统数据处理工具难以处理的海量、高速、多样的数据集合&#xff0c;通常具备3V特性&#xff08;Volume体量大、Velocity速度快、Variety多样性&#xff09;。扩展后还包括Veracity&#xff08;真实性&#x…

femap许可不足如何解决

在复杂的工程仿真领域&#xff0c;Femap以其强大的功能和广泛的应用场景而备受青睐。然而&#xff0c;随着用户需求的增长和项目规模的扩大&#xff0c;Femap许可不足的问题逐渐凸显&#xff0c;成为了许多工程师和团队面临的挑战。本文将为您详细解析Femap许可不足的原因&…

【Microsoft Store 中的软件推荐】

目录&#xff1a; &#x1f600; TranslucentTB&#x1f600; Snipaste&#x1f600; Watt Toolkit&#x1f600; NVIDIA Control Panel&#x1f600; Typedown 微软应用商店中的软件会直接安装在C盘&#xff0c;所以&#xff0c;下面分享的这些是即超级好用&#xff0c;又占用…

AOSP Android14 Launcher3——RecentsView最近任务数据加载

最近任务是Launcher中的一个重要的功能&#xff0c;显示用户最近使用的应用&#xff0c;并可以快速切换到其中的应用&#xff1b;用户可以通过底部上滑停顿进入最近任务&#xff0c;也可以在第三方应用底部上滑进最近任务。 这两种场景之前的博客也介绍过&#xff0c;本文就不…

Flink介绍——实时计算核心论文之Flink论文

引入 通过前面的文章&#xff0c;我们梳理了大数据流计算的核心发展脉络&#xff1a; S4论文详解S4论文总结Storm论文详解Storm论文总结Kafka论文详解Kafka论文总结MillWheel论文详解MillWheel论文总结Dataflow论文详解Dataflow论文总结 而我们专栏的主角Flink正是站在前人的…

极狐GitLab CEO 柳钢受邀出席 2025 全球机器学习技术大会

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;关于中文参考文档和资料有&#xff1a; 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 2025 年 4 月 18 日至 19 日&#xff0c;2025 全球机器学习技术大会&#xff08;ML-Summit 2025&#xff09;在上海隆重举行。…

Linux Sed 深度解析:从日志清洗到 K8s 等12个高频场景

看图猜诗&#xff0c;你有任何想法都可以在评论区留言哦~ 摘要&#xff1a;Sed&#xff08;Stream Editor&#xff09;作为 Linux 三剑客之一&#xff0c;凭借其流式处理与正则表达式能力&#xff0c;成为运维场景中文本批处理的核心工具。本文聚焦生产环境高频需求&#xff…

C++ STL 容器简介(蓝桥杯适用精简版)

C的万能头文件是&#xff1a; #include <bits/stdc.h> 一、常用 STL 容器 1.vector&#xff08;动态数组&#xff09; #include<iostream> #include<string> #include <vector> #include <algorithm> // 包含排序所需的头文件 using namespa…

Java语言的进化:JDK的未来版本

作为一名Java开发者&#xff0c;我们正处在一个令人兴奋的时代&#xff01;Java语言正在以前所未有的速度进化&#xff0c;每个新版本都带来令人惊喜的特性。让我们一起探索JDK未来版本的发展方向&#xff0c;看看Java将如何继续领跑编程语言界&#xff01;&#x1f4aa; &…

不要使用Round函数保留小数位了

不要使用Round函数保留小数位了 如果你表格不需要保留公式&#xff0c;那么就不要使用Round函数保留小数位了。用Excel工作圈插件&#xff0c;可以轻松以数值形式保留小数位&#xff0c;且支持合并单元格、不连贯区域快速处理。 如下图&#xff0c;有文本&#xff0c;有跨行合并…

【C++】入门基础【下】

目录 一、缺省参数二、函数重载1. 函数类型不同2. 参数个数不同3、函数类型顺序不同 三、引用1、引用的概念和定义2、引用的功能2.1 功能1&#xff1a; 做函数形参&#xff0c;修改形参影响实参2.2 功能2&#xff1a; 做函数形参&#xff0c;减少拷贝&#xff0c;提高效率2.3 功…

git比较不同分支的不同提交文件差异

背景&#xff1a;只想比较某2个分支的某2次提交的差异&#xff0c;不需要带上父提交。 以commitA为基准&#xff0c;用commitB去比较差异 直接上代码&#xff1a; commitAxxxx1 commitBxxxx2 outputFile"output.txt"# 获取与第一个父提交的文件列表 filesA$(git di…

Linux内核之struct pt_regs结构

前沿 项目开发最近进行系统hook功能实现相关业务&#xff0c;主要在centos7和8系列环境开发下关功能。调研了相关知识点&#xff0c;发现在系统7和8上内核版本差别比较大&#xff0c;7-3.10.x系列版本&#xff0c;8-4.18.x系列版本。依据两个系统的内核情况根对应的内核符号表进…

《从混乱到有序:ArkUI项目文件结构改造指南》

在ArkUI开发的广袤天地里&#xff0c;构建一个清晰、有序的文件结构&#xff0c;是打造优质应用的关键。一个合理的文件结构&#xff0c;就像为开发者精心绘制的地图&#xff0c;在项目的各个阶段&#xff0c;都能提供明确的指引&#xff0c;让开发过程顺畅无阻。今天&#xff…

C#基于Sunnyui框架和MVC模式实现用户登录管理

C#基于Sunnyui框架和MVC模式实现用户登录管理 1 Controller1.1 UserManagementController.cs&#xff08;控制器入口&#xff09; 2 Model2.1 UserRepository.cs&#xff08;用户管理模型&#xff09;2.2 User.cs&#xff08;用户结构体&#xff09;2.3 SQLiteHelper.cs&#x…

自然语言处理(NLP)技术的实例

自然语言处理&#xff08;NLP&#xff09;技术在各个领域都有广泛的应用&#xff0c;以下是几个例子&#xff1a; 语音识别&#xff1a;通过NLP技术&#xff0c;计算机可以识别和理解语音指令&#xff0c;例如智能助手如Siri和Alexa就是通过语音识别技术实现与用户的交互。 机…

Spring Boot实战(三十六)编写单元测试

目录 一、什么是单元测试&#xff1f;二、Spring Boot 中的单元测试依赖三、举例 Spring Boot 中不同层次的单元测试3.1 Service层3.2 Controller 层3.3 Repository层 四、Spring Boot 中 Mock、Spy 对象的使用4.1 使用Mock对象的背景4.2 什么是Mock对象&#xff0c;有哪些好处…

aws服务(四)文件存储服务S3 介绍使用代码集成

一、介绍 1、简介 Amazon S3 是 Amazon Web Services 提供的一种对象存储服务(Object Storage),用于在云中存储和检索任意数量的数据。它以高可用性、高扩展性和高持久性著称,非常适合用来存储网站资源、数据备份、日志文件、大数据、机器学习输入输出等。 2、主要特性 …

应用信息1.13.0发布

增加工具箱 增加启动器功能 增加布局查看器 增加手动安装和卸载应用 增加APK文件解析 增加应用多选功能 增加查看应用预装版本 增加应用信息和ADB命令导出 修复其它问题... 百度下载&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;1234

【Vue3 实战】插槽封装与懒加载

一、为什么需要插槽&#xff1f;从一个面板组件说起 在电商首页开发中&#xff0c;经常遇到这样的场景&#xff1a; 「新鲜好物」「人气推荐」同样类型模块都需要相同的标题栏&#xff0c;但内容区布局不同 这时候&#xff0c;插槽&#xff08;Slot&#xff09;就像一个「内容…