您的位置:首页 > 其它

动态内存和智能指针

2018-01-23 21:26 274 查看
一、动态内存

  c++ 内存获取和释放 new/delete,new[]/delete[]

  c 内存获取和释放 malloc/free,calloc/realloc

  上述8个函数/操作符是c/c++语言里常用来做动态内存的申请和释放的,要理解这些接口,大概需要下面几个维度的了解:

1、了解OS的进程空间模型

  一个进程的地址空间,一般划分为内核区、用户区,用户区又划分为栈区、堆区、数据区、代码区。这里的‘堆区’,‘栈区’,‘数据区’,‘内核区’,其实就是一个虚拟地址区间,动态内存最终都是从OS的’堆区’上获取的。

2、brk、mmap 系统调用

  brk系统调用,可以让进程的堆指针增长一定的大小,逻辑上消耗掉一块本进程的虚拟地址区间,malloc向OS获取的内存大小比较小时,将直接通过brk调用获取虚拟地址,结果是将本进程的brk指针推高。

  mmap系统调用,可以让进程的虚拟地址区间里切分出一块指定大小的虚拟地址区间vma_struct,并返回给用户态进程,被mmap映射返回的虚拟地址,逻辑上被消耗了,直到用户进程调用munmap,才回收回来。malloc向系统获取比较大的内存时,会通过mmap直接映射一块虚拟地址区间。mmap系统调用用处非常多,比如一个进程的所有动态库文件.so的加载,都需要通过mmap系统调用映射指定大小的虚拟地址区间,然后将.so代码动态映射到这些区域,以供进程其他部分代码访问;另外,多进程通讯,也可以使用mmap,这块另开文章详解。

  无论是brk还是mmap返回的都是虚拟地址,在第一次访问这块地址的时候,会触发缺页异常,然后内核为这块虚拟地址申请并映射物理页框,建立页表映射关系,后续对该区间虚拟地址的访问,通过页表获取物理地址,然后就可以在物理内存上读写了。

3、malloc/free 是libc库函数

  malloc/free是 libc实现的库函数,主要实现了一套内存管理机制,当其管理的内存不够时,通过brk/mmap等系统调用向内核申请进程的虚拟地址区间,如果其维护的内存能满足malloc调用,则直接返回,free时会将地址块返回空闲链表。

  malloc(size) 的时候,这个函数会多分配一块空间,用于保存size变量,free的时候,直接通过指针前移一定大小,就可以获取malloc时保存的size变量,从而free只需要一个指针作为参数就可以了calloc 库函数相当于 malloc + memset(0)

  除了libc自带的动态内存管理库malloc, 有时候还可以使用其他的内存管理库替换,比如使用google实现的tcmalloc ,只需要编译进程时链接上 tcmalloc的静态库并包含响应头文件,就可以透明地使用tcmalloc 了,与libc 的malloc相比, tcmalloc 在内存管理上有很多改进,效率和安全性更好。

4、new/new[]/delete/delete[]

  new/delete 是c++ 内置的运算符,相当于增强版的malloc/free. c++是兼容c的,一般来说,同样功能的库,c++会在安全性和功能性方面比c库做更多工作。动态内存管理这块也一样。

  new的实现会调用malloc,对于基本类型变量,它只是增加了一个cookie结构, 比如需要new的对象大小是 object_size, 则事实上调用 malloc 的参数是 object_size + cookie, 这个cookie 结构存放的信息包括对象大小,对象前后会包含两个用于检测内存溢出的变量,所有new申请的cookie块会链接成双向链表。由于内置了内存溢出检测,所以比malloc更安全。

  对于自定义类型,new会先申请上述的大小空间,然后调用自定义类型的构造函数,对object所在空间进行构造。c++比c强大的一个方面就是c++编译器可以自动做构造和析构,new运算符会自动计算需要的空间大小,然后根据类型自己调用构造函数,如果存在子类型对象,或者存在继承的基类型,new都会自动调用子类型的构造函数和基类型的构造函数完成构造。同样,delete 操作符根据cookie的size知道object的大小,如果是自定义类型,会调用析构函数对object所在空间进行析构,如果有子类型或继承,自动调用子类型和基类型的析构函数,然后将cookie块从双向链表摘除,最后调用 free_dbg 释放。

  new[] 和delete[]是另外两个操作符,用于数组类型的动态内存获取和释放,实现过程类似new/delete 。

