智能指针
一般有三种智能智能指针:std::shared_ptr
、std::unique_ptr
和std::wek_ptr
。std::shared_ptr
允许多个指针共享内存对象,std::unique_ptr
只允许一个指针独占内存对象,std::wek_ptr
与std::shared_ptr
配合使用,一般用于防止循环引用无法释放内存的问题。三者都在memory
库中
shared_ptr 类型
模板类型,需要显式说明类型,默认初始化空指针。与一般指针类似,取内容运算符使它指向一个对象。安全地使用该类型指针的方式是配合make_shared
函数共同使用。给出代码:
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<string>p;
p = make_shared<string>("hello world !");
cout << *p << endl;
return 0;
}
所有的shared_ptr
共享一个引用计数,如果指向一个对象的引用计数为0,则销毁这个对象。而unique_ptr
只能同时有一个指向对象,两者的共同操作:
p
:用作条件判断,如果p
指向一个对象,则为true
;否则是false
*p
:取内容操作p->mem
:取所指对象的mem成员p.get()
:返回p
中保存的指针。如果智能指针所指的对象被释放,则返回指针所指对象也消失swap(p, q)
:交换p
和q
指向p.swap(q)
:同上
shared_ptr
特有的操作:
make_shared<T>()
:安全地动态分配。shared_ptr<T>p(q)
:拷贝q
到p
中。p、q必须能转化到T*
p=q
:p
和q
必须能相互转化。递减p
的引用计数,增加q
的引用计数;若p
的引用计数变为0,则释放内存。
当进行赋值或者拷贝操作时,每个shared_ptr
都会记录其他shared_ptr
指向相同对象的个数。当引用计数为0,就会销毁所指的对象。
不要把智能指针和普通指针以及new
混合使用,也不要使用get
来初始化一个智能指针。但是可以使用new
作为构造成员,比如:
std::shared_ptr<int>p(new int(10));
unique_ptr 类型
该类型的指针没有类似于make_shared<>
的操作,只能直接使用new
进行初始化,将它绑定到new返回的对象上。不能拷贝这个类型的指正。
unique_ptr
特有的操作:
unique_ptr<T,D>u
:T
类型的指针,使用D
类型的可调用对象来销毁指针unique_ptr<T, D> u
:T
类型的指针,用类型为D
的对象d
来代替delete
u.release()
:放弃对指针的控制权,返回指针,将u
置空u.reset()
:释放所指的对象u.reset(q)
:指向q
所指向的对象
但是可以拷贝一个将要销毁的对象:
unique_ptr<int>clone(int p) {
return unique_ptr<int>(new int(p));
}
也可以返回一个局部对象的拷贝:
unique_ptr<int>clone(int p) {
unique_ptr<int>ret(new int(p));
return ret;
}
weak_ptr 类型
与shared_ptr
配合使用,指向shared_ptr
的对象时,不会改变后者的计数。销毁时不管是否存在weak_ptr
的指向。
特有的操作:
weak_ptr<T>p(q)
:指向q的对象,q可以是weak_ptr
或者shared_ptr
。w=p
:p可以是weak_ptr
或者shared_ptr
。w.reset()
:置空。w.lock()
:返回nullptr
或者指向w
的shared_ptr
的类型。
weak_ptr与shared_ptr合作的实例
shared_ptr
可能会出现环形引用的现象:
#include <iostream>
#include <memory>
using namespace std;
class Node {
public:
Node() {
cout << "create !" << endl;
}
~Node() {
cout << "release !" << endl;
}
void setPtr(shared_ptr<Node>node) {
p = node;
}
shared_ptr<Node>p;
};
int main() {
shared_ptr<Node>p = make_shared<Node>();
shared_ptr<Node>p1 = make_shared<Node>();
p.reset();
p1.reset();
return 0;
}
上述代码输出:
create !
create !
delete !
delete !
说明两者都被销毁了。
如果main函数改成下面这样:
int main() {
shared_ptr<Node>p = make_shared<Node>();
shared_ptr<Node>p1 = make_shared<Node>();
p->setPtr(p1); // 设置伙伴
p1->setPtr(p);
p.reset();
p1.reset();
return 0;
}
程序输出:
create !
create !
两者都没有析构,说明发生循环引用。
对于p
指向的对象来说,除了有p
指向它,还有p1.p
的指向它。执行了p.reset()
后,任然没有被销毁,而且它的p
成员任然指向p1
,p1
也不会被销毁。处理这种问题,就需要借助于weak_ptr
来执行。
为了更加形象说明,在这里使用一个二叉树的数据结构进行说明:
这里,实线是shared_ptr
,虚线是weak_ptr
类型。
#include <iostream>
#include <memory>
using namespace std;
class Node {
public:
Node(int n = 0): val(n) {}
~Node() {
cout << "delete: " << val << endl;
}
int val;
shared_ptr<Node>leftChild, rightChild;
weak_ptr<Node>parent;
};
// 前序建树,注意使用引用
void create(shared_ptr<Node>& p) {
int n;
cin >> n;
if(n == 0) {
p = nullptr;
return;
}
p = make_shared<Node>(n);
create(p->leftChild);
if(p->leftChild) { // 指向父节点
p->leftChild->parent = p;
}
create(p->rightChild);
if(p->rightChild) { // 指向父节点
p->rightChild->parent = p;
}
}
// 前序遍历
void preOrder(const shared_ptr<Node>& p) {
if(!p) {
return;
}
cout << p->val << endl;
preOrder(p->leftChild);
preOrder(p->rightChild);
}
// 从最左侧叶子一直回溯到根
void visit_left_down2top(const shared_ptr<Node>& p) {
auto t = p;
while(t->leftChild) {
t = t->leftChild;
}
while(t) {
cout << t->val << endl;
t = t->parent.lock(); // 注意这一个转化,使用lock
}
}
int main() {
shared_ptr<Node>p;
cout << "create tree: " << endl;
create(p);
cout << "visit tree preorder:" << endl;
preOrder(p);
cout << "visit left down to top:" << endl;
visit_left_down2top(p);
cout << "delete tree: " << endl;
p.reset();
return 0;
}
假设输入一个最简单的二叉树:1 2 0 0 3 0 0,结果如图:
create tree:
1 2 0 0 3 0 0
visit tree preorder:
1
2
3
visit left down to top:
2
1
delete tree:
delete: 1
delete: 3
delete: 2
可以看出,释放掉根节点,其余节点都自动释放掉了。如果把节点的parent
改成shared_ptr
,那么无法释放,因为会发生循环引用!!!!!!
动态数组
new和delete
首先说明下,如果能用标准容器,就不要自己创建。标准容器速度快,而且不易出错。
使用new
进行动态分配,使用delete
进行删除。new
构造自定义对象时,自动执行执行初始化;delete
执行删除自定义对象时,自动调用析构函数。
int* a = new int[10]; // 创建10个可以存储int的空间
delete []a; // 删除该空间,必须有[]
int* a = new int[0]; // 合法的,返回一个尾后指针
智能指针与动态数组
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动delete空间
shared_ptr
不支持这个特性。
allocator类
可以理解为一个泛型的new或者delete操作,支持更多的自定义操作类型。
基本操作:
-
allocator<T> a
:为T类型对象分配内存。 -
a.allocate(n)
:分配n个空间,但是是原始的不进行构造的内存。 -
a.deallocate(p, n)
:从p
指向的地址开始,释放n
个内存。p
必须是先前allocator
分配的内存,n必须是先前指定的大小。 -
a.construct(p, args)
:p是相应的指针,args是一个构造函数,用于初始化操作 -
a.destroy()
:释放点p指向的内存
具体可以参考有关的文档,这是一种新的思路。