本文参考及图片引用:C++ 智能指针 - 全部用法详解-CSDN博客

用处

避免CPP里面的内存泄漏;

例子:

当 new 一个对象的时候,在其生命周期结束时,系统会自动调用它的析构函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test {
public:
Test() { cout << "Test的构造函数..." << endl; }
~Test() { cout << "Test的析构函数..." << endl; }

int getDebug() { return this->debug; }

private:
int debug = 20;
};

int main()
{
Test a = Test();
cout << a.getDebug() << endl;

return 0;
}

test1

但是,当 new 一个对象指针指向一个匿名对象的时候,在这个对象生命周期应该结束时,并不会调用它的析构函数:

1
2
3
4
5
6
7
int main()
{
Test * a = new Test();
cout << a->getDebug() << endl;

return 0;
}

test2

换句话说,当有对象被引用的时候,就有可能导致内存泄漏,一旦内存泄漏,就会消耗整个程序的资源和效率,更甚至导致异常中断;

所以智能指针便是用来处理这个问题的;

实质

智能指针的实质实际上是一个模板类,它会管理给予它的特定类型指针,并对于指针的操作给予了很多运算符上的重载,所以在使用的时候可以直接将智能指针变量当作管理的指针直接使用;

所以你明白了智能指针为何可以对于引用的对象进行自动析构,因为它本身就是个对象,它的析构里自然就写进了析构引用的指针的操作;

所以?所以别再指针化的使用或引用这个类的类对象了,因为会导致之前的问题重复;

以下提到的 智能指针 这个名词,都可以理解是类的名字,它不是一个实际意义上的指针;

类别及其用法

所谓的智能指针在CPP中普遍使用也就存在4种形式: auto_ptr, unique_ptr, shared_ptr, weak_ptr;

其中,第一个在C++98中给出,后面三个在C++11中给出,作为前者的进阶版;

auto_ptr

用法:
头文件: #include < memory >
用 法: auto_ptr<类型> 变量名(new 类型)

这个类型是指针,但不用加*强调,写法比较奇怪,可以尝试用构造函数的调用来理解;

举例:

1
2
3
auto_ptr< string > str(new string(“要成为大牛~ 变得很牛逼!”));
auto_ptr<vector< int >> av(new vector< int >());
auto_ptr< int > array(new int[10]);

就第一部分遇到的问题,如何用智能指针解决:

1
2
3
4
5
6
7
int main()
{
auto_ptr<Test> a(new Test());
cout << a->getDebug() << endl; //重载运算符,也可以用*a来引用

return 0;
}

这个样子便解决了之前提到的引用匿名对象的无法析构的问题;

智能指针中三大函数

get()

作用是返回智能指针类管理的真实指针地址;

上面使用auto_ptr的代码可以等效为如下:

1
2
3
4
5
6
7
8
9
int main()
{
auto_ptr<Test> a(new Test());
Test* tmp = a.get();
cout << tmp->getDebug() << endl;
//delete tmp; 禁止析构智能指针管理的指针,不然会double free

return 0;
}
release()

作用是取消智能指针对管理地址的管理,将管理区置为NULL;

1
2
3
auto_ptr<Test> a(new Test());
Test *tmp = a.release();
delete tmp;

取消管理之后交给对应的指针变量,此时需要自己手动析构;

同时注意不能直接调用 a.release() , 如果直接使用,此时智能指针管理的指针为NULL,同时没有变量接收之前管理的内存地址,就会造成内存泄漏;

reset()

重置智能指针管理的内存地址;

1
2
3
4
5
auto_ptr<Test> a(new Test());

a.reset(); // 释放掉智能指针托管的指针内存,并将其置NULL

a.reset(new Test()); // 释放掉智能指针托管的指针内存,并将参数指针取代之

