菜鸟笔记
提升您的技术认知

C++动态内存管理

1.内存布局

1.0.概述

 

一个可执行程序文件需要在计算机硬件上运行起来,
其实质就是静态的文件被加载到内存中的过程,
可执行程序文件只是一个程序的载体。
那么执行一个应用后,
它在内存中是一个怎样的结构呢?
一个程序被加载到内存中,这块内存首先就存在两种属性:静态分配内存和动态分配内存。 
静态分配内存:是在程序编译和链接时就确定好的内存。 
动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存。
用下面三幅图就可以说明白了

 

下面是针对用户态的一个表示

1.1.讲解

 

1.1.1栈区stack

存放局部变量、临时变量、声明、返回值、指向堆对象的地址(指针)、总之存放一些小的东西、当不需要时候、栈会自动清除、比如一个加法方法里面、声明了两个int并赋值、这两个就是放在栈里面、类里面的8个基本变量常量类型、声明赋值以后都会在栈里、其他的放在堆里。注:(byte、short、int、long、char、float、double、boolean) 
栈,存放Automatic Variables,按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。保存程序中的局部变量(也就是在代码块中的变量)这样的变量伴随着函数的调用和终止,在内存中也相应的增加或者减少。这样的变量在创建时期按顺序加入,在消亡的时候按相反的顺序移除。 

1.1.2 堆区heap

存放new出来的对象、栈里面所有对象都是在堆里面有指向的、假如栈里指向堆的指针被删除、堆里的对象也要释放(C++需要手动释放)、当然我们现在好面向对象程序都有'垃圾回收机制'、会定期的把堆里没用的对象清除出去。 

堆,自由申请的空间,按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。 动态分配的内存在调用malloc()或者相关函数产生,在调用free()时释放,由程序员而不是一系列固定的规则内存持续时间,因此内存块可在一个函数中创建,在另一个函数中释放。由于这点,动态内存分配所使用的部分可能就碎片,也就是说:在活动的内存块之间散布着未使用的内存片。动态分配内存往往要比栈分配的内存慢。

每个线程都会有自己的栈,但是堆空间是共用的

问题:堆栈的不同点?

主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改: 打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。 注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。 从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。 无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:) 对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,

1.1.3 bss段

就是存放未初始化的全局变量与未初始化的静态变量。 

1.1.4 数据段gvar

 数据段存放已初始化的全局变量和静态变量。

1.1.5 代码段text

存放程序执行代码和字符串常量的一块内存区域,
里面是二进制的代码、内存区域通常属于只读, 某些架构也允许代码段为可写,
其实就是存放函数体、代码体的地方。 
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

text、data(gvar)、bss 在内存中地址较低低的位置,而堆栈则在相对较搞高的位置。

堆(Heap)往高地址方向生长,栈(Stack)往低地址方向生长。

1.2.例子

//main.cpp
 
int a = 0;   // 数据段
 
char *p1; // BSS段
 
main()
{
int b; // 栈
char s[] = "abc";//  栈
char *p2;//  栈
char *p3 = "123456"; // 123456\0在常量区,p3在栈上。
static int c =0; // BSS段
Class c1 = new Class();//new出的对象就在堆区
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
 
}

2.动态内存管理

手动管理----new/delete/new[]/delete[]




有关问题 

1,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free? 
从编程纪律、规则上说,是不可以。
因为 new 和delete 操作时, 是把对象当成一个复杂类,而执行对应的构造/析构函数。 
而 malloc和free则不执行它们。会导致不可预测的错误发生,比如内存泄漏等
但是当对象是简单类型时,确实是不会出错。

2.new和malloc的区别?
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是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
3.malloc与free的实现原理?
1、在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、,munmap这些系统调用实现的;
2、brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;
3、malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。
4、malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
4.malloc、realloc、calloc的区别
1)malloc函数
void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));申请20个int类型的空间;
2)calloc函数
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;
3)realloc函数
void realloc(void *p, size_t new_size);
给动态分配的空间分配额外的空间,用于扩充容量。
10.__stdcall和__cdecl的区别?
1)__stdcall
__stdcall是函数恢复堆栈,只有在函数代码的结尾出现一次恢复堆栈的代码;在编译时就规定了参数个数,无法实现不定个数的参数调用;
2)__cdecl
__cdecl是调用者恢复堆栈,假设有100个函数调用函数a,那么内存中就有100端恢复堆栈的代码;可以不定参数个数;每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用__stacall函数大。
5.内存泄漏?
1)内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制;
2)后果
只发生一次小的内存泄漏可能不被注意,但泄漏大量内存的程序将会出现各种证照:性能下降到内存逐渐用完,导致另一个程序失败;
3)如何排除
使用工具软件BoundsChecker,BoundsChecker是一个运行时错误检测工具,它主要定位程序运行时期发生的各种错误;
4)解决方法
智能指针。
5)检查、定位内存泄漏
检查方法:在main函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出,查看输出:
输出这样的格式{453}normal block at 0x02432CA8,868 bytes long
被{}包围的453就是我们需要的内存泄漏定位值,868 bytes long就是说这个地方有868比特内存没有释放。
定位代码位置
在main函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请453这块内存的位置中断。然后调试程序,程序中断了,查看调用堆栈。加上头文件#include 

 自动管理--智能指针

