动态内存
- 动态分配的内存的对象的生命周期与他们在哪里创建是无关的,只有被显示的释放,这些对象才会被销毁;
动态内存
new
:表示在动态内存中为对象分配空间并且返回一个指向该对象的指针,同样的可以选择对象进行初始化;delete
:接受一个动态对象的指针,销毁该对象,并且释放与之相关联的内存,但是给一点建议,应该奖这个指针指向的内容指为NULL
防止野指针的出现;- 标准库提供了两种智能指针用来管理动态内存,智能指针负责自动释放指针所指向的对象,
shared_ptr
允许多个指针指向同一个对象;unique_ptr
表示独占
所指向的对象.标准库还定义了weak_ptr
的伴随类,它是一种弱引用,指向shared_ptr
所管理的对象;这三种类型定义在memory
头文件中; shared_ptr
类:
- 对于
shared_ptr
来说,必须要指定指针类型,默认初始化智能指针保存的是一个空指针; - 智能指针与普通指针类似,解引用一个智能指针返回它指向的对象;
make_shared
:这个函数是标准库函数,这个函数在动态内存中分配一个对象并且初始化它,返回指向此对象的shared_ptr
,当使用make_shared
时;make_shared
也必须指定要创建的对象的类型,定义的方式和模板类相同,例如shared_ptr<int> p3 = make_shared<int>(42)
;- 在使用
make_shared
时,创建的对象的类型,必须有相对应的构造函数来完成初始化 过程; shared_ptr
:
- 再进行拷贝或者赋值操作时,每个
shared_ptr
都会记录有多少个其他的shared_ptr
指向这个相同的对象;也就是说shared_ptr
是有一个计数器,每当进行拷贝操作时,计数器就会递增,直到计数器为0时,它所管理的对象才会被释放; - 对于
shared_ptr
是使用计数器或者其他数据结构来记录指针共享对象,是由标准库的实现来决定的; shared_ptr
是通过析构函数来完成指针的销毁工作的.析构函数一般用来释放对象所分配的资源.shared_ptr
可以自动释放所管理的对象,并且还会自动释放相关联的内存;- 有一种特殊的情况是当
shared_ptr
存放于一个容器中,而后不在需要全部元素,而只是使用其中一部分,需要使用erase
删除不在需要的那些元素; - 程序需要使用动态内存的几种情况:
- 1.程序不需要自己需要使用多少对象;
- 2.程序不知道所需对象的准确类型;
- 3.程序需要在多个对象间共享数据;
- 通常在拷贝
vector
时,原来的vector
和副本vector
里面的元素是分离的;如果需要在共享相互之间相同的底层元素; - 当相互之间存在共享底层数据的情况,就不能够轻易的销毁底层数据;
- 使用动态内存的一个常见原因是允许使用多个对象共享相同的状态;
- 再进行拷贝或者赋值操作时,每个
- 对于
对于内存的管理
C++
强烈建议使用智能指针来管理动态内存,自己直接管理内存的类不能依赖于类对象的拷贝,赋值和晓辉操作的任何默认定义;- 对于使用
new
得到动态内存,动态分配的对象是默认初始化的,也就是说内置类型和组合类型的对象的值是没有定义的,但是类类型对象将使用默认构造函
数进行初始化; C++11
标准支持使用列表初始化动态空间分配的对象的值;- 对于自己定义了构造函数的类类型来说,要求进行值初始化,是没有意义的,对象无论采用何种形式,对象都会通过默认的构造函数来进行初始化;
- 值初始化表示
string *ps = new string()
,这个初始化过程会调用默认构造函数,而不是调用其他的构造函数; - 对于值初始化的内置类型对象有着良好的定义的值;
- 默认初始化对象的值是未定义的;
- 如果类里面依赖于编译器和成的默认构造函数的内置类型成员,如果没有在类里面进行初始化,那么他们的值也是没有定义的;
- 对于动态内存分配的对象通常建议进行初始化;
auto
使用自对推导auto p1 = new auto(obj)
,但是这里需要强调的是auto
的对象应该只有一个,否则无法进行推导操作;- 动态分配的
const
对象也必须进行初始化. - 对于一个定义了默认构造函数的类类型,其
const
动态对象可以隐式初始化,但是其他的类型的对象必须进行显示初始化,由于分配的对象是const
,返回
的指针必须是const
的; - 当内存耗尽时,
new
表达式就会失败,然后会抛出bad_alloc
的异常,阻止异常抛出,int *ps = new (nothrow) int
,,需要包含头文件new
; delete
操作分为两步:销毁给定的指针所指向的对象,释放对应的内存;传递给
delete
的指针必须是指向动态内存分配的内存或者是一个空指针;编译器做不到的几点:
- 编译器可以判断所给的变量是否为指针;
- 编译器不能够识别对象是静态指针还是动态指针;
- 编译器无法判断该指针所指向的对象是否已经被释放;
const
对象指针也是刻意被释放的,按照其他的指针的释放方式就可以;- 动态对象的生存周期直到被释放时为止;
- 如果一个函数返回了一个动态内存,那么这块内存的释放是调用者需要负责的;
- 内置类型的对象在被销毁时,什么也不会发生,当一个指针离开作用域时,不会进行内存释放,即使指向的是动态内存;
- 总结来说,由内置指针(而不是智能指针)管理的动态内存在被显示释放前会一直存在;
- 使用
new
和delete
管理动态内存会出现的几个问题
- 1.忘记删除
delete
内存,还有就是delete
内存的方式不正确; - 2.使用已经释放掉的对象,通常来说对于已经释放掉的对象,需要将指针置为
NULL
; - 3.同一块内存空间被
delete
多次;
- 1.忘记删除
- 坚持使用智能指针吧,这才是正道;
在
delete
指针之后,需要将指针赋值为nullptr
用来防止野指针的出现;int *p(new int(42)); auto q = p; delete p; p = nullptr;
需要说明的是这里虽然将
p=nullptr
并且删除了p
,但是重置了p
对于q
来说没有任何的作用,当p
被释放,q
就会编程无效的;- 实际情况中,查找指向相同内存的所有制诊是异常困难的;
shared_ptr
如果智能指针不被初始化,那么就会被初始化为一个空指针;
shared_ptr<double> p1; shared_ptr<int> p2(new int(42)); //可以使用`new`的返回值的指针来初始化智能指针;
接受指针参数的智能指针的构造函数是
explict
的,也就是不能够将一个内置指针类型转换成智能指针,所以必须使用直接初始化方式;shared_ptr<int> p1 = new int(1024); //这种初始化方式是错误的;
同样的一个普通类型的指针也不能够转换为
shared_ptr
指针;shared_ptr<int> clone(int p){ return new int(p); //错误:隐式转换为shared_ptr<int> } shared_ptr<int> clone(int p){ return shared_ptr<int>(new int(p)); }
当使用
explict
声明构造函数时,只能够以直接初始化方式进行初始化;shared_ptr
可以协调对象的析构,但是这仅仅限于自身的拷贝;
shared_ptr<int> p(new int(42)); process(p); int i = *p;
解释一下上面的代码:
1.p一开始是一个内置指针,然后转换成为智能指针
shared_ptr<int>
类型;- 2.执行拷贝操作,process(p)函数里面的引用计数器为2;外面的引用计数器为1;
- 需要牢牢记住的是,使用一个内置指针来访问一个指针指针对象所负责的对像是很危险的,因为我们无法直到对象什么时候会被销毁;
get
函数:函数返回一个内置指针,指向指针指针管理的对象,使用
get
返回的指针的代码不能delete
这个指针;shared_ptr<int> p(new int(42)); int *q = p.get(); //在这里需要注意的是不能够释放所管理的指针;否则就会出错;
所以需要注意的是:
get
用来将指针的访问权限传递给代码,只有在代码不会delete
指针的情况下,才能使用get
,尤其是不哟啊使用get
初始化
另一个智能指针;reset
函数可以用来将一个新的指针赋予一个shared_ptr
,例如:p.reset(new int(1024))
;在这个过程中,reset
会更新引用计数器,甚至释
放p
所指向的对象.reset
经常和unique
一起使用,来控制多个shared_ptr
共享的对象;- 在改变底层对象之前,我们需要检查当前对象仅有的用户,如果不是,在进行改变之前,需要制作一份新的拷贝;
- 智能指针和异常
- 在使用智能指针,及时程序快过早结束,智能指针也能够确保在内存不需要时将其释放;
- 使用智能指针在程序块结束时,能够将内存正确的释放掉;
- 异常在抛出时,异常之后的代码就不会被执行,所以以后的内存代码就会失效;
- 哑类:那些为了
C
和C++
两种语言设计的类,通常要求用户显式的释放所使用的任何资源.在资源的申请和释放之间出现异常,代码机会出现异常; - 智能指针陷阱
- 1.不使用相同的内置指针类型初始化多个智能指针;
- 2.不
delete get()
返回的指针; - 3.不适用
get()
初始化或者reset
另一个智能指针; - 4.如果使用了
get()
返回的指针,当最后一个对应的智能指针销毁后,指针就会是无效的了; - 5.如果使用智能指针管理的资源不是
new
分配的内存,需要传递给它一个删除器;
unique_ptr
:unique_ptr
拥有指向的对象,但是unique_ptr
只能够有一个特定的对象,当unique_ptr
被销毁时,它所指向的对象也就会被销毁;unique_ptr
需要采用直接初始化的方式进行初始化,例如:unique_ptr<int> p2(new int(42))
;unique_ptr
不支持拷贝以及赋值操作,但是可以通过release
或者reset
操作将指针的所有权从一个非const unique_ptr
转移给另一个unique
;例如:unique_ptr<string> p2(p1.release())
- 在指针所有权转移过程中发生的变化;
unique_ptr<string> p2(p1.release())
:p1
置为空,p2
置为p1所保存的指针;uniquee_ptr<string> p3(new string("Text"))
:p2
释放了自己所指向的指针值,然后接受p3
置为空之后的指针值;release
返回的指针通常用来初始化另一个智能指针或者给另一个智能指针赋值,release
的返回值表示的是p1.release()
里面p1
所指向内存的地址;reset
首先释放自己原来指向的内存,然后接受一个新的给定的指针;
unique_ptr
可以拷贝或者赋值一个将要被销毁的unique_ptr
,比如从函数返回的一个unique_ptr
,在这个过程中编译器执行的是一种特殊的拷贝;- 在有
auto_ptr
和unique_ptr
时,不建议使用auto_ptr
; unique_ptr
的的删除器管理方式和shared_ptr
是不一样的,删除器的格式:unique_ptr<objT,delT> p (new objT,fcn);
表示的含义是p
指向一个objT
类型的对象,然后使用一个类型为delT
的对象用来释放objT
对象,同时会调用一个名为fcn
的delT
类型对象;void f(destination &d,...){ connection c = connect(&d); unique_ptr<connection,decltype(end_connection)*> p(&c,end-connection); //在使用连接时,即使程序是由于异常退出,所占用的内存资源也会被正确释放; //delctype的返回值是一个类型,所以需要使用*来表示得到的是一个指针类型; }
weak_ptr
:若弱指针表示的含义是将一个以弱指针绑定到一个共享指针上面,是不会改变共享指针的计数器的,但是当共享指针的计数器变成0
而被释放时,
对象就会被释放,共享指针所指向的对象也会被释放;当我们创建一个
weak_ptr
时,需要使用shared_ptr
来进行初始化;auto p = make_shared<int>(42); weak_ptr<int> wp(p) //wp就是共享指针`p`的弱指针,但是并不会改变`p`的计数器的值; //当`p`的计数器为0时,就会被释放`wp`所指向的对象也会被释放;
对于上面的情况需要注意的是不能够使用
wp
来直接访问共享对象,因为共享对象可能已经被释放,访问就会出错,所以需要使用wp.lock()
来检查共享对象的
底层对象是否存在;动态数组:
- 大多数应用应该使用标准库容器而不是动态分配的数组,因为标准库容器对于空间的管理更加合理,高效;
- 使用
new
分配对象数组,例如:int *pia = new int[get_size()];
,首先[]
里面的没有要求是常量,但是要求必须是整型; - 还可以使用类型别名来分配动态数组;
typedef int arrT[42]; //int 表示类型,arrT表示的含义是类型别名的别名, int *p = new arrT;//表示分配一个42个int整型数组;
- 这里表示的动态数组本质上并不是数组,仅仅是得到了相应空间按照要求的一个元素类型的指针,所以动态数组并不能使用
begin
以及end
等,同样的也
不能够使用for
循环来处理动态数组的元素; 关于初始化的几种情况:
int *pia = new int[10] //表示的含义是10个未进行初始化; int *pia = new int[10]();//表示的含义是10个初始化为0的整型; 新标准: int *pia = new int[10]{1,2,3,4,5,6,7,8,9,0}; string *pia = new string[10]{"a","an","the",string(3,"x")};
动态分配一个空数组是合法的,对于动态数组的处理,可以使用指针的方式进行访问;
- 释放动态数组,必须使用的格式
delete [] pa
,如果没有[]
仅仅会释放首地址; - 在释放动态数组时,内存的释放过程是按照逆序进行的,首先释放最后一个元素,然后依次释放到首元素;
标准库额提供了一个可以管理
new
分配的数组的unique_ptr
版本;unique_ptr<int[]> up(new int[10]); //`int []`是不能够省略的,因为只有这样才能够调用delete []; up.release(); //自动使用`delete[]`销毁其指针;
在使用
unique_ptr
指向的数组时,是不支持使用.
运算符以及->
运算符的,但是可以使用下表运算符,进行元素的访问;shared_ptr
是不支持管理动态数组的,如果需要使用shared_ptr
来进行管理,就需要提供一个删除器;shared_ptr<int> sp(new int[10],[](int *p){delete [] p;}); sp.reset();
shared_ptr
不支持动态数组管理,不能够使用下标运算符,并且不能够使用指针的算数运算;for(size_t i=0;i!=10;i++){ *(sp.get()+i)=i; } //使用这种方式首先获得内置指针,然后通过内置指针来循环访问下标;
allacator类
new
将内存分配和对象构造结合在一起,delete
将对详细够和内存释放组合在一起;- 但是在一些情况下,需要将内存分配和对象构造分离开;
- 没有构造函数的类是不能够分配动态数组的,因为无法进行初始化;
allocator
是定义在memory
里面的,可以将内存分配和对象构造分离开来,可以按照给定的对象类型来确定恰当的内存大小和内存对齐;
allocator<string> alloc; //给`string`分配`allocator`对象 auto const p = alloc.alloc.allocate(n);//分配`n`个没有进行初始化的`string`;
- 进行构造的过程中,这些额外的参数必须是和构造的对象的类型相匹配的合法的初始化器,
为了使用
allocate
返回的内存,我们必须使用construct
构造对象,使用未构造的内存,其行为是没有定义的;while(q!=p) alloc.destory(--q); //如果需要释放这些内存就需要使用`deallocate`来完成 alloc.dealloccate(p,n);
填充和拷贝未初始化内存的算法,标准库提供了另个伴随算法,用于在未初始化内存中创建对象;