智能指针

简述

智能指针,可以简化资源的管理,从根本上消除资源(包括内存)泄漏的可能性。

智能指针本质上是 RAII 资源管理功能的自然展现而已。

简单的智能指针实现

template <typename T>
class m_ptr
{
public:
	explicit m_ptr(T* ptr = nullptr) : _ptr(ptr) {}
	~m_ptr() { delete _ptr; }

	T* get() const { return _ptr; };

	// * 运算符解引用
	T& operator*() const { return *_ptr; }
	// -> 运算符指向对象成员
	T* operator->() const { return _ptr; }
	//像指针一样用在布尔表达式里
	operator bool() const { return _ptr; }
private:
	T* _ptr;
};

拷贝

在拷贝构造函数中,通过调用 other 的 release 方法来释放它对指针的所有权。在赋值函数中,则通过拷贝构造产生一个临时对象并调用 swap 来交换对指针的所有权。

赋值分为拷贝构造和交换两步,异常只可能在第一步发生;而第一步如果发生异常的话,this 对象完全不受任何影响。无论拷贝构造成功与否,结果只有赋值成功和赋值没有效果两种状态,而不会发生因为赋值破坏了当前对象这种场景。

template <typename T>
class m_ptr
{
public:
	explicit m_ptr(T* ptr = nullptr) : _ptr(ptr) {}
	~m_ptr() { delete _ptr; }

	T* get() const { return _ptr; };

	// * 运算符解引用
	T& operator*() const { return *_ptr; }
	// -> 运算符指向对象成员
	T* operator->() const { return _ptr; }
	//像指针一样用在布尔表达式里
	operator bool() const { return _ptr; }
	
    // 实现拷贝
	m_ptr(m_ptr& other) { _ptr = other.release(); }
	m_ptr& operator=(m_ptr rhs) 
	{ 
		m_ptr(rhs).swap(*this); 
		return *this; 
	}
	T* release() 
	{ 
		T* ptr = _ptr;
		_ptr = nullptr;
		return ptr; 
	}
	void swap(m_ptr& rhs) 
	{
		std::swap(m_ptr, rhs._ptr);
	}

private:
	T* _ptr;
};

上面给出的语义本质上就是 C++98 的 auto_ptr 的定义。这个实现很别扭, C++ 委员会决定auto_ptr 在 C++17 时正式从 C++ 标准里删除。

上面实现的最大问题是,一不小心把它传递给另外一个 m_ptr,你就不再拥有这个对象了。

移动指针

m_ptr(m_ptr&& other) { _ptr = other.release(); }

m_ptr& operator=(m_ptr rhs) 
{ 
	m_ptr(rhs).swap(*this); 
	return *this; 
}
  • 把拷贝构造函数中的参数类型 m_ptr& 改成了 m_ptr&&;现在它成了移动构造函数。
  • 把赋值函数中的参数类型 m_ptr& 改成了 m_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数是移动构造还是拷贝构造。

根据 C++ 的规则,如果提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用。


m_ptr<shape> ptr1{create_shape(shape_type::circle)};
m_ptr<shape> ptr2{ptr1};             // 编译出错
m_ptr<shape> ptr3;
ptr3 = ptr1;                         // 编译出错
ptr3 = std::move(ptr1);              // OK
m_ptr<shape> ptr4{std::move(ptr3)};  // OK

这是 C++11 的 unique_ptr 的基本行为。

子类指针向基类指针转换

//circle* 是可以隐式转换成 shape* 的,但m_ptr<circle>却无法自动转换成 m_ptr<shape>,显然不够自然。
//增加一个构造函数即可。
	template <typename V>
	m_ptr(m_ptr<V>&& other) { _ptr = other.release(); }
// 利用指针转换特
// m_ptr<circle> to m_ptr<shape>  YES
// m_ptr<circle> to m_ptr<triangle> COMPLIE ERROR

需要注意,上面这个构造函数不被编译器看作移动构造函数,因而不能自动触发删除拷贝构造函数的行为。如果我们想消除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = delete 了。不过,更通用的方式仍然是同时定义标准的拷贝 / 移动构造函数和所需的模板构造函数。

非隐式的转换,要写特殊的转换函数。

引用计数

unique_ptr 一个对象只能被单个 unique_ptr 所拥有,但一种常见的情况是,多个智能指针同时拥有一个对象;当它们全部都失效时,这个对象也同时会被删除,即 shared_ptr

多个不同的 shared_ptr 不仅可以共享一个对象,在共享同一对象时也需要同时共享同一个计数。当最后一个指向对象(和共享计数)的 shared_ptr 析构时,它需要删除对象和共享计数。

