C++ 智能指针(1):auto_ptr

C++智能指针(1):auto_ptr

分析

C++中经常会出现因为没有delete指针而造成的内存泄漏,例如有下面一个类

class Obj {
public:
    Obj() { std::cout << "Construct" << std::endl; }

    Obj(const Obj &other) { std::cout << "Copy" << std::endl; }

    Obj(Obj &&other) noexcept { std::cout << "Move" << std::endl; }

    ~Obj() { std::cout << "Destruct" << std::endl; }

    void Print() { std::cout << "Print" << std::endl; }
};

创建一个指向Obj类型的指针

int main() {
    Obj *o = new Obj();
    o->Print();
    return 0;
}
/*
output:
Construct
Print
*/

我们没有进行delete o的操作,导致o没有被正确地析构,造成了内存泄漏。作为对比,创建一个Obj类型的对象

int main() {
    Obj *o1 = new Obj();
    o1->Print();
    Obj o2 = Obj();
    o2.Print();
    return 0;
}
/*
output:
Construct
Print
Construct
Print
Destruct
*/

产生这样的结果是因为对象创建在栈(stack)上,编译器会自动进行对象的创建和销毁,而指针是创建在堆(heap)上,需要手动进行创建和销毁。为了规避这样的问题,我们可以封装一个智能指针类,用类来管理指针,防止造成内存泄漏,并且尽可能的模仿指针的用法。

实现

根据auto_ptr的源码,能够大致实现AutoPointer类

template<typename T>
class AutoPointer {
public:
    explicit AutoPointer(T *t);

    ~AutoPointer();

    T &operator*();

    T *operator->();

    T *release();

    void reset(T *p);

    AutoPointer(AutoPointer<T> &other);

    AutoPointer<T> &operator=(AutoPointer<T> &other);

private:
    T *pointer;
};

template<typename T>
AutoPointer<T>::AutoPointer(T *t):pointer(t) {
    std::cout << "AutoPointer " << this << " constructor called." << std::endl;
}

template<typename T>
AutoPointer<T>::~AutoPointer() {
    std::cout << "AutoPointer " << this << " destructor called." << std::endl;
    delete pointer;
}

template<typename T>
T &AutoPointer<T>::operator*() {
    return *pointer;
}

template<typename T>
T *AutoPointer<T>::operator->() {
    return pointer;
}

template<typename T>
T *AutoPointer<T>::release() {
    T *new_pointer = pointer;
    pointer = nullptr;
    return new_pointer;
}

template<typename T>
void AutoPointer<T>::reset(T *p) {
    if (pointer != p) {
        delete pointer;
        pointer = p;
    }
}

template<typename T>
AutoPointer<T>::AutoPointer(AutoPointer<T> &other): pointer(other.release()) {
    std::cout << "AutoPointer " << this << " copy constructor called." << std::endl;
}

template<typename T>
AutoPointer<T> &AutoPointer<T>::operator=(AutoPointer<T> &other) {
    std::cout << "AutoPointer " << this << " assignment operator called." << std::endl;
    if (pointer != other.pointer)
        reset(other.release());
    return *this;
}

我们在进行拷贝构造时对参数对象的指针进行了release操作,也就是将参数对象的私有成员pointer指针置为了nullptr;在进行=操作时不仅对参数对象的指针进行了release操作,还delete掉了当前对象的pointer,再将pointer置为参数对象的pointer。这样的实现非常有效地规避了迷途指针(也称悬空指针或野指针)。

测试

现在创建一个AutoPointer智能指针类试验一下

int main() {
    Obj *o = new Obj();
    AutoPointer<Obj> a1(o);
    (*a1).Print();
    a1->Print();
    return 0;
}
/*
output:
Construct
AutoPointer 0x7fe680c02ab0 constructor called.
Print
Print
AutoPointer 0x7fe680c02ab0 destructor called.
Destruct
*/

在创建单个对象的时候能够正常地使用,现在创建两个AutoPointer类对象,用同一个Obj对象初始化

int main() {
    Obj *o = new Obj();
    AutoPointer<Obj> a1(o);
    AutoPointer<Obj> a2(o);
    return 0;
}
/*
output:
(7855,0x111b135c0) malloc: *** error for object 0x7fbe9fc02ab0: pointer being freed was not allocated
(7855,0x111b135c0) malloc: *** set a breakpoint in malloc_error_break to debug
*/

