智能指针原理与实战解析
智能指针是C++中用于自动管理动态内存的重要工具,通过RAII(资源获取即初始化)技术确保资源在对象生命周期结束时自动释放。传统指针在异常发生时容易导致内存泄漏,而智能指针能有效解决这一问题。C++提供了多种智能指针:unique_ptr(独占所有权,禁止拷贝)、shared_ptr(共享所有权,引用计数)、weak_ptr(解决循环引用问题)。auto_ptr由于设计缺陷已被弃用。智能指针通过重
目录
1.为什么需要智能指针?
下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析MergeSort 函数中的问题。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
问题分析:上面的问题分析出来我们发现有什么问题?
我们调用Func函数的时候,给p1和p2指针申请了资源,接下来在调用div函数的时候,如果new或div函数抛了异常,那么执行流会直接跳转至main函数的catch,就不会执行下面的两个delete了,这就导致了资源泄露!
2. 内存泄漏
2.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。
比如
void function() {
int* ptr = new int[100]; // 分配了100个int的内存
// ... 使用这块内存
// 函数结束时,ptr被销毁,但它指向的100个int的内存没有被释放
// 这就是内存泄漏
}
函数结束后,ptr指针被销毁,也就是无法使用这块内存了,但是没有释放,导致这个内存一直在你这里但是你又不用,因此是浪费内存了!
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
2.2 内存泄漏分类(了解)
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分 内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
2.3如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下: 内存泄漏非常常见,解决方案分为两种:
1、事前预防型。如智能指针等。
2、事后查错型。如泄 漏检测工具。
3.智能指针的使用及原理
3.1 RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:
(1)不需要显式地释放资源。
(2)采用这种方式,对象所需的资源在其生命期内始终保持有效。
接下来我们就采用RALL思想来解决上面异常安全的问题:
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
ShardPtr<int> sp1(new int);
ShardPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch(const exception& e)
{
cout<<e.what()<<endl;
}
return 0;
}
3.2 智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可 以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10
cout<<*sp1<<endl;
SmartPtr<int> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
总结一下智能指针的原理:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。
3.3 std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理。
// C++98 管理权转移 auto_ptr
namespace bit
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
auto_ptr的主要问题包括:
- 它只能管理单个动态创建的对象,而不能管理动态创建的数组(因为底层析构没有用delete[])
- 它的拷贝行为通过移动语义实现,即将内存所有权从一个指针转移到另一个指针,这种机制在STL容器中无法正常工作,因为STL容器的设计基于值语义,即容器中的元素应该能够被安全地拷贝而不改变原始对象的状态。当我们将元素放入容器时,容器会创建元素的副本,而原始对象保持不变。然而,auto_ptr的拷贝行为会转移内存所有权并将原指针设为NULL,这破坏了值语义的基本原则
结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。
3.4 std::unique_ptr
C++11中开始提供更靠谱的unique_ptr。
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理:
// C++11库才更新智能指针实现
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace bit
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
unique_ptr与scoped_ptr类似,unique_ptr是C++11引入的智能指针,它持有对对象的独有权,意味着同一时间只能有一个unique_ptr指向同一个对象。它不能进行复制构造或复制赋值操作,但支持移动构造和移动赋值,这种设计使得unique_ptr能够安全地用于STL容器中;
scoped_ptr则是一个类似auto_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。与unique_ptr相比,scoped_ptr的所有权更加严格,不支持自增、自减操作,并且不能被赋值或者复制构造(包括移动)
两者的主要区别在于:
- 可移动性:unique_ptr支持移动语义,可以作为STL容器的元素;而scoped_ptr不能移动,因此不能作为容器的元素。
- 所有权的严格程度:scoped_ptr的所有权比auto_ptr更严格,而unique_ptr则提供了更现代和灵活的内存管理方式
- 功能丰富度:uniqueptr提供了更多功能,如get()、release()、reset()等方法,以及支持自定义删除器的能力。scopedptr的功能相对简单,主要专注于自动释放资源
-
灵活性:uniqueptr可以管理单个对象,也可以管理数组(通过特殊的语法),而scopedptr通常只能管理单个对象。
下面提供一个unique_ptr作为容器元素进行移动拷贝的代码:
#include <iostream>
#include <vector>
#include <memory>
#include <utility>
class Resource {
public:
int id;
Resource(int i) : id(i) {
std::cout << "Resource " << id << " constructed\n";
}
~Resource() {
std::cout << "Resource " << id << " destroyed\n";
}
};
int main() {
// 创建vector存储unique_ptr
std::vector<std::unique_ptr<Resource>> resources;
// 添加元素(使用移动语义)
resources.push_back(std::make_unique<Resource>(1));
resources.push_back(std::make_unique<Resource>(2));
resources.push_back(std::make_unique<Resource>(3));
std::cout << "\nOriginal vector size: " << resources.size() << "\n";
// 创建另一个vector
std::vector<std::unique_ptr<Resource>> other_resources;
// 从原vector移动元素到新vector
// 使用std::move转移所有权
other_resources = std::move(resources);
std::cout << "Original vector size after move: " << resources.size() << "\n";
std::cout << "New vector size after move: " << other_resources.size() << "\n";
// 访问移动后的元素
for (const auto& ptr : other_resources) {
std::cout << "Resource ID: " << ptr->id << "\n";
}
return 0;
}
补充:unique_ptr独占资源,禁止多个多个对象管理同一资源
int* raw = new int(42);
unique_ptr<int> p1(raw); // 正确,引用计数为1(虽然unique_ptr没有引用计数)
unique_ptr<int> p2(raw); // 编译错误!unique_ptr禁止多个对象管理同一资源
3.5 std::shared_ptr、weak_ptr
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如: 老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共 享。 2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。 3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。
std::shared_ptr的线程安全问题
1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错 乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁 的,也就是说引用计数的操作是线程安全的。
2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
std::shared_ptr的循环引用
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
循环引用分析:
1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动 delete。
2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
3. node1和node2析构,引用计数减到1,因此不会释放节点资源,所以_next还指向下一个节点,_prev还指向上 一个节点。
4.也就是说,想释放node1的资源,就要先释放node2的资源,要想释放node2,就得先释放node1才行,陷入了死胡同,所以这就是循环引用,谁也无法释放谁!
总结:循环引用就是至少有两个shared_ptr保护的对象,每个对象内部还各自有shared_ptr互相指向对方;这样即使外部的共享指针析构了,内部的的共享指针也不会释放保护的资源。
解决方案:在引用计数的场景下,把节点中的_prev或者_next改成weak_ptr就可以了;
介绍一下weak_ptr:
weak_ptr是一种特殊的(弱引用)智能指针,它指向一个由shared_ptr(强引用)管理的对象,但不会增加该对象的引用计数。它的主要特点包括:
-
不控制对象生命周期:weak_ptr不控制所指向对象的生存期,它只是观察对象是否存在。
-
不影响引用计数:与shared_ptr不同,weak_ptr的构造和析构不会引起引用计数的增加或减少。
-
主要用途:weak_ptr主要用于解决循环引用问题,避免内存泄漏。当多个shared_ptr互相引用形成循环时,使用weak_ptr可以打破这个循环,确保对象能够被正确释放。
-
使用限制:weak_ptr不能直接通过*或->操作符访问对象,必须先通过lock()方法转换为shared_ptr后才能使用,转换后也不会增加引用计数!
-
weak_ptr更准确的描述应该是:一种观察型智能指针,它允许你访问shared_ptr管理的对象,但不参与对象的所有权管理,主要用于解决循环引用问题。
因此,解决循环引用的原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。
删除器:
如果不是new出来的对象,或者是用new动态创建的数组,如何通过智能指针管理呢?其实shared_ptr和unique_ptr设计了一个删除器来解决这个问题
对于栈上对象、全局或静态对象,可以使用std::shared_ptr结合一个空删除器,这样当shared_ptr析构时不会尝试delete对象:
#include <iostream>
#include <memory>
#include <vector>
// 自定义删除器,不执行删除操作
struct NoOpDeleter {
template<typename T>
void operator()(T* ptr) const {
// 不删除对象
}
};
// 处理动态数组的删除器
struct ArrayDeleter {
template<typename T>
void operator()(T* ptr) const {
delete[] ptr; // 使用delete[]释放数组
}
};
int main() {
// 1. 管理栈上对象
int stackInt = 42;
std::shared_ptr<int> ptr1(&stackInt, NoOpDeleter());
std::cout << "栈上对象值: " << *ptr1 << std::endl;
// 2. 管理静态对象
static int staticInt = 100;
std::shared_ptr<int> ptr2(&staticInt, NoOpDeleter());
std::cout << "静态对象值: " << *ptr2 << std::endl;
// 3. 管理容器中的对象
std::vector<int> vec = {1, 2, 3, 4, 5};
std::shared_ptr<std::vector<int>> ptr3(&vec, NoOpDeleter());
std::cout << "容器大小: " << ptr3->size() << std::endl;
// 4. 管理函数返回的对象
auto getObject = []() -> std::string {
return "Hello from function";
};
std::string obj = getObject();
std::shared_ptr<std::string> ptr4(&obj, NoOpDeleter());
std::cout << "函数返回对象: " << *ptr4 << std::endl;
// 5. 管理动态创建的数组
int* dynamicArray = new int[5]{10, 20, 30, 40, 50};
std::shared_ptr<int> ptr5(dynamicArray, ArrayDeleter());
std::cout << "动态数组第一个元素: " << ptr5[0] << std::endl;
// 6. 使用unique_ptr管理动态数组
std::unique_ptr<int[], ArrayDeleter> ptr6(new int[3]{100, 200, 300});
std::cout << "unique_ptr管理的数组第一个元素: " << ptr6[0] << std::endl;
return 0;
}
#include <iostream>
#include <memory>
#include <vector>
// 自定义删除器,不执行删除操作
struct NoOpDeleter {
template<typename T>
void operator()(T* ptr) const {
// 不删除对象,仅用于管理非 new 对象
}
};
int main() {
// 1. 管理栈上对象
int stackInt = 42;
std::unique_ptr<int, NoOpDeleter> ptr(&stackInt);
std::cout << "栈上对象值: " << *ptr << std::endl;
// 2. 管理静态对象
static int staticInt = 100;
std::unique_ptr<int, NoOpDeleter> ptr2(&staticInt);
std::cout << "静态对象值: " << *ptr2 << std::endl;
// 3. 管理容器中的对象
std::vector<int> vec = {10, 20, 30};
std::unique_ptr<std::vector<int>, NoOpDeleter> ptr3(&vec);
std::cout << "容器大小: " << ptr3->size() << std::endl;
return 0;
}
关键区别在于:unique_ptr的删除器是类型的一部分(构造参数可给可不给),而shared_ptr的删除器是实例化的一个参数(模版参数也能给)。这使得unique_ptr的删除器更高效(编译时确定),而shared_ptr更灵活(运行时可配置)。
实际上,shared_ptr的代码有两种版本:
template <typename T>
class shared_ptr {
// ...其他成员...
std::function<void(T*)> deleter;
};
// 特化版本 - 为特定删除器优化
template <typename T, typename Deleter>
class shared_ptr<T, Deleter> {
// ...特化的实现...
Deleter deleter; // 直接存储删除器类型,不是std::function
};
一种是用包装器包装了删除器,将其作为构造函数的参数传递;另一种是和unique一样的通过模版参数传递删除器,删除器操作上和unique_ptr的一样;
make_unique和make_shared:
两者都是用来创建各自智能指针的临时对象的,但各自的适用场景不同:
1.make_unique的用途
直接使用new创建对象并赋值给uniqueptr时,如果在构造函数抛出异常,可能会导致内存泄漏。make_unique可以确保异常安全。这一点make_shared也适用。
unique对象不支持拷贝,当作为stl容器的元素需要进行拷贝时,应当用移动构造,这个时候就需要unique对象得是右值了,而make_unique创建的临时对象就是右值!
2.shared_unique的用途
shared_ptr禁止用同一个指针进行初始化,因为会出现下面这种情况:
int* raw = new int(42);
sharedptr<int> p1(raw); // 引用计数 = 1
sharedptr<int> p2(raw); // 引用计数 = 1(而不是2!)
这种情况会导致double free,因为两个sharedptr对象各自维护自己的引用计数,都认为自己是资源的唯一所有者。
标准库提供std::make_shared
函数来创建对象,避免手动管理指针导致的double free问题。
auto p1 = std::make_shared<int>(42);
auto p3 = std::make_shared<int>(42);
auto p2 = p1; // 引用计数 = 2
注意:make_shared创建出一个临时的shared对象,赋给p1会调用移动构造,引用计数不会变化
这种方式可以让p1和p3指向不同的资源,因为make_shared底层是包含new的,这等于是new了两片空间;
另外补充make_unique和make_shared的创建数组的方式:
// 创建int数组,大小为10
auto arr = std::make_unique<int[]>(10);
// 访问数组元素
arr[0] = 42;
当指定类型为int[]后,底层就默认有delete[]的删除器了;
// C++20之前的方式
auto arr = std::shared_ptr<int>(new int[10], std::default_delete<int[]>());
// C++20的方式
auto arr = std::make_shared<int[]>(10);
总之,给智能指针初始化尽量用make_shared和make_unique!
4.unique_ptr与shared_ptr/weak_ptr的模拟实现
4.1 unique_ptr的模拟实现
#pragma once
#include<iostream>
using namespace std;
template<class T>
struct deleter
{
void operator()(T* node)
{
if (node)
{
delete node;
}
}
};
namespace myptr
{
template<class T,class Deleter = deleter<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr,Deleter deleter = Deleter())
:_ptr(ptr),
_deleter(deleter)
{}
unique_ptr(unique_ptr&& uptr)
:_ptr(uptr._ptr),
_deleter(std::move(uptr._deleter))
{
}
unique_ptr& operator=(unique_ptr&& uptr)
{
if (this != &uptr)
{
release();
swap(_ptr, uptr._ptr);
swap(_deleter, uptr._deleter);
}
return *this;
}
unique_ptr(const unique_ptr& uptr) = delete;
unique_ptr& operator=(const unique_ptr& uptr) = delete;
~unique_ptr()
{
release();
}
void release()
{
if (_ptr)
{
_deleter(_ptr);
}
}
T* get()const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
Deleter _deleter;
};
}
思路讲解:
首先,智能指针的内部要有自己的默认删除器,通常都是像我们写的deleter仿函数对象,采用delete的方式释放资源;
unique_ptr的删除器应该放在模版参数中,编译时确定删除行为,构造参数中可给可不给。
因此我们的代码中用class Deleter = deleter<T>来表示删除器类型,默认类型是我们内部实现的删除器类型,并且在底层维护了要保护的指针和删除器对象,
在构造函数中,Deleter deleter = Deleter()表示删除器可以作为参数传递,但默认值是模版参数中的删除器类型的空对象,然后通过移动构造给底层的_deleter;也就是说,只要我们在模版中传递了删除器,那么在构造时就会默认给我们底层的删除器提供模版删除器对象,我们就不需要在外部提供删除器对象了,这与标准库的unique_ptr的行为一致;
接下来,我们禁止了拷贝构造和赋值重载函数,并实现移动构造和移动赋值,在这两个函数中我们采用的移动方案不同,在移动构造中,我们采用直接赋值的方式,但是对于_deleter我们采用的是移动构造,原因是uptr本身就是右值,那么我们应该将其内部资源全部抢过来,而不是拷贝,因此我们将uptr的删除器转换为右值就可以依靠删除器对象的移动构造来移动资源了,符合移动语义的思想!还可以采用swap交换的思想,这个方法用在移动赋值中了,只不过移动赋值要注意先把自己的资源释放掉;
剩下的接口就简单了,release接口是清理资源,直接调用删除器即可,然后是得到原生指针的get接口,模拟指针行为的*和->重载;
4.2 shared_ptr、weak_ptr的模拟实现:
#pragma once
#include<iostream>
#include<functional>
using namespace std;
template<class T>
struct default_deleter
{
void operator()(T* node)
{
if (node)
{
delete node;
}
}
};
namespace myptr
{
template<class T>
class shared_ptr
{
public:
using func = function<void(T*)>;
shared_ptr(T* ptr = nullptr, func deleter = default_deleter<T>())
:_ptr(ptr),
_deleter(deleter),
_cnt(new int(1))
{}
shared_ptr(shared_ptr&& uptr)
:_ptr(uptr._ptr),
_deleter(std::move(uptr._deleter)),
_cnt(uptr._cnt)
{
uptr._ptr = nullptr;
}
shared_ptr& operator=(shared_ptr&& uptr)
{
if (this != &uptr)
{
release();
swap(_ptr, uptr._ptr);
swap(_deleter, uptr._deleter);
swap(_cnt, uptr._cnt);
}
return *this;
}
shared_ptr(const shared_ptr& uptr)
:_ptr(uptr._ptr),
_deleter(uptr._deleter),
_cnt(uptr._cnt)
{
*(_cnt)++;
}
shared_ptr& operator=(const shared_ptr& uptr)
{
if (this != &uptr)
{
release();
_ptr = uptr._ptr;
_cnt = uptr._cnt;
_deleter = uptr._deleter;
*(_cnt)++;
}
return *this;
}
~shared_ptr()
{
release();
}
void release()
{
if (_cnt && --(*_cnt)==0 && _ptr)
{
_deleter(_ptr);
}
}
T* get()const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _cnt;
func _deleter;
};
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sptr)
:_ptr(sptr.get())
{
}
weak_ptr& operator=(const shared_ptr<T>& sptr)
{
_ptr = sptr.get();
return *this;
}
private:
T* _ptr = nullptr;
};
}
我们实现的是通用版本的shared_ptr,即删除器是作为构造参数传递的,与unique的模版删除器方案不同,用户只能在外部提供删除器,如果不提供就默认采用我们内部实现的删除器,因为用户提供的删除器对象可能是各种类型的,所以我们构造函数的形参应该用包装器function定义的对象来统一接收就好了;
shared_ptr底层不仅维护了指针和删除器,还有引用计数cnt,但是你会发现我们的引用计数是用int*定义的,这里解答3个疑问:
1.为什么不用static int cnt来表示引用计数?
这种方式确实也可以让每个shared对象都看到引用计数,且也能构造时++和析构--,但却有忽略了一个问题:引用计数应该是指向同一资源的shared对象所维护的,也就是说指向不同资源的shared对象的引用计数应该是不同的,如果用static int表示,那么所有的shared对象的引用计数就都一样了!
2.为什么不能用int表示引用计数?
引用计数应该是指向同一资源的shared对象所维护的,也就是说这些shared对象应该能同时看到引用计数的变化,如果采用int,某一个对象让引用计数改变时,别的shared对象是看不到的。
3.为什么采用int*?
如果采用int*,当构造shared对象时,就new int(1),拷贝时,用的是浅拷贝,即每个shared的内部指针指向同一个空间,且引用计数的指针的指向也是相同的,这就实现了多个shared对象共享引用计数了!当一个shared对象改变引用计数,其他指向同一资源的对象也能看到!
release接口是用来释放的,先检查一下引用计数这次-1后会不会等于0,如果等于0就用删除器释放资源;
接下来是weak_ptr,它的功能比较单一,主要是用来指向shared_ptr对象指向的内容的。
注意我们的shared_ptr还是有缺陷的,比如没有处理同一资源同时给多个shared对象初始化应该报错的问题。
5.C++11和boost中智能指针的关系
1. C++ 98 中产生了第一个智能指针auto_ptr.
2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost 的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
更多推荐
所有评论(0)