C++11 智能指针 (整理)

孟佳

关注

阅读 101

2023-03-01


RAII技术

RAII技术即资源分配及初始化,使用类来封装资源的分配和初始化,再构造函数完成资源的分配和初始化,析构函数完成资源的清理,可以保证正确的初始化和资源释放

智能指针

什么是智能指针?

智能指针是借用RAII技术对普通指针进行封装,实质是一个对象,表现为一个指针,即智能的管理动态资源的释放。

智能指针的作用

1.防止忘记调用delete操作符释放资源,与程序异常后进入catch忘记释放内存及多次释放同一块内存引起的内存泄漏
2.将值语义转为引用语义

智能指针的使用

本文介绍三个智能指针:unique_ptr、shared_ptr、weak_ptr。auto_ptr已经被C++11丢弃了。

shared_ptr

1.shared_ptr多个指针指向相同的对象,使用引用计数,每一次shared_ptr的拷贝指向相同的内存,每使用一次,内部引用计数加1,每析构一次,内部引用计数减1,直至减为0,自动删除指向的堆内存,shared_ptr内部的引用计数是线程安全的,但对象的读取需要加锁
2.智能指针是一个模板类,可以指定其类型,传入的指针使用构造函数初始化,也可使用make_shared函数初始化,不能把一个指针赋给智能指针,因为一个是类,一个是指针,如shared_ptr p=new int(1);错误
3.拷贝使得对象的引用计数加1,赋值使得原对象的引用计数减1,当计数为0时,自动删除指向的内存,后来指向的对象的引用计数加1,指向后来的对象。
4.缺陷为:循环引用,会导致内存泄漏问题,解决办法为使用弱引用的智能指针,即weak_ptr

1、shared_ptr是只有c++11版本才能使用的。因此再编译或者再编写makefile的时候要注意一下。

2、shared_ptr unique_ptr 两者最大的不同在于它支持两个指针指向同一块内存。

3、shared_ptr支持delete []。c++17前​​std::shared_ptr​​都有一个严重的限制,那就是它并不支持动态数组,这个要注意;

#include <iostream>
#include <memory>

int main() {
{
int a = 10;
std::shared_ptr<int> ptra = std::make_shared<int>(a);
std::shared_ptr<int> ptra2(ptra); //copy
std::cout << ptra.use_count() << std::endl; //= 2

int b = 20;
int *pb = &a;
//std::shared_ptr<int> ptrb = pb; //error
std::shared_ptr<int> ptrb = std::make_shared<int>(b);
ptra2 = ptrb; //assign -1
pb = ptrb.get(); //获取原始指针

std::cout << ptra.use_count() << std::endl; //= 1
std::cout << ptrb.use_count() << std::endl; //= 2
}
}

unique_ptr

1.unique_ptr唯一拥有其所指对象,在同一时刻只能有一个unique_ptr指向给定对象(不允许拷贝,赋值操作),与原始指针不同的是,当异常发生时,动态资源得以释放。
2.unique_ptr本身的生命周期:从unique_ptr指针创建到离开作用域,离开作用域时,若其指向对象,所指对象被销毁(默认使用delete操作符)
3.unique_ptr指针与所指向对象的关系:创建智能指针使用构造函数初始化,使用reset重新指定,使用release释放所有权,通过移动语义转移所有权。
 

这里稍微说以下如何用unique_ptr进行相互赋值,这当中有三种情况,第一种情况是两个指针都是unique_ptr,第二个情况是我要把unique_ptr变成普通指针,第三种情况是我要把普通指针变成unique_ptr。我们举个例子 

  unique_ptr <AutoPtr> autoPtrFirst(new AutoPtr(1));

       unique_ptr <AutoPtr> autoPtrSecond(new AutoPtr(2));

  AutoPtr *p = NULL;

  好,我们针对以上三种情况进行讨论。

  第一种情况:假如我们要把autoPtrSecond指向autoPtrFirst那么我们应该怎么做呢?我们可以这样写。autoPtrSecond = std::move(autoPtrFirst),即把First指针移动走,只留下Second指针。
  第二种情况:如果我要把unique_ptr指针变成普通指针应该怎么做呢?我们可以这样写。p = autoPtrFirst.release(),这样我们就可以把autoPtrFirst移动走,然后让p指向原来的地址空间了。

  第三种情况:如果我们要把普通指针变成unique_ptr指针应该怎么做呢?我们可以这样写。autoPtrFirst.reset(p),这样我们就可以将autoPtrFirst指向p了。

支持动态数组:

std::unique_ptr<int[]> up1(new int[10]());

 建议:使用 vector<T> 容器而不是 unique_ptr<T[]>,因为该容器类型比智能指针强大得多,也灵活得多。

#include <iostream>
#include <memory>

int main() {
{
std::unique_ptr<int> uptr(new int(10)); //绑定动态对象
//std::unique_ptr<int> uptr2 = uptr; //不能賦值
//std::unique_ptr<int> uptr2(uptr); //不能拷贝
std::unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
uptr2.release(); //释放所有权
}
//超过uptr的作用域,內存释放
}

weak_ptr

