首页 > 技术文章 > 动态内存

Joezzz 2018-11-20 11:20 原文

1.new

  new操作符做两件事:分配内存+调用构造函数初始化对象

1.1operator new

  operator new是new操作符用来分配内存的函数,可以被重载

//原型
void * operator new(size_t size);

//使用示例
void *rawMemory = operator new(sizeof(string));

1.2placement new

  placement new是一个重载版本的operator new,它用于在一块已经被分配但是尚未处理的的(raw)内存中构造一个对象的情况

class Widget {
 public:
  Widget(int x);
  ...
};

//buffer是一块已分配好但是尚未处理的内存
Widget * constructWidgetInBuffer(void *buffer,int x)
{
 return new (buffer) Widget(x);
}

  在constructWidgetInBuffer里面,返回的表达式是:  new (buffer) Widget(x)

  这是new操作符的一个使用方法,须要使用一个额外的变量(buffer)。当new操作符隐式调用operator new函数时,把这个变量传递给它,被调用的operator new函数除了带有强制的參数size_t外,还必须接受void*指针參数,指向构造对象占用的内存空间。我们把这个重载版本的operator new安了一个名字,叫作placement new,它看上去像下面这样,什么也不做就直接返回那块已经分配好的地址

void* operator new(size_t, void *location)
{
    return location;
}

2.delete

  delete操作符做两件事:调用析构函数销毁对象+释放内

1)传递给delete的指针必须指向动态内存,或者是一个空指针,否则其行为是未定义的,编译不发生错误,但是运行会发生错误

2.1operator delete

  operator delete是delete操作符用来释放内存的函数,也可以被重载

//原型
void operator delete(void *memoryToBeDeallocated);

//string *ps; delete ps;的具体过程
ps->~string(); // 调用析构函数
operator delete(ps); // 使用operator delete释放对象的内存

3.placement new建立的对象的释放问题

  假设用placement new在内存中建立了对象,就应该避免在该内存中用delete操作符。因为delete操作符调用operator delete来释放内存,可是对象的内存最初不是被operator new分配的。

void * mallocShared(size_t size);//在共享内存中分配内存的函数
void freeShared(void *memory);//在共享内存中释放内存的函数

void* mm = mallocShared(sizeof(Widget));
Widget *pw =constructWidgetInBuffer(mm, 10); // 使用placement new 

delete pw; //错误!!!内存来自于mallocShared, 而不是operator new

//正确,先调用析构函数,再用freeShared释放共享内存
pw->~Widget();
freeShared(pw); 

4.动态内存的问题

1)内存泄漏:动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

解决:

  1. 良好的编码习惯
  2. 重载new和delete:

   C++中自带的new/delete本身没有提供内存泄漏检测的功能,不过我们可以重载这两个函数来追踪内存的分配和释放,以此来检测程序是否出现了内存泄漏。算法的思想是在new一块内存时将该块内存的地址以及出现的文件和行号信息记录到一个map数据结构中,以内存地址作为key。释放该内存时将map中的该记录删除,最后程序退出时遍历map,从map中得到那些没有被释放的内存信息,并释放

         3.使用智能指针(见下)

2)linux下检测和定位内存泄露的方法:

  • 可以下载Valgrind工具来检测和定位内存泄露,可以精确指出在源代码中的哪一行发生了内存泄漏
  • linux自带的mtrace命令,只能检测出是否发生内存泄漏以及泄漏的内存地址,不能定位代码行

5.智能指针(c++11)

1)为了更安全地使用动态内存,提出shared_ptr、unique_ptr、weak_ptr三种智能指针,三种类都定义在<memory>中

2)从c++11开始, auto_ptr已经被标记为弃用, 常见的替代品为shared_ptr,shared_ptr的不同之处在于引用计数,在复制(或赋值)时不会像auto_ptr那样直接转移所有权,auto_ptr对资源的拥有者只能有一个,当两个auto_ptr进行等于号(赋值)操作时,等于号后面的auto_ptr将失去资源的所有权

6.shared_ptr类

6.1概念

  允许多个shared_ptr指针指向(共享)同一个对象

6.2声明

shared_ptr<int> p1;//指向int的shared_ptr
shared_ptr<string> p2;//指向string的shared_ptr

6.3初始化

