Boost源码学习二[内存管理二]

来源:互联网 发布:mac flash更新还是不行 编辑:程序博客网 时间:2024/06/05 04:11
内存管理的另一个重要部分技术内存池,Boost库的pool提供了一个内存池分配器,用于管理在一个独立的、大的分配空间里的动态内存分配。Boost库的pool主要适用于快速分配同样大小的内存块,尤其是反复分配和释放同样大小的内存块的情况。使用pool内存池主要有以下两个优点:
  1. 能够有效地管理许多小型对象的分配和释放工作,避免了自己去管理内存而产生的内存碎片和效率低下问题。
  2.  告别程序内存泄漏的烦恼,pool库会在内部对内存自动进行管理,避免了程序员一不小心而造成的内存泄漏问题。

pool库包含四个部分,最简单地pool、分配类实例的object_pool、单件内存池singleton_pool和可用于标准库的pool_alloc。

先学习一下第一个pool最简单的最容易的内存池类。

template<typename UserAllocator >class pool{     public: explicit pool(size_type requested_size); ~pool(); size_type get_requested_size() const;  void *malloc(); void *ordered_malloc(); void *ordered_malloc(size_type n); bool is_from(void *chunk) const;  void free(void *chunk); void ordered_free(void *chunk); void free(void *chunks,size_type n); void ordered_free(void *chunks,size_type n);  bool release_memory(); bool purge_memory();};</span>

其malloc函数用于从内存池中分配内存;free函数用于释放内存,并交还给内存池,而不是系统;release_memory函数用于释放所有未被分配的内存;purge_memory函数用于释放所有内存。当然,也可以不调用free或release_memory等函数,pool接口对象在析构时会调用purge_memory自动释放所有内存。

Boost库完全开发指南例子:

#include <iostream>#include <boost/pool/pool.hpp>using namespace boost;using namespace std;int main(){ //一个可分配int的内存池   pool<> pl(sizeof(int));  //必须把void*转换成需要的类型   int *p = (int *)pl.malloc();  assert(pl.is_from(p));  cout << "pl is allocated from pool." << endl;  //释放内存池分配的内存块   pl.free(p);  cout << "Free memory pool of the allocated memory block" << endl;  //连续分配大量的内存   for(int i = 0; i < 100; ++i)  pl.ordered_malloc(10);  cout << "allocated a large of memory" << endl       << "now free all memory..." << endl;     }</span>
第二部分就是object_pool

template<typename ElementType>class object_pool:protected pool{   public:   object_pool();   ~object_pool();      element_type *malloc();   void free(element_type *p);   bool is_from(element_type *p) const;      element_type * construct();   void destory(element_type *p);};</span>

object_pool接口:主要用于对象的内存分配并自动调用类的构造函数。其construct函数用于从内存池中分配内存并自动调用构造函数,其destroy函数用于释放内存交还给内存池并自动调用析构函数。与pool接口一样,也可以不调用destroy函数,object_pool接口对象在析构时会自动释放所有内存并自动调用析构函数。另外,object_pool接口也有malloc和free函数,但其malloc只分配内存而不负责构造,free只释放内存而不负责析构。因此,最好将construct和destroy配对使用,将malloc和free配对使用,而不要两者混用。

书上的例子:

#include <iostream>#include <boost/pool/object_pool.hpp>using namespace std;using namespace boost;//一个示范类 struct demo_class{   public:   int a,b,c;   demo_class(int x = 1,int y = 2,int z = 3):a(x),b(y),c(z){}};int main(){//对象内存池 object_pool<demo_class> pl;//分配一个原始的内存块 demo_class *p = pl.malloc();assert(pl.is_from(p));cout << "ok,allocated from pl..." << endl;//p指向的内存未经过初始化 assert(p->a != 1 || p->b != 2 || p->c != 3);//构造一个对象,可以传递参数 p = pl.construct(7,8,9);assert(p->a == 7);cout << p->a << " " << p->b << " " << p->c << endl;//定义一个分配string对象的内存池 object_pool<string> pls;//连续分配大量string对象 for(int i = 0; i < 10; ++i){  string *ps = pls.construct("hello object_pool");  cout << *ps << endl;}//所有创建的对象在这里都被正确地析构、释放内存 }

