C++智能指针

智能指针

概念

  • 智能指针是原始指针的封装,其优点是会自动分配内存,不用担心潜在的内存泄露

普通指针的不足

  • new和new[]的内存需要用delete和delete[]释放
  • 程序员的主观失误,忘了或漏了释放
  • 程序员也不确定何时释放

智能指针的设计思路

  • 智能指针是类模板,在栈上创建智能指针对象
  • 普通指针交给智能指针对象
  • 智能指针对象过期时,调用析构函数释放普通指针的内存

独占指针 unique_ptr

概念

  • unique_ptr**独享一个原始指针(也就是unique_ptr指向的对象)**,也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁

源码简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T,typename D =default_delete<T> >
class unique_ptr{
public:
explicit unique_ptr(pointer p) noexcept; //不可用于转换函数
~unique_ptr() noexcept;
T& operator*() const; //重载*操作符
T* operator->() const noexcept; //重载->运算符
unique_ptr(const unique_ptr&)=delete; //禁用拷贝构造函数
unique_ptr& operator=(const unique_ptr &)=delete; //禁用赋值函数
unique_ptr(unique_ptr &&) noexcept; //右值引用
unique_ptr& operator=(unique_ptr&&) noexcept; //右值引用
//...
private:
pointer ptr; //内置的指针
}

功能描述

  • 在任何给定时候,只能有一个指针管理内存(也就是独占指针对象只管理一个指针资源)
  • 当指针超出作用域时,内存将自动释放
  • unique_ptr指针不可Copy,也不可以进行赋值,只可以Move
  • 可以通过get()获取裸指针地址(&unique_ptr对象得到的是对象指针地址不是裸指针)
  • 可以通过->调用成员函数
  • 可以通过*调用dereferencing

作为函数的参数

  • 传引用(不能传值,因为unique_ptr没有拷贝构造函数)
  • 裸指针

注意

  • 不要用同一个裸指针初始化多个unique_ptr对象
  • 不要用unique_ptr管理不是new分配的内存
  • 不支持指针的运算(+、-、++、–)

**创建方式(初始化)**:

  • 通过已有裸指针创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class AA{
    ....
    };

    int main(){
    AA* p=new AA("小明");
    unique_ptr<AA> pu1=p;
    return 0;
    }
  • 通过new来创建

    1
    unique_ptr<AA> pu1(new AA("小明"));
  • 通过std::make_unique创建(推荐)

    1
    unique_ptr<AA> pu1=make_unique<AA>("小明");   //C++14标准

