0
点赞
收藏
分享

微信扫一扫

C++智能指针详解[shared_ptr、unique_ptr、weak_ptr]]

1kesou 2022-03-12 阅读 161

一、智能指针的由来及分类

1.程序内存分区:静态区 栈区 堆区(自由空间/动态内存)
静态内存里存储着局部static对象,类的static数据成员以及定义在任何类外的变量,栈内存里保存着定义在函数内的非static对象,分配在静态内存或栈内存中的对象有着严格的生存期,由编译器自动生成和销毁。但有时需要在程序运行时动态分配对象,这个时候就需要用到动态内存了,动态内存的生存期由程序来控制,也就是说,必须在合适的时机显式地销毁他们。即当程序在堆区存储动态分配的对象且当动态对象不再使用时,必须手动释放销毁,否则会造成内存泄漏。

2.动态内存的管理:new和delete
new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针。
delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

3.动态内存管理存在的问题:
1.一种是忘记释放内存,会造成内存泄漏;
2.一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

4.智能指针产生:
为了更加安全的的使用动态内存,引入智能指针的概念。智能指针的行为类似常规指针,最大的区别是它能够自动释放所指向的对象,利用智能指针管理动态内存可以有效降低内存泄漏的可能。

5.智能指针的种类:
1.shared_ptr: 允许多个指针指向同一个对象(引用计数器机制)
2.unique_ptr: "独占“所指对象(引用计数器最大值为1的shared_ptr)
3.weak_ptr: 弱引用,指向shared_ptr所管理的对象
注:三种智能指针都定义在的头文件中

二、shared_ptr详解
shared_ptr 是C++11提供的一种智能指针类,它可以自动删除相关指针,从而彻底消除内存泄漏和悬空指针的问题。它遵循共享所有权的概念,即不同的 shared_ptr 对象可以与相同的指针相关联,内部使用引用计数机制来实现。

1.引用计数机制:
每个 shared_ptr 对象在内部指向两个内存位置:指向对象的指针和用于控制引用计数数据的指针。
1、当新的 shared_ptr 对象与指针关联时,在其构造函数中,将与此指针关联的引用计数增加1。
2、当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。
3、如果引用计数变为0,则无shared_ptr 对象与此内存关联,它使用delete函数删除该内存。

2.shared_ptr支持的操作:

shared_ptr<T> p      //空智能指针
p                    //将p用作一个条件判断,若p指向一个对象则为ture
*p                   //解引用p,获得它指向的对象
p->member            //等价于(*p).member
p.get()              //返回p中保存的指针,小心使用
swap(p,q)            //交换p和q中的指针
p.swap(q)            //交换p和q中的指针
make_shared<T>(args) //返回一个动态分配类型为T的对象,使用args初始化此对象
shared_ptr<T>p(q)    //p 是shared_ptr q的拷贝,此操作会递增q中的计数器,q中的指针必须能转换为T*
shared_ptr<T>p(u)    //p从unique_ptr u那里接管了对象的所有权,将u置空
shared_ptr<T>p(q,d)  //p接管了内置指针q所指向的对象的所有权,p将使用可调用对象d来代替delete
p=q                  //p和q必须都是shared_ptr,所保存的指针必须能相互转换,此操作会递减p的引用计数,递增q的引用计数
p.use_count()        //返回与p共享对象的智能指针数量,可能很慢,主要用于调试
p.unique()           //若p.use_count()为1,返回ture,否则返回false
p.reset()            //分离关联的原始指针,p指向的对象引用计数减少1
p.reset(q)           //令p指向新指针q
p.reset(q,d)         //令p指向q,并调用d来代替detlete
p = nullptr          //重置指针为空指针

3.shared_ptr的定义
智能指针的本质是一个模板,和普通指针定义类似,定义指针时需要声明指针可以指向值的类型。定义普通指针时,类型写在开头,定义智能指针则把类型写在尖括号里。

//普通指针定义
int *pi;

//智能指针定义(默认初始化为空指针nullptr)
std::shared_ptr<string> p1;               //允许指向string类型的空智能指针
std::shared_ptr<int> p2;                  //允许指向int类型的空智能指针
std::shared_ptr<vector<string>> p3;       //允许只想vector<string>类型的空智能指针