img
img
class share_count
{
public:
	share_count():_count(1){}
	void add() { ++_count; }
	long reduce() { return --_count; }
	long get() const { return _count; }

private:
	long _count;
};

减少计数时需要返回计数值,以供调用者判断是否它已经是最后一个指向共享计数的 shared_ptr 。

构造函数、析构函数和私有成员变量。

析构函数在看到 _ptr 非空时,需要对引用数减一,并在引用数降到零时彻底删除对象和共享计数。

template <typename T>
class m_s_ptr 
{
public:
    friend class m_s_ptr;
	explicit m_s_ptr(T* ptr = nullptr):_ptr(ptr)
	{
		if (ptr) {
			_sc = new share_count();
		}
	}

	~m_s_ptr()
	{
		if (_ptr && !_sc->reduce()) {
			delete _ptr;
			delete _sc;
		}
	}
    
	T* get() const { return _ptr; }

private:
	T* _ptr;
	share_count* _sc;
};

了方便实现赋值(及其他一些惯用法),我们需要一个新的 swap 成员函数:

void swap(m_s_ptr& rhs)
{
	std::swap(_ptr, rhs._ptr);
	std::swap(_sc, rhs._sc);
}

赋值函数跟前面一样,保持不变,拷贝构造和移动构造函数是需要更新一下的:

	//赋值 
	m_s_ptr& operator=(m_s_ptr rhs)
	{
		m_s_ptr(rhs).swap(*this);
		return *this;
	}
	
	// 拷贝构造
	m_s_ptr(const m_s_ptr& other)
	{
		_ptr = other._ptr;
		if (_ptr) {
			other._sc->add();
			_sc = other._sc;
		}
	}
	
	// 拷贝构造
	template <typename V>
	m_s_ptr(const m_s_ptr<V>& other)
	{
		_ptr = other._ptr;  
		if (_ptr) {
			other._sc->add();
			_sc = other._sc;
		}
	}
	
	// 移动构造
	template <typename V>
	m_s_ptr(m_s_ptr<V>&& other)
	{
		_ptr = other._ptr;
		if (_ptr) {
			_sc = other._sc;
			other._ptr = nullptr;
		}
	}
/*
除复制指针之外,对于拷贝构造的情况,我们需要在指针非空时把引用数加一,并复制共享计数的指针。对于移动构造的情况,我们不需要调整引用数,直接把 other._ptr 置为空,认为 other 不再指向该共享对象即可。
*/

上面的代码有个问题:它不能正确编译。编译器会报错,像:

fatal error: ‘_ptr’ is a private member of ‘m_s_ptr’

错误原因是模板的各个实例间并不天然就有 friend 关系,因而不能互访私有成员 ptr_ 和 shared_count_。我们需要在 smart_ptr 的定义中显式声明:

friend class m_s_ptr;

加一个对调试非常有用的函数,返回引用计数值:

long get_count() const
{
	if (_ptr) {
		return _sc->get();
	}
	return 0;
}

测试

class Base
{
public:
	virtual ~Base(){}

};

class A : public Base 
{
public:
	~A() { std::cout << "~A" << std::endl; }
};

int main(int argc, char** argv) 
{
	m_s_ptr<A> p1(new A());
	std::cout << "p1:"<< p1.get_count() << std::endl;
	m_s_ptr<Base> p2;
	std::cout << "p2:"<< p2.get_count() << std::endl;
	p2 = p1;
	std::cout << "p2:"<< p2.get_count() << std::endl;
}
/*
output:
p1:1
p2:0
p2:2
~A
*/

指针类型转换

static_cast

reinterpret_cast

const_cast

dynamic_cast

智能指针需要实现类似的函数模板。实现本身并不复杂,但为了实现这些转换,我们需要添加构造函数,允许在对智能指针内部的指针对象赋值时,使用一个现有的智能指针的共享计数。

template <typename V>
m_s_ptr(const m_s_ptr<V>& other, T* ptr)
{
	_ptr = ptr;
	if (_ptr) {
		other._sc->add_count();
		_sc = other._sc;
	}
}

实现一个 dynamic_pointer_cast函数模板

template <typename T, typename V>
m_s_ptr<T> dynamic_pointer_cast(const m_s_ptr<V>& other)
{
	T* ptr = dynamic_cast<T*>(other.get());
	return m_s_ptr<T>(other, ptr);
}

测试:

m_s_ptr<A> p3 = dynamic_pointer_cast<A>(p2);
std::cout << "p3:"<< p3.get_count() << std::endl; //p3:3

C++ 提供的智能指针

头文件 #include <memory>

unique_ptr