二、智能指针

1、智能指针背后的设计思想

  我们先来看一个简单的例子:

void remodel(std::string & str)
{
std::string * ps = new std::string(str);
...
if (weird_thing())
throw exception();
str = *ps;
delete ps;
return;
}


  当出现异常时(weird_thing()返回true),delete将不被执行,因此将导致内存泄露。

  如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,看是否有这种潜在的内存泄露问题,那就是一场灾难!

  这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除—因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。

  我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。

  这正是智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

  shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

2、shared_ptr类





(1)make_shared函数

  最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

shared_ptr<int> p = make_shared<int>(42);//效率比下面的高
shared_ptr<int> p2(new int(42));//不推荐,为了避免智能指针与普通指针的混用,所以最后使用make_shared,这样在内存分配之后立刻与智能指针绑定到一起.


(2)shared_ptr的拷贝和赋值

auto p = make_shared<int>(42);
auto q(p);
auto r = p; //p递增,r递减。


  每个shared_ptr都有一个关联的计数器,通常称为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增;当我们给shared_ptr赋予一个新值或者shared_ptr被销毁时,引用计数会递减。一旦引用计数变为0,shared_ptr就会自动释放自己所管理的对象。

(3)shared_ptr自动销毁所管理的对象

  shared_ptr的析构函数会递减它所指向的对象的引用计数,当引用计数变为0时,shared_ptr就会通过析构函数自动释放自己所管理的对象。当动态对象不再使用时,shared_ptr会自动释放对象,这一特性使得动态内存的使用变得非常容易(尽量使用智能指针管理动态内存)。如果将shared_ptr放于容器中,而后不再需要全部元素,而只是使用其中一部分,要记得用erase删除不再需要的那些元素。

(4)使用动态内存的原因:

- 程序不知道自己需要多少对象。

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

- 允许多个对象共享相同的状态。

(5)使用shared_ptr的一个例子:

