当前位置: 首页 > ds >正文

C++智能指针

前言

一、智能指针的使用场景

二、RAII和智能指针的设计思路

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

四、智能指针的原理

4.1 auto_ptr模拟实现

4.2 unique_ptr模拟实现

4.3 shared_ptr模拟实现

4.4 定制删除器

4.5 shared_ptr循环引用问题

4.6 weak_ptr介绍

4.7 weak_ptr模拟实现

五、C++11和boost中智能指针的关系

六、内存泄漏

6.1 什么是内存泄漏,内存泄漏的危害

6.2 如何检测内存泄漏

6.3 如何避免内存泄漏

总结


前言

本篇文章来讲智能指针,智能指针的使用在实际中还是非常多的,也是避免内存泄漏一个非常有用的手段,那我们就正式开始


一、智能指针的使用场景

根据上一篇我们讲异常的那个例子可以看到,执行流乱跳有时候是优点,有时候是缺点,我们手动申请的资源可能会因为异常的原因,而跳过释放的地方,而导致内存泄漏
double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}void Func()
{int* array1 = new int[10];int* array2 = new int[10];try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么}delete[] array1;delete[] array2;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

虽然异常的重新抛出可以解决这里的问题,但是现在只有两个资源,第一个new抛异常不需要释放,第二个new抛异常要释放第一个new,Divide抛异常要释放两个new,那如果还有更多申请的资源呢?就要一直套try/catch,代码非常难看,在这种场景下,就可以使用智能指针来解决。


二、RAII和智能指针的设计思路

RAII是Resource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指
针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,
资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常
释放,避免资源泄漏问题。
智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。
template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;}// 重载运算符,模拟指针的⾏为,⽅便访问资源T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}
private:T* _ptr;
};
double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}void Func()
{// 这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了SmartPtr<int> sp1 = new int[10];SmartPtr<int> sp2 = new int[10];for (size_t i = 0; i < 10; i++){sp1[i] = sp2[i] = i;}int len, time;cin >> len >> time;cout << Divide(len, time) << endl;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

根据栈展开查找catch子句的规则,当前throw不在try/catch语句内,就到上一层去查找,在这个过程中,沿着调用链创建的对象都将销毁,所以到了main函数中匹配catch子句时,func函数结束时,sp1和sp2对象都会调用析构函数释放资源,我们就不需要再担心这种问题了。

智能指针的拷贝构造我们没有写,如果现在要拷贝的话就是浅拷贝,一块空间析构两次会出问题,那我们要手动深拷贝吗?其实并不是,智能指针的拷贝本意上就是要让它们指向同一块空间,那析构的问题怎么解决?我们来看看标准库的智能指针是如何实现的


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

C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,
智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为,原理上而言主要是解
决智能指针拷贝时的思路不同。
下面我们在智能指针中存一个日期类的指针,方便我们打印看析构情况。
struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};

auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给 拷贝对象,这是一个非常糟糕的设计,因为这会导致被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。

int main()
{auto_ptr<Date> ap1(new Date);auto_ptr<Date> ap2(ap1);// 不能访问ap1->_month++;return 0;
}
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点是不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用它。
int main()
{unique_ptr<Date> up1(new Date);// 拷贝构造和拷贝赋值都被禁了//unique_ptr<Date> up2(up1);// 移动构造可以unique_ptr<Date> up3(move(up1));return 0;
}
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的,use_count这个函数还可以看到当前的引用计数是多少,shared_ptr非常全能。但是shared_ptr在一个场景下是会有问题的,就是循环应用,这种情况就需要用weak_ptr来解决,在模拟实现的时候再讲这个场景以及weak_ptr这个智能指针。
int main()
{shared_ptr<Date> sp1(new Date);shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl; // 3shared_ptr<Date> sp4(move(sp1));return 0;
}

四、智能指针的原理

4.1 auto_ptr模拟实现

auto_ptr是需要把被拷贝对象的资源转移给拷贝对象,然后把被拷贝对象置空即可,赋值就先要把自己管理的资源释放了,再去指向其他的资源,如果不先释放再指向就内存泄漏了。