4.shared_ptr的初始化
使用make_shared()函数初始化智能指针(常用)
通过传入的参数与构造类型的构造函数进行匹配并初始化,也可以不传入任何参数,默认使用值初始化的方式初始化对象。make_shared()函数make_share后面要加一个尖括号指出指针要指向的类型。

//eg1:
std::shared_ptr<int>p1 = std::make_shared<int>();
//eg2:
std::shared_ptr<int>p1 = std::make_shared<int>(10);
//eg2:
std::shared_ptr<string>p2 = std::make_shared<string>("I am a shared_ptr")

5.shared_ptr的拷贝与复制
shared_ptr在内部实现上有一个引用计数器,当进行拷贝和赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。只要拷贝一个shared_ptr,引用计数器的值就会上升,一旦引用计数器的值为0,它就会自动调用shared_ptr指向类型的析构函数释放相关联的动态内存。

auto p = make_shared<int>42;
auto q(p);//拷贝构造
r=q;//给r赋值,让它指向另一个地址
	//递增q指向的对象的引用计数
	//递减r原来指向的对象的引用计数
	//若r原来指向的对象已经没有引用者,会自动释放

使用动态内存的三种情况:
1.程序不知道自己需要多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据

6.shared_ptr与new一起使用
(1)new运算符分配空间

//没有赋初始值,采用默认初始化
int* p = new int;
string* p = new string;
//值初始化,p1指向一块int类型的动态内存,内置类型int会被赋值0
int *p1 = new int();
string *p1 = new string();
//直接初始化,p2指向一块int类型的动态内存,其初始化值为1024
int *p2 = new int(1024);
string *p2 = new string("hk");
vector<int> vp = new vector<int>{0,1,2,3,4,5};

//内存耗尽时,new表达式就会失败,抛出异常
int *p=new int;//如果分配失败,new抛出std::bad_alloc异常
int *p=new (nothrow)int;//如果分配失败,new返回一个空指针

(2)delete释放动态内存
为了防止内存耗尽,在动态内存使用完之后,必须将其归还给系统,使用delete归还。我们传递给delete的指针必须指向动态内存,或者是一个空指针。释放一块并非new分配的内存或者将相同的指针释放多次,其行为是未定义的。即使delete后面跟的是指向静态分配的对象或者已经释放的空间,编译还是能够通过,实际上是错误的。动态对象的生存周期直到被释放时为止,所以调用这必须记得释放内存。在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的地址有一种方法可以避免悬空指针的问题:在指针即将要离开其作用于之前释放掉它所关联的内存,如果我们需要保留指针可以在delete之后将nullptr赋予指针,这样就清楚的指出指针不指向任何对象。

(3)shared_ptr和new
如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的职能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针

std::shared_ptr<int> p1(new int());//正确
std::shared_ptr<int> p2 = new int();//错误

注:shared_ptr构造函数是explicit类型的,所以不能隐式调用它的构造函数,即像std::shared_ptr p1 = new int();我们不能将内置指针赋值给shared_ptr进行初始化,而必须采用直接初始化的方法,同样的,一个返回shared_ptr的函数的返回值也不允许返回内置指针,因为返回内置指针则是要求编译器将该内置指针隐式转换为智能指针,这是不允许的。

(4)不要混合使用普通指针和智能指针
如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。也不要使用get初始化另一个智能指针或为智能指针赋值。

