本文没有翻译版权,不选不让发布。如涉及侵权,请联系作者删除(5207798@qq.com)。
Part 1: shared_ptr Tutorial and Examples
原文链接:https://thispointer.com/learning-shared_ptr-part-1-usage-details/
这篇文章中,我们讨论C++11中的智能指针shared_ptr的用法。
什么是std::shared_ptr<> ?
shared_ptr是C++11提供的智能指针之一,它能在没有任何引用的时候自动释放持有的裸指针。因此能帮助开发人员避免内存泄露和悬空指针问题。
shared_ptr和共享所有权
多个shared_ptr共享一个底层裸指针的所有权,是通过引用计数的方式实现的。
每一个shared_ptr对象内部有两个指针
1)一个指向持有的对象;
2)另外一个指向引用计数数据;
共享所有权是如何通过引用计数实现的?
- 当一个新的shared_ptr对象持有(关联)一个裸指的时候,这个shared_ptr的构造函数增加裸指针的引用计数+1。
- 当一个shared_ptr对象超出作用域,它的析构函数将裸指针的引用计数-1。如果引用计数变为0,说明没有任何shared_ptr对象持有该(关联)这个裸指针了,将会使用deleted释放裸指针指向的内存。
创建一个shared_ptr对象
将一个shared_ptr对象和裸指针关联:
std::shared_ptr<int> p1(new int());
上边这行代码会在堆上分配两块内存:
1)一块内存用于保存int
2)一块内存用于保存引用计数,初始值为1
检查一个shared_ptr对象的引用计数
可以使用如下方法获取shared_prt对象的引用计数:
p1.use_count();
std::make_shared
如何把一个裸指针和一个shared_ptr关联起来?
std::shared_ptr<int> p1 = new int(); // Compile error
因为shared_ptr的构造函数使用了Explicit形式的参数(不支持隐式的转换数据类型),上边的代码需要使用隐式数据类型转换的构造函数。所以会发生编译错误。
Part 2: shared_ptr and Custom Deletor
原文链接:https://thispointer.com/shared_ptr-and-custom-deletor/
这篇文章讨论如何在std::shared_ptr中使用自对应的deleter。
当一个shared_ptr对象超出作用域以后,它的析构函数会将引用计数减1,如果引用计数变为0,将会delete关联的裸指针。
默认的,就是调用delete删除指针
delete Pointer;
但是,有时候我们不想用默认的delete函数删除指针,比如:
一个shared_ptr对象关联的裸指针是指向数组的而不是指向单一对象的
std::shared_ptr<int> p3(new int[12]);
shared_ptr在析构的时候,仍然会调用delete方法。然而,正确的释放指针方式是:
delete []
使用自定义的deleter
可以在shared_ptr对象的构造函数中传入一个回调函数,然后在shared_ptr对象析构的时候, 如果需要释放对象,调用这个回调函数来回收内存。
使用函数指针来定义deleter
// function that calls the delete [] on received pointer
void deleter(Sample * x)
{
std::cout << "DELETER FUNCTION CALLED\n";
delete[] x;
}
在shared_ptr对象构造函数中传入这个函数指针
// Creating a shared+ptr with custom deleter
std::shared_ptr<Sample> p3(new Sample[12], deleter);
整个例子代码如下:
#include <iostream>
#include <memory>
struct Sample
{
Sample()
{
std::cout << "CONSTRUCTOR\n";
}
~Sample()
{
std::cout << "DESTRUCTOR\n";
}
};
// function that calls the delete [] on received pointer
void deleter(Sample * x)
{
std::cout << "DELETER FUNCTION CALLED\n";
delete[] x;
}
int main()
{
// Creating a shared+ptr with custom deleter
std::shared_ptr<Sample> p3(new Sample[12], deleter);
return 0;
}
输出
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
CONSTRUCTOR
DELETER FUNCTION CALLED
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
DESTRUCTOR
使用lambda表达式或者函数对象定义deleter
shared_ptr构造函数中的deleter还可以传入lambda表达式或者函数对象。
class Deleter
{
public:
void operator() (Sample * x) {
std::cout<<"DELETER FUNCTION CALLED\n";
delete[] x;
}
};
// Function Object as deleter
std::shared_ptr<Sample> p3(new Sample[12], Deleter());
// Lambda function as deleter
std::shared_ptr<Sample> p4(new Sample[12], [](Sample * x){
std::cout<<"DELETER FUNCTION CALLED\n";
delete[] x;
});
还有些情况,资源释放的方式不是delete
下边是一个内存池使用shared_ptr的例子:
#include <iostream>
#include <memory>
struct Sample
{
};
// Memory Pool Dummy Kind of Implementation
template<typename T>
class MemoryPool
{
public:
T * AquireMemory()
{
std::cout << "AQUIRING MEMORY\n";
return (new T());
}
void ReleaseMemory(T * ptr)
{
std::cout << "RELEASE MEMORY\n";
delete ptr;
}
};
int main()
{
std::shared_ptr<MemoryPool<Sample> > memoryPoolPtr = std::make_shared<
MemoryPool<Sample> >();
std::shared_ptr<Sample> p3(memoryPoolPtr->AquireMemory(),
std::bind(&MemoryPool<Sample>::ReleaseMemory, memoryPoolPtr,
std::placeholders::_1));
return 0;
}
输出
AQUIRING MEMORY
RELEASE MEMORY
Part 3: shared_ptr vs Pointer
原文链接:https://thispointer.com//how-shared_ptr-object-is-different-from-a-raw-pointer/
这篇文章比较了C++11中的智能指针shared_ptr和普通指针。
没有++,–以及[]运算符
shared_ptr只提供以下两种运算符
-
->, *
-
==
没有:
-
算数运算符如:+, -,++, –
-
下标运算符[]
下边是例子:
#include<iostream>
#include<memory>
struct Sample
{
void dummyFunction()
{
std::cout << "dummyFunction" << std::endl;
}
};
int main()
{
std::shared_ptr<Sample> ptr = std::make_shared<Sample>();
(*ptr).dummyFunction(); // Will Work
ptr->dummyFunction(); // Will Work
// ptr[0]->dummyFunction(); // This line will not compile.
// ptr++; // This line will not compile.
//ptr--; // This line will not compile.
std::shared_ptr<Sample> ptr2(ptr);
if (ptr == ptr2) // Will work
std::cout << "ptr and ptr2 are equal" << std::endl;
return 0;
}
输出:
dummyFunction
dummyFunction
ptr and ptr2 are equal
检查NULL
如果创建一个shared_ptr,但是没有关联裸指针,它就是空的。
注意,如果裸指针是一个悬空指针,shared_ptr是没法判断出来的。
可以使用以下3种方式判断一个shared_ptr为空:
std::shared_ptr<Sample> ptr3;
if(!ptr3)
std::cout<<"Yes, ptr3 is empty" << std::endl;
if(ptr3 == NULL)
std::cout<<"ptr3 is empty" << std::endl;
if(ptr3 == nullptr)
std::cout<<"ptr3 is empty" << std::endl;
从shared_ptr中获取裸指针
std::shared_ptr<Sample> ptr = std::make_shared<Sample>();
Sample * rawptr = ptr.get();
一般来说,不应该获取shared_ptr中的裸指针。因为可能会破坏shared_ptr中的引用计数,从而造成crash。即使不对指针做任何delete,当shared_ptr中引用计数为0的时候,将会删除裸指针指向的对象,那么通过get方法的到的指针就是一个悬空指针,也是很危险的。
Part 4: Create shared_ptr objects carefully
原文链接:https://thispointer.com/create-shared_ptr-objects-carefully/
我们在创建shared_ptr对象的时候要非常小心。
特别是以下两种情况:
1)不要试图使用一个裸指针创建两个shared_ptr对象。因为不同的shared_ptr对象互相不知道对方的存在,分别独立的引用计数。
下边我们看下这样做会引起什么问题?
假设两个shared_ptr对象都是用一个裸指针创建的:
int * rawPtr = new int();
std::shared_ptr<int> ptr_1(rawPtr);
std::shared_ptr<int> ptr_2(rawPtr);
然后ptr_2超出了作用域,讲裸指针指向的内存释放了。此时ptr_1中的裸指针就是一个悬空指针。
然后当ptr_2超出作用域后,再次释放内存,就会引起Crash。
代码示例:
#include<iostream>
#include<memory>
typedef struct Sample {
Sample() {
internalValue = 0;
std::cout<<"Constructor"<<std::endl;
}
~Sample() {
std::cout<<"Destructor"<<std::endl;
}
}Sample;
int main()
{
{
Sample * rawPtr = new Sample();
std::shared_ptr<Sample> ptr_1(rawPtr);
{
std::shared_ptr<Sample> ptr_2(rawPtr);
}
// As ptr_2 dont know that the same raw pointer is used by another shared_ptr i.e. ptr_1, therefore
// here when ptr_2 goes out of scope and it deletes the raw pointer associated with it.
// Now ptr_1 is internally containing a dangling pointer. Therefore when we it
// will go out of scope it will again try to delete the already deleted raw pointer and application
// will crash.
}
return 0;
}
- 不要使用一个指向栈上对象的指针创建shared_ptr
代码示例:
#include<iostream>
#include<memory>
int main()
{
int x = 12;
std::shared_ptr<int> ptr(&x);
return 0;
}
shared_ptr的实现假定其中的裸指针是指向堆中的对象的。如果将一个栈上的对象指针传入,在超出作用的时候,仍然会造成重复释放指针的问题,引发Crash。
鉴于以上两个原因,建议不要通过裸指针直接创建shared_ptr对象。而是使用make_shared<>方法
std::shared_ptr<int> ptr_1 = make_shared<int>();
std::shared_ptr<int> ptr_2 (ptr_1);
编码规范:先创建shared_ptr,再赋值。直接避免裸指针的定义出现。
Part 5: shared_ptr, Binary trees and the problem of Cyclic References
原文链接:https://thispointer.com/shared_ptr-binary-trees-and-the-problem-of-cyclic-references/
shared_ptr的最大好处是在没有人使用指针的时候,能自动释放内存。
但是如果不小心使用,优点可能变成缺点,我们看例子,
假设我们需要设计一个二叉树,节点中有两个指针,一个指向左子节点,一个指向右子节点。
#include <iostream>
#include <memory>
class Node
{
int value;
public:
std::shared_ptr<Node> leftPtr;
std::shared_ptr<Node> rightPtr;
Node(int val) : value(val) {
std::cout<<"Contructor"<<std::endl;
}
~Node() {
std::cout<<"Destructor"<<std::endl;
}
};
int main()
{
std::shared_ptr<Node> ptr = std::make_shared<Node>(4);
ptr->leftPtr = std::make_shared<Node>(2);
ptr->rightPtr = std::make_shared<Node>(5);
return 0;
}
在上边的例子中,一切都很正常。构造函数被调用了3次,析构函数也被调用了3次。没有内存泄露。
但是如果增加一个小需求,每个node添加一个指向父节点的指针,shared_ptr就会出问题了。
代码示例:
#include <iostream>
#include <memory>
class Node
{
int value;
public:
std::shared_ptr<Node> leftPtr;
std::shared_ptr<Node> rightPtr;
std::shared_ptr<Node> parentPtr;
Node(int val) : value(val) {
std::cout<<"Contructor"<<std::endl;
}
~Node() {
std::cout<<"Destructor"<<std::endl;
}
};
int main()
{
std::shared_ptr<Node> ptr = std::make_shared<Node>(4);
ptr->leftPtr = std::make_shared<Node>(2);
ptr->leftPtr->parentPtr = ptr;
ptr->rightPtr = std::make_shared<Node>(5);
ptr->rightPtr->parentPtr = ptr;
std::cout<<"ptr reference count = "<<ptr.use_count()<<std::endl;
std::cout<<"ptr->leftPtr reference count = "<<ptr->leftPtr.use_count()<<std::endl;
std::cout<<"ptr->rightPtr reference count = "<<ptr->rightPtr.use_count()<<std::endl;
return 0;
}
构造函数仍然被调用了3次,然而析构函数一次也没有被调用。
造成这种现象的原因是循环引用。如果两个shared_ptr互相引用了对方,当它们超出作用域的时候,都不会释放其中的内存。
因为在shared_ptr的析构函数中,首先将引用计数-1,然后检查引用计数是否为0,如果为0将会释放内存,否则说明还有其他的shared_ptr在使用其中的对象。在上边的场景中,这些shared_ptrs互相引用,所以引用计数始终不为0.
让我们重放一下这个过程:
ptr超出作用域的时候,析构函数被调用;
在ptr的析构函数中,Node(4)的引用计数-1;
检查Node(4)的引用计数,发现是2,因为leftPtr中的Node(2)的parent和rightPtr中的Node(5) 的parent在使用,所以Node(4)不会被删除;
Node(4)不被删除,那么Node(4)中使用的leftPtr和rightPtr就不会超出生命周期,所以也不会被析构;
于是,没有任何析构函数被调用;
如何解决这个问题?
答案是使用weak_ptr
weak_ptr允许共享但是不持有对象。对象来自shared_ptr。
std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::weak_ptr<int> weakPtr(ptr);
weak_ptr对象不能直接使用*和->运算符来操作内部对象。必须先从weak_ptr中使用lock()方法创建一个shared_ptr对象出来。
代码示例:
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::weak_ptr<int> weakPtr(ptr);
std::shared_ptr<int> ptr_2 = weakPtr.lock();
if(ptr_2)
std::cout<<(*ptr_2)<<std::endl;
std::cout<<"Reference Count :: "<<ptr_2.use_count()<<std::endl;
if(weakPtr.expired() == false)
std::cout<<"Not expired yet"<<std::endl;
return 0;
}
注意:如果shared_ptr中的对象已经被释放掉,lock()方法会返回一个空的shared_ptr;
使用weak_ptr改进上边的二叉树的例子:
将循环引用的其中一方改成weak_ptr引用就可以打破循环。
#include <iostream>
#include <memory>
class Node
{
int value;
public:
std::shared_ptr<Node> leftPtr;
std::shared_ptr<Node> rightPtr;
// Just Changed the shared_ptr to weak_ptr
std::weak_ptr<Node> parentPtr;
Node(int val) : value(val) {
std::cout<<"Contructor"<<std::endl;
}
~Node() {
std::cout<<"Destructor"<<std::endl;
}
};
int main()
{
std::shared_ptr<Node> ptr = std::make_shared<Node>(4);
ptr->leftPtr = std::make_shared<Node>(2);
ptr->leftPtr->parentPtr = ptr;
ptr->rightPtr = std::make_shared<Node>(5);
ptr->rightPtr->parentPtr = ptr;
std::cout<<"ptr reference count = "<<ptr.use_count()<<std::endl;
std::cout<<"ptr->leftPtr reference count = "<<ptr->leftPtr.use_count()<<std::endl;
std::cout<<"ptr->rightPtr reference count = "<<ptr->rightPtr.use_count()<<std::endl;
std::cout<<"ptr->rightPtr->parentPtr reference count = "<<ptr->rightPtr->parentPtr.lock().use_count()<<std::endl;
std::cout<<"ptr->leftPtr->parentPtr reference count = "<<ptr->leftPtr->parentPtr.lock().use_count()<<std::endl;
return 0;
}
上边的代码解决了内存泄露的问题。
Part 6 : unique_ptr<> Tutorial and Examples
原文链接:https://thispointer.com/c11-unique_ptr-tutorial-and-examples/
这篇文章中,我们讨论C++11中的智能指针std::unique<>的用法。
什么是std::unique_str?
std::unique<>是C++11为了避免内存泄露问题,实现的智能指针之一。一个unique_ptr对象包装了一个裸指针,并负责这个裸指针的生命周期。当unique_ptr对象析构的时候,它的析构函数会释放其中的裸指针的内存。
unique_ptr重载了->和*操作符,因此可以像使用裸指针一样使用unique_ptr。
下边是一个使用unique_ptr的例子:
#include <iostream>
#include <memory>
struct Task {
int mId;
Task(int id ) : mId(id) {
std::cout<<"Task::Constructor"<<std::endl;
}
~Task() {
std::cout<<"Task::Destructor"<<std::endl;
}
};
int main(){
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr(new Task(23));
//Access the element through unique_ptr
int id = taskPtr->mId;
std::cout<<id<<std::endl;
return 0;
}
输出
Task::Constructor
23
Task::Destructor
taskPtr是一个unique_ptr类型的对象,接收一个Task类型的裸指针作为构造函数。当函数退出的时候,taskPtr超出了生命周期,析构函数被调用,并释放了裸指针指向的Task对象。
因此,即使函数非正常退出(包括抛出异常),taskPtr的析构函数都会被调用,裸指针指向的Task始终都会被释放,从而避免了内存泄露。
unique pointer的独占性
一个unique_ptr对象始终独占其中的裸指针。不能复制一个unique_ptr对象,只能移动裸指针的所有权。因为独占性,unique_ptr对象总是可以安全的删除其中的裸指针,不用担心引起崩溃,也不用使用引用计数。
创建一个空的unique_ptr对象
一个创建空**unique_ptr**对象的例子:
// Empty unique_ptr object
std::unique_ptr<int> ptr1;
ptr1中没有持有(关联)任何裸指针,所以是空的。
检查一个unique_ptr对象是否为空
有两种方法检查一个unique_ptr对象是否为空:
方法一:
// Check if unique pointer object is empty
if(!ptr1)
std::cout<<"ptr1 is empty"<<std::endl;
方法二:
// Check if unique pointer object is empty
if(ptr1 == nullptr)
std::cout<<"ptr1 is empty"<<std::endl;
使用裸指针创建一个unique_ptr对象
将一个对应类型的裸指针传入unique_ptr对象的构造函数即可:
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr(new Task(23));
注意,我们不能通过**=**的方式创建一个unique_ptr对象,会引发编译错误。
// std::unique_ptr<Task> taskPtr2 = new Task(); // Compile Error
重置一个unique_ptr对象
通过调用unique_ptr对象的reset()函数可以重置一个unique_ptr对象。重置对象会将内部的裸指针删除,然后unique_ptr对象变成空的。
// Reseting the unique_ptr will delete the associated
// raw pointer and make unique_ptr object empty
taskPtr.reset();
unique_ptr对象是不能复制的
unique_ptr对象是不能复制的,只能移动所有权。因此,不能创建一个unique_ptr对象的副本,无论是使用复制构造函数还是使用赋值运算符=。
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr2(new Task(55));
// Compile Error : unique_ptr object is Not copyable
std::unique_ptr<Task> taskPtr3 = taskPtr2; // Compile error
// Compile Error : unique_ptr object is Not copyable
taskPtr = taskPtr2; //compile error
unique_ptr<> 类型禁用了复制构造函数和赋值运算符。
移动unique_ptr对象的所有权
不能复制一个unique_ptr对象,但是可以移动它对裸指针的所有权。意思就是一个unique_ptr对象可以把持有的裸指针的所有权交给另外一个unique_ptr对象。我们可以通过例子来理解:
创建一个unique_ptr对象:
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr2(new Task(55));
现在,taskPtr2不是空的。
然后把taskPtr2对裸指针的所有权移动给另外一个unique_ptr对象。
{
// Transfer the ownership
std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
if(taskPtr2 == nullptr)
std::cout<<"taskPtr2 is empty"<<std::endl;
// ownership of taskPtr2 is transfered to taskPtr4
if(taskPtr4 != nullptr)
std::cout<<"taskPtr4 is not empty"<<std::endl;
std::cout<<taskPtr4->mId<<std::endl;
//taskPtr4 goes out of scope and deletes the associated raw pointer
}
std::move() 会将taskPtr2变成一个右值引用,因此unique_ptr的移动构造函数被调用,并且将裸指针的所有权转移到taskPtr4中。
此后,taskPtr2为空。
不太理解变成右值引用这句,以后回头看看
原文:
std::move() will convert the taskPtr2 to a RValue Reference. So that move constructor of unique_ptr is invoked and associated raw pointer can be transferred to taskPtr4.
解除对裸指针的持有
通过调用unique_ptr对象的release()方法可以解除对裸指针的持有,方法返回裸指针本身。
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr5(new Task(55));
if(taskPtr5 != nullptr)
std::cout<<"taskPtr5 is not empty"<<std::endl;
// Release the ownership of object from raw pointer
Task * ptr = taskPtr5.release();
if(taskPtr5 == nullptr)
std::cout<<"taskPtr5 is empty"<<std::endl;
以下是完整例子代码
#include <iostream>
#include <memory>
struct Task
{
int mId;
Task(int id ) :mId(id)
{
std::cout<<"Task::Constructor"<<std::endl;
}
~Task()
{
std::cout<<"Task::Destructor"<<std::endl;
}
};
int main()
{
// Empty unique_ptr object
std::unique_ptr<int> ptr1;
// Check if unique pointer object is empty
if(!ptr1)
std::cout<<"ptr1 is empty"<<std::endl;
// Check if unique pointer object is empty
if(ptr1 == nullptr)
std::cout<<"ptr1 is empty"<<std::endl;
// can not create unique_ptr object by initializing through assignment
// std::unique_ptr<Task> taskPtr2 = new Task(); // Compile Error
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr(new Task(23));
// Check if taskPtr is empty or it has an associated raw pointer
if(taskPtr != nullptr)
std::cout<<"taskPtr is not empty"<<std::endl;
//Access the element through unique_ptr
std::cout<<taskPtr->mId<<std::endl;
std::cout<<"Reset the taskPtr"<<std::endl;
// Reseting the unique_ptr will delete the associated
// raw pointer and make unique_ptr object empty
taskPtr.reset();
// Check if taskPtr is empty or it has an associated raw pointer
if(taskPtr == nullptr)
std::cout<<"taskPtr is empty"<<std::endl;
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr2(new Task(55));
if(taskPtr2 != nullptr)
std::cout<<"taskPtr2 is not empty"<<std::endl;
// unique_ptr object is Not copyable
//taskPtr = taskPtr2; //compile error
// unique_ptr object is Not copyable
//std::unique_ptr<Task> taskPtr3 = taskPtr2;
{
// Transfer the ownership
std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
if(taskPtr2 == nullptr)
std::cout<<"taskPtr2 is empty"<<std::endl;
// ownership of taskPtr2 is transfered to taskPtr4
if(taskPtr4 != nullptr)
std::cout<<"taskPtr4 is not empty"<<std::endl;
std::cout<<taskPtr4->mId<<std::endl;
//taskPtr4 goes out of scope and deletes the assocaited raw pointer
}
// Create a unique_ptr object through raw pointer
std::unique_ptr<Task> taskPtr5(new Task(55));
if(taskPtr5 != nullptr)
std::cout<<"taskPtr5 is not empty"<<std::endl;
// Release the ownership of object from raw pointer
Task * ptr = taskPtr5.release();
if(taskPtr5 == nullptr)
std::cout<<"taskPtr5 is empty"<<std::endl;
std::cout<<ptr->mId<<std::endl;
delete ptr;
return 0;
}
输出:
ptr1 is empty
ptr1 is empty
Task::Constructor
taskPtr is not empty
23
Reset the taskPtr
Task::Destructor
taskPtr is empty
Task::Constructor
taskPtr2 is not empty
taskPtr2 is empty
taskPtr4 is not empty
55
Task::Destructor
Task::Constructor
taskPtr5 is not empty
taskPtr5 is empty
55
Task::Destructor