智能指针
为何需要智能指针
在C++的程序设计中,对堆内存的使用是非常频繁的,并且堆内存的申请和释放都是程序员自己管理的。但总体来说,堆内存的管理是麻烦的,使用智能指针可以更好的管理堆内存。
相对于原始指针来说,智能指针不需要手动释放内存,其内存会自动释放。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
示例
#include <memory>
#include <iostream>
class Person{
public:
Person() {
std::cout << "构造函数" << std::endl;
}
~Person() {
std::cout << "析构函数" << std::endl;
}
};
int main(){
{
std::shared_ptr<Person> p(new Person);
}
return 0;
}
这里在智能指针p离开{}的作用域时,会自动释放内存并调用其析构函数。
指针指针解决的问题
智能指针主要解决了以下两种问题:
- 内存泄漏:因为智能指针的内存会自动释放,合理的使用可以避免内存泄漏问题
- 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题。
智能指针的类型
C++有四种智能指针,分别是auto_ptr, unique_ptr, shared_ptr, weak_ptr,其中第一种智能指针已经被弃用,如果你不知道其用法也不必去主动了解,后三种均为C++11支持。
shared_ptr共享型智能指针
特性
std::shared_ptr使用引用计数来保证浅拷贝的内存释放问题,每一个shared_ptr的拷贝都会指向同一块内存,只有当最后一个shared_ptr拷贝析构的时候,才会将其指向的内存释放。
std::shared_ptr的内存模型
std::shared_ptr的组成主要可以看成两部分
- 一个指向堆上创建的对象的裸指针
- 该智能指针的引用计数
而当其进行拷贝时std::shared_ptr shared_ptr2 = shared_ptr1结构会变成如下状态
两个智能指针指向的是同一块堆内存,并且共享引用计数的内存,每发生一次拷贝,引用计数都会加1.
注意 :智能指针本身是在栈上分配的,其成员指针指向的是一块分配在堆区的内存所以当一个std::shared_ptr的拷贝析构时,会将usr_count的值减一,当引用计数为0时,会自动释放指向堆区的内存raw_ptr。
线程安全
shared_ptr在多线程环境下,其引用计数时线程安全的,不过其raw_ptr的修改并非线程安全。如果需要数据安全,需要使用锁机制。
初始化
shared_ptr可以通过以下几种方式初始化
std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));
auto sp1 = make_shared<int>(100);
注意reset方法,如果ptr未初始化,调用reset可以为其初始化一个值。如果ptr有数据,reset会改变其指向,并减少原先智能指针的引用计数。
不能将一个原始指针直接赋值给智能指针
std::shared_ptr<int> p = new int(1); //错误
常用方法
- reset : 重置shared_ptr
- get :返回shared_ptr内部的裸指针
- use_count:返回shared_ptr的引用计数
- unique:若use_count == 1则返回true,否则未false
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::cout << ptr1.use_count() << ' ';
auto ptr2 = ptr1;
std::cout << ptr1.use_count() << ' ';
std::cout << ptr2.use_count() << ' ';
ptr2.reset(new int(5));
std::cout << ptr1.use_count() << ' ';
std::cout << ptr2.use_count() << ' ';
auto ptr3 = ptr2;
std::cout << ptr1.unique() << ' ' << ptr2.unique() <<" "<< ptr3.unique() << std::endl;
int* p = new int(10);
std::shared_ptr<int> ptr4(p);
std::cout << (ptr4.get() == p) << std::endl;
return 0;
}
//输出: 1 2 2 1 1 1 0 0 1
删除器
在创建智能指针时,可以给对象指定一个删除器,当智能指针的引用计数为零时,会自动调用该删除器来释放对象的内存,比如我们用std::shared_ptr来管理动态数组,std::shared_ptr的默认删除器不支持删除数组对象,我们就可以指定删除器。
std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});
需注意的问题
1.不要用同一个裸指针初始化多个智能指针
int* p = new int(10);
std::shared_ptr<int> ptr1(p);
std::shared_ptr<int> ptr2(p); //错误
2.尽量不要在函数实参中创建智能指针
f(std::shared_ptr(new int(10)), g());
观察上面这个函数调用,函数f接受了两个实参,一个是智能指针,一个是函数g()的返回值。这里可能会存在一种风险,智能指针还没有创建,g()函数调用过程中出现了异常,这样可能会导致内存泄漏,建议采用以下写法。
shared_ptr<int> p(new int);
function(p, g());
3.不要将this指针作为shared_ptr返回出来
#include <iostream>
#include <memory>
class A {
public:
std::shared_ptr<A> getSelf() {
return std::shared_ptr<A>(this);
}
};
int main() {
std::shared_ptr<A> ptr1(new A);
std::shared_ptr<A> ptr2 = ptr1->getSelf();
return 0;
}
这里其实是相当于用this来初始化了两个智能指针ptr1和ptr2,会出现重复释放的问题。如果我们需要函数返回本对象的智能指针,应该让该类继承std::enable_shared_from_this类, 再调用基类成员函数shared_from_this来获取this的shared_ptr。 示例如下:
#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A> {
public:
std::shared_ptr<A> getSelf() {
return shared_from_this();
}
};
int main() {
std::shared_ptr<A> ptr1(new A);
std::shared_ptr<A> ptr2 = ptr1->getSelf();
std::cout << ptr1.use_count() << ' ' << ptr2.use_count() << std::endl;
return 0;
}
//输出:2 2
4. 避免循环引用。
循环引用可能会导致内存泄漏,如下:
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> aptr;
};
class B {
public:
std::shared_ptr<A> aptr;
};
int main()
{
std::shared_ptr<A> ptr1(new A);
std::shared_ptr<B> ptr2(new B);
ptr1->aptr = ptr2; //ptr2被拷贝,引用计数加一
ptr2->aptr = ptr1; //ptr1被拷贝,引用计数加一
return 0;
}
因为循环引用的原因,ptr1和ptr2的引用计数都为2,且进行析构后,引用计数也不会减到0,两个指针都不会释放内存,将会出现内存泄漏。
unique_ptr独占型智能指针
特性
std::unique_ptr是独占型的智能指针, 不允许其他的智能指针共享其内部的指针,即不允许通过赋值操作,将一个unique_ptr赋值给另一个unique_ptr。
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制
虽然我们不同复制unique_ptr,但是可以通过std::move来转移unique_ptr指针的所有权。
unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
unique_ptr可以管理数组
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
和shared_ptr的选择
如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。
weak_ptr弱引用型智能指针
特性
std::weak_ptr与前两种智能指针不同, 是一种不控制对象生命周期的智能指针,它指向一个由std::shared_ptr管理的对象,其只提供了对管理对象的访问手段。weak_ptr只可以用shared_ptr或者另一个weak_ptr来构造,这两个智能指针之间也可以互相转化,并且它的构造和析构都不会改变引用计数,仅仅作为一个旁观者来监视shared_ptr中管理的资源是否存在, 同时其也没有重载操作符*和->.
weak_ptr解决的问题
weak_ptr的设计是为了配合shared_ptr工作,可以用于解决shared_ptr循环引用产生的内存泄漏问题。
基本用法
1.use_count()获取当前观察资源的引用计数
注意:weak_ptr的构造不会改变引用计数
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr1(new int(10));
std::weak_ptr<int> wp(ptr1);
std::cout << wp.use_count() << ' ';
std::shared_ptr<int> ptr2 = ptr1;
std::cout << wp.use_count() << std::endl;
return 0;
}
//输出: 1 2
2. expired()方法判断所观察资源是否已经释放
#include <iostream>
#include <memory>
int main() {
std::weak_ptr<int> wp;
{
std::shared_ptr<int> ptr(new int(10));
wp = ptr;
std::cout << wp.expired() << " ";
}
std::cout << wp.expired() << std::endl;
return 0;
}
//输出:0 1
3.lock()获取所监视的智能指针
注意:调用lock方法后,引用计数会加一
#include <iostream>
#include <memory>
std::weak_ptr<int> wp;
void f() {
if (wp.expired()) {
std::cout << "资源无效" << ' ';
}
else {
std::cout << *wp.lock() << ' ';
}
}
int main() {
{
std::shared_ptr<int> ptr(new int(10));
wp = ptr;
f();
}
f();
return 0;
}
解决循环引用问题
因为weak_ptr并不会增加引用计数,只要将类A或B中的任一个shared_ptr成员改为weak_ptr成员就可以解决问题。
#include <memory>
#include <iostream>
class B;
class A {
public:
std::weak_ptr<B> aptr;
};
class B {
public:
std::shared_ptr<A> aptr;
};
int main()
{
std::shared_ptr<A> ptr1(new A);
std::shared_ptr<B> ptr2(new B);
ptr1->aptr = ptr2; //引用计数不变
ptr2->aptr = ptr1; //ptr1被拷贝,引用计数加一
return 0;
}
在离开作用域之后,ptr2的引用计数为减为0,B指针会被析构,析构后其内部的aptr的引用计数会被减为1,然后在离开作用域后ptr1引用计数又从1减为0,A对象也被析构,不会发生内存泄漏。