第三个部分就是singleton_pool

template< typename Tag,unsigned RequestedSize >class singleton_pool{    public:static bool is_from(void *ptr);static void *malloc();static void *ordered_malloc();static void *ordered_malloc(size_type n);static void free(void *ptr);static void ordered_free(void *ptr);static void free(void *ptr,std::size_type n);static bool release_memory();static bool purge_memory();};

singleton_pool接口的构造函数是私有的,因此不能够创建一个singleton_pool接口的对象。singleton_pool接口提供了一些静态方法如malloc、free用于内存的分配和释放,其他方面与pool接口相同。

struct intpool { };struct intpool2 { };typedef singleton_pool<intpool, sizeof(int)> ipool1;typedef singleton_pool<intpool2, sizeof(int)> ipool2;    for (int i = 0; i < 10; ++i)     {         int *q1 = (int *)ipool1::malloc();         int *q2 = (int *)ipool2::malloc();        *q1 = i;        *q2 = i*i;        cout << *q1 << " and " << *q2 << endl;     }     ipool1::purge_memory();     ipool2::purge_memory();

第四部分是pool_alloc:

pool_allocator的内部实现调用了ordered_malloc和ordered_free,可以满足对大量的连续内存块的分配请求。fast_pool_allocator 的内部实现调用了malloc和free,比较适合于一次请求单个大内存块的情况,但也适用于通用分配,不过具有一些性能上的缺点。

#include <iostream>#include <vector>#include <boost/pool/pool_alloc.hpp>using namespace std;using namespace boost;int main(){ //使用pool_allocator代替标准容器默认的内存分配器     vector<int,pool_allocator<int> > v;    //vector将使用新的分配器良好工作  v.push_back(10); cout << v.size() << endl; }


最后来三段小插曲关于内存分配方式及常见错误

和内存泄漏及检测及内存布局

============================================================================

内存分配方式

============================================================================

1、内存分配方式

内存分配方式有三种:

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

2、常见的内存错误及其对策

发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。

常见的内存错误及其对策如下:

  • 内存分配未成功,却使用了它。

编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

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

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

内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

  • 内存分配成功并且已经初始化,但操作越过了内存的边界。

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

  • 忘记了释放内存,造成内存泄露。

含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

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

  • 释放了内存却继续使用它。

有三种情况:

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

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

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

【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

=============================================================================

内存泄漏

=============================================================================

内存泄漏如何调试查找,主要内容如下:

  • 1、内存泄漏简介
  • 2、Windows平台下的内存泄漏检测
    • 2.1、检测是否存在内存泄漏问题
    • 2.2、定位具体的内存泄漏地方
  • 3、Linux平台下的内存泄漏检测 
  • 4、总结

其实Windows、Linux下面的内存检测都可以单独开篇详细介绍,方法和工具也远远不止文中介绍到的,我的方法也不是最优的,如果您有更好的方法,也请您告诉我和大家。

1、内存泄漏简介及后果

wikipedia中这样定义内存泄漏:在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

最难捉摸也最难检测到的错误之一是内存泄漏,即未能正确释放以前分配的内存的 bug。 只发生一次的小的内存泄漏可能不会被注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种征兆:从性能不良(并且逐渐降低)到内存完全用尽。 更糟的是,泄漏的程序可能会用掉太多内存,以致另一个程序失败,而使用户无从查找问题的真正根源。 此外,即使无害的内存泄漏也可能是其他问题的征兆。

内存泄漏会因为减少可用内存的数量从而降低计算机的性能。最终,在最糟糕的情况下,过多的可用内存被分配掉导致全部或部分设备停止正常工作,或者应用程序崩溃。内存泄漏可能不严重,甚至能够被常规的手段检测出来。在现代操作系统中,一个应用程序使用的常规内存在程序终止时被释放。这表示一个短暂运行的应用程序中的内存泄漏不会导致严重后果。