#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class StrBlob
{
public:
typedef vector<string>::size_type st;
StrBlob():data(make_shared<vector<string>>()){}
StrBlob(initializer_list<string> il):data(make_shared<vector<string>>()){}
inline st size() const { return data->size(); }
inline bool empty() const { return data->empty(); }
inline void push_back(const string &t) { data->push_back(t); }
void pop_back();
const string & front();
const string & back();
private:
shared_ptr<vector<string>> data;
void check(st i,const string &msg) const;
};
void StrBlob::check(st i,const string &msg) const
{
if(i >= data->size())
throw out_of_range(msg);
}
const string & StrBlob::front()
{
StrBlob::check(0,"front on empty StrBlob");
return data->front();
}
const string & StrBlob::back()
{
StrBlob::check(0,"back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
StrBlob::check(0,"pop_back on empty StrBlob");
return data->pop_back();
}
int main(int argc, char const *argv[])
{
StrBlob b1;
StrBlob b2 = {"a","an","the"};
b1 = b2;
b2.push_back("about");
cout << b1.size() << endl;
cout << b2.size() << endl;
return 0;
return 0;
}


2、shared_ptr的实现和循环引用问题

template<class T>
class Dele
{
public:
void operator()(T* ptr)
{
delete ptr;
ptr=NULL;
}
};

template<class T>
class Free
{
public:
void operator()(T* ptr)
{
free(ptr);
ptr=NULL;
}
};

template<class T>
class File
{
public:
void operator()(T* ptr)
{
fclose(ptr);
}
};


template<class T,class Des=Dele<T>>
class SharedPtr
{
public:
SharedPtr(T* ptr=NULL)
:_ptr(ptr)
,count(0)
{
if(_ptr)
{
count=new int(1);
}
}

SharedPtr(const SharedPtr& sp)
{
_ptr=sp._ptr;
count=sp.count;
(*count)++;
}

SharedPtr& operator=(const SharedPtr& sp)
{
if(this!=&sp)
{
if(_ptr&&!(--*count))
{
Destory();
}
_ptr=sp._ptr;
count=sp.count;
(*count)++;
}
return *this;
}

~SharedPtr()
{
if(_ptr&&!(--(*count)))
{
Destory();
}
}

private:
void Destory()
{
Des()(_ptr);
delete count;
}

private:
T* _ptr;
int* count;
};


  这里对于shared_ptr是利用了之前string类用过的引用计数,除了封装一个原生指针外还封装了一个int*的指针,当然标准库当中肯定没这么简单,上面只是简单的进行了模拟大致的方法,仅对于上述的模拟而言,shared_ptr增加了一个引用计数空间用于保存当前管理这块原生指针指向的空间的对象有多少,而根据这个引用计数来决定在析构某一对象的时候要不要释放空间;而除此之外这里还解决了一个问题,就是对于不同的指针而言,最后进行的处理是不同的,例如文件指针需要进行关闭文件,malloc出来的空间需要的是进行free而不是delete等,上述模拟根据这一问题对于不同的指针设计了对应的删除器来进行解决。

  但是这里还有一个严重的问题,就是关于循环引用的问题。对于什么是循环引用?我们用下面这个测试用例来解释:

#include <iostream>
using namespace std;

#include <memory>
#include "SharedPtr.h"

struct Node
{
Node(int va)
:value(va)

{
cout<<"Node()"<<endl;
}

~Node()
{
cout<<"~Node()"<<endl;
}
shared_ptr<Node> _pre;
shared_ptr<Node> _next;
int value;
};

void funtest()
{
shared_ptr<Node> sp1(new Node(1));
shared_ptr<Node> sp2(new Node(2));

sp1->_next=sp2;
sp2->_pre=sp1;
}
int main()
{
funtest();
return 0;
}


  这里模拟了双向链表中的两个节点的情况,对于节点中的_pre和_next都用shared_ptr来进行管理,而节点本身也由shared_ptr进行管理,这样的话当funtest()运行结束后,我们会发现并没有调用~Node(),也就是说节点在这里并没有被释放,这里原因在于有两个shared_ptr的对象管理同一个节点,而其中的一个对象就是另一个节点当中的_pre或是_next,当你对sp2进行释放的时候,由于引用计数为2,所以这里并不能把对应节点给释放掉,而是要等待对方也就是sp1的_next释放时才能将sp2的节点释放掉,反过来对于sp1与sp2同理,也要等待sp2中的_pre释放,所以这里两者都未能释放。因此,在这里标准库就引用了weak_ptr,将上面的_pre和_next的类型换成weak_ptr,由于weak_ptr并不会增加引用计数use的值,所以这里就能够打破shared_ptr所造成的循环引用问题。但是这里要注意一点,就是weak_ptr并不能单独用来管理空间。

3、weak_ptr类

  根据上面的测试用例,我们知道sp1,sp2,_pre,_next均是shared_ptr的对象,所以上图中两个引用计数空间中的use均为2,weak均为1,而在出funtest()的作用域之前,会对栈空间上的变量进行销毁释放,也就是说在这里,会对sp1和sp2这两个对象进行释放,调用它们的析构函数,但由于在shared_ptr的析构函数中,只有当use=1,进行减减之后为0,才会释放_ptr所指向的空间,所以在这里sp1和sp2所管理的节点空间是不会被释放的,因此也不会调用~Node()这个析构函数,所以这里就出现了上篇文章末尾所出现的问题,也就是内存泄漏。

  由于在shared_ptr单独使用的时候会出现循环引用的问题,造成内存泄漏,所以标准库又从boost库当中引入了weak_ptr。对上面的测试用例进行修改:

#include <iostream>
using namespace std;

#include <memory>
#include "SharedPtr.h"

struct Node
{
Node(int va)
:value(va)

{
cout<<"Node()"<<endl;
}

~Node()
{
cout<<"~Node()"<<endl;
}
weak_ptr<Node> _pre;
weak_ptr<Node> _next;
int value;
};

void funtest()
{
shared_ptr<Node> sp1(new Node(1));
shared_ptr<Node> sp2(new Node(2));

sp1->_next=sp2;
sp2->_pre=sp1;
}
int main()
{
funtest();
return 0;
}


  其实也并没有做太大的修改,只是将其中的_pre和_next的类型换成weak_ptr,当这个测试用例运行到sp2->_pre=sp1结束后,节点之间的相互关系与上图基本无差别,但是这里有一点发生了变化,就是由于每个节点仅有一个shared_ptr的对象管理,这里两块引用计数空间中的use均为1,而由于多了weak_ptr的对象_pre或_next指向,这里weak的值就变成了2,因此根据前面的讨论,两个节点空间就成功释放了



  weak_ptr是一种不控制所指向对象生存周期的智能指针,他指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。

auto p = make_shared<int>(10); //使用shared_ptr来初始化。
weak_ptr<int>wp(p);

if(shared_ptr<int> np = wp.lock())//访问对象必须调用lock()
{
//使用np访问共享对象
}


4、unique_ptr类

  unique_ptr 独占智能指针,某个时刻只能有一个unique_ptr 指向一个给定对象。当unique_ptr 被销毁时,它所指向的对象也被销毁。unique_ptr 不支持拷贝赋值等操作,除非这个unique_ptr将要被销毁,这种情况,编译器执行一种特殊的”拷贝”

unique_ptr<int> p1(new int(42));//必须直接初始化。
unique_ptr<int> p2(p1);//error
unique_ptr<int> p3 = p1;/error

unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int(p));
return ret; //ok
}




  虽然不能拷贝或者赋值unique_ptr,但是通过调用release或者reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。调用release会切断unique_ptr和它原来管理对象间的联系。release返回的指针通常用来初始化另一个智能指针或者给另一个智能指针赋值。

