内存的分配及使用

 
内存是程序运行的环境,对内存的合理使用充斥着开发的全过程。

内存的分配

32位操作系统支持4GB内存的连续访问。但通常把内存分为两个2GB的空间,每个进程在运行时最大可以使用2GB的私有内存(0x00000000—0x7FFFFFFF)。
理论上支持的最大数组如下:
char szBuffer[2*1024*1024*1024];
当然,由于在实际运行时,程序还有代码段、临时变量段、动态内存申请等,实际是不可能用到上述那么大的数组的。
 
高端的2GB内存地址(0x80000000—0xFFFFFFFF),操作系统一般内部保留使用,即供操作系统内核代码使用。
 
在Windows和Linux平台上,一些动态链接库(Windows的dll,Linux的so)以及ocx控件等,由于是跨进程服务的,因此一般也在高2GB内存空间运行。(附议)
 
虽然每个进程都能看到自己的2GB内存以及系统的2GB内存,但不同进程之间是无法彼此看到对方的。此外,操作系统在底层也做了很多工作,比如磁盘上的虚拟内存交换,不同内存块之间的动态映射等。

虚拟内存

虚拟内存的基本思想是:用廉价但缓慢的磁盘来扩充快速却昂贵的内存。
 
在某一时刻,程序实际需要使用的虚拟内存区段的内容就被载入物理内存里。当物理内存中的数据有一段时间未被使用,它们就可能被操作系统转移到硬盘中,节省下来的物理内存空间用于载入其他需要使用的数据。
 
在进程执行过程中,操作系统负责具体细节,使每个进程都以为自己拥有整个地址空间的独家访问权。这个幻觉是通过“虚拟内存”实现的。所有进程共享机器的物理内存,当内存使用完时就用磁盘保存数据。在进程运行时,数据在磁盘和内存之间来回移动。内存管理硬件(MMU)负责把虚拟地址翻译为物理地址,并让一个进程始终运行于系统的真正内存中,应用程序员只看到虚拟地址,并不知道自己的进程在磁盘与内存之间来回切换。
 
从潜在的可能性上说,与进程有关的所有内存都将被系统所使用,如果该进程可能不会马上运行(可能它的优先级低,也可能是它处于睡眠状态),操作系统可以暂时取回所有分配给它的物理内存资源,将该进程的所有相关信息都备份到磁盘上。
 
进程只能操作位于物理内存中的页面。当进程引用一个不在物理内存中的页面时,MMU就会产生一个页错误。内存对此事做出响应,并判断该引用是否有效。如果无效,内核向进程发出一个“segmentation violation(段违规)”的信号,内核从磁盘取回该页,换入内存中,一旦页面进入内存,进程便被解锁,可以重新运行——进程本身并不知道它曾经因为页面换入事件等待了一会。

内存的使用

对于程序员,我们最重要的是能理解不同进程间私有内存空间的含义。
 
C和C++的编译器把私有内存分为3块:基栈、浮动栈和堆。如下图:
 
  1. 基栈:也叫静态存储区,这是编译器在编译期间就已经固定下来必须要使用的内存,如程序的代码段、静态变量、全局变量、const常量等。
  2. 浮动栈:很多书上称为“栈”,就是程序开始运行,随着函数、对象的一段执行,函数内部变量、对象的内部成员变量开始动态占用内存,浮动栈一般都有生命周期,函数结束或者对象析构,其对应的浮动栈空间的就拆除了,这部分内容总是变来变去,内存占用也不是固定,因此叫浮动栈。
  3. :C和C++语言都支持动态内存申请,即程序运行期可以自由申请内存,这部分内存就是在堆空间申请的。堆位于2GB的最顶端,自上向下分配,这是避免和浮动栈混到一起,不好管理。我们用到malloc和new都是从堆空间申请的内存,new比malloc多了对象的支持,可以自动调用构造函数。另外,new创建对象,其成员变量位于堆里面。
我们来看一个例子:
const int n = 100

void Func(void)

{
    char ch = 0;
    char* pBuff = (char*)malloc(10);
    //…

}
这个例子中,如果函数Fun运行,其中n由于是全局静态变量,位于基栈,ch和pBuff这两个函数内部变量位于浮动栈,而pBuff指向的内存由malloc分配,位于堆栈。
 