内置函数和使用方法

  1. get():获取裸指针的地址

    1
    2
    unique_ptr<int> p1=make_unique<int>(10);
    cout<<p1.get()<<endl; //0x6498f94942b0
  2. release()释放对原始指针的控制权,将unique_ptr对象置为空,返回裸指针地址(之前管理的指针地址)。可用于把unique_ptr传递给子函数,子函数将负责释放对象

    1
    2
    3
    unique_ptr<int> p1=make_unique<int>(10);
    cout<<p1.release()<<endl; //0x5fb45e2472b0
    if(p1==nullptr) cout<<"p1为空"<<endl; //p1为空
  3. std::move()转移对原始指针的控制权。(可用于把unique_ptr传递给子函数,子函数形参也是unique_ptr,传值的方法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //函数func4()需要一个unique_ptr对象,并且会对这个对象负责
    void func4(unique_ptr<AA> a){
    cout<<a->m_name<<endl;
    }


    int main(int argc, char *argv[])
    {
    unique_ptr<AA> p(new AA("小明"));
    cout<<"开始调用函数.\n";
    func4(std::move(p)); //将原始指针的控制权交给func4函数的形参
    cout<<"调用函数完成.\n";
    return 0;
    }
  4. reset()释放unique_ptr指向资源,以及更改指向

    1
    2
    3
    4
    5
    6
    //函数原型
    void reset(T* _ptr=(T*)nullptr);
    unique_ptr<AA>p(new AA("小明"));
    p.reset(); //释放p对象所指向的资源对象
    p.reset(nullptr); //释放p对象所指向的资源对象
    p.reset(new AA("小蓝")); //释放p所指向对象,并指向新的对象
  5. swap()交换两个unique_ptr所指向的对象的控制权

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //函数原型
    void swap(unique_ptr<T>&_Right);

    unique_ptr<AA>p1(new AA("小明"));
    unique_ptr<AA>p2(new AA("小红"));
    p1.swap(p2);
    cout<<p1->m_name<<endl;
    cout<<p2->m_name<<endl;
    //输出:
    //小红
    //小明
  6. []:unique_ptr重载了操作符[],提供了支持数组的具体化版本,操作符[]返回的是引用,可以作为左值

    使用

    1
    2
    3
    4
    5
    //unique_ptr<int[]> parr1(new int[3]);  //不指定初始值
    unique_ptr<int[]> parr1(new int[3](33,22,11)); //指定初始值
    cout<<"parr1[0]="<<parr1[0]<<endl;
    cout<<"parr1[1]="<<parr1[1]<<endl;
    cout<<"parr1[2]="<<parr1[2]<<endl;

使用技巧

  1. 如果源unique_ptr对象是一个临时右值(一般是函数的返回值),编译器允许将这个unique_ptr赋给另一个unique_ptr对象。如果源unique_ptr将存在一段时间,则编译器不允许赋值!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <iostream>
    #include <memory>
    using namespace std;


    unique_ptr<int>func(){

    unique_ptr<int> pp=make_unique<int>(100);
    return pp;
    }


    int main(int argc, char *argv[])
    {
    unique_ptr<int> p1=make_unique<int>(10);
    unique_ptr<int> p2;
    p2=p1; //错误,不可以

    p2=unique_ptr<int>(new int(1)); //使用匿名对象赋值,可以
    p1=func(); //利用函数返回值赋值,可以
    std::cout<<*p1<<std::endl;
    return 0;
    }
  2. 用nullptr给unique_ptr赋值将会释放对象,空的unique_ptr==nullptr

    image-20240417141128949

  3. 一些情况使用技巧

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    #include <iostream>
    #include <memory>
    using namespace std;

    class AA{
    public:
    string m_name;
    AA(){
    cout<<m_name<<"调用构造函数AA().\n";
    }
    AA(const string &name):m_name(name){
    cout<<"调用构造函数AA("<<m_name<<").\n";
    }
    ~AA(){
    cout<<"调用了析构函数~AA("<<m_name<<").\n";
    }
    };


    //函数func1()需要一个原始指针,但不对指针负责
    void func1(const AA* a){
    cout<<a->m_name<<endl;
    }

    //函数func2()需要一个裸指针,并且对这个指针负责
    void func2(AA* a){
    cout<<a->m_name<<endl;
    delete a;
    }

    //函数func3()需要一个unique_ptr对象,不会对这个对象负责
    void func3(const unique_ptr<AA> &a){
    cout<<a->m_name<<endl;
    }

    //函数func4()需要一个unique_ptr对象,并且会对这个对象负责
    void func4(unique_ptr<AA> a){
    cout<<a->m_name<<endl;
    }


    int main(int argc, char *argv[])
    {
    unique_ptr<AA> p(new AA("小明"));
    cout<<"开始调用函数.\n";
    func1(p.get());
    func2(p.release());
    func3(p);
    func4(std::move(p));
    cout<<"调用函数完成.\n";
    return 0;
    }
  4. unique_ptr也可以像普通指针一样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样

  5. unique_ptr不是绝对安全,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #include <iostream>
    #include <memory>
    using namespace std;

    class AA{
    public:
    string m_name;
    AA(){
    cout<<m_name<<"调用构造函数AA().\n";
    }
    AA(const string &name):m_name(name){
    cout<<"调用构造函数AA("<<m_name<<").\n";
    }
    ~AA(){
    cout<<"调用了析构函数~AA("<<m_name<<").\n";
    }
    };

    unique_ptr<AA> p1(new AA("小明全局"));


    int main(int argc, char *argv[])
    {
    unique_ptr<AA> p(new AA("小明局部"));
    //return 0; 使用return全部自动释放
    exit(0); //局部无法进行释放
    }
    //打印日志
    || 调用构造函数AA(小明全局).
    || 调用构造函数AA(小明局部).
    || 调用了析构函数~AA(小明全局).
    || Exited with code 0

共享指针 shared_ptr

概念

  • shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象,在内部采用计数机制来实现,共享不是复制,资源只有一个
  • 新的shared_ptr与对象关联时,引用计数增加1
  • shared_ptr超出作用域时,引用计数减1
  • 引用计数为0时,则表示没有任何shared_ptr与对象相联,则释放该对象

基本用法:

注:shared_ptr的构造函数也是explicit,但是,没有删除拷贝构造函数和赋值函数

  1. 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //方法1:
    shared_ptr p(new AA("小明")); //分配内存并初始化

    //方法2[推荐]:
    shared_ptr p=make_shared<AA>("小明"); //C++11标准,效率更高

    //方法3:
    AA* p=new AA("小明");
    shared_ptr<AA> p0(p); //用已存在的裸指针初始化

    //方法4:
    shared_ptr<AA> p0(new AA("小明"));
    shared_ptr<AA> p1(p0); //用已存在的shared_ptr初始化,计数加1
    shared_ptr<AA> p2=p0; //用已存在的shared_ptr初始化,计数加1
  2. use_count()显示shared_ptr所指向的资源引用次数

    1
    2
    3
    4
    shared_ptr<AA> p0=make_shared<AA>("小明");
    shared_ptr<AA> p1(p0);
    shared_ptr<AA> p2=p0;
    cout<<p0.use_count()<<"\n"; //3
  3. get()显示shared_ptr所指向的资源地址

  4. unique()判断shared_ptr所指向的资源是否被其他shared_ptr对象所共享,如果use.count()为1,返回true,否则返回false

    1
    2
    3
    4
    5
    6
    shared_ptr<AA> p0=make_shared<AA>("小明");
    shared_ptr<AA> p1(p0);
    shared_ptr<AA> p2=p0;
    cout<<p0.unique()<<"\n"; //0
    shared_ptr<AA> p3=make_shared<AA>("111");
    cout<<p3.unique()<<"\n"; //1
  5. =:shared_ptr重载了=操作符,支持赋值,因为将右值shared_ptr赋值给了左值shared_ptr,因此右值shared_ptr所指向资源计数+1,则左值shared_ptr所指向资源计数-1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int main(int argc, char *argv[])
    {
    shared_ptr<AA> p0=make_shared<AA>("小明");
    shared_ptr<AA> p1(p0);
    shared_ptr<AA> p2=p0;
    shared_ptr<AA> p3=make_shared<AA>("111");
    shared_ptr<AA> p4=p3;
    shared_ptr<AA> p5=p3;
    p2=p5; //p3+1,p0-1
    cout<<p0.use_count()<<"\n"; //2
    cout<<p3.use_count()<<"\n"; //4
    return 0;
    }
  6. []:shared_ptr重载了操作符[],提供了支持数组的具体化版本,操作符[]返回的是引用,可以作为左值

    使用

  7. std::move():可以将转移对原始指针的控制权,还可以将unique_ptr转移成shared_ptr(反过来不行)

  8. reset()改变与资源的关联关系,以及指向

    1
    2
    3
    shared_ptr<AA> p=make_shared<AA>("小明");
    p.reset(); //解除与资源的关系,资源的引用计数减1
    p.reset(new AA("111")); //解除与资源的关系,原资源的引用计数-1,关联新的资源,新资源引用计数+1
  9. swap()交换两个shared_ptr的控制权

    1
    2
    //函数原型
    void swap(shared_ptr<T> &_Right);

作为函数的参数

  • 传引用(不能传值,因为unique_ptr没有拷贝构造函数)
  • 裸指针

注意

  • 不要用同一个裸指针初始化多个shared_ptr对象
  • 不要用shared_ptr管理不是new分配的内存
  • 不支持指针的运算(+、-、++、–)

使用技巧

  1. 用nullptr给shared_ptr赋值时将计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr
  2. shared_ptr也可以像普通指针一样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样
  3. shared_ptr不是绝对安全,如果程序中调用exit()退出,全局的shared_ptr可以自动释放,但局部的shared_ptr无法释放
  4. 如果unique_ptr能解决问题,就不要在用shared_ptr,unique_ptr的效率更高,占用的资源更少

shared_ptr的线程安全性

  • shared_ptr的引用计数本身是线程安全(引用计数是原子操作)
  • 多个线程同时读同一个shared_ptr对象是线程安全的
  • 如果多个线程对同一个shared_ptr对象进行读和写,则需要加锁
  • 多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护

shared_ptr存在问题

  • shared_ptr内部维护一个共享的引用计数器,多个shared_ptr可以指向同一个资源
  • 如果出现了循环引用的情况,引用计数永远无法归0,资源不会被释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <memory>
using namespace std;

class BB;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
//引用BB类资源
shared_ptr<BB> m_p;
};


