简单聊一下C++里的RAII与智能指针

文章介绍了RAII(ResourceAcquisitionIsInitialization)编程思想,如何通过封装资源管理(如文件、内存、指针等)在对象生命周期结束时自动释放,避免手动清理带来的问题,以及std::shared_ptr和std::weak_ptr在处理共享资源和循环引用中的应用。
摘要由CSDN通过智能技术生成

RAII这个词看着很吓人,但其实很easy,这不是一个特性,也不是一个技术,它是一种编程思想(或者说习惯),类似"多写注释有助于以后重新熟悉代码"。
为了解释RAII的优越性,我们先看一段没有使用RAII的代码。

// 检查文件是否符合某种规范
bool CheckFile(const std::string file_path) {
    std::string file_content;
    ifstream ifs;
    ifs.open(file_path, std::ifstream::binary); // 打开文件

    // 检查文件是否能打开成功
    if (!ifs.is_open())
        return false;

    // 检查文件大小
    int64 file_size = ifs.seekg(0, ifs.end).tellg();
    if (file_size > 1024) {
        ifs.close();
        return false;
    }
    ifs.seekg(0, ifs.beg);

    // 获取文件第一行内容
    if (!getline(ifs, file_content)) {
        ifs.close();
        return false;
    }

    // 检查文件内容
    if (file_content != "this file is legal") {
        ifs.close();
        return false;
    }

    ifs.close(); // 关闭文件
    return true;
}

注意这段代码是没有bug的,可以正常运行,但看起来非常拧巴,就这几个return前的close,写的罗嗦不说,以后再改代码还容易出bug,随便哪次return时忘了close文件就可能出问题。

为此有很多方式可以将多个return合并为一个,比如:

bool CheckFile(const std::string file_path) {
    std::string file_content;
    ifstream ifs;
    ifs.open(file_path, std::ifstream::binary); // 打开文件

    bool is_check_ok = true;

    // 检查文件是否能打开成功
    if (is_check_ok)
        is_check_ok = ifs.is_open();

    // 检查文件大小
    if (is_check_ok) {
        int64 file_size = ifs.seekg(0, ifs.end).tellg();
        ifs.seekg(0, ifs.beg);
        is_check_ok = file_size <= 1024;
    }

    // 获取文件第一行内容
    if (is_check_ok)
        is_check_ok = (bool)getline(ifs, file_content);

    // 检查文件内容
    if (is_check_ok)
        is_check_ok = file_content == "this file is legal";

    ifs.close(); // 关闭文件
    return is_check_ok;
}
bool CheckFile(const std::string file_path) {
    std::string file_content;
    ifstream ifs;
    ifs.open(file_path, std::ifstream::binary); // 打开文件

    bool is_check_ok = false;

    do {
        // 检查文件是否能打开成功
        if (!ifs.is_open())
            break;

        // 检查文件大小
        int64 file_size = ifs.seekg(0, ifs.end).tellg();
        ifs.seekg(0, ifs.beg);
        if (file_size > 1024)
            break;

        // 获取文件第一行内容
        if (!getline(ifs, file_content))
            break;

        // 检查文件内容
        if (file_content != "this file is legal")
            break;

        is_check_ok = true;
    } while (false);

    ifs.close(); // 关闭文件
    return is_check_ok;
}
bool CheckFile(const std::string file_path) {
    std::string file_content;
    ifstream ifs;
    ifs.open(file_path, std::ifstream::binary); // 打开文件

    // lambda
    auto check_file_fun = [&]() -> bool {
        // 检查文件是否能打开成功
        if (!ifs.is_open())
            return false;

        // 检查文件大小
        __int64 file_size = ifs.seekg(0, ifs.end).tellg();
        if (file_size > 1024)
            return false;

        ifs.seekg(0, ifs.beg);

        // 获取文件第一行内容
        if (!getline(ifs, file_content))
            return false;

        // 检查文件内容
        if (file_content != "this file is legal")
            return false;

        return true;
    };

    bool is_check_ok = check_file_fun();
    ifs.close(); // 关闭文件
    return is_check_ok;
}
// 仅作演示用,实际工作中不推荐使用goto
bool CheckFile(const std::string file_path) {
    std::string file_content;
    ifstream ifs;
    ifs.open(file_path, std::ifstream::binary); // 打开文件

    bool is_check_ok = false;

    // 检查文件是否能打开成功
    if (!ifs.is_open())
        goto Exit;

    // 检查文件大小
    int64 file_size = ifs.seekg(0, ifs.end).tellg();
    ifs.seekg(0, ifs.beg);
    if (file_size > 1024)
        goto Exit;

    // 获取文件第一行内容
    if (!getline(ifs, file_content))
        goto Exit;

    // 检查文件内容
    if (file_content != "this file is legal")
        goto Exit;

    is_check_ok = true;

Exit:
    ifs.close(); // 关闭文件
    return is_check_ok;
}

哈哈哈抱歉多写了几个。

