欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

浅谈c++资源管理以及对[STL]智能指针auto_ptr源码分析,左值与右值

程序员文章站 2022-09-28 15:29:19
1. 知识引入 在C++编程中,动态分配的内存在使用完毕之后一般都要delete(释放),否则就会造成内存泄漏,导致不必要的后果。虽然大多数初学者都会有这样的意识,但是有些却...

1. 知识引入

在C++编程中,动态分配的内存在使用完毕之后一般都要delete(释放),否则就会造成内存泄漏,导致不必要的后果。虽然大多数初学者都会有这样的意识,但是有些却不以为意。我曾问我的同学关于动态内存的分配与释放,他的回答是:”只要保证new和delete成对出现就行了。如果在构造函数中new(动态分配内存),那么在析构函数中delete(释放)就可以避免内存泄漏了!”

事实果真如此么?

实例一:

(当子函数中动态分配的内存只在子函数中使用而不返回指向该动态内存的指针时,子函数中动态分配的内存即使不释放也不会造成内存泄漏,因为在销毁栈的同时,会自动释放该内存。因此,我用main函数举例)

int main(void) {
    int *a = new int(35);
    if (*a != 50) return 0;
    delete a;
    return 0;
}

在这种情况下,new和delete虽然成对出现,但是仍出现了内存泄漏的情况。因此,成对出现并不能保证不会发生内存泄漏。

实例二:

class A {
 public:
     A(): a(NULL) {}
     A(int a) {
        this->a = new int(a);
     }
     ~A() {
        if (a != NULL) delete a;
     }
     A operator =(A src) {
        this->a = new int(src.a);
     }
 private:
    int *a; 
};

int main(void) {
    A num(10);
    A num1(15);
    num = num1;
    return 0;
}

在这种情况下,构造函数new(动态分配)一段内存,析构函数delete(释放)这段内存。看似在对象建立时调用构造完成内存分配,在对象销毁时调用析构释放内存,一切都很正常不会造成内存泄漏,但是问题就出在了num = num1;上,即是重载“=”出现问题导致内存泄漏。因为在赋值之前,num中的a已经指向堆中一段内存,而且是访问该内存的唯一方式。一旦直接赋值,会导致这块内存无法被访问,因此导致了内存泄漏。

由此看来,内存泄漏的情况很容易出现,那么有没有方法可以避免这种情况发生呢?

这时,我们可以设计一个类似于资源管理的类,来实现对指针指向的动态内存的管理。这个类的精髓在于,成员变量用于指向动态内存,析构函数用于释放该变量指向的动态内存。即:

template 
class manage {
 public:
     manage(T *p) : pArr(p) {} 
     ~manage() {
        if (pArr != NULL) delete pArr;
     }
     T* get() { return pArr; }
 private:
     T *pArr;
};

这个资源管理类没有设置无参数的构造函数的原因是:该类需满足:RAII机制

RAII机制:RAII,也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。
RAII的做法:使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

那么什么是资源获取就是初始化呢?那就是new(动态分配内存)后直接将地址作为参数传入给资源管理类的对象(调用对象的构造函数)。

manage instance(new int[10]);

new(动态分配)的int[10]的地址就直接用于初始化instance对象,这就是资源获取就是初始化。

如果我们再往这个类中加入成员方法,这就构成了一个完整的资源管理类,也就是智能指针auto_ptr。

According to cplusplus.com, auto_ptr is in

class template std::auto_ptr

template  class auto_ptr;

Automatic Pointer [deprecated]
Note: This class template is deprecated as of C++11. unique_ptr is a new facility with a similar functionality, but with improved security (no fake copy assignments), added features (deleters) and support for arrays. See unique_ptr for additional information.

This class template provides a limited garbage collection facility for pointers, by allowing pointers to have the elements they point to automatically destroyed when the auto_ptr object is itself destroyed.

auto_ptr objects have the peculiarity of taking ownership of the pointers assigned to them: An auto_ptr object that has ownership over one element is in charge of destroying the element it points to and to deallocate the memory allocated to it when itself is destroyed. The destructor does this by calling operator delete automatically.

Therefore, no two auto_ptr objects should own the same element, since both would try to destruct them at some point. When an assignment operation takes place between two auto_ptr objects, ownership is transferred, which means that the object losing ownership is set to no longer point to the element (it is set to the null pointer).