注意事项以及缺陷

  • 不要将auto_ptr变量定义为全局变量以及指针;

  • 使用它的赋值运算和拷贝构造时,实际上是在做管理指针的转移;

    假如p1和p2是两个已经初始化的智能指针,那么执行p1 = p2:

    trans
    图中的地址是由get()获取;

  • STL中使用auto_ptr不安全,因为容器元素需要支持复制和赋值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    vector<auto_ptr<string>> vec;
    auto_ptr<string> p3(new string("I'm P3"));
    auto_ptr<string> p4(new string("I'm P4"));

    // 必须使用std::move修饰成右值,才可以进行插入容器中
    vec.push_back(std::move(p3));
    vec.push_back(std::move(p4));

    cout << "vec.at(0):" << *vec.at(0) << endl;
    cout << "vec[1]:" << *vec[1] << endl;


    // 风险来了:
    vec[0] = vec[1]; // 如果进行赋值,问题又回到了上面一个问题中。
    cout << "vec.at(0):" << *vec.at(0) << endl;
    cout << "vec[1]:" << *vec[1] << endl;

    此时运行这段代码会导致访问越界中断;

  • 不支持对象数组的内存管理:
    inside

unique_ptr

为了解决上述提出来的关于 auto_ptr 的缺陷, C++新版本更新了这个进阶版;

它比较于 auto_ptr 来说,多了三个优势:

  • 依然无法进行左值构造和赋值,但是可以允许临时的右值构造和赋值;
  • 在容器中使用是安全的;
  • 允许对象数组的内存管理;

同时这里要强调一下,不管是 auto_ptr 还是 unique_ptr ,它们都是基于排他所有权模式:两个指针不能指向同一个资源;

这样一来就还是有一个问题:

unique_ptr的右值赋值效果等同于auto_ptr的=号赋值,只是做指针的转移,而非复制;

同样两个智能指针使用reset接管同一个指针的时候,最后一个会起接管作用,前者会被置零;

什么叫左值,右值? –> 左值指有专门内存空间的变量, 不是左值的都叫右值,可以是寄存器里的数,也可以是一个立即数;

那么如何实现两个智能指针的复制呢?如同平常使用的对象和类型的时候=号的第一直觉操作?

引出shared_ptr;

shared_ptr

它的出现解决了复制内存地址引用给多个智能指针使用;

至于如何实现的,首先需要回想一下,智能指针是干什么的;

智能指针用于解决引用对象的自动析构,那么引用的对象都析构了,另一个智能指针引用同样内存位置该析构谁呢?NULL吗?

所以shared_ptr和unique_ptr功能一模一样,可以理解为只是多了一个引用计数的静态类变量;

当有多个智能指针指向同一个内存地址时,引用次数就是那么多,每次在智能指针类做复制的时候在构造函数里将次数加一,析构的时候,将次数减一,判断为一的时候则析构引用的内存地址,这样就解决了引用共享问题;

引用次数的获取可以使用如下函数(use_count()):

shared

但这又引出一个新的问题;

循环引用

当一个A类中有B类的智能指针,且B类里也有A类的智能指针的时候;

当B类智能指针创建时,B引用次数加一,A类智能指针创建时,A引用次数加一;

A中引用B类智能指针时,B引用次数加一为二,同理B中引用A,A的引用次数也变为二;

这个时候系统生命周期结束时,释放创建时的智能指针,则A和B的引用次数都减1,变为1;

此时A类要析构,就需要先析构其中的B类智能指针,B类要析构,就需要先析构A类,造成无限循环等待;

weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。

它的出现就是为了解决循环引用;

在智能指针类中如需引用另一个智能指针,最好就写为weak类型,这样它不会改变引用的次数,从而破局循环引用;

一个weak类型可以直接由shared拷贝构造或者复制而来;

但weak类型不能复制或者拷贝构造给shared类型,需要使用lock()函数重新变成shared类型,同时引用次数+1;

weak指针有一个函数是 expired() ,作用是判断当前指针是否还有管理的对象,有返回false,无返回true;

总结

详细有关智能指针的代码操作如赋值,构造,析构,等等以及有关循环引用的更全面的讲解请查看第一行给出的参考网址;

本文更偏向于条目梳理和简单回顾;

大多数使用智能指针会出现的错误均已在上述给出,但这里还要提一个没有给出的错误:

禁止用任何类型智能指针get函数返回的指针去初始化另一个智能指针:

1
2
shared_ptr< int > a(new int(10));
// 一个典型的错误用法 shared_ptr< int > b(a.get());

实际上对于智能指针需要注意的操作也在于复制部分,如何利用好复制带来的方便的同时避免出错,就是智能指针需要掌握的点;