智能指针
简述
智能指针,可以简化资源的管理,从根本上消除资源(包括内存)泄漏的可能性。
智能指针本质上是 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
析构时,它需要删除对象和共享计数。
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为弱引用,不用计数,可以解决循环引用的问题。
通过弱引用指针可以有效的解除循环引用的问题,但前提是要知道某个地方可能导致循环引用。
如果在程序运行时出现了循环引用,还是可能会导致内存泄漏。
- 本文链接:https://morisa66.github.io/2021/03/01/Ptr/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。