但这样用起来还是很不方便,这三种方式虽然实现了功能,也杜绝了手误写bug的可能性,但这种代码写法还是不如第一种随时return的方式直观。

如果这个ifs能自动close就好了,像一个普通变量一样,超出作用域就自动释放,这样我们就不需要专门处理它的清理工作了。

那么,有办法实现这个效果吗?答案是有,思路是用一个变量把ifs包裹起来,把ifs当初普通变量使用。比如这样:

// 智能文件类
class SmartFile {
private:
    ifstream ifs;
public:
    SmartFile(const std::string file_path) {
        ifs.open(file_path, std::ifstream::binary); // 构造函数里加载文件
    }
    ~SmartFile() {
        ifs.close(); // 析构函数里释放文件
    }
    ifstream& getIfs() {
        return ifs; // 获取ifs
    }
};

bool CheckFile(const std::string file_path) {
    std::string file_content;
    SmartFile smartIfs(file_path); // 使用智能ifs

    // 检查文件是否能打开成功
    if (!smartIfs.getIfs().is_open())
        return false;

    // 检查文件大小
    int64 file_size = smartIfs.getIfs().seekg(0, smartIfs.getIfs().end).tellg();
    if (file_size > 1024)
        return false;
    smartIfs.getIfs().seekg(0, smartIfs.getIfs().beg);

    // 获取文件第一行内容
    if (!getline(smartIfs.getIfs(), file_content))
        return false;

    // 检查文件内容
    if (file_content != "this file is legal")
        return false;

    return true;
}

注意看这个代码,新增一个SmartFile类专门处理ifstream,在构造的时候初始化,在析构的时候close,这样我们就可以像使用普通变量一样使用ifstream了。

无论我们在任何时候(全局变量、函数内、if内)使用SmartFile,只要他超出作用域,SmartFile必然被销毁,析构函数被调用,ifstream自动被close了,无需我们手动处理。

上面代码已经无限接近第一版代码了,非常符合我们日常的思维习惯,但getIfs还是有点绕口,重载运算符来让代码更简洁:

// 智能文件类
class SmartFile {
private:
    ifstream ifs;
public:
    SmartFile(const std::string file_path) {
        ifs.open(file_path, std::ifstream::binary); // 构造函数里加载文件
    }
    ~SmartFile() {
        ifs.close(); // 析构函数里释放文件
    }
    ifstream* operator->() { return &ifs; }
    operator ifstream* () { return &ifs; }
};

bool CheckFile(const std::string file_path) {
    std::string file_content;
    SmartFile smartIfs(file_path); // 使用智能ifs

    // 检查文件是否能打开成功
    if (!smartIfs->is_open())
        return false;

    // 检查文件大小
    int64 file_size = smartIfs->seekg(0, smartIfs->end).tellg();
    if (file_size > 1024)
        return false;
    smartIfs->seekg(0, smartIfs->beg);

    // 获取文件第一行内容
    if (!getline(*smartIfs, file_content))
        return false;

    // 检查文件内容
    if (file_content != "this file is legal")
        return false;

    return true;
}

是不是看起来就顺眼许多啦哈哈哈。

这就是RAII,非常的优雅。

几乎所有一切需要清理资源的,或者需要成对调用的接口,我们都可以使用RAII把它封装起来。

比如CRITICAL_SECTION,使用的时候需要Enter和Leave,可以封装:

class SmartCritSec {
    CRITICAL_SECTION* cs;
public:
    explicit SmartCritSec(CRITICAL_SECTION* cs) : cs(cs) {
        EnterCriticalSection(cs);
    }
    ~SmartCritSec() {
        LeaveCriticalSection(cs);
    }
};
void Test() {
    SmartCritSec scope(&m_access); // m_access为已经初始化过的CRITICAL_SECTION
    // work
}

比如打开文件的HANDLE,结束时需要CloseHandle,可以封装:

class SmartHandle {
private:
    HANDLE handle;
public:
    explicit SmartHandle(HANDLE handle) : handle(handle) { }
    ~SmartHandle() { CloseHandle(handle); }
    operator HANDLE() const { return handle; }
};

void Test() {
    ScopedHandle file(CreateFile(file_path, GENERIC_READ | GENERIC_WRITE, OPEN_EXISTING));
    // work
}

比如Windows的GDI对象HBRUSH创建完需要DeleteObject,可以封装:

class SmartHBrush {
    HBRUSH obj;
public:
    SmartHBrush(HBRUSH obj) : obj(obj) { }
    ~SmartHBrush() { DeleteObject(obj); }
    operator HBRUSH() const { return obj; }
};

void Test() {
    SmartHBrush brush(CreateSolidBrush(RGB(255, 0, 0)));
    // work
}

比如使用com组件时需要CoInitialize和CoUninitialize,可以封装:

class SmartCom {
public:
    SmartCom() { (void)CoInitialize(nullptr); }
    ~SmartCom() { CoUninitialize(); }
};