class BB{
public:
string m_name;
BB(){
cout<<m_name<<"调用构造函数BB().\n";
}
BB(const string &name):m_name(name){
cout<<"调用构造函数BB("<<m_name<<").\n";
}
~BB(){
cout<<"调用了析构函数~BB("<<m_name<<").\n";
}
//引用AA资源
shared_ptr<AA> m_p;
};


int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
shared_ptr<BB> p1=make_shared<BB>("小明2");
//循环引用
p0->m_p=p1;
p1->m_p=p0;

return 0;
}
//输出结果
|| 调用构造函数AA(小明1).
|| 调用构造函数BB(小明2).
|| Exited with code 0
//没有调用析构函数

智能指针删除器

概念

  • 在默认情况下,智能指针过期时,它会自动调用缺省的删除器来删除资源
  • 程序员可以自定义删除器,改变智能指针释放资源的行为
  • 删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <memory>
using namespace std;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
};

//删除器,普通函数
void deletefunc(AA* a){
cout<<"自定义删除器(全局函数)。\n";
delete a;
}

//删除器,仿函数
struct deleteclass{
void operator()(AA* a){
cout<<"自定义删除器(仿函数).\n";
delete a;
}
};

//删除器,Lambda表达式
auto deletelamb=[](AA* a){
cout<<"自定义删除器(Lambda).\n";
delete a;
};

