智能指针的三种写法
茴字有四样写法,你知道么?
为了避免内存泄漏,C++11中提出了智能指针(参考开源库boost),智能指针不是一个指针,而是一个模板类。
其基本思想都是用引用计数的方式,用对象的生命周期控制内存的生命周期。智能指针有以下三种。
unique_ptr 独占对象
shared_ptr 允许多个shared_ptr 实例指向同一个对象
weak_ptr 是shared_ptr 的辅助类
下面将结合具体的例子来分析这三个智能指针。
unique_ptr
unique_ptr对象包装一个原始指针,并负责其生命周期,unique_ptr始终是关联的原始指针的唯一所有者,无法复制unique_ptr对象。
下面是一个unique_ptr的使用测试:
unique_ptr<int> ptr1;
unique_ptr<int> ptr2(new int(6));
//检查是否为空
if (ptr1 == nullptr) cout << "ptr1空" << endl;
if (ptr2 == nullptr) cout << "ptr2空" << endl;
//获取原始指针
int* orign = ptr2.get();
cout << *orign << endl;
//unique_ptr只能移动,不能复制
ptr1 = move(ptr2);
if (ptr1 == nullptr) cout << "ptr1空" << endl;
if (ptr2 == nullptr) cout << "ptr2空" << endl;
cout << *ptr1 << endl;
//释放裸指针
int* orign2 = ptr1.release();
cout << *orign << ' ' << *orign2 << endl;
if (ptr1 == nullptr) cout << "ptr1空" << endl;
//测试reset命令
auto ptr3 = make_unique<int>(7);
orign = ptr3.get();
cout << orign << endl;
ptr3.reset();
cout << orign << endl;
if (ptr3 == nullptr) cout << "ptr3空" << endl;
if (orign == nullptr) cout << "裸指针空" << endl;
cout << *orign << endl;
ptr3.reset(orign2);
cout << *ptr3 << endl;
下面是测试结果:
ptr1空
6
ptr2空
6
6 6
ptr1空
00D5BAF0
00D5BAF0
ptr3空
-572662307
6
上面测试了unique_ptr的两种初始化方式,移动方式,重置方式,且比较了releas()和reset()的区别,要注意到使用reset()时,原先获得的裸指针所指向内容已被释放,但是该指针仍然可以使用,可能引发错误。
shared_ptr
shared_ptr的核心是引用计数技术。在每个shared_ptr对象中,都有一个指向所管理对象的指针和一个整型计数器。shared_ptr在引用计数归零时,释放原有内存。shared_ptr支持赋值和等号运算符重载。
auto ptr1 = make_shared<int>(10);
cout << "ptr1值: " << *ptr1 << endl;
cout << "ptr1引用计数:" << ptr1.use_count() << endl;
//改变引用计数测试
auto ptr2 = ptr1; //可以直接赋值,与unique_ptr不同
cout << "ptr1引用计数:" << ptr1.use_count() << endl;
cout << "ptr2引用计数:" << ptr2.use_count() << endl;
ptr2.reset();
cout << "ptr1引用计数:" << ptr1.use_count() << endl;
cout << "ptr2引用计数:" << ptr2.use_count() << endl;
测试结果如下
ptr1值: 10
ptr1引用计数:1
ptr1引用计数:2
ptr2引用计数:2
ptr1引用计数:1
ptr2引用计数:0
自定义删除器
使用shared_ptr删除数组时,需要指定删除器。
//使用默认删除器
std::shared_ptr<int> ptr2(new int[10], std::default_delete<int[]>());
///使用lambda函数
std::shared_ptr<int> ptr1(new int[10], [](int* p){delete [] p;});
也可以自定义函数来实现。
需要注意,make_shared没有方法指定自己的删除器。
两种声明方式辨析
//智能指针的两种声明方式
auto ptr1 = make_shared<int>(new int(5));
shared_ptr<int> ptr2(new int(6));
使用make函数更好,相比于直接使用new表达式,make系列函数消除了重复代码(避免new两次),改进了异常安全性。
详细可以参考下面这篇博客:优先选择make_unique和make_shared,而非直接使用new
手撕智能指针
这里以shared_ptr的实现作为例子,在上述分析中,了解到智能指针的实现依赖类内封装的引用计数,在计数器归零时,执行析构函数释放内存。并且对智能指针的使用和普通指针相同。
这里我们实现一个简单的共现智能指针实现,需要完成以下函数
- 构造函数:有参,无参,左值,右值
- 运算符重载:* & =
- 获取裸指针和引用计数的函数
- 析构函数
函数模板定义如下:
template<typename T>
class mshared_ptr {
private:
T* _ptr;
int* _cnt;
public:
//构造和析构函数
mshared_ptr()
mshared_ptr(T* ptr)
mshared_ptr(const mshared_ptr& lsh)
mshared_ptr(mshared_ptr&& lsh)
~mshared_ptr()
//运算符重载
T* operator->()
T& operator*()
mshared_ptr<T>& operator=(const mshared_ptr<T>& other)
//获取引用计数
int count()
//获得裸指针
T* get()
};
share_unique实现
template<typename T>
class mshared_ptr {
private:
T* _ptr;
int* _cnt;
public:
//构造和析构函数
mshared_ptr() :_ptr(nullptr), _cnt(new int(0)) {
cout << "无参构造" << endl;
};
mshared_ptr(T* ptr) :_ptr(ptr), _cnt(new int(1)) {
cout << "含参构造" << endl;
};
mshared_ptr(const mshared_ptr& lsh):_ptr(lsh._ptr),_cnt(lsh._cnt){
if(_cnt) (*_cnt)++;
cout << "左值构造" << endl;
}; //左值引用
mshared_ptr(mshared_ptr&& lsh):_ptr(lsh._ptr), _cnt(lsh._cnt) {
lsh._ptr = nullptr;
lsh._cnt = nullptr;
cout << "右值构造" << endl;
}; //右值引用
~mshared_ptr() {
if (_cnt&& --(*_cnt) == 0) {
delete _ptr;
delete _cnt;
cout << "析构" << endl;
}
};
//运算符重载
T* operator->() { return _ptr; };
T& operator*() { return *_ptr; };
mshared_ptr<T>& operator=(const mshared_ptr<T>& other) {
if (this != &other) {
// 释放当前资源
if (_cnt && --(*_cnt) == 0) {
delete _ptr;
delete _cnt;
}
_ptr = other._ptr;
_cnt = other._cnt;
if (_cnt) (*_cnt)++;
}
return *this;
}
//获取引用计数
int count() {
return _cnt?*_cnt:0;
};
//获得裸指针
T* get() {
return _ptr;
};
};
对功能进行试验:
mshared_ptr<int> ptr1;
mshared_ptr<int> ptr2(new int(5));
cout << ptr1.count() << ' ' << ptr2.count() << endl;
mshared_ptr<int> ptr3(move(ptr2));
cout << ptr2.count() << ' ' << ptr3.count() << endl;
ptr1 = ptr3;
cout << ptr3.count() << ' ' << ptr1.count() << endl;
mshared_ptr<int> ptr4(ptr3);
int* ori = ptr4.get();
cout << *ori << ' ' << ptr4.count()<<endl;
得到结果为
无参构造
含参构造
0 1
右值构造
0 1
2 2
左值构造
5 3
析构
可以看到我们四种构造函数都能正常使用,在引用计数计算时也表现正常。因为左值构造和等号赋值都是对一个智能指针进行操作,程序结束时仅析构一次。
线程安全优化
对于shared_ptr而言:
- 同一个shared_ptr被多个线程读,是线程安全的
- 同一个shared_ptr被多个线程写,不是线程安全的
- 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的
可见,shared_ptr内部实现了线程安全,但是具体使用时还需要程序员注意。shared_ptr内部对引用计数的改变可能导致线程不安全,现在来对其进行优化。
参考C++的多线程安全,我们可以有以下两种操作: - 使用atomic原子操作,声明引用计数为 atomic<int>。
- 使用互斥量进行lock操作,或者使用lock_guard 自动加锁、解锁