s1, s2两个AutoPointer对象会各自调用一次析构函数,Obj对象的指针o被析构了两次,显然是有问题的。

再尝试使用拷贝构造函数

int main() {
    Obj *o = new Obj();
    AutoPointer<Obj> a1(o);
    AutoPointer<Obj> a2(a1);
    return 0;
}
/*
output:
Construct
AutoPointer 0x7fd15bc02ab0 constructor called.
AutoPointer 0x7fd15bc02ab0 copy constructor called.
AutoPointer 0x7fd15bc02ab0 destructor called.
Destruct
AutoPointer 0x0 destructor called.
*/

看上去没有问题。但是o对象原本是属于s1的,在s2调用拷贝构造函数之后,s1的pointer变成了空指针,o对象属于了s2,这叫做所有权转换。

再尝试使用=操作符

int main() {
    Obj *o = new Obj();
    AutoPointer<Obj> a1(o);
    AutoPointer<Obj> a2 = a1;
    return 0;
}
/*
output:
Construct
AutoPointer 0x7ff5d5402ab0 constructor called.
AutoPointer 0x7ff5d5402ab0 copy constructor called.
AutoPointer 0x7ff5d5402ab0 destructor called.
Destruct
AutoPointer 0x0 destructor called.
*/

同样的会发生所有权转移。 

总结

AutoPointer虽然非常有效地解决了野指针问题,但又引入一些其他的问题,例如

1. 所有权转移

  • 可能会产生临时对象进行拷贝和赋值
  • 将AutoPointer作为参数进行值传递时会进行拷贝构造

2. 内存泄漏

  • 在析构函数中使用了delete进行指针的销毁,如果以数组指针进行初始化 AutoPointer<int> s1(new int[10])会因为没有销毁数组的其他元素发生内存泄漏

auto_ptr源码

template<class _Tp>
class _LIBCPP_TEMPLATE_VIS auto_ptr
{
private:
    _Tp* __ptr_;
public:
    typedef _Tp element_type;

    _LIBCPP_INLINE_VISIBILITY explicit auto_ptr(_Tp* __p = 0) throw() : __ptr_(__p) {}
    _LIBCPP_INLINE_VISIBILITY auto_ptr(auto_ptr& __p) throw() : __ptr_(__p.release()) {}
    template<class _Up> _LIBCPP_INLINE_VISIBILITY auto_ptr(auto_ptr<_Up>& __p) throw()
        : __ptr_(__p.release()) {}
    _LIBCPP_INLINE_VISIBILITY auto_ptr& operator=(auto_ptr& __p) throw()
        {reset(__p.release()); return *this;}
    template<class _Up> _LIBCPP_INLINE_VISIBILITY auto_ptr& operator=(auto_ptr<_Up>& __p) throw()
        {reset(__p.release()); return *this;}
    _LIBCPP_INLINE_VISIBILITY auto_ptr& operator=(auto_ptr_ref<_Tp> __p) throw()
        {reset(__p.__ptr_); return *this;}
    _LIBCPP_INLINE_VISIBILITY ~auto_ptr() throw() {delete __ptr_;}

    _LIBCPP_INLINE_VISIBILITY _Tp& operator*() const throw()
        {return *__ptr_;}
    _LIBCPP_INLINE_VISIBILITY _Tp* operator->() const throw() {return __ptr_;}
    _LIBCPP_INLINE_VISIBILITY _Tp* get() const throw() {return __ptr_;}
    _LIBCPP_INLINE_VISIBILITY _Tp* release() throw()
    {
        _Tp* __t = __ptr_;
        __ptr_ = 0;
        return __t;
    }
    _LIBCPP_INLINE_VISIBILITY void reset(_Tp* __p = 0) throw()
    {
        if (__ptr_ != __p)
            delete __ptr_;
        __ptr_ = __p;
    }

    _LIBCPP_INLINE_VISIBILITY auto_ptr(auto_ptr_ref<_Tp> __p) throw() : __ptr_(__p.__ptr_) {}
    template<class _Up> _LIBCPP_INLINE_VISIBILITY operator auto_ptr_ref<_Up>() throw()
        {auto_ptr_ref<_Up> __t; __t.__ptr_ = release(); return __t;}
    template<class _Up> _LIBCPP_INLINE_VISIBILITY operator auto_ptr<_Up>() throw()
        {return auto_ptr<_Up>(release());}
};
comments powered by Disqus