后台开发工程师技术能力体系之编程语言7——动态内存

动态内存

  全局对象在程序启动时分配,在程序结束时销毁;对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁;局部staitc对象在第一次使用前分配,在程序结束时销毁。除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建时无关的,只有当显式被释放时,这些对象才会销毁。
  动态对象的正确释放是编程中极易容易出错的地方,为了更安全地使用动态内存,标准库定义了两个智能指针类型来管理动态分配的对象,当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
  我们的程序目前为止只使用过静态内存栈内存静态内存用来保存局部staitc对象,类static数据成员以及定义在任何函数之外的变量栈内存用来保存定义在函数内的非static对象分配在静态内存或栈内存中的对象由编译器自动创建和销毁:对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
  除了静态内存和栈内存,每个程序还拥有一个内存池,这部分内存被称作自由空间或堆程序用堆来存储动态分配的对象——即那些在程序运行时分配的对象,动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

1.动态内存与智能指针

  在C++中,动态内存的管理是通过一对运算符来完成的:new在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化delete接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。为了更容易(同时也更安全)地使用动态内存,新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

shared_prt和unique_ptr都支持的操作
shared_ptr<T> sp unique_ptr<T> up空智能指针,可以指向类型为T的对象
p.get()返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p,q) p.swap(p)交换p和q中的指针
shared_prt独有的操作
make_shared<T>(args)返回一个shared_ptr,指向一个动态分配的类型为T的对象,使args初始化此对象
shared_ptr<T>p(q)p是shared_ptr q的拷贝,此操作会递增q中的计数器,q中的指针必须能转换为T*
p = qp和q是shared_ptr,所保存的指针必须能相互转换,此操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为0,则将其管理的源内存释放
p.unique()若p.use_count()为1,返回true,否则返回false
p.use_count()返回与p共享对象的智能指针的数量;可能很慢,主要用于调试

  当进行拷贝或赋值操作时,每个shared_pr都会记录有多少个其他shared_ptr指向相同的对象。我们可以认为每个shared_ptr都有一个关联的计数器,通常称为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增;当我们给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。这些工作都是由析构函数来完成的,shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr的析构函数就会销毁对象并释放它占用的内存。
  由于在最后一个shraed_ptr销毁前内存都不会释放,因此保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。例如,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素,在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。

2.直接管理内存

  C++语言定义了两个运算符来分配和释放动态内存,运算符new分配内存,delete释放new分配的内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。
  在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针默认情况下,动态分配的对象时默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将使用默认构造函数进行初始化。

int *pi = new int; //pi指向一个动态分配的,未初始化(默认初始化)的无名对象
string *ps = new string; //默认初始为空string

  我们还可以使用直接初始化方式来初始化一个动态分配的对象,可以采用传统的构造方式(圆括号),也可以使用新标准下的列表初始化方式(使用花括号)

int *pi = new int(1024); //pi指向的对象的值为1024
string *ps = new string(10,'9'); //*ps为“9999999999”
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

  也可以对动态分配的对象进行值初始化只需在类型名之后跟一对空括号即可。对于定义了自己的构造函数的类类型,值初始化其实就是默认初始化,但对于内置类型,值初始化的内置类型有着良好定义的值,而默认初始化的对象的值则是未定义的。

string *ps1 = new string; //默认初始化为空string
string *ps = new string(); //值初始化为空string
int *pi1 = new int; //默认初始化:*pi1的值未定义
int *pi2 = new int(); //值初始化为0

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

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

  我们称“new (nothrow)”这种形式的new为定位new,定位new表达式允许我们向new传递额外的参数,在这里我们传递给它一个由标准库定义的名为nothrow的对象,如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。
  为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统,我们通过delete表达式来将动态内存归还给系统,delete表达式执行两个动作销毁给定指针指向的对象释放对应的内存。我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。delete一个非指针类型,编译器会生成一个错误信息,但编译器不能分辨一个指针指向的是静态还是动态分配的对象,同时编译器也不能分辨一个指针所指向的内存是否已经被释放了,这些delete表达式,大多是编译器会编译通过,尽管它们是错误的

int i = 0,*pi1 = &i,*pi2 = nullptr;
double *pd = new double(33),*pd2 = pd;
delete i; //错误,i不是一个指针
delete pi1; //未定义,pi1指向一个局部变量
delete pd; //正确
delete pd2; //未定义,pd2指向的内存已经被释放了
delete pi2; //正确,释放一个空指针总是没有错误的

  当我们delete一个指针后,指针就变为无效了,也就是常说的“空悬指针”。未初始化指针的缺点空悬指针都有,通常在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。但如果有多个指针指向相同的内存,在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的,而且在实际系统中,查找指向相同内存的所有指针是非常困难的,此时最好的方法就是使用智能指针。

3.shared_ptr

  如果我们不初始化一个智能指针,他就会被初始化为一个空指针,我们还可以使用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数时explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。同样,一个返回智能指针类型的函数也不能在其返回语句中隐式转换一个普通指针。