(5)存在的陷阱
(1)不使用相同的内置指针初始化(或reset)多个智能指针
(2)不delete get()返回的指针
(3)不使用get()初始化或resel另一个指针
(4)如果使用get返回的指针,当最后一个对应的智能指针销毁后,你的指针就变为无效了
(5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

三、unique_ptr 详解
unique_ptr 独享被管理对象指针所有权的智能指针unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。unique_ptr也是智能指针的一种,它和shared_ptr不同的是unique_ptr“拥有”它所指向的对象,可以理解为unique_ptr是一个引用计数器最大值为1的shared_ptr,所以unique_ptr不能被赋值与拷贝,且其必须使用new的方式对其指向的对象 直接初始化。

1.unique_ptr支持的操作:

unique_ptr<T> u1;             //空unique_ptr,可以指向类型为T的对象,u1会使用delete来释放它的指针
unique_ptr<T,D> u2;           //空unique_ptr,可以指向类型为T的对象,u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T,D> u(d);         //空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr;                  //释放u指向的对象,将u置空
u.release();                  //u放弃对指针的控制权,返回指针,并将u置空
u.reset();                    //释放u指向的对象
u.reset(q);                   //如果提供了内置指针q,令u指向q,否则将u置为空
u.reset(nullptr);             //将u置为空

2.unique_ptr独享所有权
unique_ptr对象始终是关联的原始指针的唯一所有者。我们无法复制unique_ptr对象,它只能移动。由于每个unique_ptr对象都是原始指针的唯一所有者,因此在其析构函数中它直接删除关联的指针,不需要任何参考计数。

3.创建unique_ptr对象

//创建一个空的unique_ptr对象
std::unique_ptr<int> ptr1;
//使用原始指针创建unique_ptr对象(非空)
std::unique_ptr<Task> taskPtr(new Task(22));
std::unique_ptr<Task> taskPtr(new std::unique_ptr<Task>::element_type(23));
//不能采用赋值的方式创建unique_ptr对象
std::unique_ptr<Task> taskPtr2 = new Task();//错误
//使用std::make_unique创建  C++14新函数
std::unique_ptr<Task> taskPtr = std::make_unique<Task>(34);

//检查unique_ptr对象是否为空
//方法1
if(!ptr)
	std::cout<<"ptr1 is empty"<<endl;
//方法2
if(ptr1==nullptr)
	std::cout<<"ptr1 is empty"<<endl;

4.获取被管理对象指针,使用get()

Task *p1 = taskPtr.get();

5.重置unique_ptr对象,使用reset()
在 unique_ptr 对象上调用reset()函数将重置它,即它将释放delete关联的原始指针并使unique_ptr 对象为空。

taskPtr.reset();

6.转移unique_ptr所有权(使用move、reset或release)
由于 unique_ptr 不可复制,因此,我们无法通过复制构造函数或赋值运算符创建unique_ptr对象的副本。可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique_ptr.

//不能复制和赋值,编译错误
std::unique_ptr<Task> taskPtr3 = taskPtr2;
taskPtr = taskPtr2;

(1)使用move转移所有权

//通过原始指针创建taskPtr2
std::unique_ptr<Task> taskPtr2(new Task(55));
//把taskPtr2中关联指针的所有权转移给taskPtr4
std::unique_ptr<Task>taskPtr4 = std::move(taskPtr2);
//现在taskPtr2关联的指针为空
if(taskPtr2 == nullptr)
	std::cout<<"taskPtr2 is  empty"<<std::endl;
// taskPtr2关联指针的所有权现在转移到了taskPtr4中
if(taskPtr4 != nullptr)
	std::cout<<"taskPtr4 is not empty"<<std::endl;
// 会输出55
std::cout<< taskPtr4->mId << std::endl;

(2)使用release或reset转移所有权

//将所有权从p1转移到p2,利用release
unique_ptr<string> p2(p1.release());//release将p1置为空
//将所有权从p3转移到p2,利用reset
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release());//reset释放p2原来指向的内存

注:

  • 成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。
  • reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。
  • 调用release会切断unique_ptr和它原来管理的的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。

7.释放关联的原始指针
在 unique_ptr 对象上调用 release()将释放其关联的原始指针的所有权,并返回原始指针。这里是释放所有权,并没有delete原始指针,reset()会delete原始指针。

std::unique_ptr<Task> taskPtr5(new Task(55));
// 不为空
if(taskPtr5 != nullptr)
	std::cout<<"taskPtr5 is not empty"<<std::endl;
// 释放关联指针的所有权
Task * ptr = taskPtr5.release();
// 现在为空
if(taskPtr5 == nullptr)
	std::cout<<"taskPtr5 is empty"<<std::endl;

8.示例程序

#include <iostream>
#include <memory>

struct Task {
    int mId;
    Task(int id ) :mId(id) {
        std::cout<<"Task::Constructor"<<std::endl;
    }
    ~Task() {
        std::cout<<"Task::Destructor"<<std::endl;
    }
};