Template parameters
X: The type of the managed object, aliased as member type element_type.

不过有一点十分重要,auto_ptr的对象在构造时获得动态内存的ownership(所有权),且在析构时释放这段内存,更重要的是,在调用复制构造函数时是实现ownership(所有权)的转移而不是深拷贝,拥有这段动态内存的对象只能有一个

auto_ptr 源码如下:

template
class auto_ptr
{
private:
    T*ap;
public:
    //constructor & destructor-----------------------------------(1)
    explicit auto_ptr(T*ptr=0)throw():ap(ptr) {}

    ~auto_ptr()throw() {
        delete ap;
    }
    //Copy & assignment--------------------------------------------(2)
    auto_ptr(auto_ptr& rhs)throw():ap(rhs.release()) {}

    template
    auto_ptr(auto_ptr&rhs)throw():ap(rhs.release()) {}

    auto_ptr& operator=(auto_ptr&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }

    template
    auto_ptr& operator=(auto_ptr&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }
    //Dereference----------------------------------------------------(3)
    T& operator*()const throw()
    {
        return *ap;

    T* operator->()const throw()
    {
        return ap;
    }
    //Helper functions------------------------------------------------(4)
    //value access
    T* get()const throw()
    {
        return ap;
    }

    //release owner ship
    T* release()throw()
    {
        T* tmp(ap);
        ap = 0;
        return tmp;
    }

    //reset value
    void reset(T* ptr = 0)throw()
    {
        if(ap != ptr)
        {
            delete ap;
            ap = ptr;
        }
    }
    //Special conversions-----------------------------------------------(5)
    template
    struct auto_ptr_ref
    {
        Y*yp;
        auto_ptr_ref(Y*rhs):yp(rhs){}
    };

    auto_ptr(auto_ptr_refrhs)throw():ap(rhs.yp) {}

    auto_ptr& operator=(auto_ptr_refrhs)throw()
    {
        reset(rhs.yp);
        return*this;
    }

    template
    operator auto_ptr_ref()throw()
    {
        return auto_ptr_ref(release());
    }

    template
    operator auto_ptr()throw()
    {
        return auto_ptr(release());
    }
};

在这之前,我要先说明源码中重复出现的throw()函数。throw()函数类似一个声明,保证了该函数不会抛出任何异常,因为STL需要保证异常安全

3.异常安全

异常安全是指,一个对象碰到异常之后,还能够保证自身的正确性。

C++中’异常安全函数”提供了三种安全等级:(取自推荐的文章: “C++中的异常安全性”)
1. 基本承诺:如果异常被抛出,对象内的任何成员仍然能保持有效状态,没有数据的破坏及资源泄漏。但对象的现实状态是不可估计的,即不一定是调用前的状态,但至少保证符合对象正常的要求。
2. 强烈保证:如果异常被抛出,对象的状态保持不变。即如果调用成功,则完全成功;如果调用失败,则对象依然是调用前的状态。
3. 不抛异常保证:函数承诺不会抛出任何异常。一般内置类型的所有操作都有不抛异常的保证。

其实不加这个throw()也是可以的,不过STL有时会要求加上。

4.智能指针auto_ptr源码分析

下面我将对auto_ptr的源码进行详细的分析:

首先是构造函数与析构函数:

 //constructor & destructor-----------------------------------(1)
    explicit auto_ptr(T*ptr=0)throw():ap(ptr) {}

