为什么使用智能指针
使用C++的朋友都知道,C++中的内存泄漏是个很难解决的问题。
产生内存泄漏的几种场景:
- 由于程序员的疏忽,导致类的的析构函数中没有对所有或者部分使用的内存做回收
- 随着调用层级的增多,所有权转移后,回收资源的动作没法确定谁负责回收,会变得很难
- 在出现抛出异常的时候,内存没法回收
为了解决这个问题,由c++之父Bjarne Stroustrup提出了RAII机制
RAII机制
RAII(Resource Acquisition Is Initialization),中文翻译为资源获取即初始化,Bjarne Stroustrup 说:使用局部对象来管理资源的技术称为资源获取即初始化;
这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期由编译器管理,无需人工介入。
RAII的做法
是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。
智能指针
C++11 引入了智能指针的概念,智能指针的本质是一个对象,是一个行为表现都像指针的对象。它的封装利用了RAII机制或者思想,是C++语言的一种管理资源、避免泄漏的惯用法。
智能指针拥有以下特性:
- 拥有RAII的机制来管理指针。对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放它管理的堆内存。
- 有指针的基本功能。所有智能指针都重载了“*”和“->”操作符,让它可以直接返回对象的引用以及能用->去操作其指向的对象。若要访问智能指针原来的方法则使用“·”操作符。
- 它还需要考虑深浅拷贝问题
引用计数
引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
智能指针可分为两部分,一个是原指针,一个是引用计数(关联的计数器)。创建一个智能指针的时候引用计数为1,当引用计数为0的时候,智能指针本身会自动释放。当该智能指针被其他指针所用,引用计数就会相应的叠加,可以这么理解:引用计数的大小就是当前管理该内存的指针的数量。
智能指针包括 std::shared_ptr/std::unique_ptr/std::weak_ptr,使用它们需要包含头文件 。
std::unique_ptr(独占指针)
unique_ptr 是效率最接近普通指针的智能指针,推荐在一切可能得地方都使用
unique_ptr 在默认情况下和裸指针的大小是一样的。
所以 内存上没有任何的额外消耗,性能是最优的
std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。
既然是独占,就是不可复制。但是,我们可以利用 std::move 将其转移给其他的 unique_ptr
unique_ptr 的使用
#include<iostream>
#include<memory>
int main()
{
//std::unique_ptr<int> u(new int(10));//绑定申请的堆区资源
std::unique_ptr<int> u = std::make_unique<int>(10); // 推荐使用 make_unique (从 C++14 引入)
std::unique_ptr<int>u2(std::move(u));//转移所有权到u2(移动语义)
//不允许同一份资源,被多个unique_Ptr管理
//std::unique_ptr<int>u2(u);不能拷贝
//std::unique_ptr<int>u3=u;不能赋值
auto p=u.release();//释放所有权 返回原指针,相当于unique_ptr不参与原指针的管理了
u.reset(new int(20));//重新制定所有权
std::shared_ptr s(std::move(u2));//在有需要的时候,我们也可以将它转移到shared_ptr中管理。
}
std::shared_ptr(共享指针)
在使用 shared_ptr 之前应该考虑,是否真的需要使用 shared_ptr, 而非 unique_ptr。
shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。当引用计数为 0 的时候,则 delete 内存。
初始化和赋值(注意不要用一个原始指针初始化多个shared_ptr 会导致重复释放内存)
#include <memory>
#include<iostream>
int main() {
int* a = new int(10);
std::shared_ptr<int>s1(a);
std::shared_ptr<int>s2(new int(10));
std::shared_ptr<int>s3 = s1;
std::shared_ptr<int>s4=make_shared<int>(30);
auto s5 = std::make_shared<int>(20);
return 0;
}
shared_ptr其他成员函数
#include <memory>
#include<iostream>
#include<algorithm>
class Person
{
public:
Person(int a) :a(a){}
public:
int a;
};
int main() {
std::shared_ptr<Person>s(new Person(2));
std::cout << (*s).a << std::endl;
std::cout << s->a << std::endl;
std::cout << s.use_count() << std::endl;//得到引用计数的数量即当前管理该内存的指针的个数
Person*p = s.get();//得到原始的指针
std::shared_ptr<Person>s2 = s;
std::cout << s.use_count() << std::endl;
s.~shared_ptr();//析构函数它的作用是use_count()--然后其检查是否为0 若为0就释放,若不为0选择无视
std::cout << s2.use_count() << std::endl;//此处打印为1说明上面析构函数没有释放内存资源
std::shared_ptr<Person>s3(new Person(4));
s3.swap(s2);//交换管理的内存,交换指针
swap(s2, s3);//跟上面效果一样
if (s2.unique())std::cout << "资源是被唯一管理的" << std::endl; //用来判断资源是否是被唯一管理的
s2.reset();//强制释放资源,将s2的use_count()置为0 指针指向nullptr
s3.reset(new Person(10));//重置资源
std::cout<< s3->a<< std::endl;
return 0;
}
std::weak_ptr(弱引用指针)
weak_ptr 的作用是为了处理 shared_ptr 出现循环引用,导致出现引用计数不能为0,导致内存泄漏的问题
循环引用出现的示例代码
代码中Son 和Father 分别持有对方的智能指针。
son将要结束的时候,发现自己的成员中father的智能指针处于占用状态,所以son无法释放,father因为同样的情况也无法引用计数减到0 无法释放
#include <memory>
#include<iostream>
class Son;
class Father;
using fatherptr = std::shared_ptr<Father>;
using sonptr = std::shared_ptr<Son>;
class Father
{
public:
sonptr s;
Father();
~Father();
};
class Son
{
public:
fatherptr f;
Son();
~Son();
};
Father::Father() { std::cout << "hello father"<<std::endl; }
Father::~Father() { std::cout << "bye father"<<std::endl;}
Son::Son() { std::cout << "hellow son"<<std::endl; }
Son::~Son() { std::cout << "bye son" << std::endl; }
using fatherptr = std::shared_ptr<Father>;
using sonptr = std::shared_ptr<Son>;
int main() {
fatherptr f(new Father());
sonptr s(new Son());
f->s = s;
s->f = f; //循环调用
std::cout << f.use_count() << std::endl;//输出2
std::cout << s.use_count() << std::endl;//输出2
return 0;
}
weak_ptr解决循环引用
那么怎么解决,聪明的程序员就开发出了weak_ptr;
可以这么说,weak_ptr就是为了协助share_ptr而生。它本身不具备指针的功能没有重载的“*”和“->”.
weak_ptr相当与一个观察者,并不拥有 share_ptr的所有权;
它的成员函数都是为了监测shared_ptr所管理的资源而设计的。
weak_ptr 不会增加引用计数,因此可以打破 shared_ptr 的循环引用。
我们把其中一个成员指针改成弱引用指针就可以了。
#include <memory>
#include<iostream>
#include<algorithm>
class Son;
class Father;
using fatherptr = std::shared_ptr<Father>;
using sonweakptr = std::weak_ptr<Son>;
using sonptr = std::shared_ptr<Son>;
class Father
{
public:
sonweakptr s;
Father();
~Father();
};
class Son
{
public:
fatherptr f;
Son();
~Son();
};
Father::Father() { std::cout << "hello father"<<std::endl; }
Father::~Father() { std::cout << "bye father"<<std::endl;}
Son::Son() { std::cout << "hellow son"<<std::endl; }
Son::~Son() { std::cout << "bye son" << std::endl; }
int main() {
fatherptr f(new Father());
sonptr s(new Son());
f->s = s;
s->f = f; //循环调用
std::cout << f.use_count() << std::endl;
std::cout << s.use_count() << std::endl;
return 0;
}
参考资料
强烈推荐:视频讲解
智能指针的各种函数
C++ 智能指针的正确使用方式
https://changkun.de/modern-cpp/zh-cn/05-pointers/
https://blog.csdn.net/weixin_59253379/article/details/124840156