动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。

shared_ptr类
创建智能指针时必须提供额外的信息,指针可以指向的类型:

shared_ptr p1;
shared_ptr> p2;
默认初始化的智能指针中保存着一个空指针。 
智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象,在一个条件判断中使用智能指针就是检测它是不是空。

if(p1  && p1->empty())
    *p1 = "hi";
如下表所示是shared_ptr和unique_ptr都支持的操作: 
 
如下表所示是shared_ptr特有的操作: 

make_shared函数: 
最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。头文件和share_ptr相同,在memory中 
必须指定想要创建对象的类型,定义格式见下面例子:

shared_ptr p3 = make_shared(42);
shared_ptr p4 = make_shared(10,'9');
shared_ptr p5 = make_shared();
make_shared用其参数来构造给定类型的对象,如果我们不传递任何参数,对象就会进行值初始化

shared_ptr的拷贝和赋值 
当进行拷贝和赋值时,每个shared_ptr都会记录1有多少个其他shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。

auto p = make_shared(42);
auto q(p);
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

auto r = make_shared(42);//r指向的int只有一个引用者
r=q;//给r赋值,令它指向另一个地址
    //递增q指向的对象的引用计数
    //递减r原来指向的对象的引用计数
    //r原来指向的对象已没有引用者,会自动释放
shared_ptr自动销毁所管理的对象 
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象,它是通过另一个特殊的成员函数-析构函数完成销毁工作的,类似于构造函数,每个类都有一个析构函数。析构函数控制对象销毁时做什么操作。析构函数一般用来释放对象所分配的资源。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。

shared_ptr还会自动释放相关联的内存 
当动态对象不再被使用时,shared_ptr类还会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

使用了动态生存期的资源的类: 
程序使用动态内存的原因: 
(1)程序不知道自己需要使用多少对象 
(2)程序不知道所需对象的准确类型 
(3)程序需要在多个对象间共享数据

直接管理内存 
C++定义了两个运算符来分配和释放动态内存,new和delete,使用这两个运算符非常容易出错。

使用new动态分配和初始化对象 
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。

int *pi = new int;//pi指向一个动态分配的、未初始化的无名对象

此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针

默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

string *ps = new string;//初始化为空string
int *pi = new int;//pi指向一个未初始化的int

我们可以直接使用直接初始化方式来初始化一个动态分配一个动态分配的对象。我们可以使用传统的构造方式,在新标准下,也可以使用列表初始化

int *pi = new int(1024);
string *ps = new string(10,'9');
vector *pv = new vector{0,1,2,3,4,5,6,7,8,9};

也可以对动态分配的对象进行初始化,只需在类型名之后跟一对空括号即可;

动态分配的const对象

const int *pci = new const int(1024);
//分配并初始化一个const int
const string *pcs = new const string;
//分配并默认初始化一个const的空string

类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象就必须显式初始化。由于分配的对象是const的,new返回的指针就是一个指向const的指针。

内存耗尽: 
虽然现代计算机通常都配备大容量内村,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的空间,new表达式就会失败。默认情况下,如果new不能分配所需的内存空间,他会抛出一个bad_alloc的异常,我们可以改变使用new的方式来阻止它抛出异常

//如果分配失败,new返回一个空指针
int *p1 = new int;//如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow)int;//如果分配失败,new返回一个空指针

我们称这种形式的new为定位new,定位new表达式允许我们向new传递额外的参数,在例子中我们传给它一个由标准库定义的nothrow的对象,如果将nothrow传递给new,我们的意图是告诉它不要抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都在头文件new中。

释放动态内存 
为了防止内存耗尽,在动态内存使用完之后,必须将其归还给系统,使用delete归还。

指针值和delete 
我们传递给delete的指针必须指向动态内存,或者是一个空指针。释放一块并非new分配的内存或者将相同的指针释放多次,其行为是未定义的。即使delete后面跟的是指向静态分配的对象或者已经释放的空间,编译还是能够通过,实际上是错误的。

动态对象的生存周期直到被释放时为止 
由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放,但是通过内置指针类型来管理的内存就不是这样了,内置类型指针管理的动态对象,直到被显式释放之前都是存在的,所以调用这必须记得释放内存。

使用new和delete管理动态内存常出现的问题: 
(1)忘记delete内存 
(2)使用已经释放的对象 
(3)同一块内存释放两次

delete之后重置指针值 
在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的地址

有一种方法可以避免悬空指针的问题:在指针即将要离开其作用于之前释放掉它所关联的内存 
如果我们需要保留指针可以在delete之后将nullptr赋予指针,这样就清楚的指出指针不指向任何对象。 
动态内存的一个基本问题是可能多个指针指向相同的内存

