动态内存与智能指针

*爱你&永不变心* 提交于 2019-12-27 21:37:11

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

前言


1.生命周期

全局对象:程序启动时分配,程序结束时销毁

局部自动对象:执行流进入其定义的块时分配,执行流退出其定义的块时销毁

局部static对象:程序启动时分配(但在其定义的块或作用域内起作用),程序结束时销毁

动态分配的对象:生命周期与创建地点无关,只有当显示的被释放时,才会被销毁

智能指针:标准库定义,用于管理动态分配的内存,当一个对象应该被释放时,指向它的智能指针可以确保自动的释放它


2.内存分类

静态内存:用于存储局部static对象、类的static数据成员、全局变量

栈内存:用于存储局部非static对象

堆内存(内存池):用于存储动态分配的对象——动态分配的对象,其生命周期由程序控制,例如使用new或delete


3.动态内存

动态内存使用过程中容易产生的问题 

内存泄露:使用后忘记释放内存

引用非法内存:在尚有指针引用内存的情况下就释放它

使用动态内存的原因: 

       程序不知道自己需要使用多少对象

       程序不知道所需对象的准确类型

       程序需要在多个对象间共享底层数据——若两个对象共享底层数据,当某个对象销毁时,我们不能单方面的销毁底层数据


4.智能指针

智能指针和常规指针直接的区别:智能指针能够自动释放所指向的内存(类似java中的垃圾回收机制),而常规指针不能。

智能指针默认初始化为nullptr

智能指针分类 

     shared_ptr:允许多个指针指向同一对象

     unique_ptr:独占所指向的对象


5.智能指针和异常

如果使用智能指针,即使程序非正常结束(异常),只要程序离开作用域,局部变量(智能指针)就会被销毁,若此时引用计数为0,则所指对象或内存也会被销毁。

如果使用普通指针,则当程序非正常结束(异常)时,由于未进行delete操作,因此指针所指的对象或内存未被释放,造成内存泄露。


6.使用智能指针的基本规范(智能指针的陷阱)

不使用相同的内置指针值初始化(或reset)多个智能指针,否则容易多次释放同一对象或内存——因为这样产生的智能指针是相互独立的


int *p = new int(1);

shared_ptr<int> p1(p);

shared_ptr<int> p2(p);  // 此时p1和p2相互独立且引用计数都为1,离开作用域时会释放同一对象两次

不delete``get()返回的指针,否则离开作用域时又要释放一次内存


{

    shared_ptr<int> p = make_shared<int>(1);

    delete p.get();     // 释放一次内存

}   // 又要释放一次内存

不使用get()初始化或reset另一个智能指针,否则这两个智能指针是独立的,离开作用域时会释放同一内存两次


{

    shared_ptr<int> p1 = make_shared<int>(1);

    shared_ptr<int> p2(p1.get());   // 此时p1和p2相互独立且引用计数都为1

}   // 释放同一内存两次

如果你使用get()返回的指针,记住当最后一个对于的智能指针销毁后,你的指针就无效了——因为此时该指针为悬空指针


int *p = nullptr;

{

    shared_ptr<int> p1 = make_shared<int>(1);

    p = p1.get();

}   // 此时释放p1所指向的内存,且p为悬空指针

如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(用于代替默认的delete)——因为智能指针的自动释放默认使用delete,而delete只能用于动态内存或nullptr

智能指针

智能指针类所支持的操作

shared_ptr和unique_ptr都支持的操作


操作功能

shared_ptr<T> sp、unique_ptr<T> up空智能指针,可以指向类型为T的对象

*p解引用p所指的对象

p->data访问对象*p中的data元素

p.get()返回p中保存的指针(值)

swap(p, q)、p.swap(q)交换p和q中保存的指针(值)

shared_ptr独有的操作


操作功能

make_shared<T>(args)返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象

shared_ptr<T> p(q)、p = q、shared_ptr<T> p(q, d)拷贝初始化,p和q指向同一对象,其中d是一个删除器(用来代替默认的delete)

p.unique()若p.use_count() == 1,则返回true,否则返回false

p.use_count()返回与p共享对象的智能指针数量(引用计数)

p.reset()、p.reset(q)、p.reset(q, d)若p是唯一指向其对象的shared_ptr,则reset会释放此对象。若传递了可选的参数(内置指针)q,则会令p指向q,否则会将p置为空。若还传递了参数(可调用对象)d,则会调用d而不是delete来释放p

unique_ptr独有的操作


操作功能

unique_ptr<T> u指向类型T的对象的空unique_ptr,且默认使用delete释放指针

unique_ptr<T, D> u指向类型T的对象的空unique_ptr,且使用类型为D的可调用对象来释放指针

unique_ptr<T, D> u(d)指向类型T的对象的空unique_ptr,且使用类型为D的可调用对象d代替delete来释放指针

u = nullptr释放u指向的内存,并将u置为空

u.release()先使指针对象u放弃对指针(值)的控制权,再返回指针(值),最后将u置为空——即断开指针对象和所指对象之间的联系(只断开联系,不释放内存)