namespace hx
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._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;}~auto_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

4.2 unique_ptr模拟实现

拷贝构造和拷贝赋值直接禁掉,然后实现一个移动构造和移动赋值,也是直接转移资源即可。

namespace hx
{template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}unique_ptr(const unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;~unique_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}unique_ptr(unique_ptr<T>&& up):_ptr(up._ptr){up._ptr = nullptr;}unique_ptr<T>& operator=(unique_ptr<T>&& up){delete _ptr;_ptr = up._ptr;up._ptr = nullptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

4.3 shared_ptr模拟实现

关于shared_ptr我们要先理解引用计数的设计,每一份资源都有一个配套的引用计数,只要有一个shared_ptr指管着这块资源,引用计数就++,shared_ptr出了作用域要去调用析构,引用计数先--,然后看引用计数是否是0,如果引用计数是0了,那就说明没有其它个shared_ptr指管着这块资源了,那我析构的时候就可以把这块空间释放了,但是如果引用计数不是0,就说明还有其它的shared_ptr指管着这块资源,我析构的时候就不能释放。

那我们来思考一下,我们自己设计的时候引用计数应该怎样设计呢?直接写成成员变量吗?显然不是,这样设计就是每个对象都有一个引用计数,就比如上面的sp1和sp2,我们想让它们共用一个引用计数,引用计数是2,而不是sp1和sp2都有一个引用计数。那设计成静态的?也不行,如果设计成静态的那就是所有对象都用这一个引用计数,显然也是不合理的,那上面的sp1和sp3肯定不能用同一个引用计数。所以引用计数真正实现的方法是要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。

在赋值时判断是否是自己给自己赋值和之前不太一样,例如上面的例子,sp1 = sp1,sp1 = sp2,都叫自己给自己赋值,那在这里判断的方式就是看资源的地址相不相同,相同就是自己给自己赋值。而且也需要把删除的逻辑再判断一遍,如果当前指向的资源引用计数--完不是0,那我不需要释放,直接指向其它资源即可,如果当前指向的资源引用计数--完是0,那我就需要先释放,再指向其它资源。

同时我们也提供一个可以拿到指向的资源和引用计数的函数。

namespace hx
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}~shared_ptr(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;};
}

4.4 定制删除器

到这里大家会发现,我们的智能指针析构函数中都是对应的new单个的情况,如果是new[]那析构时就会崩溃。所以智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[]的版本,可以直接用。

int main()
{unique_ptr<Date[]> up(new Date[10]);shared_ptr<Date[]> sp(new Date[10]);return 0;
}

如果要传可调用对象的话,要注意的是:unique_ptr是在类模板参数支持的,shared_ptr是构造函数参数支持的。unique_ptr要显示实例化传类型给模版,shared_ptr只需要传对象过去让编译器自动推演类型。

下面来看一个示例:

template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}template<class T>
class DeleteArray
{
public:void operator()(T* ptr){delete[] ptr;}
};class Fclose
{
public:void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);}
};
int main()
{unique_ptr<Date, DeleteArray<Date>> up1(new Date[5]);shared_ptr<Date> sp1(new Date[5], DeleteArray<Date>());unique_ptr<Date, void(*)(Date*)> up2(new Date[5], DeleteArrayFunc<Date>);shared_ptr<Date> sp2(new Date[5], DeleteArrayFunc<Date>);auto delArrOBJ = [](Date* ptr) {delete[] ptr; };unique_ptr<Date, decltype(delArrOBJ)> up3(new Date[5], delArrOBJ);shared_ptr<Date> sp3(new Date[5], delArrOBJ);shared_ptr<FILE> sp4(fopen("Test.cpp", "r"), Fclose());shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), [](FILE* ptr) {cout << "fclose:" << ptr << endl;fclose(ptr);});return 0;
}
使用仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用,但是函数指针和lambda的类型不可以,另外delArrOBJ是对象,unique_ptr需要把delArrOBJ的类型传给类模版,但是我们拿不到lambda的类型,在这种场景下,就要用decltype来自动推演对象类型。
那我们自己实现shared_ptr的定制删除器要怎么设计呢?首先要再重载一个构造函数,带上模版参数,如果传了定制删除器就走这个构造函数
template<class D>
shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1))
{}

