Skip to content

智能指针概述

在谈智能指针之前我们首先来回顾一下之前学过的有关指针的知识,指针指向一块内存区域也可以说指针就是地址。

知识回顾

指针需要通过取址符或者动态内存分配来赋值,在C语言中使用 malloc/free 函数来管理动态内存的开辟和释放;在C++中使用 new/delete 关键字来管理。我们看下面一个实例

cpp
#define _CRT_SECURE_NO_WARNINGS
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG
#define new new (_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
//上面都是启用VS内存泄露捕捉需要的头文件定义

#include <iostream>
using namespace std;

int main() {
    int *p = (int *) malloc(sizeof(int));
    int *q = p;
    *q = 10;
    cout << "*p=[" << *p << "]" << endl;

    _CrtDumpMemoryLeaks();	//启用VS内存泄露捕捉使用的语句
    return 0;
}

在上面的实例中如果我们不修改程序,程序虽然可以正常运行,但是运行结束会造成4字节的内存泄露

这就要我们时刻记得自己申请的内存要在使用完毕以后进行释放,但是这又会带来一个问题,q指向的地址跟p指向的地址一样;如果我们在使用完p以后进行释放了,那么后续如果再使用q也会引发程序错误。所以对于指针的使用要求我们要格外的小心翼翼。

资源管理

C语言在进行资源管理的时候,比如文件指针,由于分支较多,或者由于写代码的人与维护的人不一致,导致分支没有写的那么完善,从而导致文件指针没有释放。

cpp
void UseFile(char const *fn) {
    FILE *f = fopen(fn, “r”); // 1. 获取资源
    // …… //2.使用资源
    // 回收资源有很多分支
    if (!g()) {
        fclose(f);
        return;
    }
    // ...
    if (!h()) {
        fclose(f);
        return;
    }
    // ...
    fclose(f); // 释放资源
}

根据之前单例对象自动释放的经验,我们可以想到利用对象的生命周期去管理资源。那么就可以尝试实现一个安全回收文件的程序了。

cpp
class SafeFile {
public:
    // 在构造函数中初始化资源(托管资源)
    SafeFile(FILE *fp)
        : _fp(fp) {
        std::cout << "SafeFile(FILE*) " << std::endl;
    }
    // 提供方法访问资源
    void write(const std::string &msg) {
        fwrite(msg.c_str(), 1, msg.size(), _fp);
    }
    // 利用析构函数释放资源
    ~SafeFile() {
        std::cout << "~SafeFile()" << std::endl;
        if (_fp) {
            fclose(_fp);
            std::cout << "fclose(_fp)" << std::endl;
        }
    }

private:
    FILE *_fp;
};

void test() {
    std::string msg = "hello,world";
    SafeFile sf(fopen("wd.txt", "a+"));
    sf.write(msg);
}

RAII技术*

以上例子其实已经用到了RAII的技术。所谓RAII,是C++提出的资源管理的技术,全称为Resource Acquisition Is Initialization,由C++之父Bjarne Stroustrup提出。其本质是利用对象的生命周期来管理资源(内存资源、文件描述符、文件、锁等),因为当对象的生命周期结束时,会自动调用析构函数。

RAII技术,具备以下基本特征:

  • 在构造函数中托管资源;(在给构造函数传参时初始化资源)
  • 在析构函数中释放资源;
  • 一般不允许进行复制或者赋值(对象语义);
  • 提供若干访问资源的方法(如:读写文件)。

对象语义与值语义

与对象语义相反的就是值语义。

值语义:可以进行复制或赋值(两个变量的值可以相同)

cpp
int a = 10;
int b = a;
int c = 20;
c = a;     // 赋值
int d = c; // 复制

对象语义:不允许复制或者赋值

(全世界不会有两个完全一样的人,程序世界中也不会有两个完全一样的对象)

常用手段:

  1. 将拷贝构造函数与赋值运算符函数设置为私有的
  2. 将拷贝构造函数与赋值运算符函数=delete
  3. 使用继承的思想,将基类的拷贝构造函数与赋值运算符函数删除(或设为私有),让派生类继承基类。

我们可以实现以下的一个类模板,模拟RAII的思想

cpp
template <class T>
class RAII {
public:
    // 1.在构造函数中初始化资源(托管资源)
    RAII(T *data)
        : _data(data) {
        cout << "RAII(T*)" << endl;
    }

    // 2.在析构函数中释放资源
    ~RAII() {
        cout << "~RAII()" << endl;
        if (_data) {
            delete _data;
            _data = nullptr;
        }
    }

    // 3.提供若干访问资源的方法
    T *operator->() {
        return _data;
    }

    T &operator*() {
        return *_data;
    }

    T *get() const {
        return _data;
    }

    void set(T *data) {
        if (_data) {
            delete _data;
            _data = nullptr;
        }
        _data = data;
    }

    // 4.不允许复制或赋值
    RAII(const RAII &rhs) = delete;
    RAII &operator=(const RAII &rhs) = delete;

private:
    T *_data;
};

如下,raii不是一个指针,而是一个对象,但是它的使用已经和指针完全一致了。这个对象可以托管堆上的Point对象,而且不用考虑delete。

cpp
void test0() {
    Point *pt = new Point(1, 2);
    // 智能指针的雏形
    RAII<Point> raii(pt);
    raii->print();
    (*raii).print();
}

RAII技术的本质: 利用 栈对象 的生命周期管理资源,因为栈对象在离开作用域时候,会执行析构函数。

引入智能指针

为了解决上面的困境,C++ 11中引入了新的管理指针的办法——智能指针,引入新的内存管理办法以后 以往的指针的形式就被称为是原始指针(或者说是裸指针)。智能指针是一种封装了原始指针的类,它通过重载运算符 *-> 来表现得像一个普通指针一样。

智能指针可以帮助我们进行动态分配内存对象(new出来的对象)的生命周期的管理,能过有效的防止内存泄漏,除了已经废弃的auto_ptr其余三种指针都是类模板,我们可以将new获得的地址赋值给他们。

Tips

你忘记delete操作的时间智能指针可以帮助你delete,你压根就不需要自己再进行delete了,或者说智能指针的本份就是帮你进行delete

智能指针的分类

C++ 标准库中有几种不同类型的智能指针

unique_ptr独占式指针

同一时刻只能有一个指针指向该对象,当然该对象的所有权还是可以移交出去的。

  • 不能被复制,只能移动(std::move
  • 适用于独占资源管理(如文件、网络连接)
  • 用 std::make_unique<T>(args...) 创建(

shared_ptr共享式指针

  • 采用 引用计数,多个 shared_ptr 可共享同一对象,最后一个销毁时释放资源。
  • 存在循环引用风险,可配合 std::weak_ptr 解决。
  • 用 std::make_shared<T>(args...) 创建,减少内存分配开销。

weak_ptr弱引用指针

是辅助shared_ptr工作的

  • 依赖 shared_ptr,不会增加引用计数。
  • 用于解决 shared_ptr 循环引用问题。
  • 可通过 lock() 获取 shared_ptr,判断对象是否仍然有效。

auto_ptr

C++98中引入,C++11弃用并在C++17移除

它们各自有不同的用途和行为。例如,unique_ptr 只允许一个智能指针拥有某个对象,而 shared_ptr 则允许多个智能指针共享同一个对象。

总之,原始指针和智能指针都可以用来访问内存中的对象,但智能指针提供了更好的内存管理和安全性。

参考资料