在以下情況,内存泄漏导致较严重的后果:

  • 程序运行后置之不理,并且随着时间的流失消耗越来越多的内存(比如服务器上的后台任务,尤其是嵌入式系统中的后台任务,这些任务可能被运行后很多年内都置之不理);
  • 新的内存被频繁地分配,比如当显示电脑游戏或动画视频画面时;
  • 程序能够请求未被释放的内存(比如共享内存),甚至是在程序终止的时候;
  • 泄漏在操作系统内部发生;
  • 泄漏在系统关键驱动中发生;
  • 内存非常有限,比如在嵌入式系统或便携设备中;
  • 当运行于一个终止时内存并不自动释放的操作系统(比如AmigaOS)之上,而且一旦丢失只能通过重启来恢复。

下面我们通过以下例子来介绍如何检测内存泄漏问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>
#include <iostream>
using namespace std;
 
void GetMemory(char *p,int num)
{
    p = (char*)malloc(sizeof(char) * num);//使用new也能够检测出来
}
 
int main(int argc,char** argv)
{
    char *str = NULL;
    GetMemory(str, 100);
    cout<<"Memory leak test!"<<endl;
    //如果main中存在while循环调用GetMemory
    //那么问题将变得很严重
    //while(1){GetMemory(...);}
    return 0;
}

实际中不可能这么简单,如果这么简单也用不着别的方法,程序员一眼就可以看出问题,此程序只用于测试。

2、Windows平台下的内存泄漏检测

2.1、检测是否存在内存泄漏问题

Windows平台下面Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法,原理大致如下:内存分配要通过CRT在运行时实现,只要在分配内存和释放内存时分别做好记录,程序结束时对比分配内存和释放内存的记录就可以确定是不是有内存泄漏。在vs中启用内存检测的方法如下:

  • STEP1,在程序中包括以下语句: (#include 语句必须采用上文所示顺序。 如果更改了顺序,所使用的函数可能无法正常工作。)
1
2
3
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

通过包括 crtdbg.h,将 malloc  free 函数映射到它们的调试版本,即 _malloc_dbg  _free_dbg,这两个函数将跟踪内存分配和释放。 此映射只在调试版本(在其中定义了_DEBUG)中发生。 发布版本使用普通的 malloc  free 函数。

#define 语句将 CRT 堆函数的基版本映射到对应的“Debug”版本。 并非绝对需要该语句;但如果没有该语句,内存泄漏转储包含的有用信息将较少。

  • STEP2, 在添加了上述语句之后,可以通过在程序中包括以下语句(通常应恰好放在程序退出位置之前)来转储内存泄漏信息:
1
_CrtDumpMemoryLeaks();

此时,完整的代码如下:

#define _CRTDBG_MAP_ALLOC#include <stdlib.h>#include <crtdbg.h> #include <iostream>using namespace std; void GetMemory(char *p, int num){    p = (char*)malloc(sizeof(char) * num);} int main(int argc,char** argv){    char *str = NULL;    GetMemory(str, 100);    cout<<"Memory leak test!"<<endl;    _CrtDumpMemoryLeaks();    return 0;} 

当在调试器下运行程序时,_CrtDumpMemoryLeaks 将在“输出”窗口中显示内存泄漏信息。 内存泄漏信息如下所示:

image

如果没有使用 #define _CRTDBG_MAP_ALLOC 语句,内存泄漏转储将如下所示:

image

未定义 _CRTDBG_MAP_ALLOC 时,所显示的会是:

  • 内存分配编号(在大括号内)。

  • 块类型(普通、客户端或 CRT)。

  • “普通块”是由程序分配的普通内存。

  • “客户端块”是由 MFC 程序用于需要析构函数的对象的特殊类型内存块。 MFC new 操作根据正在创建的对象的需要创建普通块或客户端块。

  • “CRT 块”是由 CRT 库为自己使用而分配的内存块。 CRT 库处理这些块的释放,因此您不大可能在内存泄漏报告中看到这些块,除非出现严重错误(例如 CRT 库损坏)。

从不会在内存泄漏信息中看到下面两种块类型:

  • “可用块”是已释放的内存块。

  • “忽略块”是您已特别标记的块,因而不出现在内存泄漏报告中。

  • 十六进制形式的内存位置。

  • 以字节为单位的块大小。

  • 前 16 字节的内容(亦为十六进制)。

定义了 _CRTDBG_MAP_ALLOC 时,还会显示在其中分配泄漏的内存的文件。 文件名后括号中的数字(本示例中为 10)是该文件中的行号。

注意:如果程序总是在同一位置退出,调用 _CrtDumpMemoryLeaks 将非常容易。 如果程序从多个位置退出,则无需在每个可能退出的位置放置对 _CrtDumpMemoryLeaks 的调用,而可以在程序开始处包含以下调用:

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

该语句在程序退出时自动调用 _CrtDumpMemoryLeaks。 必须同时设置 _CRTDBG_ALLOC_MEM_DF _CRTDBG_LEAK_CHECK_DF 两个位域,如前面所示。

2.2、定位具体的内存泄漏地方

通过上面的方法,我们几乎可以定位到是哪个地方调用内存分配函数malloc和new等,如上例中的GetMemory函数中,即第10行!但是不能定位到,在哪个地方调用GetMemory()导致的内存泄漏,而且在大型项目中可能有很多处调用GetMemory。如何要定位到在哪个地方调用GetMemory导致的内存泄漏?

定位内存泄漏的另一种技术涉及在关键点对应用程序的内存状态拍快照。 CRT 库提供一种结构类型 _CrtMemState,您可用它存储内存状态的快照:

1
_CrtMemState s1, s2, s3;

若要在给定点对内存状态拍快照,请向 _CrtMemCheckpoint 函数传递 _CrtMemState 结构。 该函数用当前内存状态的快照填充此结构:

1
_CrtMemCheckpoint( &s1 );

通过向 _CrtMemDumpStatistics 函数传递 _CrtMemState 结构,可以在任意点转储该结构的内容:

1
_CrtMemDumpStatistics( &s1 );

若要确定代码中某一部分是否发生了内存泄漏,可以在该部分之前和之后对内存状态拍快照,然后使用 _CrtMemDifference 比较这两个状态:

1
2
3
4
5
6
_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );
 
