智能指针
智能指针解决的问题
智能指针主要解决以下两个问题:
- 避免内存泄露。一般采用malloc、new在堆上分配内存需要使用free、delete手动释放;使用智能指针可以自动释放内存。
- 共享所有权指针的传播和释放,例如在多线程项目中可以很好地处理不同线程的同一个对象的析构问题。
智能指针分类
C++98有一个智能指针auto_ptr(目前已弃用),C++11有3个常用的指针shared_ptr,unique_ptr,weak_ptr。
- shared_ptr:共享对象的所有权,性能略差。
- unique_ptr:独占对象的所有权,由于没有引用计数,性能较好于shared_ptr。
- weak_ptr:该智能指针通常用来配合shared_ptr,解决循环引用的问题。
shared_ptr
内存模型图
- shared_ptr是一个模板类。
- shared_ptr内部有两个指针,一个ptr指向对象,一个ptr指向控制块。控制块里面包含着引用计数(reference count,或者叫use_count)和一个弱计数(weak_count,一般在weak_ptr的配合下才起作用)。
shared_ptr示例
//普通的内存分配
Buffer buf = new Buffer("auto free memory");
delete buf;//需要配合delete使用
//智能指针指向分配内存的对象
shared_ptr<buffer> buf = make_shared<Buffer>("auto free memory");
//在作用域外,自动释放内存
shared_ptr含义
shared_ptr使用引用计数记录对象被引用的次数。每一个shared_ptr的拷贝都指向相同的内存,当最后一个shared_ptr析构时,也就是use_count为0后,内存才会释放。
shared_ptr基本用法及常用函数
常用函数
- s.get():返回shared_ptr中保存的裸指针。
- s.reset(…):重置shared_ptr,将指针s释放并置空,引用计数减一。如果reset带了输入参数(一个对象),则指针释放并重新指向该对象,原对象对应的引用计数减一。
- s.use_count():返回shared_ptr的引用计数。
- s.unique():如果use_count为1,则返回true,否之返回false。
智能指针的构造,初始化-make_shared
一般优先使用make_shared来构造智能指针,更加高效。
auto sp1 = make_shared<int>(100);
//或者
shared_ptr<int> sp1 = make_shared<int>(100);
以下方法也可以初始化shared_ptr,但效率不如make_shared
std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));
效率不同的原因:因为shared_ptr构造函数会执行两次内存分配,一次给int对象,一次给引用计数(可能还是两个内存区域)。而make_shared只会执行一次内存分配,将两个数据一次性分配完(一个内存区域)。
explicit
shared_ptr是有explicit定义的,所以不能将一个原始指针直接赋值给一个智能指针,如下的操作是错误的:
//报错
std::shared_ptr<int> p = new int(1);//该操作会隐式调用拷贝构造函数,不满足explicit定义
//报错
int* p2 = new int(1);
std::shared_ptr<int> p = p2;//该操作会隐式调用拷贝构造函数,不满足explicit定义
这样可以约束程序员的行为,避免在使用new动态分配内存时,忘记将其交给std::shared_ptr管理,就可能发生内存泄漏。
获取原始指针get
当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:
std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get(); //
//不小心 delete p,会导致该内存delete两次
谨慎使用p.get()的返回值。p.get()的返回值就相当于一个裸指针的值,不合适地使用这个值,上述陷阱的所有错误都有可能发生。如下:
- 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的
- 保存为裸指针不知什么时候就会变成悬空指针,保存为shared_ptr则产生了独立指针
- 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误
指定删除器
shared_ptr可以指定删除器。
示例代码如下:
#include <iostream>
#include <memory>
using namespace std;
void DeleteIntPtr(int *p) {
cout << "call DeleteIntPtr" << endl;
delete p;
}
int main()
{
std::shared_ptr<int> p(new int(1), DeleteIntPtr);
return 0;
}
删除动态数组
因为shared_ptr的默认删除器不支持数组对象,所以当使用shared_ptr管理动态数组时,这时候指定删除器就起大作用了。
代码如下所示:
#include <iostream>
#include <memory>
using namespace std;
void DeleteIntPtr(int *p) {
cout << "call DeleteIntPtr" << endl;
delete p;
}
int main()
{
std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});//该指定删除器是以匿名函数lambda表达式呈现
return 0;
}
shared_ptr的注意问题
1.一个原始指针初始化多个shared_ptr,会造成二次释放同一内存空间。
int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); // 错误
因为p1,p2两个指针之间无关联关系,(每个指针的强引用计数都是1),所以在释放ptr所指向的内存时,p1和p2都会释放这个内存空间,显然有问题(一个内存空间被释放了两次)。
2.不要在函数实参中创建shared_ptr。
如下:
function(shared_ptr<int>(new int), g()); //有缺陷
因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针,代码如下:
shared_ptr<int> p(new int);
function(p, g());
3.避免循环引用。
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{//设定一个作用域
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
return 0;
}
循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,而没有减为0,导致两个指针都不会被析构,产生内存泄漏。
解决的办法是把A和B任何一个成员变量改为weak_ptr
比如:
class A {
public:
std::weak_ptr<B> bptr; // 修改为weak_ptr
~A() {
cout << "A is deleted" << endl;
}
};
unique_ptr
unique_ptr含义
- unique_ptr是一个独占型的智能指针,不能将一个unique_ptr指向的对象赋值给另一个unique_ptr
- unique_ptr可以指向一个数组
- unique_ptr需要确定删除器的类型
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制
move转移
unique_ptr不允许复制,但可以通过函数将所有权转移给其他的unique_ptr,通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。例如
unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
make_unique
make_unique是在c++14中加入标准库的。
std::make_unique upw1(std::make_unique<Widget>());
//或者
std::unique_ptr<Widget> upw2(new Widget);
unique_ptr和shared_ptr对比
1.unique_ptr可以指向一个数组,如下:
std::unique_ptr<int []> ptr1(new int[10]);
ptr1[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
std::shared_ptr<int> ptr3(new int[10]);//这种是合法的
2.unique_ptr指定删除器和shared_ptr有区别,unique_ptr需要确定删除器的类型,不能像shared_ptr那样直接指定删除器。
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //正确
shared_ptr和unique_ptr使用场景
如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。
weak_ptr
- 当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
- weak_ptr是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
- weak_ptr设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
std::weak_ptr<B> bptr; // 修改为weak_ptr
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{//设定一个作用域
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl;
return 0;
}
智能指针的线程安全
shared_ptr的内部是封装了锁的,因为指向同一个对象的智能指针的引用计数指针是指向同一块资源的。实际上引用计数在shared_ptr底层中是以指针的形式实现的,所有的对象通过指针访问同一块空间,从而实现共享。所有智能指针在对引用计数加减的时候内部是有“加锁”和“释放锁”的操作。
结论:shared_ptr的引用计数时线程安全的(因为是原子操作)。
而shared_ptr管理对象时不一定是线程安全的。
当不同的线程,操控同一个shared_ptr智能指针时,会发生线程不安全的情况。
因为智能指针管理对象需要两步, 首先一个指针指向管理的对象, 然后一个指针控制引用计数。
如果当指针指针的第一步完成了,第二步还没开始的时候CPU被占用,另一个线程也操作这个智能指针时就会出现线程安全问题。
所以在多线程中如果有操作同一个智能指针的场景,最好还是进行加锁限制。
不同的线程,操作不同的shared_ptr智能指针是线程安全的哦。=.=