1)接受一个普通指针参数的shared_ptr的构造函数是explicit的,必须使用直接初始化

2)make_share函数产生一个智能指针shared_ptr,接受一个智能指针shared_ptr参数的构造函数不是explicit的,可以用"="进行拷贝初始化

//make_shared函数:此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);//这里可以用等号
auto p4 = make_shared<string>(10, '9');

//shared_ptr和new结合使用
shared_ptr<int> p5(new int(42));//一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针使用delete来释放它关联的对象
shared_ptr<int> p6 = new int(42);//错误!!!接受内置指针参数的shared_ptr的构造函数是explicit的,必须使用直接初始化

6.4引用计数

1)每个shared_ptr都有一个相关联的计数器,称为引用计数

2)引用计数递增的情况:拷贝一个shared_ptr、用一个shared_ptr初始化另一个shared_ptr、函数传参、shared_ptr作为函数的返回值

shared_ptr<int> p = make_shared<int>(42);//p指向的对象的引用计数为1
auto q(p);//p和q指向相同的对象,引用计数为2

3)引用计数递减的情况:给shared_ptr赋新值、shared_ptr离开其作用域

shared_ptr<int> p = make_shared<int>(42);//p的引用计数为1
auto r = make_shared<int>(20);//r的引用计数为1
r = p;//r指向一个新的地址,递增p指向的对象的引用计数,递减r原来指向的对象的引用计数,r原来指向的对象已经没有引用者了,会自动释放

4)shared_ptr的析构函数会递减它所指向对象的引用计数,当引用计数变为0时,析构函数还会调用delete销毁对象并释放内存

5)use_count()可返回共享对象的shared_ptr的数量,不包括普通指针的数量,智能指针和普通指针最好不要混用

6.5删除器

1)shared_ptr被销毁时,默认使用delete;我们可以使用删除器来自定义释放操作

struct destination;
struct connection;
connection connect(destination*);//函数:建立连接
void disconnect(connection);//函数:关闭给定的连接
void fun1(destination& d)
{
    connection c = connect(&d);//建立一个连接c

    //使用连接c
    ……

    //如果在fun退出前忘记调用disconnect,就再也无法关闭c了
}

/*使用shared_ptr,并定义删除器*/
//删除器
void end_connection(connection* p)
{
    disconnect(*p);
}

void fun2(destination& d)
{
    connection c = connect(&d);//建立一个连接c
    shared_ptr<connection> p(&c, end_connection);
    //使用连接c
    ……

    //即使在fun退出前忘记调用disconnect,c也会被正确关闭:因为智能指针p将离开它的作用域,将会调用删除器
}

7.unique_ptr类

1)unique_ptr“独占”所指向的对象

2)unique_ptr不支持普通的拷贝和赋值

unique_ptr<int> p1(new int(42));
unique_ptr<int> p2(p1);//错误!!!
unique_ptr<int> p3;
p3 = p1;//错误!!!

3)可以通过release()或reset()实现“拷贝和赋值”(所有权的转移)

//p.release():p放弃对对象的控制权,返回指针,并将p置为NULL,注意不会释放内存
//p.reset():释放p指向的内存,p被置为NULL
//p.reset(q):释放p指向的内存,令p指向新的对象
unique_ptr<int> p1(new int(42));
unique_ptr<int> p2(p1.release());//p1的控制权转移给了p2,p1被置为NULL
unique_ptr<int> p3(new int(22));
p3.reset(p2.release());//释放p3原来指向的内存,令p3指向新的对象

unique_ptr<int> p4(new int(12));
p4.release();//错误!!!release()不会释放内存,而且现在丢失了指针
unique_ptr<int> p5(p4.release());//正确,且p5能够负责资源的释放
int* p6= p4.release();//正确,但是必须记得delete p6
delete p6;

4)unique_ptr拷贝和赋值的例外:可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的例子是从函数返回一个unique_ptr

//编译器知道p将要被销毁,将执行一种特殊的“拷贝”:移动
unique_ptr<int> clone(int x)
{
    unique_ptr<int> p(new int(x));
    return p;
}

5)自定义unique_ptr的删除器会影响到unique_ptr对象的构造,所以必须在尖括号中提供删除器的类型(一个函数指针)

/*使用unique_ptr,并定义删除器*/
//删除器
void end_connection(connection* p)
{
    disconnect(*p);
}