if ( _CrtMemDifference( &s3, &s1, &s2) )
   _CrtMemDumpStatistics( &s3 );

顾名思义,_CrtMemDifference 比较两个内存状态(s1 和 s2),生成这两个状态之间差异的结果(s3)。 在程序的开始和结尾放置 _CrtMemCheckpoint 调用,并使用_CrtMemDifference 比较结果,是检查内存泄漏的另一种方法。 如果检测到泄漏,则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来划分程序和定位泄漏。

如上面的例子程序我们可以这样来定位确切的调用GetMemory的地方:

#define _CRTDBG_MAP_ALLOC#include <stdlib.h>#include <crtdbg.h> #include <iostream>using namespace std; _CrtMemState s1, s2, s3; void GetMemory(char *p, int num){    p = (char*)malloc(sizeof(char) * num);} int main(int argc,char** argv){    _CrtMemCheckpoint( &s1 );    char *str = NULL;    GetMemory(str, 100);    _CrtMemCheckpoint( &s2 );    if ( _CrtMemDifference( &s3, &s1, &s2) )        _CrtMemDumpStatistics( &s3 );    cout<<"Memory leak test!"<<endl;    _CrtDumpMemoryLeaks();    return 0;}

调试时,程序输出如下结果:

image

这说明在s1和s2之间存在内存泄漏!!!如果GetMemory不是在s1和s2之间调用,那么就不会有信息输出。

3、Linux平台下的内存泄漏检测

在上面我们介绍了,vs中在代码中“包含crtdbg.h,将 malloc  free 函数映射到它们的调试版本,即 _malloc_dbg  _free_dbg,这两个函数将跟踪内存分配和释放。 此映射只在调试版本(在其中定义了_DEBUG)中发生。 发布版本使用普通的 malloc  free 函数。”即为malloc和free做了钩子,用于记录内存分配信息。

Linux下面也有原理相同的方法——mtrace,http://en.wikipedia.org/wiki/Mtrace。方法类似,我这就不具体描述,参加给出的链接。这节我主要介绍一个非常强大的工具valgrind。如下图所示:

image

如上图所示知道:

==6118== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1 
==6118==    at 0x4024F20: malloc (vg_replace_malloc.c:236) 
==6118==    by 0x8048724: GetMemory(char*, int) (in /home/netsky/workspace/a.out) 
==6118==    by 0x804874E: main (in /home/netsky/workspace/a.out)