int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明"); //使用缺省的删除器

//shared_ptr自定义删除器
shared_ptr<AA> p1(new AA("111"),deletefunc);
shared_ptr<AA> p2(new AA("111"),deleteclass());
shared_ptr<AA> p3(new AA("111"),deletelamb);

//unique_ptr自定义删除器
unique_ptr<AA,decltype(deletefunc)*> pu1(new AA("111"),deletefunc); //模板参数使用了decltype推导,也可以直接写
//unique_ptr<AA,void(*)(AA*)> pu1(new AA("111"),deletefunc);
unique_ptr<AA,deleteclass> pu2(new AA("111"),deleteclass());
unique_ptr<AA,decltype(deletelamb)> pu3(new AA("111"),deletelamb);
return 0;
}

弱智能指针 weak_ptr

引言:为了解决shared_ptr循环引用带来的未自动清理的情况,C++还引入了weak_ptr指针来帮助shared_ptr解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//将循环引用的shared_ptr改成使用weak_ptr可以解决问题
#include <iostream>
#include <memory>
using namespace std;

class BB;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
//引用BB类资源
weak_ptr<BB> m_p;
};


class BB{
public:
string m_name;
BB(){
cout<<m_name<<"调用构造函数BB().\n";
}
BB(const string &name):m_name(name){
cout<<"调用构造函数BB("<<m_name<<").\n";
}
~BB(){
cout<<"调用了析构函数~BB("<<m_name<<").\n";
}
//引用AA资源
weak_ptr<AA> m_p;
};