void fun2(destination& d)
{
    connection c = connect(&d);//建立一个连接c
    unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);//decltype(end_connection)返回函数类型,必须添加一个*来指明使用该函数类型的指针
    //使用连接c
    ……

    //即使在fun退出前忘记调用disconnect,c也会被正确关闭:因为智能指针p将离开它的作用域,将会调用删除器
}

8.weak_ptr类

1)weak_ptr只能绑定到shared_ptr所指向的对象,并且不会改变shared_ptr的计数引用

2)weak_ptr必须用shared_ptr来初始化

weak_ptr<int> wp1(make_shared<int>(42));
auto p = make_shared<int>(42);
weak_ptr<int> wp2(p);

3)即使有weak_ptr指向某个对象,但是当shared_ptr的计数引用变为0的时候,这个对象还是会被释放,所以使用weak_ptr访问对象时先调用lock()

//w.lock():若对象存在,返回指向w对象的shared_ptr,否则返回NULL
if (wp2.lock())
{
    cout << 666 << endl;
}

4)wp.use_count()返回与wp共享对象的shared_ptr的数量

9.空悬指针(dangling pointer)

1)空悬指针曾指向一个正常的对象,但是现在对象销毁了,而指针未置为NULL,就成了悬空指针

2)使用空悬指针会发生段错误

10.野指针

1)没有被初始化的指针,其内容为一个垃圾数,

2)使用野指针会发生段错误

11.动态数组

11.1声明和初始化

int x = 5;
int* pia = new int[x];//方括号中不必是常量表达式;

//可以用空的圆括号初始化(不能在圆括号里给初始化器),也可以用花括号进行列表初始化
int* pia1 = new int[x]();//5个值为0的元素
int* pia2 = new int[x] {1, 2, 3, 4, 5};

11.2动态数组不是数组

  用new分配一个数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针,所以和静态数组不同,动态分配一个空数组是合法的

int a[0];//错误!!!
int* pia = new int[0];//方括号中可以为0

11.3释放动态数组

1)一定要在delete后面加上[],来释放动态数组,否则将发生内存泄漏:没有[]的话,只有数组的第一个元素的析构函数得到执行并回收了内存占用,数组的其他元素所占内存得不到回收,导致内存泄露

int* pia = new int[5];
delete[] pia;//按逆序销毁数组中的元素

11.4智能指针和动态数组

  unique_ptr直接支持管理动态数组,shared_ptr管理动态数组需要自定义删除器

unique_ptr<int[]> up(new int[10]);
up.release();//当unique_ptr指向一个数组时,release()会调用delete[]释放内存
for (int i = 0; i < 10; ++i)//虽然智能指针,但是指向的是一个数组,不能用点和箭头运算符访问元素
    up[i] = i;

//使用shared_ptr管理动态数组,必须提供删除器,因为默认情况下shared_ptr使用delete销毁对象而不是delete[]
shared_ptr<int> sp(new int[10], [](int* p) {delete[] p; });
for (int i = 0; i < 10; ++i)//shared_ptr不支持下标运算符,只能通过get()获取一个内置指针来访问元素
    *(sp.get() + i) = i;

12.allocator类

  allocator将内存分配对象构造分离开来

allocator<string> ad;//ad可以分配string
int n = 3;
auto q = ad.allocate(n);//分配了3个未初始化的string
auto p = q;
ad.construct(p++);
ad.construct(p++, 3, 'c');
ad.construct(p, "abc");
ad.destroy(p--);//析构"abc"
ad.destroy(p--);//析构"ccc"
ad.destroy(p);//析构""
ad.deallocate(p, n);//释放分配的内存

13.用模板实现智能指针

template <typename T>
class SmartPointer {
public:
    //构造函数
    SmartPointer(T* p = nullptr) : myPtr(p), count(new unsigned int())//nullptr是默认值
    {
        if (p)
            *count = 1;
        else
            *count = 0;
    }

    ~SmartPointer()
    {
        releaseCount();
    }

    // 拷贝构造
    SmartPointer(const SmartPointer& rhs)
    {
        myPtr = rhs.myPtr;
        count = rhs.count;
        (*count)++;
    }

    // 移动构造
    SmartPointer(SmartPointer && rhs) noexcept
        : myPtr(rhs.myPtr), count(rhs.count)
    {
        rhs.myPtr = nullptr;
        rhs.count = nullptr;
    }