int main()
{
    // 空对象 unique_ptr
    std::unique_ptr<int> ptr1;

    // 检查 ptr1 是否为空
    if(!ptr1)
        std::cout<<"ptr1 is empty"<<std::endl;

    // 检查 ptr1 是否为空
    if(ptr1 == nullptr)
        std::cout<<"ptr1 is empty"<<std::endl;

    // 不能通过赋值初始化unique_ptr
    // std::unique_ptr<Task> taskPtr2 = new Task(); // Compile Error

    // 通过原始指针创建 unique_ptr
    std::unique_ptr<Task> taskPtr(new Task(23));

    // 检查 taskPtr 是否为空
    if(taskPtr != nullptr)
        std::cout<<"taskPtr is  not empty"<<std::endl;

    // 访问 unique_ptr关联指针的成员
    std::cout<<taskPtr->mId<<std::endl;

    std::cout<<"Reset the taskPtr"<<std::endl;
    // 重置 unique_ptr 为空,将删除关联的原始指针
    taskPtr.reset();

    // 检查是否为空 / 检查有没有关联的原始指针
    if(taskPtr == nullptr)
        std::cout<<"taskPtr is  empty"<<std::endl;

    // 通过原始指针创建 unique_ptr
    std::unique_ptr<Task> taskPtr2(new Task(55));

    if(taskPtr2 != nullptr)
        std::cout<<"taskPtr2 is  not empty"<<std::endl;

    // unique_ptr 对象不能复制
    //taskPtr = taskPtr2; //compile error
    //std::unique_ptr<Task> taskPtr3 = taskPtr2;

    {
        // 转移所有权(把unique_ptr中的指针转移到另一个unique_ptr中)
        std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
        // 转移后为空
        if(taskPtr2 == nullptr)
            std::cout << "taskPtr2 is  empty" << std::endl;
        // 转进来后非空
        if(taskPtr4 != nullptr)
            std::cout<<"taskPtr4 is not empty"<<std::endl;

        std::cout << taskPtr4->mId << std::endl;

        //taskPtr4 超出下面这个括号的作用于将delete其关联的指针
    }

    std::unique_ptr<Task> taskPtr5(new Task(66));

    if(taskPtr5 != nullptr)
        std::cout << "taskPtr5 is not empty" << std::endl;

    // 释放所有权
    Task * ptr = taskPtr5.release();

    if(taskPtr5 == nullptr)
        std::cout << "taskPtr5 is empty" << std::endl;

    std::cout << ptr->mId << std::endl;

    delete ptr;

    return 0;
}

四、weak_ptr详解
weak_ptr是一种不控制所指向对象生存期的弱引用智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。可以将它看成是一个不会增加引用计数器的shared_ptr,同时也不会负责指向对象的内存管理。

1.weak_ptr支持的操作

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

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针

2.weak_ptr的特点

  • 它可以默认初始化,默认初始化是一个空指针
  • 它的直接初始化需要用到shared_ptr,初始化之后将会和该shared_ptr指向同一个对象,但是该shared_ptr的引用计数不会增加
  • weak_ptr不能直接解引用来访问它所指向的对象,因为weak_ptr不管理指向对象的动态内存,该内存仍由与它直接初始化的shared_ptr管理,所以有可能会因为shared_ptr的引用计数为0而释放掉动态内存,所以weak_ptr不能解引用来访问对象

3.示例程序

void test_weak_ptr() {

	auto sptr = std::make_shared<string>(string("I have a dream!"));

	//默认初始化,空指针
	std::weak_ptr<string> nullp;

	//直接初始化
	auto wptr = std::weak_ptr<string>(sptr);

	//允许使用shared_ptr和weak_ptr进行赋值,但是都不会增加和其共享的shared_ptr的引用计数
	std::weak_ptr<string> wptr2 = wptr;
	std::weak_ptr<string> wptr3 = sptr;

	//置空wptr2
	wptr2.reset();

	//输出和wptr3共享对象的shared_ptr的引用计数,这里是1
	cout << "wptr3->use_count=" << wptr3.use_count() << endl;

	//lock返回wptr共享对象的shared_ptr,如果该shared_ptr指向的对象被释放,则返回空shared_ptr
	shared_ptr<string> sptr2 = wptr.lock();

	//此处可以正常输出"I have a dream!"
	if (sptr2) cout << "*sptr2=" << *sptr2 << endl;

}
举报

相关推荐

0 条评论