C++智能指针与动态内存分配

版权声明:所有的博客都是作为个人笔记的。。。。。。 https://blog.csdn.net/qq_35976351/article/details/82812871

智能指针

一般有三种智能智能指针:std::shared_ptrstd::unique_ptrstd::wek_ptrstd::shared_ptr允许多个指针共享内存对象,std::unique_ptr只允许一个指针独占内存对象,std::wek_ptrstd::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):交换pq指向
  • p.swap(q):同上

shared_ptr特有的操作:

  • make_shared<T>():安全地动态分配。
  • shared_ptr<T>p(q):拷贝qp中。p、q必须能转化到T*
  • p=qpq必须能相互转化。递减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>uT类型的指针,使用D类型的可调用对象来销毁指针
  • unique_ptr<T, D> uT类型的指针,用类型为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的指向。

扫描二维码关注公众号,回复: 3353920 查看本文章

特有的操作:

  • weak_ptr<T>p(q):指向q的对象,q可以是weak_ptr或者shared_ptr
  • w=p:p可以是weak_ptr或者shared_ptr
  • w.reset():置空。
  • w.lock():返回nullptr或者指向wshared_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成员任然指向p1p1也不会被销毁。处理这种问题,就需要借助于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指向的内存

具体可以参考有关的文档,这是一种新的思路。

猜你喜欢

转载自blog.csdn.net/qq_35976351/article/details/82812871