    // 拷贝赋值运算符
    SmartPointer& operator = (const SmartPointer& rhs)
    {
        if (this != &rhs)
        {
            releaseCount(); // release the current pointer.
            myPtr = rhs.myPtr;
            count = rhs.count;
            (*count)++;
        }
        return *this;
    }

    // 移动赋值运算符
    SmartPointer& operator = (SmartPointer && rhs) noexcept
    {
        if (this != &rhs)
        {
            myPtr=rhs.myPtr;
            count=rhs.count;
            rhs.myPtr = nullptr;
            rhs.count = nullptr;
        }
        return *this;
    }

    T& operator *() const    //重载*操作符,返回一个引用
    {
        return *myPtr;
    }

    T* operator ->() const    //重载->操作符,返回一个指针,因为指针的值是一个地址,可以根据这个地址找到在这个地址上的数据成员
    {
        return myPtr;
    }

    void printCount()
    {
        cout << *count << endl;
    }

private:
    void releaseCount()
    {
        if (count)//指针存在
        {
            --(*count);
            cout << "调用析构函数减小引用计数,现在引用计数为:" << (*count) << endl;
            if (*count == 0)
            {
                delete myPtr;
                delete count;
                cout << "因为引用计数为0,所以要释放内存" << endl;
            }
        }
    }


    //
private:
    T* myPtr;
    unsigned int* count;//引用计数,一定要用指针,因为发生拷贝后计数引用要+1,对那个拷贝中的count也要生效
};

int main()
{
    SmartPointer<int> i (new int(3));//调用构造函数
    SmartPointer<int> j = new int(15);//这个拷贝构造函数不是explicit的,所以可以用"="调用拷贝构造函数
    i.printCount();//输出:1
    j.printCount();//输出:1
    cout << *i << endl;//输出:3
    cout << *j << endl;//输出:15

    //把i赋值给j,那么指向15的引用计数减小变为0,指向3的引用计数变为2
    j = i;//输出:调用析构函数减小引用计数,现在引用计数为:0
                //因为引用计数为0,所以要释放内存
    cout << *j << endl;//输出:3
    i.printCount();//输出:2
    j.printCount();//输出:2,因为现在j也指向3

    return 0;
}

运行结果:

14.new/delete和malloc/free的区别

1)属性:new/delete是C++关键字;而malloc/free是库函数,需要头文件支持

2)参数:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算;而malloc则需要显式地指出所需内存的字节数

3)返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型

4)分配失败:new内存分配失败时,会抛出bac_alloc异常;malloc分配内存失败时返回NULL

5)实现过程: new会先调用operator new函数申请内存(通常底层使用malloc实现),然后调用对象的构造函数完成对象的构造。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现);而malloc/free则不会调用对象的构造函数/析构函数

6)能否重载:C++允许重载new/delete操作符,如:placement new就是一个重载版本;而malloc不允许重载

7)申请/释放内存的区域:new从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行申请的内存就属于自由存储区。而堆是操作系统中的术语,用于程序的动态内存的分配;自由存储区不等价于堆,但可以是堆,这取决于operator new 的实现细节,当operator new使用malloc申请内存时,自由存储区是堆

8)对数组的处理:c++提供了new[]与delete[]来专门处理数组类型;而malloc只是给你一块原始的内存,并不关心你要放的是数据还是别的啥

15.malloc()和free()的原理

15.1malloc()

1)malloc()申请的内存来自于堆,受物理内存容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。linux维护一个break指针,从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错;每个进程有一个rlimit表示当前进程可用的资源上限,rlimit可以通过getrlimit()函数查看,并且可以通过setrlimit对rlimit进行有条件设置

2)要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动,可通过brk()函数和sbrk()函数来移动break指针

int brk(void* addr); //brk()将break指针直接设置为某个地址
void* sbrk(int ptr_tincrement);//sbrk()将break从当前位置移动increment所指定的增量

3)堆把内存空间分成了内存块,这些内存块被挂到了一个链表上,每个内存块由meta区数据区组成,meta区记录内存块的信息(数据区大小、空闲标志位、next指针等等),数据区是真实分配的内存区域,数据区的第1个字节地址即为malloc()返回的地址