这个del是要在析构函数中用的,但是析构函数没有模版参数D,用不了,那就只能再来一个成员变量,先把del给保存下来,但是问题是,D不是类的模版参数,成员变量照样用不了D,那怎么办呢?D虽然用不了,但是我们知道del是一个可调用对象,那用包装器包装一下不就好了吗。

private:T* _ptr;int* _pcount;function<void(T*)> _del;

但是如果没有传定制删除器,那就匹配只有一个参数的构造函数,这个构造函数没有对_del初始化,那析构就出问题,所以我们可以给上一个缺省值,直接用一个lambda,在初始化列表阶段用,这样无论是new还是new[]就都解决了。

private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

如果当我们写出shared_ptr<Date> sp = new Date(2024, 9, 11);这样的代码时,是不可以的,为了防止普通指针隐式类型转换成智能指针对象,所以我们还可以在构造函数前加上explicit来限制这种隐式类型转换。

namespace hx
{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(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _del(sp.del){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){if (--(*_pcount) == 0){//delete _ptr;_del(_ptr);delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}~shared_ptr(){if (--(*_pcount) == 0){//delete _ptr;_del(_ptr);delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };};
shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值
直接构造。
shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,C++11开始可以对类型进行重载,返回值就是重载的这个类型,如果智能指针对象是一个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
int main()
{shared_ptr<Date> sp1(new Date(2024, 9, 11));shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);auto sp3 = make_shared<Date>(2024, 9, 11);shared_ptr<Date> sp4;// if (sp1.operator bool())if (sp1)cout << "sp1 is not nullptr" << endl;if (!sp4)cout << "sp1 is nullptr" << endl;return 0;
}

4.5 shared_ptr循环引用问题

shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会
导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且会解决循环引用。
struct ListNode
{int _data;ListNode* _next;ListNode* _prev~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}

这段代码会编译报错,原因就是ListNode*类型的指针和shared_ptr类型之间无法进行互相赋值,把ListNode*改成shared_ptr类型就可以通过编译

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

现在就构成了循环引用问题,我们画图来分析一下循环引用问题

当n1和n2析构后,左边节点的_next管着右边节点,右边节点的_prev管着左边节点,管理两个节点的引用计数是1,紧接着,右边节点什么时候析构呢?右边节点由左边节点的_next管着,左边节点的_next析构,右边节点就析构,那左边节点的_next什么时候析构呢?_next是左边节点的成员,左边节点析构,_next就析构,左边节点什么时候析构呢?左边节点由右边节点的_prev管着,右边节点的_prev析构,左边节点就析构,右边节点的_prev什么时候析构呢?_prev是右边节点的成员,右边节点析构,_prev就析构,右边节点什么时候析构呢?...大家可以看到,这就是一个死循环,引用计数永远无法减到0,节点永远无法析构,那总结一下,当有两个对象,他们的shared_ptr智能指针成员互相指向对方的时候,就构成循环引用,解决方法是:把ListNode结构体中的_next和_prev的类型改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。

struct ListNode
{int _data;weak_ptr<ListNode> _next;weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}

4.6 weak_ptr介绍

weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,它不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题。

weak_ptr只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main()
{shared_ptr<string> sp1(new string("111111"));shared_ptr<string> sp2(sp1);weak_ptr<string> wp = sp1;cout << wp.expired() << endl;cout << wp.use_count() << endl;// sp1和sp2都指向了其他资源,则weak_ptr就过期了sp1 = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;wp = sp1;shared_ptr<string> sp3 = wp.lock();cout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *sp1 << endl;return 0;
}

最开始的时候wp绑定sp1,绑定的资源没有过期,引用计数就返回绑定的shared_ptr的引用计数2。sp1指向其它资源,wp没有过期,引用计数是1。sp2指向其它资源,wp指向的资源过期了,并且引用计数是0。随后wp绑定到sp1,又通过lock返回了一个shared_ptr,sp1和sp3指向的资源是相同的,wp没有过期,引用计数是2。


4.7 weak_ptr模拟实现

weak_ptr实现无参的构造,以及用shared_ptr拷贝构造weak_ptr,用shared_ptr给weak_ptr赋值

namespace hx
{template<class T>class weak_ptr{public:weak_ptr(){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}private:T* _ptr = nullptr;};
}

五、C++11和boost中智能指针的关系

Boost库是为C++标准库提供扩展的一些C++程序库的总称,Boost社区建立的初衷之一就是为
C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从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_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的
scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

六、内存泄漏

6.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。

6.2 如何检测内存泄漏

Linux下内存泄漏检测工具:Linux 下几款程序内存泄漏检查工具-CSDN博客

windows下内存泄漏检测工具:针对windows平台上C++内存泄漏检测的软件和方法 - he伟_li - 博客园


6.3 如何避免内存泄漏

工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理
才有保证。
尽量使用智能指针来管理资源,如果场景比较特殊,可以采用RAII思想自己造个轮子管理。
定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。
总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错
型。如泄漏检测工具。

总结

智能指针在实际中是用的非常多的,用它来解决问题可以非常有效的避免资源泄漏,也非常方便,不用一直中间层try/catch,利用RAII,出了作用域自动调用析构,那本篇文章到这里就结束了,如果觉得小编写的还不错的,可以给一个三连支持,感谢大家。

http://www.xdnf.cn/news/3291.html

相关文章:

  • Gradio全解20——Streaming:流式传输的多媒体应用(1)——流式传输音频:魔力8号球
  • AE模板 动感节奏快闪图文展示介绍片头 Typographic Intro
  • Hadoop 集群基础指令指南
  • usb端点笔记
  • 【UE5】“对不起,您的客户端未能传递登录所需的参数”解决办法
  • QCefView应用和网页的交互
  • Github 热点项目 Qwen3 通义千问全面发布 新一代智能语言模型系统
  • WPF使用高性能图表
  • 【游戏ai】从强化学习开始自学游戏ai-2 使用IPPO自博弈对抗pongv3环境
  • 基于C++的IOT网关和平台4:github项目ctGateway交互协议
  • flutter 专题 一百零四 Flutter环境搭建
  • 零基础做自动驾驶集成测试(仿真)
  • MIPS架构详解:定义、应用与其他架构对比
  • harmonyOS 手机,双折叠,平板,PC端屏幕适配
  • 数据隐私在Web3环境下的重要性及实现方法
  • Spring Boot集成Kafka并使用多个死信队列的完整示例
  • 【MySQL】增删改查(CRUD)
  • Microsoft Entra ID 免费版管理云资源详解
  • mysql-5.7.24-linux-glibc2.12-x86_64.tar.gz的下载安装和使用
  • 上海地区IDC机房服务器托管选型报告(2025年4月30日)
  • (51单片机)LCD显示红外遥控相关数据(Delay延时函数)(LCD1602教程)(Int0和Timer0外部中断教程)(IR红外遥控模块教程)
  • LeRobot 项目部署运行逻辑(三)——机器人及舵机配置
  • 【STM32实物】基于STM32的RFID多卡识别语音播报系统设计
  • 左右分屏电商带货视频批量混剪自动剪辑生产技术软件:智能剪辑与合规化方案解析
  • 【优选算法 | 前缀和】前缀和算法:高效解决区间求和问题的关键
  • 无侵入式的解决 ViewPager2 跟横向滑动子 View 手势冲突的一种思路
  • 人工智能数学基础(五):概率论
  • Kafka Producer的acks参数对消息可靠性有何影响?
  • 阿里云服务器技术纵览:从底层架构到行业赋能​
  • PostgreSQL数据库操作基本命令