仅有一个实例拥有内存所有权

class A
{
public: 
    A(){}
    A(int _x):x(_x){}
    friend ostream& operator<<(ostream& o, const A& a)
    {
        o << a.x << endl;
        return o;
    }
private:
    int x{0};
};

template <typename T, typename... Args>
inline unique_ptr<T> m_make_unique(Args&&... args)
{
    return unique_ptr<T>(new T(forward<Args>(args)...));   
}

int main()
{
    unique_ptr<A> p1{new A(1)};
    cout << *p1;                           
    unique_ptr<A> p2 = move(p1);
    cout << *p2;                           
    unique_ptr<A> p3 = m_make_unique<A>(2);
    cout << *p3;                            
    A* ap = new A(3);						
    unique_ptr<A> p4{ap};
    // 不要同一资源多次使用
    // unique_ptr<A> p5{ap};
    cout << *p4;
    return 0;
}

输出:

1
1
2
3
delete: 3
delete: 2
delete: 1

shared_ptr

实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。

通过复制构造函数或者赋值来共享内存

错误做法:8

A* a = new A(1);
shared_ptr<A> p1{a};
shared_ptr<A> p2{a};
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;

输出:

1
1
delete: 1
delete: 14490240

正确做法:

A* a = new A(1);
shared_ptr<A> p1{a};
shared_ptr<A> p2{p1};
shared_ptr<A> p3 = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
cout << p3.use_count() << endl;

输出:

3
3
3
delete: 1

weak_ptr

shared_ptr 可以实现多个对象共享同一块内存,当最后一个对象离开其作用域时,这块内存被释放。

对于循环引用,会出现内存无法被释放的情况。

shared_ptr 错误示例1:(互相引用)

class A
{
public: 
    A(){}
    A(int _x):x(_x){}
    ~A(){cout << "delete: " << x << endl;}
    friend void connect(shared_ptr<A>& lhs, shared_ptr<A>& rhs)
    {
        if(lhs && rhs)
        {
            lhs->other = rhs;
            rhs->other = lhs;
        }
    }
    friend ostream& operator<<(ostream& o, const A& a)
    {
        o << a.x << "--" << a.other->x << endl;
        return o;
    }
private:
    int x{0};
    shared_ptr<A> other;
};

template <typename T, typename... Args>
inline unique_ptr<T> m_make_unique(Args&&... args)
{
    return unique_ptr<T>(new T(forward<Args>(args)...));   
}

int main()
{
    shared_ptr<A> p1{new A(1)};
    shared_ptr<A> p2{new A(2)};
    connect(p1, p2);
    cout << *p1;
    cout << *p2;
    cout << p1.use_count() << endl;
    cout << p2.use_count() << endl;
    return 0;
}

输出:

1--2
2--1
2
2

shared_ptr 错误示例2:(自身引用)

shared_ptr<A> p1{new A(1)};
connect(p1, p1);
cout << *p1;
cout << p1.use_count() << endl;

输出:

1--1
2

使用shared_ptr ,对象没有被析构,出现内存泄露。

要引用计数变为0时才能析构,当你想析构p1时,p2内部却引用了p1,无法析构;反过来也无法析构。

互相引用造成了死锁,最终内存泄露。这样的情形也会出现在自锁中。

weak_ptr包含由shared_ptr所管理的内存的引用,weak_ptr不拥有这块内存,不计数,不阻止shared_ptr释放内存

通过lock()返回一个shared_ptr对象来访问这块内存。

class A
{
public: 
    A(){}
    A(int _x):x(_x){}
    ~A(){cout << "delete: " << x << endl;}
    friend void connect(shared_ptr<A>& lhs, shared_ptr<A>& rhs)
    {
        if(lhs && rhs)
        {
            lhs->other = rhs;
            rhs->other = lhs;
        }
    }
    friend ostream& operator<<(ostream& o, const A& a)
    {
        o << a.x << "--" << a.other.lock()->x << endl;
        return o;
    }
private:
    int x{0};
    weak_ptr<A> other;
};

int main()
{
    shared_ptr<A> p1{new A(1)};
    shared_ptr<A> p2{new A(2)};
    connect(p1, p2);
    cout << *p1;
    cout << *p2;
    cout << p1.use_count() << endl;
    cout << p2.use_count() << endl;
    return 0;
}

输出:

1--2
2--1
1
1
delete: 2
delete: 1

weak_ptr为弱引用,不用计数,可以解决循环引用的问题。

通过弱引用指针可以有效的解除循环引用的问题,但前提是要知道某个地方可能导致循环引用。

如果在程序运行时出现了循环引用,还是可能会导致内存泄漏。