#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> clone(int p);
int main(int argc, char const *argv[])
{
unique_ptr<double> p;//
unique_ptr<string> p1(new string("ABC"));//使用new返回的指针初始化。
cout << *p1 << endl;

unique_ptr<string> p2(p1.release());//放弃对p1的控制权,返回指针并置空,然后初始化另一个指针。
cout << *p2 << endl;

unique_ptr<string> p3(new string("abc"));
p2.reset(p3.release());//释放p2的对象,并将p3的所有权转移给p2。
cout << *p2 << endl;

cout << *clone(10) << endl;
return 0;
}
unique_ptr<int> clone(int p)
{
return unique_ptr<int>(new int(p));//unique_ptr不能拷贝或者赋值,但是可以返回一个unique_ptr。
}


5、unique_ptr类为何优于auto_ptr类

  可能大家认为前面的例子已经说明了unique_ptr为何优于auto_ptr,也就是安全问题,下面再叙述的清晰一点。请看下面的语句:

auto_ptr<string> p1(new string ("auto") ; //#1
auto_ptr<string> p2;                       //#2
p2 = p1;


  在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象;但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。下面来看使用unique_ptr的情况:

unique_ptr<string> p3 (new string ("auto");   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;


  编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。但unique_ptr还有更聪明的地方。有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

unique_ptr<string> demo(const char * s)
{
unique_ptr<string> temp (new string (s));
return temp;
}

unique_ptr<string> ps;
ps = demo('Uniquely special");


  demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。

  总之,当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed


  其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

6、使用智能指针需要注意的问题:



7、如何选择智能指针?

  在掌握了这几种智能指针后,应使用哪种智能指针呢?

- 如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。

- 如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。

三、动态数组和allocator类

1、动态数组

(1)使用new和delete管理动态内存三个常见的问题:

a、忘记释放(delete)内存。

  忘记释放动态内存会导致人们常说的 “内存泄漏(memory leak)” 问题 ,因为这种内存永远不可能归还系统,除非程序退出。比如在某个作用域的代码如下:向系统申请了一块内存,离开作用域之前没有接管用户这块内存,也没有释放这块内存。

{
//....
int *p = new int(0);
//....
}


  有两个方法可以避免以上问题:

1) 在p离开它new所在作用域之前,释放这块内存。如:delete p

{
//....
int *p = new int(0);
//....
delete p;      //释放p的向系统申请的内存
p = nullptr;   //避免出现野指针。
}


2) 接管p的向系统申请的内存。 比如通过赋值,函数返回值等。

int *pAnother;
{
//....
int *p = new int(0);
//....
pAnother = p; //pAnother接管p所指向的内存。
}
//pAnother  do something
delete pAnother;   //通关pAnother,将p所申请的内存归还系统。


b、使用已经释放内存的对象。

  这种行为是未定义的,通过在释放内存后将指针设置位空指针(nullptr),有时可以避免这个问题(这是基于一个前提条件,使用动态分配内存对象前,需要检查该对象是否指向空(nullptr))。假如不对已经释放内存的对象赋值空指针,他的值是未定义的,就好比其他变量,使用未初始化的对象,其行为大都是未定义。

  nullptr(C++11刚引入)是一种特殊类型的字面值,它可以被转换成任何其他指针类型。过去程序使用NULL的预处理变量来给指针赋值。 他们的值都是0。

  使用已经释放内存的对象,如下代码:

{
int *p = new int(0);
delete p;
std::cout<<*p<<std::endl; //*p的值是未定义
}


  避免以上问题:(对已经释放内存对象赋于一个空指针,使用前进行判断是否为空指针)

{
int *p = new int(0);
delete p;
p = nullptr;
if(p!=nullptr)
std::cout<<*p<<std::endl;
}


  同样当我们定义一个指针时,如果没有立即为它分配内存,也需要将指针设置为空指针,防止不恰当使用。这里也涉及一个问题,new出来的内存也应该初始化。

c、同一块内存释放两次。

  当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个对象进行了delete操作,对象的内存就归还给系统,如果我们随后有delete第二个指针,堆空间可能被破坏。产生问题代码:

int *pAnother;
{
//....
int *p = new int(0);
pAnother =p;
//p do something....
delete p;
}
delete pAnother;  //未定义行为


  避免这个问题:在delete p 之后, 将p置为一个空指针。

  其次明白一个道理:delete p, p 必须指向一个空指针或者动态分配的内存,否则其行为未定义。这也很好就解释了为什么delete一个对象之后需要将该对象置为空指针,一是为了避免再次访问它出现未定义行为,二是为了避免再次delete它出现未定义行为。

  【Note】:

1)定义一个指针需要初始化为空指针,(除非在定义的时候给它申请一块内存)。

