栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++经验(九)-- 注意函数的异常安全性及资源的管理问题

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++经验(九)-- 注意函数的异常安全性及资源的管理问题

我们看下下面这个例子:

class A{
public:
    A(){}

    void changBackgroundImage(const std::istream& img);

private:
    Mutex   m_mutex{};
    Image*  m_pImg { nullptr };
    int     m_times{ 0 };
}

下面是 changBackgroundImage 函数的某个实现:

viod A::changBackgroundImage(const std::istream& img)
{
    lock(&m_mutex);
    delete m_image;
    ++m_times;
    m_pImg = new Image(img);
    unlock(&m_mutex);
}

上面这个函数咋一看,好像没什么问题,但如果细究下来,漏洞百出。我们在使用 C API 函数处理类型为 Mutex 的互斥器对象时,通常也是有两个函数。

void lock(Mutex* mutex);        //锁定
void unlock(Mutex* mutex);      //解锁

上面的用法也符合成对出现的规则。

但是上面 changBackgroundImage 函数的实现中存在的问题是:

  • 如果在 new Image(img); 的时候抛出了异常, 那么下面的语句 unlock(&m_mutex); 将永远不会被执行,互斥器的锁定将永远存在,不符合异常安全性函数的不泄露任何资源原则。
  • 如果在 new Image(img); 的时候抛出了异常,那么原本的对象 m_pImg 其实已经被析构 m_pImg 指向了一个已经被析构的对象, m_times 也已经被修改,不符合异常安全性函数的不允许数据被破坏原则。

当然,为了确保我们不会忘记一个被锁定的 Mutex 对象解锁,我们会建立一个 class 来解决这个问题。这个 class 的基本结构由 RAII 原则来框画。也就是满足“资源在构造时被申请,在析构时被释放”。

class Lock{
public:
    explicit Lock(Mutex* mutex) : m_pMutex(mutex)
    {
        lock(m_pMutex);
    }

    ~Lock()
    {
        unlock(m_pMutex);
    }

private:
    Lock(const Lock&);
    Lock& operator=(const Lock&);

private:
    Mutex* m_pMutex{ nullptr };
}

现在我们再来看下 changBackgroundImage 函数的实现过程:

viod A::changBackgroundImage(const std::istream& img)
{
    Lock ml(&m_mutex);
    delete m_image;
    ++m_times;
    m_pImg = new Image(img);
}

我们使用了类 Lock 来进行资源 mutex 的管理。这样就能避免我们上面所说的第一个问题,当在 new Image(img); 的过程中抛出异常时,我们也能够正常的释放 m_mutex 锁定的资源。

上面的类 Lock 在定义的时候,我们将其 copy 构造和 复制操作符声明为 private 类型,并且不去实现这两个函数,其目的是禁用类 Lock 的拷贝行为。

相对来说,上面我们通过 **以类的形式管理资源(资源管理类)**方法解决了资源泄漏的问题,当然这种方式使得函数 changBackgroundImage 的函数体也更小了,代码越少,出错的机会就越少。下面我们就需要专心解决数据被破坏的问题。

我们看下,changBackgroundImage 函数内其实是有一个指针指向的对象存储了图像的资源,而我们前面也提到过,以对象的形式进行资源管理,所以,很快就有了下面的改变。

class A{

private:
    std::tr1::shared_ptr m_pImg;
}

viod A::changBackgroundImage(const std::istream& img)
{
    Lock ml(&m_mutex);
    m_pImg.reset(new Image(img));
    ++m_times;  
}

上面这个函数已经有了很大的改变,将指针定义为我们前面说过的智能指针,通过引用计数的方式来保证资源的释放。并且能够缩小函数体。m_pImg 指向的内存也只有在 new Image(img) 执行成功的前提下被重新设置。也就是说 delete 操作只在新对象被重新分配之后才被执行。

这样也就保证了数据不被破坏,同时我们将 m_times 的自增放在了后面。保证 m_pImg 在正常进行reset之后才标记次数的改变。

一般情况下,上面的操作基本能够保证异常安全。但是上面的函数我们注意到使用的参数类型是std的istream,所以她可能存在的一个问题是,当函数体中抛出异常时,读取的标记可能被清除了。那该怎么解决呢?

或许我们可以通过修改参数类型的方式进行修改,但是今天他不是主角。今天的主角是 copy-and-swap。

我在前面学习stl的时候,曾经有提到过这种方法。这种方法的核心就是先复制一份,然后在复制的这一份上做修改,确保没有任何问题之后,再在安全的环境下将原对象和复制的对象进行交换。

struct PmImp{
    std::tr1::shared_ptr m_pImg;
    int m_times{ 0 };
}

class A{

private:
    Mutex m_mutex{};
    std::tr1::shared_ptr m_pImp;
}

viod A::changBackgroundImage(const std::istream& img)
{
    Lock ml(&m_mutex);
    std::tr1::shared_ptr pNew(new PmImp(*m_pImp));  //先进行拷贝

    pNew->m_pImg.reset(new Image(img));
    ++pNew->m_times;

    std::swap(m_pImp, pNew);
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/982215.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号