u1.reset()、u2.reset(q)、u3.reset(nullptr)重置指针(值),使其指向新对象或为空

shared_ptr类

关键概念


初始化 

用make_shared初始化(最安全)

拷贝初始化(拷贝另一个智能指针)或直接初始化(可由任意类型指针为参数)

引用计数 

引用计数决定何时释放内存

智能指针独有,内置指针没有

相互关联的智能指针的引用计数相同

内存释放 

析构函数

默认delete

自定义删除器

初始化


使用make_shared标准库函数初始化(最安全,且会为对象分配内存):


shared_ptr<string> p1 = make_shared<string>("chensu");

shared_ptr<int> p2 = make_shared<int>(24);

shared_ptr<vector<string>> p3 = make_shared<vector<string>>();  // 当参数为空时,默认进行值初始化,此时p3所指对象为空vector<string>

class Person {

private:

    string name;

    int age;

public:

    Person(const string &_name, int _age) : name(_name), age(_age) {}

    void print() const { cout << name << ", " << age << endl; }

};

shared_ptr<Person> p4 = make_shared<Person>(Person("chensu", 24));   // 参数必须与Person类的某个构造函数相匹配

使用make_shared为智能指针分配动态内存比用new直接初始化更安全的原因:使得在分配对象的同时就将shared_ptr与之绑定,避免将同一块内存绑定到多个独立创建的shared_ptr上:


Person *p = new Person("chensu", 24);

shared_ptr<Person> p1(p);

shared_ptr<Person> p2(p);   // 此时p1和p2相互独立且引用计数都为1,程序结束时会释放统一内存两次

拷贝初始化(只拷贝指针,无需重新为对象分配内存)


拷贝智能指针


auto p = make_shared<int>(42);  // 为p指向的对象分配内存,且此时该对象只有p一个引用者

auto q(p);  // p和q指向同一个对象,此时该对象有两个引用者

拷贝普通指针(和new结合):由于接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个普通指针隐式转换为智能指针,而必须使用直接初始化形式或将普通指针强制类型转换为智能指针。


shared_ptr<int> p = new int(42);    // 错误,不支持隐式转换,可以使用直接初始化或强制类型转换

shared_ptr<int> p(new int(42));     // 正确,支持直接初始化形式

shared_ptr<int> p = shared_ptr<int>(new int(42));   // 正确,能够拷贝强制转换后的智能指针

初始化并自定义释放操作:默认情况下,系统通过delete运算符来自动释放智能指针,因此一个用来初始化智能指针的普通指针必须指向动态内存,否则结果未定义。但我们可以自定义释放操作来代替默认的delete,这样就可以用指向非动态内存的普通指针来初始化智能指针


shared_ptr<int> p(q, d);  // p将使用可调用对象d来代替delete

引用计数


每个shared_ptr都各自关联一个计数器,以免发生引用非法内存(即在尚有指针引用内存的情况下就释放该内存),引用计数的初始值为1


智能指针的引用计数=所指对象绑定的互相关联的智能指针的数量 

智能指针必须互相关联的原因:拷贝智能指针有两个阶段,且这两个阶段只有互相关联的智能指针才能完成,互相独立的智能指针没有这两个阶段


拷贝引用计数(将源智能指针的引用计数拷贝给目的智能指针)

共同递增引用计数(分别将源智能指针和目的智能指针的引用计数加1)

例子:


shared_ptr<int> p(new int(42)); // p的引用计数为1

shared_ptr<int> q(p);   // 因为p和q相互关联,所以它们的引用计数都递增为2

{

    shared_ptr<int> r(p.get());   // 因为p和r相互独立(通过get初始化的指针互相独立),所以它们的引用计数都不变(既不拷贝,也不共同递增),其中p为2,r为1

}   // 程序块结束,r递减为0,并释放r所指向的内存,此时p和q都为悬空指针

int a = *p; // 未定义,因为此时p为悬空指针

永远不要用get获得的指针值初始化一个智能指针,因为这样做的话,源指针和目的智能指针是相互独立的


指向同一对象的互相关联的智能指针具有相同的引用计数

智能指针p所指对象的引用计数递增的情况——即p所指的对象与和p互相关联的其它智能指针发生新的绑定关系:


用p初始化另一个智能指针q(对象绑定另一个智能指针):


auto p = make_shared<int>(42);  // 此时p的引用计数为1

auto q = p;     // 此时p和q的引用计数为2

将p作为参数传递给一个函数(对象绑定形参):


auto p = make_shared<int>(42);  // 此时p的引用计数为1

function(p);    // 此时p的引用计数为2

将p作为函数的返回值(对象绑定一个右值指针或左值指针):


shared_ptr<int> function() {

    auto p = make_shared<int>(42);  // 此时p的引用计数为1

    return p;   // 此时p的引用计数为2 

}

...     // 即使离开了function函数,p所指的对象的引用计数依旧为1

智能指针p所指对象的引用计数递减的情况——即p所指的对象与和p互相关联的其它智能指针解除旧的绑定关系:


为p赋予一个和原来不同的新值:


auto p = make_shared<int>(42);

auto p = nullptr;     // 此时p的引用计数为0

