常见的智能指针
概述
在C++中,动态内存的管理是通过一对运算符来完成的,即new和delete。动态内存的使用容易出问题,比如忘记释放内存、释放正在使用的指针等。新的标准库提供了两种智能指针类型来管理动态对象。它们能够自动释放所指向的对象。shared_ptr允许多个指针指向同一个对象;unique_ptr则”独占“所指的对象。它们都定义在memory头文件中。
auto_ptr智能指针已经被淘汰了,因为存在很大的问题,它采用的是资源转移的方法,产生问题的是拷贝构造和赋值运算符重载函数。当调用了拷贝构造或是赋值运算符的重载函数任意一个时:假设调用了拷贝构造函数,我们的本意是用一个对象去构造一个与之一模一样的对象,可是结果呢,我们把自己给弄丢了,完全没有达到我们预期的目标。
自定义auto_ptr的实现
template<class T>
class auto_pnt {
private:
T* m_pnt;
public:
//避免无意的隐式类型转换
explicit auto_pnt(T* p = 0) :m_pnt(p) {}
auto_pnt(auto_pnt& a) throw() :m_pnt(a.release()) {}
//复制操作符,注意销毁当前指针指向的对象,并将a指针置为0,防止多次delete的异常
auto_pnt& operator=(auto_pnt& a) throw()
{
reset(a->release());
return *this;
}
~auto_pnt() { delete m_pnt; }
//区指针指向的对象
T& operator*() const throw()
{
return *m_pnt;
}
//返回当前指针的值
T* get() const throw()
{
return m_pnt;
}
//返回该指针值作为临时变量,并将当前指针置0
T* release() throw()
{
T* tmp = m_pnt;
m_pnt = 0;
return tmp;
}
//将指针指向p,并销毁当前指向的对象
void reset(T* p) throw()
{
if (p != m_pnt)
{
delete m_pnt;
m_pnt = p;
}
}
};
shared_ptr类
<1>智能指针也是模板
shared_ptr<string> p1;
最安全的分配和使用动态内存的方法是调用make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。make_shared用其参数来构造给定类型的对象。
shared_ptr<int> p2 = make_shared<int>(24);
shared_ptr<string> p3 = make_shared<string>(5,'2');
<2>shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其它shared_ptr指向相同的对象。我们可以认为每个shared_ptr都有一个关联的计数器,称其为引用计数。当拷贝一个shared_ptr,计数器就会递增。相反,当给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过析构函数来完成的。
shared_ptr的简单实现
#include<iostream>
using namespace std;
template<class T>
class Shared_ptr {
public:
Shared_ptr(T *p) :ptr(p)
{
count = new int(1);
}
Shared_ptr(const Shared_ptr& other);
Shared_ptr& operator=(const Shared_ptr& other);
~Shared_ptr();
int get_int() { return *count; }
T* operator->() { return ptr; }
T& operator*() { return *prt; }
private:
int *count;
T* ptr;
};
template<class T>
Shared_ptr<T>::Shared_ptr(const Shared_ptr& other)
{
count = ++*(other.count);
ptr = other.ptr;
}
template<class T>
Shared_ptr<T>& Shared_ptr<T>::operator=(const Shared_ptr& other)
{
++*other.count;
if (ptr != nullptr && --*count == 0)
{
delete count;
delete ptr;
}
count = other.count;
ptr = other.ptr;
return *this;
}
template<class T>
Shared_ptr<T>::~Shared_ptr()
{
if (--*count == 0)
{
delete ptr;
delete count;
}
}
int main()
{
system("pause");
return 0;
}
智能指针在多线程中的安全性
https://blog.csdn.net/Solstice/article/details/8547547
shared_ptr存在的问题
1.线程安全问题:
shared_ptr 本身不是 100% 线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、string 一样,即:
一个 shared_ptr 实体可被多个线程同时读取;
两个的 shared_ptr 实体可以被两个线程同时写入,“析构”算写操作;
如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。
2.循环引用问题
#include<iostream>
#include<memory>
using namespace std;
struct Node
{
Node(int value)
:_value(value)
{
cout << "Node()" << endl;
}
~Node()
{
cout << "~Node()" << endl;
}
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
int _value;
};
void Test2()
{
shared_ptr<Node> sp1(new Node(1));
shared_ptr<Node> sp2(new Node(2));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main()
{
Test2();
system("pause");
return 0;
}
sp1,sp2,_pre,_next均是shared_ptr的对象,所以两个引用计数空间中的use均为2,而在出Test()的作用域之前,会对栈空间上的变量进行销毁释放,也就是说在这里,会对sp1和sp2这两个对象进行释放,调用它们的析构函数,但由于在shared_ptr的析构函数中,只有当use=1,进行减减之后为0,才会释放_ptr所指向的空间,所以在这里sp1和sp2所管理的节点空间是不会被释放的,因此也不会调用~Node()这个析构函数。
由于在shared_ptr单独使用的时候会出现循环引用的问题,造成内存泄漏,所以标准库又从boost库当中引入了weak_ptr。
#include<iostream>
#include<memory>
using namespace std;
struct Node
{
Node(int value)
:_value(value)
{
cout << "Node()" << endl;
}
~Node()
{
cout << "~Node()" << endl;
}
weak_ptr<Node> _prev;
weak_ptr<Node> _next;
int _value;
};
void Test2()
{
shared_ptr<Node> sp1(new Node(1));
shared_ptr<Node> sp2(new Node(2));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main()
{
Test2();
system("pause");
return 0;
}
我能想到的智能指针的不足
1 循环引用
2 基于引用计数的一些性能损耗
直接管理内存
<1>使用new动态分配和初始化对象
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化
int *p = new int;
int q;//值是定义的
int main()
{
cout <<p<<" "<<q << endl;
system("pause");
return 0;
}
可以使用传统的构造方式(圆括号)来初始化一个动态分配的对象,也可以使用列表初始化,还有对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可。
int *p = new int(24);
vector<int> *q = new vector<int>{1,2,3,4,5};
int *r = new int();//值初始化为0
<2>释放动态内存
通过delete表达式来将动态内存归还给系统。delete p;这一过程执行了两个动作:销毁给定的指针指向的对象;释放对应的内存。
传递给delete的指针必须是指向动态分配的内存,或者是一个空指针。
unique_ptr
<1>一个unique_ptr”独占“它所指的对象。也就是说,某一时刻,只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化的形式。
unique_ptr<int> p(new int(24));
<2>由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。虽然不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个unique_ptr转移给另一个unique。
int main()
{
unique_ptr<int> p(new int(24));
unique_ptr<int> p2(p.release());//release成员返回unique_ptr当前保存的指针并将其置为空。也就是p2初始化为
//p1原来保存的指针,而p1置为空。
unique_ptr<int> p3(new int(23));
p2.reset(p3.release());//首先通过reset释放p2所指向的对象,然后初始化为p3原有的指针,并将p3置为空
cout << *p2 << endl;
system("pause");
return 0;
}
<3>不能拷贝unique_ptr的规则有一个例外就是可以拷贝或赋值一个将要被销毁的unique_ptr。其本质就是调用了移动拷贝和移动赋值。
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret (new int(p));
return ret;
}
unique_ptr的简单实现
https://blog.csdn.net/liushengxi_root/article/details/80672901#commentBox
template<typename T>
class MyUniquePtr
{
public:
explicit MyUniquePtr(T* ptr = nullptr)
:mPtr(ptr)
{}
~MyUniquePtr()
{
if (mPtr)
delete mPtr;
}
MyUniquePtr(MyUniquePtr &&p) noexcept;
MyUniquePtr& operator=(MyUniquePtr &&p) noexcept;
MyUniquePtr(const MyUniquePtr &p) = delete;
MyUniquePtr& operator=(const MyUniquePtr &p) = delete;
T* operator*() const noexcept { return mPtr; }
T& operator->()const noexcept { return *mPtr; }
//如果提供了内置指针q,则令mPtr指向这个对象;否则将mPtr置为空
void reset(T* q = nullptr) noexcept
{
if (q != mPtr) {
if (mPtr)
delete mPtr;
mPtr = q;
}
}
//放弃对指针的控制权,并且返回指针,同时将mPtr置空
T* release() noexcept
{
T* res = mPtr;
mPtr = nullptr;
return res;
}
T* get() const noexcept { return mPtr; }
private:
T* mPtr;
};
template<typename T>
MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
{
if (this != &p)
{
delete mPtr;
mPtr = q.mPtr;
p.tr = nullptr;
}
return *this;
}
template<typename T>
MyUniquePtr<T> ::MyUniquePtr(MyUniquePtr &&p) noexcept : mPtr(p.mPtr)
{
p.mPtr == NULL;
}
weak_ptr
创建一个weak_ptr时,要用一个shared_ptr来初始化它。
auto p = make_shared<int>(24);
weak_ptr<int> wp(p);
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
由于对象可能不存在,所以不能使用weak_ptr直接访问对象,而必须调用lock。此函数负责检测weak_ptr指向的对象是否存在。若存在,lock返回一个指向共享对象的shared_ptr。
weak_ptr的使用
https://blog.csdn.net/VonSdite/article/details/81556647
分配一个对象数组的方法
1.动态数组
new和delete运算符一次只能分配或者释放一个对象,但有许多情况需要一次为很多内存分配对象,比如vector中的内存不足,需要整体搬迁。C++提供了两种一次分配一个对象数组的方法。
<1>new和数组,初始化动态分配的对象数组
int *p1 = new int[24];//10个未初始化的int
int *q1 = new int[24]();//10个值初始化为0的int
int *p2 = new int[get_size()];//调用get_size确定分配多少个int
注意:虽然我们通常称new T[]分配的内存为动态内存,但这种叫法某种程度上有些误导。当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。因为分配的内存并不是一个数组类型,因此不能对动态数组使用begin或end(因为需要知道首尾指针),当然也不能用范围for语句来处理动态数组中的元素(原因同上)。
<2>释放动态数组
delete[] p;//p指向的数组中的元素按逆序销毁,即最后一个元素先被销毁,以此类推。
typedef int arr[24];
int *q = new arr;
delete[] q;//即使使用一个类型别名来定义一个数组类型,从而在new中不使用[]。我们在释放一个动态数组时也必须使用[]。
<3>智能指针和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,必须要在对象类型后面跟一对空方括号。
unique_ptr<int[]> up(new int[10]);
up.release();//自动用delete[]销毁其指针
此时的up指向的是一个数组而不是单个对象,所以不能使用点和箭头成员运算符。而是使用下标运算符来访问数组中的元素。
int main()
{
unique_ptr<int[]> up(new int[10]);
for (size_t i = 0; i != 10; i++)
up[i] = i;
up.release();//自动用delete[]销毁其指针
system("pause");
return 0;
}
与shared_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。
shared_ptr<int>sp(new int[10], [](int *p) {delete[] p; });
sp.reset();//使用lambda释放数组
shared_ptr不直接支持动态数组管理这一特性影响了我们如何访问数组中的元素,shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。
int main()
{
shared_ptr<int>sp(new int[10], [](int *p) {delete[] p; });
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i;//通过get获取一个内置指针
sp.reset();
system("pause");
return 0;
}
2.allocator类
<1>new有一些灵活性上面的缺陷,它是将内存分配和对象构造组合在一起。同样,delete将对象析构和内存释放组合在了一起。但有时,当分配了一大块内存后,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才执行对象创建操作。
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。由它分配的内存是原始的、未构造的。
int main()
{
allocator<string> alloc;//可以分配string的allocator对象
auto p = alloc.allocate(24);//allocate为24个string分配了内存。
auto q = p;
alloc.construct(q++, "hi");//注意:p是指向最后构造的元素之后的位置,所以*q指向的是未构造的内存
system("pause");
return 0;
}
当我们用完对象之后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数。
while (q != p)
alloc.destroy(--q);
一旦元素被销毁后,就可以重新使用这部分内存来保存其它string,也可以将其归还给系统。释放内存是通过调用deallocate来完成。
alloc.deallcate(p, n);//n与调用allocated分配内存时提供的大小参数必须一致
<2>当一个类定义了一个构造函数后,编译器不会再生成默认构造函数。没有默认构造函数的类不能用作动态分配数组的元素。原因是当new对象数组时,不能给动态分配的数组每个元素一个初始化值,编译器于是会自动调用构造函数,而没有默认构造函数会出错。假设A为没有默认构造函数的类,当动态申请数组时new A[Length],并不能给数组初始化值,而new操作符不只是分配内存,而且需给编译器标识出内存中存放的是什么类型数据,故需要去调用类的默认构造函数,没有则出错。