shared_ptr和new结合使用 
如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的职能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针

shared_ptr p1 = new int(1024);//错误:必须使用直接初始化形式
shared_ptr p2(new int(1024));//正确:使用了直接初始化形式

下表为定义和改变shared_ptr的其他方法: 
 
不要混合使用普通指针和智能指针 
如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。 
也不要使用get初始化另一个智能指针或为智能指针赋值

shared_ptr p(new int(42));//引用计数为1
int *q = p.get();//正确:但使用q时要注意,不要让它管理的指针被释放
{
    //新程序块
    //未定义:两个独立的share_ptr指向相同的内存
    shared_ptr(q);

}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p;//未定义,p指向的内存已经被释放了

p和q指向相同的一块内部才能,由于是相互独立创建,因此各自的引用计数都是1,当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放,p这时候就变成一个空悬指针,再次使用时,将发生未定义的行为,当p被销毁时,这块空间会被二次delete

其他shared_ptr操作 
可以使用reset来将一个新的指针赋予一个shared_ptr:

p = new int(1024);//错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024));//正确。p指向一个新对象
与赋值类似,reset会更新引用计数,如果需要的话,会释放p的对象。reset成员经常和unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

if(!p.unique())
p.reset(new string(*p));//我们不是唯一用户,分配新的拷贝
*p+=newVal;//现在我们知道自己是唯一的用户,可以改变对象的值

智能指针和异常 
如果使用智能指针,即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放,sp是一个shared_ptr,因此sp销毁时会检测引用计数,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。

使用我们自己的释放操作 
默认情况下,shared_ptr假定他们指向的是动态内存,因此当一个shared_ptr被销毁时,会自动执行delete操作,为了用shared_ptr来管理一个connection,我们必须首先必须定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。

智能指针陷阱: 
(1)不使用相同的内置指针值初始化(或reset)多个智能指针。 
(2)不delete get()返回的指针 
(3)不使用get()初始化或reset另一个智能指针 
(4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了 
(5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

unique_ptr
某个时刻只能有一个unique_ptr指向一个给定对象,由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。 
下表是unique的操作: 
 
虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique

//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr p2(p1.release());//release将p1置为空
unique_ptrp3(new string("Trex"));
//将所有权从p3转移到p2
p2.reset(p3.release());//reset释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。 
reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。 
调用release会切断unique_ptr和它原来管理的的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。 
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr.

unique_ptr clone(int p)
{
    //正确:从int*创建一个unique_ptr
    return unique_ptr(new int(p));
}

还可以返回一个局部对象的拷贝:

unique_ptr clone(int p)
{
    unique_ptr ret(new int(p));
    return ret;
}

5
向后兼容:auto_ptr 
标准库的较早版本包含了一个名为auto_ptr的类,它具有uniqued_ptr的部分特性,但不是全部。 
用unique_ptr传递删除器 
unique_ptr默认使用delete释放它指向的对象,我们可以重载一个unique_ptr中默认的删除器 
我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象删除器。

weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。 
weak_ptr的操作 
 
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针

scoped_ptr
scoped和weak_ptr的区别就是,给出了拷贝和赋值操作的声明并没有给出具体实现,并且将这两个操作定义成私有的,这样就保证scoped_ptr不能使用拷贝来构造新的对象也不能执行赋值操作,更加安全,但有了”++”“–”以及“*”“->”这些操作,比weak_ptr能实现更多功能。

 有关问题

      1. 使用智能指针管理内存资源,RAII
  1. RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
  2. 智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。
      1. 手写实现智能指针类
  1. 智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer对象的引用计数,一旦T类型对象的引用计数为0,就释放该对象。除了指针对象外,我们还需要一个引用计数的指针设定对象的值,并将引用计数计为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。通过覆写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1
      1. 智能指针的作用;
  1. C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存
  2. 智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptrshared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
  3. 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误

拷贝和赋值。拷贝使得对象的引用计数增加1赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象

  1. unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
  2. 智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
  3. weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少
      1. auto_ptr作用
  1. auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;
  2. auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr<Type>类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;
  3. auto_ptr构造函数是explicit阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;
  4. 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针
  5. Auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组
  6. auto_ptr支持所拥有的指针类型之间的隐式类型转换。
  7. 可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;
  8. T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。
      1. 智能指针怎么用?智能指针出现循环引用怎么解决?
  1. shared_ptr

调用一个名为make_shared的标准库函数,shared_ptr p = make_shared(42);通常用auto更方便,auto p = …;shared_ptr p2(new int(2));

每个shared_ptr都有一个关联的计数器,通常称为引用计数,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象;shared_ptr的析构函数就会递减它所指的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

  1. unique_ptr

一个unique_ptr拥有它所指向的对象。某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

  1. weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。

  1. 弱指针用于专门解决shared_ptr循环引用的问题,weak_ptr不会修改引用计数,即其存在与否并不影响对象的引用计数器。循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方。弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。