智能指针之shared_ptr与weak_ptr
- 前言
- 智能指针
- 实例分析
前言
C++与其他语言的不同点之一就是可以直接操作内存,这是一把双刃剑
,直接操作内存可以提高开发的灵活度,开发人员在合适的时机申请内存,在合适的时机释放
内存,减少冗余内存的占用,听起来非常不错。然而, 实际情况是申请了内存,忘记了释放,导致内存泄漏
;又或者是,申请了内存,在某些情况下被释放了,然而另一部分代码却在继续使用这块内存,导致访问了非法内存
,程序崩溃。当然,大部分的内存泄漏与访问非法内存导致的程序崩溃在debug版本中都是可以被发现的,但是还是会存在一些比较隐秘的角落,在测试期间发现不到,导致用户在试用期间发生崩溃,这是最糟的情况。
智能指针
C++11的新特性之一就是增加了三组智能指针,shared_ptr
,weak_ptr
,unique_ptr
,通过合理的使用者三组智能指针,可以极大的避免开发过程中有关内存的困扰。今天我们只介绍前两个。
shard_ptr
是可以对引用进行计数的,当有其他地方引用该指针时,引用计数加1,当有地方释放该指针时,引用计数减一,只有当引用计数未0时,这个指针才会被真正的析构。
weak_ptr
不会增加引用计数,它一般是与shard_ptr
搭配使用,使用shared_ptr
来构造weak_ptr
,weak_ptr
调用lock()方法,如果该shared_ptr
已经被析构了,则返回null,如果还没被析构,则返回一个shared_ptr
,并且该shared_ptr
的引用计数加1。
引用C++的经典书籍 **《Effective C++》**第三版中的一段话来感受下shared_ptr
的优点
实际上,返回shard_ptr让接口设计者得以阻止一大群客户犯下资源泄漏的错误,因为就如条款14所言,shard_ptr允许当前智能指针被建立起来时指定一个资源释放函数(所谓删除其,“deleter”)绑定与智能指针身上。
shared_ptr有一个特别好的性质是:它会自动使用它的"每个指针专属的删除器",因而消除另一个潜在的客户错误:所谓的 “cross-DLL problem”。这个问题发生于“对象在动态链接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁“。在许多平台上,这一类“跨DLL之new/delete成对运用”会导致运行期错误。 shared_ptr没有这个问题,因为它缺省的删除器是来自"shared_ptr诞生的那个DLL"的delete。
在C++开发中,遇到的问题越多,越会觉得**《Effective C++》第三版**真是一本好书,全书55个条款,分为
- 让自己习惯C++
- 构造/析构/赋值运算
- 资源管理
- 设计与声明
- 实现
- 继承与面向对象设计
- 模板与泛型编程
- 定值new与delete
- 杂项讨论
这九大部分,每一项条款都直击问题要害,给出防范措施,搭配**《C++ Primer》**阅读,C++程序员必备!
网上买**《Effective C++》加一本《C++ Primer》大概要100块钱,如果有需要的同学可以关注公众号程序员DeRozan**,回复1207直接领取。
下面来通过一个实例来感受下智能指针。
实例分析
下面我们通过分析一个例子,来大概感受下智能指针的好处。
一个服务,会接收一个cmd以及其携带的参数,并将其存入一个任务队列,交给一个任务线程去处理。
worker是一个对象,其有一个成员函数可以处理这个cmd。
函数asyncTaskHandler将这个cmd的handler函数以及参数封装到一个结构体中,然后放入队列m_taskQue中,会有任务线程来取任务并执行。
生产者
struct AICTask_t {int cmd;int task_id;taskStatus status;BaseWorker* worker;arg_t* arg;
};int AICTaskManager::asyncTaskHandler(int cmd, std::string &reqMsg, int reqMsgLen, std::string &respMsg, int &respMsgLen)
{TdInfo("AICTaskManager::asyncTaskHandler");TdInfo("create task");AICTask_t task;auto iter = m_workerMap.find(cmd); //m_workerMap : std::map<int, BaseWorker*> if(iter != m_workerMap.end()) {task->worker = iter->second;task->arg.cmd = cmd;task->arg.reqMsg = reqMsg;task->arg.reqMsgLen = reqMsgLen;task->arg.respMsg = respMsg;task->arg.respMsgLen = respMsgLen;task->status = prepare_to_start;}else{TdError("not found this cmd");return -1;}pthread_mutex_lock(&m_workerLock);while (m_taskQue.size() == m_maxQueSize && !m_stop){pthread_cond_wait(&m_cond_not_full, &m_workerLock);}m_taskQue.push(&task); //std::queue<AICTask_t*> m_taskQue;m_taskMap[m_task_id] = task;m_task_id++;pthread_mutex_unlock(&m_workerLock);pthread_cond_broadcast(&m_cond_not_empty);TdInfo("insert a task to queue");return 0;
}
aicThreadHandle是任务线程的线程入口函数,负责将handler以及参数拿到,并执行。
消费者
void *AICTaskManager::aicThreadHandle(void *arg)
{if(!arg) {TdError("bad parameter");return nullptr;}AICTaskManager *taskManager = (AICTaskManager *)arg;pthread_mutex_lock(&taskManager->m_workerLock);//loop for get task,every get lock, can get task while(true){// if m_taskQue not empty, get task, if empty, wait to get task// when task queue not empty, only one creater will wake up condition.while (taskManager->m_taskQue.empty() && !taskManager->m_stop){TdDebug("waiting to wake up");pthread_cond_wait(&taskManager->m_cond_not_empty, &taskManager->m_workerLock);// m_cond was waked up and get lock}//step while, now m_taskQue not emptyif (taskManager->m_stop){pthread_mutex_unlock(&taskManager->m_workerLock);TdInfo("thread % exit", pthread_self());pthread_exit(NULL);}AICTask_t* task = taskManager->m_taskQue.front();taskManager->m_taskQue.pop();pthread_mutex_unlock(&taskManager->m_workerLock);pthread_cond_broadcast(&taskManager->m_cond_not_full);TdInfo("exec function");task->status = running;(task->worker->handleCommand)(task->arg->cmd, task->arg->reqMsg, task->arg->reqMsgLen, task->arg->respMsg, task->arg->respMsgLen);task->status = over;}return nullptr;
}
上述代码的逻辑很简单,简单的 生产者消费者模式
,动态申请的内存全部使用的裸指针
来保存,而且这个代码中有一个很明显的错误。
生产者代码的第13行,AICTask_t task是在栈上申请的变量,存入std::queue<AICTask_t*> m_taskQue, m_taskQue内保存的是指针类型,所以在第34行通过取地址符传入了一个指针指向栈
上的task,结果程序在消费者的第41行崩溃了。
为什么会崩溃呢,原因就是生产者把指向task的指针放入任务队列后,函数就执行结束了,task是栈
上的变量,随着程序结束就被释放了,然而,m_taskQue还保存着指向task的指针,指向了一块未初始化
的内存。在任务处理函数中,这个指针被访问了,程序就崩溃了。
为什么不在堆上申请task呢,因为我觉得在堆上申请还需要手动释放
,容易出问题,所以想偷个懒,直接在栈上申请了,没有考虑到上面说的问题。
消费者的代码中,对于裸指针都是直接访问的,也没有检查
是不是已经被释放了。
总之,手动管理内存实在太容易出问题了,所以,使用智能指针
来管理内存,是很不错的选择。
将上面的代码进行改造,将需要用到指针的地方改为shared_ptr
,并使用weak_ptr
来检查智能指针已经被析构。
改造过后:
生产者
int AICTaskManager::asyncTaskHandler(int cmd, std::string &reqMsg, int reqMsgLen, std::string &respMsg, int &respMsgLen)
{TdInfo("AICTaskManager::asyncTaskHandler");TdInfo("create task");std::shared_ptr<AICTask_t> task = std::make_shared<AICTask_t>();auto iter = m_workerMap.find(cmd);if(iter != m_workerMap.end()) {task->worker = iter->second;task->arg.cmd = cmd;task->arg.reqMsg = reqMsg;task->arg.reqMsgLen = reqMsgLen;task->arg.respMsg = respMsg;task->arg.respMsgLen = respMsgLen;task->status = prepare_to_start;}else{TdError("not found this cmd");return -1;}pthread_mutex_lock(&m_workerLock);while (m_taskQue.size() == m_maxQueSize && !m_stop){pthread_cond_wait(&m_cond_not_full, &m_workerLock);}m_taskQue.push(task);m_taskMap[m_task_id] = task;m_task_id++;pthread_mutex_unlock(&m_workerLock);pthread_cond_broadcast(&m_cond_not_empty);TdInfo("insert a task to queue");return 0;
}
消费者
void *AICTaskManager::aicThreadHandle(void *arg)
{if(!arg) {TdError("bad parameter");return nullptr;}AICTaskManager *taskManager = (AICTaskManager *)arg;pthread_mutex_lock(&taskManager->m_workerLock);//loop for get task,every get lock, can get task while(true){// if m_taskQue not empty, get task, if empty, wait to get task// when task queue not empty, only one creater will wake up condition.while (taskManager->m_taskQue.empty() && !taskManager->m_stop){TdDebug("waiting to wake up");pthread_cond_wait(&taskManager->m_cond_not_empty, &taskManager->m_workerLock);// m_cond was waked up and get lock}//step while, now m_taskQue not emptyif (taskManager->m_stop){pthread_mutex_unlock(&taskManager->m_workerLock);TdInfo("thread % exit", pthread_self());pthread_exit(NULL);}// std::weak_ptr<AICTask_t> task = taskManager->m_taskQue.front();std::shared_ptr<AICTask_t> task = std::weak_ptr<AICTask_t>(taskManager->m_taskQue.front()).lock();if(!task){TdError("expired task");return nullptr;}taskManager->m_taskQue.pop();pthread_mutex_unlock(&taskManager->m_workerLock);pthread_cond_broadcast(&taskManager->m_cond_not_full);TdInfo("exec function");task->status = running;std::shared_ptr<BaseWorker> worker = std::weak_ptr<BaseWorker>(task->worker).lock();if (!worker){TdError("worker expired");return nullptr;}(worker->handleCommand)(task->arg.cmd, task->arg.reqMsg, task->arg.reqMsgLen, task->arg.respMsg, task->arg.respMsgLen);task->status = over;}return nullptr;
}
这样,我们在使用shared_ptr
前,先通过weak_ptr
判断是够可用,然后使用完成后,该指针会自动将引用计数减一,等到引用计数未0,也就是没有任何地方再引用它,就可以释放它指向的那块内存了。
不过需要注意的是,shard_ptr
一定要用make_shared
来构造,虽然有时携程shared_ptr<T>(new T())
也能通过编译,但是这是会导致内存泄漏的。
智能指针的用法非常多,本文讲的是最常见的一种情况,其他更复杂的用法,则需要继续学习与发现。