shared_ptr<int> p1 = new int(1024);  //错误
shared_ptr<int> p2(new int(1024); //正确

  默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们也可以将智能指针绑定到一个指向其他类型的资源的指针上,但为了这样做,必须提供自己的操作带代替delete。

定义和改变shared_ptr的其他方法
shared_ptr<T> p(q)p管理内置指针q所指向的对象;q必须指向new分配的内存且能够转换为T*类型
shared_ptr<T> p(u)p从unique_ptr u那里接管了对象的所有权,将u置为空
shared_ptr<T> p(q,d)p接管了内置指针q所指向的对象的所有权,q必须能转换为T*类型,p将使用可调用对象d来代替delete

  不要混合使用普通指针和智能指针。shared_ptr能够通过引用计数来协调对象之间的析构,但这仅限于其自身的拷贝之间。如果使用new返回的指针来初始化智能指针,就有可能在无意将同一块内存绑定到多个独立创建的shared_ptr上。 这也是为什么我们推荐使用make_shared而不是new的原因,因为这样我们就能在分配对象的同事就将shared_ptr与之绑定。当将一个shared_ptr邦绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr,一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
  不要使用get初始化另一个智能指针或为智能指针赋值。get函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get函数返回的内置指针时必须注意两点,一是在代码中不能delete此指针二是不能将另一个智能指针绑定到该指针上
  对于智能指针,即使程序块发生异常过早结束,智能指针也能确保内存会被正常释放掉;但如果使用内置指针管理内存,且在new之后在对应的delete之前发生异常,则内存不会被释放掉

4.unique_ptr

  一个unique_ptr“拥有”它所指向的对象,与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象,当unique被销毁时,它所指向的对象也被销毁。对于unique_ptr,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作,但有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr

unique_ptr<int> p1(new int(1));
unique_ptr<int> p2(p1); //错误,unique_pt不支持拷贝
unique_ptr<int> p3;
p3 = p2; //错误,unique_ptr不支持赋值
// 编译器知道要返回的对象将要被销毁,此时编译器会执行一种特殊的“拷贝”
unique_ptr<int> clone(int p){
    return unique_ptr<int>(new int(p));		
}

  虽然不能拷贝或赋值unique_ptr,但是可以通过releasereset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。release函数返回unique_ptr当前保存的指针并将unique_ptr置为空,相当于切断了unique_ptr和它原来管理的对象间的联系;reset函数接受一个可选的指针参数,令unique_ptr重新指向给定的指针并释放原来指向的对象

5. weak_ptr

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

if(shared_ptr<int> np = wp.lock()){
	//在if块中使用np访问共享对象是安全的
}

6.动态数组

  C++语言和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了另一种new表达式"new[]",可以分配并初始化一个对象数组标准库中包含了一个名为allocator的类,允许我们将分配和初始化分离,使用allocator通常会提供更好的性能和更灵活的内存管理能力。
  大多数应用都没有直接访问动态数组的需求,这些应用通常使用标准库容器而不是动态分配的数组。因为使用容器更为简单,更不容易初出现内存管理错误并且可能有更好的性能。
  如果我们在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告,我们的程序可能在执行过程中在没有任何警告的情况下行为异常
  当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符,因为unique_ptr指向的是一个数组而不是单个对象,因此这些运算符时无意义的。另一方面,当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中 的元素(但shared_ptr未定义下标运算符)。
  标准库提供了一个可以管理new分配的数组的unique_ptr版本(unique_ptr<T[]>),但shared_ptr不直接支持管理动态数组,如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器,因为shared_ptr默认使用delete销毁它指向的对象

unique_ptr<int[]> up(new int[10]); //up指向一个包含10个未初始化int的数组
up.release(); //自动调用delete[]销毁其指针

shared_ptr<int> sp(new int[10],[](int *p) {delete[] p;});
sp.reset(); //使用我们提供的lambda释放数组,它使用delete[]

  new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起,类似的,delete将对象析构和内存释放组合在了一起。当分配单个对象时,通常希望将内存分配和对象初始化组合在一起,因为在这种情况下,我们几乎可以肯定知道对象应该有什么值。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费,例如string *const p = new string[n]; //构造n个空string,new表达式分配并初始化了n个string,但是我们可能不需要n个string,少量string可能就足够了,这样我们就可能创建了一些永远也用不到的对象。更重要的是,那些没有默认构造函数的类就不能动态分配数组了。

  当分配一大块内存时,我们通常计划在这块内存上按需构造对象,在此情况下,我们希望将内存分配和对象构造分离,这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作。标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供了一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。

标准库allocator类及其算法
allocator<T> a定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n)分配一段原始的,未构造的内存,保存n个类型为T的对象
a.deallocate(p,n)释放从T*指针p中地址开始的内存,p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destory
a.construct(p,args)p必须是一个类型为T*的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destory(p)p为T*类型的指针,此算法对p指向的对象执行析构函数
allocator<string> alloc; //可以分配string的allocator对象
auto const p = alloc.allocate(n); //分配n个未初始化的string
auto q = p; //q用来指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++,10,'c'); //*q为cccccccccc
alloc.construct(q++,"hi"); //*q为hi
cout << *q << endl; //灾难:q指向未构造的内存
//当我们用完对象后,必须对每个构造的元素调用destory来销毁它们
while(q != p)
	alloc.destory(--q); //释放我们真正构造的string
alloc.destory(p); //释放内存
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值