p被销毁(例如程序离开p的作用域):


void function() {

    auto p = make_shared<int>(42);  // 此时`p`的引用计数为1

}

...     // 此时`p`的引用计数为0

一旦一个shared_ptr的引用计数变为0,它就会使用析构函数自动释放自己所管理的对象(类似于java中的垃圾回收机制)


内存释放


析构函数: 智能指针通过自己的析构函数完成销毁操作,析构函数会递减它所指向对象的引用计数,若引用计数变为0(即该对象此时没有绑定任何指针),则析构函数会销毁对象,并释放对象所占用的内存

注意事项 

由于在最后一个shared_ptr销毁之前内存都不会释放,因此保证shared_ptr在无用之后不再保留就非常重要了,若你忘记销毁程序中不再需要的shared_ptr,虽不会操作内存泄露,但会浪费内存,造成程序运行较卡

销毁不再需要的shared_ptr指针的方法是令其为nullptr

默认情况下,系统通过delete运算符来自动释放智能指针,因此一个用来初始化智能指针的普通指针必须指向动态内存,否则结果未定义。但我们可以自定义释放操作来代替默认的delete,这样就可以用指向非动态内存的普通指针来初始化智能指针

警告


不要混合使用普通指针和智能指针——因为混合使用会干扰内存的正常释放


void process(shared_ptr<int> ptr) { ... }   // 指针进入函数时引用计数加1,离开时引用计数减1

/* 情况1:只使用智能指针 */        

shared_ptr<int> p(new int(42));     // p所指内存的引用计数为1

process(p);     // p离开后引用计数不变,仍为1

int i = *p;     // 正确,因为内存未被释放

/* 情况2:混合使用智能指针和普通指针 */

int *q(new int(42));    // q所指内存的引用计数为0

process(shared_ptr<int>(q));    // q离开后引用计数不变,仍未0,但在离开时由于形参的类型为shared_ptr,因此系统会自动释放q所指向的内存,此时q为悬空指针

int j = *q;     // 未定义,此时q为悬空指针

unique_ptr类

关键概念


初始化 

拷贝初始化(拷贝另一个智能指针)或直接初始化(可由任意类型指针为参数)

对指针(值)的控制权


所有操作都必须保持指针值的唯一性

某一时刻只能有一个unique_ptr指向一个给定的对象——保持唯一性

不能拷贝或赋值unique_ptr(例外:可以拷贝或赋值一个将要被销毁的unique_ptr)——保持唯一性:


unique_ptr<int> clone(int p) {

    return unique_ptr<int>(new int(p));

}

unique_ptr<int> q = clone(1);   // 正确,该函数的返回值是一个右值(临时变量)

允许转移对指针(值)的控制权,但源指针值必须改变——保持唯一性

内存释放 

当unique_ptr为nullptr时,其原来所指向的内存会被释放

对象与unique_ptr指针之间的联系——若联系断开,则即使unique_ptr指针被销毁,对象也不会被释放

只有与对象相关联的unique_ptr被销毁时,对象才会被自动释放

对unique_ptr指针(值)的控制权


使用release()释放对指针值或对象内存的控制权(但不会释放所指对象的内存)


{

    unique_ptr<int> u(new int(1));

    u.release();    // 释放控制权(断开u和对象之间的联系)

}   // 离开作用域,销毁u,但由于此时u和对象之间无关联,因此无法释放对象内存,故造成内存泄漏

使用release()和reset()转移控制权(目的unique_ptr原来所指向的对象会被自动释放)


{

    unique_ptr<int> u1(new int(1));

    unique_ptr<int> u2(new int(2));

    u2.reset(u1.release()); // u1先释放对指针值的控制权(断开与对象的联系),再返回指针值给u2,接着将u1置为空,此时u2掌握该指针值(或对象内存)的控制权,最后自动释放之前u2控制的内存

}   // 离开作用域,释放此时u2控制的内存

向unique_ptr传递删除器


默认情况下,使用delete释放它指向的对象

由于我们必须在尖括号中unique_ptr指向的对象类型之后提供删除器类型,因此重载一个删除器会影响到unique_ptr的类型以及如何构造(或reset)该类型的对象:


void my_deleter(int *ptr) { delete ptr; }

int *q = new int(1);

unique_ptr<int, decltype(my_deleter) *)> p(q, my_deleter)

weak_ptr类

将一个weak_ptr绑定到shared_ptr上不会改变shared_ptr的引用计数

一旦weak_ptr绑定的最后一个shared_ptr被释放,该weak_ptr就成为悬空指针

由于对象可能已被释放(weak_ptr成为了悬空指针),因此我们不能使用weak_ptr直接访问对象,而必须调用lock检查weak_ptr所指的对象是否存在,若存在则返回一个shared_ptr:


shared_ptr<int> p = make_shared<int>(42);

weak_ptr<int> wp(p);

shared_ptr<int> q = wp.lock();

总结

智能指针要特别注意同一内存多次释放和内存为释放问题

shared_ptr的引用计数决定是否释放其所指向的内存

始终保证至少有一个unique_ptr和对象之间的联系,否则容易造成内存泄漏


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!