在内存理解上,最著名的例子就是线程启动时的参数传递。
 
函数启动一个线程,很多时候需要向线程传参数,但是线程是异步启动的,即很可能启动函数已经退出了,而线程函数都还没有正式开始运行,因此,绝不能用启动函数的内部变量给线程传参。道理很简单,函数的内部变量在浮动栈,但函数退出时,浮动栈自动拆除,内存空间已经被释放了。当线程启动时,按照给的参数指针去查询变量,实际上是在读一块无效的内存区域,程序会因此而崩溃。
 
那怎么办呢?我们应该直接用malloc函数给需要传递的参数分配一块内存区域,将指针传入线程,线程收到后使用,最后线程退出时,free释放。
 
我们来看例子:
//这个结构体就是参数表 

typedef struct _CListen_ListenAcceptTask_Param_ 

    Linux_Win_SOCKET m_nSocket; 
    //其他参量… …
}SCListenAcceptTaskParam; 

//习惯性写法,设置结构体后,立即声明结构体的尺寸,为后续malloc提供方便
const ULONG SCListenAcceptTaskParamSize=sizeof(SCListenAcceptTaskParam); 

//这里接收到连接请求,申请参数区域,将关键信息带入参数区域,帮助后续线程工作。
bool CListen::ListenTaskCallback(void* pCallParam,int& nStatus) 


    //正常的函数逻辑… …
    //假定s是accept到的socket,需要传入后续线程工作
    //在此准备一块参数区域,从远堆上申请
    SCListenAcceptTaskParam* pParam=(SCListenAcceptTaskParam*) malloc(SCListenAcceptTaskParamSize); 

    //给参数区域赋值
    pParam->m_nSocket=s; 
    //此处启动线程,将pParam传递给线程… …
    //正常的函数逻辑… …



//这是线程函数,负责处理上文accept到的socket 
bool CListen::ListenAcceptTask(void* pCallParam,int& nStatus) 


    //第一句话就是强制指针类型转换,获得外界传入的参数区域
    SCListenAcceptTaskParam* pParam= (SCListenAcceptTaskParam*)pCallParam; 

    //正常的函数逻辑… …
    //退出前,必须要做的工作,确保资源不被泄露
    close(pParam->m_nSocket); //关闭socket 
    free(pCallParam); // free传入的参数区域
    //… … 

}

内存bug

无规则的滥用内存和指针会导致大量的bug,程序员应该对内存的使用保持高度的敏感性和警惕性,谨慎地使用内存资源。
 
使用内存时最容易出现的bug是:
 
  • 坏指针值错误:
    1. 在指针赋值之前就用它来引用内存;
    2. 向库函数传送一个坏指针;
    3. 对指针进行释放之后再访问它的内容;
对于第三种问题,可以修改free语句,在指针释放之后再将它置为空值。
free(p); 
p = NULL;
这样,如果在指针释放之后继续使用该指针,至少程序能在终止之前进行信息转储。
  • 改写(overwrite)错误:
    1. 越过数组边界写入数据,
    2. 在动态分配的内存两端之外写入数据,
    3. 改写一些堆管理数据结构(在动态分配内存之前的区域写入数据就很容易发生这种情况)
p = malloc(256);
p[-1] = 0;
p[256] = 0;
  • 指针释放引起的错误:
    1. 释放同一个内存块两次;
    2. 释放一块未曾使用malloc分配的内存;
    3. 释放仍在使用中的内存;
    4. 释放一个无效的指针;
一个极为常见的与释放内存有关的错误就是在for(p=start;p=p->next)这样的循环中迭代一个链表,并在循环体内使用free(p)语句。这样,在下一次循环迭代时,程序就会对已经释放的指针进行解除引用操作,从而导致不可预料的结果。
我们可以这样迭代:
struct node *p, *tart, *temp;

for(p = start; p ; p = temp)
{
    temp = p->next;
    free(p);
}




posted @ 2011-12-20 10:34  __BSD__  阅读(530)  评论(0编辑  收藏  举报