    ~auto_ptr()throw() {
        delete ap;  // delete dynamic storage
    }

就如前面所讲,这个类的核心就在于含有一个模板指针aq,以及析构函数delete(释放)这个指针指向的动态内存。对于这个explicit,表明这个构造函数是一个显式的构造函数。除了满足谷歌风格以外,还限制了参数不能有隐式转换。

接着是复制构造函数和release():

//release owner ship
    T* release()throw()
    {
        T* tmp(ap); 
/*here is a type-transformation, it have definition in this class and I will analyse it in the following passage */
        ap = 0;  // set it to NULL after transfer the ownership
        return tmp;  // return the address of dynamic storage
    }
//Copy & assignment--------------------------------------------(2)
    auto_ptr(auto_ptr& rhs)throw():ap(rhs.release()) {}
    // call the release() to return the address of dynamic storage
    template
    auto_ptr(auto_ptr&rhs)throw():ap(rhs.release()) {}
    // also have the same function as above one
    auto_ptr& operator=(auto_ptr&rhs)throw()
    {
        reset(rhs.release()); // reset() will be analyse in the following passage
        return *this;
    }
    // overload "=", and reset() must pay attention to delete the dynamic storage of (*this)
    template
    auto_ptr& operator=(auto_ptr&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }
    // also have the same function as the above one

正如前面所讲,复制构造函数是实现动态内存的ownership(所有权)的转移,而不是深拷贝。为什么不采用深拷贝?原因是:因为采用深拷贝就不再满足我们设计该智能指针的初衷

既然是实现ownership(所有权)的转移,那么要通过release()函数,让原所有权拥有者放出所有权,使之返回原类型的指针,即动态内存的地址, 将之作为新所有权拥有者复制构造函数的参数。然后自身置NULL,不过所有权的新拥有者也要注意赋值前内存的释放。

细心的人也许会感觉奇怪,auto_ptr是一个模板类,为什么在复制构造函数里既有尖括号,也有省略尖括号的。对于一个模板类,一般情况下不是都要加上尖括号么?

刚开始我也感觉奇怪,后来想明白了:
在auto_ptr的定义里,auto_ptr默认是auto_ptr。auto_ptr与auto_ptr是两个不同的类,它们是相互独立的。

因此就不难理解为什么要这样定义:

auto_ptr (auto_ptr& a) throw(); // the para's type is auto_ptr
template
  auto_ptr (auto_ptr& a) throw(); // the para's type is auto_ptr instead of auto_ptr

相似的,重载”=”的两个函数也不难理解了。
但是其实,不加auto_ptr (auto_ptr& a) throw();好像也没有什么问题,因为编译器一般都会优先调用用户定义的函数。但是为了保险起见,还是加上为好。因为传入相同类型,编译器有调用默认构造函数的可能。

接着是重载 * 与 ->, 应该不难理解。

    //Dereference----------------------------------------------------(3)
    T& operator*()const throw()
    {
        return *ap;
    }
    T* operator->()const throw()
    {
        return ap;
    }

再接着就是获取成员指针的函数。

 //value access
    T* get()const throw() // interface for getting ap
    {
        return ap;
    }

    //release owner ship

因为ap是私有成员,需要get()函数提供一个访问的接口。

然后继续, 进行成员指针重置的函数。

    //reset value
    void reset(T* ptr = 0)throw()
    {
        if(ap != ptr)
        {
            delete ap;  // very important , it avoid the storage leak
            ap = ptr;  // assign of ap
        }
    }

最后是在定义auto_ptr的一个代理类。刚开始我很疑惑,为什么要再定义一个代理类,有什么不能通过auto_ptr解决么?原来auto_ptr的复制构造函数是有缺陷的。当传入的参数为左值时,可以正常编译,但是一旦传入的参数为右值时,g++上就编译不通过了。(左值右值在后面我会讲解)因为右值引用必须为const引用。

 

//Special conversions-----------------------------------------------(5)
    template
    struct auto_ptr_ref  // define a reference to automatic pointer
    {
        Y *yp;
        auto_ptr_ref(Y *rhs):yp(rhs){} // constructor
    };

    auto_ptr(auto_ptr_refrhs)throw():ap(rhs.yp) {}  // put auto_ptr_ref's object as a para

    auto_ptr& operator=(auto_ptr_refrhs)throw()
    {
        reset(rhs.yp);
        return *this;
    }
/*here is the data_transformation. With the help of it, it solves the bug of auto_ptr
*/
    template  // transform data_type to auto_ptr_ref
    operator auto_ptr_ref()throw()
    {
        return auto_ptr_ref(release());
    }

    template  // transform data_type to auto_ptr
    operator auto_ptr()throw()
    {
        return auto_ptr(release());
    }

5. 左值与右值

在查阅资料之前,我也是误解了左值与右值的定义。左值与右值常见的误区在与:认为等号左边就是左值,等号右边就是右值。其实不然,等号只是左值右值中的一个特例,并不能用于概括左值与右值的概念,即并不适用于所有地方。其实,左值与右值是相对于表达式而言,当一个表达式执行结束以后,若该对象仍恒定存在,那么说明该对象是一个左值。如果在表达式结束后,该对象不存在,说明该对象是一个临时对象,即为右值。