2)访问一个指针需要先判断该指针是否为空指针。

3)释放一个指针之后,应该将它置为空指针,防止出现野指针。

(2)动态数组的一个例子

#include <iostream>
#include <memory>
using namespace std;
int main(int argc, char const *argv[])
{
int *p = new int[10];
delete [] p;

unique_ptr<int []> up(new int[10]);//使用unique_ptr管理动态数组。
up.release();

//动态申请高维数组。
int row = 3;
int col = 5;
//先申请int* 型数组的的空间,该空间存放的是int*型的指针变量,然后把这块指针变量的空间首地址赋给array。
int** array=new int* [row];
for(int i=0 ; i<row ; ++i)
{
array[i] = new int[col];//array[i]仍然是指针。
}
for(int i=0 ; i<row ; ++i)
{
delete[] array[i];
}
delete[] array;

return 0;
}


2、allocator类





  allocator类帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

#include <iostream>
#include <memory>
#include <string>
using namespace std;
int main(int argc, char const *argv[])
{
const int n = 10;
allocator<string> alloc;
auto p = alloc.allocate(n);//只分配内存。
string s;
auto q = p;
while(cin >> s && q != p+n)
{
alloc.construct(q++,s);//必须调用construct创建对象并赋值。
}
while(q != p)
{
alloc.destroy(--q);//逐个销毁对象,destory接受一个指针。
}
alloc.deallocate(p,n);//分配多少内存,释放多少。
system("pause");
return 0;
}


参考:https://www.cnblogs.com/jiayy/p/3420122.html

http://blog.csdn.net/chenkaixin_1024/article/details/69390586

http://blog.csdn.net/lanxuezaipiao/article/details/41603883

http://blog.csdn.net/qq_33850438/article/details/52994314

http://makaidong.com/libin1105/1/1903_12244995.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: