C++内存

C++的内存管理

一个C/C++编译的程序占用内存分为以下几个部分:

  1. 栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
  2. 堆区(heap):一般由程序员分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
  3. 全局区(静态区static):存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
  4. 常量区(文字常量区):存放常量字符串,程序结束后由系统释放。
  5. 代码区(*存储区):存放函数体(类成员函数和全局区)的二进制代码。

堆和栈的区别

1.堆和栈的生长方向不同

对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

2.堆会随使用产生碎片,栈不会

对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列

3.管理方式不同

对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

4.堆未及时释放容易造成内存泄漏

5.堆都是动态分配的,栈可以动态分配可以静态分配

静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

6.分配效率

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,因此栈的效率比较高。

堆是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

7.应用场景

堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,程序开发时应尽可能使用栈。

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

*存储区和堆的区别

malloc在堆上分配的内存块,使用free释放内存,而new所申请的内存则是在*存储区上,使用delete来释放。

插播new与delete的区别

new-delete是运算符,而malloc-free是库函数,malloc-free不能进行构造和析构。

new-delete的功能可以完全覆盖malloc-free,由于C++会调用C函数,而C只能使用malloc-free,因此保留了malloc-free。

 

malloc-free

new-delete

内存分配位置

*存储区

分配成功返回值

void*

对应类型指针

分配失败返回值

NULL

抛出异常

分配内存大小

用户计算

自动

处理数组

×

已分配内存的扩充

×

互相调用

×

分配内存时内存不足

用户不可处理

用户可重新申请

重载

×

调用构造/析构

×

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Q:很多编译器的new/delete都是以malloc/free为基础来实现的,借以malloc实现的new,所申请的内存是在堆上还是在*存储区上?*存储区与堆是两块不同的内存区域吗?它们有可能相同吗?

A:从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。

而*存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为*存储区。

基本上,所有的C++编译器默认使用堆来实现*存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时借由new运算符分配的对象,说它在堆上也对,说它在*存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现*存储,例如全局变量做的对象池,这时*存储区就区别于堆了。

因此:堆是操作系统维护的一块内存,而*存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与*存储区并不等价。

 

常见的内存错误和解决方法

1.    内存分配未成功,却使用了它

使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

2.    内存分配虽然成功,但是尚未初始化就引用它

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。

3.    内存使用越界

例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

4.    内存泄漏

多由程序员忘记释放堆内存引起。动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

5.    使用已释放内存

a)    程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

b)    函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

c)    使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

6.    浅复制造成的释放已释放的内存

复制构造函数,在用户未定义复制函数时,C++提供的默认复制函数,作用是把两个类复制。

应用场景:

i.     形参是本类类型对象的引用:String test1(test2);

ii.    形参为按值传递,未使用指针or引用:void show_String(const String)

iii.   string B;  string A=B;

问题的产生

i.     复制构造函数采用的是浅复制,即复制了相关类成员的指针,而没有开辟新的空间,内存中的数据还是那一份数据。

ii.    当释放某一个指针所指对象后,操作由复制构造函数形成的另外指针将会引发内存使用错误。

解决方法:

定义开辟空间的复制函数进行深拷贝

内存泄漏&预防

内存泄漏源于new出的内存没有全部被delete掉导致的内存消耗,使用智能指针能将内存泄漏风险降至最低。

智能指针利用C++ RAII(Resource Acquisition Is Initialization,资源获取就是初始化)原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。(std::mutex互斥锁就是用的这个原则,退出局部变量使用域的时候自动释放锁)

几种智能指针↓

1.    auto_ptr(旧)--> unique_ptr(C++11)

在构造的时候获取资源,在析构的时候释放资源,并进行相关指针操作的重载,使用起来就像普通的指针。独享资源所有权。由于其构造函数声明为explicit的,因此不能通过隐式转换来构造,只能显示调用构造函数。

2.    share_ptr(C++11)

允许限定的资源被多个指针共享。

3.    weak_ptr

weak_ptr是一种用于解决shared_ptr相互引用时产生死锁问题的智能指针。如果有两个shared_ptr相互引用,那么这两个shared_ptr指针的引用计数永远不会下降为0,资源永远不会释放。weak_ptr是对对象的一种弱引用,它不会增加对象的use_count,weak_ptr和shared_ptr可以相互转化,shared_ptr可以直接赋值给weak_ptr,weak_ptr也可以通过调用lock函数来获得shared_ptr。

上一篇:线性表之-栈和队列


下一篇:new