是在main中调用了GetMemory导致的内存泄漏,GetMemory中是调用了malloc导致泄漏了100字节的内存。

Things to notice: 
• There is a lot of information in each error message; read it carefully. 
• The 6118 is the process ID; it’s usually unimportant. 
• The first line ("Heap Summary") tells you what kind of error it is. 
• Below the first line is a stack trace telling you where the problem occurred. Stack traces can get quite large, and be 
confusing, especially if you are using the C++ STL. Reading them from the bottom up can help.

• The code addresses (eg. 0x4024F20) are usually unimportant, but occasionally crucial for tracking down weirder 
bugs.

The stack trace tells you where the leaked memory was allocated. Memcheck cannot tell you why the memory leaked, 
unfortunately. (Ignore the "vg_replace_malloc.c", that’s an implementation detail.) 
There are several kinds of leaks; the two most important categories are: 
• "definitely lost": your program is leaking memory -- fix it! 
• "probably lost": your program is leaking memory, unless you’re doing funny things with pointers (such as moving 
them to point to the middle of a heap block)

Valgrind的使用请见手册http://valgrind.org/docs/manual/manual.html。

4、总结

其实内存泄漏的原因可以概括为:调用了malloc/new等内存申请的操作,但缺少了对应的free/delete,总之就是,malloc/new比free/delete的数量多。我们在编程时需要注意这点,保证每个malloc都有对应的free,每个new都有对应的deleted!!!平时要养成这样一个好的习惯。

要避免内存泄漏可以总结为以下几点:

  • 程序员要养成良好习惯,保证malloc/new和free/delete匹配;
  • 检测内存泄漏的关键原理就是,检查malloc/new和free/delete是否匹配,一些工具也就是这个原理。要做到这点,就是利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况。

==========================================================================

内存布局

==========================================================================

为什么需要知道C/C++的内存布局和在哪可以可以找到想要的数据?知道内存布局对调试程序非常有帮助,可以知道程序执行时,到底做了什么,有助于写出干净的代码。本文的主要内容如下:

  • 源文件转换为可执行文件
  • 可执行程序组成及内存布局
  • 数据存储类别
  • 一个实例
  • 总结

源文件转换为可执行文件

源文件经过以下几步生成可执行文件:

  • 1、预处理(preprocessor):对#include、#define、#ifdef/#endif、#ifndef/#endif等进行处理
  • 2、编译(compiler):将源码编译为汇编代码
  • 3、汇编(assembler):将汇编代码汇编为目标代码
  • 4、链接(linker):将目标代码链接为可执行文件

编译器和汇编器创建的目标文件包含:二进制代码(指令)、源码中的数据;链接器将多个目标文件链接成一个;装载器吧目标文件加载到内存。

image

图1 源文件到可执行文件的步骤

 

可执行程序组成及内存布局