weak_ptr是配合shared_ptr出现的,不具有普通指针的行为,没有重载operator*和->最大作用在于协助shared_ptr观测资源的使用情况,可以从一个shared_ptr或一个weak_ptr对象构造,使用其成员函数use_count()获得观测资源的引用计数,expired()功能等价于use_count()==0标识被观测的资源已经不存在。
weak_ptr可以使用lock()获得一个新的shared_ptr对象,从而操作资源,但expired()==true时,lock函数返回一个存储空指针的shared_ptr对象

#include <iostream>
#include <memory>

int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;

std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;

if(!wp.expired()){
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;
}
}
//delete memory
}

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变 shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。

weak_ptr的出现是为了解决shared_ptr循环引用的。

class B;
class A {
public:
A() { std::cout << "A's constructor " << std::endl; }
~A() { std::cout << "A's disconstructor " << std::endl; }
std::shared_ptr<B> b;
};

class B {
public:
B() { std::cout << "B's constructor " << std::endl; }
~B() { std::cout << "B's disconstructor " << std::endl; }
std::shared_ptr<A> a;
};



int main()
{
std::shared_ptr<A> spa = std::make_shared<A>();
std::shared_ptr<B> spb = std::make_shared<B>();
spa->b = spb;
spb->a = spa;
std::cout << "sqa的强引用个数 " << spa.use_count() << std::endl;
std::cout << "sqb的强引用个数 " << spb.use_count() << std::endl;

return 0;
}


// 输出结果
A's constructor
B's constructor
sqa的强引用个数 2
sqb的强引用个数 2

从上可以看出,由于spa和spb的强引用计数永远大于等于1,所以A和B的析构函数不会被调用。

当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它

auto p = make_shared<int>(10);
weak_ptr<int> wp(p);
std::cout << "p的强引用为 " << p.use_count() << std::endl; // 为1

wp和p指向相同的对象,由于是弱引用,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。

利用weak_ptr修改上面的循环引用。

class B;
class A {
public:
A() { std::cout << "A's constructor " << std::endl; }
~A() { std::cout << "A's disconstructor " << std::endl; }
std::shared_ptr<B> b;
};

class B {
public:
B() { std::cout << "B's constructor " << std::endl; }
~B() { std::cout << "B's disconstructor " << std::endl; }
std::weak_ptr<A> a;
};



int main()
{
std::shared_ptr<A> spa = std::make_shared<A>();
std::shared_ptr<B> spb = std::make_shared<B>();
spa->b = spb;
spb->a = spa;
std::cout << "sqa的强引用个数 " << spa.use_count() << std::endl;
std::cout << "sqb的强引用个数 " << spb.use_count() << std::endl;

return 0;
}



// 结果输出
A's constructor
B's constructor
sqa的强引用个数 1
sqb的强引用个数 2
A's disconstructor
B's disconstructor

可以看出spa的强引用变为1,然后其释放后 spb 为1 。所以weak_ptr配合shared_ptr解决循环引用问题。

weak_ptr使用注意点

因为weak_ptr是一种弱引用,其指向shared_ptr,可能shared_ptr已经释放了。所以我们不能使用shared_ptr直接访问对象。先看下weak_ptr的用法:

weak_ptr<T> w //空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) //与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的S型
w = p //p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
w.reset() //将W置为空
w.use_count() //与w共享对象的shared ptr的数量
w.expired() //若 w.use_count()为0,返回true,否贝y返回 false
w.lock() //如果expired为true,返回一个空shared ptr:否则返回一个 指向w的对象的shared_ptr

所以我们用weak访问对象时,可以通过lock来判断

auto p = make_shared<int>(10);
weak_ptr<int> wp(p);
if (std::shared_ptr<int> np = wp.lock()) { // 如果np不为空则条件成立
// np和p共享对象
}

智能指针的实现

智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。

#include <iostream>
#include <memory>

template<typename T>
class SmartPointer {
private:
T* _ptr;
size_t* _count;
public:
SmartPointer(T* ptr = nullptr) :
_ptr(ptr) {
if (_ptr) {
_count = new size_t(1);
} else {
_count = new size_t(0);
}
}

SmartPointer(const SmartPointer& ptr) {
if (this != &ptr) {
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}

SmartPointer& operator=(const SmartPointer& ptr) {
if (this->_ptr == ptr._ptr) {
return *this;
}

if (this->_ptr) {
(*this->_count)--;
if (this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}

this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
return *this;
}

T& operator*() {
assert(this->_ptr == nullptr);
return *(this->_ptr);

}

T* operator->() {
assert(this->_ptr == nullptr);
return this->_ptr;
}

~SmartPointer() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}

size_t use_count(){
return *this->_count;
}
};

int main() {
{
SmartPointer<int> sp(new int(10));
SmartPointer<int> sp2(sp);
SmartPointer<int> sp3(new int(20));
sp2 = sp3;
std::cout << sp.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
}
//delete operator
}

6 shared_ptr多线程

shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。这部分请看陈硕大佬的博客:​​为什么多线程读写 shared_ptr 要加锁?​​

精彩评论(0)

0 0 举报