typedef struct s_block* t_block;
struct s_block
{
    size_tsize;  /* 数据区大小 */
    t_block next;/* 指向下个块的指针 */
    int free;    /* 是否是空闲块 */
    int padding;  /* 填充4字节,保证meta块长度为8的倍数 */
    char data[1]  /* 这是一个虚拟字段,表示数据区的第一个字节 */
};

4)调用malloc()申请内存时,malloc()可以按照两种算法去链表上找内存块

  • First fit:从头开始,使用第一个数据区大于等于要求的size的块所谓此次分配的块
  • Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块

两种方法各有千秋,best fit具有较高的内存使用率,而first fit具有更高的运行效率

/* First fit */
t_block find_block(t_block* last, size_t size) 
{
    t_block b = first_block;//first_block表示堆中第一个内存块
    while (b) 
    {
        if (b->free && b->size >= size)//空闲标志位为1且数据区大小大于等于要求的size就停止查找
            break;
        *last = b;
        b = b->next;
    }
    return b;
}

5)如果现有block都不能满足size的要求,则需要再开辟一个新的block,其实是先调用sbrk()函数使break指针移动一定的增量,让新增加的堆内存成为新的内存块

#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */

t_block extend_heap(t_block last, size_t s) 
{
    t_block b;
    b = sbrk(0);
    if (sbrk(BLOCK_SIZE + s) == (void*)-1)
        return NULL;
    b->size = s;
    b->next = NULL;
    if (last)
        last->next = b;
    b->free = 0;
    return b;
}

6)分裂内存块:First fit有一个比较致命的缺点,就是很可能会在申请很小的size时选择了很大的一块内存块,此时,为了提高内存利用率,应该在剩余数据区足够大的情况下,将这个内存块分裂成两个内存块

#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */

void split_block(t_block b, size_t s) 
{
    t_block new_block;
    new_block = b->data + s;
    new_block->size = b->size - s - BLOCK_SIZE;
    new_block->next = b->next;
    new_block->free = 1;
    b->size = s;
    b->next = new_block;
}

7)malloc()源码

#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */
void* first_block = NULL;

/* other functions... */

void *malloc(size_t size) 
{
    t_block b, last;
    size_t s;
    /* 对齐地址 */
    s = align8(size);
    if (first_block) 
    {
        /* 查找合适的block */
        last = first_block;
        b = find_block(&last, s);//使用first fit算法或best fit算法
        if (b) 
        {
            /* 如果可以,则分裂 */
            if ((b->size - s) >= (BLOCK_SIZE + 8))
                split_block(b, s);
            b->free = 0;
        }
        else 
        {
            /* 没有合适的block,开辟一个新的 */
            b = extend_heap(last, s);
            if (!b)
                return NULL;
        }
    }
    else {
        b = extend_heap(NULL, s);
        if (!b)
            return NULL;
        first_block = b;
    }
    return b->data;
}

15.2free()

1)free()会先检查传入地址的合法性,如果不合法,则在程序运行时会发生错误

  • 地址应该在之前malloc所分配的区域内,即在first_block和当前break指针范围内
  • 这个地址确实是之前通过我们自己的malloc分配的

2)如果合法,将此内存块的空闲标志位free置为1,并且为了减少外碎片,如果后面的内存为空闲,那么将两个块进行合并

3)free()源码

#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */

t_block fusion(t_block b) //合并内存块
{
    if (b->next&& b->next->free) 
    {
        b->size += BLOCK_SIZE + b->next->size;
        b->next = b->next->next;
        if (b->next)
            b->next->prev = b;
    }
    return b;
}

void free(void*p) 
{
    t_block b;
    if (valid_addr(p)) 
    {
        b = get_block(p);
        b->free = 1;
        if (b->prev&& b->prev->free)
            b = fusion(b->prev);
        if (b->next)
            fusion(b);
        else 
        {
            if (b->prev)
                b->prev->prev = NULL;
            else
                first_block = NULL;
            brk(b);
        }
    }
}

16.循环引用

参考资料:

https://blog.csdn.net/zhwenx3/article/details/82789537

https://blog.csdn.net/jfkidear/article/details/9034455

https://blog.csdn.net/daniel_ustc/article/details/23096229

https://www.cnblogs.com/slgkaifa/p/6887887.html

推荐阅读