通过上面的小节,我们知道将源程序转换为可执行程序的步骤,典型的可执行文件分为两部分:

  • 代码段(Code),由机器指令组成,该部分是不可改的,编译之后就不再改变,放置在文本段(.text)。
  • 数据段(Data),它由以下几部分组:
      • 常量(constant),通常放置在只读read-only的文本段(.text
      • 静态数据(static data),初始化的放置在数据段(.data);未初始化的放置在(.bss,Block Started by Symbol,BSS段的变量只有名称和大小却没有值)
      • 动态数据(dynamic data),这些数据存储在堆(heap)或栈(stack

源程序编译后链接到一个以0地址为始地址的线性或多维虚拟地址空间。而且每个进程都拥有这样一个空间,每个指令和数据都在这个虚拟地址空间拥有确定的地址,把这个地址称为虚拟地址(Virtual Address)。将进程中的目标代码、数据等的虚拟地址组成的虚拟空间称为虚拟存储器(Virtual Memory)。典型的虚拟存储器中有类似的布局:

  • Text Segment (.text)
  • Initialized Data Segment (.data)
  • Uninitialized Data Segment (.bss)
  • The Stack
  • The Heap

如下图所示:

image

图2 进程内存布局

当进程被创建时,内核为其提供一块物理内存,将虚拟内存映射到物理内存,这些都是由操作系统来做的。

数据存储类别

讨论C/C++中的内存布局,不得不提的是数据的存储类别!数据在内存中的位置取决于它的存储类别。一个对象是内存的一个位置,解析这个对象依赖于两个属性:存储类别、数据类型。

  • 存储类别决定对象在内存中的生命周期。
  • 数据类型决定对象值的意义,在内存中占多大空间。

C/C++中由(auto、 extern、 register、 static)存储类别和对象声明的上下文决定它的存储类别。

1、自动对象(automatic objects)

autoregister将声明的对象指定为自动存储类别。他们的作用域是局部的,诸如一个函数内,一个代码块{***}内等。操作了作用域,对象会被销毁。

  • 在一个代码块中声明一个对象,如果没有执行auto,那么默认是自动存储类别。
  • 声明为register的对象是自动存储类别,存储在计算机的快速寄存器中。不可以对register对象做取值操作“&”。

2、静态对象(static objects)

静态对象可以局部的,也可以是全局的。静态对象一直保持它的值,例如进入一个函数,函数中的静态对象仍保持上次调用时的值。包含静态对象的函数不是线程安全的、不可重入的,正是因为它具有“记忆”功能。

  • 局部对象声明为静态之后,将改变它在内存中保存的位置,由动态数据--->静态数据,即从堆或栈变为数据段或bbs段。
  • 全局对象声明为静态之后,而不会改变它在内存中保存的位置,仍然是在数据段或bbs段。但是static将改变它的作用域,即该对象仅在本源文件有效。此相反的关键字是extern,使用extern修饰或者什么都不带的全局对象的作用域是整个程序。

 

一个实例

下面我们分析一段代码:

#include <stdio.h>#include <stdlib.h> int a;static int b;void func( void ){    char c;    static int d;}int main( void ){    int e;    int *pi = ( int *) malloc ( sizeof ( int ));    func ();    func ();    free (pi );    return (0);}

程序中声明的变量a、b、c、d、e、pi的存储类别和生命期如下所述:

  • a是一个未初始化的全局变量,作用域为整个程序,生命期是整个程序运行期间,在内存的bbs段
  • b是一个未初始化的静态全局变量,作用域为本源文件,生命期是整个程序运行期间,在内存的bbs段
  • c是一个未初始化的局部变量,作用域为函数func体内,即仅在函数体内可见,生命期也是函数体内,在内存的栈中
  • d是一个未初始化的静态局部变量,作用域为函数func体内,即仅在函数体内可见,生命期是整个程序运行期间,在内存的bbs段
  • e是一个未初始化的局部变量,作用域为函数main体内,即仅在函数体内可见,生命期是main函数内,在内存的栈中
  • pi是一个局部指针,指向堆中的一块内存块,该块的大小为sizeof(int),pi本身存储在内存的栈中,生命期是main函数内
  • 新申请的内存块在堆中,生命期是malloc/free之间

用图表示如下:

image

图3 例子的内存布局

 

总结

本文介绍了C/C++中由源程序到可执行文件的步骤,和可执行程序的内存布局,数据存储类别,最后还通过一个例子来说明。可执行程序中的变量在内存中的布局可以总结为如下:

  • 变量(函数外):如果未初始化,则存放在BSS段;否则存放在data段
  • 变量(函数内):如果没有指定static修饰符,则存放在栈中;否则同上
  • 常量:存放在文本段.text
  • 函数参数:存放在栈或寄存器中

内存可以分为以下几段:

  • 文本段:包含实际要执行的代码(机器指令)和常量。它通常是共享的,多个实例之间共享文本段。文本段是不可修改的。
  • 初始化数据段:包含程序已经初始化的全局变量,.data。
  • 未初始化数据段:包含程序未初始化的全局变量,.bbs。该段中的变量在执行之前初始化为0或NULL。
  • 栈:由系统管理,由高地址向低地址扩展。
  • 堆:动态内存,由用户管理。通过malloc/alloc/realloc、new/new[]申请空间,通过free、delete/delete[]释放所申请的空间。由低地址想高地址扩展。





0 0