int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
shared_ptr<BB> p1=make_shared<BB>("小明2");
//循环引用
p0->m_p=p1;
p1->m_p=p0;

return 0;
}
//输出结果
|| 调用构造函数AA(小明1).
|| 调用构造函数BB(小明2).
|| 调用了析构函数~BB(小明2).
|| 调用了析构函数~AA(小明1).
|| Exited with code 0

概念

  • weak_ptr是为了配合shared_ptr而引入的,它指向一个由shared_ptr管理的资源但不影响资源的生命周期,也就是说,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
  • 不论是否有weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放
  • weak_ptr更像是shared_ptr的助手而不是智能指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <memory>
using namespace std;

class BB;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
//引用BB类资源
weak_ptr<BB> m_p;
};


class BB{
public:
string m_name;
BB(){
cout<<m_name<<"调用构造函数BB().\n";
}
BB(const string &name):m_name(name){
cout<<"调用构造函数BB("<<m_name<<").\n";
}
~BB(){
cout<<"调用了析构函数~BB("<<m_name<<").\n";
}
//引用AA资源
weak_ptr<AA> m_p;
};


int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
shared_ptr<BB> p1=make_shared<BB>("小明2");

cout<<p0.use_count()<<endl;
cout<<p1.use_count()<<endl;
//循环引用
p0->m_p=p1;
p1->m_p=p0;

cout<<p0.use_count()<<endl;
cout<<p1.use_count()<<endl;
return 0;
}
//输出结果
|| 调用构造函数AA(小明1).
|| 调用构造函数BB(小明2).
|| 1
|| 1
|| 1
|| 1
|| 调用了析构函数~BB(小明2).
|| 调用了析构函数~AA(小明1).
|| Exited with code 0

基本用法

  • operator=();:重载了=操作符,可以将shared_ptr或weak_ptr赋值给weak_ptr
  • expired()判断weak_ptr所指资源是否过期(是否被销毁)
  • lock()将weak_ptr升级成shared_ptr,如果指向资源存在,将weak_ptr升级成shared_ptr,返回shared_ptr;如果资源已过期返回空的shared_ptr
  • reset():将当前weak_ptr指针置为空
  • swap():交换两个weak_ptr控制权

注意

  • weak_ptr不控制对象的生命周期,但是它可以知道对象是否还活着
  • lock()函数是线程安全的,因为它是一个原子操作的(对于多线程最好使用lock而不是expired)

(多线程)错误做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
{

shared_ptr<BB> p1=make_shared<BB>("小明2");

p0->m_p=p1;
p1->m_p=p0;

if(p0->m_p.expired()) cout<<"语句块内部:p0->m_p已过期.\n";
else cout<<"语句块内部:p0->m_p.lock()->m_name"<<p0->m_p.lock()->m_name<<endl;
}

if(p0->m_p.expired()) cout<<"语句块外部:p0->m_p已过期.\n";
else cout<<"语句块外部:p0->m_p.lock()->m_name"<<p0->m_p.lock()->m_name<<endl;
return 0;
}

(多线程)正确做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
{

shared_ptr<BB> p1=make_shared<BB>("小明2");

p0->m_p=p1;
p1->m_p=p0;

shared_ptr<BB> pp=p0->m_p.lock(); //把weak_ptr提升为shared_ptr
if(pp==nullptr) cout<<"语句块内部:p0->m_p已过期.\n";
else cout<<"语句块内部:pp->m_name="<<pp->m_name<<endl;
}

shared_ptr<BB> pp=p0->m_p.lock();
if(pp==nullptr) cout<<"语句块外部:p0->m_p已过期.\n";
else cout<<"语句块外部:pp->m_name="<<pp->m_name<<endl;
return 0;
}

C++智能指针
https://moonfordream.github.io/posts/C-智能指针/
作者
Moon
发布于
2024年7月25日
许可协议