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

C++:浅谈c++资源管理以及对[STL]智能指针auto_ptr源码分析

by 小威威


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 <typename T>
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<int> instance = new int[10];

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

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

2.智能指针auto_ptr源码呈现

According to cplusplus.com, auto_ptr is in <memory>

class template std::auto_ptr

template <class X> 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 T>
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<class Y>
    auto_ptr(auto_ptr<Y>&rhs)throw():ap(rhs.release()) {}

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

    template<class Y>
    auto_ptr& operator=(auto_ptr<Y>&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<class Y>
    struct auto_ptr_ref
    {
        Y*yp;
        auto_ptr_ref(Y*rhs):yp(rhs){}
    };

    auto_ptr(auto_ptr_ref<T>rhs)throw():ap(rhs.yp) {}

    auto_ptr& operator=(auto_ptr_ref<T>rhs)throw()
    {
        reset(rhs.yp);
        return*this;
    }

    template<class Y>
    operator auto_ptr_ref<Y>()throw()
    {
        return auto_ptr_ref<Y>(release());
    }

    template<class Y>
    operator auto_ptr<Y>()throw()
    {
        return auto_ptr<Y>(release());
    }
};

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

3.异常安全

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

这里推荐一篇文章:C++中的异常安全性

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<class Y>
    auto_ptr(auto_ptr<Y>&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<class Y>
    auto_ptr& operator=(auto_ptr<Y>&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }
    // also have the same function as the above one

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

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

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

刚开始我也感觉奇怪,后来想明白了:

在auto_ptr<T>的定义里,auto_ptr默认是auto_ptr<T>。auto_ptr<T>与auto_ptr<Y>是两个不同的类,它们是相互独立的。

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

auto_ptr (auto_ptr& a) throw(); // the para‘s type is auto_ptr<T>
template<class Y>
  auto_ptr (auto_ptr<Y>& a) throw(); // the para‘s type is auto_ptr<Y> instead of auto_ptr<T>

相似的,重载”=”的两个函数也不难理解了。

但是其实,不加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引用。

推荐一篇写的不错的文章:关于auto_ptr_ref的一点问题,博主对这一问题解释的非常完美!

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

    auto_ptr(auto_ptr_ref<T>rhs)throw():ap(rhs.yp) {}  // put auto_ptr_ref‘s object as a para

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

    template<class Y>  // transform data_type to auto_ptr
    operator auto_ptr<Y>()throw()
    {
        return auto_ptr<Y>(release());
    }

5. 左值与右值

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

推荐一篇写的不错的文章:C++ 右值引用



以上内容皆为本人观点,欢迎大家提出批评和指导,我们一起探讨!


时间: 2024-10-13 01:10:07

C++:浅谈c++资源管理以及对[STL]智能指针auto_ptr源码分析,左值与右值的相关文章

STL学习_stl_list.h_源码分析

stl_list.h中有几个函数自己觉得比较重要,transfer()  merge()  sort() #ifndef _SGI_STL_INTERNAL_LIST_H #define _SGI_STL_INTERNAL_LIST_H //list迭代器结构 //不同的容器往往要给容器设置符合自己的迭代器,list的迭代器类型是双向迭代器 //list的迭代器必须有能力进行递增,递减,取值,成员存取等操作 template<class T, class Ref, class Ptr> str

stl智能指针auto_ptr

感觉auto_ptr还是存在一些不足的,效率十分低,而且丢失了一般指针方便常用的操作,如++,--等,auto_ptr只重载了=,*,->这几个操作符,所以使用很不方便. 一.说明:int *a = NULL;delete a;//do nothing 应此auto_ptr的析构函数是这样子的: ~auto_ptr() { // destroy the object delete _Myptr; }    二.auto_ptr中有一个很重要的函数release _Ty *release() _T

浅谈c语言typedef 与结构体指针(个人小经验)

 #include<stdio.h> #include<string.h> typedef struct emp{ char sex[8]; char name[15]; int age; }*emp;//这里我们用typedef把emp这个结构体变成了*emp这种指向结构体成员的结构体指针 /*typedef struct emp{ char sex[8]; char name[15]; int age; }pi,*emp;//为了程序的可读性最好不要这样声明*/ int m

stl源码分析之hash table

本文主要分析g++ stl中哈希表的实现方法.stl中,除了以红黑树为底层存储结构的map和set,还有用哈希表实现的hash_map和hash_set.map和set的查询时间是对数级的,而hash_map和hash_set更快,可以达到常数级,不过哈希表需要更多内存空间,属于以空间换时间的用法,而且选择一个好的哈希函数也不那么容易. 一. 哈希表基本概念 哈希表,又名散列表,是根据关键字直接访问内存的数据结构.通过哈希函数,将键值映射转换成数组中的位置,就可以在O(1)的时间内访问到数据.举

stl源码分析之list

本文主要分析gcc4.8版本的stl list的源码实现,与vector的线性空间结构不同,list的节点是任意分散的,节点之间通过指针连接,好处是在任何位置插入删除元素都只需要常数时间,缺点是不能随机访问,查询复杂度是O(n),n为list中的元素个数.所以list非常适合应用与数据插入删除频繁的场景. 一. list节点 list节点定义如下, struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_pre

STL源码分析--仿函数 &amp; 配接器

STL源码分析-仿函数 & 配接器 仿函数就是函数对象.就实现观点而言,仿函数其实就是一个"行为类似函数"的对象.为了能够"行为类似函数",其类别定义中必须自定义(或说改写.重载)function call 运算子(operator()),拥有这样的运算子后,我们就可以在仿函数的对象后面加上一对小括号,以此调用仿函数所定义的operator().仿函数作为可配接的关键因素. 配接器在STL组件的灵活组合运用功能上,扮演着轴承.转换器的角色,adapter的定

stl源码分析之priority queue

前面两篇介绍了gcc4.8的vector和list的源码实现,这是stl最常用了两种序列式容器.除了容器之外,stl还提供了一种借助容器实现特殊操作的组件,谓之适配器,比如stack,queue,priority queue等,本文就介绍gcc4.8的priority queue的源码实现. 顾名思义,priority queue是带有优先级的队列,所以元素必须提供<操作符,与vector和list不同,priority queue允许加入元素,但是取出时只能取出优先级最高的元素. 一. pri

stl源码分析之vector

上篇简单介绍了gcc4.8提供的几种allocator的实现方法和作用,这是所有stl组件的基础,容器必须通过allocator申请分配内存和释放内存,至于底层是直接分配释放内存还是使用内存池等方法就不是组件需要考虑的事情.这篇文章开始分析gcc4.8 stl的容器源码实现.stl的容器分为序列式容器和关联式容器,前者包括vector,list,queue以及stack等常用数据结构,后者包含了map,set以及hash table等比较高级的结构,本文就从使用最广泛也是最基础的vector开始

STL源码分析--空间配置器的底层实现 (二)

STL源码分析-空间配置器 空间配置器中门道 在STL中的容器里都是使用统一的空间配置器,空间配置器就是管理分配内存和销毁内存的.在STL将在heap空间创建一个对象分为两个步骤,第一是申请一块内存,第二是在这块内存中初始化一个对象.首先申请空间是由malloc提供,初始化一个对象时由constructor管理.销毁一个对象也是由两步骤完成,第一是销毁空间上的对象,第二是释放这块内存. 同时,STL的空间配置器分为两级内存,如果申请的内存空间大于128KB,那么就使用第一级空间配置,如果小于,那