void Test() {
    SmartCom com;
    // work
}

再也不用惦记着return前执行关闭操作了。

这时有聪明的同学要问了,我很少用你说的这些handle、com、brush,有没有什么我们更常用的代码用RAII实现的,等等你这个Smart单词看起来很眼熟呀~

眼熟就对了,RAII最大的应用,就是处理指针。

比如我们写一个正常的裸指针代码如下:

bool Check() {
    std::string* s = new std::string;
    *s = "abc";
    // work
    if (!s) {
        delete s;
        return false;
    }
    if (*s == "") {
        delete s;
        return false;
    }
    delete s;
    return true;
}

那么我们就可以写一个RAII指针如下:

class SmartString {
private:
    std::string* obj;
public:
    SmartString(std::string* obj) : obj(obj) {}
    ~SmartString() { delete obj; }
    operator std::string* () const { return obj; }
    std::string* operator->() const { return obj; }
};

bool Check() {
    SmartString s(new std::string);
    *s = "abc";
    // work
    if (!s)
        return false;
    if (s->length() == 0)
        return false;
    if (*s == "")
        return false;
    return true;
}

是不是就顺眼很多啦~

来,我们把它变成一个模板,让它更通用:

template <class T>
class SmartPtr {
    T* obj;
public:
    SmartPtr(T* obj) : obj(obj) {}
    ~SmartPtr() { delete obj; }
    operator T* () const { return obj; }
    T* operator->() const { return obj; }
};

bool Check() {
    SmartPtr<std::string> s(new std::string);
    *s = "abc";
    SmartPtr<int> i(new int);
    SmartPtr<Bitmap> d(new Bitmap);
    SmartPtr<MyClassA> c(new MyClassA);
    return true;
}

这就是一个最简单的智能指针,其他所有一切的智能指针,核心原理都是这样的。

当然一个智能指针要在各种复杂环境下都正常工作,仅有这些是不够的,还需要处理其他的逻辑操作,包括重载更多运算符,包括获取裸指针等,有兴趣的小伙伴请研究一下主流智能指针的实现源码。


在智能指针的基础上再多谈一点。

我上面实现的这个智能指针,再丰富一下操作接口,就是std::unique_ptr。

假如现在我在A线程有一个智能指针,指向一个对象(即一块内存),那如果我把这个指针传递给B线程时,我需要复制这个指针,也需要复制对象本身,这样两个线程各自持有各自的智能指针,也指向各自的对象。如果不这么操作,两个线程的指针指向同一对象,那当其中一个线程退出时,智能指针将对象销毁,第二个线程访问对象必然出错。

那么,有什么办法来解决这个问题呢,答案是std::shared_ptr,智能指针内部有一个计数器,初始化时为1,每复制一次指针就将计数器+1,而销毁指针时,并不直接销毁对象,而是将计数器-1,当计数器为0时才真正销毁对象。这样不管我复制多少次智能指针,销毁多少次智能指针,只有最后一个智能指针销毁时才真的销毁对象,非常的安全,高效,更优雅了。

那么,shared_ptr有什么缺点吗,当然有的,它的缺点叫循环引用,看下面这段代码(不再详细讲shared_ptr的实现逻辑了):

struct Node {
    std::shared_ptr<Node> child_node;
};

void Test() {
    std::shared_ptr<Node> item = std::make_shared<Node>();
    item->child_node= item;
}

运行一下就会出现内存泄漏,详细解释一下为什么会出问题:

  • make_shared时new了Node对象,此时计数器为1
  • item->_node = item时将智能指针赋给了child_node,此时既不new也不delete对象,只是计数器变成2
  • 当Test执行完成后,智能指针item销毁,然后shared_ptr本来计数器为2,现在item销毁了,计数器就变成1了,shared_ptr一看计数器没有变成0,也就不会真的执行对象的销毁操作,那么第1步new出来的对象就永远存在了。

硬要解释的话就是本来new出来的对象有2个入口,销毁了一个,第二个入口还在,但我们再也拿不到这个入口了,因此造成内存泄露。

那么,有什么办法来解决shared_ptr循环引用的问题吗,当然有的,那就是weak_ptr。

weak_ptr,看名字就知道,它是一个弱引用智能指针,就是为了解决shared_ptr循环引用问题的,请看代码

struct Node {
    std::weak_ptr<Node> _node;
};

void Test() {
    std::shared_ptr<Node> item = std::make_shared<Node>();
    item->_node = item;
}

如此,便不会内存泄露了。

至于shared_ptr和weak_ptr的详细用法,大家还请继续研究。

看上面智能指针的逻辑体系,其实非常的直白,符合我们的日常思维直觉,一点都不高深,哪怕让我们自己设计一套智能指针系统,我们最终写出来的,肯定也是shared_ptr与weak_ptr这一套。

现在C++已经不推荐使用裸指针了,除非万不得已,不然还是老老实实的使用智能指针,能极大的降低bug概率和程序员的心智负担。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值